Files
summercms/.planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
Jakub Zych eb40d1bdc4 docs(08-10): add the 103-method PHP-to-Go OAuth test map
Auditable one-to-one map of every PHP OAuth functional/security test
method to its named Go test/subtest evidence, mechanically verified by
fonoteka.go/parity/oauth_audit_test.go's TestPHPTestMap.
2026-09-24 00:05:58 +02:00

22 KiB

Phase 08 PHP OAuth Test Map

D-18: every PHP OAuth functional/security test method maps to a named passing Go test/subtest.

Distribution (exactly 103 unique PHP method rows): authorize 11, client-command 8, metadata 2, migration 7, register 10, token 10, consent/scope-ceiling 7, refresh-rotation 10, revocation 8, surface-isolation 30.

parity/oauth_audit_test.go's TestPHPTestMap parses this table mechanically and fails if any row is missing, duplicated, or points at a Go test/subtest that does not exist in the stated package. Column Repo is summercms.go or fonoteka.go (sibling directories in the meta repo). Column Go Package is a Go import-style relative path from that repo's root. Subtest is empty for a bare top-level func TestX(t *testing.T).

Deferred-scope notice

Several security/TokenSurfaceIsolationTest.php rows (and one security/OAuthRevocationTest.php row) exercise route families Phase 8 does not port (albums, discogs, csv import, wishlist, collection share/switch, oauth-identities, public share) per 08-CONTEXT.md's Phase Boundary and PROJECT.md's still-Active "All 154 Płytarium API routes ported" requirement. Those rows are marked deferred-scope in Notes: the named Go subtest asserts the honest current state (the route family does not exist in the assembled table yet), not the full PHP behavior, which cannot exist until a later phase ports the feature. OAuthMigrationTest's four v119 rows are marked architecture-difference: 08-02-PLAN.md folded scope_ceiling directly into the initial CREATE TABLE instead of a second incremental migration, so there is no separate Go migration step to test add/idempotent/down/missing-table-noop against in isolation; the named subtests assert the real equivalent fact this architecture produces.

authorize (functional/OAuthAuthorizeTest.php, 11)

PHP Method Repo Go Package Go Test Subtest Notes
test_unknown_client_returns_400_with_no_location summercms.go wristband TestAuthorizeUnknownClientReturnsLocal400NoLocation
test_unregistered_redirect_uri_returns_400_with_no_location summercms.go wristband TestAuthorizeUnregisteredRedirectURIReturnsLocal400NoLocation
test_trailing_slash_mismatch_is_unregistered summercms.go wristband TestAuthorizeTrailingSlashMismatchIsUnregistered
test_plain_pkce_redirects_with_invalid_request_and_preserves_state summercms.go wristband TestPKCEChallengeMethodMustBeS256
test_valid_request_redirects_to_connect_with_opaque_handle_only summercms.go wristband TestAuthorizeValidRequestRedirectsToConnectWithOpaqueHandleOnly
test_iss_is_present_on_every_error_redirect summercms.go wristband TestAuthorizeIssIsPresentOnEveryErrorRedirect
test_null_ceiling_preserves_ai_scope summercms.go wristband TestAuthorizeNullCeilingPreservesAiScope
test_ceiling_truncates_ai_without_error summercms.go wristband TestAuthorizeCeilingTruncatesAiWithoutError
test_ceiling_preserves_offline_access_flag summercms.go wristband TestAuthorizeCeilingPreservesOfflineAccessFlag
test_ceiling_rejects_ai_only_as_invalid_scope_on_trusted_redirect summercms.go wristband TestAuthorizeCeilingRejectsAiOnlyAsInvalidScopeOnTrustedRedirect
test_ceiling_rejects_ai_plus_offline_access_as_invalid_scope summercms.go wristband TestAuthorizeCeilingRejectsAiPlusOfflineAccessAsInvalidScope

client-command (functional/OAuthClientCommandTest.php, 8)

