client_test.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
  4
  5package cooked
  6
  7import (
  8	"context"
  9	"encoding/json"
 10	"net/http"
 11	"net/http/httptest"
 12	"net/url"
 13	"testing"
 14)
 15
 16func TestReadShoppingListACIDAuthenticationLogin3StoresCookies(t *testing.T) {
 17	var sawSessionCookie bool
 18
 19	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 20		switch r.URL.Path {
 21		case "/api/public/login":
 22			if r.Method != http.MethodPost {
 23				t.Fatalf("login method = %s, want POST", r.Method)
 24			}
 25
 26			var request loginRequest
 27			if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
 28				t.Fatalf("decode login request: %v", err)
 29			}
 30			if request.Username != "configured-user" || request.Password != "configured-password" {
 31				t.Fatalf("login request = %#v, want configured credentials", request)
 32			}
 33
 34			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
 35			writeJSON(t, w, loginResponse{Username: "returned-user"})
 36		case "/api/user/returned-user/shopping-list":
 37			cookie, err := r.Cookie("cooked_session")
 38			if err != nil {
 39				t.Fatalf("missing session cookie: %v", err)
 40			}
 41			if cookie.Value != "session-value" {
 42				t.Fatalf("session cookie = %q, want session-value", cookie.Value)
 43			}
 44			sawSessionCookie = true
 45			writeJSON(t, w, shoppingListResponse{
 46				ShoppingList: ShoppingList{
 47					Aisles: []Aisle{{
 48						ID:   "pantry",
 49						Name: "Pantry",
 50						ProductGroups: []ProductGroup{{
 51							ID:       "pasta",
 52							Name:     "Pasta",
 53							Quantity: "200g",
 54						}},
 55					}},
 56				},
 57				Recipes: []string{"recipe-id"},
 58			})
 59		default:
 60			t.Fatalf("unexpected path %s", r.URL.Path)
 61		}
 62	}))
 63	defer server.Close()
 64
 65	baseURL, err := url.Parse(server.URL)
 66	if err != nil {
 67		t.Fatalf("parse server URL: %v", err)
 68	}
 69	client, err := NewClient(baseURL, "configured-user", "configured-password")
 70	if err != nil {
 71		t.Fatalf("NewClient() error = %v", err)
 72	}
 73
 74	shoppingList, err := client.ReadShoppingList(context.Background())
 75	if err != nil {
 76		t.Fatalf("ReadShoppingList() error = %v", err)
 77	}
 78
 79	if !sawSessionCookie {
 80		t.Fatal("shopping-list request did not receive session cookie")
 81	}
 82	if len(shoppingList.Aisles) != 1 || shoppingList.Aisles[0].ProductGroups[0].Name != "Pasta" {
 83		t.Fatalf("shopping list = %#v, want one Pasta item", shoppingList)
 84	}
 85}
 86
 87func TestListRecipesACIDRecipesRead1ListsSavedRecipes(t *testing.T) {
 88	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 89		switch r.URL.Path {
 90		case "/api/public/login":
 91			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
 92			writeJSON(t, w, loginResponse{Username: "returned-user"})
 93		case "/api/user/returned-user/recipes":
 94			if got := r.URL.Query().Get("page"); got != "2" {
 95				t.Fatalf("page query = %q, want 2", got)
 96			}
 97			if got := r.URL.Query().Get("page-count"); got != "5" {
 98				t.Fatalf("page-count query = %q, want 5", got)
 99			}
100			writeJSON(t, w, recipeListResponse{
101				Recipes: []RecipeCard{{
102					ID:           "recipe-1",
103					Title:        "Pasta",
104					ThumbnailURL: "https://example.invalid/thumb.jpg",
105				}},
106			})
107		default:
108			t.Fatalf("unexpected path %s", r.URL.Path)
109		}
110	}))
111	defer server.Close()
112
113	baseURL, err := url.Parse(server.URL)
114	if err != nil {
115		t.Fatalf("parse server URL: %v", err)
116	}
117	client, err := NewClient(baseURL, "configured-user", "configured-password")
118	if err != nil {
119		t.Fatalf("NewClient() error = %v", err)
120	}
121
122	recipes, err := client.ListRecipes(context.Background(), 2, 5)
123	if err != nil {
124		t.Fatalf("ListRecipes() error = %v", err)
125	}
126
127	if len(recipes) != 1 {
128		t.Fatalf("recipes length = %d, want 1", len(recipes))
129	}
130	if recipes[0].ID != "recipe-1" || recipes[0].Title != "Pasta" {
131		t.Fatalf("recipe = %#v, want recipe-1 Pasta", recipes[0])
132	}
133}
134
135func TestSearchRecipesACIDRecipesRead2SearchesSavedRecipes(t *testing.T) {
136	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
137		switch r.URL.Path {
138		case "/api/public/login":
139			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
140			writeJSON(t, w, loginResponse{Username: "returned-user"})
141		case "/api/user/returned-user/recipes/search":
142			if r.Method != http.MethodGet {
143				t.Fatalf("search method = %s, want GET", r.Method)
144			}
145			if got := r.URL.Query().Get("q"); got != "pasta & tomato" {
146				t.Fatalf("q query = %q, want pasta & tomato", got)
147			}
148			if got := r.URL.Query().Get("page"); got != "3" {
149				t.Fatalf("page query = %q, want 3", got)
150			}
151			writeJSON(t, w, recipeListResponse{
152				Recipes: []RecipeCard{{
153					ID:           "recipe-1",
154					Title:        "Pasta",
155					ThumbnailURL: "https://example.invalid/thumb.jpg",
156				}},
157			})
158		default:
159			t.Fatalf("unexpected path %s", r.URL.Path)
160		}
161	}))
162	defer server.Close()
163
164	baseURL, err := url.Parse(server.URL)
165	if err != nil {
166		t.Fatalf("parse server URL: %v", err)
167	}
168	client, err := NewClient(baseURL, "configured-user", "configured-password")
169	if err != nil {
170		t.Fatalf("NewClient() error = %v", err)
171	}
172
173	recipes, err := client.SearchRecipes(context.Background(), "pasta & tomato", 3)
174	if err != nil {
175		t.Fatalf("SearchRecipes() error = %v", err)
176	}
177
178	if len(recipes) != 1 {
179		t.Fatalf("recipes length = %d, want 1", len(recipes))
180	}
181	if recipes[0].ID != "recipe-1" || recipes[0].Title != "Pasta" {
182		t.Fatalf("recipe = %#v, want recipe-1 Pasta", recipes[0])
183	}
184}
185
186func TestReadRecipeMetadataACIDRecipesRead6ReadsMetadata(t *testing.T) {
187	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
188		switch r.URL.EscapedPath() {
189		case "/api/public/login":
190			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
191			writeJSON(t, w, loginResponse{Username: "returned-user"})
192		case "/api/recipe/recipe%2Fwith%20space/metadata":
193			if r.Method != http.MethodGet {
194				t.Fatalf("metadata method = %s, want GET", r.Method)
195			}
196			cookie, err := r.Cookie("cooked_session")
197			if err != nil {
198				t.Fatalf("missing session cookie: %v", err)
199			}
200			if cookie.Value != "session-value" {
201				t.Fatalf("session cookie = %q, want session-value", cookie.Value)
202			}
203
204			w.Header().Set("Content-Type", "application/json")
205			if _, err := w.Write([]byte(`{
206				"title": "Pasta with Tomato Sauce",
207				"image-urls": ["https://example.invalid/pasta.jpg"],
208				"owner": "returned-user",
209				"edit-permission": true
210			}`)); err != nil {
211				t.Fatalf("write metadata response: %v", err)
212			}
213		default:
214			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
215		}
216	}))
217	defer server.Close()
218
219	baseURL, err := url.Parse(server.URL)
220	if err != nil {
221		t.Fatalf("parse server URL: %v", err)
222	}
223	client, err := NewClient(baseURL, "configured-user", "configured-password")
224	if err != nil {
225		t.Fatalf("NewClient() error = %v", err)
226	}
227
228	metadata, err := client.ReadRecipeMetadata(context.Background(), "recipe/with space")
229	if err != nil {
230		t.Fatalf("ReadRecipeMetadata() error = %v", err)
231	}
232
233	if metadata.Title != "Pasta with Tomato Sauce" {
234		t.Fatalf("metadata title = %q, want Pasta with Tomato Sauce", metadata.Title)
235	}
236	if len(metadata.ImageURLs) != 1 || metadata.ImageURLs[0] != "https://example.invalid/pasta.jpg" {
237		t.Fatalf("metadata image URLs = %#v, want one pasta image", metadata.ImageURLs)
238	}
239	if metadata.Owner != "returned-user" || !metadata.EditPermission {
240		t.Fatalf("metadata owner/edit = %q/%v, want returned-user/true", metadata.Owner, metadata.EditPermission)
241	}
242}
243
244func TestReadRecipeContentACIDRecipesRead7And8ReadsContentAndPortions(t *testing.T) {
245	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
246		switch r.URL.EscapedPath() {
247		case "/api/public/login":
248			http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"})
249			writeJSON(t, w, loginResponse{Username: "returned-user"})
250		case "/api/recipe/recipe%2Fwith%20space/content":
251			if r.Method != http.MethodGet {
252				t.Fatalf("content method = %s, want GET", r.Method)
253			}
254			cookie, err := r.Cookie("cooked_session")
255			if err != nil {
256				t.Fatalf("missing session cookie: %v", err)
257			}
258			if cookie.Value != "session-value" {
259				t.Fatalf("session cookie = %q, want session-value", cookie.Value)
260			}
261
262			writeJSON(t, w, RecipeContent{
263				Content:  "# Pasta\n\n- 200g pasta\n\n1. Boil pasta.",
264				Portions: 2,
265			})
266		default:
267			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
268		}
269	}))
270	defer server.Close()
271
272	baseURL, err := url.Parse(server.URL)
273	if err != nil {
274		t.Fatalf("parse server URL: %v", err)
275	}
276	client, err := NewClient(baseURL, "configured-user", "configured-password")
277	if err != nil {
278		t.Fatalf("NewClient() error = %v", err)
279	}
280
281	content, err := client.ReadRecipeContent(context.Background(), "recipe/with space")
282	if err != nil {
283		t.Fatalf("ReadRecipeContent() error = %v", err)
284	}
285
286	if content.Content != "# Pasta\n\n- 200g pasta\n\n1. Boil pasta." {
287		t.Fatalf("content = %q, want recipe markdown", content.Content)
288	}
289	if content.Portions != 2 {
290		t.Fatalf("portions = %d, want 2", content.Portions)
291	}
292}
293
294func TestUserPathEscapesAuthenticatedUsername(t *testing.T) {
295	client := &Client{authenticatedUsername: "user/name"}
296
297	got := client.userPath("/api/user/{username}/shopping-list")
298	if got != "/api/user/user%2Fname/shopping-list" {
299		t.Fatalf("userPath() = %q, want escaped username", got)
300	}
301}
302
303func TestNewRequestPreservesEscapedUserPathWithQuery(t *testing.T) {
304	baseURL, err := url.Parse("https://example.invalid")
305	if err != nil {
306		t.Fatalf("parse base URL: %v", err)
307	}
308	client := &Client{baseURL: baseURL}
309
310	request, err := client.newRequest(context.Background(), http.MethodGet, "/api/user/user%2Fname/recipes?page=2&page-count=5", nil)
311	if err != nil {
312		t.Fatalf("newRequest() error = %v", err)
313	}
314
315	if request.URL.String() != "https://example.invalid/api/user/user%2Fname/recipes?page=2&page-count=5" {
316		t.Fatalf("request URL = %q, want escaped username preserved", request.URL.String())
317	}
318}
319
320func writeJSON[T loginResponse | shoppingListResponse | recipeListResponse | RecipeContent](t *testing.T, w http.ResponseWriter, value T) {
321	t.Helper()
322
323	w.Header().Set("Content-Type", "application/json")
324	if err := json.NewEncoder(w).Encode(value); err != nil {
325		t.Fatalf("write JSON: %v", err)
326	}
327}