91973ed6f9
browser_list ahora reporta si cada Chromium master se lanzo en modo headless, detectado por el flag de arranque (--headless / --headless=new / --headless=old) leido del cmdline. Una sola llamada devuelve navegadores activos + CDP + headless, sin tener que conectar a cada pagina para fingerprintear. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
226 lines
7.7 KiB
Go
226 lines
7.7 KiB
Go
package main
|
|
|
|
import "testing"
|
|
|
|
// TestParseChromiumMaster cubre la deteccion de master: solo procesos chromium con
|
|
// --user-data-dir y SIN --type= cuentan; el resto (wrapper sin udd, children con
|
|
// --type=, no-chromium) se descartan. Tambien valida que profile/cdp_port se
|
|
// extraen y que has_cdp refleja la presencia del flag.
|
|
func TestParseChromiumMaster(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
wantOK bool
|
|
wantProfile string
|
|
wantUDD string
|
|
wantPort string
|
|
wantHasCDP bool
|
|
}{
|
|
{
|
|
name: "master con CDP y profile",
|
|
args: []string{
|
|
"/usr/lib/chromium/chromium",
|
|
"--user-data-dir=/home/u/.config/chromium-cdp",
|
|
"--profile-directory=Personal",
|
|
"--remote-debugging-port=9222",
|
|
"--remote-allow-origins=*",
|
|
},
|
|
wantOK: true,
|
|
wantProfile: "Personal",
|
|
wantUDD: "/home/u/.config/chromium-cdp",
|
|
wantPort: "9222",
|
|
wantHasCDP: true,
|
|
},
|
|
{
|
|
name: "master humano sin CDP",
|
|
args: []string{
|
|
"/usr/lib/chromium/chromium",
|
|
"--user-data-dir=/home/u/.config/chromium-cdp",
|
|
"--profile-directory=Default",
|
|
},
|
|
wantOK: true,
|
|
wantProfile: "Default",
|
|
wantUDD: "/home/u/.config/chromium-cdp",
|
|
wantPort: "",
|
|
wantHasCDP: false,
|
|
},
|
|
{
|
|
name: "child renderer con --type= se descarta",
|
|
args: []string{
|
|
"/usr/lib/chromium/chromium",
|
|
"--type=renderer",
|
|
"--user-data-dir=/home/u/.config/chromium-cdp",
|
|
},
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "child gpu-process con --type= se descarta",
|
|
args: []string{
|
|
"/usr/lib/chromium/chromium",
|
|
"--type=gpu-process",
|
|
"--user-data-dir=/home/u/.config/chromium-cdp",
|
|
"--profile-directory=Personal",
|
|
},
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "chromium sin --user-data-dir se descarta",
|
|
args: []string{"/usr/lib/chromium/chromium", "--profile-directory=Personal"},
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "proceso no-chromium se descarta",
|
|
args: []string{"/usr/bin/firefox", "--user-data-dir=/x"},
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "argv vacio se descarta",
|
|
args: nil,
|
|
wantOK: false,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m, ok := parseChromiumMaster(1234, tc.args)
|
|
if ok != tc.wantOK {
|
|
t.Fatalf("ok = %v, want %v", ok, tc.wantOK)
|
|
}
|
|
if !ok {
|
|
return
|
|
}
|
|
if m.PID != 1234 {
|
|
t.Errorf("PID = %d, want 1234", m.PID)
|
|
}
|
|
if m.Profile != tc.wantProfile {
|
|
t.Errorf("Profile = %q, want %q", m.Profile, tc.wantProfile)
|
|
}
|
|
if m.UserDataDir != tc.wantUDD {
|
|
t.Errorf("UserDataDir = %q, want %q", m.UserDataDir, tc.wantUDD)
|
|
}
|
|
if m.CDPPort != tc.wantPort {
|
|
t.Errorf("CDPPort = %q, want %q", m.CDPPort, tc.wantPort)
|
|
}
|
|
if m.HasCDP != tc.wantHasCDP {
|
|
t.Errorf("HasCDP = %v, want %v", m.HasCDP, tc.wantHasCDP)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFlagValue valida el parseo exacto de "--name=value".
|
|
func TestFlagValue(t *testing.T) {
|
|
args := []string{"--user-data-dir=/x/y", "--profile-directory=Work", "--flag-without-value"}
|
|
if v, ok := flagValue(args, "user-data-dir"); !ok || v != "/x/y" {
|
|
t.Errorf("user-data-dir = (%q,%v), want (/x/y,true)", v, ok)
|
|
}
|
|
if v, ok := flagValue(args, "profile-directory"); !ok || v != "Work" {
|
|
t.Errorf("profile-directory = (%q,%v), want (Work,true)", v, ok)
|
|
}
|
|
if _, ok := flagValue(args, "remote-debugging-port"); ok {
|
|
t.Errorf("remote-debugging-port should be absent")
|
|
}
|
|
// Prefijo no debe hacer match parcial: "user-data" != "user-data-dir".
|
|
if _, ok := flagValue(args, "user-data"); ok {
|
|
t.Errorf("partial prefix user-data should NOT match user-data-dir")
|
|
}
|
|
}
|
|
|
|
// TestMatchMaster valida la prioridad pid > cdp_port > profile y el no-match.
|
|
func TestMatchMaster(t *testing.T) {
|
|
masters := []chromiumMaster{
|
|
{PID: 100, Profile: "Personal", CDPPort: ""},
|
|
{PID: 200, Profile: "Work", CDPPort: "9222"},
|
|
{PID: 300, Profile: "Personal", CDPPort: "9333"},
|
|
}
|
|
|
|
if m, ok := matchMaster(masters, browserCloseArgs{PID: 200}); !ok || m.PID != 200 {
|
|
t.Errorf("by pid: got (%d,%v), want (200,true)", m.PID, ok)
|
|
}
|
|
if m, ok := matchMaster(masters, browserCloseArgs{CDPPort: 9333}); !ok || m.PID != 300 {
|
|
t.Errorf("by cdp_port: got (%d,%v), want (300,true)", m.PID, ok)
|
|
}
|
|
// profile "Personal" tiene dos: gana el primero (PID 100).
|
|
if m, ok := matchMaster(masters, browserCloseArgs{Profile: "Personal"}); !ok || m.PID != 100 {
|
|
t.Errorf("by profile: got (%d,%v), want (100,true)", m.PID, ok)
|
|
}
|
|
if _, ok := matchMaster(masters, browserCloseArgs{PID: 999}); ok {
|
|
t.Errorf("unknown pid should not match")
|
|
}
|
|
if _, ok := matchMaster(masters, browserCloseArgs{Profile: "Nope"}); ok {
|
|
t.Errorf("unknown profile should not match")
|
|
}
|
|
}
|
|
|
|
// TestParseCmdline cubre el parsing de /proc/<pid>/cmdline en sus dos formatos:
|
|
// el canonico separado por NUL y el colapsado por espacios que produce Chromium
|
|
// al reescribir su titulo de proceso in-place. El segundo caso es el que rompia
|
|
// browser_list (los flags quedaban dentro de un unico argv[0] gigante).
|
|
func TestParseCmdline(t *testing.T) {
|
|
// Caso canonico: argv separado por NUL (proceso normal).
|
|
nul := []byte("/usr/lib/chromium/chromium\x00--user-data-dir=/tmp/x\x00--remote-debugging-port=9333\x00")
|
|
got := parseCmdline(nul)
|
|
want := []string{"/usr/lib/chromium/chromium", "--user-data-dir=/tmp/x", "--remote-debugging-port=9333"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("NUL: got %v, want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("NUL[%d]: got %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
|
|
// Caso Chromium: cmdline colapsado a una sola cadena separada por espacios.
|
|
collapsed := []byte("/usr/lib/chromium/chromium --remote-debugging-port=9333 --user-data-dir=/tmp/browser_mcp_userdata --no-first-run https://www.alsa.es/")
|
|
args := parseCmdline(collapsed)
|
|
if len(args) == 1 {
|
|
t.Fatalf("space-collapsed: parse devolvio un unico elemento gigante: %q", args[0])
|
|
}
|
|
if args[0] != "/usr/lib/chromium/chromium" {
|
|
t.Errorf("space-collapsed argv[0]: got %q, want chromium binary", args[0])
|
|
}
|
|
|
|
// El master debe detectarse a partir del cmdline colapsado (regresion de browser_list).
|
|
m, ok := parseChromiumMaster(18148, args)
|
|
if !ok {
|
|
t.Fatalf("space-collapsed: parseChromiumMaster no detecto el master")
|
|
}
|
|
if m.UserDataDir != "/tmp/browser_mcp_userdata" {
|
|
t.Errorf("space-collapsed udd: got %q, want /tmp/browser_mcp_userdata", m.UserDataDir)
|
|
}
|
|
if m.CDPPort != "9333" || !m.HasCDP {
|
|
t.Errorf("space-collapsed cdp: got port=%q hasCDP=%v, want 9333/true", m.CDPPort, m.HasCDP)
|
|
}
|
|
|
|
if parseCmdline([]byte("")) != nil || parseCmdline([]byte("\x00\x00")) != nil {
|
|
t.Errorf("cmdline vacio debe devolver nil")
|
|
}
|
|
}
|
|
|
|
// TestIsHeadless valida la deteccion de modo headless por el flag de lanzamiento:
|
|
// --headless, --headless=new y --headless=old cuentan; su ausencia es headed.
|
|
func TestIsHeadless(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
want bool
|
|
}{
|
|
{"sin flag (headed)", []string{"/usr/lib/chromium/chromium", "--user-data-dir=/tmp/x"}, false},
|
|
{"--headless legacy", []string{"/usr/lib/chromium/chromium", "--headless", "--user-data-dir=/tmp/x"}, true},
|
|
{"--headless=new", []string{"/usr/lib/chromium/chromium", "--headless=new"}, true},
|
|
{"--headless=old", []string{"/usr/lib/chromium/chromium", "--headless=old"}, true},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if got := isHeadless(c.args); got != c.want {
|
|
t.Errorf("isHeadless(%v) = %v, want %v", c.args, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
// El master headed real (cmdline colapsado por espacios) debe reportar headless=false.
|
|
headed := parseCmdline([]byte("/usr/lib/chromium/chromium --remote-debugging-port=9333 --user-data-dir=/tmp/browser_mcp_userdata"))
|
|
if m, ok := parseChromiumMaster(1, headed); !ok || m.Headless {
|
|
t.Errorf("master headed: ok=%v headless=%v, want ok=true headless=false", ok, m.Headless)
|
|
}
|
|
}
|