Domain names become an allocatable resource with one authority. A new core module (schema `domains`, own migration stream between core and the integrations) owns claims — a DNS node plus its whole subtree, mutually disjoint: operator shared-domain roots, member claims carved from them, and bring-your-own names proven by TXT verification — and placements, which bind a name inside a claim to a provider slug and resource ref. Verification moves to the claim and decouples from creation. A member proves control of a domain once; afterwards every name inside it places instantly, wildcard-CNAME friendly, with no further DNS work. The claim workflow activates the claim and stops — it no longer creates a site — so the sites list offers a one-click create once a domain verifies. /domains/ask answers from placements and is registered by core rather than the FedWiki adapter; its HTTP contract is unchanged. A configured `domains-ask-fallback-url` forwards names the registry does not know to a legacy answerer, the strangler seam wiki.cafe's migration needs; a name the registry knows but has archived is refused locally. FedWiki's create saga reserves the name before the farm call, carrying a workflow-minted site id so retries are idempotent, and compensates on failure. Sync places only names it owns, never stealing a member's; lifecycle transitions and the retention purge maintain servability. An unconditional boot pass seeds operator roots, releases orphaned placements, and adopts pre-existing sites — grandfathering member-owned external domains shortest-name-first, and skipping name policy, so a live single-letter site cannot lose its certificate. Members manage domains at /domains: claims with verification status, DNS records including an optional wildcard row, check-now, cancel, release. Name policy (reserved, blocked, premium, plus a single-letter guard) is operator data; refusals collapse to a plain "unavailable" so the console never becomes an oracle for who holds what. BREAKING (pre-release): `fedwiki.custom_domain_verifications` and `sites.is_custom_domain` are dropped, the flag now derived from the placement's claim kind; resource key `fedwiki_custom_domains` migrates to the platform-owned `external_domain_claims`; running verify-custom-domain workflows must be terminated before deploy.
73 lines
2.3 KiB
Go
73 lines
2.3 KiB
Go
package dnsname
|
|
|
|
import "testing"
|
|
|
|
func TestNormalize(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"already normal", "wiki.example.org", "wiki.example.org"},
|
|
{"uppercase", "Wiki.Example.ORG", "wiki.example.org"},
|
|
{"surrounding whitespace", " wiki.example.org\t", "wiki.example.org"},
|
|
{"absolute name loses its root label", "wiki.example.org.", "wiki.example.org"},
|
|
{"only one trailing dot is stripped", "wiki.example.org..", "wiki.example.org."},
|
|
{"whitespace then dot", " Wiki.Example.org. ", "wiki.example.org"},
|
|
{"empty", "", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := Normalize(tt.in); got != tt.want {
|
|
t.Errorf("Normalize(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReverseLabels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"two labels", "example.org", "org.example"},
|
|
{"three labels", "alice.example.org", "org.example.alice"},
|
|
{"four labels", "a.b.example.org", "org.example.b.a"},
|
|
{"single label", "test", "test"},
|
|
{"empty", "", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ReverseLabels(tt.in)
|
|
if got != tt.want {
|
|
t.Errorf("ReverseLabels(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
if round := ReverseLabels(got); round != tt.in {
|
|
t.Errorf("ReverseLabels is not its own inverse: %q -> %q -> %q", tt.in, got, round)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestReverseLabelsGivesSubtreePrefixes pins the property the registry's
|
|
// descendant scans rely on: a name is inside another's subtree exactly when
|
|
// its reversed form starts with the reversed parent plus a dot.
|
|
func TestReverseLabelsGivesSubtreePrefixes(t *testing.T) {
|
|
parent := ReverseLabels("example.org")
|
|
inside := []string{"wiki.example.org", "a.b.example.org"}
|
|
outside := []string{"example.org", "notexample.org", "example.org.evil.test", "org"}
|
|
|
|
for _, name := range inside {
|
|
if got := ReverseLabels(name); len(got) <= len(parent) || got[:len(parent)+1] != parent+"." {
|
|
t.Errorf("%q reversed to %q, want the prefix %q.", name, got, parent)
|
|
}
|
|
}
|
|
for _, name := range outside {
|
|
got := ReverseLabels(name)
|
|
if len(got) > len(parent) && got[:len(parent)+1] == parent+"." {
|
|
t.Errorf("%q reversed to %q, which wrongly matches the %q. subtree", name, got, parent)
|
|
}
|
|
}
|
|
}
|