checker-stun-turn/checker/turn_test.go
Pierre-Olivier Mercier 7c7706fe3f Initial commit
Adds a happyDomain checker that probes STUN/TURN servers end-to-end:
DNS/SRV discovery, UDP/TCP/TLS/DTLS dial, STUN binding + reflexive-addr
sanity, open-relay detection, authenticated TURN Allocate (long-term
creds or REST-API HMAC), public-relay check, CreatePermission + Send
round-trip through the relay, and optional ChannelBind.

Failing sub-tests carry a remediation string (`Fix`) that the HTML
report surfaces as a yellow headline callout and inline next to each
row. Mapping covers the most common coturn misconfigurations
(external-ip, relay-ip, lt-cred-mech, min-port/max-port, cert issues,
401 nonce drift, 441/442/486/508 allocation errors).

Implements sdk.EndpointDiscoverer (checker/discovery.go): every
stuns:/turns:/DTLS endpoint observed during Collect is published as a
DiscoveredEndpoint{Type: "tls"|"dtls"} so a downstream TLS checker can
verify certificates without re-parsing the observation.

Backed by pion/stun/v3 + pion/turn/v4 + pion/dtls/v3; SDK pinned to a
local replace until the EndpointDiscoverer interface ships in a tagged
release.
2026-04-26 19:55:05 +07:00

53 lines
1.7 KiB
Go

package checker
import (
"errors"
"fmt"
"testing"
"github.com/pion/stun/v3"
)
// TestStunErrorOf_PinPionFormat builds an error using pion's own
// ErrorCodeAttribute.String() so that any future change to the format
// pion/turn uses ("... (error <code>: <reason>)") makes this test fail
// loudly instead of silently breaking our diagnostic parsing.
func TestStunErrorOf_PinPionFormat(t *testing.T) {
cases := []struct {
name string
code stun.ErrorCode
reason string
wantCode int
wantReason string
}{
{"unauthorized", stun.CodeUnauthorized, "Unauthorized", 401, "Unauthorized"},
{"stale nonce", stun.CodeStaleNonce, "Stale Nonce", 438, "Stale Nonce"},
{"server error", stun.CodeServerError, "Server Error", 500, "Server Error"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
attr := stun.ErrorCodeAttribute{Code: tc.code, Reason: []byte(tc.reason)}
// Mirror pion/turn/v4@v4.0.0 client.go:296:
// fmt.Errorf("%s (error %s)", res.Type, code)
err := fmt.Errorf("error response (error %s)", attr)
gotCode, gotReason := stunErrorOf(err)
if gotCode != tc.wantCode || gotReason != tc.wantReason {
t.Errorf("stunErrorOf(%q) = (%d, %q); want (%d, %q): pion error format may have changed",
err.Error(), gotCode, gotReason, tc.wantCode, tc.wantReason)
}
})
}
}
func TestStunErrorOf_NoMatch(t *testing.T) {
code, reason := stunErrorOf(errors.New("plain error"))
if code != 0 {
t.Errorf("expected code 0 for unparseable error, got %d", code)
}
if reason != "plain error" {
t.Errorf("expected reason to fall back to message, got %q", reason)
}
if c, r := stunErrorOf(nil); c != 0 || r != "" {
t.Errorf("nil error: got (%d, %q), want (0, \"\")", c, r)
}
}