Files
echolot/server/internal/runs/runs_test.go
T
mrambossekandClaude Opus 5 987b2ceb47 server: read the verdict from where the schema puts it
Every uploaded run showed "not recorded" in the web UI because the meta
extractor read summary.verdict. Schema §7.3 calls that field
summary.overall; "verdict" is the per-category field one level down. So
the verdict was never stored, and the UI faithfully reported a gap that
was this parser's doing rather than the document's.

The test encoded the same mistake — its fixture posted
summary.verdict:"warn" — so it passed throughout against a parser that
read a field nothing writes. Corrected to summary.overall, and to a
verdict that exists: §7.3 defines green|yellow|red|inconclusive, and
"warn" was never one of them.

The eleven runs already stored had their meta backfilled from the
documents, which are kept byte-for-byte and still carry the real value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 10:52:25 +02:00

212 lines
6.3 KiB
Go

// 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":{"overall":"yellow"}}`, 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 != "yellow" || 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)
}
}
}