PHP Method Repo Go Package Go Test Subtest Notes
test_list_prints_client_id_and_uris_never_the_secret fonoteka.go plugins/golem15/fonoteka/console TestOAuthClientCommandListPrintsClientIDAndURIsNeverTheSecret
test_client_id_adds_redirect_uri_without_rotating_secret fonoteka.go plugins/golem15/fonoteka/console TestOAuthClientCommandAddRedirectURIsWithoutRotatingSecret
test_unknown_client_id_fails fonoteka.go plugins/golem15/fonoteka/console TestOAuthClientCommandUnknownClientIDFails
test_scope_option_persists_read_write_ceiling fonoteka.go plugins/golem15/fonoteka/console TestOAuthClientCommandScopeCeilingPersistsAndListsNeverTheSecret one Go test, 3 distinct assertions -> 3 PHP methods (this row + next 2)
test_absent_scope_option_stores_null_ceiling fonoteka.go plugins/golem15/fonoteka/console TestOAuthClientCommandScopeCeilingPersistsAndListsNeverTheSecret
test_invalid_scope_fails_without_creating_a_client fonoteka.go plugins/golem15/fonoteka/console TestOAuthClientCommandInvalidScopeFailsWithoutCreatingAClient
test_list_prints_scope_ceiling_never_the_secret fonoteka.go plugins/golem15/fonoteka/console TestOAuthClientCommandScopeCeilingPersistsAndListsNeverTheSecret
test_client_id_adds_redirect_uri_without_changing_ceiling fonoteka.go plugins/golem15/fonoteka/console TestOAuthClientCommandAddRedirectURIWithoutChangingCeiling new (08-10 Task 1)

metadata (functional/OAuthMetadataTest.php, 2)

PHP Method Repo Go Package Go Test Subtest Notes
test_authorization_server_metadata_is_unwrapped_rfc8414_document fonoteka.go plugins/golem15/fonoteka TestOAuthMetadataAssembled
test_authorize_without_client_is_local_error_and_register_exists fonoteka.go plugins/golem15/fonoteka TestOAuthMetadataAuthorizeWithoutClientIsLocalErrorAndRegisterExists new (08-10 Task 1)

migration (functional/OAuthMigrationTest.php, 7)

PHP Method Repo Go Package Go Test Subtest Notes
test_v117_creates_three_oauth_tables_and_named_unique_indexes fonoteka.go plugins/golem15/fonoteka/updates TestOAuthMigrationV117CreatesThreeOAuthTablesAndNamedUniqueIndexes new (08-10 Task 1)
test_v117_second_up_is_idempotent fonoteka.go plugins/golem15/fonoteka/updates TestOAuthMigrationV117SecondUpIsIdempotent new; proven at the gormigrate re-Migrate() boundary, not a raw double-func-call
test_v117_down_drops_the_three_oauth_tables fonoteka.go plugins/golem15/fonoteka/updates TestOAuthMigrationV117DownDropsTheThreeOAuthTables new
test_v119_adds_nullable_scope_ceiling fonoteka.go plugins/golem15/fonoteka/updates TestOAuthMigrationV119ScopeCeilingBakedIntoInitialSchema test_v119_adds_nullable_scope_ceiling architecture-difference: see notice above
test_v119_second_up_is_idempotent fonoteka.go plugins/golem15/fonoteka/updates TestOAuthMigrationV119ScopeCeilingBakedIntoInitialSchema test_v119_second_up_is_idempotent architecture-difference
test_v119_down_drops_scope_ceiling fonoteka.go plugins/golem15/fonoteka/updates TestOAuthMigrationV119ScopeCeilingBakedIntoInitialSchema test_v119_down_drops_scope_ceiling architecture-difference
test_v119_up_on_missing_table_is_noop fonoteka.go plugins/golem15/fonoteka/updates TestOAuthMigrationV119ScopeCeilingBakedIntoInitialSchema test_v119_up_on_missing_table_is_noop architecture-difference

register (functional/OAuthRegisterTest.php, 10)

