Files
go-sip/internal/rpc/approved_recorded_real_test.go
T

289 lines
14 KiB
Go

package rpc
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.ipao.vip/rogee/go-sip/internal/asterisk"
"git.ipao.vip/rogee/go-sip/internal/callflow"
"git.ipao.vip/rogee/go-sip/internal/media"
)
type endedRealMedia struct{}
func (endedRealMedia) ReadPayload(context.Context) ([]byte, error) { return nil, io.EOF }
func (endedRealMedia) SendPCM16(context.Context, []byte, int) error { return nil }
func (endedRealMedia) Stats() media.RTPStats { return media.RTPStats{} }
func TestRealCallRequiresLiveCaptureBoundToSignedIdentity(t *testing.T) {
root := t.TempDir()
id, trunk, target := "event-1", "trunk-1", "15003164745"
marker := filepath.Join(root, id+".active")
if err := verifyCallEvidence(root, id, trunk, target); err == nil {
t.Fatal("missing SIP/RTP evidence arm must block the call")
}
if err := os.WriteFile(marker, []byte(fmt.Sprintf("%d\t%s\t%s\n", os.Getpid(), trunk, target)), 0640); err != nil {
t.Fatal(err)
}
if err := verifyCallEvidence(root, id, trunk, target); err == nil {
t.Fatal("a live process other than tcpdump must not arm real dialing")
}
if err := verifyCallEvidence(root, id, trunk, "15830461047"); err == nil {
t.Fatal("capture bound to a different callee cannot authorize a real call")
}
if err := verifyCallEvidence(root, "../event-1", trunk, target); err == nil {
t.Fatal("call ID cannot escape the evidence directory")
}
if err := os.WriteFile(marker, []byte(fmt.Sprintf("%d\t%s\t%s\n", 2147483647, trunk, target)), 0640); err != nil {
t.Fatal(err)
}
if err := verifyCallEvidence(root, id, trunk, target); err == nil {
t.Fatal("terminated tcpdump must block the call")
}
}
func TestCaptureProcessMustWriteThisCall(t *testing.T) {
id := "event-1"
args := []string{"/usr/bin/tcpdump", "-i", "any", "-nn", "-s0", "-U", "-w", "/var/lib/sip-go-agent/evidence/event-1/capture.pcap", "udp port 5060 or (udp portrange 10000-20000)"}
if !capturesCall(args, id) {
t.Fatal("the host script's tcpdump command must be recognized")
}
for _, bad := range [][]string{
{"/usr/bin/tcpdump", "-w", "/var/lib/sip-go-agent/evidence/other/capture.pcap"},
{"/usr/bin/tcpdump", "-w", "/var/lib/sip-go-agent/evidence/event-1/other.pcap"},
{"/usr/bin/tcpdump", "-w", "event-1/capture.pcap"},
{"/usr/bin/tcpdump", "/var/lib/sip-go-agent/evidence/event-1/capture.pcap"},
} {
if capturesCall(bad, id) {
t.Fatalf("unrelated capture command accepted: %q", bad)
}
}
}
func TestApprovedRecordedRealCallReportsOnlyEndedObservedCall(t *testing.T) {
fixture, stub, puts, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
var originate, hangup int
runner := &ApprovedRecordedRealCall{Mirror: asterisk.NewHEPMirror(),
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
Delivery: fixture.Delivery,
originator: func(ctx context.Context, dial asterisk.NativeDial) (realMedia, error) {
originate++
if dial.ExecutionID != approved.SourceEventID || dial.DialedCallee != approved.DialedCallee || dial.CallerID != approved.CallerID || dial.TrunkID != approved.SelectedTrunkID || dial.MediaPayloadType != 118 {
t.Errorf("signed SIP identity or media profile changed: %+v", dial)
}
return realMedia{ctx: ctx, session: endedRealMedia{}, close: func() error { hangup++; return nil }}, nil
},
}
err := runner.Run(context.Background(), approved)
if err == nil || originate != 1 || hangup != 1 || strings.Join(stub.calls, ",") != "end,result" || puts.Load() != 0 {
t.Fatalf("real call must end and report actual media failure once: err=%v originate=%d hangup=%d calls=%v puts=%d", err, originate, hangup, stub.calls, puts.Load())
}
var result struct {
Outcome string `json:"outcome"`
Recording map[string]any `json:"recording"`
}
if err := json.Unmarshal(stub.result, &result); err != nil || result.Outcome != "failed" || len(result.Recording) != 0 {
t.Fatalf("empty media must not become an answered recording: result=%+v err=%v", result, err)
}
}
func TestApprovedRecordedRealCallReportsConfirmedMediaSetupFailure(t *testing.T) {
fixture, stub, puts, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
var originate, hangup int
runner := &ApprovedRecordedRealCall{Mirror: asterisk.NewHEPMirror(),
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
Delivery: fixture.Delivery,
originator: func(ctx context.Context, _ asterisk.NativeDial) (realMedia, error) {
originate++
return realMedia{ctx: ctx, close: func() error { hangup++; return nil }}, nil // answered channel, but no RTP capture session
},
}
err := runner.Run(context.Background(), approved)
if err == nil || originate != 1 || hangup != 1 || strings.Join(stub.calls, ",") != "end,result" || puts.Load() != 0 {
t.Fatalf("confirmed ended call without RTP still needs true failed result and empty recording: err=%v originate=%d hangup=%d calls=%v puts=%d", err, originate, hangup, stub.calls, puts.Load())
}
var result struct {
Outcome string `json:"outcome"`
Recording map[string]any `json:"recording"`
ReasonMessage string `json:"reason_message"`
}
if err := json.Unmarshal(stub.result, &result); err != nil || result.Outcome != "failed" || len(result.Recording) != 0 || result.ReasonMessage == "" {
t.Fatalf("false success after media setup failure: %+v %v", result, err)
}
}
func TestApprovedRecordedRealCallMissingMediaWithUnknownHangupDoesNotReport(t *testing.T) {
fixture, stub, _, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
runner := &ApprovedRecordedRealCall{Mirror: asterisk.NewHEPMirror(),
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
Delivery: fixture.Delivery,
originator: func(ctx context.Context, _ asterisk.NativeDial) (realMedia, error) {
return realMedia{ctx: ctx, close: func() error { return errors.New("hangup response missing") }}, nil
},
}
if err := runner.Run(context.Background(), approved); err == nil || len(stub.calls) != 0 {
t.Fatalf("ambiguous hangup cannot be reported as a confirmed end: err=%v calls=%v", err, stub.calls)
}
}
func TestApprovedRecordedRealCallDoesNotReportUnknownHangup(t *testing.T) {
fixture, stub, puts, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
var originate, hangup int
runner := &ApprovedRecordedRealCall{Mirror: asterisk.NewHEPMirror(),
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
Delivery: fixture.Delivery,
originator: func(ctx context.Context, _ asterisk.NativeDial) (realMedia, error) {
originate++
return realMedia{ctx: ctx, session: endedRealMedia{}, close: func() error { hangup++; return errors.New("ARI hangup outcome unknown") }}, nil
},
}
if err := runner.Run(context.Background(), approved); err == nil || originate != 1 || hangup != 1 || len(stub.calls) != 0 || puts.Load() != 0 {
t.Fatalf("unknown hangup must not forge call end or retry: err=%v originate=%d hangup=%d calls=%v puts=%d", err, originate, hangup, stub.calls, puts.Load())
}
}
func hepRealResultFrame(callID, payload string) []byte {
b := []byte("HEP3\x00\x00")
for _, chunk := range []struct {
kind uint16
body []byte
}{{11, []byte{1}}, {17, []byte(callID)}, {15, []byte(payload)}} {
p := make([]byte, 6+len(chunk.body))
binary.BigEndian.PutUint16(p[2:4], chunk.kind)
binary.BigEndian.PutUint16(p[4:6], uint16(len(p)))
copy(p[6:], chunk.body)
b = append(b, p...)
}
binary.BigEndian.PutUint16(b[4:6], uint16(len(b)))
return b
}
func TestApprovedRecordedRealCallReportsExactSIPResponseBeforeAnswer(t *testing.T) {
fixture, stub, _, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
mirror := asterisk.NewHEPMirror()
const callID = "isolated-480"
const common = "Via: SIP/2.0/UDP 127.0.0.1;branch=z9hG4bK-real\r\nFrom: <sip:a@localhost>;tag=x\r\nTo: <sip:b@localhost>\r\nCall-ID: isolated-480\r\nCSeq: 1 INVITE\r\nContent-Length: 0\r\n\r\n"
invite := "INVITE sip:b@localhost SIP/2.0\r\n" + common
raw := "SIP/2.0 480 Temporarily Unavailable\r\n" + common
runner := &ApprovedRecordedRealCall{
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
Mirror: mirror, Delivery: fixture.Delivery,
originator: func(_ context.Context, dial asterisk.NativeDial) (realMedia, error) {
if dial.BindSIPCall == nil || dial.BindSIPCall(dial.ExecutionID, callID) != nil {
t.Fatal("real dial did not bind the original SIP Call-ID")
}
for _, sip := range []string{invite, raw} {
if err := mirror.Observe(hepRealResultFrame(callID, sip)); err != nil {
t.Fatal(err)
}
}
return realMedia{}, &asterisk.NativeCallFailure{Phase: "answer_wait", ConfirmedEnd: true, Cause: errors.New("unanswered")}
},
}
if err := runner.Run(context.Background(), approved); err == nil || strings.Join(stub.calls, ",") != "end,result" {
t.Fatalf("verified SIP end did not reach real result: %v calls=%v", err, stub.calls)
}
var result struct {
ReasonCode *int `json:"reason_code"`
StatusLine string `json:"status_line"`
Raw string `json:"raw"`
}
if err := json.Unmarshal(stub.result, &result); err != nil || result.ReasonCode == nil || *result.ReasonCode != 480 || result.StatusLine != "SIP/2.0 480 Temporarily Unavailable" || result.Raw != raw {
t.Fatalf("original SIP 480 not delivered verbatim: %+v err=%v", result, err)
}
}
func TestApprovedRecordedRealCallReportsVerifiedPreAnswerEndWithoutRecording(t *testing.T) {
fixture, stub, puts, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
originate := 0
runner := &ApprovedRecordedRealCall{Mirror: asterisk.NewHEPMirror(),
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second, Delivery: fixture.Delivery,
originator: func(context.Context, asterisk.NativeDial) (realMedia, error) {
originate++
return realMedia{}, &asterisk.NativeCallFailure{Phase: "answer_wait", ConfirmedEnd: true, Cause: errors.New("no answer")}
},
}
if err := runner.Run(context.Background(), approved); err == nil || originate != 1 || strings.Join(stub.calls, ",") != "end,result" || puts.Load() != 0 {
t.Fatalf("confirmed pre-answer end must report actual failure once without recording: err=%v originate=%d calls=%v puts=%d", err, originate, stub.calls, puts.Load())
}
var result struct {
Outcome string `json:"outcome"`
Recording map[string]any `json:"recording"`
ReasonMessage string `json:"reason_message"`
ReasonCode *int `json:"reason_code"`
Raw *string `json:"raw"`
SIPCaptureError *string `json:"sip_capture_error"`
}
if err := json.Unmarshal(stub.result, &result); err != nil || result.Outcome != "failed" || len(result.Recording) != 0 || result.ReasonMessage != "call ended before answer" || result.ReasonCode != nil || result.Raw != nil || result.SIPCaptureError == nil || *result.SIPCaptureError == "" {
t.Fatalf("pre-answer SIP rejection must not be recorded as answered: result=%+v err=%v", result, err)
}
}
func TestApprovedRecordedRealCallDoesNotReportUnverifiedPreAnswerEnd(t *testing.T) {
fixture, stub, _, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
runner := &ApprovedRecordedRealCall{Mirror: asterisk.NewHEPMirror(),
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second, Delivery: fixture.Delivery,
originator: func(context.Context, asterisk.NativeDial) (realMedia, error) {
return realMedia{}, &asterisk.NativeCallFailure{Phase: "answer_wait", Cause: errors.New("channel state unavailable")}
},
}
if err := runner.Run(context.Background(), approved); err == nil || len(stub.calls) != 0 {
t.Fatalf("unverified call end must remain occupied: err=%v calls=%v", err, stub.calls)
}
}
func TestApprovedRecordedRealCallCannotOriginateTwiceOrReportUnknownOrigination(t *testing.T) {
fixture, stub, _, _, approved := recordedMockFixture(t, nil, time.Second)
approved.CallerID, approved.DialedCallee, approved.RingTimeout = "BD93205882", "7089"+approved.Callee, time.Second
originate := 0
runner := &ApprovedRecordedRealCall{Mirror: asterisk.NewHEPMirror(),
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
Delivery: fixture.Delivery,
originator: func(context.Context, asterisk.NativeDial) (realMedia, error) {
originate++
return realMedia{}, errors.New("ARI request outcome unknown")
},
}
prepared, err := runner.Prepare(approved)
if err != nil {
t.Fatal(err)
}
if err := prepared(context.Background()); err == nil {
t.Fatal("uncertain origination must not become a completed call")
}
if err := prepared(context.Background()); err == nil || originate != 1 || len(stub.calls) != 0 {
t.Fatalf("same accepted instruction must not reoriginate or report: err=%v originate=%d calls=%v", err, originate, stub.calls)
}
}
func TestApprovedRecordedRealCallRejectsMissingDeliveryBeforeOrigination(t *testing.T) {
_, _, _, _, approved := recordedMockFixture(t, nil, time.Second)
called := false
runner := &ApprovedRecordedRealCall{
MaxWAVBytes: 4096, MediaPayloadType: 118, ReportTimeout: time.Second,
originator: func(context.Context, asterisk.NativeDial) (realMedia, error) {
called = true
return realMedia{session: callflow.NewMemorySession(nil)}, nil
},
}
if _, err := runner.Prepare(approved); err == nil || called {
t.Fatalf("no validated delivery must block before originating: err=%v called=%v", err, called)
}
}