From f3a95c854de80a4a1e86509d891811c3fc31dcf0 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Fri, 3 Jul 2026 15:36:19 +0000 Subject: [PATCH] API-Doku: echte Referenz statt Fantasie-Features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /de/api komplett neu: reale Endpunkte, Felder, Token-Abilities, Rate-Limit und Statuscodes der API v1 (angelehnt an die Legacy-Doku von businessportal24.com/service/documentation.html); OAuth/SDKs/ Webhooks/Integrations-Behauptungen und api.example.com entfernt - docs/api/v1.yml (OpenAPI 3.1) erstellt — die docs.api.v1-Route lief bisher auf eine nie vorhandene Datei (500er aus Hub-Footer/FAQ) - Token-Vergabe korrekt beschrieben: Selfservice im Konto mit Buchungs-Gate statt 'auf Anfrage'; X-ApiKey-Migrationshinweis - ApiDocumentationTest entskippt + Seiten-Test ergaenzt Co-Authored-By: Claude Fable 5 --- docs/api/v1.yml | 310 +++++++++++++++++++++++++ resources/views/web/api.blade.php | 263 ++++++++++++++++----- tests/Feature/ApiDocumentationTest.php | 16 +- 3 files changed, 534 insertions(+), 55 deletions(-) create mode 100644 docs/api/v1.yml diff --git a/docs/api/v1.yml b/docs/api/v1.yml new file mode 100644 index 00000000..9ab3d7c6 --- /dev/null +++ b/docs/api/v1.yml @@ -0,0 +1,310 @@ +openapi: 3.1.0 +info: + title: pressekonto Publisher API + version: "1.0" + description: | + REST-API des Publisher-Hubs (pressekonto) für presseecho.de und + businessportal24.com. Pressemitteilungen werden als Entwurf angelegt, + optional mit Bildern versehen und über die Submit-Route zur + Inhaltsprüfung eingereicht — eine direkte Veröffentlichung über die + API ist bewusst nicht möglich. + + Authentifizierung über einen persönlichen API-Token (Bearer). Jeder + Token trägt Berechtigungen (Abilities), die den Zugriff auf die + Endpunkte steuern. Tokens werden im Publisher-Konto unter „API-Tokens" + selbst erstellt und widerrufen (Voraussetzung: aktive Buchung). + Rate-Limit: 60 Anfragen pro Minute je Token (Header X-RateLimit-Limit / + X-RateLimit-Remaining, bei 429 zusätzlich Retry-After). + + Migration: Der X-ApiKey-Header der Altportale wird nicht mehr + unterstützt — legacy API keys are no longer supported. +servers: + - url: https://pressekonto.de/api/v1 +security: + - bearerToken: [] +tags: + - name: press-releases + description: "Eigene Pressemitteilungen (Ability: press-releases:read bzw. press-releases:write)" + - name: images + description: "Bilder einer Pressemitteilung (Ability: press-release-images:write zum Schreiben)" + - name: companies + description: "Eigene Firmenprofile/Newsrooms (Ability: companies:read)" + - name: categories + description: "Aktive Rubriken (Ability: press-releases:read)" + - name: newsletter + description: "Newsletter-Anmeldungen (Ability: newsletter:subscribe)" +paths: + /press-releases: + get: + tags: [press-releases] + summary: Eigene Pressemitteilungen auflisten + parameters: + - name: status + in: query + description: Nach Status filtern (draft, review, published, rejected, archived) + schema: { type: string } + - name: per_page + in: query + description: Seitengröße, Standard 25, max. 100 + schema: { type: integer, maximum: 100 } + - name: page + in: query + schema: { type: integer } + responses: + "200": + description: Paginierte Liste + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: { $ref: "#/components/schemas/PressRelease" } + post: + tags: [press-releases] + summary: Pressemitteilung anlegen (immer als Entwurf) + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/PressReleaseInput" } + responses: + "201": + description: Entwurf angelegt + content: + application/json: + schema: + type: object + properties: + data: { $ref: "#/components/schemas/PressRelease" } + "403": { description: "Ability fehlt oder Firma gehört nicht zum Konto" } + "422": { description: "Validierungsfehler" } + /press-releases/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer } + get: + tags: [press-releases] + summary: Pressemitteilung abrufen + responses: + "200": + description: Mitteilung inkl. Firma, Rubrik und Bildern + content: + application/json: + schema: + type: object + properties: + data: { $ref: "#/components/schemas/PressRelease" } + "403": { description: "Nicht gefunden oder fremdes Konto" } + patch: + tags: [press-releases] + summary: Pressemitteilung ändern (nur draft/rejected) + requestBody: + content: + application/json: + schema: { $ref: "#/components/schemas/PressReleaseInput" } + responses: + "200": { description: "Aktualisierte Mitteilung" } + "409": { description: "Nur Entwürfe und abgelehnte Mitteilungen sind änderbar" } + delete: + tags: [press-releases] + summary: Pressemitteilung löschen (nicht bei published) + responses: + "204": { description: "Gelöscht" } + "409": { description: "Veröffentlichte Mitteilungen können nicht per API gelöscht werden" } + /press-releases/{id}/submit: + post: + tags: [press-releases] + summary: Zur Inhaltsprüfung einreichen + description: | + Der einzige API-Weg Richtung Veröffentlichung. Durchläuft denselben + Funnel wie das Web-Formular (Richtlinien-Filter, Kontingent, Status-Log). + parameters: + - name: id + in: path + required: true + schema: { type: integer } + 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ß" } + /press-releases/{id}/images: + parameters: + - name: id + in: path + required: true + schema: { type: integer } + get: + tags: [images] + summary: Bilder einer Mitteilung auflisten + responses: + "200": + description: Bilderliste + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: { $ref: "#/components/schemas/PressReleaseImage" } + post: + tags: [images] + summary: Bild hochladen (nur bei draft/rejected) + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [image] + properties: + image: + type: string + format: binary + description: JPG, PNG oder WebP, max. 8 MB + title: { type: string, maxLength: 120 } + description: { type: string, maxLength: 500 } + copyright: { type: string, maxLength: 255 } + is_preview: + type: boolean + description: Als Vorschaubild setzen (ersetzt ein vorhandenes) + responses: + "201": { description: "Bild gespeichert" } + "409": { description: "Mitteilung ist nicht mehr änderbar" } + "422": { description: "Ungültige Datei" } + /press-release-images/{imageId}: + delete: + tags: [images] + summary: Bild entfernen (nur bei draft/rejected) + parameters: + - name: imageId + in: path + required: true + schema: { type: integer } + responses: + "204": { description: "Gelöscht" } + "409": { description: "Mitteilung ist nicht mehr änderbar" } + /companies: + get: + tags: [companies] + summary: Eigene Firmenprofile auflisten + parameters: + - name: per_page + in: query + schema: { type: integer, maximum: 100 } + responses: + "200": { description: "Paginierte Liste eigener Firmen" } + /companies/{id}: + get: + tags: [companies] + summary: Firmenprofil abrufen + parameters: + - name: id + in: path + required: true + schema: { type: integer } + responses: + "200": { description: "Firmenprofil" } + "403": { description: "Nicht gefunden oder fremdes Konto" } + /categories: + get: + tags: [categories] + summary: Aktive Rubriken auflisten + responses: + "200": { description: "Rubriken inkl. Übersetzungen" } + /newsletter/subscribe: + post: + tags: [newsletter] + summary: Newsletter-Anmeldung anlegen (Double-Opt-in folgt per Mail) + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [portal, email] + properties: + portal: + type: string + enum: [presseecho, businessportal24] + email: { type: string, format: email, maxLength: 190 } + salutation_key: { type: string, maxLength: 20 } + first_name: { type: string, maxLength: 80 } + last_name: { type: string, maxLength: 80 } + responses: + "201": { description: "Anmeldung angelegt (unbestätigt)" } +components: + securitySchemes: + bearerToken: + type: http + scheme: bearer + description: "Persönlicher API-Token aus dem Publisher-Konto (Selfservice unter „API-Tokens")." + schemas: + PressReleaseInput: + type: object + required: [company_id, category_id, language, title, text] + properties: + company_id: + type: integer + description: ID eines eigenen Firmenprofils — bestimmt Newsroom und Portal + category_id: { type: integer } + language: + type: string + minLength: 2 + maxLength: 2 + description: de oder en + title: { type: string, maxLength: 255 } + text: + type: string + description: "Erlaubtes HTML: p, br, h2, h3, strong, em, ul, ol, li, blockquote, a" + backlink_url: { type: string, format: uri, maxLength: 255 } + keywords: { type: string, maxLength: 255 } + teaser_begin: { type: integer, minimum: 0 } + teaser_end: { type: integer, minimum: 0 } + no_export: { type: boolean } + PressRelease: + type: object + properties: + id: { type: integer } + uuid: { type: string, format: uuid } + portal: { type: string } + language: { type: string } + title: { type: string } + slug: { type: string } + text: { type: string } + backlink_url: { type: [string, "null"] } + keywords: { type: [string, "null"] } + status: + type: string + enum: [draft, review, published, rejected, archived] + hits: { type: integer } + teaser: + type: object + properties: + begin: { type: [integer, "null"] } + end: { type: [integer, "null"] } + no_export: { type: boolean } + published_at: { type: [string, "null"], format: date-time } + company: { type: object } + category: { type: object } + images: + type: array + items: { $ref: "#/components/schemas/PressReleaseImage" } + created_at: { type: string, format: date-time } + updated_at: { type: string, format: date-time } + PressReleaseImage: + type: object + properties: + id: { type: integer } + title: { type: [string, "null"] } + description: { type: [string, "null"] } + copyright: { type: [string, "null"] } + is_preview: { type: boolean } + width: { type: integer } + height: { type: integer } + mime: { type: string } diff --git a/resources/views/web/api.blade.php b/resources/views/web/api.blade.php index 95d81319..0cd1d100 100644 --- a/resources/views/web/api.blade.php +++ b/resources/views/web/api.blade.php @@ -1,95 +1,252 @@ @extends('web.layouts.web-master') -@section('title', 'API & Integrationen') -@section('meta_description', 'Integrieren Sie unsere Dienste per REST-API: Pressemitteilungen programmatisch veröffentlichen, verwalten und abrufen.') +@section('title', 'API-Dokumentation') +@section('meta_description', 'REST-API v1: Pressemitteilungen programmatisch erstellen, mit Bildern versehen und zur Prüfung einreichen — Token-basiert, JSON, versioniert.') @php - $features = [ - ['title' => 'RESTful API', 'text' => 'Moderne REST-Architektur mit JSON-Responses für einfache Integration in jede Programmiersprache.'], - ['title' => 'OAuth 2.0', 'text' => 'Sichere Authentifizierung mit OAuth 2.0 und API-Tokens für maximale Sicherheit.'], - ['title' => 'Webhooks', 'text' => 'Echtzeit-Benachrichtigungen über Events wie Veröffentlichungen oder Statusänderungen.'], - ['title' => 'Dokumentation', 'text' => 'Umfassende API-Dokumentation mit Code-Beispielen in verschiedenen Sprachen.'], - ['title' => 'Rate Limiting', 'text' => 'Faire Rate Limits mit klaren Headern und automatischer Skalierung bei Bedarf.'], - ['title' => 'SDKs', 'text' => 'Offizielle SDKs für PHP, JavaScript, Python und weitere Sprachen.'], + // Die API läuft zentral über den Publisher-Hub (pressekonto). + $apiBase = rtrim((string) config('domains.domain_portal_url', config('app.url')), '/').'/api/v1'; + + $host = request()->getHost(); + $apiBrandKey = str_contains($host, 'pressekonto') + ? 'pressekonto' + : (str_contains($host, 'presseecho') ? 'presseecho' : 'businessportal24'); + $apiContactEmail = config('domains.domains.'.$apiBrandKey.'.brand.contact_email') ?? 'info@businessportal24.com'; + + $endpoints = [ + [ + 'group' => 'Pressemitteilungen', + 'items' => [ + ['method' => 'GET', 'path' => '/press-releases', 'desc' => 'Eigene Mitteilungen auflisten (paginiert, filterbar über ?status= und ?per_page=, max. 100).', 'ability' => 'press-releases:read'], + ['method' => 'POST', 'path' => '/press-releases', 'desc' => 'Neue Mitteilung anlegen — immer als Entwurf (Status draft).', 'ability' => 'press-releases:write'], + ['method' => 'GET', 'path' => '/press-releases/{id}', 'desc' => 'Eine Mitteilung inkl. Firma, Rubrik und Bildern abrufen.', 'ability' => 'press-releases:read'], + ['method' => 'PATCH', 'path' => '/press-releases/{id}', 'desc' => 'Mitteilung ändern — nur im Status draft oder rejected.', 'ability' => 'press-releases:write'], + ['method' => 'POST', 'path' => '/press-releases/{id}/submit', 'desc' => 'Zur Inhaltsprüfung einreichen — der einzige Weg Richtung Veröffentlichung.', 'ability' => 'press-releases:write'], + ['method' => 'DELETE', 'path' => '/press-releases/{id}', 'desc' => 'Mitteilung löschen — veröffentlichte Mitteilungen sind ausgenommen.', 'ability' => 'press-releases:write'], + ], + ], + [ + 'group' => 'Bilder', + 'items' => [ + ['method' => 'GET', 'path' => '/press-releases/{id}/images', 'desc' => 'Bilder einer Mitteilung auflisten.', 'ability' => 'press-releases:read'], + ['method' => 'POST', 'path' => '/press-releases/{id}/images', 'desc' => 'Bild hochladen (multipart/form-data) — nur bei draft/rejected.', 'ability' => 'press-release-images:write'], + ['method' => 'DELETE', 'path' => '/press-release-images/{imageId}', 'desc' => 'Bild entfernen — nur bei draft/rejected.', 'ability' => 'press-release-images:write'], + ], + ], + [ + 'group' => 'Stammdaten', + 'items' => [ + ['method' => 'GET', 'path' => '/companies', 'desc' => 'Eigene Firmenprofile (Newsrooms) auflisten.', 'ability' => 'companies:read'], + ['method' => 'GET', 'path' => '/companies/{id}', 'desc' => 'Ein Firmenprofil abrufen.', 'ability' => 'companies:read'], + ['method' => 'GET', 'path' => '/categories', 'desc' => 'Aktive Rubriken inkl. Übersetzungen — liefert die category_id für neue Mitteilungen.', 'ability' => 'press-releases:read'], + ], + ], ]; - $integrations = ['WordPress', 'Zapier', 'Slack', 'HubSpot']; + $pressReleaseFields = [ + ['name' => 'company_id', 'type' => 'integer', 'required' => true, 'desc' => 'ID eines eigenen Firmenprofils — bestimmt Newsroom und Portal.'], + ['name' => 'category_id', 'type' => 'integer', 'required' => true, 'desc' => 'Rubrik (siehe GET /categories).'], + ['name' => 'language', 'type' => 'string', 'required' => true, 'desc' => 'Sprachcode, zwei Zeichen: de oder en.'], + ['name' => 'title', 'type' => 'string', 'required' => true, 'desc' => 'Überschrift, max. 255 Zeichen.'], + ['name' => 'text', 'type' => 'string', 'required' => true, 'desc' => 'Inhalt. Erlaubtes HTML: p, br, h2, h3, strong, em, ul, ol, li, blockquote, a — alles andere wird entfernt.'], + ['name' => 'backlink_url', 'type' => 'string', 'required' => false, 'desc' => 'Link zur Quelle/Website, max. 255 Zeichen.'], + ['name' => 'keywords', 'type' => 'string', 'required' => false, 'desc' => 'Schlagworte, kommagetrennt, max. 255 Zeichen.'], + ['name' => 'teaser_begin / teaser_end', 'type' => 'integer', 'required' => false, 'desc' => 'Zeichen-Offsets für den Teaser-Ausschnitt.'], + ['name' => 'no_export', 'type' => 'boolean', 'required' => false, 'desc' => 'Mitteilung von Feeds/Exporten ausnehmen.'], + ]; + + $imageFields = [ + ['name' => 'image', 'type' => 'file', 'required' => true, 'desc' => 'JPG, PNG oder WebP, max. 8 MB.'], + ['name' => 'title', 'type' => 'string', 'required' => false, 'desc' => 'Bildtitel, max. 120 Zeichen.'], + ['name' => 'description', 'type' => 'string', 'required' => false, 'desc' => 'Beschreibung, max. 500 Zeichen.'], + ['name' => 'copyright', 'type' => 'string', 'required' => false, 'desc' => 'Urheberangabe, max. 255 Zeichen.'], + ['name' => 'is_preview', 'type' => 'boolean', 'required' => false, 'desc' => 'Als Vorschaubild setzen (ersetzt ein vorhandenes).'], + ]; + + $statusCodes = [ + ['code' => '200 / 201 / 204', 'desc' => 'Erfolg — Abruf, Ressource erstellt bzw. gelöscht.'], + ['code' => '401', 'desc' => 'Kein oder ungültiger API-Token.'], + ['code' => '402', 'desc' => 'Einreichen erfordert eine aktive Buchung (Tarif oder Einzel-PM).'], + ['code' => '403', 'desc' => 'Token-Berechtigung fehlt oder die Ressource gehört nicht zu Ihrem Konto.'], + ['code' => '409', 'desc' => 'Statuskonflikt — z. B. Bearbeiten einer bereits veröffentlichten Mitteilung.'], + ['code' => '422', 'desc' => 'Validierungsfehler oder Kontingent-/Richtlinienverstoß (Details im Body).'], + ['code' => '429', 'desc' => 'Rate-Limit erreicht: 60 Anfragen pro Minute je Token (Header X-RateLimit-*).'], + ]; + + $methodColors = [ + 'GET' => 'text-[#1F6B3F] bg-[#E7F2EA]', + 'POST' => 'text-[#8A4B12] bg-[#F7EBDD]', + 'PATCH' => 'text-[#6B4FA1] bg-[#EEE9F7]', + 'DELETE' => 'text-[#A13030] bg-[#F7E5E5]', + ]; @endphp @section('content') - {{-- Überblick --}} + {{-- Überblick + Authentifizierung --}}
-

