package store import ( "context" "errors" "testing" "time" ) var listNow = time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC) func TestAddListItemDedupesTheOpenList(t *testing.T) { ctx := context.Background() s := newTestStore(t) first, err := s.AddListItem(ctx, ListItem{Item: "молоко", Source: "tap:voice", CreatedTs: listNow}) if err != nil { t.Fatalf("add: %v", err) } if !first.Created { t.Fatal("the first молоко did not create a row") } again, err := s.AddListItem(ctx, ListItem{Item: " Молоко ", Source: "tap:voice", CreatedTs: listNow}) if err != nil { t.Fatalf("add again: %v", err) } if again.Created { t.Error("молоко was added twice") } if again.ID != first.ID { t.Errorf("second add points at %d; want the existing %d", again.ID, first.ID) } if _, err := s.AddListItem(ctx, ListItem{Item: " "}); !errors.Is(err, ErrListItemEmpty) { t.Errorf("empty item: %v; want ErrListItemEmpty", err) } } // A crossed-off item does not block the next one: buying milk again next week // is a new line, the way saying an errand again is a new task. func TestCrossedOffItemComesBack(t *testing.T) { ctx := context.Background() s := newTestStore(t) first, err := s.AddListItem(ctx, ListItem{Item: "молоко", CreatedTs: listNow}) if err != nil { t.Fatalf("add: %v", err) } if err := s.SetListItemStatus(ctx, first.ID, ListItemDone, listNow); err != nil { t.Fatalf("cross off: %v", err) } next, err := s.AddListItem(ctx, ListItem{Item: "молоко", CreatedTs: listNow.Add(time.Hour)}) if err != nil { t.Fatalf("add after: %v", err) } if !next.Created || next.ID == first.ID { t.Errorf("second молоко reused row %d; want a new one", next.ID) } open, err := s.ListItems(ctx, "", "") if err != nil { t.Fatalf("list: %v", err) } if len(open) != 1 || open[0].ID != next.ID { t.Errorf("open list %+v; want only the new row", open) } } // Lists are separate stores under one table: the same word on two lists is two // items, and reading one never reads the other. func TestListsDoNotSeeEachOther(t *testing.T) { ctx := context.Background() s := newTestStore(t) if _, err := s.AddListItem(ctx, ListItem{List: "покупки", Item: "вода", CreatedTs: listNow}); err != nil { t.Fatalf("add: %v", err) } if _, err := s.AddListItem(ctx, ListItem{List: "Аптека", Item: "вода", CreatedTs: listNow}); err != nil { t.Fatalf("add: %v", err) } for _, c := range []struct{ list, want string }{ {"покупки", "покупки"}, {"аптека", "аптека"}, {"", "покупки"}, } { got, err := s.ListItems(ctx, c.list, "") if err != nil { t.Fatalf("list %q: %v", c.list, err) } if len(got) != 1 { t.Fatalf("list %q has %d items; want 1", c.list, len(got)) } if got[0].List != c.want { t.Errorf("list %q returned tag %q; want %q", c.list, got[0].List, c.want) } } } func TestClearListCrossesOffEverythingOpen(t *testing.T) { ctx := context.Background() s := newTestStore(t) for _, item := range []string{"молоко", "хлеб", "яйца"} { if _, err := s.AddListItem(ctx, ListItem{Item: item, CreatedTs: listNow}); err != nil { t.Fatalf("add %s: %v", item, err) } } if _, err := s.AddListItem(ctx, ListItem{List: "аптека", Item: "бинт", CreatedTs: listNow}); err != nil { t.Fatalf("add: %v", err) } n, err := s.ClearList(ctx, "покупки", listNow) if err != nil { t.Fatalf("clear: %v", err) } if n != 3 { t.Errorf("cleared %d; want 3", n) } left, err := s.ListItems(ctx, "покупки", "") if err != nil { t.Fatalf("list: %v", err) } if len(left) != 0 { t.Errorf("%d items still open; want none", len(left)) } done, err := s.ListItems(ctx, "покупки", ListItemDone) if err != nil { t.Fatalf("list done: %v", err) } if len(done) != 3 || done[0].ResolvedTs == nil { t.Errorf("done list %+v; want 3 rows carrying a resolved time", done) } other, err := s.ListItems(ctx, "аптека", "") if err != nil { t.Fatalf("list: %v", err) } if len(other) != 1 { t.Error("clearing покупки touched аптека") } } func TestSetListItemStatusRejectsWhatIsNotAStatus(t *testing.T) { ctx := context.Background() s := newTestStore(t) if err := s.SetListItemStatus(ctx, 1, "куплено", listNow); !errors.Is(err, ErrListItemStatus) { t.Errorf("bad status: %v; want ErrListItemStatus", err) } if err := s.SetListItemStatus(ctx, 999, ListItemDone, listNow); !errors.Is(err, ErrListItemNotFound) { t.Errorf("missing row: %v; want ErrListItemNotFound", err) } }