markTestSkipped('pdo_sqlite not available on this host'); } $this->pdo = new PDO('sqlite::memory:', null, null, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); $this->pdo->exec(self::sqliteSchema()); } public function testNonceRoundTrip(): void { $repo = new PdoNonceRepository($this->pdo); $this->assertFalse($repo->seen('n1')); $repo->remember('n1', 9999999999); $this->assertTrue($repo->seen('n1')); } public function testEventInsertFindAcknowledge(): void { $repo = new PdoEventRepository($this->pdo); $uuid = str_repeat('a', 32); $id = $repo->insert([ 'event_uuid' => $uuid, 'device_id' => 'dev-1', 'site' => 'factory-a', 'camera_id' => 'cam-1', 'event_type' => 'fall', 'severity' => 'critical', 'track_id' => 7, 'zone_id' => null, 'occurred_at' => 1700000000.5, 'duration_s' => 2.5, 'metrics' => ['confidence' => 0.9], ]); $this->assertGreaterThan(0, $id); $found = $repo->find($id); $this->assertNotNull($found); $this->assertSame('dev-1', $found['device_id']); $this->assertSame('factory-a', $found['site']); $this->assertSame($uuid, $found['event_uuid']); $this->assertSame('critical', $found['severity']); $this->assertSame(['confidence' => 0.9], $found['metrics']); $this->assertNull($found['acknowledged_by']); // Idempotency key lookup (P1-9). $this->assertSame($id, $repo->idByUuid($uuid)); $this->assertNull($repo->idByUuid(str_repeat('z', 32))); // Site-scoped page (P3-2b): matches its own site, excluded from another. $this->assertCount(1, $repo->page(10, null, null, null, 'factory-a')); $this->assertCount(0, $repo->page(10, null, null, null, 'factory-b')); $recent = $repo->recent(10, 'critical'); $this->assertCount(1, $recent); $this->assertSame([], $repo->recent(10, 'info')); $this->assertTrue($repo->acknowledge($id, 'alice')); $this->assertSame('alice', $repo->find($id)['acknowledged_by']); $this->assertFalse($repo->acknowledge(999999, 'bob')); } public function testZoneSaveIsOwnershipScoped(): void { $repo = new PdoZoneRepository($this->pdo); $zoneId = $repo->save([ 'zone_id' => null, 'device_id' => 'dev-1', 'camera_id' => 'cam-1', 'level' => 'warning', 'polygon_json' => '[[0,0],[1,0],[1,1]]', ]); $this->assertNotSame('', $zoneId); $this->assertCount(1, $repo->forCamera('cam-1')); // Legitimate update by the owning device/camera succeeds. $again = $repo->save([ 'zone_id' => $zoneId, 'device_id' => 'dev-1', 'camera_id' => 'cam-1', 'level' => 'restricted', 'polygon_json' => '[[0,0],[2,0],[2,2]]', ]); $this->assertSame($zoneId, $again); // Supplying the same zone_id under a different device/camera must not // silently overwrite it -- this is the cross-tenant guard from the // Task 13 review. $this->expectException(RuntimeException::class); $repo->save([ 'zone_id' => $zoneId, 'device_id' => 'dev-2', 'camera_id' => 'cam-2', 'level' => 'pinch', 'polygon_json' => '[[9,9],[9,8],[8,8]]', ]); } public function testZoneDelete(): void { $repo = new PdoZoneRepository($this->pdo); $zoneId = $repo->save([ 'zone_id' => null, 'device_id' => 'dev-1', 'camera_id' => 'cam-1', 'level' => 'warning', 'polygon_json' => '[[0,0],[1,0],[1,1]]', ]); $this->assertTrue($repo->delete($zoneId)); $this->assertFalse($repo->delete($zoneId)); } /** * Minimal sqlite-flavored translation of the nonces/events/zones tables * in ../schema.sql (see that file's "sqlite compatibility notes" header * for the general MySQL -> sqlite mapping rules applied here). */ private static function sqliteSchema(): string { return <<<'SQL' CREATE TABLE move_nonces ( nonce TEXT PRIMARY KEY, expires_at INTEGER NOT NULL ); CREATE TABLE move_events ( event_id INTEGER PRIMARY KEY AUTOINCREMENT, event_uuid TEXT UNIQUE, device_id TEXT NOT NULL, site TEXT, camera_id TEXT NOT NULL, event_type TEXT NOT NULL, severity TEXT NOT NULL, track_id INTEGER, zone_id TEXT, occurred_at REAL NOT NULL, duration_s REAL NOT NULL DEFAULT 0, metrics_json TEXT, spatial_x REAL, spatial_y REAL, acknowledged_by TEXT, acknowledged_at INTEGER, false_positive_by TEXT, false_positive_at INTEGER, received_at INTEGER NOT NULL ); CREATE TABLE move_zones ( zone_id TEXT PRIMARY KEY, device_id TEXT NOT NULL, camera_id TEXT NOT NULL, level TEXT NOT NULL, polygon_json TEXT NOT NULL, rules_json TEXT, updated_at INTEGER NOT NULL ); SQL; } }