mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-26 04:23:33 +00:00
281 lines
7.5 KiB
Go
281 lines
7.5 KiB
Go
package browser
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// --- resolveToIPv4 ---
|
|
|
|
func TestResolveToIPv4_IPLiteral(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{"127.0.0.1", "127.0.0.1"},
|
|
{"192.168.1.1", "192.168.1.1"},
|
|
{"::1", "::1"},
|
|
}
|
|
for _, tt := range tests {
|
|
got, err := resolveToIPv4(tt.input)
|
|
if err != nil {
|
|
t.Errorf("resolveToIPv4(%q) unexpected error: %v", tt.input, err)
|
|
continue
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("resolveToIPv4(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveToIPv4_Localhost(t *testing.T) {
|
|
ip, err := resolveToIPv4("localhost")
|
|
if err != nil {
|
|
t.Fatalf("resolveToIPv4(localhost) error: %v", err)
|
|
}
|
|
// Should resolve to 127.0.0.1 (IPv4 preferred)
|
|
parsed := net.ParseIP(ip)
|
|
if parsed == nil {
|
|
t.Fatalf("resolveToIPv4(localhost) returned non-IP: %q", ip)
|
|
}
|
|
if parsed.To4() == nil {
|
|
t.Logf("resolveToIPv4(localhost) returned IPv6 %q (no IPv4 available)", ip)
|
|
}
|
|
}
|
|
|
|
func TestResolveToIPv4_UnknownHost(t *testing.T) {
|
|
_, err := resolveToIPv4("this-host-definitely-does-not-exist.invalid")
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown host, got nil")
|
|
}
|
|
}
|
|
|
|
// --- resolveRemoteCDP ---
|
|
|
|
func TestResolveRemoteCDP_Success(t *testing.T) {
|
|
// Start a fake Chrome /json/version endpoint.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/json/version" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/abc-123",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// Extract host:port from test server URL.
|
|
wsURL := "ws://" + srv.Listener.Addr().String()
|
|
|
|
got, err := resolveRemoteCDP(wsURL)
|
|
if err != nil {
|
|
t.Fatalf("resolveRemoteCDP(%q) error: %v", wsURL, err)
|
|
}
|
|
|
|
// Should contain the devtools path.
|
|
if !strings.Contains(got, "/devtools/browser/abc-123") {
|
|
t.Errorf("resolveRemoteCDP result missing devtools path: %q", got)
|
|
}
|
|
// Should be a ws:// URL.
|
|
if !strings.HasPrefix(got, "ws://") {
|
|
t.Errorf("resolveRemoteCDP result should start with ws://: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveRemoteCDP_NonOKStatus(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("Chrome is not ready"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
wsURL := "ws://" + srv.Listener.Addr().String()
|
|
_, err := resolveRemoteCDP(wsURL)
|
|
if err == nil {
|
|
t.Fatal("expected error for 500 status, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "HTTP 500") {
|
|
t.Errorf("error should mention HTTP 500: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveRemoteCDP_EmptyWebSocketURL(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"Browser": "HeadlessChrome/124.0",
|
|
// webSocketDebuggerUrl intentionally missing
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
wsURL := "ws://" + srv.Listener.Addr().String()
|
|
_, err := resolveRemoteCDP(wsURL)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty webSocketDebuggerUrl, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "empty webSocketDebuggerUrl") {
|
|
t.Errorf("error should mention empty URL: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveRemoteCDP_InvalidJSON(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("not json"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
wsURL := "ws://" + srv.Listener.Addr().String()
|
|
_, err := resolveRemoteCDP(wsURL)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid JSON, got nil")
|
|
}
|
|
}
|
|
|
|
func TestResolveRemoteCDP_ConnectionRefused(t *testing.T) {
|
|
// Use a port that's definitely not listening.
|
|
_, err := resolveRemoteCDP("ws://127.0.0.1:1")
|
|
if err == nil {
|
|
t.Fatal("expected error for connection refused, got nil")
|
|
}
|
|
}
|
|
|
|
func TestResolveRemoteCDP_InvalidURL(t *testing.T) {
|
|
_, err := resolveRemoteCDP("://invalid")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid URL, got nil")
|
|
}
|
|
}
|
|
|
|
func TestResolveRemoteCDP_DefaultPort(t *testing.T) {
|
|
// Verify that when port is omitted, 9222 is used.
|
|
// This will fail to connect but the error should reference port 9222.
|
|
_, err := resolveRemoteCDP("ws://127.0.0.1")
|
|
if err == nil {
|
|
t.Fatal("expected error (nothing on 9222), got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "9222") {
|
|
t.Errorf("error should reference default port 9222: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveRemoteCDP_HostReplacement(t *testing.T) {
|
|
// Chrome returns ws://127.0.0.1/... but we need the server's actual IP:port.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/json/version" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
// Simulate Chrome returning localhost in the WS URL.
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"webSocketDebuggerUrl": "ws://localhost:9999/devtools/browser/xyz",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
wsURL := "ws://" + srv.Listener.Addr().String()
|
|
got, err := resolveRemoteCDP(wsURL)
|
|
if err != nil {
|
|
t.Fatalf("resolveRemoteCDP(%q) error: %v", wsURL, err)
|
|
}
|
|
|
|
// The host in the result should be the test server's address, NOT localhost:9999.
|
|
if strings.Contains(got, "localhost:9999") {
|
|
t.Errorf("host should be replaced but still has localhost:9999: %q", got)
|
|
}
|
|
if !strings.Contains(got, "/devtools/browser/xyz") {
|
|
t.Errorf("path should be preserved: %q", got)
|
|
}
|
|
}
|
|
|
|
// --- Manager options ---
|
|
|
|
func TestManagerOptions(t *testing.T) {
|
|
m := New(
|
|
WithHeadless(true),
|
|
WithRemoteURL("ws://chrome:9222"),
|
|
WithActionTimeout(2*time.Second),
|
|
WithIdleTimeout(0),
|
|
WithMaxPages(7),
|
|
)
|
|
if !m.headless {
|
|
t.Error("WithHeadless(true) not applied")
|
|
}
|
|
if m.remoteURL != "ws://chrome:9222" {
|
|
t.Errorf("WithRemoteURL not applied: %q", m.remoteURL)
|
|
}
|
|
status := m.Status()
|
|
if status.ActionTimeoutMs != 2000 {
|
|
t.Errorf("ActionTimeoutMs = %d, want 2000", status.ActionTimeoutMs)
|
|
}
|
|
if status.IdleTimeoutMs != 0 {
|
|
t.Errorf("IdleTimeoutMs = %d, want 0", status.IdleTimeoutMs)
|
|
}
|
|
if status.MaxPages != 7 {
|
|
t.Errorf("MaxPages = %d, want 7", status.MaxPages)
|
|
}
|
|
}
|
|
|
|
func TestManagerStopWhenNil(t *testing.T) {
|
|
m := New()
|
|
// Stop on a fresh manager should be a no-op.
|
|
if err := m.Close(); err != nil {
|
|
t.Errorf("Close() on nil browser should be nil, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestManagerStatusWhenStopped(t *testing.T) {
|
|
m := New()
|
|
status := m.Status()
|
|
if status.Running {
|
|
t.Error("Status.Running should be false when browser is nil")
|
|
}
|
|
}
|
|
|
|
func TestBrowserScopeKeyIncludesTenantUserAndAgent(t *testing.T) {
|
|
scope := BrowserScope{
|
|
TenantID: "tenant-a",
|
|
UserID: "user-a",
|
|
AgentID: "agent-a",
|
|
}
|
|
got := scope.Key()
|
|
for _, want := range []string{"tenant=tenant-a", "user=user-a", "agent=agent-a"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("scope key %q missing %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBrowserScopeKeyFallsBackToTenantForLegacyContext(t *testing.T) {
|
|
ctx := WithTenantID(context.Background(), "tenant-a")
|
|
if got := tenantIDFromCtx(ctx); got != "tenant-a" {
|
|
t.Fatalf("tenantIDFromCtx legacy key = %q, want tenant-a", got)
|
|
}
|
|
}
|
|
|
|
func TestGetPageForTenantRejectsUnownedEmptyTarget(t *testing.T) {
|
|
m := New()
|
|
m.pages["master-tab"] = nil
|
|
|
|
_, err := m.getPageForTenant("", "tenant-a")
|
|
if err == nil {
|
|
t.Fatal("expected scoped caller with no owned tabs to be denied")
|
|
}
|
|
}
|
|
|
|
func TestGetPageForTenantRejectsUnownedExplicitTarget(t *testing.T) {
|
|
m := New()
|
|
m.pages["master-tab"] = nil
|
|
|
|
_, err := m.getPageForTenant("master-tab", "tenant-a")
|
|
if err == nil {
|
|
t.Fatal("expected scoped caller to be denied unowned tab")
|
|
}
|
|
}
|