presseportale/tests/Feature/LegacyUserProfileImportTest.php
Kevin Adametz 94cb209a9f WS-6: E-Mail-Verifizierung, Auth-Flow-Fixes & Legacy-Rollen-Sicherheitsfix
E-Mail-Verifizierung (Entscheidung 15.06.):
- User implementiert MustVerifyEmail; Registrierung legt inaktives, rollenloses
  Konto an und leitet auf die Danke-/Notice-Seite; Registered-Event versendet die
  Verifizierungsmail. Bestätigter Link aktiviert das Konto + vergibt customer-Rolle
  (ActivateUserAfterVerification). Backfill-Migration setzt email_verified_at für
  alle Bestands-User (sonst würde die verified-Middleware ~59k aktive Legacy-User
  aussperren). Seeder-User verifiziert.

Auth-Flow-Korrekturen:
- Magic-Link-Consume: rollensicherer Redirect ohne intended() (Customer landete
  sonst per stale intended=/dashboard im 403-Admin-Bereich).
- Guest-Redirect (bootstrap/app.php) rollen-/verifizierungsbewusst statt fix
  /dashboard – schließt die 403-Sackgasse auf /login und /register.
- Logout auf der Notice-Seite via echtes POST-Formular statt Livewire-Action
  (behebt 419 beim Session-Invalidate).
- Magic-Link-Anforderung über eigenes Modal mit separater E-Mail-Eingabe.
- Unverifizierte Login-Versuche landen auf der Notice-Seite.

Sicherheitsfix Legacy-Rollen:
- UserImporter mappte Alt-Gruppe 2 (Self-Publisher) auf editor (= Admin-Zugriff).
  Mapping auf customer korrigiert; Daten-Migration stuft die 65.950 fälschlichen
  Legacy-Editoren auf customer herab. Echte admin/api-only bleiben unberührt.

Tests: Registration, EmailVerification, Authentication (Guest-Redirect),
MagicLinkLogin (Modal/Redirect/Regression), Legacy-Import (Gruppen-Mapping).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 08:16:41 +00:00

238 lines
9.2 KiB
PHP

