111 lines
5.7 KiB
Go
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{"测试<script>", "订单已创建,系统尚未付款", "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}},
|
|
}
|
|
}
|