126 lines
4 KiB
Go
126 lines
4 KiB
Go
package attest
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"basicstack.de/proofedmesh/services/admin-api/internal/model"
|
|
)
|
|
|
|
func sampleRate(t *testing.T) model.Rate {
|
|
t.Helper()
|
|
validFrom, _ := 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: validFrom,
|
|
SourceRegulation: "BMF test",
|
|
CreatedBy: "admin",
|
|
}
|
|
}
|
|
|
|
func TestHTTPSigner_HappyPath(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/sign/dsse" {
|
|
t.Errorf("unexpected path %s", r.URL.Path)
|
|
}
|
|
if auth := r.Header.Get("Authorization"); auth != "Bearer test-token" {
|
|
t.Errorf("auth header: %q", auth)
|
|
}
|
|
var req signRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if req.KeyRef != "trust-api/test-key" {
|
|
t.Errorf("key_ref: %q", req.KeyRef)
|
|
}
|
|
if req.Statement.PredicateType != "https://basicstack.de/attestations/rate_change/v1" {
|
|
t.Errorf("predicate type: %q", req.Statement.PredicateType)
|
|
}
|
|
// Fake envelope + content_hash
|
|
env := Envelope{
|
|
PayloadType: "application/vnd.in-toto+json",
|
|
Payload: "ZmFrZS1wYXlsb2Fk", // "fake-payload"
|
|
Signatures: []EnvelopeSignature{{KeyID: req.KeyRef, Sig: "ZmFrZS1zaWc"}},
|
|
}
|
|
ch, _ := EnvelopeContentHash(env)
|
|
resp := signResponse{
|
|
Envelope: env,
|
|
ContentHash: ch,
|
|
SignedAt: time.Now().UTC(),
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := HTTPSigner{BaseURL: srv.URL, TokenSource: StaticToken("test-token")}
|
|
stmt, _ := buildStatement(RateChangeEvent{
|
|
Kind: ChangeCreate,
|
|
Rate: sampleRate(t),
|
|
PerformedBy: PerformedBy{Issuer: "https://auth.basicstack.de", Subject: "admin-sub"},
|
|
PerformedAt: time.Now().UTC(),
|
|
})
|
|
env, ch, err := s.SignDSSE(context.Background(), "trust-api/test-key", stmt)
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
if !strings.HasPrefix(ch, "sha256:") || len(ch) != 71 {
|
|
t.Errorf("content_hash shape: %q", ch)
|
|
}
|
|
if env.PayloadType != "application/vnd.in-toto+json" {
|
|
t.Errorf("payload_type: %q", env.PayloadType)
|
|
}
|
|
}
|
|
|
|
func TestHTTPSigner_SurfacesServerError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, `{"error":{"code":"signing_denied","message":"OPA deny: no such key"}}`, http.StatusForbidden)
|
|
}))
|
|
defer srv.Close()
|
|
s := HTTPSigner{BaseURL: srv.URL, TokenSource: StaticToken("t")}
|
|
stmt, _ := buildStatement(RateChangeEvent{Kind: ChangeCreate, Rate: sampleRate(t)})
|
|
_, _, err := s.SignDSSE(context.Background(), "forbidden-key", stmt)
|
|
if err == nil {
|
|
t.Fatal("expected error from 403")
|
|
}
|
|
if !strings.Contains(err.Error(), "signing_denied") {
|
|
t.Errorf("expected server message in error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPSigner_RejectsEmptyBaseURL(t *testing.T) {
|
|
s := HTTPSigner{}
|
|
_, _, err := s.SignDSSE(context.Background(), "k", Statement{})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty BaseURL")
|
|
}
|
|
}
|
|
|
|
func TestBuildStatement_StableRateHash(t *testing.T) {
|
|
r := sampleRate(t)
|
|
ev := RateChangeEvent{Kind: ChangeCreate, Rate: r, PerformedAt: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)}
|
|
a, err := buildStatement(ev)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := buildStatement(ev)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Subject[0].Digest["sha256"] != b.Subject[0].Digest["sha256"] {
|
|
t.Fatalf("rate hash is not stable:\n a=%s\n b=%s", a.Subject[0].Digest["sha256"], b.Subject[0].Digest["sha256"])
|
|
}
|
|
// Changing the amount changes the hash.
|
|
r.FullDay = "99.00"
|
|
c, _ := buildStatement(RateChangeEvent{Kind: ChangeCreate, Rate: r, PerformedAt: ev.PerformedAt})
|
|
if a.Subject[0].Digest["sha256"] == c.Subject[0].Digest["sha256"] {
|
|
t.Fatal("rate hash collided on amount change")
|
|
}
|
|
}
|