134 lines
4.8 KiB
Go
134 lines
4.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/logging"
|
|
)
|
|
|
|
func captureRecoveryLogs(t *testing.T) *bytes.Buffer {
|
|
t.Helper()
|
|
prior := slog.Default()
|
|
out := new(bytes.Buffer)
|
|
logging.Configure("agent", false, out)
|
|
t.Cleanup(func() { slog.SetDefault(prior) })
|
|
return out
|
|
}
|
|
|
|
func TestRecoveryLogsEachOrphanCauseAndLeavesOriginalFiles(t *testing.T) {
|
|
out := captureRecoveryLogs(t)
|
|
root := privateRecoveryRoot(t)
|
|
recovery := RecordingRecovery{Root: root, writeState: func(string, any) error { return errors.New("metadata disk full") }}
|
|
entry := failedRecordingFixture()
|
|
if _, err := recovery.SaveFailure([]byte("RIFF private-audio"), entry, &UploadHTTPError{StatusCode: 503}); err == nil {
|
|
t.Fatal("fault hidden")
|
|
}
|
|
out.Reset()
|
|
recovery.writeState = nil
|
|
_, err := recovery.RecoverDue(context.Background(), nil, nil)
|
|
if !errors.Is(err, ErrRecordingRecoveryIncomplete) {
|
|
t.Fatal(err)
|
|
}
|
|
for _, part := range []string{"recovery_validate", "recovery_digest", "original audio has no call information"} {
|
|
if !strings.Contains(out.String(), part) {
|
|
t.Fatalf("missing %s: %s", part, out.String())
|
|
}
|
|
}
|
|
if strings.Contains(out.String(), "private-audio") {
|
|
t.Fatal("audio leaked")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, entry.Bucket, filepath.FromSlash(entry.ObjectKey), "recording.wav")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRecoveryLogsGrantCauseIdentityAndPersistedBackoff(t *testing.T) {
|
|
out := captureRecoveryLogs(t)
|
|
now := time.Date(2026, 10, 1, 10, 0, 0, 0, time.UTC)
|
|
recovery := RecordingRecovery{Root: privateRecoveryRoot(t), Now: func() time.Time { return now }}
|
|
entry, err := recovery.SaveFailure([]byte("RIFF private-audio"), failedRecordingFixture(), &UploadHTTPError{StatusCode: 503})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now = now.Add(time.Minute)
|
|
out.Reset()
|
|
expected := errors.New("temporary Dispatcher outage")
|
|
_, err = recovery.RecoverDue(context.Background(), func(context.Context, RecordingRecoveryEntry) (RecoveryTarget, error) {
|
|
return RecoveryTarget{}, expected
|
|
}, func(context.Context, RecordingRecoveryEntry) error { return nil })
|
|
if !errors.Is(err, expected) {
|
|
t.Fatalf("original grant failure was lost: %v", err)
|
|
}
|
|
for _, part := range []string{entry.SourceEventID, entry.RecordingID, entry.UploadID, "request_upload_grant", "temporary Dispatcher outage", "next_attempt_at", `"attempts":2`} {
|
|
if !strings.Contains(out.String(), part) {
|
|
t.Fatalf("missing %s: %s", part, out.String())
|
|
}
|
|
}
|
|
pending, err := recovery.Load(entry.Bucket, entry.ObjectKey)
|
|
if err != nil || pending.Attempts != 2 || pending.State != "retry_pending" {
|
|
t.Fatal(pending, err)
|
|
}
|
|
out.Reset()
|
|
now = now.Add(time.Minute)
|
|
if _, err = recovery.RecoverDue(context.Background(), nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.String(), "retry_wait") || !strings.Contains(out.String(), entry.SourceEventID) {
|
|
t.Fatal(out.String())
|
|
}
|
|
}
|
|
|
|
func TestResultJournalLogsOriginalDecodeCause(t *testing.T) {
|
|
out := captureRecoveryLogs(t)
|
|
j := ResultJournal{Root: privateRecoveryRoot(t)}
|
|
entry := ResultEntry{SourceEventID: "corrupt-result-event", DispatcherID: "11111111-1111-4111-8111-111111111111", TenantID: 1, Payload: []byte(`{"recording":{}}`)}
|
|
if err := j.SaveReady(entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path, err := j.path(entry.SourceEventID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = os.WriteFile(path, []byte("{"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = j.Recover(context.Background(), func(context.Context, ResultEntry) error { return nil })
|
|
if err == nil || !strings.Contains(err.Error(), "unexpected end of JSON input") {
|
|
t.Fatalf("original decode reason was hidden: %v", err)
|
|
}
|
|
for _, part := range []string{"decode_original_result", "unexpected end of JSON input", "event_digest"} {
|
|
if !strings.Contains(out.String(), part) {
|
|
t.Fatalf("missing %s: %s", part, out.String())
|
|
}
|
|
}
|
|
if _, err = os.Stat(path); err != nil {
|
|
t.Fatal("corrupt original file must remain", err)
|
|
}
|
|
}
|
|
|
|
func TestResultJournalLogsIndividualCauseAndOriginalIdentity(t *testing.T) {
|
|
out := captureRecoveryLogs(t)
|
|
j := ResultJournal{Root: privateRecoveryRoot(t)}
|
|
entry := ResultEntry{SourceEventID: "result-log-event", DispatcherID: "11111111-1111-4111-8111-111111111111", TenantID: 1, Payload: []byte(`{"recording":{}}`)}
|
|
if err := j.SaveReady(entry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected := errors.New("result persistence unavailable")
|
|
if err := j.Recover(context.Background(), func(context.Context, ResultEntry) error { return expected }); !errors.Is(err, expected) {
|
|
t.Fatal(err)
|
|
}
|
|
for _, part := range []string{"result-log-event", "report_original_result", "result persistence unavailable", "tenant_id"} {
|
|
if !strings.Contains(out.String(), part) {
|
|
t.Fatalf("missing %s: %s", part, out.String())
|
|
}
|
|
}
|
|
}
|