CluPilotCloud/tests/Feature/ImpersonationTest.php

152 lines
6.5 KiB
PHP

<?php
use App\Models\Customer;
use App\Models\Operator;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\URL;
it('hands over a signed link instead of relying on a shared cookie', function () {
$operator = Operator::factory()->role('Owner')->create();
$customer = Customer::factory()->create();
$response = $this->actingAs($operator, 'operator')
->post(route('admin.impersonate', $customer));
// The console and the portal are different hosts in exclusive mode, and a
// session cookie is host-bound — so the handover cannot be a cookie.
$response->assertRedirectContains('signature=');
});
it('impersonates a customer who already has a portal login, with the operator session still active throughout', function () {
// Customer::assertNotAdmin() used to check Auth::guard('operator')->check()
// instead of inspecting the `users` row it was actually handed — so with
// the OPERATOR's own session still attached (same host, non-exclusive/
// shared mode — exactly the setup start() -> enter() runs under here,
// same test client, same cookies throughout, precisely how a real
// operator's browser carries its session across both legs in shared
// mode), ensureUser() threw the instant it found a `users` row that
// already existed for the customer's email: a 500 on the very first
// impersonation of anyone who had ever signed into the portal
// themselves. The guard is gone now (I5) — this is what "gone" has to
// mean in practice.
$operator = Operator::factory()->role('Owner')->create();
$customer = Customer::factory()->create();
// A portal login that exists independently of any impersonation — e.g.
// the customer signed up themselves before ever being impersonated.
User::factory()->create(['email' => $customer->email]);
$response = $this->actingAs($operator, 'operator')
->post(route('admin.impersonate', $customer));
$location = $response->headers->get('Location');
$this->get($location)->assertRedirect(route('dashboard'));
expect(Auth::guard('web')->check())->toBeTrue()
->and(Auth::guard('web')->user()->email)->toBe($customer->email);
});
it('signs the customer in on the web guard when the link is followed', function () {
$operator = Operator::factory()->role('Owner')->create();
$customer = Customer::factory()->create();
$user = $customer->ensureUser();
$url = URL::temporarySignedRoute('impersonate.enter', now()->addSeconds(60), [
'customer' => $customer->uuid, 'operator' => $operator->uuid,
]);
$this->get($url)->assertRedirect(route('dashboard'));
expect(Auth::guard('web')->id())->toBe($user->id);
});
it('refuses the same link twice', function () {
$operator = Operator::factory()->role('Owner')->create();
$customer = Customer::factory()->create();
$user = $customer->ensureUser();
$url = URL::temporarySignedRoute('impersonate.enter', now()->addSeconds(60), [
'customer' => $customer->uuid, 'operator' => $operator->uuid,
]);
$this->get($url)->assertRedirect(route('dashboard'));
Auth::guard('web')->logout();
// A link that still works after use is a password with an expiry date.
$this->get($url)->assertForbidden();
});
it('refuses an expired link', function () {
$operator = Operator::factory()->role('Owner')->create();
$customer = Customer::factory()->create();
$user = $customer->ensureUser();
$url = URL::temporarySignedRoute('impersonate.enter', now()->subSecond(), [
'customer' => $customer->uuid, 'operator' => $operator->uuid,
]);
$this->get($url)->assertForbidden();
});
it('leaves the operator session untouched, so leaving returns to the console', function () {
$operator = Operator::factory()->role('Owner')->create();
$customer = Customer::factory()->create();
$user = $customer->ensureUser();
$url = URL::temporarySignedRoute('impersonate.enter', now()->addSeconds(60), [
'customer' => $customer->uuid, 'operator' => $operator->uuid,
]);
$this->actingAs($operator, 'operator')->get($url);
expect(Auth::guard('operator')->check())->toBeTrue();
// "Returns to the console" is the point of this test, not merely
// decoration on its name — redirect()->to(AdminArea::home()) used to
// resolve that bare path against whatever host the leave() POST itself
// arrived on (the portal's), not the console's; see the exclusive-mode
// test below for where that actually bites.
$this->post(route('impersonate.leave'))->assertRedirect(route('admin.overview'));
expect(Auth::guard('web')->check())->toBeFalse()
->and(Auth::guard('operator')->check())->toBeTrue();
});
it('signs the link against the portal host, not the console host it was issued from', function () {
// Exclusive mode is where this bites: the console has a hostname to
// itself, and RestrictAdminHost 404s any non-console route reached
// through it. impersonate.enter carries no domain of its own, so a link
// built and signed against the console's root would die the instant it
// was followed — precisely the cross-host failure this task exists to
// fix. A test that only checks `signature=` is present does not catch
// this: on dev it passes anyway, because exclusive mode is off there.
config()->set('admin_access.hosts', ['admin.example.test']);
config()->set('admin_access.exclusive', true);
config()->set('app.url', 'https://app.example.test');
$operator = Operator::factory()->role('Owner')->create();
$customer = Customer::factory()->create();
$user = $customer->ensureUser();
// Issued exactly as a real operator's click would arrive: on the
// console's own exclusive host. (Routes are compiled once at boot, under
// this suite's default non-exclusive ADMIN_HOSTS="" — so the path still
// carries the fallback /admin prefix; only RestrictAdminHost's own checks
// read admin_access.* live, per request, which is what is under test.)
$response = $this->actingAs($operator, 'operator')
->post('https://admin.example.test/admin/impersonate/'.$customer->uuid);
$location = $response->headers->get('Location');
// The link has to carry the PORTAL's host, not the console's.
expect(parse_url($location, PHP_URL_HOST))->toBe('app.example.test');
// And it must still genuinely work when followed: valid signature, signs
// the customer in.
$this->get($location)->assertRedirect(route('dashboard'));
expect(Auth::guard('web')->id())->toBe($user->id);
});