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>
218 lines
8.6 KiB
PHP
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');
|
|
});
|