177 lines
6.2 KiB
PHP
177 lines
6.2 KiB
PHP
<?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:00–02: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();
|
||
});
|