mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-21 18:20:50 +00:00
IndexAction closes over one dto.Index and serves every request to the route from it; Generate exists so each request gets its own instance, and every implementation in this repository returns a copy for that reason. The probe returned the receiver, which made it the one shape IndexAction is not written against - and inconsistent with probeRow in the same file, which already copied. A single-request test cannot tell the two apart, so the assertion is on Generate itself rather than on the action's behaviour. Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
136 lines
4.6 KiB
Go
136 lines
4.6 KiB
Go
package actions_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
gormlogger "gorm.io/gorm/logger"
|
|
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
|
|
|
"go-admin/common/actions"
|
|
"go-admin/common/dto"
|
|
"go-admin/common/models"
|
|
)
|
|
|
|
// capturingLogger records every SQL statement GORM actually executes, so a
|
|
// test can inspect it the way inspecting a *gorm.DB's own Statement cannot:
|
|
// IndexAction builds and executes its query in one unbroken chain
|
|
// (db.Model(...).Scopes(...).Find(...)...Count(...)) and never hands the
|
|
// built statement back to its caller.
|
|
type capturingLogger struct {
|
|
gormlogger.Interface
|
|
mu sync.Mutex
|
|
stmts []string
|
|
}
|
|
|
|
func (l *capturingLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
|
sql, _ := fc()
|
|
l.mu.Lock()
|
|
l.stmts = append(l.stmts, sql)
|
|
l.mu.Unlock()
|
|
}
|
|
|
|
func (l *capturingLogger) all() string {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return strings.Join(l.stmts, "\n")
|
|
}
|
|
|
|
// probeRow is a minimal model satisfying models.ActiveRecord through the
|
|
// same embeds a real app/admin model uses, so IndexAction sees exactly the
|
|
// shape it is written against.
|
|
type probeRow struct {
|
|
models.Model
|
|
models.ControlBy
|
|
Name string
|
|
}
|
|
|
|
func (probeRow) TableName() string { return "action_probe_row" }
|
|
func (e *probeRow) Generate() models.ActiveRecord { o := *e; return &o }
|
|
func (e *probeRow) GetId() interface{} { return e.Id }
|
|
|
|
// probeIndexReq is a minimal dto.Index: no search tags, page defaults.
|
|
type probeIndexReq struct {
|
|
dto.Pagination `search:"-"`
|
|
}
|
|
|
|
// Generate returns a copy, the way every dto.Index in this repository does:
|
|
// IndexAction closes over one instance and serves every request to the route
|
|
// from it, so returning the receiver would share one struct across them. The
|
|
// probe has to model that faithfully or it is not the shape IndexAction is
|
|
// written against.
|
|
func (p *probeIndexReq) Generate() dto.Index { o := *p; return &o }
|
|
func (p *probeIndexReq) Bind(*gin.Context) error { return nil }
|
|
func (p *probeIndexReq) GetNeedSearch() interface{} { return *p }
|
|
|
|
type pageEnvelope struct {
|
|
Code int32 `json:"code"`
|
|
}
|
|
|
|
// TestIndexActionAppliesDataPermission is an end-to-end guard core's own
|
|
// test suite cannot provide. The five generic CRUD actions in this package
|
|
// (create/delete/index/update/view.go) were not lowered to core (PRD 006
|
|
// F3) - they still call actions.Permission directly, in this repository, on
|
|
// a code path core knows nothing about. core's tests pin down what
|
|
// Permission does for a given scope; nothing pinned down whether this
|
|
// package's own Actions still remember to call it at all. This runs
|
|
// IndexAction exactly as a real request would, against a real in-memory
|
|
// database, and inspects the SQL GORM actually executed - not just that
|
|
// the handler returned success, which it would just as happily do with no
|
|
// filter applied at all.
|
|
func TestProbeIndexReqGenerateReturnsAFreshInstance(t *testing.T) {
|
|
p := &probeIndexReq{}
|
|
got := p.Generate()
|
|
if got == dto.Index(p) {
|
|
t.Fatal("Generate returned the receiver; IndexAction would share one instance across every request to the route")
|
|
}
|
|
}
|
|
|
|
func TestIndexActionAppliesDataPermission(t *testing.T) {
|
|
previous := config.ApplicationConfig.EnableDP
|
|
config.ApplicationConfig.EnableDP = true
|
|
t.Cleanup(func() { config.ApplicationConfig.EnableDP = previous })
|
|
|
|
cl := &capturingLogger{Interface: gormlogger.Default.LogMode(gormlogger.Silent)}
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: cl})
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&probeRow{}); err != nil {
|
|
t.Fatalf("AutoMigrate: %v", err)
|
|
}
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
w := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(w)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
|
c.Set("db", db)
|
|
c.Set(actions.PermissionKey, &actions.DataPermission{DataScope: actions.DataScopeSelf, UserId: 7})
|
|
|
|
actions.IndexAction(&probeRow{}, &probeIndexReq{}, func() interface{} { return &[]probeRow{} })(c)
|
|
|
|
var body pageEnvelope
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decoding response body %q: %v", w.Body.String(), err)
|
|
}
|
|
if body.Code != http.StatusOK {
|
|
t.Fatalf("response code = %d, want %d; body=%s", body.Code, http.StatusOK, w.Body.String())
|
|
}
|
|
|
|
sql := cl.all()
|
|
const wantFragment = "action_probe_row.create_by = "
|
|
if !strings.Contains(sql, wantFragment) {
|
|
t.Fatalf("IndexAction did not apply the data-permission scope to its query; want SQL containing %q, got:\n%s", wantFragment, sql)
|
|
}
|
|
}
|