package wristband import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) // TestPhase8RedMetadata is the Phase 8 Wave 1 RED anchor (08-CONTEXT.md // D-06). It asserts the exact unwrapped RFC 8414 metadata document recorded // from the live PHP fixture (parity/fixtures/routes/GET__.well-known_oauth-authorization-server_oauth.yaml) // and fails with the PHASE8_RED:metadata sentinel while Server.Metadata is a // stub. scripts/check-phase8-red.sh verifies this failure is fail-closed. func TestPhase8RedMetadata(t *testing.T) { opts := DefaultOptions() opts.Issuer = "https://plytarium.com" srv := NewServer(opts) req := httptest.NewRequest(http.MethodGet, "/.well-known/oauth-authorization-server", nil) req.Header.Set("Accept", "application/json") rec := httptest.NewRecorder() srv.Metadata(rec, req) if rec.Code != http.StatusOK { t.Fatalf("PHASE8_RED:metadata: status = %d, want %d", rec.Code, http.StatusOK) } const want = `{"issuer":"https://plytarium.com","authorization_endpoint":"https://plytarium.com/oauth/mcp/authorize","token_endpoint":"https://plytarium.com/oauth/mcp/token","registration_endpoint":"https://plytarium.com/oauth/mcp/register","response_types_supported":["code"],"grant_types_supported":["authorization_code","refresh_token"],"code_challenge_methods_supported":["S256"],"token_endpoint_auth_methods_supported":["none","client_secret_post","client_secret_basic"],"scopes_supported":["read","write","ai","offline_access"],"service_documentation":"https://plytarium.com/help","authorization_response_iss_parameter_supported":true}` if got := rec.Body.String(); got != want { t.Fatalf("PHASE8_RED:metadata: body mismatch\n got: %s\nwant: %s", got, want) } if ct := rec.Header().Get("Content-Type"); ct != "application/json" { t.Fatalf("PHASE8_RED:metadata: Content-Type = %q, want application/json", ct) } if cc := rec.Header().Get("Cache-Control"); cc != "no-cache, private" { t.Fatalf("PHASE8_RED:metadata: Cache-Control = %q, want \"no-cache, private\"", cc) } } // TestMetadataExactBytes is the GREEN direct-handler regression for // TestPhase8RedMetadata: same PHP-fixture bytes, now expected to pass. func TestMetadataExactBytes(t *testing.T) { opts := DefaultOptions() opts.Issuer = "https://plytarium.com" srv := NewServer(opts) req := httptest.NewRequest(http.MethodGet, "/.well-known/oauth-authorization-server", nil) req.Header.Set("Accept", "application/json") rec := httptest.NewRecorder() srv.Metadata(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } const want = `{"issuer":"https://plytarium.com","authorization_endpoint":"https://plytarium.com/oauth/mcp/authorize","token_endpoint":"https://plytarium.com/oauth/mcp/token","registration_endpoint":"https://plytarium.com/oauth/mcp/register","response_types_supported":["code"],"grant_types_supported":["authorization_code","refresh_token"],"code_challenge_methods_supported":["S256"],"token_endpoint_auth_methods_supported":["none","client_secret_post","client_secret_basic"],"scopes_supported":["read","write","ai","offline_access"],"service_documentation":"https://plytarium.com/help","authorization_response_iss_parameter_supported":true}` if got := rec.Body.String(); got != want { t.Fatalf("body mismatch\n got: %s\nwant: %s", got, want) } if strings.HasSuffix(rec.Body.String(), "\n") { t.Fatal("body has a trailing newline, want none") } if _, hasData := decodeAsMap(t, rec.Body.Bytes())["data"]; hasData { t.Fatal("body has a house \"data\" envelope, want unwrapped RFC 8414 document") } if ct := rec.Header().Get("Content-Type"); ct != "application/json" { t.Fatalf("Content-Type = %q, want application/json", ct) } if cc := rec.Header().Get("Cache-Control"); cc != "no-cache, private" { t.Fatalf("Cache-Control = %q, want %q", cc, "no-cache, private") } } // TestMetadataUsesConfiguredOptions proves service_documentation, // scopes_supported and the auth-methods list come from Options, not a // hardcoded literal, while the three protocol-constant fields never change // (D-06: only those four fields are configurable). func TestMetadataUsesConfiguredOptions(t *testing.T) { opts := Options{ Issuer: "https://example.test", ServiceDocumentationPath: "/docs/oauth", ScopesSupported: []string{"read"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_post"}, AuthorizationResponseIssParameterSupported: false, } srv := NewServer(opts) rec := httptest.NewRecorder() srv.Metadata(rec, httptest.NewRequest(http.MethodGet, "/.well-known/oauth-authorization-server", nil)) var got map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("decode response: %v", err) } if got["service_documentation"] != "https://example.test/docs/oauth" { t.Fatalf("service_documentation = %v, want configured path", got["service_documentation"]) } if scopes, _ := got["scopes_supported"].([]any); len(scopes) != 1 || scopes[0] != "read" { t.Fatalf("scopes_supported = %v, want [read]", got["scopes_supported"]) } if methods, _ := got["token_endpoint_auth_methods_supported"].([]any); len(methods) != 1 || methods[0] != "client_secret_post" { t.Fatalf("token_endpoint_auth_methods_supported = %v, want [client_secret_post]", got["token_endpoint_auth_methods_supported"]) } if got["authorization_response_iss_parameter_supported"] != false { t.Fatalf("authorization_response_iss_parameter_supported = %v, want false", got["authorization_response_iss_parameter_supported"]) } if rts, _ := got["response_types_supported"].([]any); len(rts) != 1 || rts[0] != "code" { t.Fatalf("response_types_supported = %v, want the fixed [code] constant", got["response_types_supported"]) } } func decodeAsMap(t *testing.T, body []byte) map[string]any { t.Helper() var m map[string]any if err := json.Unmarshal(body, &m); err != nil { t.Fatalf("decode response: %v", err) } return m }