test(08-01): add failing RFC 8414 metadata RED test and fail-closed verifier

- wristband.Server.Metadata is a compiling 501 stub; TestPhase8RedMetadata
  asserts the exact unwrapped PHP metadata document, headers and status and
  fails with the PHASE8_RED:metadata sentinel (D-06)
- scripts/check-phase8-red.sh implements the shared go/shell RED contract
  for the rest of Phase 8: exact selected test/package failure plus sentinel,
  rejecting unrelated fail actions, compile/setup failures, panics,
  malformed JSON, missing/duplicate sentinels and zero selection (D-04/D-18)
This commit is contained in:
Jakub Zych
2026-09-23 19:09:14 +02:00
parent a59e69211d
commit 24d35d85e8
3 changed files with 333 additions and 0 deletions

72
wristband/server.go Normal file
View File

@@ -0,0 +1,72 @@
// Package wristband implements the app-agnostic RFC 8414 / OAuth
// authorization-server surface ported from Płytarium's hand-rolled PHP OAuth
// server (08-CONTEXT.md D-05). It never imports an application package, a
// GORM type, or any fonoteka model: every deployment-specific value (issuer,
// scopes, endpoint paths, TTLs) arrives through Options, and every app-owned
// concern (users, collections, persistence) stays out of this package.
//
// D-06: PHP's RFC-minimal response shapes are wristband's defaults. There
// are no response hooks; callers cannot alter the wire bytes beyond the
// values exposed on Options.
package wristband
import "net/http"
// Options configures a Server's advertised endpoints and metadata values.
// Every field has a PHP-parity default via DefaultOptions except Issuer,
// which the caller must set from app.url with its trailing slash trimmed
// exactly once (D-03). wristband never hardcodes an app's issuer.
type Options struct {
// Issuer is app.url with exactly one trailing slash trimmed by the
// caller. Every metadata endpoint URL is built by appending a fixed
// RFC path suffix to Issuer.
Issuer string
// ServiceDocumentationPath is appended to Issuer for the metadata
// service_documentation field. PHP default: "/help".
ServiceDocumentationPath string
// ScopesSupported is the RFC 8414 scopes_supported list. PHP default:
// ["read","write","ai","offline_access"].
ScopesSupported []string
// TokenEndpointAuthMethodsSupported is the RFC 8414
// token_endpoint_auth_methods_supported list. PHP default:
// ["none","client_secret_post","client_secret_basic"].
TokenEndpointAuthMethodsSupported []string
// AuthorizationResponseIssParameterSupported is the RFC 9207 metadata
// capability flag. PHP default: true.
AuthorizationResponseIssParameterSupported bool
}
// DefaultOptions returns PHP-parity defaults for every metadata option
// other than Issuer, which the caller must set from app.url.
func DefaultOptions() Options {
return Options{
ServiceDocumentationPath: "/help",
ScopesSupported: []string{"read", "write", "ai", "offline_access"},
TokenEndpointAuthMethodsSupported: []string{"none", "client_secret_post", "client_secret_basic"},
AuthorizationResponseIssParameterSupported: true,
}
}
// Server is the app-agnostic wristband authorization-server surface. It is
// constructed with Options and never imports an application package.
type Server struct {
opts Options
}
// NewServer constructs a Server from Options.
func NewServer(opts Options) *Server {
return &Server{opts: opts}
}
// Metadata handles GET /.well-known/oauth-authorization-server, writing the
// exact unwrapped RFC 8414 document (D-06).
//
// TODO(08-01 Task 2): wire the exact writer and PHP-parity document; this
// stub intentionally does not yet satisfy TestPhase8RedMetadata.
func (s *Server) Metadata(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}

39
wristband/server_test.go Normal file
View File

@@ -0,0 +1,39 @@
package wristband
import (
"net/http"
"net/http/httptest"
"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)
}
}