PHP Method Repo Go Package Go Test Subtest Notes
test_public_client_is_issued_without_a_secret summercms.go wristband TestRegisterPublicClientResponse
test_omitted_auth_method_defaults_to_public summercms.go wristband TestRegisterPublicClientResponse same test, 2nd distinct assertion (token_endpoint_auth_method="none")
test_confidential_client_returns_secret_once summercms.go wristband TestRegisterConfidentialClientReturnsSecretOnce
test_http_non_loopback_and_javascript_uris_are_rejected summercms.go wristband TestRegisterJavascriptURIIsRejected new (08-10 Task 1); http-non-loopback half already in TestRegisterRedirectURIBounds/http-non-loopback
test_loopback_http_is_accepted summercms.go wristband TestRegisterLoopbackHTTPIsAccepted new
test_missing_redirect_uris_is_invalid summercms.go wristband TestRegisterRedirectURIBounds missing
test_form_urlencoded_is_rejected summercms.go wristband TestRegisterWrongContentType
test_orphan_sweep_deletes_stale_dcr_rows_not_artisan_clients summercms.go wristband TestRegisterSweepsStaleUnconsentedButKeepsArtisanClients
test_registered_public_client_can_authorize_and_exchange_without_secret fonoteka.go parity TestOAuthFlows full recorded 17-step lifecycle includes a public-client authorize+exchange leg
test_error_body_has_no_secret_or_stack summercms.go wristband TestRegisterErrorBodyHasNoSecretOrStackTrace new

token (functional/OAuthTokenTest.php, 10)

PHP Method Repo Go Package Go Test Subtest Notes
test_valid_code_returns_bearer_inv_token fonoteka.go plugins/golem15/fonoteka/classes/auth TestOAuthCodeExchangeMintsInvAccessAndRefreshTokens
test_access_token_authenticates_me_with_granted_scopes_and_collections fonoteka.go parity TestOAuthFlows "me" step replays the real /api/v1/fonoteka/me response against an OAuth-exchanged token
test_replaying_the_code_returns_invalid_grant summercms.go wristband TestTokenCodeSequentialReplayIsInvalidGrantSecondTime
test_wrong_verifier_returns_invalid_grant summercms.go wristband TestTokenWrongVerifierIsInvalidGrant
test_wrong_client_secret_returns_invalid_client summercms.go wristband TestTokenConfidentialClientWrongSecretIsInvalidClient
test_json_body_does_not_five_hundred summercms.go wristband TestTokenRejectsJSONBodyEvenWithValidQueryParams
test_offline_access_is_echoed_but_not_persisted_on_api_token fonoteka.go plugins/golem15/fonoteka/classes/auth TestOAuthOfflineAccessEchoedButNotPersistedOnAPIToken new (08-10 Task 1)
test_scope_omits_offline_access_when_not_requested fonoteka.go plugins/golem15/fonoteka/classes/auth TestOAuthScopeOmitsOfflineAccessWhenNotRequested new
test_public_client_exchanges_code_without_secret summercms.go wristband TestTokenPublicClientIgnoresSuppliedSecret
test_confidential_client_without_secret_is_invalid_client summercms.go wristband TestTokenConfidentialClientMissingSecretIsInvalidClient

consent/scope-ceiling (security/OAuthConsentScopeCeilingTest.php, 7)

