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

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

176 lines
6.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 TestPausedProjectBlocksNewTicketsButAllowsCallingPostgresIntegration(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)
}
testDB := db.Begin()
if testDB.Error != nil {
t.Fatal(testDB.Error)
}
defer testDB.Rollback()
server, err := New(testDB, config.Config{
Environment: "development",
EncryptionKey: bytes.Repeat([]byte{0x81}, 32),
PhoneHMACKey: bytes.Repeat([]byte{0x82}, 32),
}, logger)
if err != nil {
t.Fatal(err)
}
now := time.Date(2026, 7, 30, 2, 0, 0, 0, time.UTC)
server.now = func() time.Time { return now }
projectID := uuid.NewString()
project := model.Project{
ID: projectID, Code: "PAUSE" + strings.ToUpper(uuid.NewString()[:6]), Name: "Pause calling regression",
Status: model.ProjectRunning, Timezone: "Asia/Shanghai", TicketPrefix: "A",
CallBatchSize: 1, CallMode: model.CallModeBoth,
MaxCallTicketCount: 100, DefaultCallPeopleCount: 1, MaxCallPeopleCount: 100,
MinPartySize: 1, MaxPartySize: 10, GracePeriodMinutes: 5,
ETAMode: model.ETAFixedBatch, AverageBatchIntervalSeconds: 60,
ContinuousRatePerMinute: 1, ETAIntervalSeconds: 60,
DeviceSimulationMode: "DISABLED", CreatedAt: now, UpdatedAt: now,
}
if err := testDB.Create(&project).Error; err != nil {
t.Fatal(err)
}
staff := model.User{
ID: uuid.NewString(), Username: "pause_staff_" + uuid.NewString()[:8], PasswordHash: "unused",
Role: model.RoleStaff, Active: true, CreatedAt: now, UpdatedAt: now,
}
if err := testDB.Create(&staff).Error; err != nil {
t.Fatal(err)
}
if err := testDB.Create(&model.UserProject{UserID: staff.ID, ProjectID: projectID, CreatedAt: now}).Error; err != nil {
t.Fatal(err)
}
businessDate, err := server.businessDateFor(project)
if err != nil {
t.Fatal(err)
}
session := model.QueueSession{
ID: uuid.NewString(), ProjectID: projectID, BusinessDate: businessDate,
Status: "RUNNING", NextTicketNumber: 2, Revision: 1,
OpenedAt: now, CreatedAt: now, UpdatedAt: now,
}
if err := testDB.Create(&session).Error; err != nil {
t.Fatal(err)
}
phone := "13800138000"
phoneCiphertext, phoneNonce, err := server.cipher.Encrypt(phone, []byte("phone:"+projectID))
if err != nil {
t.Fatal(err)
}
phoneHMAC := server.cipher.Digest(phone)
ticket := model.QueueTicket{
ID: uuid.NewString(), ProjectID: projectID, QueueSessionID: session.ID,
TicketNumber: 1, DisplayNumber: "00001", PartySize: 2,
PublicTokenHash: security.HashToken(uuid.NewString()),
PhoneCiphertext: phoneCiphertext, PhoneNonce: phoneNonce, PhoneHMAC: &phoneHMAC,
Honorific: "游客", Status: model.TicketWaiting, JoinedAt: now,
PersonalDataPurgeAt: now.Add(30 * 24 * time.Hour), CreatedBy: staff.ID,
CreatedAt: now, UpdatedAt: now,
}
if err := testDB.Create(&ticket).Error; err != nil {
t.Fatal(err)
}
if err := testDB.Model(&model.Project{}).Where("id = ?", projectID).
Updates(map[string]any{"status": model.ProjectPaused, "updated_at": now}).Error; err != nil {
t.Fatal(err)
}
t.Run("blocks staff ticket creation", func(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/staff/projects/"+projectID+"/tickets",
strings.NewReader(`{"phone":"13900139000","honorific":"游客","party_size":1}`))
request.SetPathValue("id", projectID)
request.Header.Set("Idempotency-Key", "paused-staff-ticket-"+uuid.NewString())
request = request.WithContext(context.WithValue(request.Context(), principalKey, principal{User: staff}))
server.createTicket(recorder, request)
if recorder.Code != http.StatusConflict || !strings.Contains(recorder.Body.String(), `"code":"PROJECT_NOT_RUNNING"`) {
t.Fatalf("status = %d, want 409 PROJECT_NOT_RUNNING; body = %s", recorder.Code, recorder.Body.String())
}
})
t.Run("blocks public ticket creation", func(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/public/projects/"+projectID+"/tickets",
strings.NewReader(`{"phone":"13700137000","honorific":"游客","party_size":1}`))
request.Header.Set("Idempotency-Key", "paused-public-ticket-"+uuid.NewString())
server.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusConflict || !strings.Contains(recorder.Body.String(), `"code":"PROJECT_NOT_RUNNING"`) {
t.Fatalf("status = %d, want 409 PROJECT_NOT_RUNNING; body = %s", recorder.Code, recorder.Body.String())
}
})
var ticketCount int64
if err := testDB.Model(&model.QueueTicket{}).Where("project_id = ?", projectID).Count(&ticketCount).Error; err != nil {
t.Fatal(err)
}
if ticketCount != 1 {
t.Fatalf("ticket count = %d, want the original ticket only", ticketCount)
}
t.Run("allows calling the existing queue", func(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/staff/projects/"+projectID+"/call-next",
strings.NewReader(`{"expected_revision":1,"mode":"TICKET","count":1}`))
request.SetPathValue("id", projectID)
request.Header.Set("Idempotency-Key", "paused-call-next-"+uuid.NewString())
request = request.WithContext(context.WithValue(request.Context(), principalKey, principal{User: staff}))
server.callNext(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body = %s", recorder.Code, recorder.Body.String())
}
var updatedTicket model.QueueTicket
if err := testDB.First(&updatedTicket, "id = ?", ticket.ID).Error; err != nil {
t.Fatal(err)
}
if updatedTicket.Status != model.TicketCalled {
t.Fatalf("ticket status = %s, want %s", updatedTicket.Status, model.TicketCalled)
}
})
}