presseportale/tests/Feature/Api/ApiPortalResolutionTest.php
Kevin Adametz e77d9e7d09 feat: API-Zugang endet mit der Buchung, optionales Ablaufdatum für API-Tokens
Bisher blieb ein einmal erstellter Token gültig, auch wenn Abo bzw. Buchung
längst beendet waren. EnsureApiAccessIsBooked prüft jetzt bei jeder Anfrage
(v1 und /service/…-Altpfade), ob das Konto API-Zugang hat
(ApiAccessEligibilityService::hasApiAccess), sonst 403. Die Tokens bleiben
erhalten und funktionieren nach erneuter Buchung wieder.

Kunden können beim Erstellen optional ein Ablaufdatum setzen (bis 5 Jahre);
die Token-Liste zeigt „Gültig bis“ und einen Hinweis, wenn der Zugang
pausiert ist. sanctum:prune-expired räumt abgelaufene Tokens nach 30 Tagen
täglich ab. API-Doku (public/api/v1.yml) ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:41:24 +00:00

218 lines
8.6 KiB
PHP

<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\User;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
/**
* Regression zum API-Portal-Fix (13.08.2026): Kunden mit Keys beider
* Altportale senden an presseecho.de UND businessportal24.de — bisher
* erbten Firma und PM immer das User-/Firmen-Portal, sodass alles im
* selben Portal landete (zweite Kopie mit Slug "-2"). Das Ziel-Portal
* kommt jetzt aus explizitem Parameter > Request-Domain > Fallback.
*/
function portalResolutionApiUser(Portal $portal = Portal::Businessportal24): User
{
test()->seed(RolesAndPermissionsSeeder::class);
config()->set('billing.enforce_booking', false);
$user = User::factory()->create([
'is_active' => true,
'portal' => $portal,
]);
$user->forceFill(['registration_type' => 'apiuser'])->save();
apiActingAs($user, [
'press-releases:read',
'press-releases:write',
'companies:read',
]);
return $user;
}
function legacyPressReleasePayload(Company $company, Category $category, array $overrides = []): array
{
return [
'press_release' => array_merge([
'company_id' => $company->id,
'category_id' => $category->legacy_id ?? $category->id,
'language' => 'de',
'title' => 'Portal-Auflösung Test-PM',
'text' => 'Inhalt der Test-Pressemitteilung.',
], $overrides),
];
}
test('legacy company creation uses the request domain portal over the user portal', function () {
/** @var TestCase $this */
portalResolutionApiUser(Portal::Businessportal24);
config()->set('app.theme', 'presseecho');
$this->postJson('/service/users/me/companies', [
'company' => ['name' => 'Domain GmbH'],
])->assertCreated();
$company = Company::withoutGlobalScopes()->where('name', 'Domain GmbH')->firstOrFail();
expect($company->portal)->toBe(Portal::Presseecho);
});
test('legacy company creation falls back to the user portal without domain context', function () {
/** @var TestCase $this */
portalResolutionApiUser(Portal::Businessportal24);
config()->set('app.theme', 'portal');
$this->postJson('/service/users/me/companies', [
'company' => ['name' => 'Fallback GmbH'],
])->assertCreated();
$company = Company::withoutGlobalScopes()->where('name', 'Fallback GmbH')->firstOrFail();
expect($company->portal)->toBe(Portal::Businessportal24);
});
test('legacy company creation accepts an explicit portal parameter over the domain', function () {
/** @var TestCase $this */
portalResolutionApiUser(Portal::Businessportal24);
config()->set('app.theme', 'businessportal24');
$this->postJson('/service/users/me/companies', [
'company' => ['name' => 'Parameter GmbH', 'portal' => 'presseecho'],
])->assertCreated();
$company = Company::withoutGlobalScopes()->where('name', 'Parameter GmbH')->firstOrFail();
expect($company->portal)->toBe(Portal::Presseecho);
});
test('legacy press release creation uses the request domain portal over the company portal', function () {
/** @var TestCase $this */
Queue::fake();
$user = portalResolutionApiUser(Portal::Businessportal24);
$company = Company::factory()->businessportal24()->create(['owner_user_id' => $user->id]);
$user->companies()->attach($company->id, ['role' => 'owner']);
$category = Category::factory()->create(['portal' => Portal::Both, 'legacy_id' => 4711]);
config()->set('app.theme', 'presseecho');
$this->postJson('/service/pressreleases', legacyPressReleasePayload($company, $category))
->assertCreated();
$pressRelease = PressRelease::withoutGlobalScopes()->latest('id')->firstOrFail();
expect($pressRelease->portal)->toBe(Portal::Presseecho)
->and($pressRelease->slug)->toBe('portal-auflosung-test-pm');
});
test('legacy press release creation accepts an explicit portal parameter over the domain', function () {
/** @var TestCase $this */
Queue::fake();
$user = portalResolutionApiUser(Portal::Businessportal24);
$company = Company::factory()->businessportal24()->create(['owner_user_id' => $user->id]);
$user->companies()->attach($company->id, ['role' => 'owner']);
$category = Category::factory()->create(['portal' => Portal::Both, 'legacy_id' => 4712]);
config()->set('app.theme', 'businessportal24');
$this->postJson(
'/service/pressreleases',
legacyPressReleasePayload($company, $category, ['portal' => 'presseecho']),
)->assertCreated();
$pressRelease = PressRelease::withoutGlobalScopes()->latest('id')->firstOrFail();
expect($pressRelease->portal)->toBe(Portal::Presseecho);
});
test('legacy press release creation falls back to the company portal without domain context', function () {
/** @var TestCase $this */
Queue::fake();
$user = portalResolutionApiUser(Portal::Businessportal24);
$company = Company::factory()->businessportal24()->create(['owner_user_id' => $user->id]);
$user->companies()->attach($company->id, ['role' => 'owner']);
$category = Category::factory()->create(['portal' => Portal::Both, 'legacy_id' => 4713]);
config()->set('app.theme', 'portal');
$this->postJson('/service/pressreleases', legacyPressReleasePayload($company, $category))
->assertCreated();
$pressRelease = PressRelease::withoutGlobalScopes()->latest('id')->firstOrFail();
expect($pressRelease->portal)->toBe(Portal::Businessportal24);
});
test('v1 press release creation resolves the portal from parameter and domain', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$company = Company::factory()->businessportal24()->create();
$category = Category::factory()->withTranslations()->create(['portal' => Portal::Both]);
$user->companies()->attach($company->id, ['role' => 'owner']);
apiActingAs($user, ['press-releases:read', 'press-releases:write']);
config()->set('app.theme', 'presseecho');
$this->postJson('/api/v1/press-releases', [
'company_id' => $company->id,
'category_id' => $category->id,
'language' => 'de',
'title' => 'Domain bestimmt das Portal',
'text' => 'Inhalt',
])->assertCreated()->assertJsonPath('data.portal', 'presseecho');
$this->postJson('/api/v1/press-releases', [
'portal' => 'businessportal24',
'company_id' => $company->id,
'category_id' => $category->id,
'language' => 'de',
'title' => 'Parameter schlägt Domain',
'text' => 'Inhalt',
])->assertCreated()->assertJsonPath('data.portal', 'businessportal24');
});
test('v1 press release creation rejects the portal value both', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$company = Company::factory()->businessportal24()->create();
$category = Category::factory()->withTranslations()->create(['portal' => Portal::Both]);
$user->companies()->attach($company->id, ['role' => 'owner']);
apiActingAs($user, ['press-releases:read', 'press-releases:write']);
$this->postJson('/api/v1/press-releases', [
'portal' => 'both',
'company_id' => $company->id,
'category_id' => $category->id,
'language' => 'de',
'title' => 'Beide Portale sind nicht erlaubt',
'text' => 'Inhalt',
])->assertUnprocessable()->assertJsonValidationErrors(['portal']);
});
test('v1 update keeps the portal unless an explicit portal parameter is sent', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$company = Company::factory()->businessportal24()->create();
$user->companies()->attach($company->id, ['role' => 'owner']);
// Gemeinsame Kategorie, damit der Portalwechsel nicht an der
// Kategorieprüfung (Audit U10) scheitert.
$pressRelease = PressRelease::factory()
->forPortal(Portal::Businessportal24)
->for(Category::factory()->withTranslations()->create(['portal' => Portal::Both]))
->create(['user_id' => $user->id, 'company_id' => $company->id]);
apiActingAs($user, ['press-releases:read', 'press-releases:write']);
// Update über die "falsche" Domain verschiebt die PM nicht …
config()->set('app.theme', 'presseecho');
$this->patchJson("/api/v1/press-releases/{$pressRelease->id}", [
'title' => 'Titel bleibt im Portal',
])->assertOk()->assertJsonPath('data.portal', 'businessportal24');
// … ein expliziter Parameter schon.
$this->patchJson("/api/v1/press-releases/{$pressRelease->id}", [
'portal' => 'presseecho',
])->assertOk()->assertJsonPath('data.portal', 'presseecho');
});