aboutsummaryrefslogtreecommitdiff
path: root/internal/api/server_integration_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/server_integration_test.go')
-rw-r--r--internal/api/server_integration_test.go65
1 files changed, 63 insertions, 2 deletions
diff --git a/internal/api/server_integration_test.go b/internal/api/server_integration_test.go
index 1736750..edb64bd 100644
--- a/internal/api/server_integration_test.go
+++ b/internal/api/server_integration_test.go
@@ -14,7 +14,6 @@ import (
"path/filepath"
"strings"
"testing"
-
"github.com/Fuwn/plutia/internal/checkpoint"
"github.com/Fuwn/plutia/internal/config"
"github.com/Fuwn/plutia/internal/ingest"
@@ -25,32 +24,41 @@ import (
func TestProofAgainstOlderCheckpointAfterFurtherIngest(t *testing.T) {
tmp := t.TempDir()
dataDir := filepath.Join(tmp, "data")
+
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatalf("mkdir data: %v", err)
}
keyPath := filepath.Join(tmp, "mirror.key")
seed := make([]byte, ed25519.SeedSize)
+
if _, err := rand.Read(seed); err != nil {
t.Fatalf("seed: %v", err)
}
+
if err := os.WriteFile(keyPath, []byte(base64.RawURLEncoding.EncodeToString(seed)), 0o600); err != nil {
t.Fatalf("write key: %v", err)
}
recs := buildCheckpointScenarioRecords(t)
sourcePath := filepath.Join(tmp, "records.ndjson")
+
writeRecordsFile(t, sourcePath, recs)
store, err := storage.OpenPebble(dataDir)
+
if err != nil {
t.Fatalf("open pebble: %v", err)
}
+
defer store.Close()
+
if err := store.SetMode(config.ModeMirror); err != nil {
t.Fatalf("set mode: %v", err)
}
+
bl, err := storage.OpenBlockLog(dataDir, 3, 4)
+
if err != nil {
t.Fatalf("open block log: %v", err)
}
@@ -69,29 +77,37 @@ func TestProofAgainstOlderCheckpointAfterFurtherIngest(t *testing.T) {
}
cpMgr := checkpoint.NewManager(store, dataDir, keyPath)
svc := ingest.NewService(cfg, store, ingest.NewClient(sourcePath), bl, cpMgr)
+
if err := svc.Replay(context.Background()); err != nil {
t.Fatalf("replay: %v", err)
}
+
if err := svc.Flush(context.Background()); err != nil {
t.Fatalf("flush: %v", err)
}
cp2, ok, err := store.GetCheckpoint(2)
+
if err != nil || !ok {
t.Fatalf("checkpoint 2 missing: ok=%v err=%v", ok, err)
}
ts := httptest.NewServer(NewServer(cfg, store, svc, cpMgr).Handler())
+
defer ts.Close()
url := ts.URL + "/did/" + strings.ReplaceAll("did:plc:alice", ":", "%3A") + "/proof?checkpoint=2"
resp, err := http.Get(url)
+
if err != nil {
t.Fatalf("get proof: %v", err)
}
+
defer resp.Body.Close()
+
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
+
t.Fatalf("proof status: %d body=%s", resp.StatusCode, string(bodyBytes))
}
@@ -102,15 +118,19 @@ func TestProofAgainstOlderCheckpointAfterFurtherIngest(t *testing.T) {
MerkleRoot string `json:"merkle_root"`
} `json:"inclusion_proof"`
}
+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatalf("decode response: %v", err)
}
+
if body.CheckpointSequence != 2 {
t.Fatalf("unexpected checkpoint sequence: got %d want 2", body.CheckpointSequence)
}
+
if body.ChainTipReference != recs[0].CID {
t.Fatalf("expected old tip at checkpoint=2: got %s want %s", body.ChainTipReference, recs[0].CID)
}
+
if body.InclusionProof.MerkleRoot != cp2.DIDMerkleRoot {
t.Fatalf("merkle root mismatch: got %s want %s", body.InclusionProof.MerkleRoot, cp2.DIDMerkleRoot)
}
@@ -119,34 +139,44 @@ func TestProofAgainstOlderCheckpointAfterFurtherIngest(t *testing.T) {
func TestCorruptedBlockRefusesProof(t *testing.T) {
tmp := t.TempDir()
dataDir := filepath.Join(tmp, "data")
+
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatalf("mkdir data: %v", err)
}
seed := make([]byte, ed25519.SeedSize)
+
if _, err := rand.Read(seed); err != nil {
t.Fatalf("seed: %v", err)
}
+
keyPath := filepath.Join(tmp, "mirror.key")
+
if err := os.WriteFile(keyPath, []byte(base64.RawURLEncoding.EncodeToString(seed)), 0o600); err != nil {
t.Fatalf("write key: %v", err)
}
recs := buildCheckpointScenarioRecords(t)
sourcePath := filepath.Join(tmp, "records.ndjson")
+
writeRecordsFile(t, sourcePath, recs)
store, err := storage.OpenPebble(dataDir)
+
if err != nil {
t.Fatalf("open pebble: %v", err)
}
+
if err := store.SetMode(config.ModeMirror); err != nil {
t.Fatalf("set mode: %v", err)
}
+
bl, err := storage.OpenBlockLog(dataDir, 3, 4)
+
if err != nil {
t.Fatalf("open block log: %v", err)
}
+
cfg := config.Config{
Mode: config.ModeMirror,
DataDir: dataDir,
@@ -161,49 +191,67 @@ func TestCorruptedBlockRefusesProof(t *testing.T) {
}
cpMgr := checkpoint.NewManager(store, dataDir, keyPath)
svc := ingest.NewService(cfg, store, ingest.NewClient(sourcePath), bl, cpMgr)
+
if err := svc.Replay(context.Background()); err != nil {
t.Fatalf("replay: %v", err)
}
+
if _, err := svc.Snapshot(context.Background()); err != nil {
t.Fatalf("snapshot: %v", err)
}
+
svc.Close()
+
if err := store.Close(); err != nil {
t.Fatalf("close store: %v", err)
}
blockPath := filepath.Join(dataDir, "ops", "000001.zst")
b, err := os.ReadFile(blockPath)
+
if err != nil {
t.Fatalf("read block: %v", err)
}
+
b[len(b)/2] ^= 0xFF
+
if err := os.WriteFile(blockPath, b, 0o644); err != nil {
t.Fatalf("write corrupted block: %v", err)
}
store2, err := storage.OpenPebble(dataDir)
+
if err != nil {
t.Fatalf("open store2: %v", err)
}
+
defer store2.Close()
+
bl2, err := storage.OpenBlockLog(dataDir, 3, 4)
+
if err != nil {
t.Fatalf("open blocklog2: %v", err)
}
+
svc2 := ingest.NewService(cfg, store2, ingest.NewClient("file:///nonexistent"), bl2, checkpoint.NewManager(store2, dataDir, keyPath))
+
if !svc2.IsCorrupted() {
t.Fatalf("expected service to detect corruption on restart")
}
ts := httptest.NewServer(NewServer(cfg, store2, svc2, checkpoint.NewManager(store2, dataDir, keyPath)).Handler())
+
defer ts.Close()
+
url := ts.URL + "/did/" + strings.ReplaceAll("did:plc:alice", ":", "%3A") + "/proof"
resp, err := http.Get(url)
+
if err != nil {
t.Fatalf("request proof: %v", err)
}
+
defer resp.Body.Close()
+
if resp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("expected 503 for corrupted proof, got %d", resp.StatusCode)
}
@@ -211,13 +259,18 @@ func TestCorruptedBlockRefusesProof(t *testing.T) {
func writeRecordsFile(t *testing.T, path string, recs []types.ExportRecord) {
t.Helper()
+
f, err := os.Create(path)
+
if err != nil {
t.Fatalf("create records file: %v", err)
}
+
defer f.Close()
+
for _, rec := range recs {
b, _ := json.Marshal(rec)
+
if _, err := fmt.Fprintln(f, string(b)); err != nil {
t.Fatalf("write record: %v", err)
}
@@ -226,36 +279,44 @@ func writeRecordsFile(t *testing.T, path string, recs []types.ExportRecord) {
func buildCheckpointScenarioRecords(t *testing.T) []types.ExportRecord {
t.Helper()
+
pub, priv, err := ed25519.GenerateKey(rand.Reader)
+
if err != nil {
t.Fatalf("generate key: %v", err)
}
+
mk := func(seq uint64, did, prev string) types.ExportRecord {
unsigned := map[string]any{
"did": did,
"didDoc": map[string]any{"id": did, "seq": seq},
"publicKey": base64.RawURLEncoding.EncodeToString(pub),
}
+
if prev != "" {
unsigned["prev"] = prev
}
+
payload, _ := json.Marshal(unsigned)
canon, _ := types.CanonicalizeJSON(payload)
sig := ed25519.Sign(priv, canon)
op := map[string]any{}
+
for k, v := range unsigned {
op[k] = v
}
+
op["sigPayload"] = base64.RawURLEncoding.EncodeToString(canon)
op["sig"] = base64.RawURLEncoding.EncodeToString(sig)
raw, _ := json.Marshal(op)
opCanon, _ := types.CanonicalizeJSON(raw)
cid := types.ComputeDigestCID(opCanon)
+
return types.ExportRecord{Seq: seq, DID: did, CID: cid, Operation: raw}
}
-
r1 := mk(1, "did:plc:alice", "")
r2 := mk(2, "did:plc:bob", "")
r3 := mk(3, "did:plc:alice", r1.CID)
+
return []types.ExportRecord{r1, r2, r3}
}