| 1 | package analytics |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "runtime" |
| 7 | "sync" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func TestDaySaltIsStableAndRotates(t *testing.T) { |
| 12 | dir := t.TempDir() |
| 13 | a, err := DaySalt(dir, "2026-09-07") |
| 14 | if err != nil || len(a) != 32 { |
| 15 | t.Fatalf("salt: %v len=%d", err, len(a)) |
| 16 | } |
| 17 | b, _ := DaySalt(dir, "2026-09-07") |
| 18 | if string(a) != string(b) { |
| 19 | t.Fatal("same day must return the same salt (restart-stable)") |
| 20 | } |
| 21 | if runtime.GOOS != "windows" { |
| 22 | st, _ := os.Stat(filepath.Join(dir, "analytics-salt-2026-09-07")) |
| 23 | if st.Mode().Perm() != 0o600 { |
| 24 | t.Fatalf("mode %v", st.Mode().Perm()) |
| 25 | } |
| 26 | } |
| 27 | c, _ := DaySalt(dir, "2026-09-08") |
| 28 | if string(c) == string(a) { |
| 29 | t.Fatal("a new day must salt afresh") |
| 30 | } |
| 31 | if _, err := os.Stat(filepath.Join(dir, "analytics-salt-2026-09-07")); !os.IsNotExist(err) { |
| 32 | t.Fatal("rotation must delete the previous day's salt") |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | func TestDaySaltConcurrentCallersAgree(t *testing.T) { |
| 37 | dir := t.TempDir() |
| 38 | const n = 8 |
| 39 | results := make([][]byte, n) |
| 40 | errs := make([]error, n) |
| 41 | var wg sync.WaitGroup |
| 42 | wg.Add(n) |
| 43 | for i := 0; i < n; i++ { |
| 44 | i := i |
| 45 | go func() { |
| 46 | defer wg.Done() |
| 47 | results[i], errs[i] = DaySalt(dir, "2026-09-07") |
| 48 | }() |
| 49 | } |
| 50 | wg.Wait() |
| 51 | for i, err := range errs { |
| 52 | if err != nil { |
| 53 | t.Fatalf("goroutine %d: %v", i, err) |
| 54 | } |
| 55 | if len(results[i]) != 32 { |
| 56 | t.Fatalf("goroutine %d: len=%d", i, len(results[i])) |
| 57 | } |
| 58 | } |
| 59 | for i := 1; i < n; i++ { |
| 60 | if string(results[i]) != string(results[0]) { |
| 61 | t.Fatalf("goroutine %d's salt disagrees with goroutine 0's", i) |
| 62 | } |
| 63 | } |
| 64 | } |
| 65 | |