'admin-crud-'.uniqid().'@test.com', 'password' => Hash::make('secret'), 'admin' => 2, 'lang' => 'de', ]); } // ─── Model / Scopes ─────────────────────────────────────────────────────────── it('scopeOpen gibt nur offene, laufende und wartende Incidents zurück', function () { PaymentIncident::create(['title' => 'Offen', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'open', 'detected_at' => now()]); PaymentIncident::create(['title' => 'In Bearbeitung', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'in_progress', 'detected_at' => now()]); PaymentIncident::create(['title' => 'Wartet', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'waiting_provider', 'detected_at' => now()]); PaymentIncident::create(['title' => 'Gelöst', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'resolved', 'detected_at' => now()]); $open = PaymentIncident::open()->get(); expect($open)->toHaveCount(3); expect($open->pluck('title')->toArray())->not->toContain('Gelöst'); }); it('scopePayone filtert nach PAYONE', function () { PaymentIncident::create(['title' => 'PAYONE Incident', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()]); PaymentIncident::create(['title' => 'Stripe Incident', 'provider' => 'stripe', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()]); expect(PaymentIncident::payone()->count())->toBe(1); expect(PaymentIncident::payone()->first()->title)->toBe('PAYONE Incident'); }); it('scopeLastDays filtert nach Zeitraum', function () { PaymentIncident::create(['title' => 'Alt', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()->subDays(10)]); PaymentIncident::create(['title' => 'Neu', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()]); expect(PaymentIncident::lastDays(7)->count())->toBe(1); expect(PaymentIncident::lastDays(7)->first()->title)->toBe('Neu'); }); it('Accessor severity_color gibt Bootstrap-Klasse zurück', function () { $incident = new PaymentIncident(['severity' => 'critical']); expect($incident->severity_color)->toBe('danger'); $incident = new PaymentIncident(['severity' => 'high']); expect($incident->severity_color)->toBe('warning'); $incident = new PaymentIncident(['severity' => 'low']); expect($incident->severity_color)->toBe('success'); }); it('Accessor duration berechnet Dauer korrekt', function () { $incident = new PaymentIncident([ 'detected_at' => now()->subMinutes(90), 'resolved_at' => null, ]); expect($incident->duration)->toContain('h'); }); // ─── Controller: Incident anlegen ───────────────────────────────────────────── it('legt einen neuen Incident an und erstellt automatisch eine erste Aktivität', function () { $admin = makeAdmin(); $this->actingAs($admin); $request = StorePaymentIncidentRequest::create('/admin/payment-dashboard', 'POST', [ 'title' => 'PAYONE IPN ausgefallen', 'provider' => 'payone', 'type' => 'ipn_error', 'severity' => 'high', 'detected_at' => now()->format('Y-m-d H:i:s'), 'affected_orders' => 5, 'affected_revenue' => '450.00', ]); $request->setContainer(app()); $request->validateResolved(); try { (new PaymentDashboardController)->store($request); } catch (\Symfony\Component\Routing\Exception\RouteNotFoundException $e) { // Domain-spezifische Routen sind im Test-Kontext nicht registriert – erwartet. } $incident = PaymentIncident::where('title', 'PAYONE IPN ausgefallen')->first(); expect($incident)->not->toBeNull(); expect($incident->provider)->toBe('payone'); expect($incident->status)->toBe('open'); expect($incident->affected_orders)->toBe(5); expect(IncidentActivity::where('incident_id', $incident->id)->count())->toBe(1); expect(IncidentActivity::where('incident_id', $incident->id)->first()->type)->toBe('note'); }); // ─── FormRequest Validierung ────────────────────────────────────────────────── it('validiert Pflichtfelder beim Anlegen eines Incidents', function () { $rules = (new StorePaymentIncidentRequest)->rules(); $validator = Validator::make([], $rules); expect($validator->fails())->toBeTrue(); expect($validator->errors()->has('title'))->toBeTrue(); expect($validator->errors()->has('provider'))->toBeTrue(); expect($validator->errors()->has('type'))->toBeTrue(); expect($validator->errors()->has('severity'))->toBeTrue(); expect($validator->errors()->has('detected_at'))->toBeTrue(); }); it('validiert ungültige Enum-Werte in StorePaymentIncidentRequest', function () { $rules = (new StorePaymentIncidentRequest)->rules(); $validator = Validator::make([ 'title' => 'Test', 'provider' => 'bitcoin', 'type' => 'unknown', 'severity' => 'extreme', 'detected_at' => now()->toDateTimeString(), ], $rules); expect($validator->fails())->toBeTrue(); expect($validator->errors()->has('provider'))->toBeTrue(); expect($validator->errors()->has('type'))->toBeTrue(); expect($validator->errors()->has('severity'))->toBeTrue(); }); it('validiert Pflichtfelder in AddIncidentActivityRequest', function () { $rules = (new AddIncidentActivityRequest)->rules(); $validator = Validator::make([], $rules); expect($validator->fails())->toBeTrue(); expect($validator->errors()->has('type'))->toBeTrue(); expect($validator->errors()->has('title'))->toBeTrue(); }); // ─── Controller: Status ändern ──────────────────────────────────────────────── it('ändert den Status eines Incidents und erstellt eine Aktivität', function () { $admin = makeAdmin(); $this->actingAs($admin); $incident = PaymentIncident::create([ 'title' => 'Status-Test', 'provider' => 'payone', 'type' => 'outage', 'severity' => 'critical', 'detected_at' => now(), ]); $request = UpdateIncidentStatusRequest::create('/admin/payment-dashboard/'.$incident->id.'/status', 'PATCH', [ 'status' => 'resolved', ]); $request->setContainer(app()); $request->validateResolved(); $controller = new PaymentDashboardController; $controller->updateStatus($request, $incident); $incident->refresh(); expect($incident->status)->toBe('resolved'); expect($incident->resolved_at)->not->toBeNull(); $statusActivity = IncidentActivity::where('incident_id', $incident->id) ->where('type', 'status_change') ->first(); expect($statusActivity)->not->toBeNull(); expect($statusActivity->title)->toContain('Gelöst'); }); it('setzt resolved_at nicht wenn Status in_progress ist', function () { $admin = makeAdmin(); $this->actingAs($admin); $incident = PaymentIncident::create([ 'title' => 'Resolved At Test', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'detected_at' => now(), ]); $request = UpdateIncidentStatusRequest::create('', 'PATCH', ['status' => 'in_progress']); $request->setContainer(app()); $request->validateResolved(); (new PaymentDashboardController)->updateStatus($request, $incident); expect($incident->fresh()->resolved_at)->toBeNull(); }); // ─── Controller: Aktivität hinzufügen ──────────────────────────────────────── it('fügt eine Aktivität zu einem Incident hinzu', function () { $admin = makeAdmin(); $this->actingAs($admin); $incident = PaymentIncident::create([ 'title' => 'Aktivitäts-Test', 'provider' => 'payone', 'type' => 'payment_failure', 'severity' => 'medium', 'detected_at' => now(), ]); $request = AddIncidentActivityRequest::create('', 'POST', [ 'type' => 'email', 'title' => 'Mail an PAYONE gesendet', 'content' => 'Ticket eröffnet, Antwort abwarten.', ]); $request->setContainer(app()); $request->validateResolved(); (new PaymentDashboardController)->addActivity($request, $incident); $activity = IncidentActivity::where('incident_id', $incident->id) ->where('type', 'email') ->first(); expect($activity)->not->toBeNull(); expect($activity->title)->toBe('Mail an PAYONE gesendet'); expect($activity->author)->toBe($admin->name ?? 'System'); }); it('setzt Incident automatisch auf in_progress wenn Aktivität bei offenem Incident hinzugefügt wird', function () { $admin = makeAdmin(); $this->actingAs($admin); $incident = PaymentIncident::create([ 'title' => 'Auto-Status-Test', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'status' => 'open', 'detected_at' => now(), ]); $request = AddIncidentActivityRequest::create('', 'POST', ['type' => 'note', 'title' => 'Erste Notiz']); $request->setContainer(app()); $request->validateResolved(); (new PaymentDashboardController)->addActivity($request, $incident); expect($incident->fresh()->status)->toBe('in_progress'); }); it('validiert Pflichtfelder in UpdateIncidentStatusRequest', function () { $rules = (new UpdateIncidentStatusRequest)->rules(); $validator = Validator::make([], $rules); expect($validator->fails())->toBeTrue(); expect($validator->errors()->has('status'))->toBeTrue(); }); it('validiert ungültigen Status in UpdateIncidentStatusRequest', function () { $rules = (new UpdateIncidentStatusRequest)->rules(); $validator = Validator::make(['status' => 'deleted'], $rules); expect($validator->fails())->toBeTrue(); expect($validator->errors()->has('status'))->toBeTrue(); }); // ─── Cache Invalidierung ────────────────────────────────────────────────────── it('invalidiert den Cache nach Incident-Anlage', function () { $admin = makeAdmin(); $this->actingAs($admin); Cache::put('open_incident_count', 99, 60); $request = StorePaymentIncidentRequest::create('', 'POST', [ 'title' => 'Cache-Test', 'provider' => 'payone', 'type' => 'other', 'severity' => 'low', 'detected_at' => now()->format('Y-m-d H:i:s'), ]); $request->setContainer(app()); $request->validateResolved(); try { (new PaymentDashboardController)->store($request); } catch (\Symfony\Component\Routing\Exception\RouteNotFoundException $e) { // Domain-spezifische Routen sind im Test-Kontext nicht registriert – erwartet. } expect(Cache::has('open_incident_count'))->toBeFalse(); });