create(['must_change_password' => false]); } private function operator(): User { return User::factory()->operator()->create(['must_change_password' => false]); } private function viewer(): User { return User::factory()->viewer()->create(['must_change_password' => false]); } private function server(string $name = 'box', string $ip = '10.0.0.1'): Server { $s = Server::create(['name' => $name, 'ip' => $ip, 'ssh_port' => 22, 'status' => 'online']); $s->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']); return $s; } // ── route gating (operate) ────────────────────────────────────────────────── public function test_viewer_cannot_open_commands(): void { $this->actingAs($this->viewer())->get('/commands')->assertForbidden(); } public function test_operator_can_open_commands(): void { $this->actingAs($this->operator())->get('/commands')->assertOk(); } // ── ad-hoc run through the confirm token ──────────────────────────────────── public function test_run_opens_a_confirm_for_the_selected_targets(): void { $this->actingAs($this->operator()); $this->server(); Livewire::test(Index::class) ->set('command', 'uptime') ->set('scopeType', 'all') ->call('run') ->assertHasNoErrors() ->assertDispatched('openModal'); } public function test_run_requires_a_command(): void { $this->actingAs($this->operator()); $this->server(); Livewire::test(Index::class)->set('command', ' ')->call('run')->assertHasErrors('command'); } public function test_run_requires_at_least_one_target(): void { $this->actingAs($this->operator()); // no servers created → empty target Livewire::test(Index::class)->set('command', 'uptime')->set('scopeType', 'all')->call('run')->assertHasErrors('command'); } public function test_execute_runs_the_command_over_the_sealed_targets(): void { $this->actingAs($this->operator()); $server = $this->server(); $runner = Mockery::mock(CommandRunner::class); $runner->shouldReceive('run')->once() ->with('uptime', Mockery::on(fn ($servers) => $servers->pluck('id')->all() === [$server->id])) ->andReturn([['server' => 'box', 'ok' => true, 'output' => 'up 3 days']]); app()->instance(CommandRunner::class, $runner); $token = ConfirmToken::issue('commandRun', ['command' => 'uptime', 'serverIds' => [$server->id]], 'command.run', 'uptime · 1', null); ConfirmToken::confirm($token); Livewire::test(Index::class) ->call('execute', $token) ->assertOk() ->assertSet('results.0.output', 'up 3 days'); } public function test_a_forged_run_token_executes_nothing(): void { $this->actingAs($this->operator()); $runner = Mockery::mock(CommandRunner::class); $runner->shouldReceive('run')->never(); app()->instance(CommandRunner::class, $runner); Livewire::test(Index::class)->call('execute', 'not-a-token')->assertSet('results', []); } // (A viewer cannot even mount the component — the route + mount operate-gate blocks the whole // page, proven by test_viewer_cannot_open_commands; every mutating method re-gates as defence.) // ── runbooks ──────────────────────────────────────────────────────────────── public function test_operator_creates_a_runbook_and_it_is_audited(): void { $this->actingAs($this->operator()); Livewire::test(Index::class) ->set('runbookName', 'Restart nginx') ->set('runbookCommand', 'systemctl restart nginx') ->call('createRunbook') ->assertHasNoErrors(); $this->assertDatabaseHas('runbooks', ['name' => 'Restart nginx']); $this->assertDatabaseHas('audit_events', ['action' => 'runbook.create', 'target' => 'Restart nginx']); } public function test_runbook_run_opens_a_confirm_with_the_saved_command(): void { $this->actingAs($this->operator()); $this->server(); $rb = Runbook::create(['name' => 'up', 'command' => 'uptime']); Livewire::test(Index::class) ->set('scopeType', 'all') ->call('runRunbook', $rb->uuid) ->assertDispatched('openModal'); } public function test_runbook_delete_via_a_confirmed_token(): void { $this->actingAs($this->operator()); $rb = Runbook::create(['name' => 'gone', 'command' => 'x']); $token = ConfirmToken::issue('runbookDeleted', ['uuid' => $rb->uuid], 'runbook.delete', $rb->name, null); ConfirmToken::confirm($token); Livewire::test(Index::class)->call('deleteRunbook', $token)->assertOk(); $this->assertDatabaseMissing('runbooks', ['id' => $rb->id]); } public function test_group_scope_only_targets_group_members(): void { $this->actingAs($this->operator()); $group = ServerGroup::create(['name' => 'web', 'color' => 'cyan']); $member = $this->server('web1', '10.0.0.1'); $this->server('db1', '10.0.0.2'); // not in the group $group->servers()->attach($member->id); $runner = Mockery::mock(CommandRunner::class); $runner->shouldReceive('run')->once() ->with('hostname', Mockery::on(fn ($servers) => $servers->pluck('id')->all() === [$member->id])) ->andReturn([]); app()->instance(CommandRunner::class, $runner); // Issue a token as the component would for a group scope, then execute. $token = ConfirmToken::issue('commandRun', ['command' => 'hostname', 'serverIds' => [$member->id]], 'command.run', 'x', null); ConfirmToken::confirm($token); Livewire::test(Index::class)->call('execute', $token)->assertOk(); } }