presseportale/tests/Feature/Auth/AuthenticationTest.php
Kevin Adametz 6c6b9e0f26 WS-2: Magic-Link für Firmen & Pressekontakte vereinheitlicht + Schreibzugriff
Magic-Link und Pressekontakt-Zugang zu einer Seite (/anmeldelink) zusammengeführt;
altes Login-Modal entfernt, /pressekontakt-zugang leitet weiter.

- ContactAccessService deckt jetzt Firmen-E-Mail UND Pressekontakt-E-Mail ab,
  portalübergreifend (ohne PortalScope). Eine E-Mail mehrfach hinterlegt → genau
  ein Account, dem alle Firmen + Kontakte zugeordnet werden.
- Zugeordnete Firmen erhalten Pivot-Rolle 'responsible' (Schreibzugriff auf
  Stammdaten, Kontakte, Pressemitteilungen) statt nur 'member'; bestehende
  Lese-Pivots werden hochgestuft, Owner bleiben unangetastet.
- Neuer Login-Listener (SyncCompanyMembershipsOnLogin) frischt die Zuordnungen
  bei JEDEM Login (Magic-Link, Passwort, Google) auf – auch nachträglich (API)
  hinzugekommene Firmen/Kontakte mit gleicher E-Mail greifen.
- Auth-Bereich erzwingt Hellmodus: aus dem Portal übernommene .dark-Klasse wird
  am <html> entfernt (Login war im Dark Mode hängengeblieben).
- Tests: Firmen-E-Mail-Login, Multi-Firmen-Aggregation, Schreibzugriff/Upgrade,
  Per-Login-Re-Sync, Auth-Hellmodus. Sicherheits-Doku aktualisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:55:49 +00:00

144 lines
4.9 KiB
PHP

<?php
use App\Models\User;
use Database\Seeders\RolesAndPermissionsSeeder;
use Livewire\Volt\Volt as LivewireVolt;
use Tests\TestCase;
test('login screen can be rendered', function () {
/** @var TestCase $this */
$portalUrl = rtrim((string) config('domains.domain_portal_url', 'http://pressekonto.test'), '/');
$response = $this->get($portalUrl.'/login');
$response->assertStatus(200);
});
test('the auth area forces light mode and never inherits the portal dark class', function () {
/** @var TestCase $this */
$portalUrl = rtrim((string) config('domains.domain_portal_url', 'http://pressekonto.test'), '/');
// Auch mit gesetztem Dark-Cookie aus dem Portal bleibt der Login hell:
// das <html>-Tag trägt keine .dark-Klasse und das Strip-Skript ist präsent.
$response = $this->withCookie('flux_appearance', 'dark')->get($portalUrl.'/login');
$response->assertStatus(200);
$response->assertSee("classList.remove('dark')", false);
expect($response->getContent())->not->toContain('<html lang="en" class="dark"');
});
test('users can authenticate using the login screen', function () {
/** @var TestCase $this */
// Super-Admin damit canAccessAdmin() === true → Login-Redirect auf /dashboard.
// Seit der rollen-basierten Redirect-Logik (Phase 1) landen rollenlose
// User auf '/', nicht mehr auf /dashboard.
$user = User::factory()->superAdmin()->create();
$response = LivewireVolt::test('auth.login')
->set('email', $user->email)
->set('password', 'password')
->call('login');
$response
->assertHasNoErrors()
->assertRedirect(route('dashboard', absolute: false));
$this->assertAuthenticated();
$user->refresh();
expect($user->last_login_at)->not->toBeNull();
expect($user->last_login_ip)->toBe('127.0.0.1');
});
test('unverified users are redirected to the verification notice on login', function () {
/** @var TestCase $this */
$user = User::factory()->unverified()->create(['is_active' => false]);
LivewireVolt::test('auth.login')
->set('email', $user->email)
->set('password', 'password')
->call('login')
->assertHasNoErrors()
->assertRedirect(route('verification.notice', absolute: false));
$this->assertAuthenticated();
});
test('users can not authenticate with invalid password', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$response = LivewireVolt::test('auth.login')
->set('email', $user->email)
->set('password', 'wrong-password')
->call('login');
$response->assertHasErrors('email');
$this->assertGuest();
});
test('an authenticated customer visiting guest routes is not trapped on the admin dashboard', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
$this->actingAs($customer)->get('/login')->assertRedirect(route('me.dashboard'));
$this->actingAs($customer)->get('/register')->assertRedirect(route('me.dashboard'));
});
test('an authenticated admin visiting guest routes lands on the admin dashboard', function () {
/** @var TestCase $this */
$admin = User::factory()->superAdmin()->create();
$this->actingAs($admin)->get('/login')->assertRedirect(route('dashboard'));
});
test('an unverified authenticated user visiting guest routes lands on the notice', function () {
/** @var TestCase $this */
$user = User::factory()->unverified()->create(['is_active' => false]);
$this->actingAs($user)->get('/login')->assertRedirect(route('verification.notice'));
});
test('a customer login with a stale intended admin url is redirected to the customer area', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
session()->put('url.intended', url('/admin/users'));
LivewireVolt::test('auth.login')
->set('email', $customer->email)
->set('password', 'password')
->call('login')
->assertHasNoErrors()
->assertRedirect(route('me.dashboard', absolute: false));
$this->assertAuthenticatedAs($customer);
});
test('an inactive but verified user cannot log in with a password', function () {
/** @var TestCase $this */
// Factory ist standardmäßig verifiziert; nur deaktiviert.
$user = User::factory()->create(['is_active' => false]);
LivewireVolt::test('auth.login')
->set('email', $user->email)
->set('password', 'password')
->call('login')
->assertHasErrors(['email']);
$this->assertGuest();
});
test('users can logout', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/logout');
$response->assertRedirect('/');
$this->assertGuest();
});