Files
silk/server-go/internal/handler/access_test.go
T
2026-08-17 21:43:26 +08:00

171 lines
5.8 KiB
Go

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)
}
}