Files
XQKqueue/server/internal/httpapi/auth_integration_test.go
brother7 17d296263c 修复账号禁用及队列场次状态规则
问题与需求:后台禁用员工后旧会话仍可继续访问;实时队列跨天误读昨日场次;项目暂停后需禁止取号但允许叫号。

修复思路:账号权限变更时撤销会话并同步前端登录态;实时查询统一按项目时区当天场次过滤;拆分取号与叫号的状态校验,并补充前后端及 PostgreSQL 回归测试。
2026-07-30 12:02:05 +08:00

143 lines
4.3 KiB
Go

package httpapi
import (
"bytes"
"context"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"calllinesystem/server/internal/config"
"calllinesystem/server/internal/database"
"calllinesystem/server/internal/model"
"calllinesystem/server/internal/security"
"github.com/google/uuid"
)
func TestDisablingStaffRevokesSessionsAndBlocksLoginPostgresIntegration(t *testing.T) {
dsn := strings.TrimSpace(os.Getenv("TEST_DATABASE_URL"))
if dsn == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
db, err := database.Open(ctx, dsn, logger)
if err != nil {
t.Fatal(err)
}
defer database.Close(db)
sqlDB, err := database.SQLDB(db)
if err != nil {
t.Fatal(err)
}
if err := database.Migrate(ctx, sqlDB, logger); err != nil {
t.Fatal(err)
}
server, err := New(db, config.Config{
EncryptionKey: bytes.Repeat([]byte{0x61}, 32),
PhoneHMACKey: bytes.Repeat([]byte{0x62}, 32),
SessionCookieName: "queue_session",
SessionTTL: time.Hour,
}, logger)
if err != nil {
t.Fatal(err)
}
now := time.Date(2026, 7, 30, 10, 0, 0, 0, time.UTC)
server.now = func() time.Time { return now }
password := "StaffPassword123!"
passwordHash, err := security.HashPassword(password)
if err != nil {
t.Fatal(err)
}
admin := model.User{
ID: uuid.NewString(), Username: "auth_admin_" + uuid.NewString()[:8],
PasswordHash: passwordHash, Role: model.RoleAdmin, Active: true,
CreatedAt: now, UpdatedAt: now,
}
staff := model.User{
ID: uuid.NewString(), Username: "auth_staff_" + uuid.NewString()[:8],
PasswordHash: passwordHash, Role: model.RoleStaff, Active: true,
CreatedAt: now, UpdatedAt: now,
}
if err := db.Create(&admin).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&staff).Error; err != nil {
t.Fatal(err)
}
login := func() *httptest.ResponseRecorder {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/staff/auth/login",
strings.NewReader(`{"username":"`+staff.Username+`","password":"`+password+`"}`))
server.Handler().ServeHTTP(recorder, request)
return recorder
}
firstLogin := login()
if firstLogin.Code != http.StatusOK {
t.Fatalf("initial login status = %d, want 200; body = %s", firstLogin.Code, firstLogin.Body.String())
}
var staffCookie *http.Cookie
for _, cookie := range firstLogin.Result().Cookies() {
if cookie.Name == server.authCookieName(model.RoleStaff) {
staffCookie = cookie
break
}
}
if staffCookie == nil {
t.Fatal("initial login did not set the staff session cookie")
}
updateRequest := httptest.NewRequest(http.MethodPut, "/api/admin/users/"+staff.ID,
strings.NewReader(`{"role":"STAFF","active":false,"project_ids":[]}`))
updateRequest.SetPathValue("id", staff.ID)
updateRequest = updateRequest.WithContext(context.WithValue(updateRequest.Context(), principalKey, principal{User: admin}))
updateRecorder := httptest.NewRecorder()
server.updateAdminUser(updateRecorder, updateRequest)
if updateRecorder.Code != http.StatusOK {
t.Fatalf("disable status = %d, want 200; body = %s", updateRecorder.Code, updateRecorder.Body.String())
}
var disabledStaff model.User
if err := db.First(&disabledStaff, "id = ?", staff.ID).Error; err != nil {
t.Fatal(err)
}
if disabledStaff.Active {
t.Error("staff remains active after disabling")
}
var activeSessions int64
if err := db.Model(&model.AuthSession{}).
Where("user_id = ? AND revoked_at IS NULL", staff.ID).
Count(&activeSessions).Error; err != nil {
t.Fatal(err)
}
if activeSessions != 0 {
t.Errorf("active sessions after disabling staff = %d, want 0", activeSessions)
}
meRecorder := httptest.NewRecorder()
meRequest := httptest.NewRequest(http.MethodGet, "/api/staff/auth/me", nil)
meRequest.AddCookie(staffCookie)
server.Handler().ServeHTTP(meRecorder, meRequest)
if meRecorder.Code != http.StatusUnauthorized {
t.Errorf("existing session after disabling status = %d, want 401; body = %s", meRecorder.Code, meRecorder.Body.String())
}
secondLogin := login()
if secondLogin.Code != http.StatusUnauthorized {
t.Fatalf("login after disabling status = %d, want 401; body = %s", secondLogin.Code, secondLogin.Body.String())
}
}