generated from CubeCraft-Creations/Tracehound
1f253283f8
- Add request validation: Content-Type check, body size limit (64KB)
- Add field length validation (camera_id: 64, friendly_name: 128, mode: 32, resolution: 32)
- Add FPS range validation (0-240)
- Add battery_pct range validation (0-100)
- Replace ad-hoc map[string]string errors with structured APIError {error, code, details}
- Fix isUniqueConstraintErr to catch both camera_id and mac_address constraint violations
- Fix MacAddress model field from string to *string for NULL handling
- Fix splitSQL to strip -- line comments before splitting (was causing migration failures with modernc.org/sqlite)
- Add 30 integration tests covering all endpoints
- All tests pass: ok github.com/cubecraft/remoterig/internal/api
597 lines
18 KiB
Go
597 lines
18 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/cubecraft/remoterig/internal/db"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// setupTestRouter creates a test router backed by a temp file database so
|
|
// pooled connections all see the same data.
|
|
func setupTestRouter(t *testing.T) (*db.DB, chi.Router) {
|
|
t.Helper()
|
|
database, err := db.Open(t.TempDir() + "/test.db")
|
|
if err != nil {
|
|
t.Fatalf("failed to open test db: %v", err)
|
|
}
|
|
|
|
r := chi.NewRouter()
|
|
r.Get("/cameras", ListCameras(database))
|
|
r.Post("/cameras", RegisterCamera(database))
|
|
r.Get("/cameras/{id}", GetCameraDetail(database))
|
|
r.Post("/cameras/{id}/start", StartRecording(database))
|
|
r.Post("/cameras/{id}/stop", StopRecording(database))
|
|
r.Post("/cameras/{id}/status", PushStatus(database))
|
|
|
|
return database, r
|
|
}
|
|
|
|
func newReq(method, target string, body io.Reader) *http.Request {
|
|
return httptest.NewRequest(method, target, body)
|
|
}
|
|
|
|
func assertStatus(t *testing.T, resp *http.Response, expected int) {
|
|
t.Helper()
|
|
if resp.StatusCode != expected {
|
|
t.Errorf("expected status %d, got %d", expected, resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func assertError(t *testing.T, resp *http.Response, expectedStatus int, want string) {
|
|
t.Helper()
|
|
assertStatus(t, resp, expectedStatus)
|
|
body, _ := io.ReadAll(resp.Body)
|
|
var e APIError
|
|
if err := json.Unmarshal(body, &e); err != nil {
|
|
t.Fatalf("failed to unmarshal error: %v (body: %s)", err, string(body))
|
|
}
|
|
if e.Code != expectedStatus {
|
|
t.Errorf("expected code %d, got %d", expectedStatus, e.Code)
|
|
}
|
|
if !strings.Contains(e.Error, want) {
|
|
t.Errorf("expected error containing %q, got %q", want, e.Error)
|
|
}
|
|
}
|
|
|
|
func regCamera(t *testing.T, db *db.DB) string {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
r := newReq("POST", "/cameras", strings.NewReader(`{"camera_id":"CAM-001","friendly_name":"Test Camera"}`))
|
|
r.Header.Set("Content-Type", "application/json")
|
|
RegisterCamera(db)(w, r)
|
|
return "CAM-001"
|
|
}
|
|
|
|
// ==================== GET /cameras ====================
|
|
|
|
func TestListCameras_Empty(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("GET", "/cameras", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusOK)
|
|
|
|
var cameras []map[string]interface{}
|
|
json.NewDecoder(w.Result().Body).Decode(&cameras)
|
|
if cameras == nil {
|
|
t.Error("expected non-nil cameras array, got nil")
|
|
}
|
|
}
|
|
|
|
func TestListCameras_WithData(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
// Push a status
|
|
sr := newReq("POST", "/cameras/CAM-001/status",
|
|
strings.NewReader(`{"battery_pct":85,"recording":false,"mode":"video","resolution":"4K","fps":30,"online":true}`))
|
|
sr.Header.Set("Content-Type", "application/json")
|
|
sw := httptest.NewRecorder()
|
|
r.ServeHTTP(sw, sr)
|
|
assertStatus(t, sw.Result(), http.StatusOK)
|
|
|
|
// Now list
|
|
lr := newReq("GET", "/cameras", nil)
|
|
lw := httptest.NewRecorder()
|
|
r.ServeHTTP(lw, lr)
|
|
assertStatus(t, lw.Result(), http.StatusOK)
|
|
|
|
var cameras []map[string]interface{}
|
|
json.NewDecoder(lw.Result().Body).Decode(&cameras)
|
|
if len(cameras) != 1 {
|
|
t.Errorf("expected 1 camera, got %d", len(cameras))
|
|
}
|
|
}
|
|
|
|
// ==================== POST /cameras (Register) ====================
|
|
|
|
func TestRegisterCamera_Success(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"camera_id":"CAM-001","friendly_name":"Test"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusCreated)
|
|
}
|
|
|
|
func TestRegisterCamera_WithMacAddress(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"camera_id":"CAM-001","friendly_name":"Test","mac_address":"00:11:22:33:44:55"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusCreated)
|
|
}
|
|
|
|
func TestRegisterCamera_MissingBody(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
func TestRegisterCamera_InvalidJSON(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras", strings.NewReader(`{not json`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
func TestRegisterCamera_MissingRequiredFields(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"friendly_name":"Test"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "camera_id is required")
|
|
|
|
req2 := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"camera_id":"CAM-001"}`))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
w2 := httptest.NewRecorder()
|
|
r.ServeHTTP(w2, req2)
|
|
assertError(t, w2.Result(), http.StatusBadRequest, "friendly_name is required")
|
|
}
|
|
|
|
func TestRegisterCamera_FieldTooLong(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
longID := strings.Repeat("x", 65)
|
|
req := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"camera_id":"`+longID+`","friendly_name":"Test"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "camera_id must be at most 64")
|
|
|
|
longName := strings.Repeat("y", 129)
|
|
req2 := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"camera_id":"CAM-001","friendly_name":"`+longName+`"}`))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
w2 := httptest.NewRecorder()
|
|
r.ServeHTTP(w2, req2)
|
|
assertError(t, w2.Result(), http.StatusBadRequest, "friendly_name must be at most 128")
|
|
}
|
|
|
|
func TestRegisterCamera_WrongContentType(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"camera_id":"CAM-001","friendly_name":"Test"}`))
|
|
req.Header.Set("Content-Type", "text/plain")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusUnsupportedMediaType)
|
|
}
|
|
|
|
func TestRegisterCamera_NoContentType(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"camera_id":"CAM-001","friendly_name":"Test"}`))
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusCreated)
|
|
}
|
|
|
|
func TestRegisterCamera_BodyTooLarge(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
req := httptest.NewRequest("POST", "/cameras", bytes.NewReader(make([]byte, 70000)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "too large")
|
|
}
|
|
|
|
func TestRegisterCamera_Duplicate(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras",
|
|
strings.NewReader(`{"camera_id":"CAM-001","friendly_name":"Test"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusConflict, "camera already registered")
|
|
}
|
|
|
|
// ==================== GET /cameras/{id} ====================
|
|
|
|
func TestGetCameraDetail_Success(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("GET", "/cameras/CAM-001", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusOK)
|
|
}
|
|
|
|
func TestGetCameraDetail_NotFound(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("GET", "/cameras/NONEXISTENT", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusNotFound, "camera not found")
|
|
}
|
|
|
|
func TestGetCameraDetail_BadID(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("GET", "/cameras/"+strings.Repeat("x", 65), nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "camera_id must be at most 64")
|
|
}
|
|
|
|
// ==================== POST /cameras/{id}/start ====================
|
|
|
|
func TestStartRecording_Success(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/start", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusOK)
|
|
|
|
var resp map[string]string
|
|
json.NewDecoder(w.Result().Body).Decode(&resp)
|
|
if resp["status"] != "recording_started" {
|
|
t.Errorf("expected recording_started, got %q", resp["status"])
|
|
}
|
|
}
|
|
|
|
func TestStartRecording_CameraNotFound(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras/NONEXISTENT/start", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusNotFound, "camera not found")
|
|
}
|
|
|
|
func TestStartRecording_MissingID(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras//start", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "camera_id is required")
|
|
}
|
|
|
|
// ==================== POST /cameras/{id}/stop ====================
|
|
|
|
func TestStopRecording_Success(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
// Start first
|
|
sr := newReq("POST", "/cameras/CAM-001/start", nil)
|
|
sw := httptest.NewRecorder()
|
|
r.ServeHTTP(sw, sr)
|
|
assertStatus(t, sw.Result(), http.StatusOK)
|
|
|
|
// Now stop
|
|
req := newReq("POST", "/cameras/CAM-001/stop", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusOK)
|
|
|
|
var resp map[string]string
|
|
json.NewDecoder(w.Result().Body).Decode(&resp)
|
|
if resp["status"] != "recording_stopped" {
|
|
t.Errorf("expected recording_stopped, got %q", resp["status"])
|
|
}
|
|
}
|
|
|
|
func TestStopRecording_CameraNotFound(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras/NONEXISTENT/stop", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusNotFound, "camera not found")
|
|
}
|
|
|
|
// ==================== POST /cameras/{id}/status ====================
|
|
|
|
func TestPushStatus_Success(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/status",
|
|
strings.NewReader(`{"battery_pct":75,"recording":false,"mode":"video","resolution":"1080p","fps":60,"online":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusOK)
|
|
|
|
var resp map[string]string
|
|
json.NewDecoder(w.Result().Body).Decode(&resp)
|
|
if resp["status"] != "accepted" {
|
|
t.Errorf("expected accepted, got %q", resp["status"])
|
|
}
|
|
}
|
|
|
|
func TestPushStatus_CameraNotFound(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
req := newReq("POST", "/cameras/NONEXISTENT/status",
|
|
strings.NewReader(`{"battery_pct":75,"recording":false,"mode":"video","resolution":"1080p","fps":60,"online":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusNotFound, "camera not found")
|
|
}
|
|
|
|
func TestPushStatus_InvalidJSON(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/status", strings.NewReader(`{bad json`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
func TestPushStatus_InvalidFPS(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/status",
|
|
strings.NewReader(`{"battery_pct":75,"recording":false,"mode":"video","resolution":"1080p","fps":999,"online":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "fps must be between")
|
|
}
|
|
|
|
func TestPushStatus_NegativeFPS(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/status",
|
|
strings.NewReader(`{"battery_pct":75,"recording":false,"mode":"video","resolution":"1080p","fps":-1,"online":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "fps must be between")
|
|
}
|
|
|
|
func TestPushStatus_InvalidBattery(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/status",
|
|
strings.NewReader(`{"battery_pct":150,"recording":false,"mode":"video","resolution":"1080p","fps":30,"online":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "battery_pct must be between")
|
|
}
|
|
|
|
func TestPushStatus_NegativeBattery(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/status",
|
|
strings.NewReader(`{"battery_pct":-5,"recording":false,"mode":"video","resolution":"1080p","fps":30,"online":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "battery_pct must be between")
|
|
}
|
|
|
|
func TestPushStatus_ModeTooLong(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/status",
|
|
strings.NewReader(`{"battery_pct":75,"recording":false,"mode":"`+strings.Repeat("x", 33)+`","resolution":"1080p","fps":30,"online":true}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "mode must be at most")
|
|
}
|
|
|
|
func TestPushStatus_MissingBody(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
req := newReq("POST", "/cameras/CAM-001/status", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertError(t, w.Result(), http.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
// ==================== Error Response Format ====================
|
|
|
|
func TestErrorResponseFormat_Consistent(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
|
|
checks := []struct {
|
|
method, target, body string
|
|
}{
|
|
{"GET", "/cameras/NONEXISTENT", ""},
|
|
{"POST", "/cameras", "bad json"},
|
|
{"POST", "/cameras/NONEXISTENT/start", ""},
|
|
{"POST", "/cameras/NONEXISTENT/status", "bad json"},
|
|
}
|
|
|
|
for _, c := range checks {
|
|
var rd io.Reader
|
|
if c.body != "" {
|
|
rd = strings.NewReader(c.body)
|
|
}
|
|
req := newReq(c.method, c.target, rd)
|
|
if c.body != "" {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
var errResp map[string]interface{}
|
|
json.NewDecoder(w.Result().Body).Decode(&errResp)
|
|
if _, ok := errResp["error"]; !ok {
|
|
t.Errorf("%s %s: missing 'error' key: %v", c.method, c.target, errResp)
|
|
}
|
|
if _, ok := errResp["code"]; !ok {
|
|
t.Errorf("%s %s: missing 'code' key: %v", c.method, c.target, errResp)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== SQL Injection ====================
|
|
|
|
func TestSQLInjection_CameraID(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
// Chi URL params are extracted after routing, so injection attempts will
|
|
// be treated as camera_ids and fail validation (too long) or return 404.
|
|
// Use URL encoding for special characters to avoid httptest panics.
|
|
paths := []string{
|
|
"/cameras/CAM-001%27+DROP+TABLE+cameras--",
|
|
"/cameras/1+UNION+SELECT+NULL--",
|
|
"/cameras/%27+OR+%27%27%3D%27",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest("GET", path, nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
code := w.Result().StatusCode
|
|
if code != http.StatusNotFound && code != http.StatusBadRequest {
|
|
t.Errorf("unexpected status %d for injection path %s", code, path)
|
|
}
|
|
}
|
|
|
|
// Verify tables still exist
|
|
req := httptest.NewRequest("GET", "/cameras", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
assertStatus(t, w.Result(), http.StatusOK)
|
|
}
|
|
|
|
// ==================== Recording Lifecycle ====================
|
|
|
|
func TestRecordingLifecycle(t *testing.T) {
|
|
db, r := setupTestRouter(t)
|
|
defer db.Close()
|
|
regCamera(t, db)
|
|
|
|
// Start
|
|
r1 := newReq("POST", "/cameras/CAM-001/start", nil)
|
|
w1 := httptest.NewRecorder()
|
|
r.ServeHTTP(w1, r1)
|
|
assertStatus(t, w1.Result(), http.StatusOK)
|
|
|
|
// Stop
|
|
r2 := newReq("POST", "/cameras/CAM-001/stop", nil)
|
|
w2 := httptest.NewRecorder()
|
|
r.ServeHTTP(w2, r2)
|
|
assertStatus(t, w2.Result(), http.StatusOK)
|
|
|
|
// Start again
|
|
r3 := newReq("POST", "/cameras/CAM-001/start", nil)
|
|
w3 := httptest.NewRecorder()
|
|
r.ServeHTTP(w3, r3)
|
|
assertStatus(t, w3.Result(), http.StatusOK)
|
|
}
|
|
|
|
// ==================== Benchmark ====================
|
|
|
|
func BenchmarkListCameras(b *testing.B) {
|
|
db2, _ := db.Open(b.TempDir() + "/bench.db")
|
|
defer db2.Close()
|
|
for i := 0; i < 10; i++ {
|
|
id := string(rune('A'+i)) + "-CAM"
|
|
h := RegisterCamera(db2)
|
|
body := `{"camera_id":"` + id + `","friendly_name":"Test ` + string(rune('A'+i)) + `"}`
|
|
req := newReq("POST", "/cameras", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
h(w, req)
|
|
}
|
|
jh := ListCameras(db2)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
req := newReq("GET", "/cameras", nil)
|
|
w := httptest.NewRecorder()
|
|
jh(w, req)
|
|
}
|
|
}
|