client_recipe_validation_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	"net/http"
 10	"strings"
 11	"testing"
 12)
 13
 14func TestListRecipesACIDAPIClientResponses1AcceptsEmptyRecipesList(t *testing.T) {
 15	client, closeServer := newTestClient(t, rawRecipeListTestHandler(
 16		t,
 17		"/api/user/returned-user/recipes",
 18		`[]`,
 19	))
 20	defer closeServer()
 21
 22	recipes, err := client.ListRecipes(context.Background(), 1, 10)
 23	if err != nil {
 24		t.Fatalf("ListRecipes() error = %v", err)
 25	}
 26	if len(recipes) != 0 {
 27		t.Fatalf("recipes length = %d, want 0", len(recipes))
 28	}
 29}
 30
 31func TestListRecipesACIDAPIClientResponses1RejectsNullRecipesList(t *testing.T) {
 32	client, closeServer := newTestClient(t, rawRecipeListTestHandler(
 33		t,
 34		"/api/user/returned-user/recipes",
 35		`null`,
 36	))
 37	defer closeServer()
 38
 39	_, err := client.ListRecipes(context.Background(), 1, 10)
 40	if err == nil {
 41		t.Fatal("ListRecipes() error = nil, want missing recipes error")
 42	}
 43}
 44
 45func TestSearchRecipesACIDAPIClientResponses1RejectsRecipeCardsWithoutIDOrTitle(t *testing.T) {
 46	tests := []struct {
 47		name string
 48		body string
 49	}{
 50		{name: "missing id", body: `[{"title":"Pasta"}]`},
 51		{name: "blank title", body: `[{"id":"recipe-1","title":"   "}]`},
 52	}
 53
 54	for _, tt := range tests {
 55		t.Run(tt.name, func(t *testing.T) {
 56			client, closeServer := newTestClient(t, rawRecipeListTestHandler(
 57				t,
 58				"/api/user/returned-user/recipes/search",
 59				tt.body,
 60			))
 61			defer closeServer()
 62
 63			_, err := client.SearchRecipes(context.Background(), "pasta", 1)
 64			if err == nil {
 65				t.Fatal("SearchRecipes() error = nil, want invalid recipe card error")
 66			}
 67		})
 68	}
 69}
 70
 71// api-client.RESPONSES.1 recipes.SAVE.7 recipes.SAVE.9
 72func TestSaveRecipeDraftACIDAPIClientResponses1RejectsMissingRecipeID(t *testing.T) {
 73	var saveCalls int
 74	client, closeServer := newTestClient(t, rawSaveRecipeDraftTestHandler(t, &saveCalls, `{}`))
 75	defer closeServer()
 76
 77	_, err := client.SaveRecipeDraft(
 78		context.Background(),
 79		"draft/with space",
 80		"# Pasta\n\n1. Boil pasta.",
 81		4,
 82	)
 83	if err == nil {
 84		t.Fatal("SaveRecipeDraft() error = nil, want missing recipe ID error")
 85	}
 86	if !strings.Contains(err.Error(), "missing recipe ID") {
 87		t.Fatalf("SaveRecipeDraft() error = %v, want missing recipe ID error", err)
 88	}
 89	if saveCalls != 1 {
 90		t.Fatalf("save calls = %d, want 1", saveCalls)
 91	}
 92}
 93
 94// api-client.RESPONSES.1 recipes.SAVE.2 recipes.SAVE.9
 95func TestSavePreparedRecipeACIDAPIClientResponses1RejectsMissingOrBlankRecipeID(t *testing.T) {
 96	tests := []struct {
 97		name string
 98		body string
 99	}{
100		{name: "missing recipe ID", body: `{}`},
101		{name: "blank recipe ID", body: `{"recipe-id":"   "}`},
102	}
103
104	for _, tt := range tests {
105		t.Run(tt.name, func(t *testing.T) {
106			var saveCalls int
107			client, closeServer := newTestClient(t, rawSavePreparedRecipeTestHandler(t, &saveCalls, tt.body))
108			defer closeServer()
109
110			_, err := client.SavePreparedRecipe(
111				context.Background(),
112				"Pasta",
113				"# Pasta\n\n1. Boil pasta.",
114				2.5,
115			)
116			if err == nil {
117				t.Fatal("SavePreparedRecipe() error = nil, want missing recipe ID error")
118			}
119			if !strings.Contains(err.Error(), "missing recipe ID") {
120				t.Fatalf("SavePreparedRecipe() error = %v, want missing recipe ID error", err)
121			}
122			if saveCalls != 1 {
123				t.Fatalf("save calls = %d, want 1", saveCalls)
124			}
125		})
126	}
127}
128
129// api-client.RESPONSES.1 recipes.READ.8-1
130func TestReadRecipeContentACIDAPIClientResponses1AndRecipesRead8_1PreservesFractionalPortions(t *testing.T) {
131	var contentCalls int
132	client, closeServer := newTestClient(t, rawRecipeContentTestHandler(
133		t,
134		&contentCalls,
135		`{"content":"# Pasta","portions":1.5}`,
136	))
137	defer closeServer()
138
139	content, err := client.ReadRecipeContent(context.Background(), "recipe/with space")
140	if err != nil {
141		t.Fatalf("ReadRecipeContent() error = %v", err)
142	}
143	if contentCalls != 1 {
144		t.Fatalf("content calls = %d, want 1", contentCalls)
145	}
146	if content.Portions != 1.5 {
147		t.Fatalf("content portions = %g, want 1.5", content.Portions)
148	}
149}
150
151// api-client.RESPONSES.1 recipes.READ.7 recipes.READ.8
152func TestReadRecipeContentACIDAPIClientResponses1RejectsMissingContentOrPortions(t *testing.T) {
153	tests := []struct {
154		name    string
155		body    string
156		wantErr string
157	}{
158		{name: "missing content", body: `{"portions":2}`, wantErr: "missing content"},
159		{name: "blank content", body: `{"content":"   ","portions":2}`, wantErr: "missing content"},
160		{name: "missing portions", body: `{"content":"# Pasta"}`, wantErr: "missing or invalid portions"},
161		{name: "zero portions", body: `{"content":"# Pasta","portions":0}`, wantErr: "missing or invalid portions"},
162	}
163
164	for _, tt := range tests {
165		t.Run(tt.name, func(t *testing.T) {
166			var contentCalls int
167			client, closeServer := newTestClient(t, rawRecipeContentTestHandler(t, &contentCalls, tt.body))
168			defer closeServer()
169
170			_, err := client.ReadRecipeContent(context.Background(), "recipe/with space")
171			if err == nil {
172				t.Fatal("ReadRecipeContent() error = nil, want invalid recipe content error")
173			}
174			if !strings.Contains(err.Error(), tt.wantErr) {
175				t.Fatalf("ReadRecipeContent() error = %v, want %q", err, tt.wantErr)
176			}
177			if contentCalls != 1 {
178				t.Fatalf("content calls = %d, want 1", contentCalls)
179			}
180		})
181	}
182}
183
184func rawRecipeListTestHandler(t *testing.T, path, body string) http.HandlerFunc {
185	t.Helper()
186
187	return func(w http.ResponseWriter, r *http.Request) {
188		switch r.URL.Path {
189		case "/api/public/login":
190			writeLoginResponse(t, w)
191		case path:
192			w.Header().Set("Content-Type", "application/json")
193			if _, err := w.Write([]byte(body)); err != nil {
194				t.Fatalf("write raw recipe list response: %v", err)
195			}
196		default:
197			t.Fatalf("unexpected path %s", r.URL.Path)
198		}
199	}
200}
201
202func rawRecipeContentTestHandler(t *testing.T, contentCalls *int, body string) http.HandlerFunc {
203	t.Helper()
204
205	return func(w http.ResponseWriter, r *http.Request) {
206		switch r.URL.EscapedPath() {
207		case "/api/public/login":
208			writeLoginResponse(t, w)
209		case "/api/recipe/recipe%2Fwith%20space/content":
210			requireMethod(t, r, http.MethodGet, "content")
211			requireSessionCookie(t, r)
212			(*contentCalls)++
213			w.Header().Set("Content-Type", "application/json")
214			if _, err := w.Write([]byte(body)); err != nil {
215				t.Fatalf("write raw recipe content response: %v", err)
216			}
217		default:
218			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
219		}
220	}
221}
222
223func rawSavePreparedRecipeTestHandler(t *testing.T, saveCalls *int, body string) http.HandlerFunc {
224	t.Helper()
225
226	return func(w http.ResponseWriter, r *http.Request) {
227		switch r.URL.Path {
228		case "/api/public/login":
229			writeLoginResponse(t, w)
230		case "/api/recipe/import/save":
231			requireMethod(t, r, http.MethodPost, "save prepared recipe")
232			requireSessionCookie(t, r)
233			requireSavePreparedRecipeRequest(t, r)
234			(*saveCalls)++
235			w.Header().Set("Content-Type", "application/json")
236			if _, err := w.Write([]byte(body)); err != nil {
237				t.Fatalf("write raw save prepared response: %v", err)
238			}
239		default:
240			t.Fatalf("unexpected path %s", r.URL.Path)
241		}
242	}
243}
244
245func rawSaveRecipeDraftTestHandler(t *testing.T, saveCalls *int, body string) http.HandlerFunc {
246	t.Helper()
247
248	return func(w http.ResponseWriter, r *http.Request) {
249		switch r.URL.EscapedPath() {
250		case "/api/public/login":
251			writeLoginResponse(t, w)
252		case "/api/extract/draft%2Fwith%20space/save":
253			requireMethod(t, r, http.MethodPost, "save recipe draft")
254			requireSessionCookie(t, r)
255			requireRecipeContentRequest(t, r, "draft save")
256			(*saveCalls)++
257			w.Header().Set("Content-Type", "application/json")
258			if _, err := w.Write([]byte(body)); err != nil {
259				t.Fatalf("write raw save draft response: %v", err)
260			}
261		default:
262			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
263		}
264	}
265}