package token import ( "net/http" "net/http/httptest" "strings" "testing" "time" ) // The plain-text guidance sent with a 401 has to name a host `docker login` // can actually handshake against. /auth/token answers on the UI domain (that // is where the WWW-Authenticate realm points) as well as on every registry // domain, and the UI domain refuses /v2/* outright, so the guidance must name // the resolved registry service rather than echoing the request's own host. func TestSendAuthError_NamesRegistryDomainNotRequestHost(t *testing.T) { keyPath := getSharedTestKey(t) tests := []struct { name string // services declared on the handler; the issuer's service is the first. services []string // host the request arrives on. host string // service parameter the client echoes back, if any. requested string wantLogin string // hostname step 2 must name wantAbsent []string // hostnames step 2 must not name }{ { // seamark-shaped: UI on seamark.dev, registry on seamark.cr. A // client following the realm lands on the UI host. name: "split domain, request on the UI host", services: []string{"seamark.cr"}, host: "seamark.dev", wantLogin: "seamark.cr", wantAbsent: []string{"docker login seamark.dev"}, }, { name: "split domain, request on the registry host", services: []string{"seamark.cr"}, host: "seamark.cr", wantLogin: "seamark.cr", }, { // Multiple front doors: the guidance follows the service the // client is authenticating against, not the primary. name: "secondary registry domain via ?service=", services: []string{"seamark.cr", "buoy.cr"}, host: "seamark.dev", requested: "buoy.cr", wantLogin: "buoy.cr", wantAbsent: []string{"docker login seamark.dev"}, }, { // ?service= is client-influenced, so an unconfigured value falls // back to the primary registry domain, never to r.Host. name: "unconfigured ?service= falls back to the primary", services: []string{"seamark.cr"}, host: "seamark.dev", requested: "evil.example", wantLogin: "seamark.cr", wantAbsent: []string{"docker login seamark.dev", "evil.example"}, }, { // Single-domain deployment (the dev stack): the UI host is the // registry domain, and the printed hostname is still correct. name: "single domain, UI host is the registry domain", services: []string{"localhost"}, host: "localhost:5000", wantLogin: "localhost", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { issuer, err := NewIssuer(keyPath, "atcr.io", tt.services[0], 15*time.Minute) if err != nil { t.Fatalf("NewIssuer() error = %v", err) } handler := NewHandler(issuer, nil) handler.SetServices(tt.services) // A push-only scope with no credentials has no anonymous // component, so it draws the plain-text challenge. target := "/auth/token?scope=repository:bob.bsky.social/myapp:push" if tt.requested != "" { target += "&service=" + tt.requested } req := httptest.NewRequest(http.MethodGet, target, nil) req.Host = tt.host w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want %d. Body: %s", w.Code, http.StatusUnauthorized, w.Body.String()) } body := w.Body.String() want := "docker login " + tt.wantLogin if !strings.Contains(body, want) { t.Errorf("guidance does not contain %q.\nBody:\n%s", want, body) } for _, absent := range tt.wantAbsent { if strings.Contains(body, absent) { t.Errorf("guidance contains %q, which cannot serve /v2/.\nBody:\n%s", absent, body) } } // Step 1 stays on the request's own base URL: the install page // lives on the UI domain and registry domains redirect there. if wantInstall := getBaseURL(req) + "/install"; !strings.Contains(body, wantInstall) { t.Errorf("guidance does not contain install URL %q.\nBody:\n%s", wantInstall, body) } }) } } // A handler whose issuer has no service configured has no registry domain to // name, so it drops the docker login step rather than printing a host that // cannot work. func TestSendAuthError_NoServiceOmitsDockerLoginStep(t *testing.T) { keyPath := getSharedTestKey(t) issuer, err := NewIssuer(keyPath, "atcr.io", "", 15*time.Minute) if err != nil { t.Fatalf("NewIssuer() error = %v", err) } handler := NewHandler(issuer, nil) req := httptest.NewRequest(http.MethodGet, "/auth/token?scope=repository:bob.bsky.social/myapp:push", nil) req.Host = "seamark.dev" w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("status = %d, want %d", w.Code, http.StatusUnauthorized) } if body := w.Body.String(); strings.Contains(body, "docker login") { t.Errorf("guidance names a docker login host with no service configured.\nBody:\n%s", body) } }