From 47da09d9a8241b3fd01474d1ed8e865cb2b08cbc Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 10 Jun 2026 14:27:38 -0600 Subject: [PATCH] recipes: preview recipe text client --- internal/cooked/client.go | 30 +++++++++++++++++ internal/cooked/client_test.go | 61 +++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/internal/cooked/client.go b/internal/cooked/client.go index c09907ecb6f90cd877c0cf043979a4bbaf55981b..6a773ec55cd08c239d7c073b3ff93aabee140f7f 100644 --- a/internal/cooked/client.go +++ b/internal/cooked/client.go @@ -77,6 +77,13 @@ type RecipeContent struct { Portions int `json:"portions"` } +// RecipeTextPreview is Cooked's preview of raw recipe text before saving. +type RecipeTextPreview struct { + Title string `json:"title"` + Markdown string `json:"markdown"` + Portions int `json:"portions"` +} + // NewClient returns a Cooked client with an in-memory cookie jar. func NewClient(baseURL *url.URL, username, password string) (*Client, error) { jar, err := newCookieJar() @@ -161,6 +168,24 @@ func (c *Client) ReadRecipeContent(ctx context.Context, recipeID string) (Recipe return content, nil } +// PreviewRecipeText logs in when needed and previews raw recipe text without saving it. +func (c *Client) PreviewRecipeText(ctx context.Context, title, text string) (RecipeTextPreview, error) { + body, err := json.Marshal(previewRecipeTextRequest{RecipeName: title, RecipeText: text}) + if err != nil { + return RecipeTextPreview{}, fmt.Errorf("encode Cooked recipe text preview request: %w", err) + } + + var preview RecipeTextPreview + decodePreview := func(decoder *json.Decoder) error { + return decoder.Decode(&preview) + } + if err := c.doAuthenticated(ctx, http.MethodPost, "/api/new/from-text/preview", body, decodePreview); err != nil { + return RecipeTextPreview{}, err + } + + return preview, nil +} + func (c *Client) getRecipes(ctx context.Context, path string) ([]RecipeCard, error) { var response recipeListResponse decodeRecipes := func(decoder *json.Decoder) error { @@ -305,6 +330,11 @@ type recipeListResponse struct { Recipes []RecipeCard `json:"recipes"` } +type previewRecipeTextRequest struct { + RecipeName string `json:"recipe-name"` + RecipeText string `json:"recipe-text"` +} + type loginRequest struct { Username string `json:"username"` Password string `json:"password"` diff --git a/internal/cooked/client_test.go b/internal/cooked/client_test.go index d6544d55ff358d6e085622f6bca7cd50d09914e5..2632fc92b521a566424839c40be36b124e7d4ea2 100644 --- a/internal/cooked/client_test.go +++ b/internal/cooked/client_test.go @@ -102,6 +102,30 @@ func TestReadRecipeContentACIDRecipesRead7And8ReadsContentAndPortions(t *testing } } +func TestPreviewRecipeTextACIDRecipesPreviewText1To4PreviewsWithoutSaving(t *testing.T) { + var previewCalls int + client, closeServer := newTestClient(t, previewRecipeTextTestHandler(t, &previewCalls)) + defer closeServer() + + preview, err := client.PreviewRecipeText(context.Background(), "Pasta", "Boil pasta.") + if err != nil { + t.Fatalf("PreviewRecipeText() error = %v", err) + } + + if previewCalls != 1 { + t.Fatalf("preview calls = %d, want 1", previewCalls) + } + if preview.Title != "Pasta with Tomato Sauce" { + t.Fatalf("preview title = %q, want Pasta with Tomato Sauce", preview.Title) + } + if preview.Markdown != "# Pasta\n\n1. Boil pasta." { + t.Fatalf("preview markdown = %q, want recipe markdown", preview.Markdown) + } + if preview.Portions != 2 { + t.Fatalf("preview portions = %d, want 2", preview.Portions) + } +} + func TestUserPathEscapesAuthenticatedUsername(t *testing.T) { client := &Client{authenticatedUsername: "user/name"} @@ -238,6 +262,41 @@ func recipeContentTestHandler(t *testing.T) http.HandlerFunc { } } +func previewRecipeTextTestHandler(t *testing.T, previewCalls *int) http.HandlerFunc { + t.Helper() + + return func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/public/login": + writeLoginResponse(t, w) + case "/api/new/from-text/preview": + requireMethod(t, r, http.MethodPost, "preview") + requireSessionCookie(t, r) + requirePreviewRecipeTextRequest(t, r) + (*previewCalls)++ + writeJSON(t, w, RecipeTextPreview{ + Title: "Pasta with Tomato Sauce", + Markdown: "# Pasta\n\n1. Boil pasta.", + Portions: 2, + }) + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + } +} + +func requirePreviewRecipeTextRequest(t *testing.T, r *http.Request) { + t.Helper() + + var request previewRecipeTextRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Fatalf("decode preview request: %v", err) + } + if request.RecipeName != "Pasta" || request.RecipeText != "Boil pasta." { + t.Fatalf("preview request = %#v, want configured title/text", request) + } +} + func requireLoginRequest(t *testing.T, r *http.Request) { t.Helper() requireMethod(t, r, http.MethodPost, "login") @@ -331,7 +390,7 @@ func writeRecipeMetadataResponse(t *testing.T, w http.ResponseWriter) { } } -func writeJSON[T loginResponse | shoppingListResponse | recipeListResponse | RecipeContent]( +func writeJSON[T loginResponse | shoppingListResponse | recipeListResponse | RecipeContent | RecipeTextPreview]( t *testing.T, w http.ResponseWriter, value T,