set('user_id', 1); if ($site !== null) $s->set('site_scope', $site); return $s; } private function repo(): InMemoryEventRepository { $r = new InMemoryEventRepository(); $base = ['device_id' => 'pi-1', 'event_type' => 'near_fall', 'severity' => 'info', 'metrics' => [], 'occurred_at' => (float)time()]; // array_merge: the per-row override must WIN over $base (the `+` operator keeps the left // side's keys, which silently discarded the old-row occurred_at override). $r->insert(array_merge($base, ['camera_id' => 'cam-1', 'site' => 'a', 'spatial_x' => 0.4, 'spatial_y' => 0.6])); $r->insert(array_merge($base, ['camera_id' => 'cam-1', 'site' => 'b', 'spatial_x' => 0.1, 'spatial_y' => 0.1])); $r->insert(array_merge($base, ['camera_id' => 'cam-2', 'site' => 'a', 'spatial_x' => 0.9, 'spatial_y' => 0.9])); $r->insert(array_merge($base, ['camera_id' => 'cam-1', 'site' => 'a'])); // no spatial $r->insert(array_merge($base, ['camera_id' => 'cam-1', 'site' => 'a', 'spatial_x' => 0.5, 'spatial_y' => 0.5, 'occurred_at' => (float)(time() - 400 * 86400)])); // too old return $r; } private function req(string $days = ''): Request { $q = $days === '' ? [] : ['days' => $days]; return new Request('GET', '/api/heatmap/cam-1', [], '', $q, []); } public function testRequiresLogin(): void { $c = new HeatmapController($this->repo(), new ArraySessionStore()); $this->assertSame(401, $c->json($this->req(), ['camera' => 'cam-1'])->status()); } public function testReturnsCameraPointsWithinWindow(): void { $c = new HeatmapController($this->repo(), $this->sess()); $res = $c->json($this->req(), ['camera' => 'cam-1']); $this->assertSame(200, $res->status()); $data = json_decode($res->body(), true); $this->assertSame(30, $data['days']); // cam-2's point, the spatial-less row, and the 400-day-old row are all excluded. $this->assertCount(2, $data['points']); foreach ($data['points'] as $p) { $this->assertArrayHasKey('x', $p); $this->assertArrayHasKey('y', $p); $this->assertSame('near_fall', $p['event_type']); } } public function testSiteScopeFiltersPoints(): void { $c = new HeatmapController($this->repo(), $this->sess('a')); $data = json_decode($c->json($this->req(), ['camera' => 'cam-1'])->body(), true); $this->assertCount(1, $data['points']); // site-b point excluded $this->assertSame(0.4, $data['points'][0]['x']); } public function testDaysClamped(): void { $c = new HeatmapController($this->repo(), $this->sess()); $this->assertSame(90, json_decode($c->json($this->req('9999'), ['camera' => 'cam-1'])->body(), true)['days']); $this->assertSame(30, json_decode($c->json($this->req('abc'), ['camera' => 'cam-1'])->body(), true)['days']); } public function testMissingCameraIs400(): void { $c = new HeatmapController($this->repo(), $this->sess()); $this->assertSame(400, $c->json($this->req(), [])->status()); } }