Complete admin dashboard with paginated user pet trees and walk relations
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sundynix/pets-be/internal/config"
|
||||
"github.com/sundynix/pets-be/internal/database"
|
||||
"github.com/sundynix/pets-be/internal/model"
|
||||
)
|
||||
|
||||
func TestAdminPageValidation(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
for _, query := range []string{"", "?page=2&page_size=10", "?page=0", "?page_size=101", "?page=abc", "?page=-1", "?page_size=0"} {
|
||||
recorder := httptest.NewRecorder()
|
||||
context, _ := gin.CreateTestContext(recorder)
|
||||
context.Request = httptest.NewRequest(http.MethodGet, "/api/admin/users"+query, nil)
|
||||
page, valid := pageParams(context)
|
||||
expected := query == "" || query == "?page=2&page_size=10"
|
||||
if valid != expected {
|
||||
t.Errorf("%s: valid=%v", query, valid)
|
||||
}
|
||||
if query == "" && (page.Page != 1 || page.PageSize != 10) {
|
||||
t.Fatalf("default pagination: %+v", page)
|
||||
}
|
||||
if !valid && recorder.Code != 400 {
|
||||
t.Errorf("%s: status=%d", query, recorder.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestAdminListsRequireAdminRole(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
cfg := &config.Config{}
|
||||
cfg.JWT.Secret = "test-secret"
|
||||
api := New(nil, nil, cfg)
|
||||
router := gin.New()
|
||||
api.Register(router)
|
||||
token, err := api.token(uuid.NewString(), "user")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, route := range []string{"users", "users/" + uuid.NewString() + "/pets", "pets", "walks", "walks/" + uuid.NewString(), "meetups", "tasks", "files", "stats"} {
|
||||
for _, auth := range []bool{false, true} {
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/admin/"+route, nil)
|
||||
if auth {
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
expected := http.StatusUnauthorized
|
||||
if auth {
|
||||
expected = http.StatusForbidden
|
||||
}
|
||||
if recorder.Code != expected {
|
||||
t.Errorf("%s auth=%v: status=%d", route, auth, recorder.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Opt-in read-only verification against the configured PostgreSQL; never seeds or changes data.
|
||||
func TestAdminPostgresReadOnly(t *testing.T) {
|
||||
if os.Getenv("PETS_ADMIN_INTEGRATION") != "1" {
|
||||
t.Skip("set PETS_ADMIN_INTEGRATION=1 for PostgreSQL verification")
|
||||
}
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := database.Open(cfg.Postgres, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, _ := db.DB()
|
||||
defer sqlDB.Close()
|
||||
gin.SetMode(gin.TestMode)
|
||||
api := New(db, nil, cfg)
|
||||
router := gin.New()
|
||||
api.Register(router)
|
||||
token, err := api.token(uuid.NewString(), "admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read := func(path string) json.RawMessage {
|
||||
t.Helper()
|
||||
request := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != 200 {
|
||||
t.Fatalf("GET %s: %d %s", path, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return body.Data
|
||||
}
|
||||
read("/api/admin/stats")
|
||||
for _, section := range []string{"users", "pets", "walks", "meetups", "tasks", "files"} {
|
||||
var page struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
PetNames []string `json:"pet_names"`
|
||||
} `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
PageSize int `json:"page_size"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
if err := json.Unmarshal(read("/api/admin/"+section+"?page=1&page_size=10"), &page); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if page.Page != 1 || page.PageSize != 10 || len(page.Items) > 10 || page.Total < int64(len(page.Items)) {
|
||||
t.Fatalf("invalid %s pagination: %+v", section, page)
|
||||
}
|
||||
read("/api/admin/" + section + "?page=2&page_size=10")
|
||||
read("/api/admin/" + section + "?q=%25%27%5F")
|
||||
if len(page.Items) > 0 && section == "users" {
|
||||
read("/api/admin/users/" + page.Items[0].ID + "/pets")
|
||||
}
|
||||
if len(page.Items) > 0 && section == "walks" {
|
||||
read("/api/admin/walks/" + page.Items[0].ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fixtures live inside a transaction and are always rolled back.
|
||||
func TestAdminPaginationAndWalkRelations(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
if os.Getenv("PETS_ADMIN_INTEGRATION") != "1" {
|
||||
t.Skip("PostgreSQL integration disabled")
|
||||
}
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := database.Open(cfg.Postgres, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, _ := db.DB()
|
||||
defer sqlDB.Close()
|
||||
tx := db.Begin()
|
||||
if tx.Error != nil {
|
||||
t.Fatal(tx.Error)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
prefix := "admin-test-" + uuid.NewString()
|
||||
users := make([]model.User, 11)
|
||||
for i := range users {
|
||||
users[i] = model.User{Nickname: prefix, OpenID: uuid.NewString()}
|
||||
}
|
||||
if tx.Create(&users).Error != nil {
|
||||
t.Fatal("create temporary users")
|
||||
}
|
||||
pets := []model.Pet{{UserID: users[0].ID, Name: "奶盖", Species: "dog"}, {UserID: users[0].ID, Name: "布丁", Species: "dog"}}
|
||||
if tx.Create(&pets).Error != nil {
|
||||
t.Fatal("create temporary pets")
|
||||
}
|
||||
now := time.Now()
|
||||
walk := model.Walk{UserID: users[0].ID, Status: "finished", StartedAt: now, SegmentStartedAt: now, ShareToken: uuid.NewString()}
|
||||
if err := tx.Create(&walk).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, pet := range pets {
|
||||
if err := tx.Create(&model.WalkPet{WalkID: walk.ID, PetID: pet.ID}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
api := New(tx, nil, cfg)
|
||||
router := gin.New()
|
||||
api.Register(router)
|
||||
token, _ := api.token(uuid.NewString(), "admin")
|
||||
read := func(path string) json.RawMessage {
|
||||
t.Helper()
|
||||
request := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
if recorder.Code != 200 {
|
||||
t.Fatalf("%s: %d %s", path, recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
json.Unmarshal(recorder.Body.Bytes(), &body)
|
||||
return body.Data
|
||||
}
|
||||
for _, sample := range []struct {
|
||||
page string
|
||||
size int
|
||||
}{{"1", 10}, {"2", 1}} {
|
||||
var result struct {
|
||||
Items []json.RawMessage `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
if err := json.Unmarshal(read("/api/admin/users?q="+prefix+"&page="+sample.page), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Total != 11 || len(result.Items) != sample.size {
|
||||
t.Fatalf("pagination got %d rows / %d total", len(result.Items), result.Total)
|
||||
}
|
||||
}
|
||||
var children []struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
json.Unmarshal(read("/api/admin/users/"+users[0].ID+"/pets"), &children)
|
||||
if len(children) != 2 {
|
||||
t.Fatalf("user tree lost pets: %+v", children)
|
||||
}
|
||||
var result struct {
|
||||
Items []struct {
|
||||
UserID string `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
PetNames []string `json:"pet_names"`
|
||||
} `json:"items"`
|
||||
}
|
||||
json.Unmarshal(read("/api/admin/walks?q="+walk.ID), &result)
|
||||
if len(result.Items) != 1 || result.Items[0].UserID != users[0].ID || result.Items[0].UserName != prefix || len(result.Items[0].PetNames) != 2 {
|
||||
t.Fatalf("walk relation incomplete: %+v", result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user