package server import ( "context" "errors" "log/slog" "net/http/httptest" "strings" "testing" ) type fakeAuthorizer struct { served map[string]bool err error lastQuery string calls int } func (f *fakeAuthorizer) AuthorizeDomain(_ context.Context, fqdn string) (bool, error) { f.calls++ f.lastQuery = fqdn if f.err != nil { return false, f.err } return f.served[fqdn], nil } // TestDomainAskHandler covers the domain-authorization spec scenarios that // live at the HTTP layer: 200 iff the authorizer says servable, exact-match // only (no implicit child authorization), normalization before lookup, and // non-200 for missing parameters and authorizer failures. The // servable-status semantics (readonly served, archived refused) and the // legacy-answerer fallback live behind the seam, in the domains registry's // RegistryAuthorizer (internal/domains/authorizer.go). // // wantQuery == "" doubles as "the authorizer must not be reached at all", // which is what the malformed-name rows assert: the route is // unauthenticated, so a name that cannot be an FQDN is refused on shape // before it can cost a lookup. func TestDomainAskHandler(t *testing.T) { served := map[string]bool{"alice.example.test": true} tests := []struct { name string target string served map[string]bool err error wantStatus int wantQuery string }{ {"servable domain authorized", "/domains/ask?domain=alice.example.test", served, nil, 200, "alice.example.test"}, {"unknown domain refused", "/domains/ask?domain=nope.example.test", served, nil, 404, "nope.example.test"}, {"child of allocated name not implicitly authorized", "/domains/ask?domain=blog.alice.example.test", served, nil, 404, "blog.alice.example.test"}, {"query normalized before lookup", "/domains/ask?domain=Alice.Example.Test.", served, nil, 200, "alice.example.test"}, {"missing domain parameter", "/domains/ask", served, nil, 400, ""}, {"empty domain parameter", "/domains/ask?domain=", served, nil, 400, ""}, {"whitespace-only domain parameter", "/domains/ask?domain=%20%20", served, nil, 400, ""}, {"single label refused without a lookup", "/domains/ask?domain=localhost", served, nil, 400, ""}, {"illegal characters refused without a lookup", "/domains/ask?domain=not_a~domain.example.test", served, nil, 400, ""}, {"empty label refused without a lookup", "/domains/ask?domain=alice..example.test", served, nil, 400, ""}, {"leading-hyphen label refused without a lookup", "/domains/ask?domain=-alice.example.test", served, nil, 400, ""}, {"over-length name refused without a lookup", "/domains/ask?domain=" + strings.Repeat("a.", 130) + "test", served, nil, 400, ""}, {"authorizer failure is non-200", "/domains/ask?domain=alice.example.test", served, errors.New("db down"), 500, "alice.example.test"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fake := &fakeAuthorizer{served: tt.served, err: tt.err} rec := httptest.NewRecorder() // No Accept header, matching a TLS proxy's on-demand-TLS ask: the // machine-facing contract (status + plain-text body) applies. DomainAskHandler(fake, slog.Default(), nil)(rec, httptest.NewRequest("GET", tt.target, nil)) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } if tt.wantQuery == "" { if fake.calls != 0 { t.Errorf("authorizer called %d times for missing/empty param, want 0", fake.calls) } } else if fake.lastQuery != tt.wantQuery { t.Errorf("authorizer queried %q, want %q", fake.lastQuery, tt.wantQuery) } }) } } // TestDomainAskHandlerBrowserRendering covers the domain-authorization // delta's added requirement: a malformed request from a browser (Accept: // text/html) gets the styled, human-readable error page, never a bare // internal parameter name on an unstyled page; the status and the // machine-facing plain-text contract are unchanged for a caller that does // not ask for HTML (spec: domain-authorization). func TestDomainAskHandlerBrowserRendering(t *testing.T) { fake := &fakeAuthorizer{served: map[string]bool{}} tmpl := errorPageTemplates(t) t.Run("browser sees the styled page", func(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/domains/ask", nil) req.Header.Set("Accept", "text/html,application/xhtml+xml") DomainAskHandler(fake, slog.Default(), tmpl)(rec, req) if rec.Code != 400 { t.Fatalf("status = %d, want 400", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "