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
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>
69 lines
2.1 KiB
Go
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")
|
|
}
|
|
}
|