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,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Enums\AiTextDeclaration;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V1\StorePressReleaseRequest;
|
||||
|
|
@ -19,6 +20,7 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PressReleaseController extends Controller
|
||||
{
|
||||
|
|
@ -56,6 +58,15 @@ class PressReleaseController extends Controller
|
|||
// `review` erfolgt ausschließlich über die explizite submit-Route,
|
||||
// damit Blacklist-/Quota-/Log-Prüfung garantiert durchlaufen werden.
|
||||
'status' => PressReleaseStatus::Draft->value,
|
||||
// Legacy-/service-Clients kennen die KI-Kennzeichnung (Art. 50
|
||||
// KI-VO) nicht — Entscheidung 10.07.: fehlende Angabe gilt als
|
||||
// „ohne generative KI erstellt"; das API-Kunden-Mailing (R6.4)
|
||||
// weist darauf hin, dass die weitere Nutzung diese Erklärung
|
||||
// darstellt. v1-Clients deklarieren explizit beim Submit.
|
||||
...($this->isLegacyServiceRequest($request) ? [
|
||||
'ai_text_declaration' => AiTextDeclaration::None->value,
|
||||
'ai_declaration_confirmed_at' => now(),
|
||||
] : []),
|
||||
]);
|
||||
|
||||
return PressReleaseResource::make(
|
||||
|
|
@ -133,6 +144,16 @@ class PressReleaseController extends Controller
|
|||
public function submit(Request $request, int $pressRelease, PressReleaseService $service): PressReleaseResource|JsonResponse
|
||||
{
|
||||
abort_unless($request->user()->tokenCan('press-releases:write'), 403);
|
||||
|
||||
// KI-Kennzeichnungspflicht (Art. 50 KI-VO) auch via API: der
|
||||
// Submit-Call trägt den deklarierten KI-Grad des Textes und gilt
|
||||
// dokumentiert als verbindliche Bestätigung der Deklaration sowie
|
||||
// Kenntnisnahme der dauerhaften Archivierung (AGB § 13) —
|
||||
// Entscheidung 10.07.2026.
|
||||
$validated = $request->validate([
|
||||
'ai_text_declaration' => ['required', Rule::enum(AiTextDeclaration::class)],
|
||||
]);
|
||||
|
||||
$pressRelease = $this->findOwnedPressRelease($pressRelease, $request);
|
||||
abort_unless($pressRelease !== null, 403);
|
||||
|
||||
|
|
@ -142,6 +163,11 @@ class PressReleaseController extends Controller
|
|||
], 409);
|
||||
}
|
||||
|
||||
$pressRelease->update([
|
||||
'ai_text_declaration' => $validated['ai_text_declaration'],
|
||||
'ai_declaration_confirmed_at' => now(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$service->submitForReview($pressRelease);
|
||||
} catch (BookingRequiredException $exception) {
|
||||
|
|
@ -211,6 +237,16 @@ class PressReleaseController extends Controller
|
|||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kam der Call über die Legacy-Aliasse (/service/…, routes/legacy-api.php)?
|
||||
* Dort gelten Kompatibilitäts-Defaults für Felder, die die Altportale
|
||||
* nicht kannten (KI-Kennzeichnung).
|
||||
*/
|
||||
private function isLegacyServiceRequest(Request $request): bool
|
||||
{
|
||||
return str_starts_with((string) $request->route()?->getName(), 'legacy-service.');
|
||||
}
|
||||
|
||||
private function uniqueSlug(string $baseSlug, string $portal, string $language, ?int $excludeId = null): string
|
||||
{
|
||||
$slug = $baseSlug !== '' ? $baseSlug : Str::random(8);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Enums\AiImageDeclaration;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V1\StorePressReleaseImageRequest;
|
||||
|
|
@ -63,6 +64,10 @@ class PressReleaseImageController extends Controller
|
|||
'disk' => 'public',
|
||||
'path' => $stored['path'],
|
||||
'variants' => $stored['variants'],
|
||||
// v1 verlangt die KI-Kennzeichnung je Bild (FormRequest);
|
||||
// Legacy-/service-Calls ohne Feld gelten als Originalfoto
|
||||
// (Kompatibilitäts-Default, Entscheidung 10.07.2026).
|
||||
'ai_declaration' => $validated['ai_declaration'] ?? AiImageDeclaration::Original->value,
|
||||
'title' => $validated['title'] ?? null,
|
||||
'description' => $validated['description'] ?? null,
|
||||
'copyright' => $validated['copyright'] ?? null,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace App\Http\Requests\Api\V1;
|
||||
|
||||
use App\Enums\AiImageDeclaration;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StorePressReleaseImageRequest extends FormRequest
|
||||
{
|
||||
|
|
@ -18,12 +20,23 @@ class StorePressReleaseImageRequest extends FormRequest
|
|||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* KI-Kennzeichnung je Bild (Art. 50 KI-VO) ist im v1-Vertrag Pflicht;
|
||||
* Legacy-/service-Clients kennen das Feld nicht — dort greift der
|
||||
* Kompatibilitäts-Default „original" (Entscheidung 10.07.2026).
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$isLegacyServiceCall = str_starts_with((string) $this->route()?->getName(), 'legacy-service.');
|
||||
|
||||
return [
|
||||
'image' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:8192'],
|
||||
'ai_declaration' => [
|
||||
Rule::requiredIf(! $isLegacyServiceCall),
|
||||
'nullable',
|
||||
Rule::enum(AiImageDeclaration::class),
|
||||
],
|
||||
'title' => ['nullable', 'string', 'max:120'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'copyright' => ['nullable', 'string', 'max:255'],
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ R2.2+R2.3 erledigt: Hub-Landing komplett ueberarbeitet (datengetriebenes Tarif-G
|
|||
**09.07. abends — R5.3 Monitoring + Buchungsdaten + kompletter WIP in 7 Commits committet (Suite: 1149 gruen):** (a) **R5.3 Monitoring** umgesetzt (Details in R5.3): `OperationalAlert` (immer `Log::critical` + Best-Effort-Mail an `MONITORING_ALERT_EMAIL`), alle 6 Scheduler-Tasks mit `->onFailure()`, Queue-`failing`-Listener, `LOG_STACK=daily` als Prod-Soll. (b) **Buchungs-/Verlaengerungsdatum** des Abos auf der Buchungsseite (`SubscriptionPeriodResolver` holt `start_date` + `current_period_end` gecacht aus Stripe, da Cashier die Periode nicht lokal spiegelt). (c) Den kompletten bis dahin uncommitteten Arbeitsbaum in **7 fokussierte Commits** zerlegt (Branch jetzt **~187 vor `main`**): OpenAPI-Route auf `public/api/v1.yml` (500-Fix) · Depublish-Credit 35 · Buchungs-/Verlaengerungsdatum · R5.3 Monitoring · Release-Doku · Backend-WIP (Kontakt-Queue, Page-Cache, Validierungs-Feedback, Company-Portal-Restriction, i18n-Auslagerung) · Web-Frontend-Umbau (separat, User-Bereich; Kontakt-Modal durch `contact-launcher` ersetzt). **Bewusst uncommittet belassen:** `sidebar.blade.php` (User-Regel, aus allen Commits ausgeschlossen), der geloeschte `docs/`-Mount + `config/pressekonto.code-workspace` (Mount nicht mitgekommen — die einzige code-relevante Datei `docs/api/v1.yml` liegt jetzt als `public/api/v1.yml` vor), zwei Dev-Screenshots in `dev/frontend/booking/`.
|
||||
**10.07. — PM-Tracking (User-Einschub, `docs/weiteres/Pm Tracken.md`):** Ereignis-Log `press_release_events` (type view/share, channel, ohne Personenbezug) + Model/Enums/Factory. Aufrufe: Legacy-`hits`-Zaehler bleibt die angezeigte Wahrheit (bewusst einfacher Page-Request-Zaehler, User-Vorgabe), zusaetzlich View-Event je Detailaufruf; oeffentliche Detailseite zeigt Aufrufe wieder in der Meta-Zeile (wie im Altsystem, beide Hero-Varianten, nicht in der Vorschau). Shares: `POST …/press-release/{slug}/share` (throttle 30/min, nur archiv-sichtbare PMs) loggt LinkedIn/X/E-Mail/Link-kopiert-Klicks der Teilen-Buttons (fetch keepalive + CSRF-Meta); Kundenpanel-PM-Seite zeigt neue Kachel „Geteilt" mit Gesamtzahl + Kanal-Aufschluesselung. Tests: `Web/PressReleaseTrackingTest` (7). Hinweis: Branch war zu Sessionbeginn bereits vollstaendig gepusht; die „drei user_filter_presets-Migrations" (R6.5 optional) existieren nicht mehr — es gibt nur noch eine konsolidierte, Punkt gegenstandslos.
|
||||
|
||||
**10.07. nachmittags — R2.5 + QA-Nachpruefung + R4.3 + KI-Queue-Fix + API-KI-Deklaration (Suite: 1170 gruen):** R2.5 erledigt (legal-request-Texte aus freigegebenem Meldeverfahren, s. R2.5) · QA-Checkliste B–G vertieft (s. R6.1) · R4.3 erledigt (Karten-Helper am Model, s. R4.3). **Befund KI-Pruefung:** Auto-Routing (Rot → Ablehnung, Gelb/Gruen → Auto-Publish) war laengst gebaut, lief lokal aber nie — `composer run dev` startete den Worker ohne `--queue=default,classification`; gefixt, Backlog abgearbeitet (Pipeline live verifiziert; Prod-Runbook war korrekt). **Befund + Fix API (Entscheidung User 10.07.):** v1-API hatte keine KI-Deklarationsfelder → Submit lief seit 09.07. immer auf 422 (haette alle 406 Legacy-API-Kunden blockiert; Suite uebersah es, weil die Factory deklariert). Jetzt: `ai_text_declaration` Pflicht im v1-Submit (Call gilt dokumentiert als verbindliche Bestaetigung + Archiv-Kenntnisnahme AGB § 13), `ai_declaration` Pflicht je Bild-Upload; Legacy-/service-Pfade defaulten auf none/original (Mailing R6.4 weist darauf hin, dass die weitere Nutzung diese Erklaerung darstellt); OpenAPI + /api-Seite aktualisiert. Tests: Submit-/Image-/Legacy-API-Tests erweitert. **Beobachtung fuer QA/Launch:** Test-Kauderwelsch („asdas dsada…") wurde Gruen klassifiziert und auto-publiziert — vor Go-Live Prompt/Schwelle pruefen bzw. `CLASSIFICATION_GREEN_DELAY` als Sicherheitsfenster setzen [USER-ENTSCHEIDUNG].
|
||||
|
||||
**Wiedereinstieg / naechste Schritte:**
|
||||
1. ~~Klaerungsliste Rechtstexte~~ **erledigt 06.07.**; ~~R3.1-Restbacklog~~ **erledigt 06.07.** (siehe oben).
|
||||
2. **[EXTERN]:** Postfach `info@pressekonto.com` anlegen (Haupt-Rechts-Mail, 06.07.); CONTACT_EMAIL_*-Support-Postfaecher je Brand; REPORT_EMAIL_* optional · R1.2 Stripe-Live · R1.4 Preisfreigabe · R5.4 Favicons · anwaltliche Freigabe AGB v0.6/Datenschutz v0.3. (~~Depublikations-Preis~~ entschieden: 35 EUR netto.)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ Staging-Rehearsal (R6.3) mit echtem Datenbestand wiederholen.
|
|||
- [ ] **`POST /v1/press-releases` mit Sanctum-Token** — Automatisiert: `Api/V1/PressReleaseApiTest`, `PressReleaseSubmitApiTest`, `PressReleaseImageApiTest`, `CustomerDataApiTest`. Manuell: Token im Kundenkonto erzeugen (setzt aktive Buchung voraus), curl-Workflow aus der /api-Doku durchspielen.
|
||||
- [ ] **Legacy-API-Übernahme** (geändert 06.07., User-Entscheidung): Importierte Alt-Schlüssel bleiben gültig — als Bearer, per `X-ApiKey`/`api_key` und auf den `/service/…`-Pfaden; nur unbekannte Keys bekommen 410 mit Migrationshinweis. Automatisiert: `Api/LegacyApiCompatibilityTest`, `ApiAccessSecurityAndLoggingTest`. Manuell nach Staging-Import: `legacy:import-api-keys --dry-run` prüfen (lokal: 406 Keys), dann echter Lauf; einen echten Alt-Key gegen `/service/pressreleases` testen.
|
||||
- [ ] **Rate-Limit 60/min** — Automatisiert: abgedeckt. Manuell: entfällt.
|
||||
- [ ] **KI-Deklaration auch via API erzwungen** (neu 09.07.) — Submit ohne KI-Deklaration liefert einen Validierungsfehler (zentral in `PressReleaseService`, `AiDeclarationRequiredException` im API-Controller gemappt). Automatisiert: `PressReleaseAiDeclarationTest`. Manuell: entfällt; beim API-Kunden-Mailing (R6.4) auf das neue Pflichtfeld hinweisen.
|
||||
- [ ] **KI-Deklaration via API** (Entscheidung 10.07.) — v1-Submit verlangt `ai_text_declaration` (Call = verbindliche Bestätigung + Archiv-Kenntnisnahme AGB § 13), Bild-Upload verlangt `ai_declaration`; Legacy-/service-Pfade defaulten auf none/original. Automatisiert: `PressReleaseSubmitApiTest`, `PressReleaseImageApiTest`, `LegacyApiCompatibilityTest`. Manuell: entfällt; **R6.4-Mailing muss Pflichtfelder + Legacy-Default erwähnen.**
|
||||
|
||||
## F. Frontend Spokes + Hub
|
||||
|
||||
|
|
|
|||
|
|
@ -300,6 +300,7 @@ return [
|
|||
'quickstart_step_1' => '# 1. Entwurf anlegen',
|
||||
'quickstart_step_2' => '# 2. Bild hochladen (multipart)',
|
||||
'quickstart_step_3' => '# 3. Zur Inhaltsprüfung einreichen',
|
||||
'ai_declaration_note' => 'Pflicht seit Juli 2026: Der Submit-Aufruf erklärt verbindlich den KI-Grad des Textes (ai_text_declaration: none, assisted, mostly_ai, fully_ai — Transparenzpflicht nach Art. 50 KI-VO) und schließt die Kenntnisnahme der dauerhaften Archivierung ein (AGB § 13). Jedes Bild trägt seine eigene Kennzeichnung (ai_declaration: original, ai_generated, ai_edited).',
|
||||
'openapi_note' => 'Maschinenlesbare Referenz:',
|
||||
'openapi_link' => 'OpenAPI-Spezifikation (YAML)',
|
||||
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ return [
|
|||
'quickstart_step_1' => '# 1. Create a draft',
|
||||
'quickstart_step_2' => '# 2. Upload an image (multipart)',
|
||||
'quickstart_step_3' => '# 3. Submit for content review',
|
||||
'ai_declaration_note' => 'Required since July 2026: the submit call bindingly declares the AI grade of the text (ai_text_declaration: none, assisted, mostly_ai, fully_ai — transparency obligation under Art. 50 EU AI Act) and includes acknowledgement of permanent archiving (Terms § 13). Each image carries its own label (ai_declaration: original, ai_generated, ai_edited).',
|
||||
'openapi_note' => 'Machine-readable reference:',
|
||||
'openapi_link' => 'OpenAPI specification (YAML)',
|
||||
|
||||
|
|
|
|||
|
|
@ -125,16 +125,39 @@ paths:
|
|||
description: |
|
||||
Der einzige API-Weg Richtung Veröffentlichung. Durchläuft denselben
|
||||
Funnel wie das Web-Formular (Richtlinien-Filter, Kontingent, Status-Log).
|
||||
|
||||
Der Aufruf erklärt verbindlich den deklarierten KI-Grad des Textes
|
||||
(`ai_text_declaration`, Transparenzpflicht nach Art. 50 KI-VO) und
|
||||
schließt die Kenntnisnahme der dauerhaften Archivierung ein
|
||||
(AGB § 13: Depublikation nur als kostenpflichtiger Service,
|
||||
gesetzliche Ansprüche unberührt).
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [ai_text_declaration]
|
||||
properties:
|
||||
ai_text_declaration:
|
||||
type: string
|
||||
enum: [none, assisted, mostly_ai, fully_ai]
|
||||
description: >
|
||||
Deklarierter KI-Grad des Textes: ohne generative KI (none),
|
||||
KI als Formulierungshilfe mit redaktioneller Überarbeitung
|
||||
(assisted), überwiegend KI-erstellt und geprüft (mostly_ai),
|
||||
vollständig KI-generiert (fully_ai). Ab mostly_ai trägt die
|
||||
Veröffentlichung ein öffentliches Transparenz-Label.
|
||||
responses:
|
||||
"200": { description: "Eingereicht (Status review)" }
|
||||
"402": { description: "Aktive Buchung erforderlich (Tarif oder Einzel-PM)" }
|
||||
"409": { description: "Nur draft/rejected kann eingereicht werden" }
|
||||
"422": { description: "Kontingent erschöpft oder Richtlinienverstoß" }
|
||||
"422": { description: "KI-Deklaration fehlt/ungültig, Kontingent erschöpft oder Richtlinienverstoß" }
|
||||
/press-releases/{id}/images:
|
||||
parameters:
|
||||
- name: id
|
||||
|
|
@ -164,12 +187,18 @@ paths:
|
|||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [image]
|
||||
required: [image, ai_declaration]
|
||||
properties:
|
||||
image:
|
||||
type: string
|
||||
format: binary
|
||||
description: JPG, PNG oder WebP, max. 8 MB
|
||||
ai_declaration:
|
||||
type: string
|
||||
enum: [original, ai_generated, ai_edited]
|
||||
description: >
|
||||
KI-Kennzeichnung des Bildes (Art. 50 KI-VO): Originalfoto,
|
||||
KI-generiert oder KI-bearbeitet.
|
||||
title: { type: string, maxLength: 120 }
|
||||
description: { type: string, maxLength: 500 }
|
||||
copyright: { type: string, maxLength: 255 }
|
||||
|
|
|
|||
|
|
@ -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