Files
foodster/cmd/foodster/catalog_test.go
T
KessinenandEsa Kataja 174652778b Release: leftovers, the real dish list, and a day list that stays put (#2)
Tähteet — leftovers. Not a dish: it exists so a day can be recorded as "we ate what was already there" without inventing a meal nobody cooked. A special-flagged row created by migration 0002; never in the catalog, never editable, and excluded from the stage-2 suggester outright. It gets its own mark, a grey lidded tub — which exposed a bug where no categories drew the same icon as several.

The real dish list. The seed bundle is now your December 2024 list: 37 mains, 9 sides. Letut and Pannari dropped as not-dinners, Kanakintut folded into Broilerin koipireidet, has_sides assigned by rule.

The day list stays put. Choosing a day used to swap a panel in above the list and drop that day out of it, so rows below jumped up under the tap. The list is now the page; the selected day expands where it sits, with anchors so the viewport lands on the day rather than the top.

Release pipeline fixed. The last release silently pushed the previous image: release declared image and push as prerequisites and your make runs -j16, so they raced. Also, push re-derived the tag by date-sorting, which is ambiguous when two tags share a commit. It now reads what image recorded, and verifies afterwards that the registry serves what was built.

Co-authored-by: Esa Kataja <[email protected]>
Reviewed-on: #2
2026-09-05 20:24:02 +00:00

200 lines
5.2 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 TestTahteetIsLoggableButNotFood(t *testing.T) {
h := seeded(t)
// The migration creates it; nobody adds it.
special, err := listSpecial(h.db, "")
if err != nil {
t.Fatalf("listSpecial: %v", err)
}
if len(special) != 1 || special[0].Name != "Tähteet" {
t.Fatalf("special = %+v, want exactly Tähteet", special)
}
// It must not turn up among the dishes: not on the board's categories,
// not in the catalog, and not in whatever the suggester later draws from.
dishes, err := listDishes(h.db, "")
if err != nil {
t.Fatalf("listDishes: %v", err)
}
for _, d := range dishes {
if d.Name == "Tähteet" {
t.Fatal("Tähteet appears among the dishes")
}
}
// It carries no category at all, which is why it cannot be an ordinary
// dish: those are required to have one.
if len(special[0].Categories) != 0 {
t.Errorf("categories = %v, want none", special[0].Categories)
}
// And it gets its own mark: no categories is not the same as several, so
// it must not fall through to the mixed Sekalaiset one.
if got := special[0].CategoryKey(); got != "tahteet" {
t.Errorf("CategoryKey = %q, want tahteet", got)
}
// Logging it has to work exactly like logging a real meal.
date := day(t, "2026-09-05")
if err := saveEntry(h.db, date, special[0].ID, nil); err != nil {
t.Fatalf("saveEntry: %v", err)
}
entry, err := entryFor(h.db, date)
if err != nil || entry == nil {
t.Fatalf("entryFor: %v, %v", entry, err)
}
if entry.Main.Name != "Tähteet" {
t.Errorf("logged %q, want Tähteet", entry.Main.Name)
}
}
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")
}
}
}