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('refuses to create or link a portal login when the email already belongs to an operator', function () { // R21: one address must not represent both identities. This is the // provisioning/impersonation-time half of the rule; CreateNewUser enforces // the same thing at public registration (see RegisterTest). Operator::factory()->create(['email' => 'shared@example.test']); $customer = Customer::factory()->create(['email' => 'shared@example.test']); expect(fn () => $customer->ensureUser()) ->toThrow(IdentityCollisionException::class); expect(User::query()->where('email', 'shared@example.test')->exists())->toBeFalse() ->and($customer->fresh()->user_id)->toBeNull(); }); it('refuses to impersonate into an email that already belongs to an operator, instead of crashing', function () { $operator = Operator::factory()->role('Owner')->create(); Operator::factory()->create(['email' => 'shared@example.test']); $customer = Customer::factory()->create(['email' => 'shared@example.test']); $url = URL::temporarySignedRoute('impersonate.enter', now()->addSeconds(60), [ 'customer' => $customer->uuid, 'operator' => $operator->uuid, ]); $this->get($url)->assertStatus(409); expect(Auth::guard('web')->check())->toBeFalse(); }); 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); });