package launchcode_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "strings" "testing" "ant-chrome/backend/internal/automation" ) type mockAutomationStarter struct { *mockStarterWithParams scripts []automation.ScriptRecord runs []automation.ScriptRunRecord lastGetID string lastRunRequest automation.ScriptRunRequest lastRunListLimit int runResult *automation.ScriptRunRecord getErr error listErr error runErr error runListErr error } func newMockAutomationStarter() *mockAutomationStarter { return &mockAutomationStarter{ mockStarterWithParams: newMockStarterWithParams(), } } func (m *mockAutomationStarter) AutomationScriptList() ([]automation.ScriptRecord, error) { if m.listErr != nil { return nil, m.listErr } return append([]automation.ScriptRecord(nil), m.scripts...), nil } func (m *mockAutomationStarter) AutomationScriptGet(scriptID string) (*automation.ScriptRecord, error) { m.lastGetID = scriptID if m.getErr != nil { return nil, m.getErr } for _, item := range m.scripts { if item.ID == scriptID { record := item return &record, nil } } return nil, os.ErrNotExist } func (m *mockAutomationStarter) AutomationScriptRunWithOptions(input automation.ScriptRunRequest) (*automation.ScriptRunRecord, error) { m.lastRunRequest = input if m.runErr != nil { return nil, m.runErr } if m.runResult == nil { return &automation.ScriptRunRecord{ ID: "run-default", ScriptID: input.ScriptID, Status: "success", }, nil } record := *m.runResult return &record, nil } func (m *mockAutomationStarter) AutomationScriptRunList(limit int) ([]automation.ScriptRunRecord, error) { m.lastRunListLimit = limit if m.runListErr != nil { return nil, m.runListErr } items := append([]automation.ScriptRunRecord(nil), m.runs...) if limit > 0 && len(items) > limit { items = items[:limit] } return items, nil } func TestAutomationScriptsEndpointReturnsMetadata(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { ID: "news-query-txt", Name: "查询新闻并写 TXT", Description: "测试脚本", Type: "playwright-cdp", Status: "ready", EntryFile: "index.cjs", Tags: []string{"Playwright", "新闻"}, SelectorText: `{"code":"BUYER_001"}`, ParamsText: `{"keyword":"OpenAI","limit":10}`, ScriptText: `module.exports.run = async () => ({ ok: true })`, Notes: "note", CreatedAt: "2026-04-08T10:00:00Z", UpdatedAt: "2026-04-08T11:00:00Z", }, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodGet, "/api/automation/scripts", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } if strings.Contains(w.Body.String(), "scriptText") { t.Fatalf("公共脚本列表不应返回脚本文本: %s", w.Body.String()) } var resp struct { OK bool `json:"ok"` Data struct { Count int `json:"count"` Items []struct { ID string `json:"id"` Type string `json:"type"` Status string `json:"status"` Selector map[string]interface{} `json:"selector"` Params map[string]interface{} `json:"params"` } `json:"items"` } `json:"data"` } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("解析响应失败: %v", err) } if !resp.OK || resp.Data.Count != 1 || len(resp.Data.Items) != 1 { t.Fatalf("响应结构错误: %+v", resp) } item := resp.Data.Items[0] if item.ID != "news-query-txt" || item.Type != "playwright-cdp" || item.Status != "ready" { t.Fatalf("脚本元数据错误: %+v", item) } if item.Selector["code"] != "BUYER_001" { t.Fatalf("selector 解析错误: %+v", item.Selector) } if item.Params["keyword"] != "OpenAI" { t.Fatalf("params 解析错误: %+v", item.Params) } } func TestAutomationScriptDetailEndpointReturnsSingleScript(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { PackageFormat: "ant-automation-script", ManifestVersion: 1, ID: "news-query-txt", Name: "查询新闻并写 TXT", Description: "测试脚本", Type: "playwright-cdp", Status: "ready", EntryFile: "index.cjs", Tags: []string{"Playwright", "新闻"}, SelectorText: `{"code":"BUYER_001"}`, ParamsText: `{"keyword":"OpenAI","limit":10}`, ScriptText: `module.exports.run = async () => ({ ok: true })`, Notes: "note", Source: automation.ScriptSource{ Type: "git", URI: "https://example.com/repo.git", Ref: "main", }, CreatedAt: "2026-04-08T10:00:00Z", UpdatedAt: "2026-04-08T11:00:00Z", }, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodGet, "/api/automation/scripts/news-query-txt", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } if starter.lastGetID != "news-query-txt" { t.Fatalf("scriptId 路径解析错误: %s", starter.lastGetID) } if strings.Contains(w.Body.String(), "scriptText") { t.Fatalf("公共脚本详情不应返回脚本文本: %s", w.Body.String()) } var resp struct { OK bool `json:"ok"` Data struct { Item struct { ID string `json:"id"` PackageFormat string `json:"packageFormat"` ManifestVersion int `json:"manifestVersion"` Source automation.ScriptSource `json:"source"` Selector map[string]interface{} `json:"selector"` } `json:"item"` } `json:"data"` } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("解析响应失败: %v", err) } item := resp.Data.Item if !resp.OK || item.ID != "news-query-txt" { t.Fatalf("详情响应错误: %+v", resp) } if item.PackageFormat != "ant-automation-script" || item.ManifestVersion != 1 { t.Fatalf("详情元数据错误: %+v", item) } if item.Source.Type != "git" || item.Source.URI != "https://example.com/repo.git" { t.Fatalf("source 返回错误: %+v", item.Source) } if item.Selector["code"] != "BUYER_001" { t.Fatalf("selector 解析错误: %+v", item.Selector) } } func TestAutomationScriptDetailEndpointReturnsNotFound(t *testing.T) { handler := buildTestHandlerWithManager(newInMemoryService(), newMockAutomationStarter(), nil) req := httptest.NewRequest(http.MethodGet, "/api/automation/scripts/missing-script", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("期望 404,实际 %d,body=%s", w.Code, w.Body.String()) } if !strings.Contains(w.Body.String(), "script not found") { t.Fatalf("错误信息不正确: %s", w.Body.String()) } } func TestAutomationScriptRunEndpointConvertsObjectPayload(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.runResult = &automation.ScriptRunRecord{ ID: "run-1", ScriptID: "news-query-txt", Status: "success", ResultText: `{"ok":true,"summary":"done","result":{"subject":"Hello","contentText":"Mail body"}}`, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodPost, "/api/automation/scripts/run", bytes.NewBufferString(`{ "scriptId":"news-query-txt", "selector":{"code":"BUYER_001"}, "params":{"keyword":"OpenAI"} }`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } if starter.lastRunRequest.ScriptID != "news-query-txt" { t.Fatalf("scriptId 传递错误: %+v", starter.lastRunRequest) } if starter.lastRunRequest.UseScriptSelector || starter.lastRunRequest.UseScriptParams { t.Fatalf("对象参数应关闭脚本默认 selector/params: %+v", starter.lastRunRequest) } if starter.lastRunRequest.SelectorText != `{"code":"BUYER_001"}` { t.Fatalf("selectorText 转换错误: %s", starter.lastRunRequest.SelectorText) } if starter.lastRunRequest.ParamsText != `{"keyword":"OpenAI"}` { t.Fatalf("paramsText 转换错误: %s", starter.lastRunRequest.ParamsText) } var resp struct { OK bool `json:"ok"` Data struct { Result map[string]interface{} `json:"result"` Run struct { ID string `json:"id"` Status string `json:"status"` } `json:"run"` } `json:"data"` } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("解析响应失败: %v", err) } if !resp.OK || resp.Data.Run.ID != "run-1" || resp.Data.Run.Status != "success" { t.Fatalf("run 响应错误: %+v", resp) } if resp.Data.Result["subject"] != "Hello" || resp.Data.Result["contentText"] != "Mail body" { t.Fatalf("expected parsed result payload, got %+v", resp.Data.Result) } } func TestAutomationScriptRunEndpointUsesScriptDefaultsWhenFieldsOmitted(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodPost, "/api/automation/scripts/run", bytes.NewBufferString(`{"scriptId":"news-query-txt"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } if !starter.lastRunRequest.UseScriptSelector || !starter.lastRunRequest.UseScriptParams { t.Fatalf("缺省时应回退到脚本默认 selector/params: %+v", starter.lastRunRequest) } if starter.lastRunRequest.SelectorText != "" || starter.lastRunRequest.ParamsText != "" { t.Fatalf("缺省时不应透传 selectorText/paramsText: %+v", starter.lastRunRequest) } } func TestAutomationPublicHookStandardModeReturnsEnvelope(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { ID: "proton-mail-first-message", Name: "Proton 邮件搜索并读取最新邮件", PublicAPI: automation.ScriptPublicAPIConfig{ Enabled: true, Method: "POST", Path: "mail/proton-first-message", RequestMode: "standard", ResponseMode: "envelope", TimeoutMs: 120000, }, }, } starter.runResult = &automation.ScriptRunRecord{ ID: "run-hook-1", ScriptID: "proton-mail-first-message", ScriptName: "Proton 邮件搜索并读取最新邮件", Status: "success", Summary: "已返回最新命中邮件内容", ResultText: `{"ok":true,"result":{"verificationCode":"429792","recipientEmail":"target@example.com"}}`, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodPost, "/api/automation/hooks/mail/proton-first-message", bytes.NewBufferString(`{ "code":"BUYER_001", "params":{"recipientQuery":"target@example.com"} }`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } if starter.lastRunRequest.ScriptID != "proton-mail-first-message" { t.Fatalf("scriptId 透传错误: %+v", starter.lastRunRequest) } if starter.lastRunRequest.UseScriptSelector || starter.lastRunRequest.UseScriptParams { t.Fatalf("公共 Hook 应使用请求里的 code/param: %+v", starter.lastRunRequest) } if starter.lastRunRequest.SelectorText != `{"code":"BUYER_001"}` { t.Fatalf("selectorText 转换错误: %s", starter.lastRunRequest.SelectorText) } if starter.lastRunRequest.ParamsText != `{"recipientQuery":"target@example.com"}` { t.Fatalf("paramsText 转换错误: %s", starter.lastRunRequest.ParamsText) } var resp struct { OK bool `json:"ok"` Status string `json:"status"` Data map[string]interface{} `json:"data"` } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("解析响应失败: %v", err) } if !resp.OK || resp.Status != "success" { t.Fatalf("hook 响应错误: %+v", resp) } if resp.Data["verificationCode"] != "429792" { t.Fatalf("expected data payload, got %+v", resp.Data) } } func TestAutomationPublicHookParamsOnlyModeReturnsResultOnly(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { ID: "proton-mail-first-message", Name: "Proton 邮件搜索并读取最新邮件", PublicAPI: automation.ScriptPublicAPIConfig{ Enabled: true, Method: "POST", Path: "mail/proton-result-only", RequestMode: "params-only", ResponseMode: "result-only", TimeoutMs: 45000, }, }, } starter.runResult = &automation.ScriptRunRecord{ ID: "run-hook-2", ScriptID: "proton-mail-first-message", ScriptName: "Proton 邮件搜索并读取最新邮件", Status: "success", Summary: "已返回最新命中邮件内容", ResultText: `{"ok":true,"result":{"verificationCode":"429792","mailboxName":"ChatGPT"}}`, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodPost, "/api/automation/hooks/mail/proton-result-only?timeoutMs=60000", bytes.NewBufferString(`{ "code":"BUYER_001", "params":{"recipientQuery":"target@example.com"} }`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } if starter.lastRunRequest.UseScriptSelector || starter.lastRunRequest.UseScriptParams { t.Fatalf("公共 Hook 应透传 code/param: %+v", starter.lastRunRequest) } if starter.lastRunRequest.ParamsText != `{"recipientQuery":"target@example.com"}` { t.Fatalf("paramsText 转换错误: %s", starter.lastRunRequest.ParamsText) } if starter.lastRunRequest.TimeoutMs != 60000 { t.Fatalf("timeoutMs 透传错误: %+v", starter.lastRunRequest) } var resp map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("解析响应失败: %v", err) } data, ok := resp["data"].(map[string]interface{}) if !ok || data["verificationCode"] != "429792" || data["mailboxName"] != "ChatGPT" { t.Fatalf("expected data payload, got %+v", resp) } } func TestAutomationPublicHookResultOnlyCompactsDownloadFields(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { ID: "grok-image-generate-download", Name: "Grok 生成图片并下载", PublicAPI: automation.ScriptPublicAPIConfig{ Enabled: true, Method: "POST", Path: "image/grok-generate-download", RequestMode: "params-only", ResponseMode: "result-only", TimeoutMs: 300000, }, }, } starter.runResult = &automation.ScriptRunRecord{ ID: "run-grok-image", ScriptID: "grok-image-generate-download", ScriptName: "Grok 生成图片并下载", Status: "success", Summary: "Grok 图片已生成并下载", ResultText: `{"ok":true,"downloadAddress":"D:/tmp/grok.png","downloadPath":"D:/tmp/grok.png","sourceImageUrl":"https://example.com/image.png","steps":[{"step":"open"}],"startedAt":"2026-06-03T00:00:00Z"}`, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodPost, "/api/automation/hooks/image/grok-generate-download", bytes.NewBufferString(`{"code":"BUYER_001","params":{"prompt":"ant"}}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } var resp map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("解析响应失败: %v", err) } data, ok := resp["data"].(map[string]interface{}) if !ok || data["downloadAddress"] != "D:/tmp/grok.png" || data["downloadPath"] != "D:/tmp/grok.png" || data["sourceImageUrl"] != "https://example.com/image.png" { t.Fatalf("下载字段缺失: %+v", resp) } if _, exists := data["steps"]; exists { t.Fatalf("不应返回冗余 steps: %+v", resp) } if _, exists := data["runId"]; exists { t.Fatalf("不应返回 runId: %+v", resp) } } func TestAutomationPublicHookAppliesRequestBodyVariables(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { ID: "image-generate", Name: "图片生成", ParamsText: `{"prompt":"默认提示词","selectors":{"promptInput":"#prompt-textarea","generatedImage":"img.generated"},"timeoutMs":300000}`, PublicAPI: automation.ScriptPublicAPIConfig{ Enabled: true, Method: "POST", Path: "image/generate", RequestMode: "standard", ResponseMode: "envelope", TimeoutMs: 300000, RequestBodyText: `{"code":"{{code}}","params":{"prompt":"{{prompt}}","outputFileName":"{{outputFileName}}"}}`, Variables: []automation.ScriptPublicAPIVariable{ {Name: "prompt", DefaultValue: "默认提示词", Required: true}, {Name: "outputFileName", DefaultValue: "generated-image.png"}, }, }, }, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodPost, "/api/automation/hooks/image/generate", bytes.NewBufferString(`{ "code":"BUYER_001", "params":{"prompt":"海边的机器人","outputFileName":"robot.png"} }`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } if starter.lastRunRequest.UseScriptParams { t.Fatalf("变量模板应生成 params: %+v", starter.lastRunRequest) } if starter.lastRunRequest.SelectorText != `{"code":"BUYER_001"}` { t.Fatalf("变量模板应生成 selector: %+v", starter.lastRunRequest) } var params map[string]interface{} if err := json.Unmarshal([]byte(starter.lastRunRequest.ParamsText), ¶ms); err != nil { t.Fatalf("解析 paramsText 失败: %v", err) } if params["prompt"] != "海边的机器人" || params["outputFileName"] != "robot.png" { t.Fatalf("变量未映射到 params: %+v", params) } selectors, ok := params["selectors"].(map[string]interface{}) if !ok || selectors["promptInput"] != "#prompt-textarea" || selectors["generatedImage"] != "img.generated" { t.Fatalf("默认 selectors 不应被变量模板覆盖丢失: %+v", params) } if params["timeoutMs"] != float64(300000) { t.Fatalf("默认 timeoutMs 不应被变量模板覆盖丢失: %+v", params) } if starter.lastRunRequest.TimeoutMs != 300000 { t.Fatalf("timeoutMs 错误: %+v", starter.lastRunRequest) } } func TestAutomationPublicHookReturnsNotFoundWhenDisabled(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { ID: "disabled-hook", Name: "Disabled Hook", PublicAPI: automation.ScriptPublicAPIConfig{ Enabled: false, Path: "mail/disabled-hook", }, }, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodPost, "/api/automation/hooks/mail/disabled-hook", bytes.NewBufferString(`{}`)) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("期望 404,实际 %d,body=%s", w.Code, w.Body.String()) } } func TestAutomationPublicHookRejectsLegacyParamAndTopLevelVariables(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { ID: "strict-hook", Name: "Strict Hook", PublicAPI: automation.ScriptPublicAPIConfig{ Enabled: true, Method: "POST", Path: "mail/strict-hook", TimeoutMs: 120000, }, }, } handler := buildTestHandlerWithManager(svc, starter, nil) cases := []struct { name string body string }{ {name: "legacy-param", body: `{"code":"BUYER_001","param":{"recipientQuery":"target@example.com"}}`}, {name: "top-level-variable", body: `{"code":"BUYER_001","recipientQuery":"target@example.com"}`}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/api/automation/hooks/mail/strict-hook", bytes.NewBufferString(tc.body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("期望 400,实际 %d,body=%s", w.Code, w.Body.String()) } var resp struct { OK bool `json:"ok"` Error struct { Code string `json:"code"` } `json:"error"` } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("解析响应失败: %v", err) } if resp.OK || resp.Error.Code != "invalid_request" { t.Fatalf("错误 envelope 不正确: %+v", resp) } }) } } func TestAutomationPublicHookRejectsInvalidTimeout(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.scripts = []automation.ScriptRecord{ { ID: "strict-hook-timeout", Name: "Strict Hook Timeout", PublicAPI: automation.ScriptPublicAPIConfig{ Enabled: true, Method: "POST", Path: "mail/strict-hook-timeout", TimeoutMs: 120000, }, }, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodPost, "/api/automation/hooks/mail/strict-hook-timeout", bytes.NewBufferString(`{"code":"BUYER_001","params":{},"timeoutMs":999}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("期望 400,实际 %d,body=%s", w.Code, w.Body.String()) } if !strings.Contains(w.Body.String(), "timeoutMs must be between 1000 and 1800000") { t.Fatalf("错误信息不正确: %s", w.Body.String()) } } func TestAutomationScriptRunsEndpointPassesLimit(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() starter.runs = []automation.ScriptRunRecord{ {ID: "run-1", ScriptID: "script-a", Status: "success"}, {ID: "run-2", ScriptID: "script-b", Status: "failed"}, } handler := buildTestHandlerWithManager(svc, starter, nil) req := httptest.NewRequest(http.MethodGet, "/api/automation/scripts/runs?limit=1", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("期望 200,实际 %d,body=%s", w.Code, w.Body.String()) } if starter.lastRunListLimit != 1 { t.Fatalf("limit 透传错误: %d", starter.lastRunListLimit) } var resp struct { OK bool `json:"ok"` Data struct { Count int `json:"count"` Items []automation.ScriptRunRecord `json:"items"` } `json:"data"` } if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("解析响应失败: %v", err) } if !resp.OK || resp.Data.Count != 1 || len(resp.Data.Items) != 1 { t.Fatalf("runs 响应错误: %+v", resp) } } func TestAutomationScriptAPIUnavailableReturnsServiceUnavailable(t *testing.T) { handler := buildTestHandlerWithManager(newInMemoryService(), newMockStarterWithParams(), nil) req := httptest.NewRequest(http.MethodGet, "/api/automation/scripts", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusServiceUnavailable { t.Fatalf("期望 503,实际 %d,body=%s", w.Code, w.Body.String()) } } func TestAutomationScriptRunEndpointRejectsInvalidBody(t *testing.T) { svc := newInMemoryService() starter := newMockAutomationStarter() handler := buildTestHandlerWithManager(svc, starter, nil) t.Run("invalid-json", func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/api/automation/scripts/run", bytes.NewBufferString("{bad json}")) w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("期望 400,实际 %d,body=%s", w.Code, w.Body.String()) } }) t.Run("selector-must-be-object", func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/api/automation/scripts/run", bytes.NewBufferString(`{ "scriptId":"news-query-txt", "selector":"BUYER_001" }`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("期望 400,实际 %d,body=%s", w.Code, w.Body.String()) } if !strings.Contains(w.Body.String(), "selector must be a JSON object") { t.Fatalf("错误信息不正确: %s", w.Body.String()) } }) t.Run("timeout-must-be-in-range", func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/api/automation/scripts/run", bytes.NewBufferString(`{ "scriptId":"news-query-txt", "timeoutMs":1800001 }`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("期望 400,实际 %d,body=%s", w.Code, w.Body.String()) } if !strings.Contains(w.Body.String(), "timeoutMs must be between 1000 and 1800000") { t.Fatalf("错误信息不正确: %s", w.Body.String()) } }) }