package store import ( "os" "path/filepath" "testing" ) func settings() Settings { s := Defaults() s.StartDate = "2026-06-20" return s } func TestDayNumber(t *testing.T) { s := settings() cases := []struct { date string want int }{ {"2026-06-19", 0}, // the day before day 1 {"2026-06-20", 1}, // day 1 {"2026-06-21", 2}, // and the day after {"2026-07-31", 42}, // across a month boundary {"2026-09-02", 75}, // the last day {"2026-09-03", 0}, // one past the end {"nonsense", 0}, } for _, c := range cases { if got := s.DayNumber(c.date); got != c.want { t.Errorf("DayNumber(%q) = %d, want %d", c.date, got, c.want) } } } // The clocks go back on 2026-11-01 in America/Los_Angeles. Dates are parsed in // UTC precisely so that the day either side of it still counts as one day. func TestDayNumberAcrossDST(t *testing.T) { s := Defaults() s.StartDate = "2026-10-30" s.Length = 10 for i, date := range []string{"2026-10-30", "2026-10-31", "2026-11-01", "2026-11-02"} { if got, want := s.DayNumber(date), i+1; got != want { t.Errorf("DayNumber(%q) = %d, want %d", date, got, want) } } } func TestMarks(t *testing.T) { s := settings() // protein >= 180, calories <= 2200, water >= 128 full := [3]string{"one", "two", "three"} t.Run("everything hit", func(t *testing.T) { m := s.Marks(Day{Workout: true, Protein: ptr(180), Calories: ptr(2200), Water: ptr(128), Gratitude: full}) if !m.Complete() { t.Fatalf("targets met exactly should score 5, got %d", m.Score()) } }) t.Run("nothing logged is not a perfect calorie day", func(t *testing.T) { m := s.Marks(Day{}) if m.Calories { t.Error("an unlogged calorie count must not count as under the cap") } if m.Score() != 0 { t.Errorf("empty day scored %d, want 0", m.Score()) } }) t.Run("zero is logged", func(t *testing.T) { m := s.Marks(Day{Calories: ptr(0)}) if !m.Calories { t.Error("zero calories is under the cap and should count") } }) t.Run("calories as a floor", func(t *testing.T) { over := s over.CalorieMode = "over" if over.Marks(Day{Calories: ptr(2000)}).Calories { t.Error("2000 is below a 2200 floor and should not count") } if !over.Marks(Day{Calories: ptr(2400)}).Calories { t.Error("2400 clears a 2200 floor and should count") } }) t.Run("gratitude needs all three", func(t *testing.T) { if s.Marks(Day{Gratitude: [3]string{"one", " ", "three"}}).Gratitude { t.Error("a blank line should not count as written") } }) } func TestSaveAndReopen(t *testing.T) { path := filepath.Join(t.TempDir(), "nested", "tracker.json") st, err := Open(path) if err != nil { t.Fatalf("open: %v", err) } if err := st.SaveSettings(settings()); err != nil { t.Fatalf("save settings: %v", err) } day := Day{Date: "2026-07-01", Workout: true, Protein: ptr(190), Gratitude: [3]string{"a", "b", "c"}} if err := st.SaveDay(day); err != nil { t.Fatalf("save day: %v", err) } reopened, err := Open(path) if err != nil { t.Fatalf("reopen: %v", err) } got := reopened.Day("2026-07-01") if !got.Workout || got.Protein == nil || *got.Protein != 190 { t.Fatalf("day did not survive the round trip: %+v", got) } if got.Calories != nil { t.Error("a number that was never entered came back set") } if reopened.Settings().StartDate != "2026-06-20" { t.Error("settings did not survive the round trip") } } func TestSaveEmptyDayRemovesIt(t *testing.T) { path := filepath.Join(t.TempDir(), "tracker.json") st, _ := Open(path) if err := st.SaveDay(Day{Date: "2026-07-01", Workout: true}); err != nil { t.Fatalf("save: %v", err) } if err := st.SaveDay(Day{Date: "2026-07-01"}); err != nil { t.Fatalf("clear: %v", err) } if st.Day("2026-07-01").Logged() { t.Error("a day cleared of everything should not stay in the file") } } // A day handed out must not share memory with the stored one, or editing the // copy would quietly rewrite history. func TestDayIsACopy(t *testing.T) { path := filepath.Join(t.TempDir(), "tracker.json") st, _ := Open(path) st.SaveDay(Day{Date: "2026-07-01", Protein: ptr(190)}) got := st.Day("2026-07-01") *got.Protein = 5 if again := st.Day("2026-07-01"); *again.Protein != 190 { t.Errorf("stored day changed to %d", *again.Protein) } } func TestOpenRefusesUnreadableFile(t *testing.T) { path := filepath.Join(t.TempDir(), "tracker.json") if err := os.WriteFile(path, []byte("{not json"), 0o644); err != nil { t.Fatal(err) } if _, err := Open(path); err == nil { t.Fatal("Open accepted a corrupt file; it would have been overwritten") } } func TestDaysFillsGaps(t *testing.T) { path := filepath.Join(t.TempDir(), "tracker.json") st, _ := Open(path) st.SaveSettings(settings()) st.SaveDay(Day{Date: "2026-06-22", Workout: true}) days := st.Days(settings().Start(), 5) if len(days) != 5 { t.Fatalf("got %d days, want 5", len(days)) } if days[0].Date != "2026-06-20" || days[4].Date != "2026-06-24" { t.Errorf("wrong span: %s..%s", days[0].Date, days[4].Date) } if days[1].Logged() { t.Error("a day never written should come back empty") } if !days[2].Workout { t.Error("the written day is missing from the span") } } func TestImportRejectsJunkDates(t *testing.T) { path := filepath.Join(t.TempDir(), "tracker.json") st, _ := Open(path) if err := st.Import([]byte(`{"days":{"last tuesday":{"workout":true}}}`)); err == nil { t.Fatal("Import accepted a key that is not a date") } } func ptr(n int) *int { return &n }