package handler import ( "net/http/httptest" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/gin-gonic/gin" "gorm.io/driver/postgres" "gorm.io/gorm" ) func TestCanAccessRoomAdmin(t *testing.T) { gin.SetMode(gin.TestMode) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Set("user", map[string]interface{}{"sub": "admin-1", "role": "admin"}) roomID := "room-1" if !canAccessRoom(nil, c, &roomID) { t.Fatal("admin should have global object access") } } func TestCanAccessRoomMissingUser(t *testing.T) { gin.SetMode(gin.TestMode) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) roomID := "room-1" if canAccessRoom(nil, c, &roomID) { t.Fatal("missing user should not pass room access") } } func TestApplyRoomScopeUsesOrganizationMembership(t *testing.T) { gin.SetMode(gin.TestMode) sqlDB, mock, err := sqlmock.New() if err != nil { t.Fatalf("create sqlmock: %v", err) } defer sqlDB.Close() db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{}) if err != nil { t.Fatalf("open gorm: %v", err) } rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Set("user", map[string]interface{}{"sub": "user-1", "role": "viewer"}) mock.ExpectQuery(".*CAST\\(room_id AS text\\).*organization_members.*"). WithArgs("user-1"). WillReturnRows(sqlmock.NewRows([]string{"id", "name", "org_id"})) var rooms []map[string]interface{} q := applyRoomScope(db.Model(&struct{}{}).Table("rooms"), c, "room_id") if err := q.Find(&rooms).Error; err != nil { t.Fatalf("scoped query failed: %v", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("unmet sql expectations: %v", err) } } func TestCanAccessDeviceKeyAdmin(t *testing.T) { gin.SetMode(gin.TestMode) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Set("user", map[string]interface{}{"sub": "admin-1", "role": "admin"}) if !canAccessDeviceKey(nil, c, "device-a") { t.Fatal("admin should have global device access") } } func TestCanAccessDeviceKeyMissingUser(t *testing.T) { gin.SetMode(gin.TestMode) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) if canAccessDeviceKey(nil, c, "device-a") { t.Fatal("missing user should not pass device access") } } func TestObjectRoomIDSensorResolvesThroughDevice(t *testing.T) { gin.SetMode(gin.TestMode) sqlDB, mock, err := sqlmock.New() if err != nil { t.Fatalf("create sqlmock: %v", err) } defer sqlDB.Close() db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{}) if err != nil { t.Fatalf("open gorm: %v", err) } mock.ExpectQuery(`.*FROM "sensors".*WHERE id = \$1.*`). WillReturnRows(sqlmock.NewRows([]string{"id", "name", "metric", "unit", "data_type", "device_id", "created_at"}). AddRow("sensor-1", "温度", "temperature", nil, nil, "device-1", time.Now())) mock.ExpectQuery(`.*FROM "devices".*WHERE id = \$1.*`). WillReturnRows(sqlmock.NewRows([]string{"id", "device_key", "name", "kind", "model", "firmware", "online_status", "last_seen", "room_id", "topic", "created_at", "updated_at"}). AddRow("device-1", "device-a", "温度设备", "sensor", nil, nil, "online", nil, "room-1", nil, time.Now(), time.Now())) roomID, ok := objectRoomID(db, "sensor", "sensor-1") if !ok || roomID == nil || *roomID != "room-1" { t.Fatalf("sensor room = %v, ok = %v, want room-1", roomID, ok) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("unmet sql expectations: %v", err) } } func TestRequireObjectAccessForbiddenForForeignDevice(t *testing.T) { gin.SetMode(gin.TestMode) sqlDB, mock, err := sqlmock.New() if err != nil { t.Fatalf("create sqlmock: %v", err) } defer sqlDB.Close() db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{}) if err != nil { t.Fatalf("open gorm: %v", err) } rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Set("user", map[string]interface{}{"sub": "user-1", "role": "viewer"}) mock.ExpectQuery(`.*FROM "devices".*WHERE id = \$1.*`). WillReturnRows(sqlmock.NewRows([]string{"id", "device_key", "name", "kind", "model", "firmware", "online_status", "last_seen", "room_id", "topic", "created_at", "updated_at"}). AddRow("device-1", "device-a", "温度设备", "sensor", nil, nil, "online", nil, "room-1", nil, time.Now(), time.Now())) mock.ExpectQuery(`.*FROM "rooms".*organization_members.*`). WithArgs("room-1", "user-1"). WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) if requireObjectAccess(c, db, "device", "device-1") { t.Fatal("foreign device should not pass object access") } if rec.Code != 403 { t.Fatalf("status = %d, want 403", rec.Code) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("unmet sql expectations: %v", err) } } func TestFirstAccessibleRoomIDUsesOrgScope(t *testing.T) { gin.SetMode(gin.TestMode) sqlDB, mock, err := sqlmock.New() if err != nil { t.Fatalf("create sqlmock: %v", err) } defer sqlDB.Close() db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{}) if err != nil { t.Fatalf("open gorm: %v", err) } rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Set("user", map[string]interface{}{"sub": "user-1", "role": "viewer"}) mock.ExpectQuery(`.*FROM "rooms".*CAST\(org_id AS text\).*organization_members.*`). WillReturnRows(sqlmock.NewRows([]string{"id", "name", "code", "location", "description", "capacity", "stage", "region", "org_id", "status", "created_at", "updated_at"}). AddRow("room-1", "一号蚕房", nil, nil, nil, nil, nil, nil, "org-1", "active", time.Now(), time.Now())) roomID := firstAccessibleRoomID(db, c) if roomID == nil || *roomID != "room-1" { t.Fatalf("first accessible room = %v, want room-1", roomID) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("unmet sql expectations: %v", err) } }