diff --git a/internal/cooked/client.go b/internal/cooked/client.go index b9898f30ba691eb8c26af081450c9cdf920721ba..b06e697ded7d517c4b1310e4fa899c39498aacc6 100644 --- a/internal/cooked/client.go +++ b/internal/cooked/client.go @@ -71,6 +71,12 @@ type RecipeMetadata struct { EditPermission bool `json:"edit-permission"` } +// RecipeContent is the Markdown-style body and portions for a Cooked recipe. +type RecipeContent struct { + Content string `json:"content"` + 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() @@ -135,6 +141,20 @@ func (c *Client) ReadRecipeMetadata(ctx context.Context, recipeID string) (Recip return metadata, nil } +// ReadRecipeContent logs in when needed and returns recipe content and portions. +func (c *Client) ReadRecipeContent(ctx context.Context, recipeID string) (RecipeContent, error) { + var content RecipeContent + decodeContent := func(decoder *json.Decoder) error { + return decoder.Decode(&content) + } + path := "/api/recipe/" + url.PathEscape(recipeID) + "/content" + if err := c.doAuthenticated(ctx, http.MethodGet, path, nil, decodeContent); err != nil { + return RecipeContent{}, err + } + + return content, 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 abe4c8601ad91439d8cad34a5d0bca0c1bf5f855..f1e1925271b4b0e9c6025cc81e64c4a572e072dc 100644 --- a/internal/cooked/client_test.go +++ b/internal/cooked/client_test.go @@ -241,6 +241,56 @@ func TestReadRecipeMetadataACIDRecipesRead6ReadsMetadata(t *testing.T) { } } +func TestReadRecipeContentACIDRecipesRead7And8ReadsContentAndPortions(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/content": + if r.Method != http.MethodGet { + t.Fatalf("content 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) + } + + writeJSON(t, w, RecipeContent{ + Content: "# Pasta\n\n- 200g pasta\n\n1. Boil pasta.", + Portions: 2, + }) + 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) + } + + content, err := client.ReadRecipeContent(context.Background(), "recipe/with space") + if err != nil { + t.Fatalf("ReadRecipeContent() error = %v", err) + } + + if content.Content != "# Pasta\n\n- 200g pasta\n\n1. Boil pasta." { + t.Fatalf("content = %q, want recipe markdown", content.Content) + } + if content.Portions != 2 { + t.Fatalf("portions = %d, want 2", content.Portions) + } +} + func TestUserPathEscapesAuthenticatedUsername(t *testing.T) { client := &Client{authenticatedUsername: "user/name"} @@ -267,7 +317,7 @@ func TestNewRequestPreservesEscapedUserPathWithQuery(t *testing.T) { } } -func writeJSON[T loginResponse | shoppingListResponse | recipeListResponse](t *testing.T, w http.ResponseWriter, value T) { +func writeJSON[T loginResponse | shoppingListResponse | recipeListResponse | RecipeContent](t *testing.T, w http.ResponseWriter, value T) { t.Helper() w.Header().Set("Content-Type", "application/json")