Skip to content

Commit

Permalink
[feature] add authentication assertions to KernelBrowser (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
kbond authored Apr 14, 2022
1 parent d30d5d6 commit 582d70f
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 8 deletions.
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,6 @@ $browser
->assertXml()
->assertHtml()

// authenticate a user for subsequent actions
->actingAs($user) // \Symfony\Component\Security\Core\User\UserInterface

// If using zenstruck/foundry, you can pass a factory/proxy
->actingAs(UserFactory::new())

// by default, exceptions are caught and converted to a response
// use the BROWSER_CATCH_EXCEPTIONS environment variable to change default
// this disables that behaviour allowing you to use TestCase::expectException()
Expand Down Expand Up @@ -296,6 +290,35 @@ $browser->use(function(\Symfony\Component\HttpKernel\DataCollector\RequestDataCo
})
```

#### Authentication

The _KernelBrowser_ has helpers and assertions for authentication:

```php
/** @var \Zenstruck\Browser\KernelBrowser $browser **/

$browser
// authenticate a user for subsequent actions
->actingAs($user) // \Symfony\Component\Security\Core\User\UserInterface

// If using zenstruck/foundry, you can pass a factory/proxy
->actingAs(UserFactory::new())

// fail if authenticated
->assertNotAuthenticated()

// fail if NOT authenticated
->assertAuthenticated()

// fails if NOT authenticated as "kbond"
->assertAuthenticated('kbond')

// \Symfony\Component\Security\Core\User\UserInterface or, if using
// zenstruck/foundry, you can pass a factory/proxy
->assertAuthenticated($user)
;
```

#### HTTP Requests

The _KernelBrowser_ can be used for testing API endpoints. The following http methods are available:
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"symfony/framework-bundle": "^5.4|^6.0",
"symfony/polyfill-php80": "^1.20",
"zenstruck/assert": "^1.0",
"zenstruck/callback": "^1.1"
"zenstruck/callback": "^1.4.2"
},
"require-dev": {
"dbrekelmans/bdi": "^1.0",
Expand Down
10 changes: 10 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ parameters:
count: 1
path: src/Browser/HttpOptions.php

-
message: "#^Cannot call method getToken\\(\\) on object\\|null\\.$#"
count: 1
path: src/Browser/KernelBrowser.php

-
message: "#^Cannot call method getUserIdentifier\\(\\) on Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\TokenInterface\\|null\\.$#"
count: 1
path: src/Browser/KernelBrowser.php

-
message: "#^Method Zenstruck\\\\Browser\\\\KernelBrowser\\:\\:delete\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
Expand Down
68 changes: 67 additions & 1 deletion src/Browser/KernelBrowser.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Symfony\Bundle\FrameworkBundle\KernelBrowser as SymfonyKernelBrowser;
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Zenstruck\Assert;
use Zenstruck\Browser;
Expand Down Expand Up @@ -115,7 +116,9 @@ final public function withProfiling(): self
}

/**
* @param object|UserInterface|Proxy|Factory $user
* @param UserInterface|Proxy<UserInterface>|Factory<UserInterface> $user
*
* @return static
*/
public function actingAs(object $user, ?string $firewall = null): self
{
Expand All @@ -136,6 +139,58 @@ public function actingAs(object $user, ?string $firewall = null): self
return $this;
}

/**
* @param string|UserInterface|Proxy<UserInterface>|Factory<UserInterface>|null $as
*
* @return static
*/
public function assertAuthenticated($as = null): self
{
Assert::that($token = $this->securityToken())
->isNotNull('Expected to be authenticated but NOT.')
;

if (!$as) {
return $this;
}

if ($as instanceof Factory) {
$as = $as->create();
}

if ($as instanceof Proxy) {
$as = $as->object();
}

if ($as instanceof UserInterface) {
$as = $as->getUserIdentifier();
}

if (!\is_string($as)) {
throw new \LogicException(\sprintf('%s() requires the "as" user be a string or %s.', __METHOD__, UserInterface::class));
}

Assert::that($token->getUserIdentifier())
->is($as, 'Expected to be authenticated as "{expected}" but authenticated as "{actual}".')
;

return $this;
}

/**
* @return static
*/
public function assertNotAuthenticated(): self
{
Assert::that($token = $this->securityToken())
->isNull('Expected to NOT be authenticated but authenticated as "{actual}".', [
'actual' => $token ? $token->getUserIdentifier() : null,
])
;

return $this;
}

final public function profile(): Profile
{
if (!$profile = $this->client()->getProfile()) {
Expand Down Expand Up @@ -428,4 +483,15 @@ protected function useParameters(): array
})),
];
}

private function securityToken(): ?TokenInterface
{
$container = $this->client()->getContainer();

if (!$container->has('security.token_storage')) {
throw new \LogicException('Security not available/enabled.');
}

return $container->get('security.token_storage')->getToken();
}
}
39 changes: 39 additions & 0 deletions tests/KernelBrowserTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,45 @@ public function can_act_as_user_with_foundry_proxy(): void
;
}

/**
* @test
*/
public function can_make_authentication_assertions(): void
{
// todo remove this requirement in foundry
Factory::boot(new Configuration());

$username = 'kevin';
$user = new InMemoryUser('kevin', 'pass');
$factory = factory(InMemoryUser::class, ['username' => 'kevin', 'password' => 'pass']);
$proxy = factory(InMemoryUser::class)->create(['username' => 'kevin', 'password' => 'pass']);

$this->browser()
->assertNotAuthenticated()
->actingAs($user)
->assertAuthenticated()
->assertAuthenticated($username)
->assertAuthenticated($user)
->assertAuthenticated($factory)
->assertAuthenticated($proxy)
->visit('/user')
->assertAuthenticated()
->assertAuthenticated($username)
;
}

/**
* @test
*/
public function can_check_if_not_authenticated_after_request(): void
{
$this->browser()
->visit('/page1')
->assertNotAuthenticated()
->assertSeeIn('a', 'a link')
;
}

/**
* @test
*/
Expand Down

0 comments on commit 582d70f

Please sign in to comment.