Files
foodster/cmd/foodster/catalog_test.go
T
Esa Kataja d15f741ab1 Add dish CRUD to the UI, including inline from Kirjaa
The catalog could only be filled by importing JSON, which is a poor way to
add the one dish you are about to eat.

Ruoat now covers PRD §7.3 in full: add and edit mains with their categories
and has_sides, add and edit sides, and delete either. Deletes are soft, so a
log entry keeps resolving the dish it used and the freed name can be reused.
Validation messages are Finnish and the rejected form comes back filled in
rather than blank. Bulk import moves into a details element, since it is now
the occasional path rather than the only one.

Kirjaa gets the same ability without the detour: a search that finds nothing
offers to add what was typed, and saving creates the dish and continues
straight to the sides step. An empty catalog shows the same card instead of
dead-ending on a link to another tab, and the search box is no longer hidden
behind the empty state.

The importer's own insert is gone; it and the UI both go through createMain
and createSide, so duplicate detection lives in one place and reason() can
match on errNameTaken instead of poking at driver strings.
2026-09-05 18:44:09 +03:00

151 lines
3.7 KiB
Go

package main
import (
"errors"
"testing"
)
func TestCreateMain(t *testing.T) {
h := seeded(t)
id, err := createMain(h.db, "Uunikala", []string{"fish"}, true)
if err != nil {
t.Fatalf("createMain: %v", err)
}
dish, err := dishByID(h.db, id)
if err != nil {
t.Fatalf("dishByID: %v", err)
}
if dish.Name != "Uunikala" || !dish.HasSides {
t.Errorf("got %+v", dish)
}
if len(dish.Categories) != 1 || dish.Categories[0] != "fish" {
t.Errorf("categories = %v, want [fish]", dish.Categories)
}
}
func TestCreateMainRejectsDuplicateName(t *testing.T) {
h := seeded(t)
// Seeded bundle already has Lohikeitto; casing must not matter.
_, err := createMain(h.db, "lohikeitto", []string{"fish"}, true)
if !errors.Is(err, errNameTaken) {
t.Errorf("err = %v, want errNameTaken", err)
}
}
func TestUpdateMainReplacesCategories(t *testing.T) {
h := seeded(t)
id := h.mainNamed(t, "Tortillat")
if err := updateMain(h.db, id, "Tortillat", []string{"chicken"}, false); err != nil {
t.Fatalf("updateMain: %v", err)
}
dish, err := dishByID(h.db, id)
if err != nil {
t.Fatalf("dishByID: %v", err)
}
if len(dish.Categories) != 1 || dish.Categories[0] != "chicken" {
t.Errorf("categories = %v, want [chicken]", dish.Categories)
}
if dish.HasSides {
t.Error("has_sides should have been cleared")
}
if got := dish.CategoryKey(); got != "kana" {
t.Errorf("CategoryKey = %q, want kana", got)
}
}
func TestUpdateMainRejectsAnotherDishesName(t *testing.T) {
h := seeded(t)
err := updateMain(h.db, h.mainNamed(t, "Tortillat"), "Lohikeitto", []string{"fish"}, true)
if !errors.Is(err, errNameTaken) {
t.Errorf("err = %v, want errNameTaken", err)
}
}
func TestSideCRUD(t *testing.T) {
h := seeded(t)
if err := createSide(h.db, "Lohkoperunat"); err != nil {
t.Fatalf("createSide: %v", err)
}
if err := createSide(h.db, "lohkoperunat"); !errors.Is(err, errNameTaken) {
t.Errorf("duplicate side err = %v, want errNameTaken", err)
}
id := h.sideNamed(t, "Lohkoperunat")
if err := updateSide(h.db, id, "Lohkoperunat uunista"); err != nil {
t.Fatalf("updateSide: %v", err)
}
side, err := sideByID(h.db, id)
if err != nil {
t.Fatalf("sideByID: %v", err)
}
if side.Name != "Lohkoperunat uunista" {
t.Errorf("name = %q", side.Name)
}
}
func TestSoftDeleteHidesDishButKeepsHistory(t *testing.T) {
h := seeded(t)
id := h.mainNamed(t, "Lohikeitto")
date := day(t, "2026-09-05")
if err := saveEntry(h.db, date, id, nil); err != nil {
t.Fatalf("saveEntry: %v", err)
}
if err := softDeleteMain(h.db, id); err != nil {
t.Fatalf("softDeleteMain: %v", err)
}
// Gone from the pickers...
dishes, err := listDishes(h.db, "")
if err != nil {
t.Fatalf("listDishes: %v", err)
}
for _, d := range dishes {
if d.ID == id {
t.Fatal("soft-deleted dish still appears in the catalog")
}
}
if _, err := dishByID(h.db, id); err == nil {
t.Error("dishByID returned a soft-deleted dish")
}
// ...but the log entry still resolves its name (PRD §6).
entry, err := entryFor(h.db, date)
if err != nil || entry == nil {
t.Fatalf("entryFor: %v, %v", entry, err)
}
if entry.Main.Name != "Lohikeitto" {
t.Errorf("historical name = %q, want Lohikeitto", entry.Main.Name)
}
// And the freed name can be reused.
if _, err := createMain(h.db, "Lohikeitto", []string{"fish"}, true); err != nil {
t.Errorf("name still blocked after soft delete: %v", err)
}
}
func TestSoftDeleteSideHidesItFromPickers(t *testing.T) {
h := seeded(t)
id := h.sideNamed(t, "Riisi")
if err := softDeleteSide(h.db, id); err != nil {
t.Fatalf("softDeleteSide: %v", err)
}
sides, err := listSides(h.db)
if err != nil {
t.Fatalf("listSides: %v", err)
}
for _, s := range sides {
if s.ID == id {
t.Fatal("soft-deleted side still appears")
}
}
}