presseportale/tests/Feature/Auth/CompanyMembershipAuditTest.php
Kevin Adametz 05cf53e17d security/api/betrieb: Fortify-2FA-Nebenwege gesperrt, Policy nur für aktive Super-Admins, Nachprüfungen aus dem Umsetzungsaudit (Block 7, U08–U10, U13, 7.1, 7.2, 7.5)
- Fortifys sieben 2FA-Verwaltungsrouten antworten mit 404 (Middleware über
  fortify.middleware); die Sicherheitsseite verlangt weiter den zweiten Faktor (U09)
- PressReleasePolicy/CompanyPolicy::before() bejahen nur aktive Super-Admins (7.2)
- API-Update prüft bei Portalwechsel die bestehende Kategorie gegen das neue Portal (U10)
- Aggregation: Zeitbudget 480 s unter dem Lock von 600 s, Marke nur unter
  Zeilensperre vom erwarteten Stand aus, sonst Fehler-Exit (U13)
- Newsletter-Bereinigungsmigration behält je Paar die sent-Zeile (U08)
- E-Mail-Wechsel stößt den Rechte-Sync sofort an; company-memberships:audit
  --classify zeigt vorher die Kandidaten und begrenzt mit --before (7.1)
- Detailseite wählt bei Slug-Kollision wie der Melde-Resolver (7.5)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 12:03:04 +00:00

78 lines
4.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Models\Company;
use App\Models\Contact;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
/**
* Bestandsprüfung der `responsible`-Zuordnungen (Audit F01/F10, Plan 1.4):
* Welche Verantwortlichkeiten lassen sich heute noch über eine hinterlegte
* E-Mail erklären, welche nicht – und `--classify` markiert die erklärbaren
* als automatisch vergeben, damit der Login-Sync sie später zurücknehmen kann.
*/
test('the audit lists unexplained responsible links and classifies explained ones on request', function () {
/** @var TestCase $this */
Storage::fake('local');
$viaCompany = User::factory()->create(['email' => 'firma@example.test']);
$viaContact = User::factory()->create(['email' => 'kontakt@example.test']);
$orphan = User::factory()->create(['email' => 'niemand@example.test']);
$companyA = Company::factory()->presseecho()->create(['email' => 'firma@example.test', 'name' => 'Erklärbar AG']);
$companyB = Company::factory()->presseecho()->create(['email' => 'andere@example.test', 'name' => 'Kontakt GmbH']);
Contact::factory()->for($companyB)->create(['email' => 'Kontakt@Example.test', 'portal' => $companyB->portal->value]);
$companyC = Company::factory()->presseecho()->create(['email' => 'dritte@example.test', 'name' => 'Verwaist KG']);
$viaCompany->companies()->attach($companyA->id, ['role' => 'responsible']);
$viaContact->companies()->attach($companyB->id, ['role' => 'responsible']);
$orphan->companies()->attach($companyC->id, ['role' => 'responsible']);
$orphan->companies()->attach($companyA->id, ['role' => 'owner']);
$this->artisan('company-memberships:audit', ['--since' => '2000-01-01'])
->expectsOutputToContain('Verwaist KG')
->doesntExpectOutputToContain('Erklärbar AG')
->doesntExpectOutputToContain('Kontakt GmbH')
->assertSuccessful();
// Ohne --classify bleibt alles manual.
expect(DB::table('company_user')->where('granted_via', 'auto')->count())->toBe(0);
$this->artisan('company-memberships:audit', ['--classify' => true, '--since' => '2000-01-01'])->assertSuccessful();
$via = fn (int $userId, int $companyId): string => DB::table('company_user')->where('user_id', $userId)->where('company_id', $companyId)->value('granted_via');
expect($via($viaCompany->id, $companyA->id))->toBe('auto')
->and($via($viaContact->id, $companyB->id))->toBe('auto')
->and($via($orphan->id, $companyC->id))->toBe('manual')
->and($via($orphan->id, $companyA->id))->toBe('manual');
expect(collect(Storage::disk('local')->files())->filter(fn (string $f) => str_starts_with($f, 'company-memberships-unexplained-')))->toHaveCount(1);
});
test('classify previews its candidates and skips links created after the given deploy time (Audit 7.1)', function () {
/** @var TestCase $this */
Storage::fake('local');
$legacy = User::factory()->create(['email' => 'alt@example.test']);
$recent = User::factory()->create(['email' => 'neu@example.test']);
$companyOld = Company::factory()->presseecho()->create(['email' => 'alt@example.test', 'name' => 'Altbestand AG']);
$companyNew = Company::factory()->presseecho()->create(['email' => 'neu@example.test', 'name' => 'Frisch GmbH']);
$legacy->companies()->attach($companyOld->id, ['role' => 'responsible', 'created_at' => '2026-01-10 08:00:00', 'updated_at' => '2026-01-10 08:00:00']);
// Nach dem Deploy manuell vergeben: trägt ihre Herkunft bereits, darf nicht umgestuft werden.
$recent->companies()->attach($companyNew->id, ['role' => 'responsible', 'granted_via' => 'manual', 'created_at' => '2026-09-20 09:00:00', 'updated_at' => '2026-09-20 09:00:00']);
$this->artisan('company-memberships:audit', ['--classify' => true, '--since' => '2000-01-01', '--before' => '2026-09-19 00:00:00'])
->expectsOutputToContain('Umzustufen auf auto: 1 Zuordnungen vor 2026-09-19 00:00:00')
->expectsOutputToContain('Altbestand AG')
->expectsOutputToContain('1 Zuordnungen als automatisch (auto) markiert.')
->assertSuccessful();
$via = fn (int $userId, int $companyId): string => DB::table('company_user')->where('user_id', $userId)->where('company_id', $companyId)->value('granted_via');
expect($via($legacy->id, $companyOld->id))->toBe('auto')
->and($via($recent->id, $companyNew->id))->toBe('manual');
});