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 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.
| 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 |
$identityis the person: an id, anAuthentication\IdentityInterface, an entity or an array with the primary key column of your users table (idunless it is called otherwise). The person must be known to the configured user provider (thefindermust find them), orInvalidArgumentExceptionis thrown.$scopesempty means the resource'sdefaultScopes. Scopes the resource does not declare, or the person may not hold (userProvider.scopes), throwInvalidArgumentException.$resourceis a resource identifier;nullis the default resource. An undeclared one throwsInvalidArgumentException.
-
The token is a personal token built through the public connection service, valid for one day. That means
OAuthServer.personalTokensmust betruein the test configuration; withfalsethe trait throwsLogicExceptionwith a clear message. -
A personal token has no client:
OAuthServer::token($request)->clientisnullfor 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, orOAuthServer::server()->tokenIssuer()->issue()with a client created throughclientRegistrar()). -
The header applies to the next request only: IntegrationTestTrait clears request headers after sending. Call
withOAuthToken()oractAsOAuth()again before each request. A second call replaces the token instead of sending twoAuthorizationvalues. -
The configuration is read when a token is issued, but the application under test builds its server from
OAuthServer::server(), which is cached. CallOAuthServer::reset()after changing theOAuthServerconfiguration inside a test (and insetUp()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 toOAuthServer::csrfSkip(). To test what a browser with a session does, send the session cookie too:$this->cookie(session_name(), 'any-value'). Cookies set withcookie()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(aPsr\Clock\ClockInterface),transport(aCakeDC\OAuth\Server\Client\Cimd\HttpTransportInterface) anddns(aCakeDC\OAuth\Server\Client\Cimd\DnsResolverInterface), and resets the server.