package attest import ( "context" "errors" "testing" "time" "basicstack.de/proofedmesh/services/admin-api/internal/model" ) type fakeSigner struct { err error wantKeyRef string tookStmt Statement returnEnv Envelope returnHash string } func (f *fakeSigner) SignDSSE(_ context.Context, keyRef string, stmt Statement) (Envelope, string, error) { if f.err != nil { return Envelope{}, "", f.err } f.tookStmt = stmt if f.wantKeyRef != "" && keyRef != f.wantKeyRef { return Envelope{}, "", errors.New("unexpected keyRef") } return f.returnEnv, f.returnHash, nil } type fakeSubmitter struct { err error gotEnv Envelope gotHash string returnRct LogReceipt } func (f *fakeSubmitter) Submit(_ context.Context, env Envelope, ch string) (LogReceipt, error) { if f.err != nil { return LogReceipt{}, f.err } f.gotEnv = env f.gotHash = ch if f.returnRct.ContentHash == "" { f.returnRct.ContentHash = ch } return f.returnRct, nil } func TestOrchestrator_HappyPath(t *testing.T) { env := Envelope{PayloadType: "application/vnd.in-toto+json", Payload: "aGVsbG8"} ch, _ := EnvelopeContentHash(env) s := &fakeSigner{wantKeyRef: "k", returnEnv: env, returnHash: ch} sub := &fakeSubmitter{returnRct: LogReceipt{LogIndex: 7, ContentHash: ch, LoggedAt: time.Now()}} orch := Orchestrator{Signer: s, Submitter: sub, KeyRef: "k"} gotHash, receipt, err := orch.SignAndLog(context.Background(), RateChangeEvent{ Kind: ChangeCreate, Rate: sampleRateMinimal(), PerformedBy: PerformedBy{Issuer: "https://auth.basicstack.de", Subject: "u", SessionID: "s"}, PerformedAt: time.Now(), }) if err != nil { t.Fatalf("unexpected error: %v", err) } if gotHash != ch { t.Errorf("hash mismatch: %q vs %q", gotHash, ch) } if receipt.LogIndex != 7 { t.Errorf("receipt log_index: %d", receipt.LogIndex) } if sub.gotHash != ch { t.Errorf("submitter saw different hash: %q vs %q", sub.gotHash, ch) } if s.tookStmt.PredicateType != "https://basicstack.de/attestations/rate_change/v1" { t.Errorf("predicateType in statement: %q", s.tookStmt.PredicateType) } } func TestOrchestrator_SignerErrorStops(t *testing.T) { orch := Orchestrator{ Signer: &fakeSigner{err: errors.New("bao down")}, Submitter: &fakeSubmitter{}, KeyRef: "k", } _, _, err := orch.SignAndLog(context.Background(), RateChangeEvent{Rate: sampleRateMinimal()}) if err == nil { t.Fatal("expected error") } } func TestOrchestrator_SubmitterErrorPropagates(t *testing.T) { env := Envelope{PayloadType: "application/vnd.in-toto+json"} ch, _ := EnvelopeContentHash(env) orch := Orchestrator{ Signer: &fakeSigner{wantKeyRef: "k", returnEnv: env, returnHash: ch}, Submitter: &fakeSubmitter{err: errors.New("trillian down")}, KeyRef: "k", } gotHash, _, err := orch.SignAndLog(context.Background(), RateChangeEvent{Rate: sampleRateMinimal()}) if err == nil { t.Fatal("expected error") } // The hash from the signer is still returned so a reconciler can retry. if gotHash != ch { t.Errorf("expected signer hash to leak through on submitter error, got %q", gotHash) } } func TestOrchestrator_RejectsMisconfiguration(t *testing.T) { cases := []Orchestrator{ {Signer: nil, Submitter: &fakeSubmitter{}, KeyRef: "k"}, {Signer: &fakeSigner{}, Submitter: nil, KeyRef: "k"}, {Signer: &fakeSigner{}, Submitter: &fakeSubmitter{}, KeyRef: ""}, } for i, o := range cases { if _, _, err := o.SignAndLog(context.Background(), RateChangeEvent{}); err == nil { t.Errorf("case %d: expected error for misconfigured orchestrator", i) } } } func sampleRateMinimal() model.Rate { t, _ := time.Parse("2006-01-02", "2026-01-01") return model.Rate{ RateID: "PDR-1", Country: "DE", Currency: "EUR", FullDay: "28.00", PartialDayMin8h: "14.00", ArrivalDay: "14.00", DepartureDay: "14.00", ValidFrom: t, SourceRegulation: "test", CreatedBy: "admin", } }