feat(cloud): event payload validator with enum whitelist
This commit is contained in:
parent
4bbd28f95f
commit
b5388b2a68
2 changed files with 74 additions and 0 deletions
40
cloud/src/Ingest/EventValidator.php
Normal file
40
cloud/src/Ingest/EventValidator.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace App\Ingest;
|
||||
|
||||
final class EventValidator
|
||||
{
|
||||
private const TYPES = ['fall','no_motion','immobility','ghost','zone_warning',
|
||||
'zone_restricted','pinch','monitoring_degraded','device_fault'];
|
||||
private const SEVERITIES = ['critical','high','warning','info'];
|
||||
private const REQUIRED = ['device_id','camera_id','event_type','severity','occurred_at'];
|
||||
|
||||
public static function validate(array $p): array
|
||||
{
|
||||
$errors = [];
|
||||
foreach (self::REQUIRED as $k) {
|
||||
if (!array_key_exists($k, $p) || $p[$k] === '' || $p[$k] === null) $errors[] = "missing:$k";
|
||||
}
|
||||
if (isset($p['event_type']) && !in_array($p['event_type'], self::TYPES, true)) $errors[] = 'bad:event_type';
|
||||
if (isset($p['severity']) && !in_array($p['severity'], self::SEVERITIES, true)) $errors[] = 'bad:severity';
|
||||
$snaps = $p['snapshots'] ?? [];
|
||||
if (!is_array($snaps)) $errors[] = 'bad:snapshots';
|
||||
elseif (count($snaps) > 3) $errors[] = 'too_many:snapshots';
|
||||
else foreach ($snaps as $s) if (!is_string($s)) { $errors[] = 'bad:snapshot_item'; break; }
|
||||
|
||||
if ($errors) return ['ok' => false, 'errors' => $errors, 'clean' => []];
|
||||
|
||||
$clean = [
|
||||
'device_id' => (string)$p['device_id'],
|
||||
'camera_id' => (string)$p['camera_id'],
|
||||
'event_type' => (string)$p['event_type'],
|
||||
'severity' => (string)$p['severity'],
|
||||
'track_id' => isset($p['track_id']) ? (int)$p['track_id'] : null,
|
||||
'zone_id' => isset($p['zone_id']) ? (string)$p['zone_id'] : null,
|
||||
'occurred_at' => (float)$p['occurred_at'],
|
||||
'duration_s' => (float)($p['duration_s'] ?? 0.0),
|
||||
'metrics' => is_array($p['metrics'] ?? null) ? $p['metrics'] : [],
|
||||
];
|
||||
return ['ok' => true, 'errors' => [], 'clean' => $clean];
|
||||
}
|
||||
}
|
||||
34
cloud/tests/EventValidatorTest.php
Normal file
34
cloud/tests/EventValidatorTest.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace App\Tests;
|
||||
use App\Ingest\EventValidator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class EventValidatorTest extends TestCase
|
||||
{
|
||||
private function valid(): array {
|
||||
return ['device_id' => 'pi-1', 'camera_id' => 'cam-1', 'event_type' => 'fall',
|
||||
'severity' => 'high', 'occurred_at' => 1000.0, 'snapshots' => ['aGk=']];
|
||||
}
|
||||
public function testAcceptsValid(): void {
|
||||
$r = EventValidator::validate($this->valid());
|
||||
$this->assertTrue($r['ok'], implode(',', $r['errors']));
|
||||
$this->assertSame('fall', $r['clean']['event_type']);
|
||||
}
|
||||
public function testRejectsUnknownEventType(): void {
|
||||
$p = $this->valid(); $p['event_type'] = 'explode';
|
||||
$this->assertFalse(EventValidator::validate($p)['ok']);
|
||||
}
|
||||
public function testRejectsUnknownSeverity(): void {
|
||||
$p = $this->valid(); $p['severity'] = 'ULTRA';
|
||||
$this->assertFalse(EventValidator::validate($p)['ok']);
|
||||
}
|
||||
public function testRejectsMissingRequired(): void {
|
||||
$p = $this->valid(); unset($p['device_id']);
|
||||
$this->assertFalse(EventValidator::validate($p)['ok']);
|
||||
}
|
||||
public function testRejectsTooManySnapshots(): void {
|
||||
$p = $this->valid(); $p['snapshots'] = ['a', 'b', 'c', 'd'];
|
||||
$this->assertFalse(EventValidator::validate($p)['ok']);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue