Skip to content

Commit d2cdea9

Browse files
committed
refactor: improve testing API consistency
1 parent 8dd35db commit d2cdea9

File tree

8 files changed

+246
-80
lines changed

8 files changed

+246
-80
lines changed

docs/2-features/17-oauth.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ final class OAuthControllerTest extends IntegrationTestCase
250250
// that redirects to the provider
251251
$this->http
252252
->get('/oauth/discord')
253-
->assertRedirect();
253+
->assertRedirect($oauth->lastAuthorizationUrl);
254254

255255
// We check that the authorization URL was generated,
256256
// optionally specifying scopes and options
@@ -260,7 +260,7 @@ final class OAuthControllerTest extends IntegrationTestCase
260260
// with a fake code and the expected state
261261
$this->http
262262
->get("/oauth/discord/callback?code=some-fake-code&state={$oauth->getState()}")
263-
->assertRedirect();
263+
->assertRedirect('/');
264264

265265
// We assert that an access token was retrieved
266266
// with the same fake code we provided before

packages/auth/src/OAuth/OAuthClient.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@ interface OAuthClient
1414
public function getAuthorizationUrl(array $scopes = [], array $options = []): string;
1515

1616
/**
17-
* Get the state parameter for CSRF protection.
17+
* Gets the state parameter for CSRF protection.
1818
*/
1919
public function getState(): ?string;
2020

2121
/**
22-
* Exchange an authorization code for an access token.
22+
* Exchanges an authorization code for an access token.
2323
*/
2424
public function getAccessToken(string $code): AccessToken;
2525

2626
/**
27-
* Get user information from an OAuth provider using an access token.
27+
* Gets user information from an OAuth provider using an access token.
2828
*/
2929
public function getUser(AccessToken $token): OAuthUser;
3030

3131
/**
32-
* Complete OAuth flow with code and get user information.
32+
* Completes OAuth flow with code and get user information.
3333
*/
3434
public function fetchUser(string $code): OAuthUser;
3535
}

packages/auth/src/OAuth/OAuthConfig.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ interface OAuthConfig extends HasTag
2727
get;
2828
}
2929

30+
/**
31+
* The client ID for the OAuth provider.
32+
*/
33+
public string $clientId {
34+
get;
35+
}
36+
3037
/**
3138
* The application URI to redirect to after authorization.
3239
*/

packages/auth/src/OAuth/Testing/OAuthTester.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
namespace Tempest\Auth\OAuth\Testing;
66

77
use Tempest\Auth\OAuth\OAuthClient;
8+
use Tempest\Auth\OAuth\OAuthConfig;
89
use Tempest\Auth\OAuth\OAuthUser;
910
use Tempest\Container\Container;
10-
use Tempest\Support\Str;
11+
use Tempest\Router\UriGenerator;
1112
use UnitEnum;
1213

