| index 702937843..4a82252f8 100644 |
| --- a/internal/console/appdomains_test.go |
| +++ b/internal/console/appdomains_test.go |
| @@ -59,6 +59,30 @@ func TestAppDomainAttachNeedsCSRF(t *testing.T) { |
| claimAbsent(t, fx, host) |
| } |
| |
| +// TestAppDomainAttachLowercasesHost: a member typing Docs.Example.com must |
| +// not be refused by ValidHostShape's lowercase-only label regex — the door |
| +// normalizes case before the host ever reaches the core, and the claim |
| +// lands at the normalized key, not the one typed. |
| +func TestAppDomainAttachLowercasesHost(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + c := signInPaul(t, fx) |
| + csrf := sessionToken(t, fx, c) |
| + const mixed = "Docs.Example.com" |
| + const lower = "docs.example.com" |
| + |
| + status, loc, body := postForm(t, c, fx.u("/apps/kass/domains/attach"), |
| + url.Values{"csrf": {csrf}, "host": {mixed}}) |
| + if status != http.StatusSeeOther || loc != fx.path("/apps/kass/settings") { |
| + t.Fatalf("attach with a mixed-case host = %d -> %q, want 303 -> settings:\n%s", status, loc, body) |
| + } |
| + if _, _, err := domainclaim.Get(context.Background(), fx.rel, lower); err != nil { |
| + t.Errorf("claim at the lowercase key: %v, want it there", err) |
| + } |
| + if _, _, err := domainclaim.Get(context.Background(), fx.rel, mixed); err == nil { |
| + t.Error("a claim also landed at the mixed-case key — the door did not normalize") |
| + } |
| +} |
| + |
| // TestAppDomainAttachIsOwnerOnly matches TestAppDeleteIsOwnerOnly's shape: |
| // a member gets the same byte-identical 404 a total stranger would, and |
| // nothing is claimed. |
| @@ -136,6 +160,30 @@ func TestAppDomainDetachHappyPath(t *testing.T) { |
| claimAbsent(t, fx, host) |
| } |
| |
| +// TestAppDomainDetachUnattachedHostIs404 exercises renderDoorRefusal's 404 |
| +// branch: the owner's own gates all pass (CSRF, RoleOwner, confirm == host), |
| +// but the core has nothing to release, so its doorRefusal{404,...} must |
| +// render as the stranger's own byte-identical 404 — never a message that |
| +// would tell a prober "an app exists here, but this host does not". |
| +func TestAppDomainDetachUnattachedHostIs404(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + c := signInPaul(t, fx) |
| + csrf := sessionToken(t, fx, c) |
| + const host = "never-attached.example.com" |
| + |
| + status, _, body := postForm(t, c, fx.u("/apps/kass/domains/detach"), |
| + url.Values{"csrf": {csrf}, "host": {host}, "confirm": {host}}) |
| + if status != http.StatusNotFound { |
| + t.Fatalf("detach of an unattached host = %d, want 404:\n%s", status, body) |
| + } |
| + strangerBody := strangerRefusal(t, fx, "code-zoe-appdomaindetach-unattached", fx.u("/apps/kass/domains/detach"), |
| + url.Values{"host": {host}, "confirm": {host}}) |
| + if body != strangerBody { |
| + t.Fatalf("owner's unattached-host 404 is not byte-identical to the stranger's:\n(owner) %q\n(stranger) %q", body, strangerBody) |
| + } |
| + claimAbsent(t, fx, host) |
| +} |
| + |
| // TestAppDomainDetachIsOwnerOnly mirrors the attach case: a member gets the |
| // same byte-identical 404 a stranger would. |
| func TestAppDomainDetachIsOwnerOnly(t *testing.T) { |