create(); Instance::factory()->create(['customer_id' => $customer->id, 'host_id' => $host->id, 'status' => 'active']); return $customer; } it('publishes a window and emails affected customers once (idempotent)', function () { Mail::fake(); $host = Host::factory()->active()->create(); $customer = affectedCustomerOn($host); Livewire::actingAs(operator('Owner'))->test(Maintenance::class) ->set('title', 'Netz-Wartung') ->set('startsAt', now()->addDay()->format('Y-m-d\TH:i')) ->set('endsAt', now()->addDay()->addHours(2)->format('Y-m-d\TH:i')) ->set('hostIds', [$host->id]) ->call('publish')->assertHasNoErrors(); $window = MaintenanceWindow::query()->where('title', 'Netz-Wartung')->first(); expect($window)->not->toBeNull()->and($window->state)->toBe('scheduled') ->and($window->hosts()->count())->toBe(1); Mail::assertQueued(MaintenanceAnnouncementMail::class, 1); expect(MaintenanceNotification::query()->where('customer_id', $customer->id)->count())->toBe(1); // The ledger's unique (window, customer, event) makes re-announcing a no-op. expect(fn () => MaintenanceNotification::query()->create([ 'maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => 'announcement', 'email' => $customer->email, ]))->toThrow(Illuminate\Database\UniqueConstraintViolationException::class); // Resending while the first mail is still pending must NOT duplicate. Livewire::actingAs(operator('Owner'))->test(Maintenance::class)->call('resend', $window->uuid); Mail::assertQueued(MaintenanceAnnouncementMail::class, 1); }); it('emails a cancellation to customers who were announced', function () { Mail::fake(); $host = Host::factory()->active()->create(); $customer = affectedCustomerOn($host); $window = MaintenanceWindow::factory()->scheduled()->create(); $window->hosts()->attach($host->id); // Simulate the customer having actually received the announcement. \App\Models\MaintenanceNotification::create([ 'maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => 'announcement', 'email' => $customer->email, 'sent_at' => now(), ]); Livewire::actingAs(operator('Owner'))->test(Maintenance::class)->call('cancel', $window->uuid); expect($window->fresh()->state)->toBe('cancelled'); Mail::assertQueued(\App\Mail\MaintenanceCancelledMail::class, 1); }); it('scopes the per-instance badge to that instance host', function () { $hostA = Host::factory()->active()->create(); $hostB = Host::factory()->active()->create(); $customer = Customer::factory()->create(); $instA = Instance::factory()->create(['customer_id' => $customer->id, 'host_id' => $hostA->id, 'status' => 'active']); $instB = Instance::factory()->create(['customer_id' => $customer->id, 'host_id' => $hostB->id, 'status' => 'active']); // A window on host A only. $window = MaintenanceWindow::factory()->scheduled()->create(); $window->hosts()->attach($hostA->id); expect(MaintenanceWindow::forInstance($instA))->toHaveCount(1) ->and(MaintenanceWindow::forInstance($instB))->toHaveCount(0) // not another instance's window ->and(MaintenanceWindow::bannerFor($customer))->toHaveCount(1); // customer-level banner still shows }); it('requires a host to publish', function () { Livewire::actingAs(operator('Owner'))->test(Maintenance::class) ->set('title', 'X') ->set('startsAt', now()->addDay()->format('Y-m-d\TH:i')) ->set('endsAt', now()->addDay()->addHours(2)->format('Y-m-d\TH:i')) ->set('hostIds', []) ->call('publish')->assertHasErrors(['hostIds']); }); it('forbids non-privileged roles from managing maintenance', function () { Livewire::actingAs(operator('Support'))->test(Maintenance::class) ->set('title', 'X')->set('startsAt', now()->addDay()->format('Y-m-d\TH:i')) ->set('endsAt', now()->addDay()->addHours(2)->format('Y-m-d\TH:i')) ->call('saveDraft')->assertForbidden(); }); it('shows the banner to an affected customer within the horizon', function () { $host = Host::factory()->active()->create(); $customer = affectedCustomerOn($host); $window = MaintenanceWindow::factory()->scheduled()->create(['starts_at' => now()->addDay(), 'ends_at' => now()->addDay()->addHour()]); $window->hosts()->attach($host->id); $banner = MaintenanceWindow::bannerFor($customer); expect($banner)->toHaveCount(1)->and($banner->first()->id)->toBe($window->id); // A customer on another host sees nothing. $other = affectedCustomerOn(Host::factory()->active()->create()); expect(MaintenanceWindow::bannerFor($other))->toHaveCount(0); });