Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: shutter in integration tests

authored by

Patrick Dewey and committed by tangled.org 3a8cc0d0 d110d8f2

+1685
+17
tests/integration/__snapshots__/all_beans.snap
··· 1 + --- 2 + title: all beans 3 + test_name: TestSnap_PDS_FullRepo 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + [ 8 + { 9 + "$type": "social.arabica.alpha.bean", 10 + "closed": false, 11 + "createdAt": "<TIMESTAMP>", 12 + "name": "Monarch", 13 + "origin": "Blend", 14 + "roastLevel": "Medium", 15 + "roasterRef": "at://<DID>/social.arabica.alpha.roaster/<RKEY:roaster>" 16 + } 17 + ]
+14
tests/integration/__snapshots__/all_brewers.snap
··· 1 + --- 2 + title: all brewers 3 + test_name: TestSnap_PDS_FullRepo 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + [ 8 + { 9 + "$type": "social.arabica.alpha.brewer", 10 + "brewerType": "Pour Over", 11 + "createdAt": "<TIMESTAMP>", 12 + "name": "V60" 13 + } 14 + ]
+20
tests/integration/__snapshots__/all_brews.snap
··· 1 + --- 2 + title: all brews 3 + test_name: TestSnap_PDS_FullRepo 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + [ 8 + { 9 + "$type": "social.arabica.alpha.brew", 10 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 11 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 12 + "coffeeAmount": 15, 13 + "createdAt": "<TIMESTAMP>", 14 + "grinderRef": "at://<DID>/social.arabica.alpha.grinder/<RKEY:grinder>", 15 + "method": "Pour Over", 16 + "rating": 7, 17 + "timeSeconds": 180, 18 + "waterAmount": 250 19 + } 20 + ]
+15
tests/integration/__snapshots__/all_grinders.snap
··· 1 + --- 2 + title: all grinders 3 + test_name: TestSnap_PDS_FullRepo 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + [ 8 + { 9 + "$type": "social.arabica.alpha.grinder", 10 + "burrType": "Flat", 11 + "createdAt": "<TIMESTAMP>", 12 + "grinderType": "Electric", 13 + "name": "DF64" 14 + } 15 + ]
+21
tests/integration/__snapshots__/all_roasters.snap
··· 1 + --- 2 + title: all roasters 3 + test_name: TestSnap_PDS_FullRepo 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + [ 8 + { 9 + "$type": "social.arabica.alpha.roaster", 10 + "createdAt": "<TIMESTAMP>", 11 + "location": "Brooklyn, NY", 12 + "name": "Sey", 13 + "website": "https://seycoffee.com" 14 + }, 15 + { 16 + "$type": "social.arabica.alpha.roaster", 17 + "createdAt": "<TIMESTAMP>", 18 + "location": "Rogers, AR", 19 + "name": "Onyx" 20 + } 21 + ]
+18
tests/integration/__snapshots__/bean_all_fields.snap
··· 1 + --- 2 + title: bean all fields 3 + test_name: TestSnap_PDS_BeanPermutations/all_fields 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.bean", 9 + "closed": false, 10 + "createdAt": "<TIMESTAMP>", 11 + "description": "Balanced and clean", 12 + "name": "Full Bean", 13 + "origin": "Colombia", 14 + "process": "Washed", 15 + "roastLevel": "Medium", 16 + "roasterRef": "at://<DID>/social.arabica.alpha.roaster/<RKEY:roaster>", 17 + "variety": "Caturra" 18 + }
+12
tests/integration/__snapshots__/bean_name_only.snap
··· 1 + --- 2 + title: bean name only 3 + test_name: TestSnap_PDS_BeanPermutations/name_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.bean", 9 + "closed": false, 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Bare Bean" 12 + }
+13
tests/integration/__snapshots__/bean_with_description.snap
··· 1 + --- 2 + title: bean with description 3 + test_name: TestSnap_PDS_BeanPermutations/with_description 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.bean", 9 + "closed": false, 10 + "createdAt": "<TIMESTAMP>", 11 + "description": "Juicy and complex", 12 + "name": "Described Bean" 13 + }
+13
tests/integration/__snapshots__/bean_with_origin.snap
··· 1 + --- 2 + title: bean with origin 3 + test_name: TestSnap_PDS_BeanPermutations/with_origin 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.bean", 9 + "closed": false, 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Origin Bean", 12 + "origin": "Ethiopia" 13 + }
+13
tests/integration/__snapshots__/bean_with_roast_level.snap
··· 1 + --- 2 + title: bean with roast level 3 + test_name: TestSnap_PDS_BeanPermutations/with_roast_level 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.bean", 9 + "closed": false, 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Roasted Bean", 12 + "roastLevel": "Dark" 13 + }
+17
tests/integration/__snapshots__/bean_with_roaster_ref.snap
··· 1 + --- 2 + title: bean with roaster ref 3 + test_name: TestSnap_PDS_BeanWithRoaster 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.bean", 9 + "closed": false, 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Geometry", 12 + "origin": "Colombia", 13 + "process": "Washed", 14 + "roastLevel": "Medium", 15 + "roasterRef": "at://<DID>/social.arabica.alpha.roaster/<RKEY:roaster>", 16 + "variety": "Caturra" 17 + }
+13
tests/integration/__snapshots__/bean_with_roaster_ref_only.snap
··· 1 + --- 2 + title: bean with roaster ref only 3 + test_name: TestSnap_PDS_BeanPermutations/with_roaster_ref_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.bean", 9 + "closed": false, 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Sourced Bean", 12 + "roasterRef": "at://<DID>/social.arabica.alpha.roaster/<RKEY:roaster>" 13 + }
+14
tests/integration/__snapshots__/bean_with_variety_and_process.snap
··· 1 + --- 2 + title: bean with variety and process 3 + test_name: TestSnap_PDS_BeanPermutations/with_variety_and_process 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.bean", 9 + "closed": false, 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Processed Bean", 12 + "process": "Natural", 13 + "variety": "Gesha" 14 + }
+24
tests/integration/__snapshots__/brew_after_pourover_to_espresso.snap
··· 1 + --- 2 + title: brew after pourover to espresso 3 + test_name: TestSnap_PDS_BrewMethodSwap 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:espBrewer>", 11 + "coffeeAmount": 18, 12 + "createdAt": "<TIMESTAMP>", 13 + "espressoParams": { 14 + "preInfusionSeconds": 5, 15 + "pressure": 90, 16 + "yieldWeight": 360 17 + }, 18 + "grinderRef": "at://<DID>/social.arabica.alpha.grinder/<RKEY:grinder>", 19 + "method": "Espresso", 20 + "rating": 9, 21 + "tastingNotes": "syrupy, chocolate", 22 + "timeSeconds": 28, 23 + "waterAmount": 36 24 + }
+13
tests/integration/__snapshots__/brew_bean_with_amounts.snap
··· 1 + --- 2 + title: brew bean with amounts 3 + test_name: TestSnap_PDS_BrewPermutations/bean_with_amounts 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "coffeeAmount": 13, 11 + "createdAt": "<TIMESTAMP>", 12 + "waterAmount": 200 13 + }
+12
tests/integration/__snapshots__/brew_bean_with_method.snap
··· 1 + --- 2 + title: brew bean with method 3 + test_name: TestSnap_PDS_BrewPermutations/bean_with_method 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "createdAt": "<TIMESTAMP>", 11 + "method": "French Press" 12 + }
+17
tests/integration/__snapshots__/brew_espresso_partial_params.snap
··· 1 + --- 2 + title: brew espresso partial params 3 + test_name: TestSnap_PDS_BrewPermutations/espresso_partial_params 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:espBrewer>", 11 + "coffeeAmount": 18, 12 + "createdAt": "<TIMESTAMP>", 13 + "espressoParams": { 14 + "yieldWeight": 400 15 + }, 16 + "method": "Espresso" 17 + }
+16
tests/integration/__snapshots__/brew_espresso_pressure_only.snap
··· 1 + --- 2 + title: brew espresso pressure only 3 + test_name: TestSnap_PDS_BrewPermutations/espresso_pressure_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:espBrewer>", 11 + "createdAt": "<TIMESTAMP>", 12 + "espressoParams": { 13 + "pressure": 60 14 + }, 15 + "method": "Espresso" 16 + }
+11
tests/integration/__snapshots__/brew_minimal_bean_only.snap
··· 1 + --- 2 + title: brew minimal bean only 3 + test_name: TestSnap_PDS_BrewPermutations/minimal_bean_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "createdAt": "<TIMESTAMP>" 11 + }
+17
tests/integration/__snapshots__/brew_pourover_partial_params.snap
··· 1 + --- 2 + title: brew pourover partial params 3 + test_name: TestSnap_PDS_BrewPermutations/pourover_partial_params 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 11 + "createdAt": "<TIMESTAMP>", 12 + "method": "Pour Over", 13 + "pouroverParams": { 14 + "bloomSeconds": 45, 15 + "bloomWater": 50 16 + } 17 + }
+16
tests/integration/__snapshots__/brew_pourover_with_filter_only.snap
··· 1 + --- 2 + title: brew pourover with filter only 3 + test_name: TestSnap_PDS_BrewPermutations/pourover_with_filter_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 11 + "createdAt": "<TIMESTAMP>", 12 + "method": "Pour Over", 13 + "pouroverParams": { 14 + "filter": "Sibarist FAST" 15 + } 16 + }
+22
tests/integration/__snapshots__/brew_pours_without_params.snap
··· 1 + --- 2 + title: brew pours without params 3 + test_name: TestSnap_PDS_BrewPermutations/pours_without_params 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "createdAt": "<TIMESTAMP>", 11 + "method": "Pour Over", 12 + "pours": [ 13 + { 14 + "timeSeconds": 0, 15 + "waterAmount": 60 16 + }, 17 + { 18 + "timeSeconds": 30, 19 + "waterAmount": 240 20 + } 21 + ] 22 + }
+18
tests/integration/__snapshots__/brew_single_pour.snap
··· 1 + --- 2 + title: brew single pour 3 + test_name: TestSnap_PDS_BrewPermutations/single_pour 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "createdAt": "<TIMESTAMP>", 11 + "method": "Pour Over", 12 + "pours": [ 13 + { 14 + "timeSeconds": 0, 15 + "waterAmount": 300 16 + } 17 + ] 18 + }
+13
tests/integration/__snapshots__/brew_with_brewer_no_params.snap
··· 1 + --- 2 + title: brew with brewer no params 3 + test_name: TestSnap_PDS_BrewPermutations/with_brewer_no_params 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 11 + "createdAt": "<TIMESTAMP>", 12 + "method": "Pour Over" 13 + }
+13
tests/integration/__snapshots__/brew_with_grinder_and_grind_size.snap
··· 1 + --- 2 + title: brew with grinder and grind size 3 + test_name: TestSnap_PDS_BrewPermutations/with_grinder_and_grind_size 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "createdAt": "<TIMESTAMP>", 11 + "grindSize": "Fine", 12 + "grinderRef": "at://<DID>/social.arabica.alpha.grinder/<RKEY:grinder>" 13 + }
+16
tests/integration/__snapshots__/brew_with_recipe_ref.snap
··· 1 + --- 2 + title: brew with recipe ref 3 + test_name: TestSnap_PDS_BrewPermutations/with_recipe_ref 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 11 + "coffeeAmount": 18, 12 + "createdAt": "<TIMESTAMP>", 13 + "method": "Pour Over", 14 + "recipeRef": "at://<DID>/social.arabica.alpha.recipe/<RKEY:recipe>", 15 + "waterAmount": 300 16 + }
+13
tests/integration/__snapshots__/brew_with_tasting_notes_and_rating.snap
··· 1 + --- 2 + title: brew with tasting notes and rating 3 + test_name: TestSnap_PDS_BrewPermutations/with_tasting_notes_and_rating 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "createdAt": "<TIMESTAMP>", 11 + "rating": 9, 12 + "tastingNotes": "cherry, jasmine, silky" 13 + }
+13
tests/integration/__snapshots__/brew_with_temperature_and_time.snap
··· 1 + --- 2 + title: brew with temperature and time 3 + test_name: TestSnap_PDS_BrewPermutations/with_temperature_and_time 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "createdAt": "<TIMESTAMP>", 11 + "temperature": 960, 12 + "timeSeconds": 240 13 + }
+13
tests/integration/__snapshots__/brewer_all_fields.snap
··· 1 + --- 2 + title: brewer all fields 3 + test_name: TestSnap_PDS_BrewerPermutations/all_fields 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brewer", 9 + "brewerType": "Pour Over", 10 + "createdAt": "<TIMESTAMP>", 11 + "description": "Ceramic, size 02", 12 + "name": "Full Brewer" 13 + }
+11
tests/integration/__snapshots__/brewer_name_only.snap
··· 1 + --- 2 + title: brewer name only 3 + test_name: TestSnap_PDS_BrewerPermutations/name_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brewer", 9 + "createdAt": "<TIMESTAMP>", 10 + "name": "Bare Brewer" 11 + }
+13
tests/integration/__snapshots__/brewer_record.snap
··· 1 + --- 2 + title: brewer record 3 + test_name: TestSnap_PDS_BrewerCreate 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brewer", 9 + "brewerType": "Pour Over", 10 + "createdAt": "<TIMESTAMP>", 11 + "description": "Plastic, size 02", 12 + "name": "Hario V60 02" 13 + }
+12
tests/integration/__snapshots__/brewer_with_description.snap
··· 1 + --- 2 + title: brewer with description 3 + test_name: TestSnap_PDS_BrewerPermutations/with_description 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brewer", 9 + "createdAt": "<TIMESTAMP>", 10 + "description": "12oz capacity", 11 + "name": "Described Brewer" 12 + }
+12
tests/integration/__snapshots__/brewer_with_type.snap
··· 1 + --- 2 + title: brewer with type 3 + test_name: TestSnap_PDS_BrewerPermutations/with_type 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brewer", 9 + "brewerType": "Immersion", 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Typed Brewer" 12 + }
+24
tests/integration/__snapshots__/espresso_brew_record.snap
··· 1 + --- 2 + title: espresso brew record 3 + test_name: TestSnap_PDS_EspressoBrew 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 11 + "coffeeAmount": 18, 12 + "createdAt": "<TIMESTAMP>", 13 + "espressoParams": { 14 + "preInfusionSeconds": 5, 15 + "pressure": 90, 16 + "yieldWeight": 360 17 + }, 18 + "grinderRef": "at://<DID>/social.arabica.alpha.grinder/<RKEY:grinder>", 19 + "method": "Espresso", 20 + "rating": 9, 21 + "tastingNotes": "syrupy, chocolate", 22 + "timeSeconds": 28, 23 + "waterAmount": 36 24 + }
+14
tests/integration/__snapshots__/grinder_all_fields.snap
··· 1 + --- 2 + title: grinder all fields 3 + test_name: TestSnap_PDS_GrinderPermutations/all_fields 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.grinder", 9 + "burrType": "Flat", 10 + "createdAt": "<TIMESTAMP>", 11 + "grinderType": "Electric", 12 + "name": "Full Grinder", 13 + "notes": "64mm SSP multipurpose" 14 + }
+11
tests/integration/__snapshots__/grinder_name_only.snap
··· 1 + --- 2 + title: grinder name only 3 + test_name: TestSnap_PDS_GrinderPermutations/name_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.grinder", 9 + "createdAt": "<TIMESTAMP>", 10 + "name": "Bare Grinder" 11 + }
+14
tests/integration/__snapshots__/grinder_record.snap
··· 1 + --- 2 + title: grinder record 3 + test_name: TestSnap_PDS_GrinderCreate 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.grinder", 9 + "burrType": "Conical", 10 + "createdAt": "<TIMESTAMP>", 11 + "grinderType": "Manual", 12 + "name": "Comandante C40", 13 + "notes": "red clix installed" 14 + }
+13
tests/integration/__snapshots__/grinder_type_and_burr.snap
··· 1 + --- 2 + title: grinder type and burr 3 + test_name: TestSnap_PDS_GrinderPermutations/type_and_burr 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.grinder", 9 + "burrType": "Conical", 10 + "createdAt": "<TIMESTAMP>", 11 + "grinderType": "Manual", 12 + "name": "Full Manual" 13 + }
+12
tests/integration/__snapshots__/grinder_with_burr.snap
··· 1 + --- 2 + title: grinder with burr 3 + test_name: TestSnap_PDS_GrinderPermutations/with_burr 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.grinder", 9 + "burrType": "Flat", 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Burr Grinder" 12 + }
+12
tests/integration/__snapshots__/grinder_with_notes.snap
··· 1 + --- 2 + title: grinder with notes 3 + test_name: TestSnap_PDS_GrinderPermutations/with_notes 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.grinder", 9 + "createdAt": "<TIMESTAMP>", 10 + "name": "Noted Grinder", 11 + "notes": "SSP burrs installed" 12 + }
+12
tests/integration/__snapshots__/grinder_with_type.snap
··· 1 + --- 2 + title: grinder with type 3 + test_name: TestSnap_PDS_GrinderPermutations/with_type 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.grinder", 9 + "createdAt": "<TIMESTAMP>", 10 + "grinderType": "Electric", 11 + "name": "Typed Grinder" 12 + }
+41
tests/integration/__snapshots__/pourover_brew_record.snap
··· 1 + --- 2 + title: pourover brew record 3 + test_name: TestSnap_PDS_PouroverBrew 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.brew", 9 + "beanRef": "at://<DID>/social.arabica.alpha.bean/<RKEY:bean>", 10 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 11 + "coffeeAmount": 18, 12 + "createdAt": "<TIMESTAMP>", 13 + "grindSize": "Medium", 14 + "grinderRef": "at://<DID>/social.arabica.alpha.grinder/<RKEY:grinder>", 15 + "method": "Pour Over", 16 + "pouroverParams": { 17 + "bloomSeconds": 30, 18 + "bloomWater": 60, 19 + "drawdownSeconds": 45, 20 + "filter": "Hario tabbed" 21 + }, 22 + "pours": [ 23 + { 24 + "timeSeconds": 0, 25 + "waterAmount": 60 26 + }, 27 + { 28 + "timeSeconds": 45, 29 + "waterAmount": 120 30 + }, 31 + { 32 + "timeSeconds": 90, 33 + "waterAmount": 120 34 + } 35 + ], 36 + "rating": 8, 37 + "tastingNotes": "bright, floral", 38 + "temperature": 940, 39 + "timeSeconds": 210, 40 + "waterAmount": 300 41 + }
+30
tests/integration/__snapshots__/recipe_all_fields.snap
··· 1 + --- 2 + title: recipe all fields 3 + test_name: TestSnap_PDS_RecipePermutations/all_fields 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.recipe", 9 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 10 + "brewerType": "Pour Over", 11 + "coffeeAmount": 200, 12 + "createdAt": "<TIMESTAMP>", 13 + "name": "Full Recipe", 14 + "notes": "Competition recipe", 15 + "pours": [ 16 + { 17 + "timeSeconds": 0, 18 + "waterAmount": 60 19 + }, 20 + { 21 + "timeSeconds": 45, 22 + "waterAmount": 120 23 + }, 24 + { 25 + "timeSeconds": 90, 26 + "waterAmount": 120 27 + } 28 + ], 29 + "waterAmount": 3000 30 + }
+11
tests/integration/__snapshots__/recipe_name_only.snap
··· 1 + --- 2 + title: recipe name only 3 + test_name: TestSnap_PDS_RecipePermutations/name_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.recipe", 9 + "createdAt": "<TIMESTAMP>", 10 + "name": "Bare Recipe" 11 + }
+13
tests/integration/__snapshots__/recipe_with_amounts_no_pours.snap
··· 1 + --- 2 + title: recipe with amounts no pours 3 + test_name: TestSnap_PDS_RecipePermutations/with_amounts_no_pours 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.recipe", 9 + "coffeeAmount": 150, 10 + "createdAt": "<TIMESTAMP>", 11 + "name": "Amounts Recipe", 12 + "waterAmount": 2500 13 + }
+13
tests/integration/__snapshots__/recipe_with_brewer_ref.snap
··· 1 + --- 2 + title: recipe with brewer ref 3 + test_name: TestSnap_PDS_RecipePermutations/with_brewer_ref 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.recipe", 9 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 10 + "brewerType": "Pour Over", 11 + "createdAt": "<TIMESTAMP>", 12 + "name": "Brewer Recipe" 13 + }
+12
tests/integration/__snapshots__/recipe_with_notes.snap
··· 1 + --- 2 + title: recipe with notes 3 + test_name: TestSnap_PDS_RecipePermutations/with_notes 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.recipe", 9 + "createdAt": "<TIMESTAMP>", 10 + "name": "Noted Recipe", 11 + "notes": "Hoffmann method" 12 + }
+38
tests/integration/__snapshots__/recipe_with_pours.snap
··· 1 + --- 2 + title: recipe with pours 3 + test_name: TestSnap_PDS_RecipeWithPours 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.recipe", 9 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 10 + "brewerType": "Pour Over", 11 + "coffeeAmount": 200, 12 + "createdAt": "<TIMESTAMP>", 13 + "name": "4:6 Method", 14 + "notes": "Tetsu Kasuya 4:6", 15 + "pours": [ 16 + { 17 + "timeSeconds": 0, 18 + "waterAmount": 50 19 + }, 20 + { 21 + "timeSeconds": 45, 22 + "waterAmount": 70 23 + }, 24 + { 25 + "timeSeconds": 90, 26 + "waterAmount": 60 27 + }, 28 + { 29 + "timeSeconds": 120, 30 + "waterAmount": 60 31 + }, 32 + { 33 + "timeSeconds": 150, 34 + "waterAmount": 60 35 + } 36 + ], 37 + "waterAmount": 3000 38 + }
+21
tests/integration/__snapshots__/recipe_with_single_pour.snap
··· 1 + --- 2 + title: recipe with single pour 3 + test_name: TestSnap_PDS_RecipePermutations/with_single_pour 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.recipe", 9 + "brewerRef": "at://<DID>/social.arabica.alpha.brewer/<RKEY:brewer>", 10 + "brewerType": "Pour Over", 11 + "coffeeAmount": 180, 12 + "createdAt": "<TIMESTAMP>", 13 + "name": "Single Pour Recipe", 14 + "pours": [ 15 + { 16 + "timeSeconds": 0, 17 + "waterAmount": 300 18 + } 19 + ], 20 + "waterAmount": 3000 21 + }
+13
tests/integration/__snapshots__/roaster_after_update.snap
··· 1 + --- 2 + title: roaster after update 3 + test_name: TestSnap_PDS_RoasterUpdate 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.roaster", 9 + "createdAt": "<TIMESTAMP>", 10 + "location": "Brooklyn, NY", 11 + "name": "Sey Coffee Roasters", 12 + "website": "https://seycoffee.com" 13 + }
+13
tests/integration/__snapshots__/roaster_all_fields.snap
··· 1 + --- 2 + title: roaster all fields 3 + test_name: TestSnap_PDS_RoasterPermutations/all_fields 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.roaster", 9 + "createdAt": "<TIMESTAMP>", 10 + "location": "Seattle, WA", 11 + "name": "Full Roaster", 12 + "website": "https://full.example.com" 13 + }
+12
tests/integration/__snapshots__/roaster_name_and_location.snap
··· 1 + --- 2 + title: roaster name and location 3 + test_name: TestSnap_PDS_RoasterPermutations/name_and_location 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.roaster", 9 + "createdAt": "<TIMESTAMP>", 10 + "location": "Portland, OR", 11 + "name": "Located Roaster" 12 + }
+12
tests/integration/__snapshots__/roaster_name_and_website.snap
··· 1 + --- 2 + title: roaster name and website 3 + test_name: TestSnap_PDS_RoasterPermutations/name_and_website 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.roaster", 9 + "createdAt": "<TIMESTAMP>", 10 + "name": "Web Roaster", 11 + "website": "https://example.com" 12 + }
+11
tests/integration/__snapshots__/roaster_name_only.snap
··· 1 + --- 2 + title: roaster name only 3 + test_name: TestSnap_PDS_RoasterPermutations/name_only 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.roaster", 9 + "createdAt": "<TIMESTAMP>", 10 + "name": "Bare Roaster" 11 + }
+13
tests/integration/__snapshots__/roaster_record.snap
··· 1 + --- 2 + title: roaster record 3 + test_name: TestSnap_PDS_RoasterCreate 4 + file_name: snapshot_test.go 5 + version: 0.1.0 6 + --- 7 + { 8 + "$type": "social.arabica.alpha.roaster", 9 + "createdAt": "<TIMESTAMP>", 10 + "location": "Durham, NC", 11 + "name": "Counter Culture", 12 + "website": "https://counterculturecoffee.com" 13 + }
+2
tests/integration/go.mod
··· 91 91 github.com/jmespath/go-jmespath v0.4.0 // indirect 92 92 github.com/klauspost/compress v1.18.3 // indirect 93 93 github.com/klauspost/cpuid/v2 v2.3.0 // indirect 94 + github.com/kortschak/utter v1.7.0 // indirect 94 95 github.com/labstack/echo-contrib v0.50.1 // indirect 95 96 github.com/labstack/echo/v4 v4.15.1 // indirect 96 97 github.com/labstack/gommon v0.4.2 // indirect ··· 122 123 github.com/prometheus/client_model v0.6.2 // indirect 123 124 github.com/prometheus/common v0.67.5 // indirect 124 125 github.com/prometheus/procfs v0.20.1 // indirect 126 + github.com/ptdewey/shutter v0.2.1 // indirect 125 127 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 126 128 github.com/samber/lo v1.53.0 // indirect 127 129 github.com/samber/slog-echo v1.21.0 // indirect
+39
tests/integration/harness.go
··· 103 103 accountsMu sync.RWMutex 104 104 accounts map[string]*atclient.APIClient 105 105 106 + // atpClients maps DID -> atp.Client for direct PDS access in tests. 107 + atpClients map[string]*atp.Client 108 + 106 109 cleanup []func() 107 110 } 108 111 ··· 161 164 FeedIndex: feedIndex, 162 165 SessionCache: sessionCache, 163 166 accounts: make(map[string]*atclient.APIClient), 167 + atpClients: make(map[string]*atp.Client), 164 168 } 165 169 166 170 // Provider routes XRPC calls based on the DID in the request context. The ··· 246 250 247 251 h.accountsMu.Lock() 248 252 h.accounts[acct.DID] = apiClient 253 + h.atpClients[acct.DID] = atp.NewClient(apiClient, syntax.DID(acct.DID)) 249 254 h.accountsMu.Unlock() 250 255 251 256 return acct ··· 347 352 // through both cache layers down to a real PDS read. 348 353 func (h *Harness) InvalidateSessionCache(acct TestAccount) { 349 354 h.SessionCache.Invalidate(h.SessionIDFor(acct)) 355 + } 356 + 357 + // PDSGetRecord fetches a single record directly from the PDS via XRPC, 358 + // bypassing all arabica caching and conversion layers. Returns the raw 359 + // record value as stored in the user's repo. 360 + func (h *Harness) PDSGetRecord(acct TestAccount, collection, rkey string) map[string]any { 361 + h.T.Helper() 362 + h.accountsMu.RLock() 363 + client := h.atpClients[acct.DID] 364 + h.accountsMu.RUnlock() 365 + require.NotNil(h.T, client, "no atp client for DID %s", acct.DID) 366 + 367 + rec, err := client.GetRecord(context.Background(), collection, rkey) 368 + require.NoError(h.T, err) 369 + return rec.Value 370 + } 371 + 372 + // PDSListRecords fetches all records in a collection directly from the PDS 373 + // via XRPC. Returns raw record values as stored in the user's repo. 374 + func (h *Harness) PDSListRecords(acct TestAccount, collection string) []map[string]any { 375 + h.T.Helper() 376 + h.accountsMu.RLock() 377 + client := h.atpClients[acct.DID] 378 + h.accountsMu.RUnlock() 379 + require.NotNil(h.T, client, "no atp client for DID %s", acct.DID) 380 + 381 + records, err := client.ListAllRecords(context.Background(), collection) 382 + require.NoError(h.T, err) 383 + 384 + values := make([]map[string]any, len(records)) 385 + for i, r := range records { 386 + values[i] = r.Value 387 + } 388 + return values 350 389 } 351 390 352 391 // Delete sends a DELETE request as the primary account.
+784
tests/integration/snapshot_test.go
··· 1 + package integration 2 + 3 + import ( 4 + "encoding/json" 5 + "net/url" 6 + "testing" 7 + 8 + "arabica/internal/atproto" 9 + 10 + "github.com/ptdewey/shutter" 11 + "github.com/stretchr/testify/require" 12 + ) 13 + 14 + // scrubPDS returns shutter options that replace PDS-generated dynamic values 15 + // (AT-URIs containing DIDs and rkeys, timestamps) with stable placeholders so 16 + // snapshots are deterministic across runs. 17 + func scrubPDS(did string, rkeys map[string]string) []shutter.Option { 18 + opts := []shutter.Option{ 19 + shutter.ScrubTimestamp(), 20 + shutter.ScrubRegex(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s"]*`, "<TIMESTAMP>"), 21 + shutter.ScrubExact(did, "<DID>"), 22 + } 23 + for label, rkey := range rkeys { 24 + opts = append(opts, shutter.ScrubExact(rkey, "<RKEY:"+label+">")) 25 + } 26 + return opts 27 + } 28 + 29 + // snapPDSRecord is a helper that fetches a record directly from the PDS and 30 + // snapshots it. This verifies the raw AT Protocol record shape, bypassing all 31 + // arabica caching and model conversion. 32 + func snapPDSRecord(t *testing.T, h *Harness, title, collection, rkey string, rkeys map[string]string) { 33 + t.Helper() 34 + raw := h.PDSGetRecord(h.PrimaryAccount, collection, rkey) 35 + b, err := json.MarshalIndent(raw, "", " ") 36 + require.NoError(t, err) 37 + shutter.SnapJSON(t, title, string(b), 38 + scrubPDS(h.PrimaryAccount.DID, rkeys)..., 39 + ) 40 + } 41 + 42 + // snapPDSCollection is a helper that lists all records in a collection directly 43 + // from the PDS and snapshots them. 44 + func snapPDSCollection(t *testing.T, h *Harness, title, collection string, rkeys map[string]string) { 45 + t.Helper() 46 + records := h.PDSListRecords(h.PrimaryAccount, collection) 47 + b, err := json.MarshalIndent(records, "", " ") 48 + require.NoError(t, err) 49 + shutter.SnapJSON(t, title, string(b), 50 + scrubPDS(h.PrimaryAccount.DID, rkeys)..., 51 + ) 52 + } 53 + 54 + // --- Roaster --- 55 + 56 + func TestSnap_PDS_RoasterCreate(t *testing.T) { 57 + h := StartHarness(t, nil) 58 + 59 + rkey := mustRKey(t, h.PostForm("/api/roasters", form( 60 + "name", "Counter Culture", 61 + "location", "Durham, NC", 62 + "website", "https://counterculturecoffee.com", 63 + )), "roaster") 64 + 65 + snapPDSRecord(t, h, "roaster record", atproto.NSIDRoaster, rkey, map[string]string{ 66 + "roaster": rkey, 67 + }) 68 + } 69 + 70 + func TestSnap_PDS_RoasterUpdate(t *testing.T) { 71 + h := StartHarness(t, nil) 72 + 73 + rkey := mustRKey(t, h.PostForm("/api/roasters", form("name", "Sey Coffee", "location", "Brooklyn, NY")), "roaster") 74 + 75 + updateResp := h.PutForm("/api/roasters/"+rkey, form( 76 + "name", "Sey Coffee Roasters", 77 + "location", "Brooklyn, NY", 78 + "website", "https://seycoffee.com", 79 + )) 80 + require.Equal(t, 200, updateResp.StatusCode, statusErr(updateResp, ReadBody(t, updateResp))) 81 + 82 + snapPDSRecord(t, h, "roaster after update", atproto.NSIDRoaster, rkey, map[string]string{ 83 + "roaster": rkey, 84 + }) 85 + } 86 + 87 + // --- Bean with roaster reference --- 88 + 89 + func TestSnap_PDS_BeanWithRoaster(t *testing.T) { 90 + h := StartHarness(t, nil) 91 + 92 + roasterRKey := mustRKey(t, h.PostForm("/api/roasters", form("name", "Onyx Coffee Lab")), "roaster") 93 + beanRKey := mustRKey(t, h.PostForm("/api/beans", form( 94 + "name", "Geometry", 95 + "origin", "Colombia", 96 + "roast_level", "Medium", 97 + "process", "Washed", 98 + "variety", "Caturra", 99 + "roaster_rkey", roasterRKey, 100 + )), "bean") 101 + 102 + snapPDSRecord(t, h, "bean with roaster ref", atproto.NSIDBean, beanRKey, map[string]string{ 103 + "roaster": roasterRKey, 104 + "bean": beanRKey, 105 + }) 106 + } 107 + 108 + // --- Grinder --- 109 + 110 + func TestSnap_PDS_GrinderCreate(t *testing.T) { 111 + h := StartHarness(t, nil) 112 + 113 + rkey := mustRKey(t, h.PostForm("/api/grinders", form( 114 + "name", "Comandante C40", 115 + "grinder_type", "Manual", 116 + "burr_type", "Conical", 117 + "notes", "red clix installed", 118 + )), "grinder") 119 + 120 + snapPDSRecord(t, h, "grinder record", atproto.NSIDGrinder, rkey, map[string]string{ 121 + "grinder": rkey, 122 + }) 123 + } 124 + 125 + // --- Brewer --- 126 + 127 + func TestSnap_PDS_BrewerCreate(t *testing.T) { 128 + h := StartHarness(t, nil) 129 + 130 + rkey := mustRKey(t, h.PostForm("/api/brewers", form( 131 + "name", "Hario V60 02", 132 + "brewer_type", "Pour Over", 133 + "description", "Plastic, size 02", 134 + )), "brewer") 135 + 136 + snapPDSRecord(t, h, "brewer record", atproto.NSIDBrewer, rkey, map[string]string{ 137 + "brewer": rkey, 138 + }) 139 + } 140 + 141 + // --- Recipe with pours --- 142 + 143 + func TestSnap_PDS_RecipeWithPours(t *testing.T) { 144 + h := StartHarness(t, nil) 145 + 146 + brewerRKey := mustRKey(t, h.PostForm("/api/brewers", form("name", "V60", "brewer_type", "Pour Over")), "brewer") 147 + recipeRKey := mustRKey(t, h.PostForm("/api/recipes", form( 148 + "name", "4:6 Method", 149 + "brewer_rkey", brewerRKey, 150 + "brewer_type", "Pour Over", 151 + "coffee_amount", "20", 152 + "water_amount", "300", 153 + "notes", "Tetsu Kasuya 4:6", 154 + "pour_water_0", "50", 155 + "pour_time_0", "0", 156 + "pour_water_1", "70", 157 + "pour_time_1", "45", 158 + "pour_water_2", "60", 159 + "pour_time_2", "90", 160 + "pour_water_3", "60", 161 + "pour_time_3", "120", 162 + "pour_water_4", "60", 163 + "pour_time_4", "150", 164 + )), "recipe") 165 + 166 + snapPDSRecord(t, h, "recipe with pours", atproto.NSIDRecipe, recipeRKey, map[string]string{ 167 + "brewer": brewerRKey, 168 + "recipe": recipeRKey, 169 + }) 170 + } 171 + 172 + // --- Pourover brew (full references + pours + params) --- 173 + 174 + func TestSnap_PDS_PouroverBrew(t *testing.T) { 175 + h := StartHarness(t, nil) 176 + 177 + roasterRKey := mustRKey(t, h.PostForm("/api/roasters", form("name", "Snap Roaster")), "roaster") 178 + beanRKey := mustRKey(t, h.PostForm("/api/beans", form( 179 + "name", "Snap Bean", "roaster_rkey", roasterRKey, "roast_level", "Light", 180 + )), "bean") 181 + grinderRKey := mustRKey(t, h.PostForm("/api/grinders", form("name", "Snap Grinder", "grinder_type", "Manual")), "grinder") 182 + brewerRKey := mustRKey(t, h.PostForm("/api/brewers", form("name", "Snap V60", "brewer_type", "Pour Over")), "brewer") 183 + 184 + brewForm := url.Values{} 185 + brewForm.Set("bean_rkey", beanRKey) 186 + brewForm.Set("grinder_rkey", grinderRKey) 187 + brewForm.Set("brewer_rkey", brewerRKey) 188 + brewForm.Set("method", "Pour Over") 189 + brewForm.Set("temperature", "94") 190 + brewForm.Set("water_amount", "300") 191 + brewForm.Set("coffee_amount", "18") 192 + brewForm.Set("time_seconds", "210") 193 + brewForm.Set("rating", "8") 194 + brewForm.Set("grind_size", "Medium") 195 + brewForm.Set("tasting_notes", "bright, floral") 196 + brewForm.Set("pour_water_0", "60") 197 + brewForm.Set("pour_time_0", "0") 198 + brewForm.Set("pour_water_1", "120") 199 + brewForm.Set("pour_time_1", "45") 200 + brewForm.Set("pour_water_2", "120") 201 + brewForm.Set("pour_time_2", "90") 202 + brewForm.Set("pourover_bloom_water", "60") 203 + brewForm.Set("pourover_bloom_seconds", "30") 204 + brewForm.Set("pourover_drawdown_seconds", "45") 205 + brewForm.Set("pourover_filter", "Hario tabbed") 206 + 207 + resp := h.PostForm("/brews", brewForm) 208 + require.Equal(t, 200, resp.StatusCode, statusErr(resp, ReadBody(t, resp))) 209 + 210 + // Get the brew rkey from the arabica API, then snapshot the raw PDS record. 211 + data := fetchData(t, h) 212 + require.Len(t, data.Brews, 1) 213 + brewRKey := data.Brews[0].RKey 214 + 215 + rkeys := map[string]string{ 216 + "roaster": roasterRKey, 217 + "bean": beanRKey, 218 + "grinder": grinderRKey, 219 + "brewer": brewerRKey, 220 + "brew": brewRKey, 221 + } 222 + 223 + snapPDSRecord(t, h, "pourover brew record", atproto.NSIDBrew, brewRKey, rkeys) 224 + } 225 + 226 + // --- Espresso brew (method-specific params) --- 227 + 228 + func TestSnap_PDS_EspressoBrew(t *testing.T) { 229 + h := StartHarness(t, nil) 230 + 231 + roasterRKey := mustRKey(t, h.PostForm("/api/roasters", form("name", "Snap Roaster")), "roaster") 232 + beanRKey := mustRKey(t, h.PostForm("/api/beans", form( 233 + "name", "Snap Bean", "roaster_rkey", roasterRKey, "roast_level", "Medium", 234 + )), "bean") 235 + grinderRKey := mustRKey(t, h.PostForm("/api/grinders", form("name", "Snap Grinder", "grinder_type", "Electric")), "grinder") 236 + brewerRKey := mustRKey(t, h.PostForm("/api/brewers", form("name", "Snap Espresso", "brewer_type", "Espresso")), "brewer") 237 + 238 + brewForm := url.Values{} 239 + brewForm.Set("bean_rkey", beanRKey) 240 + brewForm.Set("grinder_rkey", grinderRKey) 241 + brewForm.Set("brewer_rkey", brewerRKey) 242 + brewForm.Set("method", "Espresso") 243 + brewForm.Set("water_amount", "36") 244 + brewForm.Set("coffee_amount", "18") 245 + brewForm.Set("time_seconds", "28") 246 + brewForm.Set("rating", "9") 247 + brewForm.Set("tasting_notes", "syrupy, chocolate") 248 + brewForm.Set("espresso_yield_weight", "36") 249 + brewForm.Set("espresso_pressure", "9") 250 + brewForm.Set("espresso_pre_infusion_seconds", "5") 251 + 252 + resp := h.PostForm("/brews", brewForm) 253 + require.Equal(t, 200, resp.StatusCode, statusErr(resp, ReadBody(t, resp))) 254 + 255 + data := fetchData(t, h) 256 + require.Len(t, data.Brews, 1) 257 + brewRKey := data.Brews[0].RKey 258 + 259 + rkeys := map[string]string{ 260 + "roaster": roasterRKey, 261 + "bean": beanRKey, 262 + "grinder": grinderRKey, 263 + "brewer": brewerRKey, 264 + "brew": brewRKey, 265 + } 266 + 267 + snapPDSRecord(t, h, "espresso brew record", atproto.NSIDBrew, brewRKey, rkeys) 268 + } 269 + 270 + // --- Brew update: pourover → espresso (verify old params removed) --- 271 + 272 + func TestSnap_PDS_BrewMethodSwap(t *testing.T) { 273 + h := StartHarness(t, nil) 274 + 275 + roasterRKey := mustRKey(t, h.PostForm("/api/roasters", form("name", "Snap Roaster")), "roaster") 276 + beanRKey := mustRKey(t, h.PostForm("/api/beans", form( 277 + "name", "Snap Bean", "roaster_rkey", roasterRKey, "roast_level", "Medium", 278 + )), "bean") 279 + grinderRKey := mustRKey(t, h.PostForm("/api/grinders", form("name", "Snap Grinder", "grinder_type", "Electric")), "grinder") 280 + pourBrewerRKey := mustRKey(t, h.PostForm("/api/brewers", form("name", "Snap V60", "brewer_type", "Pour Over")), "brewer") 281 + espBrewerRKey := mustRKey(t, h.PostForm("/api/brewers", form("name", "Snap Espresso", "brewer_type", "Espresso")), "brewer") 282 + 283 + // Create as pourover. 284 + createForm := url.Values{} 285 + createForm.Set("bean_rkey", beanRKey) 286 + createForm.Set("grinder_rkey", grinderRKey) 287 + createForm.Set("brewer_rkey", pourBrewerRKey) 288 + createForm.Set("method", "Pour Over") 289 + createForm.Set("water_amount", "300") 290 + createForm.Set("coffee_amount", "18") 291 + createForm.Set("time_seconds", "210") 292 + createForm.Set("rating", "7") 293 + createForm.Set("pour_water_0", "60") 294 + createForm.Set("pour_time_0", "0") 295 + createForm.Set("pour_water_1", "240") 296 + createForm.Set("pour_time_1", "45") 297 + createForm.Set("pourover_bloom_water", "60") 298 + createForm.Set("pourover_bloom_seconds", "30") 299 + 300 + resp := h.PostForm("/brews", createForm) 301 + require.Equal(t, 200, resp.StatusCode, statusErr(resp, ReadBody(t, resp))) 302 + 303 + data := fetchData(t, h) 304 + require.Len(t, data.Brews, 1) 305 + brewRKey := data.Brews[0].RKey 306 + 307 + // Update to espresso. 308 + updateForm := url.Values{} 309 + updateForm.Set("bean_rkey", beanRKey) 310 + updateForm.Set("grinder_rkey", grinderRKey) 311 + updateForm.Set("brewer_rkey", espBrewerRKey) 312 + updateForm.Set("method", "Espresso") 313 + updateForm.Set("water_amount", "36") 314 + updateForm.Set("coffee_amount", "18") 315 + updateForm.Set("time_seconds", "28") 316 + updateForm.Set("rating", "9") 317 + updateForm.Set("tasting_notes", "syrupy, chocolate") 318 + updateForm.Set("espresso_yield_weight", "36") 319 + updateForm.Set("espresso_pressure", "9") 320 + updateForm.Set("espresso_pre_infusion_seconds", "5") 321 + 322 + updateResp := h.PutForm("/brews/"+brewRKey, updateForm) 323 + require.Equal(t, 200, updateResp.StatusCode, statusErr(updateResp, ReadBody(t, updateResp))) 324 + 325 + rkeys := map[string]string{ 326 + "roaster": roasterRKey, 327 + "bean": beanRKey, 328 + "grinder": grinderRKey, 329 + "pourBrewer": pourBrewerRKey, 330 + "espBrewer": espBrewerRKey, 331 + "brew": brewRKey, 332 + } 333 + 334 + snapPDSRecord(t, h, "brew after pourover to espresso", atproto.NSIDBrew, brewRKey, rkeys) 335 + } 336 + 337 + // --- Roaster field permutations --- 338 + 339 + func TestSnap_PDS_RoasterPermutations(t *testing.T) { 340 + cases := []struct { 341 + name string 342 + form []string 343 + }{ 344 + {"name only", []string{"name", "Bare Roaster"}}, 345 + {"name and location", []string{"name", "Located Roaster", "location", "Portland, OR"}}, 346 + {"name and website", []string{"name", "Web Roaster", "website", "https://example.com"}}, 347 + {"all fields", []string{"name", "Full Roaster", "location", "Seattle, WA", "website", "https://full.example.com"}}, 348 + } 349 + 350 + for _, tc := range cases { 351 + t.Run(tc.name, func(t *testing.T) { 352 + h := StartHarness(t, nil) 353 + rkey := mustRKey(t, h.PostForm("/api/roasters", form(tc.form...)), "roaster") 354 + snapPDSRecord(t, h, "roaster "+tc.name, atproto.NSIDRoaster, rkey, map[string]string{ 355 + "roaster": rkey, 356 + }) 357 + }) 358 + } 359 + } 360 + 361 + // --- Bean field permutations --- 362 + 363 + func TestSnap_PDS_BeanPermutations(t *testing.T) { 364 + h := StartHarness(t, nil) 365 + roasterRKey := mustRKey(t, h.PostForm("/api/roasters", form("name", "Perm Roaster")), "roaster") 366 + 367 + cases := []struct { 368 + name string 369 + form []string 370 + }{ 371 + {"name only", []string{"name", "Bare Bean"}}, 372 + {"with origin", []string{"name", "Origin Bean", "origin", "Ethiopia"}}, 373 + {"with roast level", []string{"name", "Roasted Bean", "roast_level", "Dark"}}, 374 + {"with variety and process", []string{ 375 + "name", "Processed Bean", "variety", "Gesha", "process", "Natural", 376 + }}, 377 + {"with roaster ref only", []string{ 378 + "name", "Sourced Bean", "roaster_rkey", roasterRKey, 379 + }}, 380 + {"with description", []string{ 381 + "name", "Described Bean", "description", "Juicy and complex", 382 + }}, 383 + {"all fields", []string{ 384 + "name", "Full Bean", 385 + "origin", "Colombia", 386 + "variety", "Caturra", 387 + "roast_level", "Medium", 388 + "process", "Washed", 389 + "description", "Balanced and clean", 390 + "roaster_rkey", roasterRKey, 391 + }}, 392 + } 393 + 394 + for _, tc := range cases { 395 + t.Run(tc.name, func(t *testing.T) { 396 + rkey := mustRKey(t, h.PostForm("/api/beans", form(tc.form...)), "bean") 397 + snapPDSRecord(t, h, "bean "+tc.name, atproto.NSIDBean, rkey, map[string]string{ 398 + "roaster": roasterRKey, 399 + "bean": rkey, 400 + }) 401 + }) 402 + } 403 + } 404 + 405 + // --- Grinder field permutations --- 406 + 407 + func TestSnap_PDS_GrinderPermutations(t *testing.T) { 408 + cases := []struct { 409 + name string 410 + form []string 411 + }{ 412 + {"name only", []string{"name", "Bare Grinder"}}, 413 + {"with type", []string{"name", "Typed Grinder", "grinder_type", "Electric"}}, 414 + {"with burr", []string{"name", "Burr Grinder", "burr_type", "Flat"}}, 415 + {"with notes", []string{"name", "Noted Grinder", "notes", "SSP burrs installed"}}, 416 + {"type and burr", []string{ 417 + "name", "Full Manual", "grinder_type", "Manual", "burr_type", "Conical", 418 + }}, 419 + {"all fields", []string{ 420 + "name", "Full Grinder", 421 + "grinder_type", "Electric", 422 + "burr_type", "Flat", 423 + "notes", "64mm SSP multipurpose", 424 + }}, 425 + } 426 + 427 + for _, tc := range cases { 428 + t.Run(tc.name, func(t *testing.T) { 429 + h := StartHarness(t, nil) 430 + rkey := mustRKey(t, h.PostForm("/api/grinders", form(tc.form...)), "grinder") 431 + snapPDSRecord(t, h, "grinder "+tc.name, atproto.NSIDGrinder, rkey, map[string]string{ 432 + "grinder": rkey, 433 + }) 434 + }) 435 + } 436 + } 437 + 438 + // --- Brewer field permutations --- 439 + 440 + func TestSnap_PDS_BrewerPermutations(t *testing.T) { 441 + cases := []struct { 442 + name string 443 + form []string 444 + }{ 445 + {"name only", []string{"name", "Bare Brewer"}}, 446 + {"with type", []string{"name", "Typed Brewer", "brewer_type", "Immersion"}}, 447 + {"with description", []string{"name", "Described Brewer", "description", "12oz capacity"}}, 448 + {"all fields", []string{ 449 + "name", "Full Brewer", 450 + "brewer_type", "Pour Over", 451 + "description", "Ceramic, size 02", 452 + }}, 453 + } 454 + 455 + for _, tc := range cases { 456 + t.Run(tc.name, func(t *testing.T) { 457 + h := StartHarness(t, nil) 458 + rkey := mustRKey(t, h.PostForm("/api/brewers", form(tc.form...)), "brewer") 459 + snapPDSRecord(t, h, "brewer "+tc.name, atproto.NSIDBrewer, rkey, map[string]string{ 460 + "brewer": rkey, 461 + }) 462 + }) 463 + } 464 + } 465 + 466 + // --- Recipe field permutations --- 467 + 468 + func TestSnap_PDS_RecipePermutations(t *testing.T) { 469 + h := StartHarness(t, nil) 470 + brewerRKey := mustRKey(t, h.PostForm("/api/brewers", form("name", "Recipe Brewer", "brewer_type", "Pour Over")), "brewer") 471 + 472 + cases := []struct { 473 + name string 474 + form []string 475 + }{ 476 + {"name only", []string{"name", "Bare Recipe"}}, 477 + {"with brewer ref", []string{ 478 + "name", "Brewer Recipe", "brewer_rkey", brewerRKey, "brewer_type", "Pour Over", 479 + }}, 480 + {"with amounts no pours", []string{ 481 + "name", "Amounts Recipe", 482 + "coffee_amount", "15", 483 + "water_amount", "250", 484 + }}, 485 + {"with notes", []string{ 486 + "name", "Noted Recipe", 487 + "notes", "Hoffmann method", 488 + }}, 489 + {"with single pour", []string{ 490 + "name", "Single Pour Recipe", 491 + "brewer_rkey", brewerRKey, 492 + "brewer_type", "Pour Over", 493 + "coffee_amount", "18", 494 + "water_amount", "300", 495 + "pour_water_0", "300", 496 + "pour_time_0", "0", 497 + }}, 498 + {"all fields", []string{ 499 + "name", "Full Recipe", 500 + "brewer_rkey", brewerRKey, 501 + "brewer_type", "Pour Over", 502 + "coffee_amount", "20", 503 + "water_amount", "300", 504 + "notes", "Competition recipe", 505 + "pour_water_0", "60", 506 + "pour_time_0", "0", 507 + "pour_water_1", "120", 508 + "pour_time_1", "45", 509 + "pour_water_2", "120", 510 + "pour_time_2", "90", 511 + }}, 512 + } 513 + 514 + for _, tc := range cases { 515 + t.Run(tc.name, func(t *testing.T) { 516 + rkey := mustRKey(t, h.PostForm("/api/recipes", form(tc.form...)), "recipe") 517 + snapPDSRecord(t, h, "recipe "+tc.name, atproto.NSIDRecipe, rkey, map[string]string{ 518 + "brewer": brewerRKey, 519 + "recipe": rkey, 520 + }) 521 + }) 522 + } 523 + } 524 + 525 + // --- Brew field permutations --- 526 + 527 + func TestSnap_PDS_BrewPermutations(t *testing.T) { 528 + h := StartHarness(t, nil) 529 + 530 + roasterRKey := mustRKey(t, h.PostForm("/api/roasters", form("name", "Perm Roaster")), "roaster") 531 + beanRKey := mustRKey(t, h.PostForm("/api/beans", form( 532 + "name", "Perm Bean", "roaster_rkey", roasterRKey, "roast_level", "Light", 533 + )), "bean") 534 + grinderRKey := mustRKey(t, h.PostForm("/api/grinders", form("name", "Perm Grinder", "grinder_type", "Manual")), "grinder") 535 + brewerRKey := mustRKey(t, h.PostForm("/api/brewers", form("name", "Perm V60", "brewer_type", "Pour Over")), "brewer") 536 + espBrewerRKey := mustRKey(t, h.PostForm("/api/brewers", form("name", "Perm Espresso", "brewer_type", "Espresso")), "brewer") 537 + recipeRKey := mustRKey(t, h.PostForm("/api/recipes", form( 538 + "name", "Perm Recipe", "brewer_rkey", brewerRKey, "brewer_type", "Pour Over", 539 + "coffee_amount", "18", "water_amount", "300", 540 + )), "recipe") 541 + 542 + rkeys := map[string]string{ 543 + "roaster": roasterRKey, 544 + "bean": beanRKey, 545 + "grinder": grinderRKey, 546 + "brewer": brewerRKey, 547 + "espBrewer": espBrewerRKey, 548 + "recipe": recipeRKey, 549 + } 550 + 551 + cases := []struct { 552 + name string 553 + fields url.Values 554 + }{ 555 + {"minimal bean only", func() url.Values { 556 + f := url.Values{} 557 + f.Set("bean_rkey", beanRKey) 558 + return f 559 + }()}, 560 + {"bean with method", func() url.Values { 561 + f := url.Values{} 562 + f.Set("bean_rkey", beanRKey) 563 + f.Set("method", "French Press") 564 + return f 565 + }()}, 566 + {"bean with amounts", func() url.Values { 567 + f := url.Values{} 568 + f.Set("bean_rkey", beanRKey) 569 + f.Set("water_amount", "200") 570 + f.Set("coffee_amount", "13") 571 + return f 572 + }()}, 573 + {"with grinder and grind size", func() url.Values { 574 + f := url.Values{} 575 + f.Set("bean_rkey", beanRKey) 576 + f.Set("grinder_rkey", grinderRKey) 577 + f.Set("grind_size", "Fine") 578 + return f 579 + }()}, 580 + {"with brewer no params", func() url.Values { 581 + f := url.Values{} 582 + f.Set("bean_rkey", beanRKey) 583 + f.Set("brewer_rkey", brewerRKey) 584 + f.Set("method", "Pour Over") 585 + return f 586 + }()}, 587 + {"with recipe ref", func() url.Values { 588 + f := url.Values{} 589 + f.Set("bean_rkey", beanRKey) 590 + f.Set("brewer_rkey", brewerRKey) 591 + f.Set("recipe_rkey", recipeRKey) 592 + f.Set("method", "Pour Over") 593 + f.Set("water_amount", "300") 594 + f.Set("coffee_amount", "18") 595 + return f 596 + }()}, 597 + {"with temperature and time", func() url.Values { 598 + f := url.Values{} 599 + f.Set("bean_rkey", beanRKey) 600 + f.Set("temperature", "96") 601 + f.Set("time_seconds", "240") 602 + return f 603 + }()}, 604 + {"with tasting notes and rating", func() url.Values { 605 + f := url.Values{} 606 + f.Set("bean_rkey", beanRKey) 607 + f.Set("tasting_notes", "cherry, jasmine, silky") 608 + f.Set("rating", "9") 609 + return f 610 + }()}, 611 + {"pourover partial params", func() url.Values { 612 + f := url.Values{} 613 + f.Set("bean_rkey", beanRKey) 614 + f.Set("brewer_rkey", brewerRKey) 615 + f.Set("method", "Pour Over") 616 + f.Set("pourover_bloom_water", "50") 617 + f.Set("pourover_bloom_seconds", "45") 618 + return f 619 + }()}, 620 + {"pourover with filter only", func() url.Values { 621 + f := url.Values{} 622 + f.Set("bean_rkey", beanRKey) 623 + f.Set("brewer_rkey", brewerRKey) 624 + f.Set("method", "Pour Over") 625 + f.Set("pourover_filter", "Sibarist FAST") 626 + return f 627 + }()}, 628 + {"espresso partial params", func() url.Values { 629 + f := url.Values{} 630 + f.Set("bean_rkey", beanRKey) 631 + f.Set("brewer_rkey", espBrewerRKey) 632 + f.Set("method", "Espresso") 633 + f.Set("coffee_amount", "18") 634 + f.Set("espresso_yield_weight", "40") 635 + return f 636 + }()}, 637 + {"espresso pressure only", func() url.Values { 638 + f := url.Values{} 639 + f.Set("bean_rkey", beanRKey) 640 + f.Set("brewer_rkey", espBrewerRKey) 641 + f.Set("method", "Espresso") 642 + f.Set("espresso_pressure", "6") 643 + return f 644 + }()}, 645 + {"pours without params", func() url.Values { 646 + f := url.Values{} 647 + f.Set("bean_rkey", beanRKey) 648 + f.Set("method", "Pour Over") 649 + f.Set("pour_water_0", "60") 650 + f.Set("pour_time_0", "0") 651 + f.Set("pour_water_1", "240") 652 + f.Set("pour_time_1", "30") 653 + return f 654 + }()}, 655 + {"single pour", func() url.Values { 656 + f := url.Values{} 657 + f.Set("bean_rkey", beanRKey) 658 + f.Set("method", "Pour Over") 659 + f.Set("pour_water_0", "300") 660 + f.Set("pour_time_0", "0") 661 + return f 662 + }()}, 663 + } 664 + 665 + for _, tc := range cases { 666 + t.Run(tc.name, func(t *testing.T) { 667 + // Each subtest needs its own harness to avoid brew accumulation. 668 + sub := StartHarness(t, nil) 669 + // Re-create the deps in this harness. 670 + subRoasterRKey := mustRKey(t, sub.PostForm("/api/roasters", form("name", "Perm Roaster")), "roaster") 671 + subBeanRKey := mustRKey(t, sub.PostForm("/api/beans", form( 672 + "name", "Perm Bean", "roaster_rkey", subRoasterRKey, "roast_level", "Light", 673 + )), "bean") 674 + subGrinderRKey := mustRKey(t, sub.PostForm("/api/grinders", form("name", "Perm Grinder", "grinder_type", "Manual")), "grinder") 675 + subBrewerRKey := mustRKey(t, sub.PostForm("/api/brewers", form("name", "Perm V60", "brewer_type", "Pour Over")), "brewer") 676 + subEspBrewerRKey := mustRKey(t, sub.PostForm("/api/brewers", form("name", "Perm Espresso", "brewer_type", "Espresso")), "brewer") 677 + subRecipeRKey := mustRKey(t, sub.PostForm("/api/recipes", form( 678 + "name", "Perm Recipe", "brewer_rkey", subBrewerRKey, "brewer_type", "Pour Over", 679 + "coffee_amount", "18", "water_amount", "300", 680 + )), "recipe") 681 + 682 + // Remap the form values to use this harness's rkeys. 683 + remapped := url.Values{} 684 + for k, vs := range tc.fields { 685 + for _, v := range vs { 686 + switch v { 687 + case beanRKey: 688 + v = subBeanRKey 689 + case grinderRKey: 690 + v = subGrinderRKey 691 + case brewerRKey: 692 + v = subBrewerRKey 693 + case espBrewerRKey: 694 + v = subEspBrewerRKey 695 + case recipeRKey: 696 + v = subRecipeRKey 697 + } 698 + remapped.Set(k, v) 699 + } 700 + } 701 + 702 + resp := sub.PostForm("/brews", remapped) 703 + require.Equal(t, 200, resp.StatusCode, statusErr(resp, ReadBody(t, resp))) 704 + 705 + data := fetchData(t, sub) 706 + require.Len(t, data.Brews, 1) 707 + brewRKey := data.Brews[0].RKey 708 + 709 + subRkeys := map[string]string{ 710 + "roaster": subRoasterRKey, 711 + "bean": subBeanRKey, 712 + "grinder": subGrinderRKey, 713 + "brewer": subBrewerRKey, 714 + "espBrewer": subEspBrewerRKey, 715 + "recipe": subRecipeRKey, 716 + "brew": brewRKey, 717 + } 718 + snapPDSRecord(t, sub, "brew "+tc.name, atproto.NSIDBrew, brewRKey, subRkeys) 719 + }) 720 + } 721 + 722 + // Suppress unused variable warnings — these were used to build the test 723 + // case form values above. 724 + _ = rkeys 725 + } 726 + 727 + // --- Full user repo: create multiple entities, snapshot entire collections --- 728 + 729 + func TestSnap_PDS_FullRepo(t *testing.T) { 730 + h := StartHarness(t, nil) 731 + 732 + roasterRKey := mustRKey(t, h.PostForm("/api/roasters", form( 733 + "name", "Onyx", "location", "Rogers, AR", 734 + )), "roaster") 735 + roaster2RKey := mustRKey(t, h.PostForm("/api/roasters", form( 736 + "name", "Sey", "location", "Brooklyn, NY", "website", "https://seycoffee.com", 737 + )), "roaster") 738 + 739 + beanRKey := mustRKey(t, h.PostForm("/api/beans", form( 740 + "name", "Monarch", "roaster_rkey", roasterRKey, "roast_level", "Medium", "origin", "Blend", 741 + )), "bean") 742 + 743 + grinderRKey := mustRKey(t, h.PostForm("/api/grinders", form( 744 + "name", "DF64", "grinder_type", "Electric", "burr_type", "Flat", 745 + )), "grinder") 746 + 747 + brewerRKey := mustRKey(t, h.PostForm("/api/brewers", form( 748 + "name", "V60", "brewer_type", "Pour Over", 749 + )), "brewer") 750 + 751 + // Create a brew. 752 + brewForm := url.Values{} 753 + brewForm.Set("bean_rkey", beanRKey) 754 + brewForm.Set("grinder_rkey", grinderRKey) 755 + brewForm.Set("brewer_rkey", brewerRKey) 756 + brewForm.Set("method", "Pour Over") 757 + brewForm.Set("water_amount", "250") 758 + brewForm.Set("coffee_amount", "15") 759 + brewForm.Set("time_seconds", "180") 760 + brewForm.Set("rating", "7") 761 + 762 + resp := h.PostForm("/brews", brewForm) 763 + require.Equal(t, 200, resp.StatusCode, statusErr(resp, ReadBody(t, resp))) 764 + 765 + data := fetchData(t, h) 766 + require.Len(t, data.Brews, 1) 767 + 768 + // Collect all rkeys for scrubbing. 769 + rkeys := map[string]string{ 770 + "roaster": roasterRKey, 771 + "roaster2": roaster2RKey, 772 + "bean": beanRKey, 773 + "grinder": grinderRKey, 774 + "brewer": brewerRKey, 775 + "brew": data.Brews[0].RKey, 776 + } 777 + 778 + // Snapshot each collection from the PDS. 779 + snapPDSCollection(t, h, "all roasters", atproto.NSIDRoaster, rkeys) 780 + snapPDSCollection(t, h, "all beans", atproto.NSIDBean, rkeys) 781 + snapPDSCollection(t, h, "all grinders", atproto.NSIDGrinder, rkeys) 782 + snapPDSCollection(t, h, "all brewers", atproto.NSIDBrewer, rkeys) 783 + snapPDSCollection(t, h, "all brews", atproto.NSIDBrew, rkeys) 784 + }