package agent import ( "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "strings" "sync/atomic" "testing" "time" "git.ipao.vip/rogee/go-sip/internal/media" ) func TestRecordingDeliveryCaptureFailureDescribesFailureWithoutInventingUpload(t *testing.T) { for _, tc := range []struct { name string cause error }{ {name: "bounded_capture_failed", cause: media.ErrWAVTooLarge}, {name: "empty_expected_recording"}, } { t.Run(tc.name, func(t *testing.T) { delivery, stub, completed, puts, root := testDirectRecordingDelivery(t, http.StatusOK) completed.WAV = nil completed.CaptureError = tc.cause if err := delivery.Complete(context.Background(), completed); err != nil { t.Fatal(err) } var result struct { Outcome string `json:"outcome"` ReasonMessage string `json:"reason_message"` Recording map[string]any `json:"recording"` } if err := json.Unmarshal(stub.resultBody, &result); err != nil { t.Fatal(err) } if !reflect.DeepEqual(stub.calls, []string{"end", "result"}) || puts.Load() != 0 || stub.resultProof != nil || result.Outcome != "answered" || !strings.Contains(result.ReasonMessage, "recording generation failed") || len(result.Recording) != 0 { t.Fatalf("failed capture invented upload or changed the actual call outcome: calls=%v puts=%d result=%+v", stub.calls, puts.Load(), result) } entries, err := os.ReadDir(root) if err != nil || len(entries) != 0 { t.Fatalf("no generated audio created a business file: entries=%d err=%v", len(entries), err) } }) } } func TestRecordingDeliveryRejectsInvalidWAVBeforeGrantOrPUT(t *testing.T) { delivery, stub, completed, puts, _ := testDirectRecordingDelivery(t, http.StatusOK) completed.WAV = bytes.Clone(completed.WAV) completed.WAV[0] = 'X' if err := delivery.Complete(context.Background(), completed); err == nil { t.Fatal("corrupt WAV was reported as a valid upload") } if !reflect.DeepEqual(stub.calls, []string{"end"}) || stub.asset != nil || puts.Load() != 0 { t.Fatalf("invalid media reached upload: calls=%v asset=%v puts=%d", stub.calls, stub.asset, puts.Load()) } } func TestRecordingDeliveryRecoveryDiskFailureNeverReportsUploaded(t *testing.T) { delivery, stub, completed, puts, root := testDirectRecordingDelivery(t, http.StatusServiceUnavailable) delivery.Recovery.writeState = func(string, any) error { return errors.New("injected disk full") } err := delivery.Complete(context.Background(), completed) var rejected *UploadHTTPError if !errors.As(err, &rejected) || !strings.Contains(err.Error(), "injected disk full") || puts.Load() != 1 { t.Fatalf("OSS and recovery-write failures were not both visible: err=%v puts=%d", err, puts.Load()) } original, readErr := os.ReadFile(filepath.Join(root, stub.grant.Bucket, filepath.FromSlash(stub.grant.ObjectKey), "recording.wav")) if readErr != nil || !bytes.Equal(original, completed.WAV) || len(stub.resultBody) != 0 { t.Fatalf("disk failure lost the original audio or forged a result: read=%v result_size=%d", readErr, len(stub.resultBody)) } if err := delivery.Complete(context.Background(), completed); !errors.Is(err, ErrRecordingRetryManaged) || puts.Load() != 1 { t.Fatalf("disk failure triggered an implicit second PUT: err=%v puts=%d", err, puts.Load()) } } type recordingFailingTransport func(*http.Request) (*http.Response, error) func (f recordingFailingTransport) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func TestRecordingDeliveryUnknownPUTKeepsManualBarrierAcrossRestart(t *testing.T) { delivery, stub, completed, _, root := testDirectRecordingDelivery(t, http.StatusOK) var attempts atomic.Int32 delivery.Recovery.Upload.HTTPClient = &http.Client{Transport: recordingFailingTransport(func(*http.Request) (*http.Response, error) { attempts.Add(1) return nil, errors.New("injected lost OSS response") })} if err := delivery.Complete(context.Background(), completed); !errors.Is(err, ErrUploadOutcomeUnknown) || attempts.Load() != 1 { t.Fatalf("unknown PUT was not retained as unknown: err=%v attempts=%d", err, attempts.Load()) } entry, err := delivery.Recovery.Load(stub.grant.Bucket, stub.grant.ObjectKey) if err != nil || entry.State != "outcome_unknown" || len(stub.resultBody) != 0 { t.Fatalf("unknown PUT fabricated an uploaded result: state=%s err=%v", entry.State, err) } restarted := testRecordingDelivery(stub) restarted.Recovery = &RecordingRecovery{Root: root, Upload: delivery.Recovery.Upload} _, err = restarted.RetryRecovered(context.Background(), entry.Bucket, entry.ObjectKey) if !errors.Is(err, ErrUploadOutcomeUnknown) || attempts.Load() != 1 || !reflect.DeepEqual(stub.calls, []string{"end", "grant"}) { t.Fatalf("restart retried unknown PUT or reported a final result: err=%v attempts=%d calls=%v", err, attempts.Load(), stub.calls) } } func TestRecordingDeliveryRestartRetriesOriginalAndReportsConfirmedPUTOnly(t *testing.T) { delivery, stub, completed, originalPUTs, root := testDirectRecordingDelivery(t, http.StatusServiceUnavailable) now := time.Now() delivery.Recovery.Now = func() time.Time { return now } delivery.Recovery.Upload.Now = func() time.Time { return now } if err := delivery.Complete(context.Background(), completed); err == nil || originalPUTs.Load() != 1 { t.Fatalf("original failed PUT not detected: err=%v puts=%d", err, originalPUTs.Load()) } original, err := delivery.Recovery.Load(stub.grant.Bucket, stub.grant.ObjectKey) if err != nil || original.State != "retry_pending" { t.Fatalf("original upload identity not persisted: state=%s err=%v", original.State, err) } now = now.Add(time.Minute) var recoveredPUTs atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { recoveredPUTs.Add(1) if r.Method != http.MethodPut || r.URL.Path != "/fresh-token" { http.Error(w, "changed request", http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) })) defer server.Close() stub.grant.TargetUrl = server.URL + "/fresh-token" stub.grant.ExpiresAtUnixMs = now.Add(15 * time.Minute).UnixMilli() stub.resultError = errors.New("injected Dispatcher result failure after confirmed PUT") freshRecovery := func() *RecordingRecovery { return &RecordingRecovery{Root: root, Now: func() time.Time { return now }, Upload: UploadClient{AllowInsecureHTTP: true, Now: func() time.Time { return now }}} } restarted := testRecordingDelivery(stub) restarted.Recovery = freshRecovery() _, err = restarted.RetryRecovered(context.Background(), original.Bucket, original.ObjectKey) if err == nil || recoveredPUTs.Load() != 1 || originalPUTs.Load() != 1 { t.Fatalf("first restarted retry did not preserve original grant/result boundary: err=%v recovered=%d initial=%d", err, recoveredPUTs.Load(), originalPUTs.Load()) } pending, err := restarted.Recovery.Load(original.Bucket, original.ObjectKey) if err != nil || pending.State != "uploaded_unreported" || stub.asset == nil || stub.asset.CallId != original.CallID || stub.asset.ExecutionId != original.SourceEventID || stub.asset.ChecksumSha256 != original.SHA256 || stub.asset.SizeBytes != original.SizeBytes || stub.asset.DurationMs != completed.DurationMS || !bytes.Equal(stub.resultBody, original.ResultPayload) || stub.resultProof == nil { t.Fatalf("confirmed PUT was not preserved before result delivery: state=%s asset=%+v proof=%+v err=%v", pending.State, stub.asset, stub.resultProof, err) } stub.resultError = nil again := testRecordingDelivery(stub) again.Recovery = freshRecovery() finished, err := again.RetryRecovered(context.Background(), original.Bucket, original.ObjectKey) if err != nil || finished.State != "delivered" || recoveredPUTs.Load() != 1 || !reflect.DeepEqual(stub.calls, []string{"end", "grant", "grant", "result", "result"}) { t.Fatalf("restart repeated confirmed PUT or lost result: state=%s calls=%v puts=%d err=%v", finished.State, stub.calls, recoveredPUTs.Load(), err) } }