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.
69 lines
4 KiB
PHP
69 lines
4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace App\Tests;
|
|
use App\Http\Request;
|
|
use App\Auth\ArraySessionStore;
|
|
use App\Geometry\Polygon;
|
|
use App\Dashboard\ZoneConfigController;
|
|
use App\Tests\Support\{InMemoryZoneRepository, InMemoryAuditRepository};
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ZoneConfigTest 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 testPolygonValidation(): void {
|
|
$this->assertTrue(Polygon::validate([[0,0],[10,0],[10,10]]));
|
|
$this->assertFalse(Polygon::validate([[0,0],[10,0]])); // <3
|
|
$this->assertFalse(Polygon::validate([[0,0],[10,0],['x',10]])); // non-numeric
|
|
}
|
|
private function saveReq(array $body, array $headers = ['X-Csrf-Token' => 'tok']): Request {
|
|
return new Request('POST', '/api/zones', $headers, json_encode($body), [], []);
|
|
}
|
|
public function testSaveRequiresPermission(): void {
|
|
$c = new ZoneConfigController(new InMemoryZoneRepository(), new InMemoryAuditRepository(), $this->authed('viewer'));
|
|
$res = $c->save($this->saveReq(['camera_id' => 'cam-1', 'level' => 'restricted', 'polygon' => [[0,0],[9,0],[9,9]]]));
|
|
$this->assertSame(403, $res->status());
|
|
}
|
|
public function testSaveValidZone(): void {
|
|
$repo = new InMemoryZoneRepository();
|
|
$c = new ZoneConfigController($repo, new InMemoryAuditRepository(), $this->authed('operator'));
|
|
$res = $c->save($this->saveReq(['device_id' => 'pi-1', 'camera_id' => 'cam-1', 'level' => 'restricted', 'polygon' => [[0,0],[9,0],[9,9]]]));
|
|
$this->assertSame(201, $res->status());
|
|
$this->assertCount(1, $repo->forCamera('cam-1'));
|
|
}
|
|
public function testSaveRejectsBadLevelOrPolygon(): void {
|
|
$c = new ZoneConfigController(new InMemoryZoneRepository(), new InMemoryAuditRepository(), $this->authed('operator'));
|
|
$this->assertSame(400, $c->save($this->saveReq(['camera_id' => 'cam-1', 'level' => 'BOOM', 'polygon' => [[0,0],[9,0],[9,9]]]))->status());
|
|
$this->assertSame(400, $c->save($this->saveReq(['camera_id' => 'cam-1', 'level' => 'warning', 'polygon' => [[0,0]]]))->status());
|
|
}
|
|
public function testSaveRejectsMissingOrWrongCsrf(): void {
|
|
$repo = new InMemoryZoneRepository();
|
|
$c = new ZoneConfigController($repo, new InMemoryAuditRepository(), $this->authed('operator'));
|
|
$body = ['device_id' => 'pi-1', 'camera_id' => 'cam-1', 'level' => 'restricted', 'polygon' => [[0,0],[9,0],[9,9]]];
|
|
// missing header
|
|
$this->assertSame(403, $c->save($this->saveReq($body, []))->status());
|
|
// wrong token
|
|
$this->assertSame(403, $c->save($this->saveReq($body, ['X-Csrf-Token' => 'nope']))->status());
|
|
// save did not proceed
|
|
$this->assertCount(0, $repo->forCamera('cam-1'));
|
|
}
|
|
public function testDeleteRejectsMissingOrWrongCsrf(): void {
|
|
$repo = new InMemoryZoneRepository();
|
|
$repo->save(['zone_id' => 'z1', 'device_id' => 'pi-1', 'camera_id' => 'cam-1', 'level' => 'restricted', 'polygon_json' => '[[0,0],[9,0],[9,9]]']);
|
|
$c = new ZoneConfigController($repo, new InMemoryAuditRepository(), $this->authed('operator'));
|
|
// missing header
|
|
$missing = new Request('POST', '/api/zones/z1/delete', [], '', [], []);
|
|
$this->assertSame(403, $c->delete($missing, ['id' => 'z1'])->status());
|
|
// wrong token
|
|
$wrong = new Request('POST', '/api/zones/z1/delete', ['X-Csrf-Token' => 'nope'], '', [], []);
|
|
$this->assertSame(403, $c->delete($wrong, ['id' => 'z1'])->status());
|
|
// delete did not proceed
|
|
$this->assertCount(1, $repo->forCamera('cam-1'));
|
|
// valid token proceeds
|
|
$ok = new Request('POST', '/api/zones/z1/delete', ['X-Csrf-Token' => 'tok'], '', [], []);
|
|
$this->assertSame(200, $c->delete($ok, ['id' => 'z1'])->status());
|
|
$this->assertCount(0, $repo->forCamera('cam-1'));
|
|
}
|
|
}
|