Leistungsstarke REST-API

+

Grundlagen

- Unsere RESTful API ermöglicht es Ihnen, Pressemitteilungen programmatisch zu veröffentlichen, zu - verwalten und abzurufen – ideal für Unternehmen mit hohem Veröffentlichungsvolumen oder bestehenden - Content-Management-Systemen. + Die API v1 ist ein RESTful Web Service mit JSON-Responses. Basis-URL: + {{ $apiBase }}. Eine über die API angelegte Mitteilung ist immer ein + Entwurf; veröffentlicht wird sie erst nach dem Einreichen über die Submit-Route und der + anschließenden Inhaltsprüfung — sie erscheint dann wie jede andere Mitteilung auf beiden Portalen.

+

Authentifizierung

- Die API ist vollständig dokumentiert und unterstützt moderne Authentifizierungsmethoden sowie - Webhooks für Echtzeit-Benachrichtigungen. + Alle Anfragen benötigen einen persönlichen API-Token als Bearer-Header: + Authorization: Bearer <token>. Jeder Token trägt Berechtigungen + (z. B. press-releases:write), die den Zugriff auf die jeweiligen Endpunkte steuern. + Tokens erstellen und widerrufen Sie selbst in Ihrem Publisher-Konto unter „API-Tokens" — + Voraussetzung ist eine aktive Buchung (Tarif oder Einzel-Pressemitteilung). + Der X-ApiKey-Header der Altportale wird nicht mehr unterstützt.

