ProofedMesh/services/transparency-log/pkg/httpapi/server_test.go
CTO 69b3f3012f
All checks were successful
validate / Rego (opa fmt + opa test) (push) Successful in 14s
validate / JSON Schema (ajv) (push) Successful in 20s
validate / Go (vet + test) (push) Successful in 20s
services: add transparency-log Go service (ADR-0005 POST /api/v1/log/entries)
- services/transparency-log/pkg/backend: Backend interface with in-memory
  append-log (RFC 6962 Merkle tree) + Trillian wrapper (prod stub).
- Idempotenz-Pflicht: Wiederaufruf mit identischem content_hash liefert
  200 + gleichen log_index + aktuellen inclusion_proof.
- content_hash-Verifikation serverseitig; Mismatch → 409.
- pkg/httpapi: ADR-0005-konformer Handler (201/200/400/404/409/503).
- cmd/transparency-log: minimaler HTTP-Server, memory default,
  --backend trillian als Stub (503).
- Interop-Test in services/admin-api/internal/tlog/interop_test.go
  fährt die echte transparency-log pkg gegen die admin-api
  HTTPSubmitter — Happy-Path, Idempotenz und content_hash-Mismatch
  abgedeckt.
- go test: 10 Tests transparency-log + 2 Interop-Tests admin-api, go
  vet clean in beiden Modulen.

Folge-Subtask: Trillian-Deployment + Keys (siehe ADR-0005 Open Points).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 18:08:53 +00:00

156 lines
4.6 KiB
Go

package httpapi
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"basicstack.de/proofedmesh/services/transparency-log/pkg/backend"
)
func fixtureEnvelope() backend.Envelope {
return backend.Envelope{
PayloadType: "application/vnd.in-toto+json",
Payload: "aGVsbG8",
Signatures: []backend.EnvelopeSignature{{KeyID: "k", Sig: "c2ln"}},
}
}
func newServer(t *testing.T) *httptest.Server {
t.Helper()
s := &Server{Backend: backend.NewMemory()}
return httptest.NewServer(s.Handler())
}
func TestSubmit_HappyPath_201(t *testing.T) {
srv := newServer(t)
defer srv.Close()
env := fixtureEnvelope()
ch, err := backend.EnvelopeContentHash(env)
if err != nil {
t.Fatalf("hash: %v", err)
}
body, _ := json.Marshal(submitRequest{Envelope: env, ContentHash: ch})
resp, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body)))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
raw, _ := io.ReadAll(resp.Body)
t.Fatalf("want 201, got %d: %s", resp.StatusCode, raw)
}
var rct backend.Receipt
if err := json.NewDecoder(resp.Body).Decode(&rct); err != nil {
t.Fatalf("decode: %v", err)
}
if rct.ContentHash != ch {
t.Errorf("content_hash echoed wrong: %q vs %q", rct.ContentHash, ch)
}
if rct.LogIndex != 0 {
t.Errorf("log_index: %d", rct.LogIndex)
}
if rct.InclusionProof.TreeSize != 1 {
t.Errorf("tree_size: %d", rct.InclusionProof.TreeSize)
}
}
func TestSubmit_Idempotent_200_SameIndex(t *testing.T) {
srv := newServer(t)
defer srv.Close()
env := fixtureEnvelope()
ch, _ := backend.EnvelopeContentHash(env)
body, _ := json.Marshal(submitRequest{Envelope: env, ContentHash: ch})
// First call -> 201
resp1, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body)))
if err != nil {
t.Fatalf("post1: %v", err)
}
resp1.Body.Close()
if resp1.StatusCode != http.StatusCreated {
t.Fatalf("first status: %d", resp1.StatusCode)
}
// Second call -> 200
resp2, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body)))
if err != nil {
t.Fatalf("post2: %v", err)
}
defer resp2.Body.Close()
if resp2.StatusCode != http.StatusOK {
raw, _ := io.ReadAll(resp2.Body)
t.Fatalf("idempotent repeat status: want 200, got %d, body=%s", resp2.StatusCode, raw)
}
var rct2 backend.Receipt
if err := json.NewDecoder(resp2.Body).Decode(&rct2); err != nil {
t.Fatalf("decode: %v", err)
}
if rct2.LogIndex != 0 {
t.Errorf("idempotent log_index: want 0, got %d", rct2.LogIndex)
}
}
func TestSubmit_ContentHashMismatch_409(t *testing.T) {
srv := newServer(t)
defer srv.Close()
env := fixtureEnvelope()
bogus := "sha256:" + strings.Repeat("0", 64)
body, _ := json.Marshal(submitRequest{Envelope: env, ContentHash: bogus})
resp, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body)))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusConflict {
raw, _ := io.ReadAll(resp.Body)
t.Fatalf("want 409, got %d: %s", resp.StatusCode, raw)
}
}
func TestSubmit_RejectsMissingContentHash(t *testing.T) {
srv := newServer(t)
defer srv.Close()
body := `{"envelope":{"payloadType":"application/vnd.in-toto+json","payload":"aGVsbG8","signatures":[]}}`
resp, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(body))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("want 400, got %d", resp.StatusCode)
}
}
func TestSubmit_UnknownRoute_404(t *testing.T) {
srv := newServer(t)
defer srv.Close()
resp, err := http.Post(srv.URL+"/api/v1/log/other", "application/json", strings.NewReader("{}"))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Fatalf("want 404, got %d", resp.StatusCode)
}
}
func TestSubmit_TrillianStub_Returns503(t *testing.T) {
srv := httptest.NewServer((&Server{Backend: backend.Trillian{}}).Handler())
defer srv.Close()
env := fixtureEnvelope()
ch, _ := backend.EnvelopeContentHash(env)
body, _ := json.Marshal(submitRequest{Envelope: env, ContentHash: ch})
resp, err := http.Post(srv.URL+"/api/v1/log/entries", "application/json", strings.NewReader(string(body)))
if err != nil {
t.Fatalf("post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusServiceUnavailable {
raw, _ := io.ReadAll(resp.Body)
t.Fatalf("trillian stub want 503, got %d: %s", resp.StatusCode, raw)
}
}