mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-12 02:03:55 +00:00
117 lines
5.6 KiB
PHP
117 lines
5.6 KiB
PHP
<?php
|
|
/**
|
|
* Public unit test for EMCP_Tools_OAuth_Metadata::strip_home_path() /
|
|
* path_matches() — the fix for OAuth discovery on subdirectory installs.
|
|
*
|
|
* A subdirectory install (WordPress at /gpt-build/) receives the RFC 9728
|
|
* protected-resource request as `/gpt-build/.well-known/oauth-protected-resource`,
|
|
* but the well-known paths are site-root-relative. Without stripping the home
|
|
* path, the advertised metadata URL 404s and OAuth discovery (ChatGPT, Claude,
|
|
* any RFC 9728 client) dead-ends. This pins the path handling.
|
|
*
|
|
* @package EMCP_Tools
|
|
*/
|
|
|
|
require_once dirname( __DIR__ ) . '/includes/class-site-context.php';
|
|
require_once dirname( __DIR__ ) . '/includes/oauth/class-oauth-server.php';
|
|
require_once dirname( __DIR__ ) . '/includes/oauth/class-oauth-metadata.php';
|
|
require_once dirname( __DIR__ ) . '/includes/oauth/class-oauth-bearer.php';
|
|
|
|
class OAuthMetadataSubdirTest extends \PHPUnit\Framework\TestCase {
|
|
|
|
protected function setUp(): void {
|
|
emcp_test_reset();
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
unset( $GLOBALS['emcp_test']['home_url'] );
|
|
}
|
|
|
|
/** Root install (home path empty): paths pass through unchanged. */
|
|
public function test_root_install_is_a_noop() {
|
|
$GLOBALS['emcp_test']['home_url'] = 'https://example.test';
|
|
$this->assertSame(
|
|
'/.well-known/oauth-protected-resource',
|
|
EMCP_Tools_OAuth_Metadata::strip_home_path( '/.well-known/oauth-protected-resource' )
|
|
);
|
|
}
|
|
|
|
/** Subdirectory install: the home-path prefix is stripped so the match works. */
|
|
public function test_subdirectory_prefix_is_stripped() {
|
|
$GLOBALS['emcp_test']['home_url'] = 'https://demo.emcptools.com/gpt-build';
|
|
$this->assertSame(
|
|
'/.well-known/oauth-protected-resource',
|
|
EMCP_Tools_OAuth_Metadata::strip_home_path( '/gpt-build/.well-known/oauth-protected-resource' )
|
|
);
|
|
}
|
|
|
|
/** The RFC 9728 resource-scoped variant is stripped + still matches. */
|
|
public function test_subdirectory_resource_scoped_variant() {
|
|
$GLOBALS['emcp_test']['home_url'] = 'https://demo.emcptools.com/gpt-build';
|
|
$stripped = EMCP_Tools_OAuth_Metadata::strip_home_path(
|
|
'/gpt-build/.well-known/oauth-protected-resource/gpt-build/wp-json/mcp/emcp-tools-server'
|
|
);
|
|
$this->assertSame( '/.well-known/oauth-protected-resource/gpt-build/wp-json/mcp/emcp-tools-server', $stripped );
|
|
$this->assertTrue(
|
|
EMCP_Tools_OAuth_Metadata::path_matches( $stripped, EMCP_Tools_OAuth_Metadata::PATH_PROTECTED_RESOURCE )
|
|
);
|
|
}
|
|
|
|
/** A non-well-known path under the subdir is left alone (won't match). */
|
|
public function test_unrelated_subdir_path_unchanged() {
|
|
$GLOBALS['emcp_test']['home_url'] = 'https://demo.emcptools.com/gpt-build';
|
|
$this->assertSame(
|
|
'/gpt-build-other/page',
|
|
EMCP_Tools_OAuth_Metadata::strip_home_path( '/gpt-build-other/page' )
|
|
);
|
|
}
|
|
|
|
/** path_matches: exact + resource-scoped true, sibling false. */
|
|
public function test_path_matches_semantics() {
|
|
$this->assertTrue( EMCP_Tools_OAuth_Metadata::path_matches( '/.well-known/oauth-protected-resource', '/.well-known/oauth-protected-resource' ) );
|
|
$this->assertTrue( EMCP_Tools_OAuth_Metadata::path_matches( '/.well-known/oauth-protected-resource/wp-json/mcp/x', '/.well-known/oauth-protected-resource' ) );
|
|
$this->assertFalse( EMCP_Tools_OAuth_Metadata::path_matches( '/.well-known/oauth-protected-resource-other', '/.well-known/oauth-protected-resource' ) );
|
|
}
|
|
|
|
/** Resource comparison normalizes only URI components that are insensitive. */
|
|
public function test_resource_uri_normalization() {
|
|
$this->assertSame(
|
|
'https://example.test/wp-json/mcp/emcp-tools-server',
|
|
EMCP_Tools_OAuth_Metadata::normalize_resource_uri( 'HTTPS://EXAMPLE.TEST/wp-json/mcp/emcp-tools-server/' )
|
|
);
|
|
$this->assertSame( '', EMCP_Tools_OAuth_Metadata::normalize_resource_uri( '/wp-json/mcp/emcp-tools-server' ) );
|
|
$this->assertSame( '', EMCP_Tools_OAuth_Metadata::normalize_resource_uri( 'https://example.test/resource#fragment' ) );
|
|
$this->assertSame( '', EMCP_Tools_OAuth_Metadata::normalize_resource_uri( 'https://[email protected]/resource' ) );
|
|
}
|
|
|
|
/** OAuth codes and tokens must not be accepted for a different audience. */
|
|
public function test_resource_match_is_bound_to_the_canonical_mcp_endpoint() {
|
|
$resource = EMCP_Tools_OAuth_Metadata::resource();
|
|
$this->assertTrue( EMCP_Tools_OAuth_Metadata::resource_matches( $resource . '/' ) );
|
|
$this->assertFalse( EMCP_Tools_OAuth_Metadata::resource_matches( 'https://attacker.example/wp-json/mcp/emcp-tools-server' ) );
|
|
$this->assertFalse( EMCP_Tools_OAuth_Metadata::resource_matches( $resource . '?audience=other' ) );
|
|
}
|
|
|
|
/** The challenge must never advertise the shared bare metadata path (#130). */
|
|
public function test_challenge_advertises_emcp_specific_metadata_url() {
|
|
$metadata_url = EMCP_Tools_OAuth_Metadata::protected_resource_url();
|
|
$challenge = EMCP_Tools_OAuth_Bearer::challenge_value();
|
|
|
|
$this->assertStringContainsString( 'resource_metadata="' . $metadata_url . '"', $challenge );
|
|
$this->assertStringNotContainsString( 'resource_metadata="' . EMCP_Tools_OAuth_Metadata::issuer() . EMCP_Tools_OAuth_Metadata::PATH_PROTECTED_RESOURCE . '"', $challenge );
|
|
$this->assertStringContainsString( 'scope="mcp"', $challenge );
|
|
}
|
|
|
|
/** A valid-looking document for another plugin must fail diagnostics (#130). */
|
|
public function test_metadata_identifier_rejects_another_plugins_document() {
|
|
$expected = EMCP_Tools_OAuth_Metadata::resource();
|
|
$this->assertTrue( EMCP_Tools_OAuth_Metadata::identifier_matches( $expected, $expected ) );
|
|
$this->assertFalse(
|
|
EMCP_Tools_OAuth_Metadata::identifier_matches(
|
|
EMCP_Tools_OAuth_Metadata::issuer() . '/wp-json/mcp/another-plugin',
|
|
$expected
|
|
)
|
|
);
|
|
}
|
|
}
|