package browser import ( "context" "errors" "net" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strconv" "strings" "testing" "chub/internal/domain" ) func TestCDPInspectorValidatesEndpointFromProfileFile(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/json/version" { t.Fatalf("path = %q", r.URL.Path) } _, _ = w.Write([]byte(`{"Browser":"Chrome/136.0.0.0"}`)) })) defer server.Close() parsed, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } _, portText, err := net.SplitHostPort(parsed.Host) if err != nil { t.Fatal(err) } profile := t.TempDir() if err := os.WriteFile(filepath.Join(profile, "DevToolsActivePort"), []byte(portText+"\n/devtools/browser/test\n"), 0o600); err != nil { t.Fatal(err) } endpoint, err := NewCDPInspector().InspectRemoteDebugEndpoint(context.Background(), domain.BrowserChrome, profile) if err != nil { t.Fatalf("InspectRemoteDebugEndpoint() error = %v", err) } port, _ := strconv.Atoi(portText) if endpoint.Port != port || endpoint.Browser != "Chrome/136.0.0.0" { t.Fatalf("endpoint = %#v", endpoint) } direct, err := NewCDPInspector().InspectRemoteDebugPort(context.Background(), domain.BrowserChrome, port) if err != nil || direct.Port != port { t.Fatalf("InspectRemoteDebugPort() = %#v, %v", direct, err) } } func TestCDPInspectorRejectsMismatchedAndMalformedEndpoints(t *testing.T) { profile := t.TempDir() if err := os.WriteFile(filepath.Join(profile, "DevToolsActivePort"), []byte("not-a-port\n/devtools/browser/test\n"), 0o600); err != nil { t.Fatal(err) } if _, err := NewCDPInspector().InspectRemoteDebugEndpoint(context.Background(), domain.BrowserChrome, profile); !errors.Is(err, ErrRemoteDebugEndpointUnavailable) { t.Fatalf("malformed endpoint error = %v", err) } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"Browser":"Edg/136.0.0.0"}`)) })) defer server.Close() parsed, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } _, portText, err := net.SplitHostPort(parsed.Host) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(profile, "DevToolsActivePort"), []byte(portText+"\n/devtools/browser/test\n"), 0o600); err != nil { t.Fatal(err) } if _, err := NewCDPInspector().InspectRemoteDebugEndpoint(context.Background(), domain.BrowserChrome, profile); !errors.Is(err, ErrRemoteDebugEndpointUnavailable) { t.Fatalf("mismatched endpoint error = %v", err) } } func TestCDPInspectorFindsNextAvailablePort(t *testing.T) { listener, err := net.Listen("tcp4", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() start := listener.Addr().(*net.TCPAddr).Port port, err := NewCDPInspector().FindAvailableRemoteDebugPort(context.Background(), start) if err != nil { t.Fatal(err) } if port != start+1 { t.Fatalf("port = %d, want %d", port, start+1) } } func TestCDPInspectorListsVerifiedTargetsWithoutControlEndpoints(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/json/version": _, _ = w.Write([]byte(`{"Browser":"Chrome/136.0.0.0"}`)) case "/json/list": _, _ = w.Write([]byte(`[ {"id":"page-1","type":"page","title":"运营后台","url":"https://user:secret@example.com/orders?token=hidden#section","webSocketDebuggerUrl":"ws://127.0.0.1/devtools/page/page-1"}, {"id":"worker-1","type":"service_worker","title":"","url":"chrome-extension://example/worker.js?key=hidden"} ]`)) default: t.Fatalf("path = %q", r.URL.Path) } })) defer server.Close() parsed, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } _, portText, err := net.SplitHostPort(parsed.Host) if err != nil { t.Fatal(err) } port, _ := strconv.Atoi(portText) targets, err := NewCDPInspector().ListRemoteDebugTargets(context.Background(), domain.BrowserChrome, port) if err != nil || len(targets) != 2 { t.Fatalf("ListRemoteDebugTargets() = %#v, %v", targets, err) } if targets[0].ID != "page-1" || targets[0].Type != "page" || targets[0].Title != "运营后台" || targets[0].URL != "https://example.com/orders" { t.Fatalf("first target = %#v", targets[0]) } if targets[1].URL != "chrome-extension://example/worker.js" { t.Fatalf("second target URL = %q", targets[1].URL) } } func TestCDPInspectorRejectsMalformedOrOversizedTargetList(t *testing.T) { for _, test := range []struct { name string status int response string }{ {name: "malformed JSON", status: http.StatusOK, response: `not-json`}, {name: "too many targets", status: http.StatusOK, response: `[` + strings.Repeat(`{"id":"page","type":"page"},`, remoteDebugTargetsMaxCount) + `{"id":"last","type":"page"}]`}, {name: "non-success status", status: http.StatusServiceUnavailable, response: `temporarily unavailable`}, } { t.Run(test.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/json/version" { _, _ = w.Write([]byte(`{"Browser":"Chrome/136.0.0.0"}`)) return } w.WriteHeader(test.status) _, _ = w.Write([]byte(test.response)) })) defer server.Close() parsed, err := url.Parse(server.URL) if err != nil { t.Fatal(err) } _, portText, err := net.SplitHostPort(parsed.Host) if err != nil { t.Fatal(err) } port, _ := strconv.Atoi(portText) if _, err := NewCDPInspector().ListRemoteDebugTargets(context.Background(), domain.BrowserChrome, port); !errors.Is(err, ErrRemoteDebugEndpointUnavailable) { t.Fatalf("ListRemoteDebugTargets() error = %v", err) } }) } }