server_test.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
  4
  5package mcp
  6
  7import (
  8	"context"
  9	"testing"
 10
 11	"git.secluded.site/cooked-mcp/internal/cooked"
 12)
 13
 14func TestCallToolACIDToolsSurface1ReadsShoppingList(t *testing.T) {
 15	backend := &fakeBackend{shoppingList: cooked.ShoppingList{
 16		Aisles: []cooked.Aisle{{
 17			ID:   "pantry",
 18			Name: "Pantry",
 19			ProductGroups: []cooked.ProductGroup{{
 20				ID:       "pasta",
 21				Name:     "Pasta",
 22				Quantity: "200g",
 23			}},
 24		}},
 25	}}
 26	server := NewServer(backend, "test")
 27
 28	output, err := server.CallReadTool(context.Background(), ReadArguments{Target: "shopping_list"})
 29	if err != nil {
 30		t.Fatalf("CallReadTool() error = %v", err)
 31	}
 32
 33	if len(output.Aisles) != 1 {
 34		t.Fatalf("structured aisles = %d, want 1", len(output.Aisles))
 35	}
 36	if output.Aisles[0].ProductGroups[0].Name != "Pasta" {
 37		t.Fatalf("first item = %q, want Pasta", output.Aisles[0].ProductGroups[0].Name)
 38	}
 39}
 40
 41func TestCallToolACIDRecipesRead1ListsRecipes(t *testing.T) {
 42	backend := &fakeBackend{recipes: []cooked.RecipeCard{{ID: "recipe-1", Title: "Pasta", ThumbnailURL: "https://example.invalid/thumb.jpg"}}}
 43	server := NewServer(backend, "test")
 44
 45	output, err := server.CallReadTool(context.Background(), ReadArguments{Target: "recipes", Page: 2, Limit: 5})
 46	if err != nil {
 47		t.Fatalf("CallReadTool() error = %v", err)
 48	}
 49
 50	if backend.page != 2 {
 51		t.Fatalf("backend page = %d, want 2", backend.page)
 52	}
 53	if backend.limit != 5 {
 54		t.Fatalf("backend limit = %d, want 5", backend.limit)
 55	}
 56	if len(output.Recipes) != 1 {
 57		t.Fatalf("structured recipes = %d, want 1", len(output.Recipes))
 58	}
 59	if output.Recipes[0].ID != "recipe-1" || output.Recipes[0].Title != "Pasta" {
 60		t.Fatalf("first recipe = %#v, want recipe-1 Pasta", output.Recipes[0])
 61	}
 62}
 63
 64func TestCallToolACIDRecipesPagination4DefaultsAndCapsRecipeLimit(t *testing.T) {
 65	backend := &fakeBackend{}
 66	server := NewServer(backend, "test")
 67
 68	_, err := server.CallReadTool(context.Background(), ReadArguments{Target: "recipes", Limit: 99})
 69	if err != nil {
 70		t.Fatalf("CallReadTool() error = %v", err)
 71	}
 72
 73	if backend.page != 1 {
 74		t.Fatalf("backend page = %d, want default page 1", backend.page)
 75	}
 76	if backend.limit != 30 {
 77		t.Fatalf("backend limit = %d, want capped limit 30", backend.limit)
 78	}
 79}
 80
 81func TestCallToolACIDRecipesRead2SearchesRecipes(t *testing.T) {
 82	backend := &fakeBackend{searchRecipes: []cooked.RecipeCard{
 83		{ID: "recipe-1", Title: "Pasta", ThumbnailURL: "https://example.invalid/thumb.jpg"},
 84		{ID: "recipe-2", Title: "Tomato Pasta", ThumbnailURL: "https://example.invalid/thumb2.jpg"},
 85	}}
 86	server := NewServer(backend, "test")
 87
 88	output, err := server.CallReadTool(context.Background(), ReadArguments{
 89		Target: "recipes",
 90		Query:  " pasta ",
 91		Page:   2,
 92		Limit:  1,
 93	})
 94	if err != nil {
 95		t.Fatalf("CallReadTool() error = %v", err)
 96	}
 97
 98	if backend.searchQuery != "pasta" {
 99		t.Fatalf("backend search query = %q, want pasta", backend.searchQuery)
100	}
101	if backend.searchPage != 2 {
102		t.Fatalf("backend search page = %d, want 2", backend.searchPage)
103	}
104	if len(output.Recipes) != 1 {
105		t.Fatalf("structured recipes = %d, want truncated result length 1", len(output.Recipes))
106	}
107	if output.Recipes[0].ID != "recipe-1" || output.Recipes[0].Title != "Pasta" {
108		t.Fatalf("first recipe = %#v, want recipe-1 Pasta", output.Recipes[0])
109	}
110}
111
112type fakeBackend struct {
113	shoppingList  cooked.ShoppingList
114	recipes       []cooked.RecipeCard
115	searchRecipes []cooked.RecipeCard
116	page          int
117	limit         int
118	searchQuery   string
119	searchPage    int
120}
121
122func (f *fakeBackend) ReadShoppingList(context.Context) (cooked.ShoppingList, error) {
123	return f.shoppingList, nil
124}
125
126func (f *fakeBackend) ListRecipes(_ context.Context, page, limit int) ([]cooked.RecipeCard, error) {
127	f.page = page
128	f.limit = limit
129
130	return f.recipes, nil
131}
132
133func (f *fakeBackend) SearchRecipes(_ context.Context, query string, page int) ([]cooked.RecipeCard, error) {
134	f.searchQuery = query
135	f.searchPage = page
136
137	return f.searchRecipes, nil
138}