package store import ( "errors" "os" "path/filepath" "testing" "time" ) func TestUpdatePersistsAndSurvivesReopen(t *testing.T) { p := filepath.Join(t.TempDir(), "state.json") st, err := Open(p) if err != nil { t.Fatal(err) } if err := st.Update(func(s *State) error { s.Users["u1"] = &User{ID: "u1", Username: "alice"} return nil }); err != nil { t.Fatal(err) } again, err := Open(p) if err != nil { t.Fatal(err) } var name string again.Read(func(s *State) { name = s.Users["u1"].Username }) if name != "alice" { t.Fatalf("reopened username = %q", name) } if fi, _ := os.Stat(p); fi.Mode().Perm() != 0o600 { t.Fatalf("state file mode = %v", fi.Mode().Perm()) } } func TestFailedUpdateLeavesStateUntouched(t *testing.T) { st, err := Open(filepath.Join(t.TempDir(), "state.json")) if err != nil { t.Fatal(err) } boom := errors.New("boom") if err := st.Update(func(s *State) error { s.Users["u1"] = &User{ID: "u1"} return boom }); !errors.Is(err, boom) { t.Fatalf("err = %v", err) } var n int st.Read(func(s *State) { n = len(s.Users) }) if n != 0 { t.Fatalf("users after failed update = %d", n) } } func TestGCDropsExpired(t *testing.T) { st, _ := Open(filepath.Join(t.TempDir(), "state.json")) now := time.Now() _ = st.Update(func(s *State) error { s.Sessions["old"] = &Session{Token: "old", ExpiresAt: now.Add(-time.Hour)} s.Sessions["new"] = &Session{Token: "new", ExpiresAt: now.Add(time.Hour)} s.Invites["spent"] = &Invite{Token: "spent", ExpiresAt: now.Add(time.Hour), MaxUses: 1, Uses: 1} return nil }) if err := st.GC(now); err != nil { t.Fatal(err) } st.Read(func(s *State) { if len(s.Sessions) != 1 || s.Sessions["new"] == nil { t.Fatalf("sessions after GC = %v", s.Sessions) } if len(s.Invites) != 0 { t.Fatalf("invites after GC = %v", s.Invites) } }) }