//go:build linux package provisioner import ( family "amadan.net/rastrillo/rastrillo/crypto" "bytes" "context" "errors" "io" "os" "os/exec" "path/filepath" "testing" ) func journalFixture(t *testing.T) *journal { t.Helper() f, k := floorFixture(t) if e := f.initialize(k); e != nil { t.Fatal(e) } d, e := openPrivateDirectory(persistentDirectory(t)) if e != nil { t.Fatal(e) } t.Cleanup(func() { d.close() }) j, e := openJournal(d, f, k, true) if e != nil { t.Fatal(e) } t.Cleanup(func() { j.close() }) return j } func TestJournalCrashRecoveryAndExactReplay(t *testing.T) { for _, boundary := range []string{"prepared", "floor", "finalized"} { t.Run(boundary, func(t *testing.T) { j := journalFixture(t) synthetic := []byte("private-owner-481@example.invalid") stop := errors.New("synthetic interruption") j.boundary = func(stage string) error { if stage == boundary { return stop } return nil } if _, e := j.append(context.Background(), testToken(5), testToken(6), "directory.seed-team", "unseeded", synthetic); e != stop { t.Fatal("boundary not reached", e) } j.close() reopened, e := openJournal(j.dir, j.floor, j.key, false) if e != nil { t.Fatal("resume", e) } defer reopened.close() first, e := reopened.append(context.Background(), testToken(5), testToken(6), "directory.seed-team", "unseeded", synthetic) if e != nil { t.Fatal(e) } repeated, e := reopened.append(context.Background(), testToken(5), testToken(6), "directory.seed-team", "unseeded", synthetic) if e != nil || !bytes.Equal(first, repeated) { t.Fatal("replay changed record", e) } if _, e = reopened.append(context.Background(), testToken(5), testToken(6), "directory.seed-team", "unseeded", []byte("changed")); e != ErrRefused { t.Fatal("changed replay", e) } var n int if e = reopened.db.QueryRow("SELECT count(*) FROM records WHERE state='finalized'").Scan(&n); e != nil || n != 1 { t.Fatal("non-atomic journal", e) } for _, suffix := range []string{"", "-wal", "-shm"} { data, e := os.ReadFile(filepath.Join(j.dir.file.Name(), "registry.db") + suffix) if e != nil && !errors.Is(e, os.ErrNotExist) { t.Fatal(e) } if bytes.Contains(data, synthetic) { t.Fatal("plaintext journal payload") } } }) } } func TestJournalSurvivingFloorRefusesOldBackup(t *testing.T) { j := journalFixture(t) if _, e := j.append(context.Background(), testToken(5), testToken(6), "directory.initialize", "absent", []byte("first private input")); e != nil { t.Fatal(e) } if _, e := j.db.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); e != nil { t.Fatal(e) } backup, e := os.ReadFile(filepath.Join(j.dir.file.Name(), "registry.db")) if e != nil { t.Fatal(e) } if _, e = j.append(context.Background(), testToken(5), testToken(7), "directory.seed-team", "empty", []byte("second private input")); e != nil { t.Fatal(e) } j.close() if e = os.WriteFile(filepath.Join(j.dir.file.Name(), "registry.db"), backup, 0600); e != nil { t.Fatal(e) } if restored, e := openJournal(j.dir, j.floor, j.key, false); e != ErrRefused { if restored != nil { restored.close() } t.Fatal("stale backup accepted", e) } } func TestJournalProcessDeathRecovery(t *testing.T) { type childInput struct { State, Floor, Boundary string Identity floorIdentity Key []byte } if os.Getenv("OFICINA_JOURNAL_CHILD") == "1" { raw, e := io.ReadAll(io.LimitReader(os.Stdin, 16385)) if e != nil || len(raw) > 16384 { t.Fatal("private child input") } var in childInput if closedJSON(raw, &in) != nil { t.Fatal("child shape") } key, e := family.UnmarshalKeypair(in.Key) if e != nil { t.Fatal("child key") } dir, e := openPrivateDirectory(in.State) if e != nil { t.Fatal(e) } defer dir.close() floorDir, e := openPrivateDirectory(in.Floor) if e != nil { t.Fatal(e) } defer floorDir.close() floor, e := newNativeFloor(floorDir, in.Identity) if e != nil { t.Fatal(e) } j, e := openJournal(dir, floor, key, false) if e != nil { t.Fatal(e) } defer j.close() floor.markerBoundary = func() error { if in.Boundary == "marker-created" { os.Exit(72) } return nil } j.boundary = func(stage string) error { if stage == in.Boundary { os.Exit(72) } return nil } if _, e = j.append(context.Background(), testToken(5), testToken(6), "directory.seed-team", "empty", []byte("synthetic process-death payload")); e != nil { t.Fatal(e) } t.Fatal("death boundary not reached") } for _, stage := range []string{"prepared", "marker-created", "floor", "finalized"} { t.Run(stage, func(t *testing.T) { j := journalFixture(t) j.close() key, e := family.MarshalKeypair(j.key) if e != nil { t.Fatal(e) } input, e := canonicalJSON(childInput{j.dir.file.Name(), j.floor.dir.file.Name(), stage, j.floor.identity, key}) if e != nil { t.Fatal(e) } cmd := exec.Command(os.Args[0], "-test.run=^TestJournalProcessDeathRecovery$") cmd.Env = append(os.Environ(), "OFICINA_JOURNAL_CHILD=1") cmd.Stdin = bytes.NewReader(input) output, e := cmd.CombinedOutput() var exit *exec.ExitError if !errors.As(e, &exit) || exit.ExitCode() != 72 { t.Fatalf("child did not stop at durable boundary: %v %s", e, output) } reopened, e := openJournal(j.dir, j.floor, j.key, false) if e != nil { t.Fatal("process death resume", e) } defer reopened.close() if _, e = reopened.append(context.Background(), testToken(5), testToken(6), "directory.seed-team", "empty", []byte("synthetic process-death payload")); e != nil { t.Fatal(e) } point, _, e := j.floor.current() if e != nil || point.Sequence != 1 { t.Fatal("sequence reused", e) } if _, e = reopened.append(context.Background(), testToken(5), testToken(7), "directory.seed-team", "empty", []byte("next operation")); e != nil { t.Fatal("next operation did not self-heal marker", e) } point, _, e = j.floor.current() if e != nil || point.Sequence != 2 { t.Fatal("next sequence", e) } }) } } func TestJournalPublicMetadataRejectsPrivateOrArbitraryText(t *testing.T) { j := journalFixture(t) for _, pair := range [][2]string{{"owner@example.invalid", "empty"}, {"directory.seed-team", "owner@example.invalid"}, {"arbitrary-sql", "empty"}} { if _, e := j.append(context.Background(), testToken(5), testToken(6), pair[0], pair[1], []byte("sealed")); e != ErrRefused { t.Fatal("arbitrary public journal metadata", e) } } var n int if e := j.db.QueryRow("SELECT count(*) FROM records").Scan(&n); e != nil || n != 0 { t.Fatal("rejected metadata persisted", e) } }