package httpapi import ( "bytes" "context" "encoding/json" "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 TestCurrentDayViewsIgnoreYesterdayRunningSessionPostgresIntegration(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{ Environment: "development", EncryptionKey: bytes.Repeat([]byte{0x71}, 32), PhoneHMACKey: bytes.Repeat([]byte{0x72}, 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 } shanghai := time.FixedZone("Asia/Shanghai", 8*60*60) yesterday := time.Date(2026, 7, 29, 0, 0, 0, 0, shanghai) displayToken := uuid.NewString() + uuid.NewString() displayTokenHash := security.HashToken(displayToken) projectID := uuid.NewString() project := model.Project{ ID: projectID, Code: "ROLL" + strings.ToUpper(uuid.NewString()[:6]), Name: "Rollover 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, ExperiencedPeopleStart: 10, DisplayTokenHash: &displayTokenHash, DeviceSimulationMode: "DISABLED", CreatedAt: now, UpdatedAt: now, } if err := db.Create(&project).Error; err != nil { t.Fatal(err) } user := model.User{ ID: uuid.NewString(), Username: "roll_" + uuid.NewString()[:8], PasswordHash: "unused", Role: model.RoleStaff, Active: true, CreatedAt: now, UpdatedAt: now, } if err := db.Create(&user).Error; err != nil { t.Fatal(err) } if err := db.Create(&model.UserProject{UserID: user.ID, ProjectID: projectID, CreatedAt: now}).Error; err != nil { t.Fatal(err) } session := model.QueueSession{ ID: uuid.NewString(), ProjectID: projectID, BusinessDate: yesterday, Status: "RUNNING", NextTicketNumber: 2, Revision: 1, OpenedAt: yesterday, CreatedAt: yesterday, UpdatedAt: yesterday, } if err := db.Create(&session).Error; err != nil { t.Fatal(err) } phone, err := security.NormalizePhone("13800138000") if err != nil { t.Fatal(err) } 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: yesterday, PersonalDataPurgeAt: now.Add(30 * 24 * time.Hour), CreatedBy: user.ID, CreatedAt: yesterday, UpdatedAt: yesterday, } if err := db.Create(&ticket).Error; err != nil { t.Fatal(err) } t.Run("staff queue", func(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/api/staff/projects/"+projectID+"/queue", nil) request.SetPathValue("id", projectID) request = request.WithContext(context.WithValue(request.Context(), principalKey, principal{User: user})) recorder := httptest.NewRecorder() server.queueSnapshot(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String()) } var response struct { Session any `json:"session"` Metrics struct { WaitingTickets int64 `json:"waiting_ticket_count"` WaitingPeople int64 `json:"waiting_people_count"` } `json:"metrics"` } if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { t.Fatal(err) } if response.Session != nil || response.Metrics.WaitingTickets != 0 || response.Metrics.WaitingPeople != 0 { t.Fatalf("today staff queue reused yesterday: %#v", response) } }) t.Run("display", func(t *testing.T) { recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/display/"+displayToken+"/snapshot", nil) server.Handler().ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String()) } var response displaySnapshotDTO if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { t.Fatal(err) } if response.WaitingTicketCount != 0 || response.WaitingPeopleCount != 0 { t.Fatalf("today display reused yesterday: %#v", response) } if response.ExperiencedPeople != int64(project.ExperiencedPeopleStart) { t.Fatalf("experienced people = %d, want configured start %d", response.ExperiencedPeople, project.ExperiencedPeopleStart) } }) t.Run("display by project code", func(t *testing.T) { recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/display/"+strings.ToLower(project.Code)+"/snapshot", nil) server.Handler().ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String()) } var response displaySnapshotDTO if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { t.Fatal(err) } if response.Project.ID != project.ID { t.Fatalf("project = %q, want %q", response.Project.ID, project.ID) } }) t.Run("admin project", func(t *testing.T) { projection, _, _, _, _, err := server.adminProjectProjection(context.Background(), project) if err != nil { t.Fatal(err) } if projection["waiting_ticket_count"] != int64(0) || projection["waiting_people_count"] != int64(0) { t.Fatalf("today admin project reused yesterday: %#v", projection) } if projection["experienced_people"] != int64(project.ExperiencedPeopleStart) { t.Fatalf("experienced people = %#v, want configured start %d", projection["experienced_people"], project.ExperiencedPeopleStart) } }) t.Run("admin overview", func(t *testing.T) { recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, "/api/admin/overview", nil) server.adminOverview(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String()) } var response struct { Summary struct { WaitingTickets int64 `json:"waiting_ticket_count"` WaitingPeople int64 `json:"waiting_people_count"` } `json:"summary"` Totals struct { ActiveSessions int64 `json:"active_sessions"` WaitingTickets int64 `json:"waiting_ticket_count"` WaitingPeople int64 `json:"waiting_people_count"` } `json:"totals"` Projects []struct { ID string `json:"id"` WaitingTickets int64 `json:"waiting_ticket_count"` WaitingPeople int64 `json:"waiting_people_count"` } `json:"projects"` ActiveTickets []struct { ID string `json:"id"` } `json:"active_tickets"` } if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { t.Fatal(err) } if response.Summary.WaitingTickets != 0 || response.Summary.WaitingPeople != 0 || response.Totals.ActiveSessions != 0 || response.Totals.WaitingTickets != 0 || response.Totals.WaitingPeople != 0 { t.Fatalf("today admin overview totals reused yesterday data: %#v", response) } for _, item := range response.Projects { if item.ID == projectID && (item.WaitingTickets != 0 || item.WaitingPeople != 0) { t.Fatalf("today admin overview reused yesterday project data: %#v", item) } } for _, item := range response.ActiveTickets { if item.ID == ticket.ID { t.Fatalf("today admin overview returned yesterday active ticket: %#v", item) } } }) t.Run("phone lookup", func(t *testing.T) { recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodPost, "/api/internal/status/search", strings.NewReader(`{"phone":"13800138000"}`)) server.statusByPhone(recorder, request, false) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String()) } var response struct { Tickets []json.RawMessage `json:"tickets"` } if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { t.Fatal(err) } if len(response.Tickets) != 0 { t.Fatalf("today phone lookup returned yesterday tickets: %s", recorder.Body.String()) } }) }