Eigene Compat-Controller (Api/Legacy) fuer die 5 real genutzten
/service-Endpoints (Prod-Log-Beleg): Company-/Kontakt-Create (liefen
410!), PM-Create, Bild-Upload, published-only-GET — verschachtelte
Alt-Params, 201+Location+{}, Alt-Fehlerformat, Hit-Zaehlung,
Legacy-Land-Mapping.
- Anlegen = Einreichen (Alt-Verhalten); Sofort-Publish via neuer
Permission api:immediate-publication (WS-7: connektar zuweisen)
- Admin-Schalter 'KI-Pruefung Legacy-API' (app_settings-Tabelle,
Toggle auf /admin/press-releases): an = review + KI-Routing
- Admin-PM-Liste: KI-Spalte (Ampel-Badge) + manueller Pruef-Button
ohne Routing fuer ungepruefte PMs
- Mails bei API-Einlieferern an Pressekontakt, sonst Firmen-Mail,
Fallback Einlieferer (User-Entscheidung 10.07.)
- Tests: LegacyServiceParityTest (connektar-Flow end-to-end);
Suite 1182 gruen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
176 lines
6.8 KiB
PHP
176 lines
6.8 KiB
PHP
<?php
|
|
|
|
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\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Laravel\Sanctum\PersonalAccessToken;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Übernahme bestehender Legacy-API-Integrationen (06.07.2026): importierte
|
|
* api_keys bleiben gültig, alte Übergabeformen (X-ApiKey/api_key) und die
|
|
* dokumentierten /service/…-Pfade funktionieren weiter.
|
|
*/
|
|
function eligibleLegacyApiUser(int $legacyId = 4711): User
|
|
{
|
|
$user = User::factory()->create([
|
|
'is_active' => true,
|
|
'portal' => Portal::Businessportal24,
|
|
'legacy_portal' => 'businessportal24',
|
|
'legacy_id' => $legacyId,
|
|
]);
|
|
|
|
// Aktiver Zahlungsstatus → API-berechtigt (ApiAccessEligibilityService).
|
|
UserPaymentOption::factory()->create([
|
|
'user_id' => $user->id,
|
|
'payment_option_id' => PaymentOption::factory()->create()->id,
|
|
'status' => UserPaymentOptionStatus::Grandfathered->value,
|
|
'grandfathered_until' => null,
|
|
'current_period_start' => now()->subMonth(),
|
|
'current_period_end' => now()->addMonths(11),
|
|
'stripe_subscription_id' => null,
|
|
]);
|
|
|
|
return $user;
|
|
}
|
|
|
|
test('the importer turns legacy api keys into sanctum tokens idempotently', function () {
|
|
$user = eligibleLegacyApiUser();
|
|
|
|
// Nicht berechtigter Kandidat (inaktiv) und unbekanntes Legacy-Mapping.
|
|
$inactive = User::factory()->create([
|
|
'is_active' => false,
|
|
'legacy_portal' => 'businessportal24',
|
|
'legacy_id' => 999,
|
|
]);
|
|
|
|
$rows = [
|
|
['legacy_id' => 4711, 'api_key' => 'legacy-key-abc123'],
|
|
['legacy_id' => 999, 'api_key' => 'legacy-key-inactive'],
|
|
['legacy_id' => 123456, 'api_key' => 'legacy-key-unmapped'],
|
|
];
|
|
|
|
$importer = app(LegacyApiKeyImporter::class);
|
|
$result = $importer->import('businessportal24', $rows);
|
|
|
|
expect($result['imported'])->toBe(1);
|
|
expect($result['ineligible'])->toBe(1);
|
|
expect($result['ineligible_users'])->toBe([$inactive->email]);
|
|
expect($result['unknown_user'])->toBe(1);
|
|
|
|
$token = PersonalAccessToken::findToken('legacy-key-abc123');
|
|
expect($token)->not->toBeNull();
|
|
expect($token->tokenable_id)->toBe($user->id);
|
|
expect($token->name)->toBe(LegacyApiKeyImporter::TOKEN_NAME);
|
|
expect($token->abilities)->toBe(LegacyApiKeyImporter::ABILITIES);
|
|
|
|
// Re-Run legt keine Duplikate an.
|
|
$rerun = $importer->import('businessportal24', $rows);
|
|
expect($rerun['imported'])->toBe(0);
|
|
expect($rerun['already_present'])->toBe(1);
|
|
expect(PersonalAccessToken::query()->where('token', hash('sha256', 'legacy-key-abc123'))->count())->toBe(1);
|
|
});
|
|
|
|
test('an imported legacy key authenticates via the old header and query forms', function () {
|
|
/** @var TestCase $this */
|
|
eligibleLegacyApiUser();
|
|
app(LegacyApiKeyImporter::class)->import('businessportal24', [
|
|
['legacy_id' => 4711, 'api_key' => 'legacy-key-abc123'],
|
|
]);
|
|
|
|
// Dokumentierter Alt-Header der Portale: X-ApiKey.
|
|
$this->getJson('/api/v1/categories', ['X-ApiKey' => 'legacy-key-abc123'])
|
|
->assertOk();
|
|
|
|
// Verbreitete Schreibvariante + Query-Parameter.
|
|
$this->getJson('/api/v1/categories', ['X-Api-Key' => 'legacy-key-abc123'])
|
|
->assertOk();
|
|
$this->getJson('/api/v1/categories?api_key=legacy-key-abc123')
|
|
->assertOk();
|
|
});
|
|
|
|
test('unknown legacy keys still get the 410 migration hint', function () {
|
|
/** @var TestCase $this */
|
|
$this->getJson('/api/v1/categories', ['X-ApiKey' => 'gibt-es-nicht'])
|
|
->assertStatus(410)
|
|
->assertJsonStructure(['message', 'migration_url', 'docs_url']);
|
|
});
|
|
|
|
test('the documented legacy service paths keep working with an imported key', function () {
|
|
/** @var TestCase $this */
|
|
eligibleLegacyApiUser();
|
|
app(LegacyApiKeyImporter::class)->import('businessportal24', [
|
|
['legacy_id' => 4711, 'api_key' => 'legacy-key-abc123'],
|
|
]);
|
|
|
|
$this->getJson('/service/pressreleases', ['X-ApiKey' => 'legacy-key-abc123'])
|
|
->assertOk();
|
|
|
|
$this->getJson('/service/users/me/companies', ['X-ApiKey' => 'legacy-key-abc123'])
|
|
->assertOk();
|
|
});
|
|
|
|
test('the legacy service paths require authentication', function () {
|
|
/** @var TestCase $this */
|
|
$this->getJson('/service/pressreleases')->assertUnauthorized();
|
|
});
|
|
|
|
test('retired legacy service endpoints answer with a helpful 410 instead of 404', function () {
|
|
/** @var TestCase $this */
|
|
// Company-Create ist seit 10.07. ein echter Paritaets-Endpoint (S1);
|
|
// als weiterhin abgeschalteter Alt-Pfad dient das Einzelbild-GET.
|
|
$this->getJson('/service/pressreleases/1/images/1')
|
|
->assertStatus(410)
|
|
->assertJsonStructure(['message', 'account_url', 'docs_url']);
|
|
|
|
$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).
|
|
Queue::fake();
|
|
$response = $this->postJson('/service/pressreleases', [
|
|
'press_release' => [
|
|
'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((int) basename((string) $response->headers->get('Location')));
|
|
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", [
|
|
'press_release_image' => [
|
|
'image' => UploadedFile::fake()->image('alt-client.jpg'),
|
|
],
|
|
], ['X-ApiKey' => 'legacy-key-abc123'])->assertCreated();
|
|
|
|
expect($pressRelease->images()->first()->ai_declaration?->value)->toBe('original');
|
|
});
|