Five components that ship as one product:
- behavision/ the recognition engine. RTSP ingest, YuNet detection, IoU
tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
FastAPI dashboard. Identity is decided once per TRACK from an
average of at least three embeddings, never per frame.
- agent/ the Go edge agent: supervises the engine, holds a durable
spool, and drains it to MQTT. Nothing is acked before the
broker confirms.
- desktop/ the shop PC application (Wails + React + tray).
- server/ the cloud API, MQTT consumer, reports and assistant.
- web/ platform.loyaly.ai, the head-office app, embedded in the
server binary.
The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.
CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
105 lines
3.3 KiB
Go
105 lines
3.3 KiB
Go
package mqtt
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPlaintextToAPublicHostIsRefused(t *testing.T) {
|
|
// The payloads carry customer visit records and the connection carries the
|
|
// tenant's broker password. A tcp:// URL to a public host is not a config
|
|
// choice, it is a mistake — and one that WORKS, which is exactly why it
|
|
// has to fail here rather than be noticed after a year of traffic.
|
|
for _, url := range []string{
|
|
"tcp://broker.example.com:1883",
|
|
"mqtt://66.116.226.234:1883",
|
|
"tcp://10.0.0.5:1883",
|
|
"tcp://[2001:db8::1]:1883",
|
|
} {
|
|
if _, err := NewClient(ClientOptions{BrokerURL: url}); err == nil ||
|
|
!strings.Contains(err.Error(), "refusing plaintext") {
|
|
t.Errorf("%s was not refused (err=%v)", url, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlaintextToLocalhostIsAllowed(t *testing.T) {
|
|
// Local testing against a Mosquitto on the same box crosses no network.
|
|
// Checked at the transport gate rather than through NewClient: dialling a
|
|
// port nothing is listening on burns the full 20s connect timeout, and a
|
|
// slow test is a test people start skipping.
|
|
for _, url := range []string{"tcp://127.0.0.1:1883", "tcp://localhost:1883",
|
|
"mqtt://[::1]:1883"} {
|
|
if err := checkTransport(url); err != nil {
|
|
t.Errorf("loopback %s was refused: %v", url, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlaintextEscapeHatchIsExplicit(t *testing.T) {
|
|
// An override must exist for a lab, but it has to be a deliberate act,
|
|
// not a config field someone leaves set.
|
|
t.Setenv("BEHAVISION_ALLOW_PLAINTEXT_MQTT", "1")
|
|
if err := checkTransport("tcp://broker.example.com:1883"); err != nil {
|
|
t.Fatalf("escape hatch did not apply: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTLSUrlsSkipTheTransportCheck(t *testing.T) {
|
|
for _, url := range []string{"tls://b:8883", "ssl://b:8883", "wss://b:443"} {
|
|
if err := checkTransport(url); err != nil {
|
|
t.Errorf("%s rejected: %v", url, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnEmptyBrokerUrlIsAnError(t *testing.T) {
|
|
if _, err := NewClient(ClientOptions{}); err == nil {
|
|
t.Fatal("empty broker url accepted")
|
|
}
|
|
}
|
|
|
|
func TestTLSConfigRejectsAnUnreadableCA(t *testing.T) {
|
|
// Silently falling back to system roots when a pinned CA is missing would
|
|
// quietly undo the pinning.
|
|
if _, err := tlsConfig(ClientOptions{CAFile: "/nonexistent/ca.pem"}); err == nil {
|
|
t.Fatal("missing CA file accepted")
|
|
}
|
|
}
|
|
|
|
func TestTLSConfigRejectsAFileWithNoCertificates(t *testing.T) {
|
|
f := t.TempDir() + "/not-a-cert.pem"
|
|
if err := writeFile(f, "hello"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tlsConfig(ClientOptions{CAFile: f}); err == nil {
|
|
t.Fatal("a file with no PEM certificates was accepted as a CA")
|
|
}
|
|
}
|
|
|
|
func TestTLSFloorIsTLS12(t *testing.T) {
|
|
cfg, err := tlsConfig(ClientOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.MinVersion < 0x0303 {
|
|
t.Fatalf("MinVersion %#x allows TLS below 1.2", cfg.MinVersion)
|
|
}
|
|
}
|
|
|
|
func TestPublishOnADeadClientErrorsRatherThanPanics(t *testing.T) {
|
|
// The pump calls this on every tick; a nil-client panic would take the
|
|
// whole agent down instead of backing off.
|
|
c := &Client{}
|
|
if err := c.Publish(nil, "t", []byte("{}")); err == nil { //nolint:staticcheck
|
|
t.Fatal("publish on an unconnected client reported success")
|
|
}
|
|
if c.Connected() {
|
|
t.Fatal("an unconnected client reported Connected")
|
|
}
|
|
}
|
|
|
|
func writeFile(path, content string) error {
|
|
return osWriteFile(path, []byte(content), 0o600)
|
|
}
|