<?php
use App\Models\User;
use App\Services\Import\ImportContext;
use App\Services\Import\UserImporter;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
test('legacy user import creates linked profile data from sf guard user profile', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
Config::set('database.connections.mysql_presseecho', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
'foreign_key_constraints' => false,
]);
DB::purge('mysql_presseecho');
Schema::connection('mysql_presseecho')->create('sf_guard_user', function (Blueprint $table): void {
$table->integer('id')->primary();
$table->string('username')->nullable();
$table->boolean('is_active')->default(true);
$table->boolean('is_super_admin')->default(false);
$table->timestamp('last_login')->nullable();
$table->string('ip_address')->nullable();
$table->timestamp('created_at')->nullable();
$table->timestamp('updated_at')->nullable();
});
Schema::connection('mysql_presseecho')->create('sf_guard_user_profile', function (Blueprint $table): void {
$table->integer('user_id')->primary();
$table->string('email');
$table->integer('salutation_id')->nullable();
$table->string('title')->nullable();
$table->string('first_name')->nullable();
$table->string('last_name')->nullable();
$table->string('address', 1000)->nullable();
$table->integer('country_id')->nullable();
$table->string('phone')->nullable();
$table->date('birthdate')->nullable();
$table->string('language')->nullable();
$table->string('backlink_url')->nullable();
$table->boolean('show_stats')->default(false);
$table->timestamp('validation_date')->nullable();
$table->timestamp('contract_date')->nullable();
$table->string('registration_type')->nullable();
$table->string('validate')->nullable();
$table->string('tax_id_number')->nullable();
$table->boolean('tax_exempt')->default(false);
$table->string('tax_exempt_reason', 1000)->nullable();
$table->boolean('disable_footer_code')->default(false);
});
Schema::connection('mysql_presseecho')->create('sf_guard_user_group', function (Blueprint $table): void {
$table->integer('user_id');
$table->integer('group_id');
});
DB::connection('mysql_presseecho')->table('sf_guard_user')->insert([
'id' => 42,
'username' => 'legacy-user',
'is_active' => true,
'is_super_admin' => false,
'last_login' => '2026-04-01 10:00:00',
'ip_address' => '127.0.0.1',
'created_at' => '2020-01-01 00:00:00',
'updated_at' => '2020-01-02 00:00:00',
]);
DB::connection('mysql_presseecho')->table('sf_guard_user_profile')->insert([
'user_id' => 42,
'email' => 'legacy-profile@example.com',
'salutation_id' => 2,
'title' => 'Dr.',
'first_name' => 'Legacy',
'last_name' => 'Profil',
'address' => 'Profilstrasse 7',
'country_id' => 177,
'phone' => '030123456',
'birthdate' => '1980-05-10',
'language' => 'de',
'backlink_url' => 'https://legacy.example.com',
'show_stats' => true,
'validation_date' => '2021-01-01 00:00:00',
'contract_date' => '2020-12-31 00:00:00',
'registration_type' => 'company',
'validate' => 'abc123',
'tax_id_number' => 'DE123456789',
'tax_exempt' => true,
'tax_exempt_reason' => 'Reverse Charge',
'disable_footer_code' => true,
]);
DB::connection('mysql_presseecho')->table('sf_guard_user_group')->insert([
'user_id' => 42,
'group_id' => 4,
]);
$result = app(UserImporter::class)->run(new ImportContext('presseecho', false, true));
expect($result->failed())->toBe(0);
expect($result->imported())->toBe(1);
$user = User::query()
->with('profile')
->where('email', 'legacy-profile@example.com')
->firstOrFail();
expect($user->profile)->not->toBeNull();
expect($user->profile?->salutation_key)->toBe('mrs');
expect($user->profile?->title)->toBe('Dr.');
expect($user->profile?->first_name)->toBe('Legacy');
expect($user->profile?->last_name)->toBe('Profil');
expect($user->profile?->address)->toBe('Profilstrasse 7');
expect($user->profile?->country_code)->toBe('DE');
expect($user->profile?->birthdate?->format('Y-m-d'))->toBe('1980-05-10');
expect($user->profile?->backlink_url)->toBe('https://legacy.example.com');
expect($user->profile?->show_stats)->toBeTrue();
expect($user->profile?->tax_id_number)->toBe('DE123456789');
expect($user->profile?->tax_exempt)->toBeTrue();
expect($user->profile?->tax_exempt_reason)->toBe('Reverse Charge');
expect($user->profile?->disable_footer_code)->toBeTrue();
});
/**
* Baut das minimale Legacy-Schema und importiert einen User in der gegebenen
* Alt-Gruppe. Dient der Rollen-Mapping-Absicherung.
*/
function importLegacyUserInGroup(int $groupId, string $email): void
{
Config::set('database.connections.mysql_presseecho', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
'foreign_key_constraints' => false,
]);
DB::purge('mysql_presseecho');
Schema::connection('mysql_presseecho')->create('sf_guard_user', function (Blueprint $table): void {
$table->integer('id')->primary();
$table->string('username')->nullable();
$table->boolean('is_active')->default(true);
$table->boolean('is_super_admin')->default(false);
$table->timestamp('last_login')->nullable();
$table->string('ip_address')->nullable();
$table->timestamp('created_at')->nullable();
$table->timestamp('updated_at')->nullable();
});
Schema::connection('mysql_presseecho')->create('sf_guard_user_profile', function (Blueprint $table): void {
$table->integer('user_id')->primary();
$table->string('email');
$table->integer('salutation_id')->nullable();
$table->string('title')->nullable();
$table->string('first_name')->nullable();
$table->string('last_name')->nullable();
$table->string('address', 1000)->nullable();
$table->integer('country_id')->nullable();
$table->string('phone')->nullable();
$table->date('birthdate')->nullable();
$table->string('language')->nullable();
$table->string('backlink_url')->nullable();
$table->boolean('show_stats')->default(false);
$table->timestamp('validation_date')->nullable();
$table->timestamp('contract_date')->nullable();
$table->string('registration_type')->nullable();
$table->string('validate')->nullable();
$table->string('tax_id_number')->nullable();
$table->boolean('tax_exempt')->default(false);
$table->string('tax_exempt_reason', 1000)->nullable();
$table->boolean('disable_footer_code')->default(false);
});
Schema::connection('mysql_presseecho')->create('sf_guard_user_group', function (Blueprint $table): void {
$table->integer('user_id');
$table->integer('group_id');
});
DB::connection('mysql_presseecho')->table('sf_guard_user')->insert([
'id' => 77,
'username' => 'grp-user',
'is_active' => true,
'is_super_admin' => false,
'created_at' => '2020-01-01 00:00:00',
'updated_at' => '2020-01-02 00:00:00',
]);
DB::connection('mysql_presseecho')->table('sf_guard_user_profile')->insert([
'user_id' => 77,
'email' => $email,
'first_name' => 'Gruppen',
'last_name' => 'Nutzer',
'language' => 'de',
]);
DB::connection('mysql_presseecho')->table('sf_guard_user_group')->insert([
'user_id' => 77,
'group_id' => $groupId,
]);
app(UserImporter::class)->run(new ImportContext('presseecho', false, true));
}
test('legacy group 2 (self-publisher) imports as customer without admin access', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
importLegacyUserInGroup(2, 'legacy-group2@example.com');
$user = User::query()->where('email', 'legacy-group2@example.com')->firstOrFail();
// Alt-Gruppe 2 ("editor") darf NICHT die neue editor-Rolle (Admin-Zugriff)
// erhalten, sondern customer ("Mein Bereich").
expect($user->hasRole('customer'))->toBeTrue();
expect($user->hasRole('editor'))->toBeFalse();
expect($user->canAccessAdmin())->toBeFalse();
expect($user->canAccessCustomer())->toBeTrue();
});
test('legacy group 1 still imports as admin', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
importLegacyUserInGroup(1, 'legacy-group1@example.com');
$user = User::query()->where('email', 'legacy-group1@example.com')->firstOrFail();
expect($user->hasRole('admin'))->toBeTrue();
expect($user->canAccessAdmin())->toBeTrue();
});