| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- package scope
- import (
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "github.com/gofiber/fiber/v2"
- gormsqlite "github.com/glebarez/sqlite"
- "gorm.io/gorm"
- )
- // testCourse 带 institution_id/campus_id 列的 fixture model(scope 包不依赖业务域,
- // 测试用本地 model 承载两列,DryRun 捕获 SQL 验证 scoping 注入)。
- type testCourse struct {
- ID int64 `gorm:"column:id;primaryKey"`
- InstitutionID int64 `gorm:"column:institution_id"`
- CampusID int64 `gorm:"column:campus_id"`
- Name string `gorm:"column:name"`
- }
- func (testCourse) TableName() string { return "t_course" }
- // newDryRunDB 建 GORM DryRun 句柄(仅构造 SQL,不真正读写数据)。
- //
- // 选用 glebarez/sqlite(纯 Go,基于 modernc.org/sqlite,**无 CGO**)内存库:
- // - mysql 驱动在 gorm.Open 时会 ping TCP 建连,DryRun 也跳不过该初始化握手;
- // - 纯 Go SQLite `:memory:` 进程内运行,无网络/外部依赖,且 **CGO_ENABLED=0 下可编译可运行**
- // (对比 gorm.io/driver/sqlite 依赖 mattn/go-sqlite3,CGO_ENABLED=0 时是 stub 会失败);
- // - DryRun 渲染的 WHERE 片段(institution_id/campus_id 过滤)与方言无关,断言逻辑不受影响。
- // 故本包单测在 CI/容器(常 CGO_ENABLED=0)亦稳定通过。
- func newDryRunDB(t *testing.T) *gorm.DB {
- t.Helper()
- db, err := gorm.Open(gormsqlite.Open(":memory:"), &gorm.Config{DryRun: true})
- if err != nil {
- t.Fatalf("gorm.Open 失败: %v", err)
- }
- return db
- }
- // sqlOf 执行 DryRun Find,返回生成的 SQL 字符串。
- func sqlOf(t *testing.T, db *gorm.DB, q *gorm.DB) string {
- t.Helper()
- var out []testCourse
- if err := q.Model(&testCourse{}).Find(&out).Error; err != nil {
- t.Fatalf("DryRun Find 失败: %v", err)
- }
- return q.Statement.SQL.String()
- }
- // TestScopingNormalCampusScoped 普通用户 + 校区级表 → 强制 institution_id + campus_id。
- func TestScopingNormalCampusScoped(t *testing.T) {
- db := newDryRunDB(t)
- base := BaseRepo{DB: db, Sc: &Scope{InstitutionID: 42, CampusID: 7}, CampusScoped: true}
- q := base.Scoping(db.Session(&gorm.Session{NewDB: true}))
- sql := sqlOf(t, db, q)
- if !strings.Contains(sql, "institution_id = ?") {
- t.Errorf("应含 institution_id 过滤,SQL=%q", sql)
- }
- if !strings.Contains(sql, "campus_id = ?") {
- t.Errorf("CampusScoped=true 且 campus>0 应含 campus_id 过滤,SQL=%q", sql)
- }
- }
- // TestScopingInstitutionOnly 机构级资源(CampusScoped=false)→ 仅 institution_id,不加 campus_id。
- func TestScopingInstitutionOnly(t *testing.T) {
- db := newDryRunDB(t)
- base := BaseRepo{DB: db, Sc: &Scope{InstitutionID: 42, CampusID: 7}, CampusScoped: false}
- q := base.Scoping(db.Session(&gorm.Session{NewDB: true}))
- sql := sqlOf(t, db, q)
- if !strings.Contains(sql, "institution_id = ?") {
- t.Errorf("应含 institution_id 过滤,SQL=%q", sql)
- }
- if strings.Contains(sql, "campus_id = ?") {
- t.Errorf("CampusScoped=false 不应含 campus_id 过滤(表无此列,加会 SQL 报错),SQL=%q", sql)
- }
- }
- // TestScopingSuperAdmin 平台超管 → 不加任何作用域过滤(跨机构)。
- func TestScopingSuperAdmin(t *testing.T) {
- db := newDryRunDB(t)
- base := BaseRepo{DB: db, Sc: &Scope{InstitutionID: 42, CampusID: 7, SuperAdmin: true}, CampusScoped: true}
- q := base.Scoping(db.Session(&gorm.Session{NewDB: true}))
- sql := sqlOf(t, db, q)
- if strings.Contains(sql, "institution_id = ?") || strings.Contains(sql, "campus_id = ?") {
- t.Errorf("超管不应加作用域过滤,SQL=%q", sql)
- }
- }
- // TestScopingNilScopePanics 无作用域上下文进入数据访问层 → panic(fail-fast 纵深防御)。
- func TestScopingNilScopePanics(t *testing.T) {
- db := newDryRunDB(t)
- base := BaseRepo{DB: db, Sc: nil, CampusScoped: true}
- defer func() {
- if r := recover(); r == nil {
- t.Errorf("Sc=nil 应 panic(拒绝无作用域裸查),但正常返回了")
- }
- }()
- _ = base.Scoping(db)
- }
- // TestScopingZeroCampus 普通用户 + CampusID=0(机构级视角,不限校区)→ 仅 institution_id。
- func TestScopingZeroCampus(t *testing.T) {
- db := newDryRunDB(t)
- base := BaseRepo{DB: db, Sc: &Scope{InstitutionID: 42, CampusID: 0}, CampusScoped: true}
- q := base.Scoping(db.Session(&gorm.Session{NewDB: true}))
- sql := sqlOf(t, db, q)
- if !strings.Contains(sql, "institution_id = ?") {
- t.Errorf("应含 institution_id 过滤,SQL=%q", sql)
- }
- if strings.Contains(sql, "campus_id = ?") {
- t.Errorf("campus_id=0 不应加校区过滤(0=不限),SQL=%q", sql)
- }
- }
- // TestSetFromFiber 验证作用域写入/读取 fiber.Ctx 局部值往返(真实 *fiber.Ctx,经 app.Test)。
- func TestSetFromFiber(t *testing.T) {
- // nil 防御
- if got := FromFiber(nil); got != nil {
- t.Errorf("FromFiber(nil) 应返回 nil,得 %+v", got)
- }
- app := fiber.New(fiber.Config{DisableStartupMessage: true})
- app.Get("/echo", func(c *fiber.Ctx) error {
- sc := &Scope{InstitutionID: 9, CampusID: 3, UserID: "01ABC", Role: "teacher", SuperAdmin: false}
- Set(c, sc) // 写入
- got := FromFiber(c) // 读回
- if got == nil {
- return c.Status(fiber.StatusInternalServerError).SendString("FromFiber returned nil")
- }
- if got.InstitutionID != 9 || got.CampusID != 3 || got.UserID != "01ABC" || got.Role != "teacher" || got.SuperAdmin {
- return c.Status(fiber.StatusInternalServerError).SendString("scope roundtrip mismatch")
- }
- return c.SendString("ok")
- })
- req := httptest.NewRequest(http.MethodGet, "/echo", nil)
- resp, err := app.Test(req, -1)
- if err != nil {
- t.Fatalf("app.Test 失败: %v", err)
- }
- if resp.StatusCode != fiber.StatusOK {
- t.Fatalf("roundtrip 状态码=%d(期望 200)", resp.StatusCode)
- }
- }
|