PHP Method Repo Go Package Go Test Subtest Notes
test_consent_requesting_ai_when_client_asked_read_grants_read_only fonoteka.go plugins/golem15/fonoteka TestOAuthConsentIntersectionGrantsOnlyOriginallyRequestedScopes new (08-10 Task 1)
test_consent_submitting_a_scope_outside_mintable_is_422 fonoteka.go plugins/golem15/fonoteka TestOAuthConsentEmptySubmittedScopeIntersectionIs422
test_submitted_inaccessible_collection_id_never_enters_the_grant fonoteka.go plugins/golem15/fonoteka TestOAuthConsentCollectionIDsAlwaysServerResolvedActivePin new; collection_ids is never read from the request body at all (architecturally stronger than PHP's runtime filter)
test_issued_collection_ids_equal_the_active_pin fonoteka.go plugins/golem15/fonoteka TestOAuthConsentCollectionIDsAlwaysServerResolvedActivePin same test, 2nd distinct assertion
test_second_consent_on_the_same_request_id_fails fonoteka.go plugins/golem15/fonoteka TestOAuthConsentSecondConsentOnSameRequestIDFails new
test_show_returns_host_and_pinned_name_without_collection_id fonoteka.go plugins/golem15/fonoteka TestOAuthConsentShowExactKeySet new
test_authorize_route_ceiling_drops_ai_from_scopes_requested fonoteka.go plugins/golem15/fonoteka TestOAuthConsentScopeCeilingViaShow

refresh-rotation (security/OAuthRefreshRotationTest.php, 10)

PHP Method Repo Go Package Go Test Subtest Notes
test_s256_verification_accepts_matching_verifier fonoteka.go plugins/golem15/fonoteka/classes/auth TestOAuthCodeExchangeMintsInvAccessAndRefreshTokens real-Postgres positive S256 match
test_s256_verification_rejects_wrong_verifier fonoteka.go plugins/golem15/fonoteka/classes/auth TestOAuthCodeExchangeWrongVerifierIsInvalidGrantAndMintsNothing
test_plain_pkce_is_rejected_even_when_verifier_equals_challenge summercms.go wristband TestTokenPlainPKCEIsRejectedEvenWhenVerifierEqualsChallenge new (08-10 Task 1)
test_code_cannot_be_exchanged_twice summercms.go wristband TestTokenCodeSequentialReplayIsInvalidGrantSecondTime
test_expired_code_is_rejected summercms.go wristband TestTokenExpiredCodeIsInvalidGrant
test_code_with_different_redirect_uri_is_rejected summercms.go wristband TestTokenRedirectURIMismatchIsInvalidGrant
test_code_presented_by_a_different_client_is_rejected summercms.go wristband TestTokenClientMismatchIsInvalidGrant
test_rotation_issues_a_new_refresh_and_marks_the_old_one_rotated summercms.go wristband TestRefreshRotationIssuesNewPairAndKeepsPredecessorAsEvidence
test_replaying_rotated_refresh_revokes_the_entire_chain fonoteka.go plugins/golem15/fonoteka/classes/auth TestOAuthRefreshReplayRevokesLineageAndBothAccessTokens
test_revoke_chain_leaves_no_usable_api_token fonoteka.go plugins/golem15/fonoteka/classes/auth TestOAuthConnectedAppRevokeStoreKillsAccessAndLineage

revocation (security/OAuthRevocationTest.php, 8)

PHP Method Repo Go Package Go Test Subtest Notes
test_connected_app_revoke_kills_access_and_refresh fonoteka.go plugins/golem15/fonoteka TestConnectedAppsRevokeKillsAccessAndRefreshThenDisappearsFromList
test_rotated_predecessor_is_dead_after_connected_app_revoke fonoteka.go plugins/golem15/fonoteka TestOAuthRevocationRotatedPredecessorIsDeadAfterConnectedAppRevoke new (08-10 Task 1)
test_manual_token_route_on_oauth_token_kills_refresh_chain fonoteka.go plugins/golem15/fonoteka TestOAuthRevocationManualTokenRouteKillsRefreshChain new; GREEN companion of this plan's Rule-1 fix to controllers/api/token_api_controller.go Destroy
test_foreign_or_missing_connected_app_id_returns_404_never_403 fonoteka.go plugins/golem15/fonoteka TestConnectedAppsRevokeForeignAndMissingAndManualShareExact404
test_second_consent_survives_revoking_the_first fonoteka.go plugins/golem15/fonoteka TestOAuthRevocationSecondGrantSurvivesRevokingFirst new
test_oauth_token_reading_album_outside_allow_list_is_404_never_403 fonoteka.go plugins/golem15/fonoteka TestOAuthRevocationReadingOutsideAllowListRouteNotYetPorted new; deferred-scope (albums unported, see notice above)
test_revoked_and_rotated_oauth_tokens_leave_the_connected_apps_list fonoteka.go plugins/golem15/fonoteka TestOAuthRevocationRotatedTokenLeavesExactlyOneConnectedApp new; revoke-then-empty half covered by TestOAuthRevocationListsSignalEachOtherExcludingOAuthFromManual
test_lists_signal_each_other_and_oauth_tokens_are_absent_from_manual_list fonoteka.go plugins/golem15/fonoteka TestOAuthRevocationListsSignalEachOtherExcludingOAuthFromManual new

surface-isolation (security/TokenSurfaceIsolationTest.php, 30)

PHP Method Repo Go Package Go Test Subtest Notes
test_final_surfaces_have_flat_albums_and_no_legacy_hierarchy_routes fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_final_surfaces_have_flat_albums_and_no_legacy_hierarchy_routes deferred-scope: albums unported
test_token_surface_excludes_collection_create_membership_and_token_management fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_token_surface_excludes_collection_create_membership_and_token_management real: token surface has exactly genres+me today
test_fixed_album_routes_precede_numeric_album_route fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_fixed_album_routes_precede_numeric_album_route deferred-scope
test_jwt_group_carries_no_token_scope_middleware fonoteka.go plugins/golem15/fonoteka TestFullRouteTableAuthGroupMutualExclusivity existing Phase 6 test (T-06-02/T-06-10), walks the real assembled table
test_read_only_token_is_forbidden_on_write_and_ai_routes fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_read_only_token_is_forbidden_on_write_and_ai_routes deferred-scope: no write/ai-scoped route exists yet
test_discogs_import_route_exists_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_discogs_import_route_exists_on_jwt_surface_only deferred-scope: discogs unported
test_csv_import_routes_exist_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_csv_import_routes_exist_on_jwt_surface_only deferred-scope: csv import unported
test_discogs_credential_routes_exist_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_discogs_credential_routes_exist_on_jwt_surface_only deferred-scope
test_oauth_identity_routes_exist_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_oauth_identity_routes_exist_on_jwt_surface_only deferred-scope: social login explicitly out of scope (08-CONTEXT.md)
test_album_stats_route_exists_on_jwt_and_token_surfaces fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_album_stats_route_exists_on_jwt_and_token_surfaces deferred-scope
test_album_sync_route_exists_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_album_sync_route_exists_on_jwt_surface_only deferred-scope
test_album_missing_route_exists_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_album_missing_route_exists_on_jwt_surface_only deferred-scope
test_album_match_and_apply_routes_exist_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_album_match_and_apply_routes_exist_on_jwt_surface_only deferred-scope
test_wishlist_match_and_apply_routes_exist_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_wishlist_match_and_apply_routes_exist_on_jwt_surface_only deferred-scope: wishlist unported
test_token_surface_wishlist_album_routes_are_index_store_update_destroy_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_token_surface_wishlist_album_routes_are_index_store_update_destroy_only deferred-scope
test_collection_share_routes_exist_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_collection_share_routes_exist_on_jwt_surface_only deferred-scope
test_wishlist_share_routes_exist_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_wishlist_share_routes_exist_on_jwt_surface_only deferred-scope
test_me_locale_routes_exist_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_me_locale_routes_exist_on_jwt_surface_only real: /_fonoteka/api/v1/me/locale exists on jwt surface only
test_collection_switch_route_exists_on_jwt_surface_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_collection_switch_route_exists_on_jwt_surface_only deferred-scope
test_public_share_routes_are_get_only_and_absent_from_token_surface fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_public_share_routes_are_get_only_and_absent_from_token_surface deferred-scope: public_share group registers zero routes today
test_public_wishlist_share_routes_are_get_only_and_absent_from_token_surface fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_public_wishlist_share_routes_are_get_only_and_absent_from_token_surface deferred-scope
test_public_group_carries_no_jwt_or_scope_middleware fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_public_group_carries_no_jwt_or_scope_middleware deferred-scope: no route registered in this group yet
test_missing_token_returns_401_and_write_token_reaches_collection_update fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_missing_token_returns_401_and_write_token_reaches_collection_update deferred-scope: no collection-update route yet
test_oauth_mcp_routes_carry_neither_jwt_scope_nor_web_middleware fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_oauth_mcp_routes_carry_neither_jwt_scope_nor_web_middleware real
test_oauth_mcp_routes_resolve_to_fonoteka_not_social_auth fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_oauth_mcp_routes_resolve_to_fonoteka_not_social_auth real: asserts RouteInfo.PluginID == golem15.fonoteka
test_jwt_group_contains_oauth_consent_and_connected_apps fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_jwt_group_contains_oauth_consent_and_connected_apps real
test_code_or_token_producing_routes_do_not_carry_inv_scope fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_code_or_token_producing_routes_do_not_carry_inv_scope real
test_token_surface_has_exactly_one_discogs_route fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_token_surface_has_exactly_one_discogs_route deferred-scope: current count is 0, PHP's is 1; update once discogs ships
test_interactive_discogs_import_routes_stay_jwt_only fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_interactive_discogs_import_routes_stay_jwt_only deferred-scope
test_no_route_on_the_token_prefix_is_bearerless fonoteka.go plugins/golem15/fonoteka TestOAuthTokenSurfaceIsolationCoverage test_no_route_on_the_token_prefix_is_bearerless real