package httpapi import ( "encoding/json" "net/http/httptest" "strings" "testing" "time" "calllinesystem/server/internal/config" "calllinesystem/server/internal/model" ) func TestDisplayBatchDTOCannotSerializePersonalFields(t *testing.T) { batch := model.CallBatch{BatchSequence: 7, Status: "CALLED", CalledAt: time.Unix(100, 0).UTC()} view := newDisplayBatchDTO(batch, []displayTicketDTO{{ TicketNumber: "00042", DisplayNumber: "00042", Status: model.TicketCalled, }}) body, err := json.Marshal(view) if err != nil { t.Fatal(err) } encoded := string(body) for _, forbidden := range []string{"honorific", "phone", "phone_last4", "last_name", "ticket_id", "joined_at", "created_by"} { if strings.Contains(encoded, forbidden) { t.Fatalf("public display DTO leaked forbidden field %q: %s", forbidden, encoded) } } if !strings.Contains(encoded, `"ticket_number":"00042"`) { t.Fatalf("public ticket number missing: %s", encoded) } } func TestPublicDisplayProjectViewOnlySerializesDisplayFields(t *testing.T) { view := publicDisplayProjectView(map[string]any{ "id": "project-1", "name": "东门观光车", "status": model.ProjectRunning, "waiting_count": 3, "waiting_ticket_count": 3, "waiting_people_count": 7, "issued_ticket_count": 15, "latest_ticket_number": "00015", "experienced_people": 12, "current_batch": map[string]any{"tickets": []map[string]any{{"ticket_number": "00013"}}}, "estimated_wait": map[string]any{"available": true}, "last_updated_at": time.Unix(100, 0).UTC(), "phone": "13800138000", "last_name": "张", "visitor_notice": "internal", "device_status": map[string]any{"status": "FAILURE"}, "display_token_hash": "secret", }) body, err := json.Marshal(view) if err != nil { t.Fatal(err) } encoded := string(body) for _, forbidden := range []string{"phone", "last_name", "visitor_notice", "device_status", "display_token_hash", "13800138000"} { if strings.Contains(encoded, forbidden) { t.Fatalf("public display overview leaked forbidden field %q: %s", forbidden, encoded) } } for _, required := range []string{`"name":"东门观光车"`, `"ticket_number":"00013"`, `"waiting_people_count":7`} { if !strings.Contains(encoded, required) { t.Fatalf("public display overview missing field %q: %s", required, encoded) } } } func TestNormalizeDisplayProjectCode(t *testing.T) { tests := []struct { input string want string ok bool }{ {input: "raft", want: "RAFT", ok: true}, {input: " east-ride ", want: "EAST-RIDE", ok: true}, {input: "A", ok: false}, {input: "bad code", ok: false}, {input: strings.Repeat("a", 40), ok: false}, } for _, test := range tests { got, ok := normalizeDisplayProjectCode(test.input) if got != test.want || ok != test.ok { t.Fatalf("normalizeDisplayProjectCode(%q) = %q, %v; want %q, %v", test.input, got, ok, test.want, test.ok) } } } func TestPublicPhoneLookupIsDisabledInProduction(t *testing.T) { server := &Server{config: config.Config{Environment: "production"}} recorder := httptest.NewRecorder() request := httptest.NewRequest("POST", "/api/public/status/search", strings.NewReader(`{"phone":"13800138000"}`)) server.publicStatusByPhone(recorder, request) if recorder.Code != 404 { t.Fatalf("production phone lookup status = %d, want 404", recorder.Code) } } func TestPublicPhoneLookupRejectsInvalidPhone(t *testing.T) { server := &Server{config: config.Config{Environment: "development"}} recorder := httptest.NewRecorder() request := httptest.NewRequest("POST", "/api/public/status/search", strings.NewReader(`{"phone":"123"}`)) server.publicStatusByPhone(recorder, request) if recorder.Code != 422 { t.Fatalf("invalid phone lookup status = %d, want 422", recorder.Code) } }