Files
go-sip/internal/agent/recording_cleanup_proof_test.go
T

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)
}
}
})
}
}