ProofedMesh/services/admin-api/internal/httpapi/attest_integration_test.go

124 lines
4.1 KiB
Go
Raw Normal View History

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)
}
}
}