From 98e69f6cb6eade5bfdd1c5d8ab03fbc64e495165 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 10 Jun 2026 13:37:16 -0600 Subject: [PATCH] recipes: fetch recipe metadata --- internal/cooked/client.go | 22 +++++++++++++ internal/cooked/client_test.go | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/internal/cooked/client.go b/internal/cooked/client.go index 00e9808c5a1523d1bfa4dda26bc883363acc6fcc..b9898f30ba691eb8c26af081450c9cdf920721ba 100644 --- a/internal/cooked/client.go +++ b/internal/cooked/client.go @@ -63,6 +63,14 @@ type RecipeCard struct { ThumbnailURL string `json:"thumbnail-url,omitempty"` } +// RecipeMetadata is metadata for a Cooked recipe. +type RecipeMetadata struct { + Title string `json:"title"` + ImageURLs []string `json:"image-urls"` + Owner string `json:"owner"` + EditPermission bool `json:"edit-permission"` +} + // NewClient returns a Cooked client with an in-memory cookie jar. func NewClient(baseURL *url.URL, username, password string) (*Client, error) { jar, err := newCookieJar() @@ -113,6 +121,20 @@ func (c *Client) SearchRecipes(ctx context.Context, queryText string, page int) return c.getRecipes(ctx, "/api/user/{username}/recipes/search?"+query.Encode()) } +// ReadRecipeMetadata logs in when needed and returns recipe metadata. +func (c *Client) ReadRecipeMetadata(ctx context.Context, recipeID string) (RecipeMetadata, error) { + var metadata RecipeMetadata + decodeMetadata := func(decoder *json.Decoder) error { + return decoder.Decode(&metadata) + } + path := "/api/recipe/" + url.PathEscape(recipeID) + "/metadata" + if err := c.doAuthenticated(ctx, http.MethodGet, path, nil, decodeMetadata); err != nil { + return RecipeMetadata{}, err + } + + return metadata, nil +} + func (c *Client) getRecipes(ctx context.Context, path string) ([]RecipeCard, error) { var response recipeListResponse decodeRecipes := func(decoder *json.Decoder) error { diff --git a/internal/cooked/client_test.go b/internal/cooked/client_test.go index 7bf34b4701e32492bebecaf922bc61e869053b13..abe4c8601ad91439d8cad34a5d0bca0c1bf5f855 100644 --- a/internal/cooked/client_test.go +++ b/internal/cooked/client_test.go @@ -183,6 +183,64 @@ func TestSearchRecipesACIDRecipesRead2SearchesSavedRecipes(t *testing.T) { } } +func TestReadRecipeMetadataACIDRecipesRead6ReadsMetadata(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.EscapedPath() { + case "/api/public/login": + http.SetCookie(w, &http.Cookie{Name: "cooked_session", Value: "session-value", Path: "/"}) + writeJSON(t, w, loginResponse{Username: "returned-user"}) + case "/api/recipe/recipe%2Fwith%20space/metadata": + if r.Method != http.MethodGet { + t.Fatalf("metadata method = %s, want GET", r.Method) + } + cookie, err := r.Cookie("cooked_session") + if err != nil { + t.Fatalf("missing session cookie: %v", err) + } + if cookie.Value != "session-value" { + t.Fatalf("session cookie = %q, want session-value", cookie.Value) + } + + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write([]byte(`{ + "title": "Pasta with Tomato Sauce", + "image-urls": ["https://example.invalid/pasta.jpg"], + "owner": "returned-user", + "edit-permission": true + }`)); err != nil { + t.Fatalf("write metadata response: %v", err) + } + default: + t.Fatalf("unexpected path %s", r.URL.EscapedPath()) + } + })) + defer server.Close() + + baseURL, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("parse server URL: %v", err) + } + client, err := NewClient(baseURL, "configured-user", "configured-password") + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + + metadata, err := client.ReadRecipeMetadata(context.Background(), "recipe/with space") + if err != nil { + t.Fatalf("ReadRecipeMetadata() error = %v", err) + } + + if metadata.Title != "Pasta with Tomato Sauce" { + t.Fatalf("metadata title = %q, want Pasta with Tomato Sauce", metadata.Title) + } + if len(metadata.ImageURLs) != 1 || metadata.ImageURLs[0] != "https://example.invalid/pasta.jpg" { + t.Fatalf("metadata image URLs = %#v, want one pasta image", metadata.ImageURLs) + } + if metadata.Owner != "returned-user" || !metadata.EditPermission { + t.Fatalf("metadata owner/edit = %q/%v, want returned-user/true", metadata.Owner, metadata.EditPermission) + } +} + func TestUserPathEscapesAuthenticatedUsername(t *testing.T) { client := &Client{authenticatedUsername: "user/name"}