Files
Ant-Browser/backend/test/launchcode/server_automation_public_hook_test.go
T

455 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package launchcode_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"ant-chrome/backend/internal/automation"
)
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(`{
"instance":{"type":"existing","selector":{"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,实际 %dbody=%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.TargetMode != "existing" || starter.lastRunRequest.SelectorText != "" {
t.Fatalf("instance 转换错误: %+v", starter.lastRunRequest)
}
if targetInput := automationRunTargetInputJSON(t, starter.lastRunRequest.TargetInput); !strings.Contains(targetInput, `"code":"BUYER_001"`) {
t.Fatalf("targetInput 转换错误: %s", targetInput)
}
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 TestAutomationPublicHookSupportsInstanceModes(t *testing.T) {
cases := []struct {
name string
body string
expectedMode string
expectedSelector bool
expectedInput string
useScriptTarget bool
}{
{
name: "rotate",
body: `{
"instance":{"type":"rotate","selector":{"groupId":"group-a","tags":["chatgpt"]}},
"params":{"recipientQuery":"target@example.com"}
}`,
expectedMode: "rotate",
expectedInput: `"groupId":"group-a"`,
},
{
name: "create",
body: `{
"instance":{"type":"create","templateSelector":{"code":"TEMPLATE_001"},"createNameTemplate":"ChatGPT-Image-${timestamp}"},
"params":{"recipientQuery":"target@example.com"}
}`,
expectedMode: "create",
expectedInput: `"createNameTemplate":"ChatGPT-Image-${timestamp}"`,
},
{
name: "script-default",
body: `{
"instance":{"type":"script-default"},
"params":{"recipientQuery":"target@example.com"}
}`,
useScriptTarget: true,
},
{
name: "legacy-code",
body: `{
"code":"BUYER_001",
"params":{"recipientQuery":"target@example.com"}
}`,
expectedSelector: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(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,
},
},
}
handler := buildTestHandlerWithManager(svc, starter, nil)
req := httptest.NewRequest(http.MethodPost, "/api/automation/hooks/mail/proton-first-message", bytes.NewBufferString(tc.body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("期望 200,实际 %dbody=%s", w.Code, w.Body.String())
}
if starter.lastRunRequest.TargetMode != tc.expectedMode {
t.Fatalf("targetMode 错误: %+v", starter.lastRunRequest)
}
if starter.lastRunRequest.UseScriptSelector != tc.useScriptTarget {
t.Fatalf("useScriptSelector 错误: %+v", starter.lastRunRequest)
}
if tc.expectedInput != "" {
targetInput := automationRunTargetInputJSON(t, starter.lastRunRequest.TargetInput)
if !strings.Contains(targetInput, tc.expectedInput) {
t.Fatalf("targetInput 转换错误: %s", targetInput)
}
}
if tc.expectedSelector && starter.lastRunRequest.SelectorText != `{"code":"BUYER_001"}` {
t.Fatalf("legacy selectorText 转换错误: %+v", starter.lastRunRequest)
}
})
}
}
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,实际 %dbody=%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,实际 %dbody=%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: `{"instance":{"type":"existing","selector":{"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,实际 %dbody=%s", w.Code, w.Body.String())
}
if starter.lastRunRequest.UseScriptParams {
t.Fatalf("变量模板应生成 params: %+v", starter.lastRunRequest)
}
if starter.lastRunRequest.TargetMode != "existing" || starter.lastRunRequest.SelectorText != "" {
t.Fatalf("变量模板应生成 instance target: %+v", starter.lastRunRequest)
}
if targetInput := automationRunTargetInputJSON(t, starter.lastRunRequest.TargetInput); !strings.Contains(targetInput, `"code":"BUYER_001"`) {
t.Fatalf("变量模板 targetInput 错误: %s", targetInput)
}
var params map[string]interface{}
if err := json.Unmarshal([]byte(starter.lastRunRequest.ParamsText), &params); 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,实际 %dbody=%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,实际 %dbody=%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,实际 %dbody=%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())
}
}