move_detect/cloud/tests/ZoneConfigTest.php
유창욱 3405c1e2cc fix(cloud): enforce CSRF on authenticated state-changing POSTs (M1)
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.
2026-07-12 09:16:38 +09:00

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'));
}
}