API: KI-Deklaration als v1-Pflichtfeld, Legacy-Default fuer /service (Entscheidung 10.07.)
Befund: v1 hatte keine KI-Deklarationsfelder — Submit lief seit der KI-Kennzeichnungspflicht (09.07.) immer auf 422 und haette alle Legacy-API-Kunden blockiert (Suite uebersah es: Factory deklariert). - v1-Submit verlangt ai_text_declaration; der Call gilt dokumentiert als verbindliche Bestaetigung (Art. 50 KI-VO) und schliesst die Archiv-Kenntnisnahme (AGB par. 13) ein - Bild-Upload verlangt ai_declaration (original/ai_generated/ai_edited) - Legacy-/service-Aliasse defaulten auf none/original — das API-Kunden-Mailing (R6.4) weist auf die Erklaerungswirkung hin - OpenAPI v1.yml + lang-Hinweis aktualisiert; Tests erweitert (Submit/Image/LegacyCompatibility) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3843e534da
commit
92af015532
11 changed files with 213 additions and 10 deletions
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
use App\Enums\Portal;
|
||||
use App\Enums\UserPaymentOptionStatus;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\PaymentOption;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPaymentOption;
|
||||
use App\Services\Api\LegacyApiKeyImporter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
|
@ -126,3 +131,38 @@ test('retired legacy service endpoints answer with a helpful 410 instead of 404'
|
|||
|
||||
$this->getJson('/service/irgendwas')->assertStatus(410);
|
||||
});
|
||||
|
||||
test('legacy service calls default the ai declaration instead of breaking old clients', function () {
|
||||
/** @var TestCase $this */
|
||||
Storage::fake('public');
|
||||
|
||||
$user = eligibleLegacyApiUser();
|
||||
app(LegacyApiKeyImporter::class)->import('businessportal24', [
|
||||
['legacy_id' => 4711, 'api_key' => 'legacy-key-abc123'],
|
||||
]);
|
||||
|
||||
$company = Company::factory()->create(['portal' => Portal::Businessportal24]);
|
||||
$user->companies()->attach($company->id, ['role' => 'owner']);
|
||||
$category = Category::factory()->create(['portal' => Portal::Businessportal24]);
|
||||
|
||||
// Store ohne KI-Felder (Alt-Client): Default "ohne generative KI erstellt"
|
||||
// (Entscheidung 10.07.2026 — das API-Kunden-Mailing weist darauf hin).
|
||||
$response = $this->postJson('/service/pressreleases', [
|
||||
'company_id' => $company->id,
|
||||
'category_id' => $category->id,
|
||||
'language' => 'de',
|
||||
'title' => 'Alt-Client-Meldung',
|
||||
'text' => 'Inhalt aus einer bestehenden Integration.',
|
||||
], ['X-ApiKey' => 'legacy-key-abc123'])->assertCreated();
|
||||
|
||||
$pressRelease = PressRelease::withoutGlobalScopes()->find($response->json('data.id'));
|
||||
expect($pressRelease->ai_text_declaration?->value)->toBe('none')
|
||||
->and($pressRelease->ai_declaration_confirmed_at)->not->toBeNull();
|
||||
|
||||
// Bild-Upload ohne KI-Feld: Default Originalfoto.
|
||||
$this->postJson("/service/pressreleases/{$pressRelease->id}/images", [
|
||||
'image' => UploadedFile::fake()->image('alt-client.jpg'),
|
||||
], ['X-ApiKey' => 'legacy-key-abc123'])->assertCreated();
|
||||
|
||||
expect($pressRelease->images()->first()->ai_declaration?->value)->toBe('original');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ test('api user can upload list and delete own press release images', function ()
|
|||
|
||||
$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,
|
||||
]);
|
||||
|
|
@ -101,6 +102,7 @@ test('api user cannot manage another users press release images', function () {
|
|||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
||||
'image' => UploadedFile::fake()->image('foreign.jpg'),
|
||||
'ai_declaration' => 'original',
|
||||
])->assertForbidden();
|
||||
});
|
||||
|
||||
|
|
@ -117,6 +119,32 @@ test('published press release images cannot be changed via api', function () {
|
|||
|
||||
$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('public');
|
||||
|
||||
$user = User::factory()->create();
|
||||
$pressRelease = PressRelease::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'status' => PressReleaseStatus::Draft->value,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ test('api submit route raises a draft to review without consuming quota and writ
|
|||
|
||||
Sanctum::actingAs($user, ['press-releases:write']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit")
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'none'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.status', PressReleaseStatus::Review->value);
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ test('api submit responds 402 when the booking gate is enforced', function () {
|
|||
|
||||
Sanctum::actingAs($user, ['press-releases:write']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit")
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'none'])
|
||||
->assertStatus(402);
|
||||
|
||||
expect($pressRelease->fresh()->status)->toBe(PressReleaseStatus::Draft);
|
||||
|
|
@ -99,7 +99,7 @@ test('api submit responds 422 when the monthly quota is exhausted', function ()
|
|||
|
||||
Sanctum::actingAs($user, ['press-releases:write']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit")
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'none'])
|
||||
->assertStatus(422);
|
||||
|
||||
expect($pressRelease->fresh()->status)->toBe(PressReleaseStatus::Draft);
|
||||
|
|
@ -119,7 +119,7 @@ test('api submit auto-rejects a press release containing a banned word', functio
|
|||
|
||||
Sanctum::actingAs($user, ['press-releases:write']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit")
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'none'])
|
||||
->assertStatus(422);
|
||||
|
||||
expect($pressRelease->fresh()->status)->toBe(PressReleaseStatus::Rejected);
|
||||
|
|
@ -135,7 +135,7 @@ test('api submit requires the write ability', function () {
|
|||
|
||||
Sanctum::actingAs($user, ['press-releases:read']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit")
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'none'])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ test('api submit rejects a press release already in review', function () {
|
|||
|
||||
Sanctum::actingAs($user, ['press-releases:write']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit")
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'none'])
|
||||
->assertStatus(409);
|
||||
});
|
||||
|
||||
|
|
@ -163,6 +163,54 @@ test('api user cannot submit another users press release', function () {
|
|||
|
||||
Sanctum::actingAs($otherUser, ['press-releases:write']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit")
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'none'])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('api submit requires the ai text declaration (Art. 50 KI-VO)', function () {
|
||||
/** @var TestCase $this */
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$pressRelease = PressRelease::factory()->aiUndeclared()->create([
|
||||
'user_id' => $user->id,
|
||||
'status' => PressReleaseStatus::Draft->value,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user, ['press-releases:write']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit")
|
||||
->assertUnprocessable()
|
||||
->assertInvalid(['ai_text_declaration']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'erfunden'])
|
||||
->assertUnprocessable()
|
||||
->assertInvalid(['ai_text_declaration']);
|
||||
|
||||
expect($pressRelease->fresh()->status)->toBe(PressReleaseStatus::Draft);
|
||||
});
|
||||
|
||||
test('api submit records the declared ai grade as binding confirmation', function () {
|
||||
/** @var TestCase $this */
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$pressRelease = PressRelease::factory()->aiUndeclared()->create([
|
||||
'user_id' => $user->id,
|
||||
'status' => PressReleaseStatus::Draft->value,
|
||||
'title' => 'Saubere Pressemitteilung',
|
||||
'text' => 'Vollkommen unauffälliger Inhalt.',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user, ['press-releases:write']);
|
||||
|
||||
$this->postJson("/api/v1/press-releases/{$pressRelease->id}/submit", ['ai_text_declaration' => 'assisted'])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.status', PressReleaseStatus::Review->value);
|
||||
|
||||
$fresh = $pressRelease->fresh();
|
||||
expect($fresh->ai_text_declaration?->value)->toBe('assisted')
|
||||
->and($fresh->ai_declaration_confirmed_at)->not->toBeNull()
|
||||
// Audit-Trail: der Submit protokolliert die Deklaration versioniert.
|
||||
->and($fresh->aiDeclarations()->count())->toBe(1);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue