Files
cmbuyer/admin/internal/server/task_detail_test.go
T

111 lines
5.7 KiB
Go

package server_test
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"cmbuyer/admin/internal/deviceauth"
"cmbuyer/admin/internal/taskdetail"
)
const detailTaskID = "a3c9f507-7473-4fa6-8d71-8786c34c6301"
func TestTaskDetailRequiresAdminBeforeLookup(t *testing.T) {
details := &recordingDetailStore{detail: taskDetailFixture()}
router, _ := newRouterWithDependencies(t, &memoryStore{}, details, emptyEvidenceStore{}, deviceauth.RejectAllAuthenticator{})
response := serve(router, http.MethodGet, "/tasks/"+detailTaskID, nil, nil)
if response.Code != http.StatusSeeOther || !strings.HasPrefix(response.Header().Get("Location"), "/login?return_to=") || details.calls != 0 {
t.Fatalf("anonymous detail = %d/%q, calls=%d", response.Code, response.Header().Get("Location"), details.calls)
}
}
func TestTaskDetailFullPageAndDrawerShareAuditContent(t *testing.T) {
details := &recordingDetailStore{detail: taskDetailFixture()}
router, _ := newRouterWithDependencies(t, &memoryStore{}, details, emptyEvidenceStore{}, deviceauth.RejectAllAuthenticator{})
cookie := authenticate(t, router)
full := serve(router, http.MethodGet, "/tasks/"+detailTaskID, nil, cookie)
if full.Code != http.StatusOK || !strings.Contains(full.Body.String(), "<!doctype html>") || !strings.Contains(full.Body.String(), `data-task-detail-content`) {
t.Fatalf("full detail = %d/%q", full.Code, full.Body.String())
}
request := httptest.NewRequest(http.MethodGet, "/tasks/"+detailTaskID, nil)
request.AddCookie(cookie)
request.Header.Set("X-CMBuyer-View", "drawer")
request.Header.Set("Accept", "text/html")
request.Header.Set("Sec-Fetch-Site", "same-origin")
fragment := httptest.NewRecorder()
router.ServeHTTP(fragment, request)
if fragment.Code != http.StatusOK || strings.Contains(fragment.Body.String(), "<!doctype html>") || !strings.Contains(fragment.Body.String(), `data-task-detail-content`) {
t.Fatalf("fragment detail = %d/%q", fragment.Code, fragment.Body.String())
}
for _, text := range []string{"测试&lt;script&gt;", "订单已创建,系统尚未付款", "SKU_PANEL_GATE_1", "/evidence/b3c9f507-7473-4fa6-8d71-8786c34c6301", "暂无规格、价格或数量读数", "本页没有重试、再次提交或付款动作"} {
if !strings.Contains(full.Body.String(), text) || !strings.Contains(fragment.Body.String(), text) {
t.Fatalf("shared detail missing %q", text)
}
}
if strings.Contains(full.Body.String(), "<script>") || strings.Contains(fragment.Body.String(), "<script>") {
t.Fatal("task title was not HTML escaped")
}
if got := fragment.Header().Get("Vary"); got != "X-CMBuyer-View, Accept, Sec-Fetch-Site" {
t.Fatalf("fragment Vary = %q", got)
}
if details.calls != 2 {
t.Fatalf("detail store calls = %d, want 2", details.calls)
}
}
func TestTaskDetailRejectsForgedFragmentAndMissingTask(t *testing.T) {
details := &recordingDetailStore{err: taskdetail.ErrNotFound}
router, _ := newRouterWithDependencies(t, &memoryStore{}, details, emptyEvidenceStore{}, deviceauth.RejectAllAuthenticator{})
cookie := authenticate(t, router)
for name, headers := range map[string]map[string]string{
"unknown view": {"X-CMBuyer-View": "xml", "Accept": "text/html"},
"missing fetch site": {"X-CMBuyer-View": "drawer", "Accept": "text/html"},
"cross-site drawer": {"X-CMBuyer-View": "drawer", "Accept": "text/html", "Sec-Fetch-Site": "cross-site"},
"wrong accept": {"X-CMBuyer-View": "drawer", "Accept": "application/json", "Sec-Fetch-Site": "same-origin"},
"html quality zero": {"X-CMBuyer-View": "drawer", "Accept": "text/html;q=0, application/json", "Sec-Fetch-Site": "same-origin"},
"html substring mime": {"X-CMBuyer-View": "drawer", "Accept": "application/nottext/html", "Sec-Fetch-Site": "same-origin"},
} {
t.Run(name, func(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/tasks/"+detailTaskID, nil)
request.AddCookie(cookie)
for key, value := range headers {
request.Header.Set(key, value)
}
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code < 400 || response.Code >= 500 || response.Body.Len() != 0 {
t.Fatalf("forged fragment = %d/%q", response.Code, response.Body.String())
}
})
}
missing := serve(router, http.MethodGet, "/tasks/not-a-uuid", nil, cookie)
if missing.Code != http.StatusNotFound || missing.Body.Len() != 0 {
t.Fatalf("missing detail = %d/%q", missing.Code, missing.Body.String())
}
}
type recordingDetailStore struct {
detail taskdetail.Detail
err error
calls int
}
func (store *recordingDetailStore) Get(context.Context, string) (taskdetail.Detail, error) {
store.calls++
return store.detail, store.err
}
func taskDetailFixture() taskdetail.Detail {
started := time.Date(2026, 8, 4, 1, 2, 3, 0, time.UTC)
return taskdetail.Detail{
Task: taskdetail.Task{ID: detailTaskID, Source: "MANUAL", Title: "测试<script>", GoodsID: "937122477375", SKUColor: "黑色", SKUSize: "M", Quantity: 2, MaxTotalPrice: "30.00", Status: "WAITING_PAYMENT", Version: 3, CreatedAt: started, UpdatedAt: started},
Authorizations: []taskdetail.Authorization{{ID: "c3c9f507-7473-4fa6-8d71-8786c34c6301", Status: "FENCED", CreatedBy: "admin", TotalPriceCap: "30.00", TaskVersion: 2, CreatedAt: started, ExpiresAt: started.Add(time.Hour)}},
Attempts: []taskdetail.Attempt{{ID: "d3c9f507-7473-4fa6-8d71-8786c34c6301", AuthorizationID: "c3c9f507-7473-4fa6-8d71-8786c34c6301", Status: "CLAIMED", ClaimGeneration: 1, StartedAt: started}},
Evidence: []taskdetail.Evidence{{ID: "b3c9f507-7473-4fa6-8d71-8786c34c6301", AttemptID: "d3c9f507-7473-4fa6-8d71-8786c34c6301", Kind: "SKU_PANEL_GATE_1", PrivacyTier: "INTERNAL_RAW", SHA256: strings.Repeat("a", 64), ByteSize: 100, ContentType: "image/png", Width: 100, Height: 200, CapturedAt: started}},
}
}