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>
189 lines
7 KiB
PHP
189 lines
7 KiB
PHP
<?php
|
|
|
|
use App\Enums\PressReleaseStatus;
|
|
use App\Models\PressRelease;
|
|
use App\Models\PressReleaseImage;
|
|
use App\Models\User;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
|
|
test('api user can upload list and delete own press release images', function () {
|
|
/** @var TestCase $this */
|
|
Storage::fake('local');
|
|
|
|
$user = User::factory()->create();
|
|
$pressRelease = PressRelease::factory()->create([
|
|
'user_id' => $user->id,
|
|
'status' => PressReleaseStatus::Draft->value,
|
|
]);
|
|
|
|
apiActingAs($user, ['press-releases:read', 'press-release-images:write']);
|
|
|
|
$uploadResponse = $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
|
'image' => UploadedFile::fake()->image('press-release.jpg', 1200, 800)->size(5120),
|
|
'ai_declaration' => 'original',
|
|
'title' => 'Pressefoto',
|
|
'is_preview' => true,
|
|
]);
|
|
|
|
// Pressebilder werden auf das Hero-/Cover-Format normalisiert (harte
|
|
// Obergrenze 1280x580, siehe ImageService::PRESS_RELEASE_IMAGE_VARIANTS).
|
|
// Gespeichert werden daher die Cover-Maße, nicht die Originalgröße.
|
|
$uploadResponse
|
|
->assertCreated()
|
|
->assertJsonPath('data.title', 'Pressefoto')
|
|
->assertJsonPath('data.is_preview', true)
|
|
->assertJsonPath('data.width', 1280)
|
|
->assertJsonPath('data.height', 580)
|
|
->assertJsonStructure(['data' => ['variants', 'urls']]);
|
|
|
|
$image = PressReleaseImage::query()->firstOrFail();
|
|
expect($image->disk)->toBe('local');
|
|
expect(Storage::disk('local')->exists($image->path))->toBeTrue();
|
|
expect($image->variants)->toBeArray();
|
|
expect($image->variants)->toHaveKeys(['thumb', 'medium', 'large']);
|
|
|
|
foreach ($image->variants as $variantPath) {
|
|
expect(Storage::disk('local')->exists($variantPath))->toBeTrue();
|
|
}
|
|
|
|
$this->getJson("/api/v1/press-releases/{$pressRelease->id}/images")
|
|
->assertOk()
|
|
->assertJsonPath('data.0.id', $image->id)
|
|
->assertJsonPath('data.0.url', route('media.press-release-images.show', ['image' => $image->id]))
|
|
// Variant-URLs müssen über die Media-Route zeigen (Disk 'local'), nicht
|
|
// auf /storage/ — sonst 404 für private Bilder.
|
|
->assertJsonPath('data.0.urls.thumb', route('media.press-release-images.show', ['image' => $image->id, 'variant' => 'thumb']))
|
|
->assertJsonPath('data.0.urls.large', route('media.press-release-images.show', ['image' => $image->id, 'variant' => 'large']));
|
|
|
|
$listedUrls = $this->getJson("/api/v1/press-releases/{$pressRelease->id}/images")->json('data.0.urls');
|
|
foreach ($listedUrls as $variantUrl) {
|
|
expect($variantUrl)->not->toContain('/storage/');
|
|
}
|
|
|
|
$this->getJson("/api/v1/press-releases/{$pressRelease->id}")
|
|
->assertOk()
|
|
->assertJsonPath('data.images.0.id', $image->id);
|
|
|
|
$variantPaths = $image->variants ?? [];
|
|
|
|
$this->deleteJson("/api/v1/press-release-images/{$image->id}")
|
|
->assertNoContent();
|
|
|
|
expect(Storage::disk('local')->exists($image->path))->toBeFalse();
|
|
foreach ($variantPaths as $variantPath) {
|
|
expect(Storage::disk('local')->exists($variantPath))->toBeFalse();
|
|
}
|
|
expect(PressReleaseImage::withTrashed()->find($image->id)?->trashed())->toBeTrue();
|
|
});
|
|
|
|
test('image upload is limited to eight megabytes', function () {
|
|
/** @var TestCase $this */
|
|
Storage::fake('local');
|
|
|
|
$user = User::factory()->create();
|
|
$pressRelease = PressRelease::factory()->create([
|
|
'user_id' => $user->id,
|
|
'status' => PressReleaseStatus::Draft->value,
|
|
]);
|
|
|
|
apiActingAs($user, ['press-release-images:write']);
|
|
|
|
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
|
'image' => UploadedFile::fake()->image('too-large.jpg')->size(8193),
|
|
])->assertUnprocessable()
|
|
->assertInvalid(['image']);
|
|
});
|
|
|
|
test('api user cannot manage another users press release images', function () {
|
|
/** @var TestCase $this */
|
|
Storage::fake('local');
|
|
|
|
$owner = User::factory()->create();
|
|
$otherUser = User::factory()->create();
|
|
$pressRelease = PressRelease::factory()->create([
|
|
'user_id' => $owner->id,
|
|
'status' => PressReleaseStatus::Draft->value,
|
|
]);
|
|
|
|
apiActingAs($otherUser, ['press-releases:read', 'press-release-images:write']);
|
|
|
|
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
|
'image' => UploadedFile::fake()->image('foreign.jpg'),
|
|
'ai_declaration' => 'original',
|
|
])->assertForbidden();
|
|
});
|
|
|
|
test('published press release images cannot be changed via api', function () {
|
|
/** @var TestCase $this */
|
|
Storage::fake('local');
|
|
|
|
$user = User::factory()->create();
|
|
$pressRelease = PressRelease::factory()->published()->create([
|
|
'user_id' => $user->id,
|
|
]);
|
|
|
|
apiActingAs($user, ['press-release-images:write']);
|
|
|
|
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
|
'image' => UploadedFile::fake()->image('published.jpg'),
|
|
'ai_declaration' => 'original',
|
|
])->assertConflict()
|
|
->assertJsonPath('message', 'Only draft or rejected press releases may be edited.');
|
|
});
|
|
|
|
test('api image upload requires the per-image ai declaration', function () {
|
|
/** @var TestCase $this */
|
|
Storage::fake('local');
|
|
|
|
$user = User::factory()->create();
|
|
$pressRelease = PressRelease::factory()->create([
|
|
'user_id' => $user->id,
|
|
'status' => PressReleaseStatus::Draft->value,
|
|
]);
|
|
|
|
apiActingAs($user, ['press-release-images:write']);
|
|
|
|
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
|
'image' => UploadedFile::fake()->image('undeklariert.jpg'),
|
|
])->assertUnprocessable()
|
|
->assertInvalid(['ai_declaration']);
|
|
|
|
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
|
'image' => UploadedFile::fake()->image('ki-bild.jpg'),
|
|
'ai_declaration' => 'ai_generated',
|
|
])->assertCreated();
|
|
|
|
expect($pressRelease->images()->first()->ai_declaration?->value)->toBe('ai_generated');
|
|
});
|
|
|
|
test('private press release images follow publication and ownership access', function () {
|
|
/** @var TestCase $this */
|
|
Storage::fake('local');
|
|
$owner = User::factory()->create();
|
|
$draft = PressRelease::factory()->create([
|
|
'user_id' => $owner->id,
|
|
'status' => PressReleaseStatus::Draft->value,
|
|
]);
|
|
$image = $draft->images()->create([
|
|
'disk' => 'local',
|
|
'path' => "press-releases/{$draft->id}/images/private.jpg",
|
|
'variants' => [],
|
|
'mime' => 'image/jpeg',
|
|
]);
|
|
Storage::disk('local')->put($image->path, 'image data');
|
|
$url = "https://pressekonto.test/media/press-release-images/{$image->id}";
|
|
|
|
$this->get($url)->assertNotFound();
|
|
$this->actingAs($owner)->get($url)->assertSuccessful();
|
|
|
|
$draft->update([
|
|
'status' => PressReleaseStatus::Published->value,
|
|
'published_at' => now()->subMinute(),
|
|
]);
|
|
|
|
auth()->forgetGuards();
|
|
$this->get("https://presseecho.test/media/press-release-images/{$image->id}")
|
|
->assertSuccessful();
|
|
});
|