ProofedMesh/services/dummy-sink/internal/handover/builder_test.go
CTO (Paperclip) b45ad34169
All checks were successful
validate / JSON Schema (ajv) (push) Successful in 19s
build-images / signing-service image (push) Successful in 22s
validate / Rego (opa fmt + opa test) (push) Successful in 13s
validate / DB (migrate up/down dry-run) (push) Successful in 26s
validate / Go (vet + test) (push) Successful in 1m13s
dummy-sink: Phase-1 mock sink (sink:dummy-sink) (DEV-611)
Trivial Senken-Adapter per §7 of architecture V1.1. Accepts
{decision_id, payload_hash, payload, approval_ref, transform_ref}
on POST /api/v1/sink/dummy/deliver; calls verification-service's
/api/v1/verify/chain and branches:

- valid=true → atomic .tmp+rename write of payload to
  /data/dummy-sink/<decision_id>.json, builds a
  basicstack.de/attestations/handover/v1 statement, signs via
  signing-service with the double identity flow (Pocket-ID bearer
  + projected SA token), submits DSSE envelope to the TLog and
  returns 201 {handover_ref, log_index}.
- valid=false & replay=true → 409.
- valid=false → 403 {reasons:[…]}.

Replay protection lives in verification-service per the parent plan;
dummy-sink keeps no local replay store. The payload_hash field is
verified against both the raw and canonical form so a verifier can
reproduce either hash.

Tests use httptest-backed fakes for verify / sign / tlog and cover
the DEV-611 acceptance criteria plus the bad-request shapes:
- happy path writes the file, calls signer with the handover
  statement, submits the envelope to the TLog
- 403 keeps the disk and never calls signer/tlog
- replay returns 409 without re-signing
- payload_hash mismatch rejects before verify
- malformed bodies return 400
- verify unavailable → 502, signing denied → 403

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 20:20:40 +00:00

69 lines
2.1 KiB
Go

package handover
import (
"testing"
"time"
)
func TestBuildHappyPath(t *testing.T) {
in := Input{
DecisionID: "dec-1",
PayloadHash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
ApprovalRef: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
TransformRef: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
SinkID: "sink:dummy-sink",
SinkRecordRef: "file:///data/dummy-sink/dec-1.json",
Actor: "agent-dummy-sink",
IssuedAt: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC),
}
stmt, err := Build(in)
if err != nil {
t.Fatalf("Build: %v", err)
}
if stmt["predicateType"] != PredicateType {
t.Fatalf("predicateType = %v, want %v", stmt["predicateType"], PredicateType)
}
pred := stmt["predicate"].(map[string]any)
if pred["decision_id"] != "dec-1" {
t.Fatalf("predicate.decision_id = %v", pred["decision_id"])
}
if pred["approval_ref"] != in.ApprovalRef {
t.Fatalf("predicate.approval_ref = %v", pred["approval_ref"])
}
if pred["issued_at"] != "2026-01-02T03:04:05Z" {
t.Fatalf("predicate.issued_at = %v", pred["issued_at"])
}
subject := stmt["subject"].([]any)[0].(map[string]any)
digest := subject["digest"].(map[string]any)
wantHex := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
if digest["sha256"] != wantHex {
t.Fatalf("digest.sha256 = %v, want %v", digest["sha256"], wantHex)
}
materials := pred["materials"].([]any)
if len(materials) != 2 {
t.Fatalf("materials len = %d, want 2", len(materials))
}
}
func TestBuildRequiresFields(t *testing.T) {
_, err := Build(Input{})
if err == nil {
t.Fatalf("Build empty: want err")
}
}
func TestStripSha256Prefix(t *testing.T) {
hex, err := StripSha256Prefix("sha256:abcd")
if err != nil {
t.Fatalf("StripSha256Prefix: %v", err)
}
if hex != "abcd" {
t.Fatalf("hex = %q, want abcd", hex)
}
if _, err := StripSha256Prefix("sha512:abcd"); err == nil {
t.Fatalf("sha512 prefix should error")
}
if _, err := StripSha256Prefix(""); err == nil {
t.Fatalf("empty should error")
}
}