- {{-- Features --}} -

API-Features

-
- @foreach ($features as $feature) -
-

{{ $feature['title'] }}

-

{{ $feature['text'] }}

+ {{-- Endpunkte --}} +

Endpunkte

+
+ @foreach ($endpoints as $group) +
+

{{ $group['group'] }}

+
+ @foreach ($group['items'] as $endpoint) +
+ {{ $endpoint['method'] }} +
+ {{ $endpoint['path'] }} +

+ {{ $endpoint['desc'] }} + · Berechtigung: {{ $endpoint['ability'] }} +

+
+
+ @endforeach +
@endforeach
- {{-- Code-Beispiel --}} + {{-- Felder --}} +

Felder einer Pressemitteilung

+
+ + + + + + + + + + + @foreach ($pressReleaseFields as $field) + + + + + + + @endforeach + +
FeldTypPflichtBeschreibung
{{ $field['name'] }}{{ $field['type'] }}{{ $field['required'] ? 'ja' : '—' }}{{ $field['desc'] }}
+
+ +

Felder eines Bild-Uploads

+
+ + + + + + + + + + + @foreach ($imageFields as $field) + + + + + + + @endforeach + +
FeldTypPflichtBeschreibung
{{ $field['name'] }}{{ $field['type'] }}{{ $field['required'] ? 'ja' : '—' }}{{ $field['desc'] }}
+
+ + {{-- Workflow-Beispiel --}}

