package handler import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" ) // helper to build gin engine with given handler without auth for focused handler tests func newTestRouter(h *Handler, register func(*gin.Engine)) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() register(r) return r } // helper to set up sqlmock-backed handler func newMockHandler(t *testing.T) (*Handler, sqlmock.Sqlmock, func()) { t.Helper() db, mock, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock: %v", err) } h := &Handler{DB: db, JWTSecret: "secret"} cleanup := func() { db.Close() } return h, mock, cleanup } func TestCreateSensor(t *testing.T) { h, mock, cleanup := newMockHandler(t) defer cleanup() mock.ExpectExec("INSERT INTO sensors"). WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), "temp", "bool", 1). WillReturnResult(sqlmock.NewResult(0, 1)) r := newTestRouter(h, func(r *gin.Engine) { r.POST("/sensors", h.CreateSensor) }) body := map[string]interface{}{ "device_id": "11111111-1111-1111-1111-111111111111", "name": "temp", "type": "bool", "data_type_id": 1, } buf, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/sensors", bytes.NewReader(buf)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201 got %d, body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("expectations: %v", err) } } func TestGetSensors(t *testing.T) { h, mock, cleanup := newMockHandler(t) defer cleanup() rows := sqlmock.NewRows([]string{"id", "device_id", "name", "type", "data_type_id", "created_at", "updated_at"}). AddRow("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "temp", "bool", 1, time.Now(), time.Now()) mock.ExpectQuery(`SELECT BIN_TO_UUID\(id\)`).WillReturnRows(rows) r := newTestRouter(h, func(r *gin.Engine) { r.GET("/sensors", h.GetSensors) }) req := httptest.NewRequest(http.MethodGet, "/sensors", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200 got %d, body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("expectations: %v", err) } } func TestCreateActor(t *testing.T) { h, mock, cleanup := newMockHandler(t) defer cleanup() mock.ExpectExec("INSERT INTO actors"). WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), "led", "switch", 1). WillReturnResult(sqlmock.NewResult(0, 1)) r := newTestRouter(h, func(r *gin.Engine) { r.POST("/actors", h.CreateActor) }) body := map[string]interface{}{ "device_id": "11111111-1111-1111-1111-111111111111", "name": "led", "type": "switch", "data_type_id": 1, } buf, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/actors", bytes.NewReader(buf)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201 got %d, body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("expectations: %v", err) } } func TestGetActors(t *testing.T) { h, mock, cleanup := newMockHandler(t) defer cleanup() rows := sqlmock.NewRows([]string{"id", "device_id", "name", "type", "data_type_id", "created_at", "updated_at"}). AddRow("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "led", "switch", 1, time.Now(), time.Now()) mock.ExpectQuery(`SELECT BIN_TO_UUID\(id\).*FROM actors`).WillReturnRows(rows) r := newTestRouter(h, func(r *gin.Engine) { r.GET("/actors", h.GetActors) }) req := httptest.NewRequest(http.MethodGet, "/actors", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200 got %d, body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("expectations: %v", err) } } func TestCreateSensorReading(t *testing.T) { h, mock, cleanup := newMockHandler(t) defer cleanup() mock.ExpectExec("INSERT INTO sensor_readings"). WithArgs(sqlmock.AnyArg(), 42.0, sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(10, 1)) r := newTestRouter(h, func(r *gin.Engine) { r.POST("/sensor-readings", h.CreateSensorReading) }) body := map[string]interface{}{ "sensor_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "value": 42.0, } buf, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/sensor-readings", bytes.NewReader(buf)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201 got %d, body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("expectations: %v", err) } } func TestGetSensorReadings(t *testing.T) { h, mock, cleanup := newMockHandler(t) defer cleanup() rows := sqlmock.NewRows([]string{"id", "sensor_id", "value", "value_at"}). AddRow(int64(1), "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", 1.23, time.Now()) mock.ExpectQuery(`SELECT id, BIN_TO_UUID\(sensor_id\)`).WillReturnRows(rows) r := newTestRouter(h, func(r *gin.Engine) { r.GET("/sensor-readings", h.GetSensorReadings) }) req := httptest.NewRequest(http.MethodGet, "/sensor-readings", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200 got %d, body=%s", w.Code, w.Body.String()) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("expectations: %v", err) } }