138 lines
4 KiB
Go
138 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")
|
||
|
|
}
|
||
|
|
}
|