seed(RolesAndPermissionsSeeder::class); config()->set('billing.enforce_booking', false); $user = User::factory()->create([ 'is_active' => true, 'portal' => Portal::Businessportal24, ]); $user->forceFill(['registration_type' => 'apiuser'])->save(); if ($immediatePublication) { $user->givePermissionTo('api:immediate-publication'); } Sanctum::actingAs($user, [ 'press-releases:read', 'press-releases:write', 'press-release-images:write', 'companies:read', ]); return $user; } test('the full connektar flow works like the old api with immediate publishing', function () { /** @var TestCase $this */ Queue::fake(); Storage::fake('local'); $user = parityApiUser(); // 1. Firma anlegen — verschachtelte Alt-Params, 201 + Location + {}. $response = $this->postJson('/service/users/me/companies', [ 'company' => [ 'name' => 'Muster Energie GmbH', 'address' => "Musterstraße 1\n33602 Bielefeld", 'country_id' => 177, 'email' => 'info@muster-energie.de', 'website' => 'https://muster-energie.de', ], ])->assertCreated(); expect($response->headers->get('Location'))->toContain('/service/users/me/companies/') ->and($response->json())->toBe([]); $company = Company::withoutGlobalScopes()->latest('id')->first(); expect($company->country_code)->toBe('DE') ->and($company->is_active)->toBeTrue() ->and($company->owner_user_id)->toBe($user->id); // 2. Pressekontakt anlegen — salutation_id 1 → mr. $this->postJson("/service/users/me/companies/{$company->id}/contacts", [ 'contact' => [ 'salutation_id' => 1, 'first_name' => 'Max', 'last_name' => 'Mustermann', 'email' => 'presse@muster-energie.de', ], ])->assertCreated(); $contact = $company->contacts()->first(); expect($contact->salutation_key)->toBe('mr'); // 3. PM anlegen — Anlegen IST Einreichen; mit Sofort-Publish-Recht // und Schalter aus geht sie sofort live (Alt-Verhalten, Log-Beleg). $category = Category::factory()->create(['portal' => Portal::Businessportal24, 'legacy_id' => 4242]); $response = $this->postJson('/service/pressreleases', [ 'press_release' => [ 'company_id' => $company->id, 'category_id' => 4242, 'language' => 'de', 'title' => 'Muster Energie eröffnet neuen Standort', 'text' => 'Vollkommen unauffälliger Inhalt aus einer Alt-Integration.', 'contacts_list' => [$contact->id], ], ])->assertCreated(); expect($response->json())->toBe([]); $pressRelease = PressRelease::withoutGlobalScopes()->latest('id')->first(); expect($pressRelease->status)->toBe(PressReleaseStatus::Published) ->and($pressRelease->published_at)->not->toBeNull() ->and($pressRelease->category_id)->toBe($category->id) ->and($pressRelease->ai_text_declaration?->value)->toBe('none') ->and($pressRelease->ai_declaration_confirmed_at)->not->toBeNull(); // 4. Bild hochladen — verschachtelt, KI-Default Originalfoto. $this->postJson("/service/pressreleases/{$pressRelease->id}/images", [ 'press_release_image' => [ 'image' => UploadedFile::fake()->image('pressefoto.jpg', 1200, 800), 'copyright' => 'Muster Energie GmbH', ], ])->assertCreated(); expect($pressRelease->images()->first()->ai_declaration?->value)->toBe('original'); // 5. GET als Veröffentlichungs-Kontrolle: flaches Alt-Objekt, Hits zählen. $this->getJson("/service/pressreleases/{$pressRelease->id}") ->assertOk() ->assertJsonStructure(['id', 'title', 'text', 'teaser_end', 'language', 'category_id', 'view_url', 'company' => ['viewUrl', 'name']]) ->assertJsonPath('company.name', 'Muster Energie GmbH'); $trackedRelease = PressRelease::withoutGlobalScopes()->findOrFail($pressRelease->id); expect($trackedRelease->hits)->toBe(1) ->and($trackedRelease->events()->where('type', 'view')->count())->toBe(1); }); test('the admin switch routes legacy submissions through the ai review instead', function () { /** @var TestCase $this */ Queue::fake(); $user = parityApiUser(); AppSetting::setValue(LegacyPressReleaseController::SETTING_AI_REVIEW, true); $company = Company::factory()->create(['portal' => Portal::Businessportal24, 'is_active' => true]); $user->companies()->attach($company->id, ['role' => 'owner']); $category = Category::factory()->create(['portal' => Portal::Businessportal24]); $this->postJson('/service/pressreleases', [ 'press_release' => [ 'company_id' => $company->id, 'category_id' => $category->id, 'title' => 'Wird erst KI-geprüft', 'text' => 'Inhalt, der durch die KI-Prüfung läuft.', ], ])->assertCreated(); $pressRelease = PressRelease::withoutGlobalScopes()->latest('id')->first(); expect($pressRelease->status)->toBe(PressReleaseStatus::Review); Queue::assertPushed(ClassifyPressRelease::class); // Polling-Kontrolle des Alt-Clients: unveröffentlicht = 404. $this->getJson("/service/pressreleases/{$pressRelease->id}")->assertNotFound(); }); test('without the immediate publication permission legacy posts stay in review', function () { /** @var TestCase $this */ Queue::fake(); $user = parityApiUser(immediatePublication: false); $company = Company::factory()->create(['portal' => Portal::Businessportal24, 'is_active' => true]); $user->companies()->attach($company->id, ['role' => 'owner']); $category = Category::factory()->create(['portal' => Portal::Businessportal24]); $this->postJson('/service/pressreleases', [ 'press_release' => [ 'company_id' => $company->id, 'category_id' => $category->id, 'title' => 'Ohne Sonderrecht', 'text' => 'Inhalt ohne Sofort-Publish-Privileg.', ], ])->assertCreated(); expect(PressRelease::withoutGlobalScopes()->latest('id')->first()->status)->toBe(PressReleaseStatus::Review); }); test('legacy api html is sanitized before persistence', function () { /** @var TestCase $this */ Queue::fake(); $user = parityApiUser(immediatePublication: false); $company = Company::factory()->create(['portal' => Portal::Businessportal24, 'is_active' => true]); $user->companies()->attach($company->id, ['role' => 'owner']); $category = Category::factory()->create(['portal' => Portal::Businessportal24]); $this->postJson('/service/pressreleases', [ 'press_release' => [ 'company_id' => $company->id, 'category_id' => $category->id, 'title' => 'Bereinigter Legacy-Inhalt', 'text' => '

Erlaubter Text

Link', ], ])->assertCreated(); $text = PressRelease::withoutGlobalScopes()->latest('id')->value('text'); expect($text) ->toContain('

Erlaubter Text

') ->not->toContain('not->toContain('javascript:'); }); test('legacy errors use the old global and field error format', function () { /** @var TestCase $this */ parityApiUser(); $this->postJson('/service/pressreleases', [ 'press_release' => ['title' => 'Ohne Pflichtfelder'], ])->assertUnprocessable() ->assertJsonStructure(['global_errors', 'field_errors']); // Fremde Firma → Alt-Wortlaut als global_error. $foreign = Company::factory()->create(['portal' => Portal::Businessportal24]); $this->postJson("/service/users/me/companies/{$foreign->id}/contacts", [ 'contact' => ['first_name' => 'Max', 'last_name' => 'Muster'], ])->assertForbidden() ->assertJsonPath('global_errors.0', 'You may only access companies which are related to your user-account'); }); test('api submissions notify the press contact instead of the api submitter', function () { /** @var TestCase $this */ Mail::fake(); Queue::fake(); $user = parityApiUser(); $company = Company::factory()->create([ 'portal' => Portal::Businessportal24, 'is_active' => true, 'email' => 'firma@muster.de', ]); $user->companies()->attach($company->id, ['role' => 'owner']); $category = Category::factory()->create(['portal' => Portal::Businessportal24]); $contact = $company->contacts()->create([ 'portal' => Portal::Businessportal24->value, 'first_name' => 'Max', 'last_name' => 'Mustermann', 'email' => 'presse@muster.de', ]); $this->postJson('/service/pressreleases', [ 'press_release' => [ 'company_id' => $company->id, 'category_id' => $category->id, 'title' => 'Mail geht an den Pressekontakt', 'text' => 'Inhalt für den Mail-Routing-Test.', 'contacts_list' => [$contact->id], ], ])->assertCreated(); Mail::assertQueued(PressReleasePublished::class, fn ($mail) => $mail->hasTo('presse@muster.de')); });