sdk(P2.3): MockAttesto — local emulator in all three SDKs
Customers can now test their full ingest-and-verify pipeline in CI with zero network and zero Attesto account. Python attesto.testing.MockAttesto (context manager over a local HTTP server + pytest-fixture friendly), TypeScript createMockServer() (fetch-compatible handler, WebCrypto Ed25519, edge-safe), and Go attestotest.NewServer() (httptest) implement the v2 subset the SDKs use — streams, single+batch events, head, receipts, tenant event listings — with REAL seq/hash-chain semantics via the same frozen canonical functions, the server-side number-policy mirror (422), and windows/checkpoints built on demand with per-leaf inclusion proofs (promote-odd-node fold). Hard rule, test-enforced in all three languages: mock evidence is structurally incapable of passing as real — every emitted object carries "mock": true, receipts are signed by a per-instance throwaway key under kid attesto-mock-ed25519, and verify_receipt against any real witness key fails. Acceptance: the P1 verify suite (receipt, payload commitment, inclusion, completeness) passes against the emulator with real clients in all three SDKs; head tracking sees an honestly chained sequence. READMEs gain a "Testing without Attesto" quickstart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
132
attestotest/server_test.go
Normal file
132
attestotest/server_test.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package attestotest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
attesto "go.attesto.eu/sdk"
|
||||
)
|
||||
|
||||
func newClient(t *testing.T, s *Server) *attesto.Client {
|
||||
t.Helper()
|
||||
client, err := attesto.NewClient(s.APIKey, attesto.WithBaseURL(s.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func toSignedReceipt(t *testing.T, wire attesto.M) attesto.SignedReceipt {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(wire["receipt"])
|
||||
var receipt attesto.SignedReceipt
|
||||
if err := json.Unmarshal(raw, &receipt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return receipt
|
||||
}
|
||||
|
||||
func TestFullPipelineAgainstTheEmulator(t *testing.T) {
|
||||
server := NewServer()
|
||||
defer server.Close()
|
||||
client := newClient(t, server)
|
||||
ctx := context.Background()
|
||||
|
||||
stream, err := client.CreateStream(ctx, attesto.StreamCreateInput{UseCase: "ci", PolicyID: "mock-policy"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receipt, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{
|
||||
SourceRef: "e1", Payload: attesto.M{"decision": "approve", "score_bp": 8700},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.LogEvents(ctx, stream.StreamID, []attesto.EventInput{
|
||||
{SourceRef: "e2", Payload: attesto.M{"n": 2}},
|
||||
{SourceRef: "e3", Payload: attesto.M{"n": 3}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stored, err := client.GetReceipt(ctx, receipt.StreamEventID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report := attesto.VerifyReceiptOffline(stored.Receipt, server.PublicKeyHex)
|
||||
if !report.OK {
|
||||
t.Fatalf("offline verification failed: %v", report.Problems)
|
||||
}
|
||||
|
||||
events, err := client.ListTenantStreamEvents(ctx, stream.StreamID, 100, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plain := make([]map[string]any, len(events))
|
||||
for i, e := range events {
|
||||
plain[i] = e
|
||||
}
|
||||
comp := attesto.VerifyCompleteness(plain, 1, 3)
|
||||
if !comp.OK {
|
||||
t.Fatalf("completeness failed: %v", comp.Problems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInclusionProofsFromBuiltWindowVerify(t *testing.T) {
|
||||
server := NewServer()
|
||||
defer server.Close()
|
||||
client := newClient(t, server)
|
||||
ctx := context.Background()
|
||||
stream, _ := client.CreateStream(ctx, attesto.StreamCreateInput{UseCase: "ci", PolicyID: "mock-policy"})
|
||||
for i := 0; i < 5; i++ { // odd leaf count exercises the promote rule
|
||||
if _, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{
|
||||
SourceRef: "e", Payload: attesto.M{"i": i},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
window, err := server.BuildWindow(stream.StreamID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, leaf := range window.Leaves {
|
||||
ok, err := attesto.VerifyInclusionProof(leaf.LeafHash, leaf.Proof, window.RootHash)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("leaf %d failed inclusion: ok=%v err=%v", leaf.LeafIndex, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockReceiptsCannotPassAsReal(t *testing.T) {
|
||||
server := NewServer()
|
||||
defer server.Close()
|
||||
client := newClient(t, server)
|
||||
ctx := context.Background()
|
||||
stream, _ := client.CreateStream(ctx, attesto.StreamCreateInput{UseCase: "ci", PolicyID: "mock-policy"})
|
||||
receipt, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{SourceRef: "e1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := client.GetReceipt(ctx, receipt.StreamEventID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Structurally marked.
|
||||
if stored.Receipt.Payload["mock"] != true {
|
||||
t.Error("mock receipt payload must declare mock: true")
|
||||
}
|
||||
signer, _ := stored.Receipt.Payload["signer"].(map[string]any)
|
||||
if signer["kid"] != MockKid {
|
||||
t.Errorf("kid = %v, want %s", signer["kid"], MockKid)
|
||||
}
|
||||
// Rejected against a different ("real") witness key.
|
||||
realPub, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
report := attesto.VerifyReceiptOffline(stored.Receipt, hex.EncodeToString(realPub))
|
||||
if report.OK {
|
||||
t.Fatal("mock receipt verified against a real key — must never happen")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user