Schneller Einstieg

-

Veröffentlichen Sie Ihre erste Pressemitteilung in wenigen Zeilen Code.

-
-
// Beispiel: Pressemitteilung veröffentlichen
-
curl -X POST https://api.example.com/v1/releases \
+        

Der typische Ablauf: Entwurf anlegen, Bild hochladen, zur Prüfung einreichen.

+
+
# 1. Entwurf anlegen
+curl -X POST {{ $apiBase }}/press-releases \
   -H "Authorization: Bearer YOUR_API_TOKEN" \
   -H "Content-Type: application/json" \
   -d '{
+    "company_id": 1,
+    "category_id": 2,
+    "language": "de",
     "title": "Neue Produktankündigung",
-    "content": "Unser neues Produkt …",
-    "category": "wirtschaft",
-    "publish_at": "2026-01-15T10:00:00Z"
-  }'
-
- {{-- TODO: Link auf die vollständige API-Dokumentation, sobald sie online ist. --}} -

Die vollständige Dokumentation folgt. Bei Fragen zum API-Zugang hilft unser Support.

+ "text": "<p>Unser neues Produkt …</p>" + }' - {{-- Integrationen --}} -

Native Integrationen

-
- @foreach ($integrations as $integration) -
- {{ $integration }} -
- @endforeach +# 2. Bild hochladen (multipart) +curl -X POST {{ $apiBase }}/press-releases/123/images \ + -H "Authorization: Bearer YOUR_API_TOKEN" \ + -F "image=@pressefoto.jpg" \ + -F "copyright=Mustermann GmbH" \ + -F "is_preview=1" + +# 3. Zur Inhaltsprüfung einreichen +curl -X POST {{ $apiBase }}/press-releases/123/submit \ + -H "Authorization: Bearer YOUR_API_TOKEN"
+
+

