package attest import ( "strings" "testing" "time" ) // TestBuildStatement_StableBytes locks the canonical statement shape so a // future accidental field reorder cannot silently diverge the reconciler's // signatures from admin-api's. The expected bytes were produced by this // package itself on first commit; if they change, admin-api's statement must // change in lockstep. func TestBuildStatement_StableBytes(t *testing.T) { city := "Berlin" ev := RateChangeEvent{ Kind: ChangeCreate, Rate: Rate{ RateID: "PDR-1", Country: "DE", City: &city, Currency: "EUR", FullDay: "28.00", PartialDayMin8h: "14.00", ArrivalDay: "14.00", DepartureDay: "14.00", ValidFrom: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), SourceRegulation: "BRKG § 9 Abs. 1", CreatedBy: "admin", CreatedAt: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), }, PerformedBy: PerformedBy{ Issuer: "https://auth.basicstack.de", Subject: "attest-reconciler@proofedmesh", }, PerformedAt: time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC), } stmt, err := BuildStatement(ev) if err != nil { t.Fatalf("BuildStatement err: %v", err) } if stmt.Type != "https://in-toto.io/Statement/v1" { t.Errorf("unexpected statement type %q", stmt.Type) } if stmt.PredicateType != "https://basicstack.de/attestations/rate_change/v1" { t.Errorf("unexpected predicateType %q", stmt.PredicateType) } if len(stmt.Subject) != 1 || stmt.Subject[0].Name != "per_diem_rate/PDR-1" { t.Fatalf("unexpected subject: %+v", stmt.Subject) } digest := stmt.Subject[0].Digest["sha256"] if !strings.HasPrefix(digest, "sha256:") || len(digest) != len("sha256:")+64 { t.Errorf("subject.digest not sha256:<64hex>: %q", digest) } // Re-call must be stable (bit-for-bit reproducible — the whole point). stmt2, _ := BuildStatement(ev) b1, _ := canonicalJSON(stmt) b2, _ := canonicalJSON(stmt2) if string(b1) != string(b2) { t.Errorf("BuildStatement is not deterministic") } } func TestOrchestrator_RejectsMissingDeps(t *testing.T) { o := Orchestrator{KeyRef: "k"} _, _, err := o.SignAndLog(nil, RateChangeEvent{}) if err == nil { t.Fatal("expected error from Orchestrator with nil Signer") } }