19-05-2026 Rebrand Pressekonto, Hub-Flux UI und Legacy-Media-Migration

Umbenennung presseportale → pressekonto in Domains, Themes und Dokumentation.
Design-Tokens, Portal-Shell, Customer-Dashboard, Auth- und Admin-PM-Views.
Artisan-Befehl migrate:legacy-media mit Tests und Hub-Flux-Entwicklungsdocs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-05-19 16:36:13 +00:00
parent 092ee0e918
commit 0a3e52d603
112 changed files with 8464 additions and 1649 deletions

View file

@ -40,7 +40,7 @@ test('admin can create a category-bound footer code', function () {
LivewireVolt::test('admin.footer-codes.create')
->set('title', 'Wirtschafts-Disclaimer')
->set('content', '<p>Powered by Presseportale</p>')
->set('content', '<p>Powered by Pressekonto</p>')
->set('portal', Portal::Both->value)
->set('language', 'de')
->set('priority', 10)

View file

@ -8,7 +8,7 @@ test('admin portal page renders with the portal vite manifest', function () {
$admin = User::factory()->superAdmin()->create();
$this->actingAs($admin)
->get('https://presseportale.test/admin/companies')
->get('https://pressekonto.test/admin/companies')
->assertSuccessful()
->assertSee('Firmen');
});

View file

@ -6,7 +6,7 @@ use Tests\TestCase;
test('login screen can be rendered', function () {
/** @var TestCase $this */
$portalUrl = rtrim((string) config('domains.domain_portal_url', 'http://presseportale.test'), '/');
$portalUrl = rtrim((string) config('domains.domain_portal_url', 'http://pressekonto.test'), '/');
$response = $this->get($portalUrl.'/login');
$response->assertStatus(200);
@ -14,7 +14,10 @@ test('login screen can be rendered', function () {
test('users can authenticate using the login screen', function () {
/** @var TestCase $this */
$user = User::factory()->create();
// 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)

View file

@ -9,16 +9,20 @@ test('registration screen can be rendered', function () {
});
test('new users can register', function () {
// terms_accepted ist seit dem Hub-Auth-Refresh Pflicht (AGB-Checkbox).
// Frisch registrierte User haben keine Rolle → Login-Code fällt auf '/'
// zurück (siehe Phase 1 rollen-basierter Redirect in login/register).
$response = Volt::test('auth.register')
->set('name', 'Test User')
->set('email', 'test@example.com')
->set('password', 'password')
->set('password_confirmation', 'password')
->set('terms_accepted', true)
->call('register');
$response
->assertHasNoErrors()
->assertRedirect(route('dashboard', absolute: false));
->assertRedirect('/');
$this->assertAuthenticated();
});
});

View file

@ -0,0 +1,130 @@
<?php
use App\Enums\PressReleaseStatus;
use App\Models\BillingAddress;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\Profile;
use App\Models\User;
use Database\Seeders\RolesAndPermissionsSeeder;
use Livewire\Volt\Volt as LivewireVolt;
use Tests\TestCase;
beforeEach(function () {
$this->seed(RolesAndPermissionsSeeder::class);
});
test('customer dashboard renders core sections without errors', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
$this->actingAs($customer);
LivewireVolt::test('customer.dashboard')
->assertSee('Mein Dashboard')
->assertSee(__('Gesamt'))
->assertSee(__('Veröffentlicht'))
->assertSee(__('In Prüfung'))
->assertSee(__('Entwürfe'))
->assertSee(__('Meine letzten Pressemitteilungen'))
->assertSee(__('Datenqualität'))
->assertSee(__('Brand-Bridge'));
});
test('empty-state with three-step intro shows when no press releases exist', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
$this->actingAs($customer);
LivewireVolt::test('customer.dashboard')
->assertSee(__('Noch keine Pressemitteilungen'))
->assertSee(__('Erste Pressemitteilung erstellen'))
->assertSee(__('Firma zuordnen'))
->assertSee(__('Mitteilung verfassen'))
->assertSee(__('Zur Prüfung senden'));
});
test('press release list and KPI counts render when data exists', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
$company = Company::factory()->create(['owner_user_id' => $customer->id]);
PressRelease::factory()
->published()
->create([
'user_id' => $customer->id,
'company_id' => $company->id,
'title' => 'Phase 2 Demo Release',
]);
PressRelease::factory()->create([
'user_id' => $customer->id,
'company_id' => $company->id,
'status' => PressReleaseStatus::Draft->value,
]);
$this->actingAs($customer);
LivewireVolt::test('customer.dashboard')
->assertSee('Phase 2 Demo Release')
->assertDontSee(__('Noch keine Pressemitteilungen'))
->assertSee('Alle anzeigen');
});
test('profile completeness hint with percentage appears for partial profiles', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
Profile::factory()->create([
'user_id' => $customer->id,
'first_name' => 'Test',
'last_name' => 'Customer',
'phone' => null,
'address' => null,
'country_code' => null,
'salutation_key' => null,
]);
$this->actingAs($customer);
LivewireVolt::test('customer.dashboard')
->assertSee(__('Profil unvollständig'))
->assertSee('33');
});
test('billing address hint disappears once address is complete', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
Profile::factory()->create([
'user_id' => $customer->id,
'salutation_key' => 'mr',
'first_name' => 'Test',
'last_name' => 'Customer',
'phone' => '+49 30 1234567',
'address' => 'Teststr. 1',
'country_code' => 'DE',
]);
BillingAddress::query()->create([
'user_id' => $customer->id,
'name' => 'Test Customer GmbH',
'address1' => 'Teststr. 1',
'postal_code' => '10115',
'city' => 'Berlin',
'country_code' => 'DE',
]);
$this->actingAs($customer);
LivewireVolt::test('customer.dashboard')
->assertDontSee(__('Profil unvollständig'))
->assertDontSee(__('Rechnungsadresse fehlt'))
->assertSee(__('Alles im grünen Bereich'));
});

View file

@ -0,0 +1,229 @@
<?php
use App\Enums\Portal;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\PressReleaseImage;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;
test('legacy media migration copies company logos from migration folder using legacy database rows', function () {
/** @var TestCase $this */
Storage::fake('public');
configureLegacyMediaConnection();
$basePath = prepareLegacyMediaFolder();
File::ensureDirectoryExists("{$basePath}/presseecho/uploads/company");
File::put("{$basePath}/presseecho/uploads/company/logo-a.jpg", migrateLegacyMediaFakeImageBytes());
DB::connection('mysql_presseecho')->table('company')->insert([
'id' => 123,
'logo' => 'logo-a.jpg',
]);
$company = Company::factory()->create([
'portal' => Portal::Presseecho->value,
'legacy_portal' => Portal::Presseecho->value,
'legacy_id' => 123,
'logo_path' => null,
'logo_variants' => null,
]);
$this->artisan('legacy:migrate-media', [
'--portal' => Portal::Presseecho->value,
'--type' => 'company-logos',
'--base-path' => $basePath,
])
->assertSuccessful()
->expectsOutputToContain('presseecho/company-logos: Legacy 1, migriert 1, Thumbnail-Fallback 0, DB-Updates 1');
$company->refresh();
expect($company->logo_path)->toBe("company-logos/presseecho/{$company->id}/logo-a.jpg")
->and($company->logo_variants)->toHaveKeys(['sq', 'wide']);
Storage::disk('public')->assertExists($company->logo_path);
foreach ($company->logo_variants as $variantPath) {
Storage::disk('public')->assertExists($variantPath);
}
});
test('legacy media migration creates press release image records from legacy database rows', function () {
/** @var TestCase $this */
Storage::fake('public');
configureLegacyMediaConnection();
$basePath = prepareLegacyMediaFolder();
File::ensureDirectoryExists("{$basePath}/presseecho/uploads/pressreleaseimage");
File::put("{$basePath}/presseecho/uploads/pressreleaseimage/photo-a.jpg", migrateLegacyMediaFakeImageBytes(1600, 1200));
DB::connection('mysql_presseecho')->table('press_release_image')->insert([
'id' => 987,
'title' => 'Pressefoto',
'description' => 'Beschreibung',
'image' => 'photo-a.jpg',
'copyright' => 'Copyright',
'press_release_id' => 456,
'is_preview_image' => true,
]);
$pressRelease = PressRelease::factory()->create([
'portal' => Portal::Presseecho->value,
'legacy_portal' => Portal::Presseecho->value,
'legacy_id' => 456,
]);
$this->artisan('legacy:migrate-media', [
'--portal' => Portal::Presseecho->value,
'--type' => 'press-release-images',
'--base-path' => $basePath,
])
->assertSuccessful()
->expectsOutputToContain('presseecho/press-release-images: Legacy 1, migriert 1, Thumbnail-Fallback 0, DB-Updates 1');
$image = PressReleaseImage::query()
->where('legacy_portal', Portal::Presseecho->value)
->where('legacy_id', 987)
->firstOrFail();
expect($image->press_release_id)->toBe($pressRelease->id)
->and($image->path)->toBe("press-releases/{$pressRelease->id}/images/photo-a.jpg")
->and($image->title)->toBe('Pressefoto')
->and($image->is_preview)->toBeTrue()
->and($image->variants)->toHaveKeys(['thumb', 'medium', 'large'])
->and($image->width)->toBe(1600)
->and($image->height)->toBe(1200);
Storage::disk('public')->assertExists($image->path);
foreach ($image->variants as $variantPath) {
Storage::disk('public')->assertExists($variantPath);
}
});
test('legacy media migration reports missing source files without changing the target record', function () {
/** @var TestCase $this */
Storage::fake('public');
configureLegacyMediaConnection();
$basePath = prepareLegacyMediaFolder();
DB::connection('mysql_presseecho')->table('company')->insert([
'id' => 321,
'logo' => 'missing.png',
]);
$company = Company::factory()->create([
'portal' => Portal::Presseecho->value,
'legacy_portal' => Portal::Presseecho->value,
'legacy_id' => 321,
'logo_path' => null,
]);
$this->artisan('legacy:migrate-media', [
'--portal' => Portal::Presseecho->value,
'--type' => 'company-logos',
'--base-path' => $basePath,
])
->assertFailed()
->expectsOutputToContain('Datei fehlt: presseecho/company/missing.png');
expect($company->fresh()->logo_path)->toBeNull();
});
test('legacy media migration falls back to legacy thumbnails by image id', function () {
/** @var TestCase $this */
Storage::fake('public');
configureLegacyMediaConnection();
$basePath = prepareLegacyMediaFolder();
File::ensureDirectoryExists("{$basePath}/presseecho/thumbnails/pressreleaseimage/press_image_preview/42/01/00");
File::put("{$basePath}/presseecho/thumbnails/pressreleaseimage/press_image_preview/42/01/00/pool-10042.png", migrateLegacyMediaFakeImageBytes(filename: 'legacy.png'));
DB::connection('mysql_presseecho')->table('press_release_image')->insert([
'id' => 10042,
'title' => 'Pool',
'description' => null,
'image' => 'missing-original.jpg',
'copyright' => null,
'press_release_id' => 654,
'is_preview_image' => false,
]);
$pressRelease = PressRelease::factory()->create([
'portal' => Portal::Presseecho->value,
'legacy_portal' => Portal::Presseecho->value,
'legacy_id' => 654,
]);
$this->artisan('legacy:migrate-media', [
'--portal' => Portal::Presseecho->value,
'--type' => 'press-release-images',
'--base-path' => $basePath,
])
->assertSuccessful()
->expectsOutputToContain('Thumbnail-Fallback 1');
$image = PressReleaseImage::query()
->where('legacy_portal', Portal::Presseecho->value)
->where('legacy_id', 10042)
->firstOrFail();
expect($image->path)->toBe("press-releases/{$pressRelease->id}/images/pool-10042.png")
->and($image->variants)->toHaveKeys(['thumb', 'medium', 'large']);
Storage::disk('public')->assertExists($image->path);
});
function configureLegacyMediaConnection(): void
{
Config::set('database.connections.mysql_presseecho', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
'foreign_key_constraints' => false,
]);
DB::purge('mysql_presseecho');
Schema::connection('mysql_presseecho')->dropIfExists('company');
Schema::connection('mysql_presseecho')->dropIfExists('press_release_image');
Schema::connection('mysql_presseecho')->create('company', function (Blueprint $table): void {
$table->integer('id')->primary();
$table->string('logo')->nullable();
});
Schema::connection('mysql_presseecho')->create('press_release_image', function (Blueprint $table): void {
$table->integer('id')->primary();
$table->string('title')->nullable();
$table->text('description')->nullable();
$table->string('image')->nullable();
$table->string('copyright')->nullable();
$table->integer('press_release_id');
$table->boolean('is_preview_image')->default(false);
});
}
function prepareLegacyMediaFolder(): string
{
$basePath = storage_path('framework/testing/legacy-media-'.Str::random(8));
File::deleteDirectory($basePath);
File::ensureDirectoryExists($basePath);
return $basePath;
}
function migrateLegacyMediaFakeImageBytes(int $width = 800, int $height = 600, string $filename = 'legacy.jpg'): string
{
return UploadedFile::fake()->image($filename, $width, $height)->get();
}

