Files
Behavision/server/internal/blob/blob_test.go
Suriyakumarvijayanayagam dad04e8cda Behavision: face recognition for retail, edge to head office
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
2026-09-04 11:14:18 +05:30

117 lines
3.7 KiB
Go

package blob
import (
"strings"
"testing"
"time"
)
func testStore(t *testing.T) *Store {
t.Helper()
s, err := New(Config{
Region: "sgp1", Endpoint: "sgp1.example.com", Bucket: "b",
AccessKey: "AK", SecretKey: "SK", Prefix: "behavision",
})
if err != nil {
t.Fatal(err)
}
return s
}
func TestNewNamesTheMissingSetting(t *testing.T) {
_, err := New(Config{Region: "sgp1"})
if err == nil {
t.Fatal("an empty config was accepted")
}
// A deploy fails at 9am in a shop, not at a keyboard, so the error has to
// say which variable is missing.
if !strings.Contains(err.Error(), "is missing") {
t.Fatalf("unhelpful error: %v", err)
}
}
// The server builds keys from the credential the request authenticated with,
// so a site cannot write into another site's prefix. This is the property that
// lets uploads be presigned instead of handing shop PCs a bucket key.
func TestKeyIsNamespacedAndCannotEscapeItsPrefix(t *testing.T) {
s := testStore(t)
at := time.Date(2026, 8, 31, 12, 0, 0, 0, time.UTC)
got := s.Key("acme", "store1", "aaaa-bbbb", at)
want := "behavision/acme/store1/2026/08/31/aaaa-bbbb.jpg"
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
// Slugs are constrained by a CHECK and visit ids are uuids, so this should
// never fire - which is why it is cheap to keep. One "../" reaching a key
// builder is a cross-tenant overwrite.
evil := s.Key("../../rival", "../store9", "../../../etc/passwd", at)
if strings.Contains(evil, "..") || strings.Contains(evil, "/etc/") {
t.Fatalf("a key escaped its prefix: %q", evil)
}
if !strings.HasPrefix(evil, "behavision/") {
t.Fatalf("key left the deployment prefix: %q", evil)
}
}
// The agent sends back the key it was given and a buggy or compromised one
// could send any string. Without this the server would presign reads for
// arbitrary objects in a bucket it shares with another application.
func TestOwnsKeyRejectsAnythingOutsideThePrefix(t *testing.T) {
s := testStore(t)
for _, key := range []string{
"", "Profile/93/user_profile-28.jpg", "behavision/../Profile/x.jpg",
"behavision//x.jpg", "other/behavision/x.jpg",
} {
if s.OwnsKey(key) {
t.Errorf("OwnsKey(%q) = true", key)
}
}
if !s.OwnsKey("behavision/acme/store1/2026/08/31/x.jpg") {
t.Error("a legitimate key was rejected")
}
}
func TestPresignRefusesForeignKeys(t *testing.T) {
s := testStore(t)
if _, err := s.PresignGet("Profile/93/user_profile-28.jpg", time.Minute); err == nil {
t.Fatal("presigned a read for another application's object")
}
if _, _, err := s.PresignPut("Profile/93/x.jpg", time.Minute); err == nil {
t.Fatal("presigned a write outside our prefix")
}
}
// A presigned URL is a bearer token for one object. A day-long one forwarded
// in an email outlives every reason it was issued for.
func TestPresignClampsAbsurdLifetimes(t *testing.T) {
s := testStore(t)
for _, ttl := range []time.Duration{0, -time.Hour, 72 * time.Hour} {
u, err := s.PresignGet("behavision/a/b/2026/08/31/x.jpg", ttl)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(u, "X-Amz-Expires=900") {
t.Errorf("ttl %s was not clamped to 15 minutes: %s", ttl, u)
}
}
}
func TestPresignedPutSignsThePrivateACL(t *testing.T) {
s := testStore(t)
u, hdr, err := s.PresignPut("behavision/a/b/2026/08/31/x.jpg", time.Minute)
if err != nil {
t.Fatal(err)
}
// Signed, so the agent must send it and cannot choose to publish instead.
if !strings.Contains(u, "x-amz-acl") {
t.Fatalf("the ACL is not part of the signature: %s", u)
}
if hdr.Get("x-amz-acl") != "private" {
t.Fatalf("caller is not told to send a private ACL: %v", hdr)
}
if strings.Contains(u, s.cfg.SecretKey) {
t.Fatal("the secret key is in the URL")
}
}