Skip to content

Commit b41f3e9

Browse files
committed
test(decision): harden receipt journal refresh
1 parent 8271ce9 commit b41f3e9

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

decision/receipt_export_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"encoding/json"
88
"net/http"
99
"net/http/httptest"
10+
"os"
1011
"path/filepath"
12+
"strings"
1113
"sync/atomic"
1214
"testing"
1315
)
@@ -50,6 +52,27 @@ func TestReceiptExporterDiscoversEvidenceAppendedByAnotherProcess(t *testing.T)
5052
}
5153
}
5254

55+
func TestReceiptExporterSurfacesIncompleteExternalJournalRecord(t *testing.T) {
56+
t.Parallel()
57+
path := filepath.Join(t.TempDir(), "receipts.jsonl")
58+
journal, err := OpenReceiptJournal(path)
59+
if err != nil {
60+
t.Fatal(err)
61+
}
62+
exporter, err := NewReceiptExporter(ReceiptExporterConfig{
63+
Journal: journal, Endpoint: "http://127.0.0.1:1/receipts", AckPath: filepath.Join(t.TempDir(), "acks"),
64+
})
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
if err := os.WriteFile(path, []byte("{"), 0o600); err != nil {
69+
t.Fatal(err)
70+
}
71+
if err := exporter.ExportOnce(context.Background()); err == nil || !strings.Contains(err.Error(), "refresh receipt journal") {
72+
t.Fatalf("incomplete external journal error=%v", err)
73+
}
74+
}
75+
5376
func TestReceiptExporterRetriesAndAcknowledgesSignedEvidence(t *testing.T) {
5477
t.Parallel()
5578
journal, err := OpenReceiptJournal(filepath.Join(t.TempDir(), "receipts.jsonl"))

decision/receipt_journal_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import (
66
"context"
77
"crypto/ed25519"
88
"crypto/rand"
9+
"encoding/json"
910
"os"
1011
"path/filepath"
12+
"strings"
1113
"testing"
1214
"time"
15+
16+
"github.com/pilot-protocol/common/fsutil"
1317
)
1418

1519
func journalReceipt(t *testing.T, observedAt int64, result EnforcementResult) Receipt {
@@ -107,3 +111,92 @@ func TestReceiptJournalRejectsUnsignedCorruptAndUnsafeFiles(t *testing.T) {
107111
t.Fatal("symlink journal was accepted")
108112
}
109113
}
114+
115+
func TestReceiptJournalRefreshRejectsUnsafeExternalChanges(t *testing.T) {
116+
t.Parallel()
117+
if err := (*ReceiptJournal)(nil).Refresh(); err == nil {
118+
t.Fatal("nil journal refresh succeeded")
119+
}
120+
t.Run("incomplete record", func(t *testing.T) {
121+
path := filepath.Join(t.TempDir(), "receipts.jsonl")
122+
journal, err := OpenReceiptJournal(path)
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
if err := os.WriteFile(path, []byte("{"), 0o600); err != nil {
127+
t.Fatal(err)
128+
}
129+
if err := journal.Refresh(); err == nil || !strings.Contains(err.Error(), "incomplete trailing record") {
130+
t.Fatalf("incomplete refresh error=%v", err)
131+
}
132+
})
133+
t.Run("truncated journal", func(t *testing.T) {
134+
path := filepath.Join(t.TempDir(), "receipts.jsonl")
135+
journal, err := OpenReceiptJournal(path)
136+
if err != nil {
137+
t.Fatal(err)
138+
}
139+
if err := journal.AppendReceipt(context.Background(), journalReceipt(t, 1785500000, Enforced)); err != nil {
140+
t.Fatal(err)
141+
}
142+
if err := journal.Refresh(); err != nil {
143+
t.Fatal(err)
144+
}
145+
if err := os.Truncate(path, 0); err != nil {
146+
t.Fatal(err)
147+
}
148+
if err := journal.Refresh(); err == nil || !strings.Contains(err.Error(), "truncated") {
149+
t.Fatalf("truncated refresh error=%v", err)
150+
}
151+
})
152+
t.Run("symlink replacement", func(t *testing.T) {
153+
directory := t.TempDir()
154+
path := filepath.Join(directory, "receipts.jsonl")
155+
journal, err := OpenReceiptJournal(path)
156+
if err != nil {
157+
t.Fatal(err)
158+
}
159+
target := filepath.Join(directory, "target.jsonl")
160+
if err := os.WriteFile(target, nil, 0o600); err != nil {
161+
t.Fatal(err)
162+
}
163+
if err := os.Symlink(target, path); err != nil {
164+
t.Fatal(err)
165+
}
166+
if err := journal.Refresh(); err == nil || !strings.Contains(err.Error(), "symlink") {
167+
t.Fatalf("symlink refresh error=%v", err)
168+
}
169+
})
170+
}
171+
172+
func TestReceiptJournalRefreshRejectsConflictingExternalRecord(t *testing.T) {
173+
t.Parallel()
174+
path := filepath.Join(t.TempDir(), "receipts.jsonl")
175+
journal, err := OpenReceiptJournal(path)
176+
if err != nil {
177+
t.Fatal(err)
178+
}
179+
receipt := journalReceipt(t, 1785500000, Enforced)
180+
if err := journal.AppendReceipt(context.Background(), receipt); err != nil {
181+
t.Fatal(err)
182+
}
183+
if err := journal.Refresh(); err != nil {
184+
t.Fatal(err)
185+
}
186+
conflict := receipt
187+
conflict.ObservedAt++
188+
_, privateKey, _ := ed25519.GenerateKey(rand.Reader)
189+
if err := conflict.Sign(privateKey); err != nil {
190+
t.Fatal(err)
191+
}
192+
body, err := json.Marshal(conflict)
193+
if err != nil {
194+
t.Fatal(err)
195+
}
196+
if err := fsutil.AppendSync(path, append(body, '\n')); err != nil {
197+
t.Fatal(err)
198+
}
199+
if err := journal.Refresh(); err == nil || !strings.Contains(err.Error(), "conflicting receipt journal id") {
200+
t.Fatalf("conflicting refresh error=%v", err)
201+
}
202+
}

0 commit comments

Comments
 (0)