presseportale/tests/Feature/Api/V1/PressReleaseApiTest.php
Kevin Adametz ec6705ee99 tests: Kategorie-Portal in den API-Tests festlegen
Die Kategorie-Factory würfelt das Portal; seit der Portal-Prüfung im
API-Store (Audit F11) schlug 'api submitted html text is sanitized'
zufällig mit 422 fehl.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:48:30 +00:00

206 lines
8.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\User;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
test('api user can create list update and delete own press releases', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$company = Company::factory()->presseecho()->create();
$category = Category::factory()->withTranslations()->create(['portal' => Portal::Both]);
$user->companies()->attach($company->id, ['role' => 'owner']);
Sanctum::actingAs($user, ['press-releases:read', 'press-releases:write']);
$createResponse = $this->postJson('/api/v1/press-releases', [
'company_id' => $company->id,
'category_id' => $category->id,
'language' => 'de',
'title' => 'Neue API Pressemitteilung',
'text' => 'Inhalt der Pressemitteilung',
'status' => 'draft',
]);
$createResponse
->assertCreated()
->assertJsonPath('data.title', 'Neue API Pressemitteilung')
->assertJsonPath('data.portal', 'presseecho');
$pressReleaseId = PressRelease::withoutGlobalScopes()
->where('title', 'Neue API Pressemitteilung')
->value('id');
$this->getJson('/api/v1/press-releases')
->assertOk()
->assertJsonPath('data.0.id', $pressReleaseId);
$this->patchJson("/api/v1/press-releases/{$pressReleaseId}", [
'title' => 'Aktualisierte API Pressemitteilung',
])
->assertOk()
->assertJsonPath('data.title', 'Aktualisierte API Pressemitteilung');
$this->deleteJson("/api/v1/press-releases/{$pressReleaseId}")
->assertNoContent();
expect(PressRelease::withoutGlobalScopes()->withTrashed()->find($pressReleaseId)?->trashed())->toBeTrue();
});
test('api submitted html text is sanitized before persistence', function () {
/**
* Regression zu Befund M6 (Audit 14.07.2026): Über die API eingereichtes
* HTML muss – wie im Web-Formular – vor der Speicherung durch den Sanitizer.
*/
/** @var TestCase $this */
$user = User::factory()->create();
$company = Company::factory()->presseecho()->create();
$category = Category::factory()->withTranslations()->create(['portal' => Portal::Both]);
$user->companies()->attach($company->id, ['role' => 'owner']);
Sanctum::actingAs($user, ['press-releases:read', 'press-releases:write']);
$this->postJson('/api/v1/press-releases', [
'company_id' => $company->id,
'category_id' => $category->id,
'language' => 'de',
'title' => 'XSS Versuch',
'text' => '<p>Sauber</p><script>alert(document.cookie)</script><a href="javascript:alert(1)">x</a>',
'status' => 'draft',
])->assertCreated();
$stored = PressRelease::withoutGlobalScopes()->where('title', 'XSS Versuch')->firstOrFail();
expect($stored->text)
->not->toContain('<script')
->not->toContain('javascript:')
->toContain('<p>Sauber</p>');
});
test('api write ability is required to create press releases', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$company = Company::factory()->presseecho()->create();
$category = Category::factory()->withTranslations()->create(['portal' => Portal::Both]);
$user->companies()->attach($company->id, ['role' => 'owner']);
Sanctum::actingAs($user, ['press-releases:read']);
$this->postJson('/api/v1/press-releases', [
'company_id' => $company->id,
'category_id' => $category->id,
'language' => 'de',
'title' => 'Nicht erlaubt',
'text' => 'Inhalt',
])->assertForbidden();
});
test('api user cannot access another users press release', function () {
/** @var TestCase $this */
$owner = User::factory()->create();
$otherUser = User::factory()->create();
$pressRelease = PressRelease::factory()->create(['user_id' => $owner->id]);
Sanctum::actingAs($otherUser, ['press-releases:read']);
$this->getJson("/api/v1/press-releases/{$pressRelease->id}")
->assertForbidden();
});
test('a soft deleted press release disappears from the api (Audit F04)', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$pressRelease = PressRelease::factory()->create(['user_id' => $user->id, 'title' => 'Bald geloescht']);
Sanctum::actingAs($user, ['press-releases:read', 'press-releases:write']);
$this->deleteJson("/api/v1/press-releases/{$pressRelease->id}")->assertNoContent();
$this->getJson('/api/v1/press-releases')->assertOk()->assertJsonMissing(['title' => 'Bald geloescht']);
$this->getJson("/api/v1/press-releases/{$pressRelease->id}")->assertForbidden();
$this->patchJson("/api/v1/press-releases/{$pressRelease->id}", ['title' => 'Wiederbelebt'])->assertForbidden();
expect(PressRelease::withoutGlobalScopes()->withTrashed()->find($pressRelease->id)?->title)->toBe('Bald geloescht');
});
test('a soft deleted company is neither listed nor usable for new press releases (Audit F04)', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$company = Company::factory()->presseecho()->create(['name' => 'Geloeschte Firma']);
$category = Category::factory()->withTranslations()->create(['portal' => Portal::Both]);
$user->companies()->attach($company->id, ['role' => 'owner']);
$company->delete();
Sanctum::actingAs($user, ['press-releases:read', 'press-releases:write', 'companies:read']);
$this->getJson('/api/v1/companies')->assertOk()->assertJsonMissing(['name' => 'Geloeschte Firma']);
$this->getJson("/api/v1/companies/{$company->id}")->assertForbidden();
$this->postJson('/api/v1/press-releases', [
'company_id' => $company->id,
'category_id' => $category->id,
'language' => 'de',
'title' => 'Fuer eine geloeschte Firma',
'text' => 'Inhalt',
])->assertForbidden();
});
test('api list parameters are validated instead of passed through (Audit F11)', function (array $query) {
/** @var TestCase $this */
$user = User::factory()->create();
PressRelease::factory()->count(3)->create(['user_id' => $user->id]);
Sanctum::actingAs($user, ['press-releases:read', 'companies:read']);
$this->getJson('/api/v1/press-releases?'.http_build_query($query))->assertUnprocessable();
$this->getJson('/api/v1/companies?'.http_build_query(array_intersect_key($query, ['per_page' => true])) ?: '/api/v1/companies?per_page=-1')
->assertUnprocessable();
})->with([
'negative per_page' => [['per_page' => -1]],
'zero per_page' => [['per_page' => 0]],
'per_page above the cap' => [['per_page' => 101]],
'textual per_page' => [['per_page' => 'alle']],
'status as array' => [['status' => ['draft'], 'per_page' => -1]],
'unknown status' => [['status' => 'gibt-es-nicht', 'per_page' => -1]],
]);
test('api list accepts the documented parameter range and always limits the query (Audit F11)', function () {
/** @var TestCase $this */
$user = User::factory()->create();
PressRelease::factory()->count(3)->create(['user_id' => $user->id]);
Sanctum::actingAs($user, ['press-releases:read']);
$this->getJson('/api/v1/press-releases?per_page=1')->assertOk()->assertJsonCount(1, 'data')->assertJsonPath('meta.per_page', 1);
$this->getJson('/api/v1/press-releases?per_page=100&status=draft')->assertOk()->assertJsonCount(3, 'data');
});
test('api press release input rejects unsupported languages and inactive or foreign categories (Audit F11)', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$company = Company::factory()->presseecho()->create();
$user->companies()->attach($company->id, ['role' => 'owner']);
$category = Category::factory()->withTranslations()->create(['portal' => Portal::Presseecho, 'is_active' => true]);
$inactive = Category::factory()->withTranslations()->create(['portal' => Portal::Presseecho, 'is_active' => false]);
$foreign = Category::factory()->withTranslations()->create(['portal' => Portal::Businessportal24, 'is_active' => true]);
Sanctum::actingAs($user, ['press-releases:read', 'press-releases:write']);
$payload = fn (array $overrides): array => [
'company_id' => $company->id,
'category_id' => $category->id,
'language' => 'de',
'title' => 'Vertragspruefung',
'text' => 'Inhalt',
...$overrides,
];
$this->postJson('/api/v1/press-releases', $payload(['language' => 'zz']))->assertUnprocessable()->assertJsonValidationErrors(['language']);
$this->postJson('/api/v1/press-releases', $payload(['category_id' => $inactive->id]))->assertUnprocessable()->assertJsonValidationErrors(['category_id']);
$this->postJson('/api/v1/press-releases', $payload(['category_id' => $foreign->id]))->assertUnprocessable()->assertJsonValidationErrors(['category_id']);
$this->postJson('/api/v1/press-releases', $payload(['language' => 'en']))->assertCreated();
});