package api import ( "bytes" "net/http" "net/http/httptest" "strings" "testing" ) // A minimal but real JPEG header: SOI + APP0. The endpoint checks the bytes, // not the Content-Type header, so a test that sends anything else is not // testing the same path a shop PC uses. func jpegBytes(padTo int) []byte { b := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 'J', 'F', 'I', 'F', 0} for len(b) < padTo { b = append(b, 0x00) } return append(b, 0xFF, 0xD9) } func TestAnAgentCanStoreItsCameraPicture(t *testing.T) { srv, fs := newServer(t) fs.addAgent("agent-token", AgentPrincipal{ClientID: "client-1", SiteID: "site-1"}) rr := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/api/agent/cameras/entrance/snapshot", bytes.NewReader(jpegBytes(64))) req.Header.Set("Authorization", "Bearer agent-token") srv.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Fatalf("status %d: %s", rr.Code, rr.Body) } if len(fs.snapshots["entrance"]) == 0 { t.Fatal("nothing was stored") } // The tenant and site come from the AGENT's credential, never the request. // A camera id a caller can set must not be able to choose whose camera it // decorates. if fs.lastSnapshotClient != "client-1" || fs.lastSnapshotSite != "site-1" { t.Fatalf("stored against %s/%s", fs.lastSnapshotClient, fs.lastSnapshotSite) } } // This endpoint stores whatever it is handed and serves it back to a browser // later, so the one thing it must not become is a way to park arbitrary content // under a URL this server will serve. func TestOnlyAJPEGIsAccepted(t *testing.T) { srv, fs := newServer(t) fs.addAgent("agent-token", AgentPrincipal{ClientID: "client-1", SiteID: "site-1"}) for _, body := range []string{ "", "GIF89a", "", } { rr := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/api/agent/cameras/entrance/snapshot", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer agent-token") // Claiming to be a JPEG must not help: the check is on the bytes. req.Header.Set("Content-Type", "image/jpeg") srv.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("body %q was accepted with status %d", body, rr.Code) } } } // One row per camera is what makes this safe to keep in the database at all, // but a single oversized request still has to be bounded - it is read into // memory before anything else looks at it. func TestAnOversizedSnapshotIsRefused(t *testing.T) { srv, fs := newServer(t) fs.addAgent("agent-token", AgentPrincipal{ClientID: "client-1", SiteID: "site-1"}) rr := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/api/agent/cameras/entrance/snapshot", bytes.NewReader(jpegBytes(maxSnapshotBytes+1024))) req.Header.Set("Authorization", "Bearer agent-token") srv.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusRequestEntityTooLarge { t.Fatalf("status %d", rr.Code) } if len(fs.snapshots) != 0 { t.Fatal("an oversized snapshot was stored anyway") } } func TestAnUnauthenticatedAgentCannotStoreAPicture(t *testing.T) { srv, _ := newServer(t) rr := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/api/agent/cameras/entrance/snapshot", bytes.NewReader(jpegBytes(64))) srv.Routes().ServeHTTP(rr, req) if rr.Code != http.StatusUnauthorized { t.Fatalf("status %d", rr.Code) } }