Skip to content

Latest commit

 

History

History
128 lines (103 loc) · 5.57 KB

testing.md

File metadata and controls

128 lines (103 loc) · 5.57 KB

Testing

CakeDC\OAuthServer\TestSuite\OAuthTestTrait lets integration tests call your protected routes as an authenticated OAuth client, without walking through the OAuth flow. Use it together with CakePHP's IntegrationTestTrait.

// tests/TestCase/Controller/Api/PostsControllerTest.php
namespace App\Test\TestCase\Controller\Api;

use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
use CakeDC\OAuthServer\OAuthServer;
use CakeDC\OAuthServer\TestSuite\OAuthTestTrait;

class PostsControllerTest extends TestCase
{
    use IntegrationTestTrait;
    use OAuthTestTrait;

    protected array $fixtures = ['app.Users'];

    protected function setUp(): void
    {
        parent::setUp();
        OAuthServer::reset();
    }

    public function testEditorCanReadPosts(): void
    {
        $this->actAsOAuth(1, ['content:read'], 'https://example.com/mcp');
        $this->get('/api/posts');

        $this->assertResponseOk();
    }

    public function testAnInvalidTokenIsRefused(): void
    {
        // An invalid token with this server's prefix is challenged with 401.
        $this->configRequest(['headers' => ['Authorization' => 'Bearer example_at_invalid']]);
        $this->get('/api/posts');

        $this->assertResponseCode(401);
    }
}

The test database

The trait issues real tokens, stored in the plugin's tables (oauth_clients, oauth_tokens, oauth_authorization_codes, oauth_consents), so the test database needs the plugin's migrations. The tests/bootstrap.php of the application skeleton runs only the application's own:

(new Migrator())->run();

Run the plugin's in the same call instead:

use Migrations\TestSuite\Migrator;

(new Migrator())->runMany([
    [],
    ['plugin' => 'CakeDC/OAuthServer'],
]);

[] stands for the application's migrations. If the application uses cakedc/users, its migrations go in the same list: ['plugin' => 'CakeDC/Users']. Without the plugin tables, the first token the trait issues fails on a missing table.

The methods

Method Does
oauthTokenFor($identity, array $scopes = [], ?string $resource = null): string Issues a token and returns its value
withOAuthToken(string $token): void Sends the token as Authorization: Bearer ... on the next request
actAsOAuth($identity, array $scopes = [], ?string $resource = null): string Both of the above; returns the token
  • $identity is the person: an id, an Authentication\IdentityInterface, an entity or an array with the primary key column of your users table (id unless it is called otherwise). The person must be known to the configured user provider (the finder must find them), or InvalidArgumentException is thrown.
  • $scopes empty means the resource's defaultScopes. Scopes the resource does not declare, or the person may not hold (userProvider.scopes), throw InvalidArgumentException.
  • $resource is a resource identifier; null is the default resource. An undeclared one throws InvalidArgumentException.

What to know

  • The token is a personal token built through the public connection service, valid for one day. That means OAuthServer.personalTokens must be true in the test configuration; with false the trait throws LogicException with a clear message.

  • A personal token has no client: OAuthServer::token($request)->client is null for it. Code that depends on the client (its name, its id) cannot be tested with the trait; issue a token through a real client for that (the OAuth flow, or OAuthServer::server()->tokenIssuer()->issue() with a client created through clientRegistrar()).

  • The header applies to the next request only: IntegrationTestTrait clears request headers after sending. Call withOAuthToken() or actAsOAuth() again before each request. A second call replaces the token instead of sending two Authorization values.

  • The configuration is read when a token is issued, but the application under test builds its server from OAuthServer::server(), which is cached. Call OAuthServer::reset() after changing the OAuthServer configuration inside a test (and in setUp() as shown).

  • The authenticator that the route uses must be loaded in your getAuthenticationService(), for the same resource you pass here. See protecting-apis.

  • IntegrationTestTrait's session() puts data in the session but sends no cookie, so a POST after it looks like a request without credentials to OAuthServer::csrfSkip(). To test what a browser with a session does, send the session cookie too: $this->cookie(session_name(), 'any-value'). Cookies set with cookie() stay for the following requests of the test.

  • Tests do not need a rate limit cache: set 'rateLimit' => ['cache' => null] in the test configuration only (never in production), as the plugin's own tests do. Counters would otherwise accumulate across tests in one process.

  • For code that needs a fixed time or a fake metadata fetch, replace the services for the test with oauthReplaceServices(). The real ones come back after each test (oauthRestoreServices() runs as a PHPUnit #[After] method):

    use CakeDC\OAuth\Server\Testing\FrozenClock;
    
    $clock = new FrozenClock();
    $this->oauthReplaceServices(clock: $clock);
    $token = $this->oauthTokenFor(1, ['content:read']);
    $clock->advance(2 * 86400);
    // the token has expired now

    It takes clock (a Psr\Clock\ClockInterface), transport (a CakeDC\OAuth\Server\Client\Cimd\HttpTransportInterface) and dns (a CakeDC\OAuth\Server\Client\Cimd\DnsResolverInterface), and resets the server.