Only SameSite=Lax protected the acknowledge and zone save/delete endpoints, even though a Csrf helper and mintable token already existed. Add GET /api/csrf (CsrfController::token) to mint/return a per-session token, check it (auth -> rbac -> csrf -> mutate) in EventDetailController::acknowledge and ZoneConfigController::save/delete, and wire app.js/zone-draw.js to fetch the token on load and send it as X-Csrf-Token on every POST.
60 lines
3.5 KiB
PHP
60 lines
3.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace App\Tests;
|
|
use App\Http\Request;
|
|
use App\Auth\ArraySessionStore;
|
|
use App\Dashboard\{EventListController, EventDetailController, SnapshotProxy};
|
|
use App\Tests\Support\{InMemoryEventRepository, InMemorySnapshotRepository, InMemoryAuditRepository};
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class DashboardTest extends TestCase
|
|
{
|
|
private function authed(string $role = 'operator'): ArraySessionStore {
|
|
$s = new ArraySessionStore(); $s->set('user_id', 1); $s->set('username', 'u'); $s->set('role', $role); $s->set('csrf', 'tok'); return $s;
|
|
}
|
|
public function testListRequiresAuth(): void {
|
|
$repo = new InMemoryEventRepository();
|
|
$c = new EventListController($repo, new ArraySessionStore());
|
|
$this->assertSame(401, $c->json(new Request('GET', '/api/events', [], '', [], []))->status());
|
|
}
|
|
public function testListReturnsRecent(): void {
|
|
$repo = new InMemoryEventRepository();
|
|
$repo->insert(['severity' => 'high', 'event_type' => 'fall']);
|
|
$c = new EventListController($repo, $this->authed());
|
|
$res = $c->json(new Request('GET', '/api/events', [], '', [], []));
|
|
$this->assertSame(200, $res->status());
|
|
$this->assertCount(1, json_decode($res->body(), true)['events']);
|
|
}
|
|
public function testAcknowledgeRequiresPermissionAndSetsUser(): void {
|
|
$repo = new InMemoryEventRepository();
|
|
$id = $repo->insert(['severity' => 'critical', 'event_type' => 'no_motion']);
|
|
$req = new Request('POST', "/events/$id/ack", ['X-Csrf-Token' => 'tok'], '', [], []);
|
|
// viewer denied (RBAC check fires before CSRF)
|
|
$cv = new EventDetailController($repo, new InMemorySnapshotRepository(), new InMemoryAuditRepository(), $this->authed('viewer'));
|
|
$this->assertSame(403, $cv->acknowledge($req, ['id' => (string)$id])->status());
|
|
// operator allowed
|
|
$co = new EventDetailController($repo, new InMemorySnapshotRepository(), new InMemoryAuditRepository(), $this->authed('operator'));
|
|
$this->assertSame(200, $co->acknowledge($req, ['id' => (string)$id])->status());
|
|
$this->assertSame('u', $repo->find($id)['acknowledged_by']);
|
|
}
|
|
public function testAcknowledgeRejectsMissingOrWrongCsrfToken(): void {
|
|
$repo = new InMemoryEventRepository();
|
|
$id = $repo->insert(['severity' => 'critical', 'event_type' => 'no_motion']);
|
|
$c = new EventDetailController($repo, new InMemorySnapshotRepository(), new InMemoryAuditRepository(), $this->authed('operator'));
|
|
// missing header
|
|
$missing = new Request('POST', "/events/$id/ack", [], '', [], []);
|
|
$this->assertSame(403, $c->acknowledge($missing, ['id' => (string)$id])->status());
|
|
// wrong token
|
|
$wrong = new Request('POST', "/events/$id/ack", ['X-Csrf-Token' => 'nope'], '', [], []);
|
|
$this->assertSame(403, $c->acknowledge($wrong, ['id' => (string)$id])->status());
|
|
// acknowledge did not proceed
|
|
$this->assertNull($repo->find($id)['acknowledged_by']);
|
|
}
|
|
public function testSnapshotProxyAuthAnd404(): void {
|
|
$snaps = new InMemorySnapshotRepository();
|
|
$proxy = new SnapshotProxy($snaps, new ArraySessionStore()); // unauth
|
|
$this->assertSame(401, $proxy->serve(new Request('GET', '/snap/x', [], '', [], []), ['uuid' => 'x'])->status());
|
|
$proxy2 = new SnapshotProxy($snaps, $this->authed());
|
|
$this->assertSame(404, $proxy2->serve(new Request('GET', '/snap/missing', [], '', [], []), ['uuid' => 'missing'])->status());
|
|
}
|
|
}
|