232 lines
7.0 KiB
Go
232 lines
7.0 KiB
Go
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)
|
|
}
|
|
}
|