recipes: fetch recipe metadata

Amolith created

Change summary

internal/cooked/client.go      | 22 +++++++++++++
internal/cooked/client_test.go | 58 ++++++++++++++++++++++++++++++++++++
2 files changed, 80 insertions(+)

Detailed changes

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 {

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"}