// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package runs import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "testing" "time" ) func doc(id, anon string) []byte { return []byte(fmt.Sprintf( `{"run":{"id":%q,"started_at":"2026-08-01T10:00:00Z","privacy":{"anonymization":%q}},`+ `"findings":[{"id":"f1"},{"id":"f2"}],"summary":{"verdict":"warn"}}`, id, anon)) } func open(t *testing.T, p Policy) (*Store, string) { t.Helper() dir := t.TempDir() s, err := Open(dir, p) if err != nil { t.Fatalf("open: %v", err) } return s, dir } func TestModeOffRefusesEverything(t *testing.T) { p := DefaultPolicy() p.Mode = ModeOff s, _ := open(t, p) if _, err := s.Put("dev1", doc("run-1", AnonFull), false); !errors.Is(err, ErrDisabled) { t.Fatalf("want ErrDisabled, got %v", err) } } // ModeAccount turns on whether the *caller* has signed in, and nothing else. A device that has // not is refused with a reason it can act on; one that has is treated exactly like anonymous mode. func TestModeAccountTurnsOnWhetherTheCallerSignedIn(t *testing.T) { p := DefaultPolicy() p.Mode = ModeAccount s, _ := open(t, p) if _, err := s.Put("dev1", doc("run-1", AnonFull), false); !errors.Is(err, ErrNeedAccount) { t.Fatalf("an un-signed-in device was not refused: %v", err) } if _, err := s.Put("dev1", doc("run-2", AnonFull), true); err != nil { t.Fatalf("a signed-in device was refused: %v", err) } } // Signing in must not open a door that the operator closed outright: mode=off means off. func TestSigningInDoesNotOverrideModeOff(t *testing.T) { p := DefaultPolicy() p.Mode = ModeOff s, _ := open(t, p) if _, err := s.Put("dev1", doc("run-1", AnonFull), true); !errors.Is(err, ErrDisabled) { t.Fatalf("a signed-in device uploaded to a server with uploads off: %v", err) } } func TestMinAnonymizationEnforced(t *testing.T) { p := DefaultPolicy() p.MinAnonymization = AnonBalanced s, _ := open(t, p) if _, err := s.Put("dev1", doc("run-full", AnonFull), false); !errors.Is(err, ErrNotAnonEnough) { t.Fatalf("full should be refused when balanced is required, got %v", err) } // An undeclared level means nothing was stripped, so it must be treated as "full". if _, err := s.Put("dev1", []byte(`{"run":{"id":"run-bare"},"summary":{}}`), false); !errors.Is(err, ErrNotAnonEnough) { t.Fatalf("undeclared level should be treated as full, got %v", err) } for _, lvl := range []string{AnonBalanced, AnonStrict} { if _, err := s.Put("dev1", doc("run-"+lvl, lvl), false); err != nil { t.Fatalf("%s should be accepted: %v", lvl, err) } } } func TestSizeLimit(t *testing.T) { p := DefaultPolicy() p.MaxBytes = 200 s, _ := open(t, p) big := append(doc("run-1", AnonFull), make([]byte, 400)...) if _, err := s.Put("dev1", big, false); !errors.Is(err, ErrTooLarge) { t.Fatalf("want ErrTooLarge, got %v", err) } } func TestRetentionByCountKeepsNewest(t *testing.T) { p := DefaultPolicy() p.MaxRunsPerDevice = 3 s, _ := open(t, p) for i := 0; i < 6; i++ { if _, err := s.Put("dev1", doc(fmt.Sprintf("run-%d", i), AnonFull), false); err != nil { t.Fatalf("put %d: %v", i, err) } time.Sleep(2 * time.Millisecond) // distinct UploadedAt so "newest" is well defined } got := s.List("dev1") if len(got) != 3 { t.Fatalf("kept %d runs, want 3", len(got)) } for i, want := range []string{"run-5", "run-4", "run-3"} { if got[i].ID != want { t.Fatalf("kept[%d] = %s, want %s (newest first)", i, got[i].ID, want) } } // The documents themselves must be gone too, not just their index entries. if _, err := s.Get("dev1", "run-0"); err == nil { t.Fatal("purged run is still readable") } } func TestRetentionByAge(t *testing.T) { p := DefaultPolicy() p.RetentionDays = 7 p.MaxRunsPerDevice = 0 s, dir := open(t, p) if _, err := s.Put("dev1", doc("run-old", AnonFull), false); err != nil { t.Fatal(err) } // Backdate the index entry past the retention window. metaPath := filepath.Join(dir, "runs", "dev1", "run-old.meta.json") b, _ := os.ReadFile(metaPath) var m Meta _ = json.Unmarshal(b, &m) m.UploadedAt = time.Now().Add(-30 * 24 * time.Hour) nb, _ := json.Marshal(m) if err := os.WriteFile(metaPath, nb, 0o600); err != nil { t.Fatal(err) } if _, err := s.Put("dev1", doc("run-new", AnonFull), false); err != nil { t.Fatal(err) } got := s.List("dev1") if len(got) != 1 || got[0].ID != "run-new" { t.Fatalf("age retention did not drop the old run: %+v", got) } } // Run and device ids reach the filesystem, so a hostile one must not be able to climb out of the // store directory or overwrite another device's data. func TestIDsCannotEscapeTheStoreDirectory(t *testing.T) { s, dir := open(t, DefaultPolicy()) if _, err := s.Put("../../etc", doc("../../../passwd", AnonFull), false); err != nil { t.Fatalf("put: %v", err) } var found []string _ = filepath.Walk(dir, func(p string, info os.FileInfo, err error) error { if err == nil && !info.IsDir() { rel, _ := filepath.Rel(dir, p) found = append(found, filepath.ToSlash(rel)) } return nil }) for _, f := range found { if strings.Contains(f, "..") { t.Fatalf("path escaped the store: %s", f) } } if len(found) == 0 { t.Fatal("nothing written at all") } } func TestListIsPerDevice(t *testing.T) { s, _ := open(t, DefaultPolicy()) if _, err := s.Put("devA", doc("run-a", AnonFull), false); err != nil { t.Fatal(err) } if _, err := s.Put("devB", doc("run-b", AnonFull), false); err != nil { t.Fatal(err) } if got := s.List("devA"); len(got) != 1 || got[0].ID != "run-a" { t.Fatalf("devA sees %+v", got) } if _, err := s.Get("devA", "run-b"); err == nil { t.Fatal("devA could read devB's run") } } func TestMetaSummarisesTheDocument(t *testing.T) { s, _ := open(t, DefaultPolicy()) m, err := s.Put("dev1", doc("run-1", AnonBalanced), false) if err != nil { t.Fatal(err) } if m.FindingCount != 2 || m.Verdict != "warn" || m.Anonymization != AnonBalanced { t.Fatalf("meta not extracted: %+v", m) } if m.StartedAt != "2026-08-01T10:00:00Z" { t.Fatalf("started_at = %q", m.StartedAt) } } func TestMalformedRejected(t *testing.T) { s, _ := open(t, DefaultPolicy()) for _, body := range [][]byte{[]byte("not json"), []byte(`{"run":{}}`), []byte(`{}`)} { if _, err := s.Put("dev1", body, false); !errors.Is(err, ErrMalformed) { t.Fatalf("body %q: want ErrMalformed, got %v", body, err) } } }