168 lines
6.6 KiB
Go
168 lines
6.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"calllinesystem/server/internal/model"
|
|
)
|
|
|
|
func TestValidateAdminProjectRequestNormalizesProjectProfile(t *testing.T) {
|
|
got, err := validateAdminProjectRequest(adminProjectRequest{Name: " 云岭漂流 ", Code: "ride-east", Timezone: "Asia/Shanghai", TicketPrefix: "q"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Name != "云岭漂流" || got.Code != "RIDE-EAST" || got.Timezone != "Asia/Shanghai" || got.TicketPrefix != "Q" {
|
|
t.Fatalf("normalized project = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestValidateAdminProjectRequestUsesDefaultTimezone(t *testing.T) {
|
|
got, err := validateAdminProjectRequest(adminProjectRequest{Name: "漂流", Code: "RIDE", TicketPrefix: "A"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Timezone != "Asia/Shanghai" {
|
|
t.Fatalf("default timezone = %q", got.Timezone)
|
|
}
|
|
}
|
|
|
|
func TestValidateAdminProjectRequestRejectsInvalidProfile(t *testing.T) {
|
|
tests := []adminProjectRequest{
|
|
{Name: "", Code: "RIDE", Timezone: "Asia/Shanghai", TicketPrefix: "A"},
|
|
{Name: "漂流", Code: "x", Timezone: "Asia/Shanghai", TicketPrefix: "A"},
|
|
{Name: "漂流", Code: "RIDE", Timezone: "Mars/Base", TicketPrefix: "A"},
|
|
{Name: "漂流", Code: "RIDE", Timezone: "Asia/Shanghai", TicketPrefix: "A-"},
|
|
}
|
|
for _, input := range tests {
|
|
if _, err := validateAdminProjectRequest(input); err == nil {
|
|
t.Fatalf("expected invalid project request: %#v", input)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRemovesLastActiveAdmin(t *testing.T) {
|
|
admin := model.User{Role: model.RoleAdmin, Active: true}
|
|
if !removesLastActiveAdmin(admin, model.RoleStaff, true, 0) {
|
|
t.Fatal("demoting the last active admin must be rejected")
|
|
}
|
|
if !removesLastActiveAdmin(admin, model.RoleAdmin, false, 0) {
|
|
t.Fatal("disabling the last active admin must be rejected")
|
|
}
|
|
if removesLastActiveAdmin(admin, model.RoleStaff, true, 1) {
|
|
t.Fatal("demotion is allowed when another active admin remains")
|
|
}
|
|
if removesLastActiveAdmin(admin, model.RoleAdmin, true, 0) {
|
|
t.Fatal("keeping the last admin active must be allowed")
|
|
}
|
|
}
|
|
|
|
func TestAdminUserViewMarksPermanentSuperAdminProtected(t *testing.T) {
|
|
view := adminUserView(model.User{Username: model.SuperAdminUsername}, nil)
|
|
if view["protected"] != true {
|
|
t.Fatalf("super admin view must be protected: %#v", view)
|
|
}
|
|
regular := adminUserView(model.User{Username: "admin02"}, nil)
|
|
if regular["protected"] != false {
|
|
t.Fatalf("regular admin view must remain editable: %#v", regular)
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectSettingsVisitorNotice(t *testing.T) {
|
|
notice := " 请在入口附近等候。 "
|
|
updates, err := validateProjectSettings(updateProjectSettingsRequest{VisitorNotice: ¬ice})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if updates["visitor_notice"] != "请在入口附近等候。" {
|
|
t.Fatalf("visitor notice was not trimmed: %#v", updates)
|
|
}
|
|
tooLong := strings.Repeat("字", 241)
|
|
if _, err := validateProjectSettings(updateProjectSettingsRequest{VisitorNotice: &tooLong}); err == nil {
|
|
t.Fatal("expected visitor notice length validation")
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectSettingsETAInterval(t *testing.T) {
|
|
interval := 90
|
|
updates, err := validateProjectSettings(updateProjectSettingsRequest{ETAIntervalSeconds: &interval})
|
|
if err != nil || updates["eta_interval_seconds"] != 90 {
|
|
t.Fatalf("unexpected interval result: %#v, %v", updates, err)
|
|
}
|
|
invalid := 0
|
|
if _, err := validateProjectSettings(updateProjectSettingsRequest{ETAIntervalSeconds: &invalid}); err == nil {
|
|
t.Fatal("expected interval validation")
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectSettingsExperiencedPeopleStart(t *testing.T) {
|
|
start := 120
|
|
updates, err := validateProjectSettings(updateProjectSettingsRequest{ExperiencedPeopleStart: &start})
|
|
if err != nil || updates["experienced_people_start"] != 120 {
|
|
t.Fatalf("unexpected experienced people start result: %#v, %v", updates, err)
|
|
}
|
|
negative := -1
|
|
if _, err := validateProjectSettings(updateProjectSettingsRequest{ExperiencedPeopleStart: &negative}); err == nil {
|
|
t.Fatal("expected negative experienced people start to be rejected")
|
|
}
|
|
tooLarge := 1000000001
|
|
if _, err := validateProjectSettings(updateProjectSettingsRequest{ExperiencedPeopleStart: &tooLarge}); err == nil {
|
|
t.Fatal("expected oversized experienced people start to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectSettingsPeopleAndCallModes(t *testing.T) {
|
|
mode := model.CallModeBoth
|
|
minParty, maxParty := 1, 8
|
|
ticketDefault, ticketLimit := 5, 20
|
|
peopleDefault, peopleLimit := 12, 40
|
|
updates, err := validateProjectSettings(updateProjectSettingsRequest{
|
|
CallMode: &mode, MinPartySize: &minParty, MaxPartySize: &maxParty,
|
|
DefaultCallTicketCount: &ticketDefault, MaxCallTicketCount: &ticketLimit,
|
|
DefaultCallPeopleCount: &peopleDefault, MaxCallPeopleCount: &peopleLimit,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
project := model.Project{CallBatchSize: 1, MaxCallTicketCount: 100, DefaultCallPeopleCount: 1, MaxCallPeopleCount: 100, MinPartySize: 1, MaxPartySize: 10}
|
|
if err := validateProjectSettingsCombination(project, updates); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if updates["call_mode"] != model.CallModeBoth || updates["max_party_size"] != 8 || updates["call_batch_size"] != 5 {
|
|
t.Fatalf("unexpected updates: %#v", updates)
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectSettingsAllowsTicketDefaultAboveLegacyLimit(t *testing.T) {
|
|
value := 250
|
|
updates, err := validateProjectSettings(updateProjectSettingsRequest{DefaultCallTicketCount: &value})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if updates["call_batch_size"] != 250 {
|
|
t.Fatalf("updates = %#v", updates)
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectSettingsRejectsInvalidCombinedRanges(t *testing.T) {
|
|
project := model.Project{CallBatchSize: 5, MaxCallTicketCount: 10, DefaultCallPeopleCount: 5, MaxCallPeopleCount: 10, MinPartySize: 1, MaxPartySize: 6}
|
|
if err := validateProjectSettingsCombination(project, map[string]any{"max_call_ticket_count": 4}); err == nil {
|
|
t.Fatal("expected ticket default above limit to fail")
|
|
}
|
|
if err := validateProjectSettingsCombination(project, map[string]any{"min_party_size": 7}); err == nil {
|
|
t.Fatal("expected invalid party range to fail")
|
|
}
|
|
}
|
|
|
|
func TestProjectAllowsConfiguredCallModes(t *testing.T) {
|
|
if !projectAllowsCallMode(model.CallModeBoth, model.CallModeTicket) || !projectAllowsCallMode(model.CallModeBoth, model.CallModePeople) {
|
|
t.Fatal("BOTH must allow ticket and people calls")
|
|
}
|
|
if !projectAllowsCallMode(model.CallModeTicket, model.CallModeTicket) || projectAllowsCallMode(model.CallModeTicket, model.CallModePeople) {
|
|
t.Fatal("TICKET must allow only ticket calls")
|
|
}
|
|
if !projectAllowsCallMode(model.CallModePeople, model.CallModePeople) || projectAllowsCallMode(model.CallModePeople, model.CallModeTicket) {
|
|
t.Fatal("PEOPLE must allow only people calls")
|
|
}
|
|
}
|