audit: Nacharbeit Umsetzungsaudit 25.09.2026 — Bildfreigabe, Beweisarchiv, KI-Atomarität, Berichte (Block 8, N01–N05, N07, N08, N10–N21, 5.1, 5.2)
Bilder (N01, N05, N07, N08): gemeinsamer Freigabezustand PressReleaseImage::isPubliclyDeliverable() für Seiten, OG und Medienroute (Bestand bleibt sichtbar, v2 nur mit Bestätigung, Admin-Upload bis zum Kundenklick nicht öffentlich); eigene Ability confirmImageRights nur für den Kontoinhaber, ohne Super-Admin-Kurzschluss, Identität und ausstehende Bestätigung auch im Service; Status-Regel unter der PM-Sperre; Cache-Flush nach dem Commit außerhalb der Dateibereinigung; Snapshot nach Bestätigung an veröffentlichter PM; Bestätigungstexte an die gezeigte Kennung gebunden. Beweisarchiv (N02, N10, N11, N12, N13, 5.1): Tombstone unter Zeilensperre mit erneuter Hold-/Fristprüfung; Dateioperationen ausgewertet, Commands mit FAILURE; record() über .tmp + move; Marker archive_pending_at mit archive:retry-pending und archive:sweep-orphans; Anonymisierung entfernt Bilder über den Remover mit Antragsbezug; Draft-Purge räumt Medien nie veröffentlichter abgelehnter Einreichungen; App-Kopien fallen mit dem Archiv-Tombstone; label_corrected nur für veröffentlichte Bilder; archive_entries.legal_request_id; FileDeletion-Helfer; Systemstatus-Checks. KI und Hintergrund (N03, N04, N14, N20, N21): Ergebnis-UPDATE und Audit in einer Transaktion; erwarteter Hash bis in reject()/holdForManualReview(); Ablaufmail per beanspruchtem Marker in einer Transaktion; strikte Schemaprüfung der Vision-Antwort; Fortschrittsmarke ungecacht. Berichte und Canonical (N15–N19, 5.2): lückenlose Alterskohorten; Feed-Identität mit Portal und Sprache; Sitemap-Rolle in der Kundenansicht; Indexverteilung auf einer Grundgesamtheit; Kopien nach Wegfall der Erstquelle neu bewertet; Kategorie-Admin leert Seiten-Caches; Dry-Run zählt alle Tabellen. Gesamtsuite: 2.122 Tests grün, einer übersprungen; Larastan und Pint sauber. Zwei Migrationen (nullable Spalten, INSTANT). N09 offen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
34fa342c7f
commit
2a2215b3c3
66 changed files with 3092 additions and 368 deletions
|
|
@ -7,6 +7,7 @@ use App\Enums\ConfirmationChannel;
|
|||
use App\Enums\Portal;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\ArchiveEntry;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\ImageDeclarationCorrection;
|
||||
|
|
@ -18,12 +19,14 @@ use App\Models\User;
|
|||
use App\Services\Admin\AdminSystemHealth;
|
||||
use App\Services\PressRelease\ImageRightsConfirmationRequiredException;
|
||||
use App\Services\PressRelease\Images\ImageCorrectionService;
|
||||
use App\Services\PressRelease\Images\PressReleaseImageRemover;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Volt\Volt as LivewireVolt;
|
||||
use Tests\Support\FailingDisk;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
|
|
@ -375,3 +378,119 @@ test('the system health warns about failed provenance checks of the last seven d
|
|||
expect($checks->get('image_provenance')['status'])->toBe('warn')
|
||||
->and($checks->get('image_provenance')['detail'])->toContain('1 Bild-Upload');
|
||||
});
|
||||
|
||||
test('the media route applies the shared delivery rule: legacy stays public, withheld and unconfirmed v2 images do not', function () {
|
||||
/** @var TestCase $this */
|
||||
['pr' => $pr, 'author' => $author] = lifecycleRelease('published');
|
||||
$pr->forceFill(['published_at' => now()->subDay(), 'published_ever_at' => now()->subDay()])->save();
|
||||
$admin = lifecycleAdmin();
|
||||
|
||||
$legacy = PressReleaseImage::factory()->for($pr)->legacy()->create(['disk' => 'local', 'path' => 'presseecho/press/legacy.jpg']);
|
||||
Storage::disk('local')->put($legacy->path, 'legacy-bytes');
|
||||
$withheld = lifecycleImage($pr, attributes: ['withheld_at' => now()]);
|
||||
$unconfirmed = lifecycleImage($pr, confirmed: false, attributes: ['rights_confirmed_at' => null, 'customer_confirmation_requested_at' => now()]);
|
||||
$confirmed = lifecycleImage($pr);
|
||||
|
||||
$url = fn (PressReleaseImage $image): string => route('media.press-release-images.show', ['image' => $image->id]);
|
||||
|
||||
expect($legacy->isPubliclyDeliverable())->toBeTrue()
|
||||
->and($withheld->isPubliclyDeliverable())->toBeFalse()
|
||||
->and($unconfirmed->isPubliclyDeliverable())->toBeFalse()
|
||||
->and($confirmed->isPubliclyDeliverable())->toBeTrue()
|
||||
->and(PressReleaseImage::query()->publiclyVisible()->pluck('id')->all())->toEqualCanonicalizing([$legacy->id, $confirmed->id]);
|
||||
|
||||
$this->get($url($legacy))->assertSuccessful()->assertHeader('Cache-Control', 'max-age=86400, public');
|
||||
$this->get($url($confirmed))->assertSuccessful()->assertHeader('Cache-Control', 'max-age=86400, public');
|
||||
$this->get($url($withheld))->assertNotFound();
|
||||
$this->get($url($unconfirmed))->assertNotFound();
|
||||
|
||||
// Berechtigte interne Ansicht bleibt, aber ohne öffentlichen Cache.
|
||||
$this->actingAs($author)->get($url($withheld))->assertSuccessful()->assertHeader('Cache-Control', 'no-store, private');
|
||||
$this->actingAs($admin)->get($url($unconfirmed))->assertSuccessful()->assertHeader('Cache-Control', 'no-store, private');
|
||||
|
||||
app(ImageCorrectionService::class)->release($withheld, $admin, 'Einwand geprüft');
|
||||
auth()->logout();
|
||||
|
||||
$this->get($url($withheld))->assertSuccessful()->assertHeader('Cache-Control', 'max-age=86400, public');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Umsetzungsaudit 25.09.2026: N13, 5.1 b, 5.1 e
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('the draft purge also cleans media of stale rejected submissions that were never published and keeps their status', function () {
|
||||
/** @var TestCase $this */
|
||||
['pr' => $stale] = lifecycleRelease('rejected');
|
||||
$staleImage = lifecycleImage($stale);
|
||||
DB::table('press_releases')->where('id', $stale->id)->update(['updated_at' => now()->subDays(200)]);
|
||||
|
||||
['pr' => $fresh] = lifecycleRelease('rejected');
|
||||
$freshImage = lifecycleImage($fresh);
|
||||
|
||||
// Altbestand: im Altsystem „offline genommen", war einmal veröffentlicht.
|
||||
['pr' => $formerlyPublished] = lifecycleRelease('rejected');
|
||||
$formerlyPublished->forceFill(['published_ever_at' => now()->subYear()])->save();
|
||||
$keptImage = lifecycleImage($formerlyPublished, true, ['published_ever_at' => now()->subYear()]);
|
||||
DB::table('press_releases')->where('id', $formerlyPublished->id)->update(['updated_at' => now()->subDays(400)]);
|
||||
|
||||
$this->artisan('press-releases:purge-drafts', ['--days' => 180, '--dry-run' => true])
|
||||
->expectsOutputToContain('1 abgelehnte Einreichungen würden bereinigt')
|
||||
->assertSuccessful();
|
||||
expect(PressReleaseImage::query()->whereKey($staleImage->id)->exists())->toBeTrue();
|
||||
|
||||
$this->artisan('press-releases:purge-drafts', ['--days' => 180])
|
||||
->expectsOutputToContain('abgelehnte Einreichungen bereinigt: 1, Bilder gelöscht: 1')
|
||||
->assertSuccessful();
|
||||
|
||||
expect(PressReleaseImage::withTrashed()->whereKey($staleImage->id)->exists())->toBeFalse()
|
||||
->and(Storage::disk('local')->exists($staleImage->original_path))->toBeFalse()
|
||||
->and(ImageRightsConfirmation::query()->where('subject_id', $staleImage->id)->count())->toBe(0)
|
||||
->and($stale->fresh()->status)->toBe(PressReleaseStatus::Rejected)
|
||||
->and(PressReleaseImage::query()->whereKey($freshImage->id)->exists())->toBeTrue()
|
||||
->and(PressReleaseImage::query()->whereKey($keptImage->id)->exists())->toBeTrue()
|
||||
->and(Storage::disk('local')->exists($keptImage->original_path))->toBeTrue();
|
||||
|
||||
// Wiederholungsfest: ohne Bilder zählt die abgelehnte Meldung nicht mehr.
|
||||
$this->artisan('press-releases:purge-drafts', ['--days' => 180])
|
||||
->expectsOutputToContain('abgelehnte Einreichungen bereinigt: 0, Bilder gelöscht: 0')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('a correction of a never published image is logged but not archived', function () {
|
||||
Storage::fake('archive');
|
||||
config()->set('archive.key', 'base64:'.base64_encode(random_bytes(32)));
|
||||
['pr' => $pr] = lifecycleRelease('draft');
|
||||
$image = lifecycleImage($pr);
|
||||
|
||||
app(ImageCorrectionService::class)->correctCredit($image, lifecycleAdmin(), 'Neuer Nachweis', 'Tippfehler');
|
||||
|
||||
expect(ImageDeclarationCorrection::query()->where('press_release_image_id', $image->id)->count())->toBe(1)
|
||||
->and(ArchiveEntry::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('the immediate deletion keeps the image row when a local file cannot be deleted, and the health check counts leftovers', function () {
|
||||
['pr' => $pr] = lifecycleRelease('draft');
|
||||
$image = lifecycleImage($pr);
|
||||
|
||||
FailingDisk::install('local', ['delete' => fn () => false]);
|
||||
|
||||
expect(fn () => app(PressReleaseImageRemover::class)->remove($image))
|
||||
->toThrow(RuntimeException::class);
|
||||
|
||||
FailingDisk::restore('local');
|
||||
|
||||
expect(PressReleaseImage::query()->whereKey($image->id)->exists())->toBeTrue()
|
||||
->and(ImageRightsConfirmation::query()->where('subject_id', $image->id)->count())->toBe(1)
|
||||
->and(Storage::disk('local')->exists($image->original_path))->toBeTrue();
|
||||
|
||||
// Entferntes v2-Bild mit noch vorhandenem Original: Systemstatus warnt.
|
||||
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
||||
expect($checks->get('image_evidence_leftovers')['status'])->toBe('ok');
|
||||
|
||||
$image->forceFill(['published_ever_at' => now()])->save();
|
||||
$image->delete();
|
||||
|
||||
$checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key');
|
||||
expect($checks->get('image_evidence_leftovers')['status'])->toBe('warn')
|
||||
->and($checks->get('image_evidence_leftovers')['detail'])->toContain('1 entfernte(s) Bild(er)');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue