mengstack-api/internal/testutil/server.go
MengStack Dev df809f1045
Some checks failed
CI / Build & Test (push) Failing after 1m31s
feat: user CRUD API + dashboard stats + kernel infrastructure
- Add user management endpoints (list/create/get/update/delete) with pagination and search
- Add dashboard stats endpoint with tenant/user/online counts and growth metrics
- Add tenant resolver middleware for multi-tenant request scoping
- Add i18n kernel with zh/en message files and AcceptLanguage middleware
- Add WebSocket hub/handler for real-time communication
- Add job scheduler kernel with cron support
- Add plugin sandbox for isolated execution
- Add storage kernel (local filesystem)
- Add event bus kernel for pub/sub
- Add cache kernel abstraction
- Add database migration runner and version upgrade checker
- Add rate limiting middleware with Redis backend
- Add SQL migrations for rbac, audit_logs, settings, notifications, examples
- Extend user repository with list/delete/count operations
- Register all module routes with tenant resolver
2026-10-03 03:42:58 +08:00

151 lines
3.0 KiB
Go

package testutil
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func init() {
gin.SetMode(gin.TestMode)
}
type APITest struct {
t *testing.T
engine *gin.Engine
}
func NewAPITest(t *testing.T, engine *gin.Engine) *APITest {
t.Helper()
return &APITest{t: t, engine: engine}
}
func (a *APITest) Request(method, path string, body interface{}) *APIRequest {
t := a.t
t.Helper()
var reader io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
t.Fatalf("marshal request body: %v", err)
}
reader = bytes.NewReader(b)
}
req := httptest.NewRequest(method, path, reader)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return &APIRequest{t: t, engine: a.engine, req: req}
}
func (a *APITest) Get(path string) *APIRequest {
return a.Request("GET", path, nil)
}
func (a *APITest) Post(path string, body interface{}) *APIRequest {
return a.Request("POST", path, body)
}
func (a *APITest) Put(path string, body interface{}) *APIRequest {
return a.Request("PUT", path, body)
}
func (a *APITest) Delete(path string) *APIRequest {
return a.Request("DELETE", path, nil)
}
type APIRequest struct {
t *testing.T
engine *gin.Engine
req *http.Request
}
func (r *APIRequest) WithToken(token string) *APIRequest {
r.req.Header.Set("Authorization", "Bearer "+token)
return r
}
func (r *APIRequest) WithTenant(tenantID string) *APIRequest {
r.req.Header.Set("X-Tenant-ID", tenantID)
return r
}
func (r *APIRequest) WithHeader(key, value string) *APIRequest {
r.req.Header.Set(key, value)
return r
}
func (r *APIRequest) Do() *APIResponse {
r.t.Helper()
w := httptest.NewRecorder()
r.engine.ServeHTTP(w, r.req)
resp := &APIResponse{
Code: w.Code,
Headers: w.Header(),
BodyBytes: w.Body.Bytes(),
t: r.t,
}
var result map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &result); err == nil {
resp.JSON = result
}
return resp
}
type APIResponse struct {
Code int
Headers http.Header
BodyBytes []byte
JSON map[string]interface{}
t *testing.T
}
func (r *APIResponse) AssertStatus(expected int) *APIResponse {
r.t.Helper()
if r.Code != expected {
r.t.Errorf("expected status %d, got %d; body: %s", expected, r.Code, string(r.BodyBytes))
}
return r
}
func (r *APIResponse) AssertCode(expected float64) *APIResponse {
r.t.Helper()
if r.JSON == nil {
r.t.Fatal("response is not JSON")
}
got, ok := r.JSON["code"]
if !ok {
r.t.Fatal("response has no 'code' field")
}
if got.(float64) != expected {
r.t.Errorf("expected code %.0f, got %.0f; body: %s", expected, got, string(r.BodyBytes))
}
return r
}
func (r *APIResponse) Data() map[string]interface{} {
r.t.Helper()
if r.JSON == nil {
r.t.Fatal("response is not JSON")
}
data, ok := r.JSON["data"]
if !ok {
r.t.Fatal("response has no 'data' field")
}
m, ok := data.(map[string]interface{})
if !ok {
r.t.Fatalf("data is not an object: %T", data)
}
return m
}