mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
The speaker firmware constructs the OAuth host by appending "oauth" to the first label of the configured streaming hostname (aftertouch.lan → aftertouchoauth.lan, used by both Spotify and Amazon Music token refresh). AfterTouch's DNS server previously only hijacked the hardcoded list of Bose hostnames, so operators self-hosting at a custom hostname had to add the OAuth alias themselves — and the amazon-music-oauth.md / spotify-overview.md docs incorrectly claimed the DNS server handled it automatically. ofthesun9 (#337) caught this via the worst variant: IP-based serverURL (192.168.0.30 → 192oauth.168.0.30), which is a malformed hostname no DNS resolver can answer for. There is no clean DNS workaround for the IP case — the operator must use a hostname. Three changes: - pkg/discovery/dns.go DeriveOAuthHostnames parses the configured serverURL, derives <first-label>oauth.<rest> when the host is a hostname (not IP), and adds it to the DNSDiscovery hijack list. IP serverURLs deliberately yield no derivation — the malformed name isn't worth handling and the new health check surfaces the trap. - New checks_oauth_target health check fires a Warning when serverURL is an IP literal, with a concrete example of the malformed name (`192oauth.168.0.30`) and a ManualCommand pointing at the switch. - amazon-music-oauth.md and spotify-overview.md rewritten: drop the false "automatic" claim, document the three resolution paths (AfterTouch DNS + speaker resolves via it / external LAN DNS / per-speaker /etc/hosts), and explicitly flag IP-based --server-url as incompatible with OAuth on either provider. Tests cover the derivation matrix (hostname / IPv4 / IPv6 / single label / empty / garbage URL), shouldIntercept's new behaviour (derived host hit, base host not auto-hijacked, case-insensitive), the health check's four states, and the malformed-host helper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
3.8 KiB
Go
131 lines
3.8 KiB
Go
package discovery
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDeriveOAuthHostnames(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
serverURL string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "hostname with single domain part",
|
|
serverURL: "https://aftertouch.lan:8443",
|
|
want: []string{"aftertouchoauth.lan"},
|
|
},
|
|
{
|
|
name: "hostname with multiple domain parts",
|
|
serverURL: "https://aftertouch.example.local:8443",
|
|
want: []string{"aftertouchoauth.example.local"},
|
|
},
|
|
{
|
|
name: "HTTP scheme also works",
|
|
serverURL: "http://aftertouch.lan:8000",
|
|
want: []string{"aftertouchoauth.lan"},
|
|
},
|
|
{
|
|
name: "Case is normalised to lower",
|
|
serverURL: "https://AfterTouch.LAN:8443",
|
|
want: []string{"aftertouchoauth.lan"},
|
|
},
|
|
{
|
|
name: "IPv4 yields no derivation (malformed result)",
|
|
serverURL: "https://192.168.0.30:8443",
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "IPv6 yields no derivation",
|
|
serverURL: "https://[fd00::1]:8443",
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "Single-label hostname yields no derivation",
|
|
serverURL: "https://aftertouch:8443",
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "Empty serverURL is a no-op",
|
|
serverURL: "",
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "Garbage URL is a no-op",
|
|
serverURL: ":::not a url",
|
|
want: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := DeriveOAuthHostnames(tc.serverURL)
|
|
if len(got) != len(tc.want) {
|
|
t.Fatalf("len mismatch: got %v, want %v", got, tc.want)
|
|
}
|
|
|
|
for i := range got {
|
|
if got[i] != tc.want[i] {
|
|
t.Errorf("index %d: got %q, want %q", i, got[i], tc.want[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShouldIntercept_DerivedHostnameFromHostnameServerURL(t *testing.T) {
|
|
d := NewDNSDiscovery([]string{"8.8.8.8"}, "192.0.2.10", "https://aftertouch.lan:8443")
|
|
|
|
// Bose hostnames still match by substring.
|
|
if !d.shouldIntercept("streamingoauth.bose.com") {
|
|
t.Errorf("expected Bose oauth host to be intercepted")
|
|
}
|
|
|
|
// Derived host matches exactly (case-insensitive).
|
|
if !d.shouldIntercept("aftertouchoauth.lan") {
|
|
t.Errorf("expected derived OAuth subdomain to be intercepted")
|
|
}
|
|
|
|
if !d.shouldIntercept("AFTERTOUCHOAUTH.LAN") {
|
|
t.Errorf("expected case-insensitive match on derived OAuth subdomain")
|
|
}
|
|
|
|
// Unrelated hosts are not hijacked.
|
|
if d.shouldIntercept("example.com") {
|
|
t.Errorf("unrelated host must not be intercepted")
|
|
}
|
|
|
|
// The base host (without -oauth) is NOT auto-hijacked — only the
|
|
// OAuth-derivation. Bose-substring filter and the operator's own
|
|
// migration handle the base host.
|
|
if d.shouldIntercept("aftertouch.lan") {
|
|
t.Errorf("base hostname must not be auto-intercepted; only the OAuth variant is derived")
|
|
}
|
|
}
|
|
|
|
func TestShouldIntercept_NoDerivationFromIPServerURL(t *testing.T) {
|
|
d := NewDNSDiscovery([]string{"8.8.8.8"}, "192.168.0.30", "https://192.168.0.30:8443")
|
|
|
|
if len(d.derivedHosts) != 0 {
|
|
t.Errorf("expected no derived hosts for IP-based serverURL, got %v", d.derivedHosts)
|
|
}
|
|
|
|
if d.shouldIntercept("192oauth.168.0.30") {
|
|
t.Errorf("malformed IP-derived OAuth name must not be intercepted (it's never a valid DNS query in the first place)")
|
|
}
|
|
}
|
|
|
|
func TestNewDNSDiscovery_LogsDerivationOnce(t *testing.T) {
|
|
// This is a smoke test — the constructor should not panic and should
|
|
// store the derivation. We don't capture the log output here (the
|
|
// dns.go init path uses package log.Printf and isn't easily diverted
|
|
// without test infrastructure), but we do confirm the derivedHosts
|
|
// field is populated as expected.
|
|
d := NewDNSDiscovery(nil, "192.0.2.10", "https://aftertouch.lan")
|
|
|
|
if len(d.derivedHosts) != 1 || !strings.Contains(d.derivedHosts[0], "oauth") {
|
|
t.Errorf("expected derivedHosts to carry the OAuth variant, got %v", d.derivedHosts)
|
|
}
|
|
}
|