ProofedMesh/services/admin-api/internal/tlog/submitter_test.go
CTO Agent cacdf7e286
Some checks failed
validate / Rego (opa fmt + opa test) (push) Successful in 13s
validate / JSON Schema (ajv) (push) Has been cancelled
validate / Go (vet + test) (push) Has been cancelled
attest+tlog: ADR-0005 Signing/TLog API contract, HTTP clients + orchestrator in admin-api (go vet clean, 11 new tests pass)
2026-10-25 17:51:33 +00:00

137 lines
4 KiB
Go

package tlog
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"basicstack.de/proofedmesh/services/admin-api/internal/attest"
)
type fakeTLogServer struct {
mu sync.Mutex
byHash map[string]attest.LogReceipt
nextIdx int64
}
func newFakeServer() *fakeTLogServer {
return &fakeTLogServer{byHash: make(map[string]attest.LogReceipt)}
}
func (f *fakeTLogServer) handler(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/log/entries" {
t.Errorf("unexpected path %s", r.URL.Path)
}
var req submitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Verify content_hash matches the envelope on the wire.
recomputed, _ := attest.EnvelopeContentHash(req.Envelope)
if recomputed != req.ContentHash {
http.Error(w, `{"error":"content_hash mismatch"}`, http.StatusConflict)
return
}
f.mu.Lock()
defer f.mu.Unlock()
if existing, ok := f.byHash[req.ContentHash]; ok {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) // idempotent repeat
_ = json.NewEncoder(w).Encode(existing)
return
}
f.nextIdx++
receipt := attest.LogReceipt{
LogIndex: f.nextIdx,
ContentHash: req.ContentHash,
LoggedAt: time.Now().UTC(),
InclusionProof: attest.InclusionProof{
TreeSize: f.nextIdx + 1,
TreeRootHash: "sha256:" + strings.Repeat("a", 64),
SignedTreeHeadRef: "sha256:" + strings.Repeat("b", 64),
Hashes: []string{"sha256:" + strings.Repeat("c", 64)},
},
}
f.byHash[req.ContentHash] = receipt
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(receipt)
})
}
func sampleEnvelope() attest.Envelope {
return attest.Envelope{
PayloadType: "application/vnd.in-toto+json",
Payload: "aGVsbG8", // "hello" (base64url)
Signatures: []attest.EnvelopeSignature{{KeyID: "k", Sig: "c2ln"}},
}
}
func TestSubmit_HappyPathAndIdempotency(t *testing.T) {
f := newFakeServer()
srv := httptest.NewServer(f.handler(t))
defer srv.Close()
s := HTTPSubmitter{BaseURL: srv.URL, TokenSource: attest.StaticToken("t")}
env := sampleEnvelope()
ch, err := attest.EnvelopeContentHash(env)
if err != nil {
t.Fatalf("content_hash: %v", err)
}
r1, err := s.Submit(context.Background(), env, ch)
if err != nil {
t.Fatalf("first submit: %v", err)
}
if r1.LogIndex != 1 {
t.Errorf("first log_index: %d", r1.LogIndex)
}
// Re-submit must return the SAME log_index.
r2, err := s.Submit(context.Background(), env, ch)
if err != nil {
t.Fatalf("second submit: %v", err)
}
if r2.LogIndex != 1 {
t.Errorf("idempotent repeat log_index changed: %d", r2.LogIndex)
}
}
func TestSubmit_RejectsContentHashMismatch(t *testing.T) {
f := newFakeServer()
srv := httptest.NewServer(f.handler(t))
defer srv.Close()
s := HTTPSubmitter{BaseURL: srv.URL, TokenSource: attest.StaticToken("t")}
env := sampleEnvelope()
_, err := s.Submit(context.Background(), env, "sha256:"+strings.Repeat("f", 64))
if err == nil {
t.Fatal("expected server to reject content_hash mismatch")
}
}
func TestSubmit_SurfacesServerErrors(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "trillian down", http.StatusServiceUnavailable)
}))
defer srv.Close()
s := HTTPSubmitter{BaseURL: srv.URL, TokenSource: attest.StaticToken("t")}
_, err := s.Submit(context.Background(), sampleEnvelope(), "sha256:"+strings.Repeat("a", 64))
if err == nil {
t.Fatal("expected error from 503")
}
if !strings.Contains(err.Error(), "status 503") {
t.Errorf("expected status in error, got %v", err)
}
}
func TestSubmit_RejectsEmptyBaseURL(t *testing.T) {
s := HTTPSubmitter{}
_, err := s.Submit(context.Background(), sampleEnvelope(), "sha256:"+strings.Repeat("a", 64))
if err == nil {
t.Fatal("expected error for empty BaseURL")
}
}