CluPilotCloud/tests/Feature/Admin/UpdateWindowTest.php

177 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
use App\Livewire\Admin\Settings as AdminSettings;
use App\Services\Deployment\UpdateChannel;
use App\Services\Deployment\UpdateWindow;
use App\Support\Settings as AppSettings;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\File;
use Livewire\Livewire;
/**
* Updating on a schedule instead of on a click.
*
* The button does not go away — manual stays possible at any hour. This is the
* option beside it, and the tests are about the two things it must never do:
* run outside the window it was given, and do anything at all while it is
* switched off.
*/
beforeEach(function () {
File::deleteDirectory(storage_path('app/deploy'));
});
function windowStatus(): void
{
File::ensureDirectoryExists(storage_path('app/deploy'));
File::put(storage_path('app/deploy/update-status.json'), json_encode([
'state' => 'idle',
'checked_at' => now()->toIso8601String(),
'behind' => 1,
]));
}
function openWindow(array $days, string $from = '05:00', string $to = '06:00'): void
{
AppSettings::set(UpdateWindow::KEY_ENABLED, true);
AppSettings::set(UpdateWindow::KEY_DAYS, $days);
AppSettings::set(UpdateWindow::KEY_FROM, $from);
AppSettings::set(UpdateWindow::KEY_TO, $to);
}
it('does nothing at all while the automation is off', function () {
// The switch has to mean it. An installation whose owner deliberately
// updates by hand must never find itself in maintenance mode at dawn.
windowStatus();
AppSettings::set(UpdateWindow::KEY_ENABLED, false);
$this->artisan('clupilot:auto-update')->assertSuccessful();
expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse();
});
it('takes an available release inside the window', function () {
// Wednesday, 05:30 local.
Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC'));
windowStatus();
openWindow([3]);
expect(app(UpdateWindow::class)->isOpen())->toBeTrue();
$this->artisan('clupilot:auto-update')->assertSuccessful();
expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeTrue();
});
it('leaves it alone outside the hours', function () {
// Wednesday, 11:30 local — the right day, the wrong time.
Carbon::setTestNow(Carbon::parse('2026-07-29 09:30:00', 'UTC'));
windowStatus();
openWindow([3]);
expect(app(UpdateWindow::class)->isOpen())->toBeFalse();
$this->artisan('clupilot:auto-update');
expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse();
});
it('leaves it alone on a day that was not chosen', function () {
Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC')); // Wednesday
windowStatus();
openWindow([1, 2]); // Mon, Tue
expect(app(UpdateWindow::class)->isOpen())->toBeFalse();
});
it('understands a window that runs past midnight', function () {
// 23:0002:00 is a perfectly reasonable thing to ask for, and reading it
// as an empty range would silently mean "never".
$window = app(UpdateWindow::class);
// Wednesday 23:30 local — inside, on the chosen day.
openWindow([3], '23:00', '02:00');
Carbon::setTestNow(Carbon::parse('2026-07-29 21:30:00', 'UTC'));
expect($window->isOpen())->toBeTrue();
// Thursday 01:30 local — still the tail of Wednesday's window.
Carbon::setTestNow(Carbon::parse('2026-07-29 23:30:00', 'UTC'));
expect($window->isOpen())->toBeTrue();
// Thursday 03:30 local — over.
Carbon::setTestNow(Carbon::parse('2026-07-30 01:30:00', 'UTC'));
expect($window->isOpen())->toBeFalse();
});
it('finds nothing to do when the operator already updated by hand', function () {
// Named by the owner as the case that must not surprise them: pressed at
// four, window at five. Nothing guards against it explicitly — an
// installation that is current simply has nothing available.
Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC'));
File::ensureDirectoryExists(storage_path('app/deploy'));
File::put(storage_path('app/deploy/update-status.json'), json_encode([
'state' => 'idle',
'checked_at' => now()->toIso8601String(),
'behind' => 0,
]));
openWindow([3]);
expect(app(UpdateChannel::class)->state()['available'])->toBeFalse();
$this->artisan('clupilot:auto-update');
expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse();
});
it('does not queue a second run over one already in flight', function () {
Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC'));
windowStatus();
openWindow([3]);
app(UpdateChannel::class)->request('owner@example.com');
$before = File::get(storage_path('app/deploy/update-request.json'));
$this->artisan('clupilot:auto-update');
expect(File::get(storage_path('app/deploy/update-request.json')))->toBe($before);
});
it('will not save an automation that could never run', function () {
// Switched on with no day selected is a configuration that reads as working
// and does nothing. Refused rather than silently corrected.
Livewire::actingAs(operator('Owner'), 'operator')
->test(AdminSettings::class)
->set('autoUpdate', true)
->set('autoDays', [])
->set('autoFrom', '05:00')
->set('autoTo', '06:00')
->call('saveUpdateWindow')
->assertHasErrors('autoDays');
expect(app(UpdateWindow::class)->enabled())->toBeFalse();
});
it('saves a window the owner can see again afterwards', function () {
Livewire::actingAs(operator('Owner'), 'operator')
->test(AdminSettings::class)
->set('autoUpdate', true)
->set('autoDays', [2, 4])
->set('autoFrom', '04:30')
->set('autoTo', '05:30')
->call('saveUpdateWindow')
->assertHasNoErrors();
$window = app(UpdateWindow::class);
expect($window->enabled())->toBeTrue()
->and($window->days())->toBe([2, 4])
->and($window->from())->toBe('04:30')
->and($window->to())->toBe('05:30');
});
it('needs the capability to change when the installation updates itself', function () {
Livewire::actingAs(operator('Support'), 'operator')
->test(AdminSettings::class)
->call('saveUpdateWindow')
->assertForbidden();
});