package xmppsig import ( "context" "fmt" "testing" "time" "github.com/pion/webrtc/v4" "gosrc.io/xmpp/stanza" ) func TestExtractServices_ProsodyResponse(t *testing.T) { // verbatim shape from prosody 0.12 mod_external_services raw := []byte(`` + `` + `` + `` + `` + ``) got, err := extractServices(raw) if err != nil { t.Fatalf("extractServices: %v", err) } if len(got) != 2 { t.Fatalf("want 2 services, got %d: %+v", len(got), got) } if got[0] != (Service{Type: "stun", Host: "172.18.0.1", Port: 3478, Transport: "udp"}) { t.Errorf("STUN entry mismatch: %+v", got[0]) } if got[1] != (Service{ Type: "turn", Host: "172.18.0.1", Port: 3478, Transport: "udp", Username: "1779481470", Password: "F5aXytaIwbEJWt6FJ66OqeEQtvw=", }) { t.Errorf("TURN entry mismatch: %+v", got[1]) } } func TestToICEServers_StunAndTurn(t *testing.T) { in := []Service{ {Type: "stun", Host: "stun.example.com", Port: 3478, Transport: "udp"}, {Type: "turn", Host: "turn.example.com", Port: 3478, Transport: "udp", Username: "u", Password: "p"}, {Type: "turns", Host: "turn.example.com", Port: 5349, Transport: "tcp", Username: "u", Password: "p"}, } out := ToICEServers(in) if len(out) != 3 { t.Fatalf("want 3 servers, got %d", len(out)) } if out[0].URLs[0] != "stun:stun.example.com:3478" { t.Errorf("STUN URL mismatch: %q", out[0].URLs[0]) } if out[1].URLs[0] != "turn:turn.example.com:3478?transport=udp" { t.Errorf("TURN URL mismatch: %q", out[1].URLs[0]) } if out[1].Username != "u" || out[1].Credential != "p" { t.Errorf("TURN creds mismatch: user=%q cred=%v", out[1].Username, out[1].Credential) } if out[1].CredentialType != webrtc.ICECredentialTypePassword { t.Errorf("TURN cred type: want Password, got %v", out[1].CredentialType) } if out[2].URLs[0] != "turns:turn.example.com:5349?transport=tcp" { t.Errorf("TURNS URL mismatch: %q", out[2].URLs[0]) } } // pion rejects the whole PC on any creds-less turn URL, so a single // misconfigured prosody entry must not poison every call func TestToICEServers_SkipsBrokenAndUnknown(t *testing.T) { in := []Service{ {Type: "stun", Host: "h", Port: 3478, Transport: "udp"}, {Type: "turn", Host: "h", Port: 3478, Transport: "udp", Username: "u", Password: "p"}, // turns with creds: kept {Type: "turns", Host: "h", Port: 5349, Transport: "tcp", Username: "u", Password: "p"}, // turn with no creds: dropped {Type: "turn", Host: "broken", Port: 3478, Transport: "udp"}, // turns with no creds (prosody algorithm-omission bug): dropped {Type: "turns", Host: "broken", Port: 5349, Transport: "tcp"}, // turn with username but no password: dropped (defensive) {Type: "turn", Host: "half", Port: 3478, Transport: "udp", Username: "u"}, // unknown type: dropped {Type: "ftp", Host: "h", Port: 21, Transport: "tcp"}, } out := ToICEServers(in) if len(out) != 3 { t.Fatalf("want 3 servers (stun + turn-with-creds + turns-with-creds), got %d: %+v", len(out), out) } for _, srv := range out { for _, u := range srv.URLs { if (containsScheme(u, "turn:") || containsScheme(u, "turns:")) && (srv.Username == "" || srv.Credential == nil || srv.Credential == "") { t.Errorf("TURN entry kept without creds: %+v", srv) } } } } func containsScheme(url, scheme string) bool { return len(url) >= len(scheme) && url[:len(scheme)] == scheme } // minimal Sender for QueryServices E2E against a hand-crafted response type fakeSender struct { sent []stanza.Packet reply func(*stanza.IQ) stanza.IQ failSend bool } func (f *fakeSender) Send(p stanza.Packet) error { if f.failSend { return fmt.Errorf("simulated send failure") } f.sent = append(f.sent, p) return nil } func (f *fakeSender) SendRaw(string) error { return nil } func (f *fakeSender) SendIQ(_ context.Context, iq *stanza.IQ) (chan stanza.IQ, error) { ch := make(chan stanza.IQ, 1) if err := f.Send(iq); err != nil { return nil, err } if f.reply != nil { ch <- f.reply(iq) } return ch, nil } func TestQueryServices_ParsesReply(t *testing.T) { fs := &fakeSender{ reply: func(req *stanza.IQ) stanza.IQ { respPtr, _ := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeResult, From: req.To, To: req.From, Id: req.Id, }) respPtr.Payload = &servicesQuery{Services: []serviceElem{ {Type: "stun", Host: "1.2.3.4", Port: "3478", Transport: "udp"}, {Type: "turn", Host: "1.2.3.4", Port: "3478", Transport: "udp", Username: "u", Password: "p"}, }} return *respPtr }, } got, err := QueryServices(fs, "tlgrm.example.com", "example.com", time.Second) if err != nil { t.Fatalf("QueryServices: %v", err) } if len(got) != 2 { t.Fatalf("want 2, got %d: %+v", len(got), got) } if got[0].Type != "stun" || got[1].Type != "turn" { t.Errorf("unexpected service types: %+v", got) } if got[1].Username != "u" || got[1].Password != "p" { t.Errorf("TURN creds lost: %+v", got[1]) } } func TestQueryServices_Timeout(t *testing.T) { // no reply hook -> never delivers, must hit context timeout fs := &fakeSender{} _, err := QueryServices(fs, "tlgrm.example.com", "example.com", 50*time.Millisecond) if err == nil { t.Error("expected timeout error") } } func TestQueryServices_ErrorIQ(t *testing.T) { fs := &fakeSender{ reply: func(req *stanza.IQ) stanza.IQ { respPtr, _ := stanza.NewIQ(stanza.Attrs{ Type: stanza.IQTypeError, From: req.To, To: req.From, Id: req.Id, }) return *respPtr }, } if _, err := QueryServices(fs, "tlgrm.example.com", "example.com", time.Second); err == nil { t.Error("expected error on ") } }