package httpapi import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "basicstack.de/proofedmesh/services/admin-api/internal/attest" "basicstack.de/proofedmesh/services/admin-api/internal/store" ) type recordingSigner struct { calls int lastKey string } func (s *recordingSigner) SignDSSE(_ context.Context, keyRef string, stmt attest.Statement) (attest.Envelope, string, error) { s.calls++ s.lastKey = keyRef env := attest.Envelope{ PayloadType: "application/vnd.in-toto+json", Payload: "ZmFrZS1wYXlsb2Fk", Signatures: []attest.EnvelopeSignature{{KeyID: keyRef, Sig: "ZmFrZS1zaWc"}}, } ch, _ := attest.EnvelopeContentHash(env) return env, ch, nil } type recordingSubmitter struct { calls int last attest.LogReceipt } func (s *recordingSubmitter) Submit(_ context.Context, env attest.Envelope, ch string) (attest.LogReceipt, error) { s.calls++ r := attest.LogReceipt{ LogIndex: int64(s.calls), ContentHash: ch, LoggedAt: time.Now().UTC(), } s.last = r return r, nil } func TestCreate_WithAttestation_AttachesHash(t *testing.T) { sig := &recordingSigner{} sub := &recordingSubmitter{} deps := Dependencies{ Store: store.NewMemory(), Now: func() time.Time { return time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC) }, Attest: &attest.Orchestrator{Signer: sig, Submitter: sub, KeyRef: "trust-api/test-key"}, } srv := httptest.NewServer(newMux(deps)) defer srv.Close() body := `{"country":"DE","currency":"EUR","full_day":"28.00","partial_day_min8h":"14.00","arrival_day":"14.00","departure_day":"14.00","valid_from":"2026-01-01T00:00:00Z","source_regulation":"BMF 2026"}` req, _ := http.NewRequest("POST", srv.URL+"/admin/per-diem-rates/", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } var out map[string]map[string]any _ = json.NewDecoder(resp.Body).Decode(&out) ref, _ := out["rate"]["rate_change_attestation_ref"].(string) if !strings.HasPrefix(ref, "sha256:") || len(ref) != 71 { t.Fatalf("rate_change_attestation_ref not attached as sha256 hash: %q", ref) } if sig.calls != 1 || sub.calls != 1 { t.Fatalf("expected one signer + one submitter call, got signer=%d submitter=%d", sig.calls, sub.calls) } if sig.lastKey != "trust-api/test-key" { t.Errorf("key ref not passed through: %q", sig.lastKey) } } type failingSubmitter struct{} func (failingSubmitter) Submit(_ context.Context, _ attest.Envelope, _ string) (attest.LogReceipt, error) { return attest.LogReceipt{}, &net503Error{} } type net503Error struct{} func (*net503Error) Error() string { return "tlog: status 503: trillian down" } func TestCreate_WithFailingTLog_RowPending(t *testing.T) { deps := Dependencies{ Store: store.NewMemory(), Now: func() time.Time { return time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC) }, Attest: &attest.Orchestrator{Signer: &recordingSigner{}, Submitter: failingSubmitter{}, KeyRef: "k"}, } srv := httptest.NewServer(newMux(deps)) defer srv.Close() body := `{"country":"DE","currency":"EUR","full_day":"28.00","partial_day_min8h":"14.00","arrival_day":"14.00","departure_day":"14.00","valid_from":"2026-01-01T00:00:00Z","source_regulation":"BMF 2026"}` req, _ := http.NewRequest("POST", srv.URL+"/admin/per-diem-rates/", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() // Create still succeeds: Row exists, attestation pending. if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201 even when tlog is down, got %d", resp.StatusCode) } var out map[string]map[string]any _ = json.NewDecoder(resp.Body).Decode(&out) if _, hasRef := out["rate"]["rate_change_attestation_ref"]; hasRef { if v := out["rate"]["rate_change_attestation_ref"]; v != nil { t.Fatalf("expected no attestation ref when tlog is down, got %v", v) } } }