View file

@ -23,7 +23,8 @@ test('businessportal24 homepage renders the editorial shell', function () {
->assertSeeText('Termine & Events')
->assertSeeText('Branchen-Index')
->assertSeeText('Heute im Fokus')
->assertSee('businessportal<span class="text-brand">24</span>', false)
->assertSee('businessportal', false)
->assertSee('>24<', false)
->assertDontSeeText('führende Plattform')
->assertDontSeeText('maximale Reichweite')
->assertDontSeeText('Exklusiv-Interview');

View file

@ -2,12 +2,12 @@
use Tests\TestCase;
test('presseportale hub homepage renders the publisher landing shell', function () {
test('pressekonto hub homepage renders the publisher landing shell', function () {
/** @var TestCase $this */
$this->get('https://presseportale.test/')
$this->get('https://pressekonto.test/')
->assertSuccessful()
->assertSee('presse', false)
->assertSee('portale', false)
->assertSee('konto', false)
->assertSeeText('Publisher-Hub')
->assertSeeText('Ein Konto.')
->assertSeeText('Was Sie hier können')
@ -18,57 +18,57 @@ test('presseportale hub homepage renders the publisher landing shell', function
->assertSeeText('Standard')
->assertSeeText('Pro')
->assertSeeText('Enterprise')
->assertSeeText('Hinter presseportale')
->assertSeeText('Hinter pressekonto')
->assertSeeText('Plattform im Überblick')
->assertSeeText('Häufige Fragen')
->assertSeeText('Loslegen')
->assertSeeText('Alle Systeme betriebsbereit');
});
test('presseportale hub homepage uses the brand-mark splitting without TLDs', function () {
test('pressekonto hub homepage uses the brand-mark splitting without TLDs', function () {
/** @var TestCase $this */
$response = $this->get('https://presseportale.test/');
$response = $this->get('https://pressekonto.test/');
$content = $response->getContent();
$response->assertSuccessful();
// Markennamen erscheinen ausschliesslich gesplittet (presse|echo, businessportal|24, presse|portale).
// Markennamen erscheinen ausschliesslich gesplittet (presse|echo, businessportal|24, presse|konto).
expect($content)
->not->toContain('presseecho.de')
->not->toContain('businessportal24.com')
->toContain('businessportal')
->toContain('>24<')
->toContain('>echo<')
->toContain('>portale<');
->toContain('>konto<');
// presseportale darf ausschliesslich in der Vertriebs-Mailto-Adresse als Domain auftauchen,
// pressekonto darf ausschliesslich in der Vertriebs-Mailto-Adresse als Domain auftauchen,
// niemals als Marken-Schreibweise im Lese-Text.
expect(substr_count($content, 'presseportale.com'))
->toBe(substr_count($content, 'vertrieb@presseportale.com'));
expect(substr_count($content, 'pressekonto.de'))
->toBe(substr_count($content, 'vertrieb@pressekonto.de'));
});
test('presseportale hub homepage loads the hub theme assets, not portal admin', function () {
test('pressekonto hub homepage loads the hub theme assets, not portal admin', function () {
/** @var TestCase $this */
$response = $this->get('https://presseportale.test/');
$response = $this->get('https://pressekonto.test/');
$response
->assertSuccessful()
->assertSee('theme-presseportale', false)
->assertSee('theme-pressekonto', false)
->assertDontSee('theme-businessportal24', false)
->assertDontSee('theme-presseecho', false);
});
test('presseportale hub homepage hides the brand-context banner without a from parameter', function () {
test('pressekonto hub homepage hides the brand-context banner without a from parameter', function () {
/** @var TestCase $this */
$this->get('https://presseportale.test/')
$this->get('https://pressekonto.test/')
->assertSuccessful()
->assertDontSeeText('Sie kommen von');
});
test('presseportale hub homepage shows the brand-context banner when arriving from presseecho', function () {
test('pressekonto hub homepage shows the brand-context banner when arriving from presseecho', function () {
/** @var TestCase $this */
$response = $this->get('https://presseportale.test/?from=presseecho')
$response = $this->get('https://pressekonto.test/?from=presseecho')
->assertSuccessful()
->assertSeeText('Sie kommen von')
->assertSeeText('Zurück zu');
@ -77,9 +77,9 @@ test('presseportale hub homepage shows the brand-context banner when arriving fr
$response->assertDontSee('presseecho.de', false);
});
test('presseportale hub homepage shows the brand-context banner when arriving from businessportal24', function () {
test('pressekonto hub homepage shows the brand-context banner when arriving from businessportal24', function () {
/** @var TestCase $this */
$response = $this->get('https://presseportale.test/?from=businessportal24')
$response = $this->get('https://pressekonto.test/?from=businessportal24')
->assertSuccessful()
->assertSeeText('Sie kommen von')
->assertSeeText('Zurück zu');