package sipcall import ( "context" "errors" "git.ipao.vip/rogee/go-sip/internal/asterisk" "os" "path/filepath" "strings" "testing" "time" ) type fakeCall struct { closeErr error ended bool ctx context.Context } func (c *fakeCall) Close() error { return c.closeErr } func (c *fakeCall) EndConfirmed() bool { return c.ended } func (c *fakeCall) Done() context.Context { return c.ctx } type fakeHost struct { busy, applyErr, dialErr, restoreErr error calls, restores int call *fakeCall } func (h *fakeHost) CheckIdle(context.Context) error { return h.busy } func (h *fakeHost) Apply(context.Context, Config) (func() error, error) { return func() error { h.restores++; return h.restoreErr }, h.applyErr } func (h *fakeHost) Dial(ctx context.Context, request asterisk.NativeDial) (testCall, error) { h.calls++ if h.dialErr != nil { return nil, h.dialErr } if err := request.BindSIPCall(request.ExecutionID, "sip-id-123"); err != nil { return nil, err } if h.call == nil { h.call = &fakeCall{ended: true, ctx: context.Background()} } return h.call, nil } func (h *fakeHost) Close() error { return nil } type fakeCapture struct { stops int err error } func (c *fakeCapture) Check() error { return nil } func (c *fakeCapture) Stop(_ context.Context, _ string) (Evidence, error) { c.stops++ return Evidence{PCAP: "capture.pcap", SIPTimeline: "sip.tsv", Packets: 10, FinalINVITECode: 200}, c.err } func fixtureConfig(t *testing.T) Config { t.Helper() c, err := ParseConfig(strings.NewReader(validENV)) if err != nil { t.Fatal(err) } c.OutputDir = t.TempDir() c.ConfigDir = t.TempDir() if err := os.WriteFile(filepath.Join(c.ConfigDir, "pjsip.conf"), []byte("private config"), 0600); err != nil { t.Fatal(err) } if err := os.Chmod(c.OutputDir, 0700); err != nil { t.Fatal(err) } c.resultDir = c.OutputDir c.Hold = time.Millisecond return c } func TestRunRejectsInvalidConfigBeforeCaptureOrDial(t *testing.T) { c := fixtureConfig(t) c.ConfigDir = t.TempDir() if err := os.WriteFile(filepath.Join(c.ConfigDir, "pjsip.conf"), []byte("private config"), 0644); err != nil { t.Fatal(err) } h := &fakeHost{} captureStarted := false r, err := run(context.Background(), c, "18601010101", true, h, func(context.Context, Config, string) (captureSession, error) { captureStarted = true return &fakeCapture{}, nil }) if err == nil || captureStarted || h.calls != 0 || r != nil { t.Fatalf("unsafe configuration reached side effects: report=%+v, err=%v, capture=%v, calls=%d", r, err, captureStarted, h.calls) } entries, err := os.ReadDir(c.OutputDir) if err != nil || len(entries) != 0 { t.Fatalf("preflight created output: %v, %v", entries, err) } } func TestRunDefaultNeedsNoCaptureToolsAndDialsOnce(t *testing.T) { c := fixtureConfig(t) h := &fakeHost{} r, err := run(context.Background(), c, "18601010101", false, h, func(context.Context, Config, string) (captureSession, error) { t.Fatal("capture must be completely absent by default") return nil, nil }) if err != nil { t.Fatal(err) } if r.Status != "connected" || !r.EndConfirmed || r.CallID != "sip-id-123" || r.DialedCallee != "708918601010101" || h.calls != 1 || h.restores != 1 || r.Evidence != nil { t.Fatalf("wrong single call: %+v", r) } data, err := os.ReadFile(filepath.Join(r.Directory, "result.json")) if err != nil || !strings.Contains(string(data), "connected") { t.Fatalf("missing private result: %v", err) } info, _ := os.Stat(r.Directory) if info.Mode().Perm() != 0700 { t.Fatal("results must be private") } } func TestRunFailuresAreVisibleAndNeverRetry(t *testing.T) { for _, phase := range []string{"busy", "apply", "capture", "dial", "end", "restore", "decode"} { t.Run(phase, func(t *testing.T) { c := fixtureConfig(t) h := &fakeHost{} cap := &fakeCapture{} debug := phase == "capture" || phase == "decode" sentinel := errors.New("diagnostic failure") switch phase { case "busy": h.busy = sentinel case "apply": h.applyErr = sentinel case "dial": h.dialErr = sentinel case "end": h.call = &fakeCall{ctx: context.Background()} case "restore": h.restoreErr = sentinel case "decode": cap.err = sentinel } r, err := run(context.Background(), c, "18601010101", debug, h, func(context.Context, Config, string) (captureSession, error) { if phase == "capture" { return nil, sentinel } return cap, nil }) if err == nil || r.Status == "connected" || h.calls > 1 { t.Fatalf("failure disguised: %+v %v", r, err) } if (phase == "busy" || phase == "capture" || phase == "apply") && h.calls != 0 { t.Fatal("preflight failure dialed") } if phase == "end" && h.restores != 0 { t.Fatal("unknown live channel must retain its configuration") } if debug && phase == "decode" && cap.stops != 1 { t.Fatal("capture must always be finalized") } if _, err = os.Stat(filepath.Join(r.Directory, "result.json")); err != nil { t.Fatalf("failure detail missing: %v", err) } }) } } func TestRunDebugHasLinkedEvidenceAndCancellationDoesNotRedial(t *testing.T) { c := fixtureConfig(t) h := &fakeHost{} cap := &fakeCapture{} r, err := run(context.Background(), c, "18601010101", true, h, func(context.Context, Config, string) (captureSession, error) { return cap, nil }) if err != nil || r.Evidence == nil || cap.stops != 1 { t.Fatalf("evidence missing: %+v %v", r, err) } ctx, cancel := context.WithCancel(context.Background()) cancel() h = &fakeHost{} _, err = run(ctx, c, "18601010101", false, h, nil) if err == nil || h.calls != 0 { t.Fatal("cancelled invocation must not dial") } }