ProofedMesh/services/approval-service/internal/canonical/canonical_test.go

41 lines
897 B
Go
Raw Permalink Normal View History

approval-service: Phase-1 content-binding approval service (DEV-608) New services/approval-service with the Session → Preview → Confirm pipeline required by DEV-606/DEV-608: * POST /api/v1/sessions starts a Pocket-ID-bound approval session. * POST /api/v1/approvals/preview renders content server-side, computes H_content = sha256(canonical JSON), stores the (nonce, salt, canonical, challenge) tuple and returns the human verification digits plus the WebAuthn challenge = nonce || H_content. * POST /api/v1/approvals/confirm verifies the WebAuthn assertion against the stored challenge, DSSE-signs the Approval statement via the signing-service (key_ref=trust-api/approval-signer-2026-q4), submits to the transparency-log, and returns {approval_id, attestation_ref, log_index}. TLog failures leave the row at attestation-pending for the reconciler. * db/trust/migrations/0004_approval_sessions.{up,down}.sql: new tables approval_sessions, approval_previews, approvals (schema matches the Phase 1 store contract). * internal/content: canonical JSON + H_content + 6-digit verification code (byte-compatible with signing-service/internal/canonical). * internal/webauthn: stdlib-only Phase-1 verifier; wire shape stays WebAuthn-compatible for the Phase-2 swap to go-webauthn/webauthn. * internal/signer, internal/tlog: HTTP clients mapping 5xx / connection errors to typed ErrSignerUnavailable / ErrTLogUnavailable so the HTTP layer can return 502 for signing failures and persist attestation-pending on TLog failure. * internal/service.Orchestrator ties the pipeline together and persists the Approval BEFORE the TLog submit (never return a log_index for an un-persisted approval). Tests cover: happy path, tampered H_content → 400, expired challenge → 403, signer 5xx + conn-refused → 502, TLog failure → row stays attestation-pending, duplicate confirm → 409, session expired → 403. go vet clean, go test ./... passes. Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 19:42:06 +00:00
package canonical
import "testing"
func TestMarshalSortsKeys(t *testing.T) {
got, err := Marshal(map[string]any{"b": 1, "a": 2})
if err != nil {
t.Fatal(err)
}
// Both keys must be present and sorted by name
want := `{"a":2,"b":1}`
if string(got) != want {
t.Errorf("got %q want %q", string(got), want)
}
}
func TestMarshalStableAcrossRuns(t *testing.T) {
input := map[string]any{
"outer": map[string]any{"z": 1, "a": 2, "m": 3},
"arr": []any{"3", "1", "2"},
}
a, _ := Marshal(input)
b, _ := Marshal(input)
if string(a) != string(b) {
t.Errorf("not stable:\n%s\nvs\n%s", a, b)
}
}
func TestHashReturnsPrefix(t *testing.T) {
hash, raw, err := Hash(map[string]any{"k": "v"})
if err != nil {
t.Fatal(err)
}
if hash[:7] != "sha256:" {
t.Errorf("hash missing sha256: prefix: %q", hash)
}
if string(raw) != `{"k":"v"}` {
t.Errorf("canonical form: %q", raw)
}
}