+ Maschinenlesbare Referenz: OpenAPI-Spezifikation (YAML). +

+ + {{-- Statuscodes --}} +

HTTP-Statuscodes

+
+ + + + + + + + + @foreach ($statusCodes as $status) + + + + + @endforeach + +
CodeBedeutung
{{ $status['code'] }}{{ $status['desc'] }}
{{-- CTA --}} -
+
-

Starten Sie mit der API

-

API-Zugang ist ab dem passenden Tarif verfügbar – Details finden Sie auf der Preisseite.

+

API-Token erstellen

+

Tokens verwalten Sie selbst im Publisher-Konto. Bei Fragen zur Anbindung hilft der Support: {{ $apiContactEmail }}

diff --git a/tests/Feature/ApiDocumentationTest.php b/tests/Feature/ApiDocumentationTest.php index 42c7f0fb..ed2d13e1 100644 --- a/tests/Feature/ApiDocumentationTest.php +++ b/tests/Feature/ApiDocumentationTest.php @@ -9,5 +9,17 @@ test('api v1 documentation is publicly available', function () { ->assertHeader('content-type', 'application/yaml; charset=UTF-8') ->assertSee('openapi: 3.1.0', false) ->assertSee('/press-releases:', false) - ->assertSee('Legacy API keys are no longer supported.', false); -})->skip('OpenAPI-Spec docs/api/v1.yml ist noch nicht erstellt (eigener API-Doku-Track, siehe dev/migration 2026/07-API-MIGRATION.md). Assertions bleiben erhalten; Skip entfernen, sobald die Datei vorliegt.'); + ->assertSee('legacy API keys are no longer supported', false); +}); + +test('the public api page documents the real endpoints', function () { + /** @var TestCase $this */ + $this->get('https://pressekonto.test/de/api') + ->assertSuccessful() + ->assertSeeText('API-Dokumentation') + ->assertSee('/press-releases/{id}/submit', false) + ->assertSee('press-releases:write', false) + // Keine Fantasie-Features mehr (OAuth/SDKs/Webhooks gab es nie). + ->assertDontSee('OAuth 2.0') + ->assertDontSee('api.example.com'); +});