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 TestUserPathEscapesAuthenticatedUsername(t *testing.T) {
245 client := &Client{authenticatedUsername: "user/name"}
246
247 got := client.userPath("/api/user/{username}/shopping-list")
248 if got != "/api/user/user%2Fname/shopping-list" {
249 t.Fatalf("userPath() = %q, want escaped username", got)
250 }
251}
252
253func TestNewRequestPreservesEscapedUserPathWithQuery(t *testing.T) {
254 baseURL, err := url.Parse("https://example.invalid")
255 if err != nil {
256 t.Fatalf("parse base URL: %v", err)
257 }
258 client := &Client{baseURL: baseURL}
259
260 request, err := client.newRequest(context.Background(), http.MethodGet, "/api/user/user%2Fname/recipes?page=2&page-count=5", nil)
261 if err != nil {
262 t.Fatalf("newRequest() error = %v", err)
263 }
264
265 if request.URL.String() != "https://example.invalid/api/user/user%2Fname/recipes?page=2&page-count=5" {
266 t.Fatalf("request URL = %q, want escaped username preserved", request.URL.String())
267 }
268}
269
270func writeJSON[T loginResponse | shoppingListResponse | recipeListResponse](t *testing.T, w http.ResponseWriter, value T) {
271 t.Helper()
272
273 w.Header().Set("Content-Type", "application/json")
274 if err := json.NewEncoder(w).Encode(value); err != nil {
275 t.Fatalf("write JSON: %v", err)
276 }
277}