package audio import ( "sync" "testing" ) // shared between tg->xmpp and xmpp->tg tests type fakeNtg struct { mu sync.Mutex handler NtgFrameHandler unregistered int // count of cancel() invocations returned by OnFrame sentPCM [][]byte micCalls int clears int micCallCapture []bool // ordered record of capture-flag arg per SetExternalMicrophone call sendErr error // if set, SendMicrophonePCM returns this } // stashes handler so tests can dispatch directly; cancel just bumps a // counter (handler stays reachable so tests can exercise closed-flag guards) func (f *fakeNtg) OnFrame(h NtgFrameHandler) func() { f.mu.Lock() defer f.mu.Unlock() f.handler = h return func() { f.mu.Lock() defer f.mu.Unlock() f.unregistered++ } } func (f *fakeNtg) SetExternalMicrophone(_ int64, capture bool, _ uint32, _ uint8) error { f.mu.Lock() defer f.mu.Unlock() f.micCalls++ f.micCallCapture = append(f.micCallCapture, capture) return nil } func (f *fakeNtg) ClearStreams(int64) error { f.mu.Lock() defer f.mu.Unlock() f.clears++ return nil } func (f *fakeNtg) SendMicrophonePCM(_ int64, pcm []byte) error { cp := make([]byte, len(pcm)) copy(cp, pcm) f.mu.Lock() defer f.mu.Unlock() err := f.sendErr f.sentPCM = append(f.sentPCM, cp) return err } func (f *fakeNtg) sentCount() int { f.mu.Lock() defer f.mu.Unlock() return len(f.sentPCM) } func newTestTgToXmpp(t *testing.T) (*TgToXmpp, *fakeNtg) { t.Helper() n := &fakeNtg{} h, err := NewTgToXmpp(TgToXmppOptions{ Ntg: n, ChatID: 12345, }) if err != nil { t.Fatalf("NewTgToXmpp: %v", err) } return h, n } func TestTgToXmppStartConfiguresPlayback(t *testing.T) { h, n := newTestTgToXmpp(t) if err := h.Start(); err != nil { t.Fatalf("Start: %v", err) } defer h.Close() n.mu.Lock() defer n.mu.Unlock() if n.micCalls != 1 { t.Errorf("SetExternalMicrophone called %d times, want 1 (playback only)", n.micCalls) } if len(n.micCallCapture) != 1 || n.micCallCapture[0] { t.Errorf("expected capture=false (playback) for tg->xmpp, got %v", n.micCallCapture) } } func TestTgToXmppCloseStopsCallbackHandling(t *testing.T) { h, n := newTestTgToXmpp(t) if err := h.Start(); err != nil { t.Fatalf("Start: %v", err) } if err := h.Close(); err != nil { t.Fatalf("Close: %v", err) } // Post-close dispatches must be ignored. frame := PCMFrame{SSRC: 1, Data: make([]byte, NtgFrameBytes)} n.handler(h.opts.ChatID, []PCMFrame{frame, frame, frame, frame}) if got := len(h.acc); got != 0 { t.Errorf("closed half accumulated %d bytes, want 0", got) } } // mirrors ntgcalls' callback-per-goroutine dispatch; run with -race func TestTgToXmppFeedFrameConcurrent(t *testing.T) { h, _ := newTestTgToXmpp(t) const N = 100 var wg sync.WaitGroup wg.Add(N) for i := 0; i < N; i++ { go func() { defer wg.Done() f := PCMFrame{SSRC: 42, Data: make([]byte, NtgFrameBytes)} h.feedFrame(f) }() } wg.Wait() h.mu.Lock() defer h.mu.Unlock() if got := len(h.acc); got != 0 && got != NtgFrameBytes { t.Errorf("acc len=%d after %d concurrent feeds; want 0 or %d", got, N, NtgFrameBytes) } }