@@ -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
1519func 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