›
byrcsc/laravel-dev-login · 1.x
Drive a profile from a test, and turn the page on for browser tests that click its buttons.
Laravel's actingAs() is still the right tool for authenticating in a feature
test. Reach for this package in a test when the thing under test is the
profile itself: a resolver of yours, a tenant resolver, or a browser test that
clicks a real button.
testing is on the default environment allowlist, so the package is allowed to
run in a test suite. The enable flag still has to say yes.
Authenticator takes a Profile and returns the authenticated user. It needs
no routes, so the gates never come into it:
use ByRcsc\LaravelDevLogin\Authenticator;
use ByRcsc\LaravelDevLogin\Profile;
it('logs in through my resolver', function (): void {
User::factory()->create(['email' => 'admin@example.com']);
app(Authenticator::class)->login(new Profile(
key: 'admin',
label: 'Admin',
email: 'admin@example.com',
resolver: App\DevLogin\FindUserByUsername::class,
));
expect(auth()->check())->toBeTrue();
});Every profile key is available as a constructor argument, so a test can build a shape that is not in your config file.
This is the seam to use for testing a UserResolver or a TenantResolver.
Assert on your own resolver's side effects rather than on the redirect, which
belongs to the controller.
Config the authenticator reads at call time, dev-login.resolver and
dev-login.tenant_resolver, can be set with a plain config()->set() inside
the test. It is only the gates that are decided earlier.
A resolver is a class with one method, so most of it needs no framework at
all. Where it does, drive it through Authenticator to get the ordering the
real flow gives it:
use ByRcsc\LaravelDevLogin\Authenticator;
use ByRcsc\LaravelDevLogin\Profile;
it('resolves the user inside the tenant', function (): void {
config()->set('dev-login.tenant_resolver', App\DevLogin\MakeTenantCurrent::class);
app(Authenticator::class)->login(new Profile(
key: 'acme-owner',
label: 'Owner',
email: 'owner@acme.test',
tenant: 'acme',
));
expect(Tenant::current()?->slug)->toBe('acme');
});The tenant is made current before the user is resolved, so a resolver that asserts what was current when it ran is asserting the contract's promise.
The gates are read while the application boots, and route registration is
decided there. A config()->set('dev-login.enabled', true) inside a test body
lands too late: the routes are already absent.
Set it before the application boots instead. In phpunit.xml:
<php>
<env name="DEV_LOGIN_ENABLED" value="true"/>
</php>Or in .env.testing:
DEV_LOGIN_ENABLED=trueBoth reach enabled through the same env() call the config file makes.
Requests in Laravel's test suite arrive on localhost by default, which is on
the shipped allowed-hosts list. If your suite sets APP_URL to something
else, add that host to allowed_hosts.
With the flag on, the routes behave like any others:
it('shows a button per profile', function (): void {
config()->set('dev-login.profiles', [
'admin' => ['label' => 'Admin', 'email' => 'admin@example.com'],
]);
$this->get('/dev-login')->assertOk()->assertSee('Admin');
});Profiles can be set inside the test body, because ProfileRepository reads
config on every call rather than at boot. Only the gates need to be decided
earlier.
use Illuminate\Auth\Events\Login;
use Illuminate\Support\Facades\Event;
it('authenticates and fires the login event', function (): void {
User::factory()->create(['email' => 'admin@example.com']);
config()->set('dev-login.profiles', [
'admin' => ['label' => 'Admin', 'email' => 'admin@example.com'],
]);
Event::fake([Login::class]);
$this->post('/dev-login/admin')->assertRedirect('/');
Event::assertDispatched(Login::class);
expect(auth()->check())->toBeTrue();
});GET /dev-login/{profile} returns 405. There is no login-by-GET route to
assert against.
A browser test can click a real button, which is the one thing a feature test
cannot check. It needs the same three things any other request does: the
enable flag on, an environment on the allowlist, and the host the browser uses
on allowed_hosts.
The host is the part that catches people out. A browser driver visits the
application over a real URL, so whatever APP_URL resolves to has to be on the
list. 127.0.0.1 and localhost are already there.
Gatekeeper answers from live config, so a test can assert an allowed-hosts
list without booting anything:
use ByRcsc\LaravelDevLogin\Gatekeeper;
it('allows our review apps', function (): void {
config()->set('dev-login.allowed_hosts', ['*.review.example.com']);
expect(app(Gatekeeper::class)->hostIsAllowed('pr-42.review.example.com'))->toBeTrue()
->and(app(Gatekeeper::class)->hostIsAllowed('review.example.com'))->toBeFalse();
});passes() reads the environment through the application, so a test that
changes app.env has to rebuild the application for it to take effect.
Authenticator::login() does.