mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
channel: master version: 1.1.0 source-ref: master published-at-utc: 2026-03-29T11:32:04Z
329 lines
10 KiB
Go
329 lines
10 KiB
Go
package launchcode_test
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
|
||
"ant-chrome/backend/internal/browser"
|
||
"ant-chrome/backend/internal/config"
|
||
)
|
||
|
||
func TestListProfilesAPIIncludesLaunchCodes(t *testing.T) {
|
||
svc := newInMemoryService()
|
||
mgr := newProfileCreateTestManager(t, func(cfg *config.Config) {
|
||
cfg.Browser.Proxies = []config.BrowserProxy{
|
||
{
|
||
ProxyId: "proxy-us",
|
||
ProxyName: "US Residential",
|
||
ProxyConfig: "socks5://127.0.0.1:1080",
|
||
},
|
||
}
|
||
})
|
||
starter := &managerBackedStarter{mgr: mgr}
|
||
handler := buildTestHandlerWithManager(svc, starter, mgr)
|
||
|
||
first, err := mgr.Create(browser.ProfileInput{
|
||
ProfileName: "buyer-a",
|
||
ProxyId: "proxy-us",
|
||
Tags: []string{"电商"},
|
||
Keywords: []string{"buyer-a"},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("创建测试实例失败: %v", err)
|
||
}
|
||
second, err := mgr.Create(browser.ProfileInput{
|
||
ProfileName: "buyer-b",
|
||
ProxyConfig: "http://127.0.0.1:8080",
|
||
Keywords: []string{"buyer-b"},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("创建测试实例失败: %v", err)
|
||
}
|
||
if _, err := svc.SetCode(first.ProfileId, "BUYER_A"); err != nil {
|
||
t.Fatalf("设置 launchCode 失败: %v", err)
|
||
}
|
||
|
||
req := httptest.NewRequest(http.MethodGet, "/api/profiles", nil)
|
||
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 struct {
|
||
OK bool `json:"ok"`
|
||
Count int `json:"count"`
|
||
Items []browser.Profile `json:"items"`
|
||
}
|
||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||
t.Fatalf("解析响应失败: %v", err)
|
||
}
|
||
if !resp.OK || resp.Count != 2 || len(resp.Items) != 2 {
|
||
t.Fatalf("列表响应错误: %+v", resp)
|
||
}
|
||
|
||
seen := make(map[string]browser.Profile, len(resp.Items))
|
||
for _, item := range resp.Items {
|
||
if item.LaunchCode == "" {
|
||
t.Fatalf("列表应返回 launchCode: %+v", resp.Items)
|
||
}
|
||
seen[item.ProfileId] = item
|
||
}
|
||
if _, ok := seen[first.ProfileId]; !ok {
|
||
t.Fatalf("列表缺少第一个实例: %+v", resp.Items)
|
||
}
|
||
if _, ok := seen[second.ProfileId]; !ok {
|
||
t.Fatalf("列表缺少第二个实例: %+v", resp.Items)
|
||
}
|
||
}
|
||
|
||
func TestGetProfileAPIReturnsProfileByID(t *testing.T) {
|
||
svc := newInMemoryService()
|
||
mgr := newProfileCreateTestManager(t, nil)
|
||
starter := &managerBackedStarter{mgr: mgr}
|
||
handler := buildTestHandlerWithManager(svc, starter, mgr)
|
||
|
||
profile, err := mgr.Create(browser.ProfileInput{
|
||
ProfileName: "buyer-get",
|
||
ProxyConfig: "http://127.0.0.1:8080",
|
||
Tags: []string{"北美"},
|
||
Keywords: []string{"buyer-get"},
|
||
GroupId: "group-get",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("创建测试实例失败: %v", err)
|
||
}
|
||
if _, err := svc.SetCode(profile.ProfileId, "BUYER_GET"); err != nil {
|
||
t.Fatalf("设置 launchCode 失败: %v", err)
|
||
}
|
||
|
||
req := httptest.NewRequest(http.MethodGet, "/api/profiles/"+profile.ProfileId, nil)
|
||
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 struct {
|
||
OK bool `json:"ok"`
|
||
LaunchCode string `json:"launchCode"`
|
||
Profile *browser.Profile `json:"profile"`
|
||
}
|
||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||
t.Fatalf("解析响应失败: %v", err)
|
||
}
|
||
if !resp.OK || resp.Profile == nil {
|
||
t.Fatalf("查询响应错误: %+v", resp)
|
||
}
|
||
if resp.LaunchCode != "BUYER_GET" || resp.Profile.GroupId != "group-get" {
|
||
t.Fatalf("查询字段错误: %+v", resp)
|
||
}
|
||
}
|
||
|
||
func TestUpdateProfileAPIUpdatesFieldsAndAutoLaunches(t *testing.T) {
|
||
svc := newInMemoryService()
|
||
mgr := newProfileCreateTestManager(t, func(cfg *config.Config) {
|
||
cfg.Browser.Proxies = []config.BrowserProxy{
|
||
{
|
||
ProxyId: "proxy-us",
|
||
ProxyName: "US Residential",
|
||
ProxyConfig: "socks5://127.0.0.1:1080",
|
||
},
|
||
}
|
||
})
|
||
starter := &managerBackedStarter{mgr: mgr}
|
||
handler := buildTestHandlerWithManager(svc, starter, mgr)
|
||
|
||
profile, err := mgr.Create(browser.ProfileInput{
|
||
ProfileName: "buyer-old",
|
||
ProxyConfig: "http://127.0.0.1:8080",
|
||
Keywords: []string{"buyer-old"},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("创建测试实例失败: %v", err)
|
||
}
|
||
if _, err := svc.SetCode(profile.ProfileId, "BUYER_OLD"); err != nil {
|
||
t.Fatalf("设置 launchCode 失败: %v", err)
|
||
}
|
||
|
||
payload := bytes.NewBufferString(`{
|
||
"profile": {
|
||
"profileName": "buyer-new",
|
||
"userDataDir": "buyers/buyer-new",
|
||
"proxyId": "proxy-us",
|
||
"launchArgs": ["--lang=en-US"],
|
||
"tags": ["电商", "北美"],
|
||
"keywords": ["buyer-new", "amazon"],
|
||
"groupId": "group-sales-us"
|
||
},
|
||
"launchCode": "BUYER_NEW",
|
||
"autoLaunch": true,
|
||
"start": {
|
||
"launchArgs": ["--window-size=1280,800"],
|
||
"startUrls": ["https://example.com/order"],
|
||
"skipDefaultStartUrls": true
|
||
}
|
||
}`)
|
||
|
||
req := httptest.NewRequest(http.MethodPut, "/api/profiles/"+profile.ProfileId, payload)
|
||
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 len(starter.started) != 1 {
|
||
t.Fatalf("更新后应自动启动 1 次,实际 %+v", starter.started)
|
||
}
|
||
if len(starter.lastParams.StartURLs) != 1 || starter.lastParams.StartURLs[0] != "https://example.com/order" {
|
||
t.Fatalf("startUrls 未透传: %+v", starter.lastParams)
|
||
}
|
||
|
||
updated, status, errMsg := handlerProfileSnapshot(t, mgr, svc, profile.ProfileId)
|
||
if errMsg != "" || status != http.StatusOK {
|
||
t.Fatalf("读取更新后实例失败: status=%d err=%s", status, errMsg)
|
||
}
|
||
if updated.ProfileName != "buyer-new" || updated.ProxyId != "proxy-us" || updated.ProxyConfig != "socks5://127.0.0.1:1080" {
|
||
t.Fatalf("更新未生效: %+v", updated)
|
||
}
|
||
if updated.GroupId != "group-sales-us" || updated.LaunchCode != "BUYER_NEW" || !updated.Running {
|
||
t.Fatalf("更新后的分组/launchCode/运行状态错误: %+v", updated)
|
||
}
|
||
}
|
||
|
||
func TestUpdateProfileAPIRollsBackOnDuplicateLaunchCode(t *testing.T) {
|
||
svc := newInMemoryService()
|
||
mgr := newProfileCreateTestManager(t, nil)
|
||
starter := &managerBackedStarter{mgr: mgr}
|
||
handler := buildTestHandlerWithManager(svc, starter, mgr)
|
||
|
||
first, err := mgr.Create(browser.ProfileInput{
|
||
ProfileName: "buyer-first",
|
||
ProxyConfig: "http://127.0.0.1:8080",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("创建测试实例失败: %v", err)
|
||
}
|
||
second, err := mgr.Create(browser.ProfileInput{
|
||
ProfileName: "buyer-second",
|
||
ProxyConfig: "http://127.0.0.1:9090",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("创建测试实例失败: %v", err)
|
||
}
|
||
if _, err := svc.SetCode(first.ProfileId, "BUYER_FIRST"); err != nil {
|
||
t.Fatalf("设置 launchCode 失败: %v", err)
|
||
}
|
||
if _, err := svc.SetCode(second.ProfileId, "BUYER_SECOND"); err != nil {
|
||
t.Fatalf("设置 launchCode 失败: %v", err)
|
||
}
|
||
|
||
payload := bytes.NewBufferString(`{
|
||
"profile": {
|
||
"profileName": "buyer-first-updated",
|
||
"proxyConfig": "http://127.0.0.1:10080",
|
||
"keywords": ["buyer-first-updated"]
|
||
},
|
||
"launchCode": "BUYER_SECOND"
|
||
}`)
|
||
|
||
req := httptest.NewRequest(http.MethodPut, "/api/profiles/"+first.ProfileId, payload)
|
||
req.Header.Set("Content-Type", "application/json")
|
||
w := httptest.NewRecorder()
|
||
handler.ServeHTTP(w, req)
|
||
|
||
if w.Code != http.StatusConflict {
|
||
t.Fatalf("期望 409,实际 %d,body=%s", w.Code, w.Body.String())
|
||
}
|
||
|
||
current, status, errMsg := handlerProfileSnapshot(t, mgr, svc, first.ProfileId)
|
||
if errMsg != "" || status != http.StatusOK {
|
||
t.Fatalf("读取回滚后实例失败: status=%d err=%s", status, errMsg)
|
||
}
|
||
if current.ProfileName != "buyer-first" || current.ProxyConfig != "http://127.0.0.1:8080" || current.LaunchCode != "BUYER_FIRST" {
|
||
t.Fatalf("launchCode 冲突后应回滚更新: %+v", current)
|
||
}
|
||
}
|
||
|
||
func TestDeleteProfileAPIRemovesProfileAndLaunchCode(t *testing.T) {
|
||
svc := newInMemoryService()
|
||
mgr := newProfileCreateTestManager(t, nil)
|
||
starter := &managerBackedStarter{mgr: mgr}
|
||
handler := buildTestHandlerWithManager(svc, starter, mgr)
|
||
|
||
profile, err := mgr.Create(browser.ProfileInput{ProfileName: "buyer-delete"})
|
||
if err != nil {
|
||
t.Fatalf("创建测试实例失败: %v", err)
|
||
}
|
||
if _, err := svc.SetCode(profile.ProfileId, "BUYER_DELETE"); err != nil {
|
||
t.Fatalf("设置 launchCode 失败: %v", err)
|
||
}
|
||
|
||
req := httptest.NewRequest(http.MethodDelete, "/api/profiles/"+profile.ProfileId, 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 _, ok := mgr.Profiles[profile.ProfileId]; ok {
|
||
t.Fatalf("实例删除后仍存在于内存: %s", profile.ProfileId)
|
||
}
|
||
if _, err := svc.Resolve("BUYER_DELETE"); err == nil {
|
||
t.Fatal("删除后 launchCode 仍可解析")
|
||
}
|
||
}
|
||
|
||
func TestDeleteProfileAPIRejectsRunningProfile(t *testing.T) {
|
||
svc := newInMemoryService()
|
||
mgr := newProfileCreateTestManager(t, nil)
|
||
starter := &managerBackedStarter{mgr: mgr}
|
||
handler := buildTestHandlerWithManager(svc, starter, mgr)
|
||
|
||
profile, err := mgr.Create(browser.ProfileInput{ProfileName: "buyer-running"})
|
||
if err != nil {
|
||
t.Fatalf("创建测试实例失败: %v", err)
|
||
}
|
||
mgr.Profiles[profile.ProfileId].Running = true
|
||
|
||
req := httptest.NewRequest(http.MethodDelete, "/api/profiles/"+profile.ProfileId, nil)
|
||
w := httptest.NewRecorder()
|
||
handler.ServeHTTP(w, req)
|
||
|
||
if w.Code != http.StatusConflict {
|
||
t.Fatalf("期望 409,实际 %d,body=%s", w.Code, w.Body.String())
|
||
}
|
||
if _, ok := mgr.Profiles[profile.ProfileId]; !ok {
|
||
t.Fatalf("运行中实例不应被删除: %s", profile.ProfileId)
|
||
}
|
||
}
|
||
|
||
func handlerProfileSnapshot(t *testing.T, mgr *browser.Manager, svc interface {
|
||
EnsureCode(profileID string) (string, error)
|
||
}, profileID string) (*browser.Profile, int, string) {
|
||
t.Helper()
|
||
|
||
mgr.Mutex.Lock()
|
||
profile, ok := mgr.Profiles[profileID]
|
||
var snapshot browser.Profile
|
||
if ok && profile != nil {
|
||
snapshot = *profile
|
||
}
|
||
mgr.Mutex.Unlock()
|
||
if !ok {
|
||
return nil, http.StatusNotFound, "profile not found"
|
||
}
|
||
if snapshot.LaunchCode == "" {
|
||
if code, err := svc.EnsureCode(snapshot.ProfileId); err == nil {
|
||
snapshot.LaunchCode = code
|
||
}
|
||
}
|
||
return &snapshot, http.StatusOK, ""
|
||
}
|