# Conflicts: # account/Makefile # account/go.mod # docs/account-admin-settings.md # docs/account-svc-plus.md
234 lines
6.8 KiB
Go
234 lines
6.8 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"account/internal/model"
|
|
"account/internal/service"
|
|
"account/internal/store"
|
|
)
|
|
|
|
type adminSettingsTestEnv struct {
|
|
router *gin.Engine
|
|
adminToken string
|
|
operatorToken string
|
|
userToken string
|
|
}
|
|
|
|
func setupAdminSettingsTestRouter(t *testing.T) adminSettingsTestEnv {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.AdminSetting{}); err != nil {
|
|
t.Fatalf("auto migrate: %v", err)
|
|
}
|
|
service.SetDB(db)
|
|
t.Cleanup(func() {
|
|
service.SetDB(nil)
|
|
sqlDB, _ := db.DB()
|
|
sqlDB.Close()
|
|
})
|
|
|
|
memoryStore := store.NewMemoryStore()
|
|
ctx := context.Background()
|
|
|
|
createUser := func(name, email, password, role string, level int) string {
|
|
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
t.Fatalf("hash password: %v", err)
|
|
}
|
|
user := &store.User{
|
|
Name: name,
|
|
Email: email,
|
|
PasswordHash: string(hashed),
|
|
Role: role,
|
|
Level: level,
|
|
EmailVerified: true,
|
|
}
|
|
if err := memoryStore.CreateUser(ctx, user); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
return password
|
|
}
|
|
|
|
adminPassword := createUser("admin", "admin@example.com", "AdminPass123!", store.RoleAdmin, store.LevelAdmin)
|
|
operatorPassword := createUser("operator", "operator@example.com", "OperatorPass123!", store.RoleOperator, store.LevelOperator)
|
|
userPassword := createUser("user", "user@example.com", "UserPass123!", store.RoleUser, store.LevelUser)
|
|
|
|
router := gin.New()
|
|
RegisterRoutes(router, WithStore(memoryStore), WithEmailVerification(false))
|
|
|
|
login := func(email, password string) string {
|
|
payload := map[string]string{
|
|
"email": email,
|
|
"password": password,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("login failed for %s: %d %s", email, resp.Code, resp.Body.String())
|
|
}
|
|
var result struct {
|
|
Token string `json:"token"`
|
|
}
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &result); err != nil {
|
|
t.Fatalf("decode login response: %v", err)
|
|
}
|
|
if result.Token == "" {
|
|
t.Fatalf("expected session token for %s", email)
|
|
}
|
|
return result.Token
|
|
}
|
|
|
|
env := adminSettingsTestEnv{router: router}
|
|
env.adminToken = login("admin@example.com", adminPassword)
|
|
env.operatorToken = login("operator@example.com", operatorPassword)
|
|
env.userToken = login("user@example.com", userPassword)
|
|
|
|
return env
|
|
}
|
|
|
|
func TestAdminSettingsReadWrite(t *testing.T) {
|
|
env := setupAdminSettingsTestRouter(t)
|
|
router := env.router
|
|
|
|
payload := map[string]any{
|
|
"version": 0,
|
|
"matrix": map[string]map[string]bool{
|
|
"registration": {
|
|
"admin": true,
|
|
"operator": false,
|
|
},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/auth/admin/settings", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
|
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d (%s)", resp.Code, resp.Body.String())
|
|
}
|
|
|
|
var postResp struct {
|
|
Version uint64 `json:"version"`
|
|
Matrix map[string]map[string]bool `json:"matrix"`
|
|
}
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &postResp); err != nil {
|
|
t.Fatalf("unmarshal response: %v", err)
|
|
}
|
|
if postResp.Version != 1 {
|
|
t.Fatalf("expected version 1, got %d", postResp.Version)
|
|
}
|
|
if !postResp.Matrix["registration"]["admin"] {
|
|
t.Fatalf("expected admin flag to be true")
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/api/auth/admin/settings", nil)
|
|
req.Header.Set("Authorization", "Bearer "+env.operatorToken)
|
|
resp = httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d (%s)", resp.Code, resp.Body.String())
|
|
}
|
|
var getResp struct {
|
|
Version uint64 `json:"version"`
|
|
Matrix map[string]map[string]bool `json:"matrix"`
|
|
}
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &getResp); err != nil {
|
|
t.Fatalf("unmarshal get response: %v", err)
|
|
}
|
|
if getResp.Version != postResp.Version {
|
|
t.Fatalf("expected version %d, got %d", postResp.Version, getResp.Version)
|
|
}
|
|
if getResp.Matrix["registration"]["operator"] {
|
|
t.Fatalf("expected operator flag to remain false")
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsUnauthorized(t *testing.T) {
|
|
env := setupAdminSettingsTestRouter(t)
|
|
router := env.router
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/auth/admin/settings", nil)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected status 401, got %d", resp.Code)
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"version": 0,
|
|
"matrix": map[string]map[string]bool{},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req = httptest.NewRequest(http.MethodPost, "/api/auth/admin/settings", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+env.userToken)
|
|
resp = httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusForbidden {
|
|
t.Fatalf("expected status 403, got %d", resp.Code)
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsVersionConflict(t *testing.T) {
|
|
env := setupAdminSettingsTestRouter(t)
|
|
router := env.router
|
|
|
|
payload := map[string]any{
|
|
"version": 0,
|
|
"matrix": map[string]map[string]bool{
|
|
"registration": {"admin": true},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/auth/admin/settings", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
|
resp := httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d", resp.Code)
|
|
}
|
|
|
|
// Replay the payload with the stale version.
|
|
req = httptest.NewRequest(http.MethodPost, "/api/auth/admin/settings", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
|
resp = httptest.NewRecorder()
|
|
router.ServeHTTP(resp, req)
|
|
if resp.Code != http.StatusConflict {
|
|
t.Fatalf("expected status 409, got %d", resp.Code)
|
|
}
|
|
var conflict struct {
|
|
Version uint64 `json:"version"`
|
|
}
|
|
if err := json.Unmarshal(resp.Body.Bytes(), &conflict); err != nil {
|
|
t.Fatalf("unmarshal conflict response: %v", err)
|
|
}
|
|
if conflict.Version != 1 {
|
|
t.Fatalf("expected current version 1, got %d", conflict.Version)
|
|
}
|
|
}
|