Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m44s
The directory, the repo and the hostname are all fit; the module path, the image, the namespace and the data file were still tracker. Nothing is deployed yet, so this is free now and would not be later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
200 lines
5.4 KiB
Go
200 lines
5.4 KiB
Go
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", "fit.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(), "fit.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(), "fit.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(), "fit.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(), "fit.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(), "fit.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 }
|