ai cooking
0
fork

Configure Feed

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

unncessary helpers. Add test helpers instead (#417)

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
bb7fd09b 5ed9d8b0

+199 -290
+5 -3
internal/recipes/params.go
··· 98 98 return base64.RawURLEncoding.EncodeToString(decoded[len(seedBytes):]), true 99 99 } 100 100 101 - func (s *server) ParseQueryArgs(ctx context.Context, r *http.Request) (*generatorParams, error) { 101 + func ParseQueryArgs(ctx context.Context, r *http.Request, ls locServer) (*generatorParams, error) { 102 102 loc := r.URL.Query().Get("location") 103 103 if loc == "" { 104 104 return nil, errors.New("must provide location id") 105 105 } 106 + if ls == nil { 107 + return nil, errors.New("location lookup is required") 108 + } 106 109 107 - l, err := s.locServer.GetLocationByID(ctx, loc) 110 + l, err := ls.GetLocationByID(ctx, loc) 108 111 if err != nil { 109 112 return nil, err 110 113 } 111 - 112 114 storeLoc, err := resolveStoreTimeLocation(ctx, l) 113 115 if err != nil { 114 116 return nil, err
+5 -9
internal/recipes/params_test.go
··· 45 45 nowFn = oldNowFn 46 46 }() 47 47 48 - s := &server{ 49 - locServer: staticLocationLookup{ 50 - location: &locations.Location{ 51 - ID: "store-1", 52 - Name: "Test Store", 53 - ZipCode: "10001", 54 - }, 55 - }, 48 + location := &locations.Location{ 49 + ID: "store-1", 50 + Name: "Test Store", 51 + ZipCode: "10001", 56 52 } 57 53 58 54 req := httptest.NewRequest("GET", "/recipes?location=store-1", nil) 59 - p, err := s.ParseQueryArgs(context.Background(), req) 55 + p, err := ParseQueryArgs(context.Background(), req, staticLocationLookup{location: location}) 60 56 if err != nil { 61 57 t.Fatalf("ParseQueryArgs returned error: %v", err) 62 58 }
+1 -20
internal/recipes/server.go
··· 105 105 } 106 106 } 107 107 108 - func (s *server) recipeImageIO() imageio { 109 - if s.imageio.Cache != nil { 110 - return s.imageio 111 - } 112 - return imageio{Cache: s.recipeio.Cache} 113 - } 114 - 115 - func (s *server) RecipeImageExists(ctx context.Context, hash string) (bool, error) { 116 - return s.recipeImageIO().RecipeImageExists(ctx, hash) 117 - } 118 - 119 - func (s *server) RecipeImageFromCache(ctx context.Context, hash string) (io.ReadCloser, error) { 120 - return s.recipeImageIO().RecipeImageFromCache(ctx, hash) 121 - } 122 - 123 - func (s *server) SaveRecipeImage(ctx context.Context, hash string, image *ai.GeneratedImage) error { 124 - return s.recipeImageIO().SaveRecipeImage(ctx, hash, image) 125 - } 126 - 127 108 func (s *server) Register(mux routing.Registrar) { 128 109 mux.HandleFunc("GET /recipes", s.handleRecipes) 129 110 mux.HandleFunc("POST /recipes/{hash}/regenerate", s.handleRegenerate) ··· 973 954 return 974 955 } 975 956 976 - p, err := s.ParseQueryArgs(ctx, r) 957 + p, err := ParseQueryArgs(ctx, r, s.locServer) 977 958 if err != nil { 978 959 http.Error(w, fmt.Sprintf("invalid query parameters: %v", err), http.StatusBadRequest) 979 960 return
+108 -203
internal/recipes/server_test.go
··· 78 78 req := httptest.NewRequest(http.MethodGet, "/recipes?h="+url.QueryEscape(legacyHash), nil) 79 79 rr := httptest.NewRecorder() 80 80 81 - s := &server{} 81 + s := newTestServer(t) 82 82 s.handleRecipes(rr, req) 83 83 84 84 if rr.Code != http.StatusSeeOther { ··· 108 108 req := httptest.NewRequest(http.MethodGet, "/recipes?h="+url.QueryEscape(legacyHash)+"&mail=true&start=2026-01-25T00%3A00%3A00Z", nil) 109 109 rr := httptest.NewRecorder() 110 110 111 - s := &server{} 111 + s := newTestServer(t) 112 112 s.handleRecipes(rr, req) 113 113 114 114 if rr.Code != http.StatusSeeOther { ··· 132 132 Name: "Test Store", 133 133 ZipCode: "94105", 134 134 } 135 - s := &server{ 136 - recipeio: IO(cacheStore), 137 - storage: storage, 138 - clerk: auth.DefaultMock(), 139 - generator: mock{}, 140 - locServer: staticLocationLookup{location: location}, 141 - } 135 + s := newTestServer(t, 136 + withTestCache(cacheStore), 137 + withTestStorage(storage), 138 + withTestLocationServer(staticLocationLookup{location: location}), 139 + ) 142 140 t.Cleanup(s.Wait) 143 141 144 142 currentUser, err := storage.FindOrCreateFromClerk(t.Context(), "mock-clerk-user-id", auth.DefaultMock()) ··· 151 149 } 152 150 153 151 req := httptest.NewRequest(http.MethodGet, "/recipes?location=70001001&date=2026-03-06&instructions=make+it+vegetarian", nil) 154 - expectedParams, err := s.ParseQueryArgs(t.Context(), req) 152 + expectedParams, err := ParseQueryArgs(t.Context(), req, staticLocationLookup{location: location}) 155 153 if err != nil { 156 154 t.Fatalf("failed to build expected params: %v", err) 157 155 } ··· 198 196 199 197 func TestHandleRecipes_GuestRedirectsToSignInWhenCacheMisses(t *testing.T) { 200 198 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 201 - s := &server{ 202 - recipeio: IO(cacheStore), 203 - storage: users.NewStorage(cacheStore), 204 - clerk: noSessionAuth{}, 205 - locServer: staticLocationLookup{location: &locations.Location{ 199 + s := newTestServer(t, 200 + withTestCache(cacheStore), 201 + withTestClerk(noSessionAuth{}), 202 + withTestLocationServer(staticLocationLookup{location: &locations.Location{ 206 203 ID: "70001001", 207 204 Name: "Test Store", 208 205 ZipCode: "94105", 209 - }}, 210 - } 206 + }}), 207 + ) 211 208 212 209 req := httptest.NewRequest(http.MethodGet, "/recipes?location=70001001&instructions=make+it+vegetarian", nil) 213 210 rr := httptest.NewRecorder() ··· 224 221 225 222 func TestHandleRecipes_GuestRedirectsToCachedHashWhenCacheHits(t *testing.T) { 226 223 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 227 - s := &server{ 228 - recipeio: IO(cacheStore), 229 - storage: users.NewStorage(cacheStore), 230 - clerk: noSessionAuth{}, 231 - locServer: staticLocationLookup{location: &locations.Location{ 224 + s := newTestServer(t, 225 + withTestCache(cacheStore), 226 + withTestClerk(noSessionAuth{}), 227 + withTestLocationServer(staticLocationLookup{location: &locations.Location{ 232 228 ID: "70001001", 233 229 Name: "Test Store", 234 230 ZipCode: "94105", 235 - }}, 236 - } 231 + }}), 232 + ) 237 233 238 234 p := DefaultParams(&locations.Location{ID: "70001001", Name: "Test Store", ZipCode: "94105"}, time.Date(2026, 3, 6, 0, 0, 0, 0, time.FixedZone("PST", -8*60*60))) 239 235 p.Instructions = "make it vegetarian" ··· 273 269 Name: "Test Store", 274 270 ZipCode: "94105", 275 271 } 276 - s := &server{ 277 - recipeio: IO(cacheStore), 278 - storage: storage, 279 - clerk: auth.DefaultMock(), 280 - generator: mock{}, 281 - locServer: staticLocationLookup{location: location}, 282 - } 272 + s := newTestServer(t, 273 + withTestCache(cacheStore), 274 + withTestStorage(storage), 275 + withTestLocationServer(staticLocationLookup{location: location}), 276 + ) 283 277 t.Cleanup(s.Wait) 284 278 285 279 currentUser, err := storage.FindOrCreateFromClerk(t.Context(), "mock-clerk-user-id", auth.DefaultMock()) ··· 337 331 338 332 func TestHandleSingle_NormalizesLegacyOriginHashToCanonicalHash(t *testing.T) { 339 333 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 340 - s := &server{ 341 - recipeio: IO(cacheStore), 342 - storage: users.NewStorage(cacheStore), 343 - clerk: auth.DefaultMock(), 344 - } 334 + s := newTestServer(t, withTestCache(cacheStore)) 345 335 346 336 p := DefaultParams( 347 337 &locations.Location{ID: "70002001", Name: "Canonical Test Store"}, ··· 398 388 399 389 func TestHandleSingle_LegacyOriginHashDoesNotFailWhenParamsMissing(t *testing.T) { 400 390 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 401 - s := &server{ 402 - recipeio: IO(cacheStore), 403 - storage: users.NewStorage(cacheStore), 404 - clerk: auth.DefaultMock(), 405 - } 391 + s := newTestServer(t, withTestCache(cacheStore)) 406 392 407 393 p := DefaultParams( 408 394 &locations.Location{ID: "70002002", Name: "Ignored"}, ··· 447 433 448 434 func TestHandleSingle_IncludesCachedWineRecommendation(t *testing.T) { 449 435 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 450 - s := &server{ 451 - recipeio: IO(cacheStore), 452 - storage: users.NewStorage(cacheStore), 453 - clerk: auth.DefaultMock(), 454 - } 436 + s := newTestServer(t, withTestCache(cacheStore)) 455 437 456 438 p := DefaultParams( 457 439 &locations.Location{ID: "70003001", Name: "Wine Store"}, ··· 524 506 525 507 func TestHandleQuestion_RequiresSignedInUser(t *testing.T) { 526 508 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 527 - s := &server{ 528 - recipeio: IO(cacheStore), 529 - storage: users.NewStorage(cacheStore), 530 - clerk: noSessionAuth{}, 531 - } 509 + s := newTestServer(t, withTestCache(cacheStore), withTestClerk(noSessionAuth{})) 532 510 533 511 form := url.Values{ 534 512 "conversation_id": {"conv-test"}, ··· 549 527 550 528 func TestHandleQuestion_RejectsNonHTMXRequest(t *testing.T) { 551 529 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 552 - s := &server{ 553 - recipeio: IO(cacheStore), 554 - storage: users.NewStorage(cacheStore), 555 - clerk: auth.DefaultMock(), 556 - } 530 + s := newTestServer(t, withTestCache(cacheStore)) 557 531 558 532 form := url.Values{ 559 533 "conversation_id": {"conv-test"}, ··· 629 603 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 630 604 storage := users.NewStorage(cacheStore) 631 605 generator := &captureKickgenerationGenerator{called: make(chan struct{}, 1)} 632 - s := &server{ 633 - recipeio: IO(cacheStore), 634 - clerk: auth.DefaultMock(), 635 - storage: storage, 636 - generator: generator, 637 - } 606 + s := newTestServer(t, 607 + withTestCache(cacheStore), 608 + withTestStorage(storage), 609 + withTestGenerator(generator), 610 + ) 638 611 t.Cleanup(s.Wait) 639 612 640 613 now := time.Now() ··· 737 710 738 711 func TestHandleQuestion_HTMXReturnsThreadFragment(t *testing.T) { 739 712 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 740 - s := &server{ 741 - recipeio: IO(cacheStore), 742 - storage: users.NewStorage(cacheStore), 743 - clerk: auth.DefaultMock(), 744 - generator: &captureQuestionGenerator{}, 745 - } 713 + s := newTestServer(t, 714 + withTestCache(cacheStore), 715 + withTestGenerator(&captureQuestionGenerator{}), 716 + ) 746 717 747 718 form := url.Values{ 748 719 "conversation_id": {"conv-test"}, ··· 776 747 777 748 func TestHandleQuestion_NoSessionHTMXSetsRedirectHeader(t *testing.T) { 778 749 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 779 - s := &server{ 780 - recipeio: IO(cacheStore), 781 - storage: users.NewStorage(cacheStore), 782 - clerk: noSessionAuth{}, 783 - } 750 + s := newTestServer(t, withTestCache(cacheStore), withTestClerk(noSessionAuth{})) 784 751 785 752 form := url.Values{ 786 753 "conversation_id": {"conv-test"}, ··· 805 772 func TestHandleQuestion_PrependsRecipeTitleForModelQuestion(t *testing.T) { 806 773 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 807 774 g := &captureQuestionGenerator{} 808 - s := &server{ 809 - recipeio: IO(cacheStore), 810 - storage: users.NewStorage(cacheStore), 811 - clerk: auth.DefaultMock(), 812 - generator: g, 813 - } 775 + s := newTestServer(t, 776 + withTestCache(cacheStore), 777 + withTestGenerator(g), 778 + ) 814 779 815 780 form := url.Values{ 816 781 "conversation_id": {"conv-test"}, ··· 835 800 836 801 func TestHandleWine_RejectsNonHTMXRequest(t *testing.T) { 837 802 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 838 - s := &server{ 839 - recipeio: IO(cacheStore), 840 - storage: users.NewStorage(cacheStore), 841 - clerk: auth.DefaultMock(), 842 - } 803 + s := newTestServer(t, withTestCache(cacheStore)) 843 804 844 805 req := httptest.NewRequest(http.MethodPost, "/recipe/hash/wine", nil) 845 806 req.SetPathValue("hash", "hash") ··· 854 815 855 816 func TestHandleWine_NoSessionHTMXSetsRedirectHeader(t *testing.T) { 856 817 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 857 - s := &server{ 858 - recipeio: IO(cacheStore), 859 - storage: users.NewStorage(cacheStore), 860 - clerk: noSessionAuth{}, 861 - } 818 + s := newTestServer(t, withTestCache(cacheStore), withTestClerk(noSessionAuth{})) 862 819 863 820 req := httptest.NewRequest(http.MethodPost, "/recipe/hash/wine?view=shopping", nil) 864 821 req.Header.Set("HX-Request", "true") ··· 878 835 func TestHandleWine_HTMXReturnsWineFragment(t *testing.T) { 879 836 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 880 837 g := &captureQuestionGenerator{} 881 - s := &server{ 882 - recipeio: IO(cacheStore), 883 - storage: users.NewStorage(cacheStore), 884 - clerk: auth.DefaultMock(), 885 - generator: g, 886 - } 838 + s := newTestServer(t, 839 + withTestCache(cacheStore), 840 + withTestGenerator(g), 841 + ) 887 842 888 843 p := DefaultParams(&locations.Location{ID: "70003002", Name: "Wine Test Store"}, time.Now()) 889 844 p.ConversationID = "conv-wine" ··· 938 893 func TestHandleWine_ShoppingVariantReturnsShoppingFragment(t *testing.T) { 939 894 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 940 895 g := &captureQuestionGenerator{} 941 - s := &server{ 942 - recipeio: IO(cacheStore), 943 - storage: users.NewStorage(cacheStore), 944 - clerk: auth.DefaultMock(), 945 - generator: g, 946 - } 896 + s := newTestServer(t, 897 + withTestCache(cacheStore), 898 + withTestGenerator(g), 899 + ) 947 900 948 901 p := DefaultParams(&locations.Location{ID: "70003002", Name: "Wine Test Store"}, time.Now()) 949 902 p.ConversationID = "conv-wine" ··· 992 945 func TestHandleWine_UsesCachedWineRecommendation(t *testing.T) { 993 946 cacheStore := cache.NewInMemoryCache() 994 947 g1 := &captureQuestionGenerator{wineRecommendation: "Try a crisp riesling."} 995 - s1 := &server{ 996 - recipeio: IO(cacheStore), 997 - storage: users.NewStorage(cacheStore), 998 - clerk: auth.DefaultMock(), 999 - generator: g1, 1000 - } 948 + s1 := newTestServer(t, 949 + withTestCache(cacheStore), 950 + withTestGenerator(g1), 951 + ) 1001 952 1002 953 p := DefaultParams(&locations.Location{ID: "70003002", Name: "Wine Test Store"}, time.Now()) 1003 954 p.ConversationID = "conv-wine" ··· 1035 986 } 1036 987 1037 988 g2 := &captureQuestionGenerator{panicOnWine: true} 1038 - s2 := &server{ 1039 - recipeio: IO(cacheStore), 1040 - storage: users.NewStorage(cacheStore), 1041 - clerk: auth.DefaultMock(), 1042 - generator: g2, 1043 - } 989 + s2 := newTestServer(t, 990 + withTestCache(cacheStore), 991 + withTestGenerator(g2), 992 + ) 1044 993 1045 994 req2 := httptest.NewRequest(http.MethodPost, "/recipe/"+recipeHash+"/wine", nil) 1046 995 req2.Header.Set("HX-Request", "true") ··· 1062 1011 func TestHandleRecipeImage_ServesCachedImageWithoutGenerator(t *testing.T) { 1063 1012 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1064 1013 g := &captureQuestionGenerator{panicOnImage: true} 1065 - s := &server{ 1066 - recipeio: IO(cacheStore), 1067 - storage: users.NewStorage(cacheStore), 1068 - clerk: auth.DefaultMock(), 1069 - generator: g, 1070 - } 1014 + s := newTestServer(t, 1015 + withTestCache(cacheStore), 1016 + withTestGenerator(g), 1017 + ) 1071 1018 1072 1019 recipe := ai.Recipe{ 1073 1020 Title: "Roast Chicken", ··· 1104 1051 func TestHandleGenerateRecipeImage_GeneratesAndCachesOnMiss(t *testing.T) { 1105 1052 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1106 1053 g := &captureQuestionGenerator{imageBody: []byte("generated-image-bytes")} 1107 - s := &server{ 1108 - recipeio: IO(cacheStore), 1109 - storage: users.NewStorage(cacheStore), 1110 - clerk: auth.DefaultMock(), 1111 - generator: g, 1112 - } 1054 + s := newTestServer(t, 1055 + withTestCache(cacheStore), 1056 + withTestGenerator(g), 1057 + ) 1113 1058 1114 1059 recipe := ai.Recipe{ 1115 1060 Title: "Roast Chicken", ··· 1177 1122 func TestHandleSaveRecipe_SavesRecipeToUserProfile(t *testing.T) { 1178 1123 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1179 1124 storage := users.NewStorage(cacheStore) 1180 - s := &server{ 1181 - recipeio: IO(cacheStore), 1182 - storage: storage, 1183 - clerk: auth.DefaultMock(), 1184 - } 1125 + s := newTestServer(t, 1126 + withTestCache(cacheStore), 1127 + withTestStorage(storage), 1128 + ) 1185 1129 1186 1130 recipe := ai.Recipe{ 1187 1131 Title: "Save Me", ··· 1247 1191 1248 1192 func TestHandleSaveRecipe_NoSessionHTMXSetsRedirectHeader(t *testing.T) { 1249 1193 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1250 - s := &server{ 1251 - recipeio: IO(cacheStore), 1252 - storage: users.NewStorage(cacheStore), 1253 - clerk: noSessionAuth{}, 1254 - } 1194 + s := newTestServer(t, withTestCache(cacheStore), withTestClerk(noSessionAuth{})) 1255 1195 1256 1196 req := httptest.NewRequest(http.MethodPost, "/recipe/hash/save", nil) 1257 1197 req.Header.Set("HX-Request", "true") ··· 1271 1211 func TestHandleSaveRecipe_UsesRequestHashForSelectionKey(t *testing.T) { 1272 1212 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1273 1213 storage := users.NewStorage(cacheStore) 1274 - s := &server{ 1275 - recipeio: IO(cacheStore), 1276 - storage: storage, 1277 - clerk: auth.DefaultMock(), 1278 - } 1214 + s := newTestServer(t, 1215 + withTestCache(cacheStore), 1216 + withTestStorage(storage), 1217 + ) 1279 1218 1280 1219 recipe := ai.Recipe{ 1281 1220 Title: "Save Me", ··· 1327 1266 func TestHandleDismissRecipe_RemovesRecipeFromUserProfile(t *testing.T) { 1328 1267 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1329 1268 storage := users.NewStorage(cacheStore) 1330 - s := &server{ 1331 - recipeio: IO(cacheStore), 1332 - storage: storage, 1333 - clerk: auth.DefaultMock(), 1334 - } 1269 + s := newTestServer(t, 1270 + withTestCache(cacheStore), 1271 + withTestStorage(storage), 1272 + ) 1335 1273 1336 1274 recipe := ai.Recipe{ 1337 1275 Title: "Dismiss Recipe", ··· 1420 1358 1421 1359 func TestHandleDismissRecipe_NoSessionHTMXSetsRedirectHeader(t *testing.T) { 1422 1360 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1423 - s := &server{ 1424 - recipeio: IO(cacheStore), 1425 - storage: users.NewStorage(cacheStore), 1426 - clerk: noSessionAuth{}, 1427 - } 1361 + s := newTestServer(t, withTestCache(cacheStore), withTestClerk(noSessionAuth{})) 1428 1362 1429 1363 req := httptest.NewRequest(http.MethodPost, "/recipe/hash/dismiss", nil) 1430 1364 req.Header.Set("HX-Request", "true") ··· 1444 1378 func TestHandleDismissRecipe_UsesRequestHashForSelectionKey(t *testing.T) { 1445 1379 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1446 1380 storage := users.NewStorage(cacheStore) 1447 - s := &server{ 1448 - recipeio: IO(cacheStore), 1449 - storage: storage, 1450 - clerk: auth.DefaultMock(), 1451 - } 1381 + s := newTestServer(t, 1382 + withTestCache(cacheStore), 1383 + withTestStorage(storage), 1384 + ) 1452 1385 1453 1386 recipe := ai.Recipe{ 1454 1387 Title: "Dismiss Recipe", ··· 1517 1450 func TestHandleRegenerate_UsesServerSideSelectionAndRedirects(t *testing.T) { 1518 1451 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1519 1452 storage := users.NewStorage(cacheStore) 1520 - s := &server{ 1521 - recipeio: IO(cacheStore), 1522 - storage: storage, 1523 - clerk: auth.DefaultMock(), 1524 - generator: mock{}, 1525 - } 1453 + s := newTestServer(t, 1454 + withTestCache(cacheStore), 1455 + withTestStorage(storage), 1456 + ) 1526 1457 t.Cleanup(s.Wait) 1527 1458 1528 1459 p := DefaultParams(&locations.Location{ID: "70004001", Name: "Store"}, time.Now()) ··· 1600 1531 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1601 1532 storage := users.NewStorage(cacheStore) 1602 1533 generator := &captureKickgenerationGenerator{called: make(chan struct{}, 1)} 1603 - s := &server{ 1604 - recipeio: IO(cacheStore), 1605 - storage: storage, 1606 - clerk: auth.DefaultMock(), 1607 - generator: generator, 1608 - } 1534 + s := newTestServer(t, 1535 + withTestCache(cacheStore), 1536 + withTestStorage(storage), 1537 + withTestGenerator(generator), 1538 + ) 1609 1539 t.Cleanup(s.Wait) 1610 1540 1611 1541 alreadySaved := ai.Recipe{Title: "Already Saved", Description: "Saved earlier"} ··· 1670 1600 func TestHandleFinalize_UsesServerSideSelection(t *testing.T) { 1671 1601 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1672 1602 storage := users.NewStorage(cacheStore) 1673 - s := &server{ 1674 - recipeio: IO(cacheStore), 1675 - storage: storage, 1676 - clerk: auth.DefaultMock(), 1677 - } 1603 + s := newTestServer(t, 1604 + withTestCache(cacheStore), 1605 + withTestStorage(storage), 1606 + ) 1678 1607 1679 1608 p := DefaultParams(&locations.Location{ID: "70004001", Name: "Store"}, time.Now()) 1680 1609 p.ConversationID = "conv-123" ··· 1738 1667 1739 1668 func TestParamsForAction_PreservesBaseSelectionWhenSelectionCacheEmpty(t *testing.T) { 1740 1669 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1741 - s := &server{ 1742 - recipeio: IO(cacheStore), 1743 - } 1670 + s := newTestServer(t, withTestCache(cacheStore)) 1744 1671 1745 1672 savedRecipe := ai.Recipe{Title: "Saved Recipe", Description: "Saved"} 1746 1673 dismissedRecipe := ai.Recipe{Title: "Dismissed Recipe", Description: "Dismissed"} ··· 1776 1703 1777 1704 func TestParamsForAction_MergesSelectionAndRemovesOppositeRecipes(t *testing.T) { 1778 1705 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1779 - s := &server{ 1780 - recipeio: IO(cacheStore), 1781 - } 1706 + s := newTestServer(t, withTestCache(cacheStore)) 1782 1707 1783 1708 savedRecipe := ai.Recipe{Title: "Saved Recipe", Description: "Saved"} 1784 1709 dismissedRecipe := ai.Recipe{Title: "Dismissed Recipe", Description: "Dismissed"} ··· 1818 1743 1819 1744 func TestHandleFeedback_CookedButtonSavesCookedState(t *testing.T) { 1820 1745 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1821 - s := &server{ 1822 - recipeio: IO(cacheStore), 1823 - storage: users.NewStorage(cacheStore), 1824 - clerk: auth.DefaultMock(), 1825 - } 1746 + s := newTestServer(t, withTestCache(cacheStore)) 1826 1747 1827 1748 form := url.Values{ 1828 1749 "cooked": {"true"}, ··· 1859 1780 1860 1781 func TestHandleFeedback_SavesStarsAndComment(t *testing.T) { 1861 1782 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1862 - s := &server{ 1863 - recipeio: IO(cacheStore), 1864 - storage: users.NewStorage(cacheStore), 1865 - clerk: auth.DefaultMock(), 1866 - } 1783 + s := newTestServer(t, withTestCache(cacheStore)) 1867 1784 1868 1785 form := url.Values{ 1869 1786 "cooked": {"true"}, ··· 1899 1816 1900 1817 func TestHandleFeedback_NoSessionHTMXSetsRedirectHeader(t *testing.T) { 1901 1818 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1902 - s := &server{ 1903 - recipeio: IO(cacheStore), 1904 - storage: users.NewStorage(cacheStore), 1905 - clerk: noSessionAuth{}, 1906 - } 1819 + s := newTestServer(t, withTestCache(cacheStore), withTestClerk(noSessionAuth{})) 1907 1820 1908 1821 form := url.Values{ 1909 1822 "cooked": {"true"}, ··· 1926 1839 1927 1840 func TestHandleFeedback_InvalidStarsRejected(t *testing.T) { 1928 1841 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1929 - s := &server{ 1930 - recipeio: IO(cacheStore), 1931 - storage: users.NewStorage(cacheStore), 1932 - clerk: auth.DefaultMock(), 1933 - } 1842 + s := newTestServer(t, withTestCache(cacheStore)) 1934 1843 1935 1844 form := url.Values{ 1936 1845 "cooked": {"true"}, ··· 1951 1860 1952 1861 func TestHandleFeedback_RejectsNonHTMXRequest(t *testing.T) { 1953 1862 cacheStore := cache.NewFileCache(filepath.Join(t.TempDir(), "cache")) 1954 - s := &server{ 1955 - recipeio: IO(cacheStore), 1956 - storage: users.NewStorage(cacheStore), 1957 - clerk: auth.DefaultMock(), 1958 - } 1863 + s := newTestServer(t, withTestCache(cacheStore)) 1959 1864 1960 1865 form := url.Values{ 1961 1866 "cooked": {"true"},
+74
internal/recipes/server_test_helpers_test.go
··· 1 + package recipes 2 + 3 + import ( 4 + "path/filepath" 5 + "testing" 6 + 7 + "careme/internal/auth" 8 + "careme/internal/cache" 9 + "careme/internal/config" 10 + "careme/internal/users" 11 + ) 12 + 13 + type testServerConfig struct { 14 + cfg *config.Config 15 + cache cache.ListCache 16 + imageCache cache.Cache 17 + storage *users.Storage 18 + generator generator 19 + locServer locServer 20 + clerk auth.AuthClient 21 + } 22 + 23 + type testServerOption func(*testServerConfig) 24 + 25 + func newTestServer(t testing.TB, opts ...testServerOption) *server { 26 + t.Helper() 27 + 28 + cfg := testServerConfig{ 29 + cache: cache.NewFileCache(filepath.Join(t.TempDir(), "cache")), 30 + generator: mock{}, 31 + clerk: auth.DefaultMock(), 32 + } 33 + for _, opt := range opts { 34 + opt(&cfg) 35 + } 36 + if cfg.imageCache == nil { 37 + cfg.imageCache = cfg.cache 38 + } 39 + if cfg.storage == nil { 40 + cfg.storage = users.NewStorage(cfg.cache) 41 + } 42 + 43 + return NewHandler(cfg.cfg, cfg.storage, cfg.generator, cfg.locServer, cfg.cache, cfg.imageCache, cfg.clerk) 44 + } 45 + 46 + func withTestCache(c cache.ListCache) testServerOption { 47 + return func(cfg *testServerConfig) { 48 + cfg.cache = c 49 + } 50 + } 51 + 52 + func withTestStorage(storage *users.Storage) testServerOption { 53 + return func(cfg *testServerConfig) { 54 + cfg.storage = storage 55 + } 56 + } 57 + 58 + func withTestGenerator(g generator) testServerOption { 59 + return func(cfg *testServerConfig) { 60 + cfg.generator = g 61 + } 62 + } 63 + 64 + func withTestLocationServer(ls locServer) testServerOption { 65 + return func(cfg *testServerConfig) { 66 + cfg.locServer = ls 67 + } 68 + } 69 + 70 + func withTestClerk(clerk auth.AuthClient) testServerOption { 71 + return func(cfg *testServerConfig) { 72 + cfg.clerk = clerk 73 + } 74 + }
+6 -55
internal/recipes/user_profile_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "os" 6 5 "strings" 7 6 "testing" 8 7 "time" 9 8 10 9 "careme/internal/ai" 11 - "careme/internal/cache" 12 - "careme/internal/users" 13 10 utypes "careme/internal/users/types" 14 11 ) 15 12 16 13 func TestSaveRecipesToUserProfile(t *testing.T) { 17 - // Create temporary cache 18 - tmpDir, err := os.MkdirTemp("", "careme-test-user-*") 19 - if err != nil { 20 - t.Fatalf("failed to create temp dir: %v", err) 21 - } 22 - t.Cleanup(func() { 23 - if err := os.RemoveAll(tmpDir); err != nil { 24 - t.Fatalf("failed to remove temp dir: %v", err) 25 - } 26 - }) 27 - 28 - tmpCache := cache.NewFileCache(tmpDir) 29 - storage := users.NewStorage(tmpCache) 14 + srv := newTestServer(t) 15 + storage := srv.storage 30 16 31 17 // Create a test user 32 18 testUser := &utypes.User{ ··· 38 24 } 39 25 if err := storage.Update(testUser); err != nil { 40 26 t.Fatalf("failed to create test user: %v", err) 41 - } 42 - 43 - // Create server instance 44 - srv := &server{ 45 - storage: storage, 46 27 } 47 28 48 29 // Create test recipes ··· 76 57 } 77 58 78 59 func TestSaveRecipesToUserProfile_NoDuplicates(t *testing.T) { 79 - // Create temporary cache 80 - tmpDir, err := os.MkdirTemp("", "careme-test-user-*") 81 - if err != nil { 82 - t.Fatalf("failed to create temp dir: %v", err) 83 - } 84 - t.Cleanup(func() { 85 - if err := os.RemoveAll(tmpDir); err != nil { 86 - t.Fatalf("failed to remove temp dir: %v", err) 87 - } 88 - }) 89 - 90 - tmpCache := cache.NewFileCache(tmpDir) 91 - storage := users.NewStorage(tmpCache) 60 + srv := newTestServer(t) 61 + storage := srv.storage 92 62 93 63 // Try to save the same recipe again (case-insensitive) 94 64 savedRecipe := ai.Recipe{ ··· 112 82 t.Fatalf("failed to create test user: %v", err) 113 83 } 114 84 115 - // Create server instance 116 - srv := &server{ 117 - storage: storage, 118 - } 119 - 120 85 // Save recipes to user profile 121 86 ctx := context.Background() 122 87 if err := srv.saveRecipesToUserProfile(ctx, testUser, savedRecipe); err != nil { ··· 146 111 } 147 112 148 113 func TestRemoveRecipeFromUserProfile(t *testing.T) { 149 - tmpDir, err := os.MkdirTemp("", "careme-test-user-*") 150 - if err != nil { 151 - t.Fatalf("failed to create temp dir: %v", err) 152 - } 153 - t.Cleanup(func() { 154 - if err := os.RemoveAll(tmpDir); err != nil { 155 - t.Fatalf("failed to remove temp dir: %v", err) 156 - } 157 - }) 158 - 159 - tmpCache := cache.NewFileCache(tmpDir) 160 - storage := users.NewStorage(tmpCache) 114 + srv := newTestServer(t) 115 + storage := srv.storage 161 116 162 117 keep := utypes.Recipe{ 163 118 Title: "Keep Recipe", ··· 178 133 } 179 134 if err := storage.Update(testUser); err != nil { 180 135 t.Fatalf("failed to create test user: %v", err) 181 - } 182 - 183 - srv := &server{ 184 - storage: storage, 185 136 } 186 137 187 138 if err := srv.removeRecipeFromUserProfile(context.Background(), *testUser, remove.Hash); err != nil {