1314
final readonly class OAuthTester
@@ -21,13 +22,12 @@ public function __construct(
2122
*/
2223
public function fake(OAuthUser $user, null|string|UnitEnum $tag = null): TestingOAuthClient
2324
{
25+
$config = $this->container->get(OAuthConfig::class, $tag);
26+
$uri = $this->container->get(UriGenerator::class);
27+
2428
$this->container->singleton(
2529
className: OAuthClient::class,
26-
definition: $client = new TestingOAuthClient($user, match (true) {
27-
is_string($tag) => $tag,
28-
$tag instanceof UnitEnum => Str\to_kebab_case($tag->name),
29-
default => 'default',
30-
}),
30+
definition: $client = new TestingOAuthClient($user, $config, $uri),
3131
tag: $tag,
3232
);
3333

packages/auth/src/OAuth/Testing/TestingOAuthClient.php

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,36 @@
77
use League\OAuth2\Client\Token\AccessToken;
88
use PHPUnit\Framework\Assert;
99
use Tempest\Auth\OAuth\OAuthClient;
10+
use Tempest\Auth\OAuth\OAuthConfig;
1011
use Tempest\Auth\OAuth\OAuthUser;
12+
use Tempest\Router\UriGenerator;
13+
use Tempest\Support\Arr;
1114
use Tempest\Support\Random;
1215
use Tempest\Support\Str;
13-
use UnitEnum;
1416

1517
final class TestingOAuthClient implements OAuthClient
1618
{
1719
private ?string $state = null;
1820

19-
private ?string $baseUrl = null;
21+
private(set) ?string $baseUrl = null;
2022

21-
private ?string $clientId = null;
23+
public string $clientId {
24+
get => $this->clientId ?? $this->config->clientId;
25+
}
2226

23-
private ?string $redirectUri = null;
27+
/**
28+
* The application URI to redirect to after authorization.
29+
*/
30+
public string $redirectUri {
31+
get => $this->uri->createUri($this->redirectUri ?? $this->config->redirectUri);
32+
}
33+
34+
/**
35+
* The last authorization URL that was generated by the client.
36+
*/
37+
public ?string $lastAuthorizationUrl {
38+
get => Arr\last($this->authorizationUrls)['url'] ?? null;
39+
}
2440

2541
private array $authorizationUrls = [];
2642

@@ -29,17 +45,21 @@ final class TestingOAuthClient implements OAuthClient
2945
private array $callbacks = [];
3046

3147
public function __construct(
32-
private OAuthUser $user,
33-
private readonly null|UnitEnum|string $tag = null,
48+
private(set) OAuthUser $user,
49+
private(set) OAuthConfig $config,
50+
private UriGenerator $uri,
3451
) {}
3552

3653
public function getAuthorizationUrl(array $scopes = [], array $options = []): string
3754
{
3855
$this->state = Random\secure_string(16);
3956

57+
$provider = $this->config->createProvider();
58+
$provider->getBaseAuthorizationUrl();
59+
4060
$url = sprintf(
4161
'%s/oauth/authorize?redirect_uri=%s&client_id=%s&state=%s',
42-
$this->baseUrl,
62+
$this->baseUrl ?? $provider->getBaseAuthorizationUrl(),
4363
$this->redirectUri,
4464
$this->clientId,
4565
$this->state,
@@ -143,7 +163,7 @@ public function assertAuthorizationUrlGenerated(?array $scopes = null, ?array $o
143163
Assert::assertNotEmpty($this->authorizationUrls, 'No authorization URL was generated.');
144164

145165
if ($options !== null) {
146-
$lastUrl = end($this->authorizationUrls);
166+
$lastUrl = Arr\last($this->authorizationUrls);
147167

148168
Assert::assertEquals(
149169
expected: $options,
@@ -153,7 +173,7 @@ public function assertAuthorizationUrlGenerated(?array $scopes = null, ?array $o
153173
}
154174

155175
if ($scopes !== null) {
156-
$lastUrl = end($this->authorizationUrls);
176+
$lastUrl = Arr\last($this->authorizationUrls);
157177

158178
Assert::assertEquals(
159179
expected: $scopes,

packages/auth/tests/OAuth/Testing/OAuthTestingTest.php

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPUnit\Framework\Attributes\Test;
88
use PHPUnit\Framework\Attributes\TestWith;
99
use PHPUnit\Framework\TestCase;
10+
use Tempest\Auth\OAuth\Config\GitHubOAuthConfig;
1011
use Tempest\Auth\OAuth\OAuthClient;
1112
use Tempest\Auth\OAuth\OAuthUser;
1213
use Tempest\Auth\OAuth\Testing\OAuthTester;
@@ -31,65 +32,6 @@ final class OAuthTestingTest extends TestCase
3132
);
3233
}
3334

34-
#[Test]
35-
#[TestWith(['github'])]
36-
#[TestWith([null])]
37-
public function can_fake_oauth_client(?string $tag): void
38-
{
39-
$client = $this->oauth->fake($this->user, $tag);
40-
41-
$this->assertInstanceOf(TestingOAuthClient::class, $client);
42-
$this->assertSame($client, $this->container->get(OAuthClient::class, $tag));
43-
}
44-
45-
#[Test]
46-
public function testing_oauth_client_generates_authorization_url(): void
47-
{
48-
$client = $this->oauth
49-
->fake($this->user)
50-
->withBaseUrl('https://provider.test')
51-
->withRedirectUri('https://tempest.test/oauth/callback')
52-
->withClientId('test');
53-
54-
$url = $client->getAuthorizationUrl(['scope' => 'user:email']);
55-
56-
$this->assertStringContainsString('https://tempest.test/oauth/callback', $url);
57-
$this->assertStringContainsString('https://provider.test', $url);
58-
$this->assertStringContainsString('client_id=test', $url);
59-
$this->assertNotEmpty($client->getState());
60-
61-
$client->assertAuthorizationUrlGenerated(['scope' => 'user:email']);
62-
63-
$this->assertEquals(1, $client->getAuthorizationUrlCount());
64-
}
65-
66-
#[Test]
67-
public function testing_oauth_client_handles_callback(): void
68-
{
69-
$client = $this->oauth->fake(new OAuthUser(
70-
id: 'jondoe-123',
71-
72-
name: 'Jon Doe',
73-
nickname: 'jondoe',
74-
avatar: 'https://avatars.githubusercontent.com/u/jondoe-123',
75-
raw: [],
76-
));
77-
78-
$user = $client->fetchUser('jondoe-123');
79-
80-
$this->assertEquals('jondoe-123', $user->id);
81-
$this->assertEquals('[email protected]', $user->email);
82-
$this->assertEquals('Jon Doe', $user->name);
83-
$this->assertEquals('jondoe', $user->nickname);
84-
$this->assertEquals('default', $user->provider);
85-
86-
$client->assertUserFetched('jondoe-123');
87-
$client->assertAccessTokenRetrieved('jondoe-123');
88-
89-
$this->assertEquals(1, $client->getCallbackCount());
90-
$this->assertEquals(1, $client->getAccessTokenCount());
91-
}
92-
9335
#[Test]
9436
public function integration(): void
9537
{

tests/Integration/Auth/OAuth/OAuthClientTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public function throws_exception_when_oauth_provider_is_missing(): void
3030

3131
public string $redirectUri = '';
3232

33+
public string $clientId = '';
34+
3335
public null|string|UnitEnum $tag = null;
3436

3537
public function createProvider(): AbstractProvider

0 commit comments

Comments
 (0)