129 lines
4.3 KiB
Go
129 lines
4.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCleanupProofMustPersistBeforeEitherOriginalIsDeleted(t *testing.T) {
|
|
r := &RecordingRecovery{Root: filepath.Join(t.TempDir(), "private")}
|
|
e := deliveredRecordingFixture(t, r)
|
|
_, audio, info, _ := r.paths(e.Bucket, e.ObjectKey)
|
|
r.writeState = func(path string, value any) error {
|
|
if filepath.Dir(path) == filepath.Join(r.Root, recordingCleanupDirectory) {
|
|
return errors.New("proof disk failure")
|
|
}
|
|
return writeJSONAtomic(path, value)
|
|
}
|
|
if _, err := r.Retry(context.Background(), e.Bucket, e.ObjectKey, nil, nil); err == nil {
|
|
t.Fatal("proof failure swallowed")
|
|
}
|
|
for _, path := range []string{audio, info} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("original lost before durable proof: %v", err)
|
|
}
|
|
}
|
|
r.writeState = nil
|
|
if _, err := r.Retry(context.Background(), e.Bucket, e.ObjectKey, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCleanupProofBlocksTargetReuseUntilFinalRemovalAndIsolatesFailures(t *testing.T) {
|
|
r := &RecordingRecovery{Root: filepath.Join(t.TempDir(), "private")}
|
|
e := deliveredRecordingFixture(t, r)
|
|
proof, _ := r.cleanupProofPath(e.Bucket, e.ObjectKey)
|
|
r.removeFile = func(path string) error {
|
|
if path == proof {
|
|
return errors.New("proof removal failure")
|
|
}
|
|
return os.Remove(path)
|
|
}
|
|
if _, err := r.Retry(context.Background(), e.Bucket, e.ObjectKey, nil, nil); err == nil {
|
|
t.Fatal("proof deletion failure swallowed")
|
|
}
|
|
dir, _, _, _ := r.paths(e.Bucket, e.ObjectKey)
|
|
if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal("original leaf deletion did not finish")
|
|
}
|
|
fresh := failedRecordingFixture()
|
|
if _, err := r.SaveFailure([]byte("new recording"), fresh, &UploadHTTPError{StatusCode: 503}); err == nil {
|
|
t.Fatal("new target overwritten before old proof cleanup")
|
|
}
|
|
other := fresh
|
|
other.SourceEventID = "independent-delivered"
|
|
other.ObjectKey = "tenant/call/independent.wav"
|
|
if _, err := r.SaveFailure([]byte("independent recording"), other, &UploadHTTPError{StatusCode: 503}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := r.Load(other.Bucket, other.ObjectKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other.State = "delivered"
|
|
other.Uploaded = &UploadResult{StatusCode: 200, SizeBytes: other.SizeBytes, SHA256: other.SHA256}
|
|
_, _, info, _ := r.paths(other.Bucket, other.ObjectKey)
|
|
if err := r.persistState(info, other); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count, err := r.CleanupDelivered(context.Background()); err == nil || count != 1 {
|
|
t.Fatalf("independent cleanup blocked: count=%d err=%v", count, err)
|
|
}
|
|
if _, err := os.Stat(proof); err != nil {
|
|
t.Fatal("failure lost completion receipt")
|
|
}
|
|
restarted := &RecordingRecovery{Root: r.Root}
|
|
if count, err := restarted.CleanupDelivered(context.Background()); err != nil || count != 1 {
|
|
t.Fatalf("final proof not resumed: count=%d err=%v", count, err)
|
|
}
|
|
if _, err := restarted.SaveFailure([]byte("new recording"), fresh, &UploadHTTPError{StatusCode: 503}); err != nil {
|
|
t.Fatal("cleaned identity cannot be reused", err)
|
|
}
|
|
}
|
|
|
|
func TestCorruptOrConflictingCleanupProofNeverDeletesOriginals(t *testing.T) {
|
|
for _, damage := range []string{"identity", "filename", "state", "receipt", "json"} {
|
|
t.Run(damage, func(t *testing.T) {
|
|
r := &RecordingRecovery{Root: filepath.Join(t.TempDir(), "private")}
|
|
e := deliveredRecordingFixture(t, r)
|
|
proof, err := r.preserveCleanupProof(e)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, audio, info, _ := r.paths(e.Bucket, e.ObjectKey)
|
|
switch damage {
|
|
case "identity":
|
|
e.SourceEventID = "wrong-original"
|
|
err = r.persistState(proof, e)
|
|
case "filename":
|
|
renamed := filepath.Join(filepath.Dir(proof), strings.Repeat("0", 64)+".json")
|
|
err = os.Rename(proof, renamed)
|
|
proof = renamed
|
|
case "state":
|
|
e.State = "uploaded_unreported"
|
|
err = r.persistState(proof, e)
|
|
case "receipt":
|
|
e.Uploaded.StatusCode = 500
|
|
err = r.persistState(proof, e)
|
|
case "json":
|
|
err = os.WriteFile(proof, []byte("broken original proof"), 0600)
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := r.resumeCleanupProofs(context.Background()); err == nil {
|
|
t.Fatal("invalid proof accepted")
|
|
}
|
|
for _, path := range []string{audio, info, proof} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("invalid association deleted a file: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|