go-webview/cdp_test.go
Snider f38ceb3bd6
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
Add missing webview tests
2026-04-16 00:15:00 +01:00

404 lines
10 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package webview
import (
"context"
"errors"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/gorilla/websocket"
)
func newConnectedCDPClient(t *testing.T, target *fakeCDPTarget) *CDPClient {
t.Helper()
client, err := NewCDPClient(target.server.DebugURL())
if err != nil {
t.Fatalf("NewCDPClient returned error: %v", err)
}
t.Cleanup(func() {
_ = client.Close()
})
return client
}
func TestCdp_parseDebugURL_Good(t *testing.T) {
tests := []string{
"http://localhost:9222",
"http://127.0.0.1:9222",
"http://[::1]:9222",
"https://localhost:9222/",
}
for _, raw := range tests {
t.Run(raw, func(t *testing.T) {
got, err := parseDebugURL(raw)
if err != nil {
t.Fatalf("parseDebugURL returned error: %v", err)
}
if got.Scheme != strings.Split(raw, ":")[0] {
t.Fatalf("parseDebugURL scheme = %q, want %q", got.Scheme, strings.Split(raw, ":")[0])
}
if got.Path != "/" {
t.Fatalf("parseDebugURL path = %q, want %q", got.Path, "/")
}
})
}
}
func TestCdp_parseDebugURL_Bad(t *testing.T) {
tests := []string{
"http://example.com:9222",
"http://localhost:9222/json",
"http://localhost:9222?x=1",
"http://localhost:9222#frag",
"http://user:pass@localhost:9222",
"ftp://localhost:9222",
"localhost:9222",
}
for _, raw := range tests {
t.Run(raw, func(t *testing.T) {
if _, err := parseDebugURL(raw); err == nil {
t.Fatalf("parseDebugURL(%q) returned nil error", raw)
}
})
}
}
func TestCdp_validateNavigationURL_Good(t *testing.T) {
for _, raw := range []string{
"http://localhost:8080/path?q=1",
"https://example.com",
"about:blank",
} {
t.Run(raw, func(t *testing.T) {
if err := validateNavigationURL(raw); err != nil {
t.Fatalf("validateNavigationURL returned error: %v", err)
}
})
}
}
func TestCdp_validateNavigationURL_Bad(t *testing.T) {
for _, raw := range []string{
"javascript:alert(1)",
"data:text/html,hello",
"file:///etc/passwd",
"about:srcdoc",
"http://",
"https://user:pass@example.com",
} {
t.Run(raw, func(t *testing.T) {
if err := validateNavigationURL(raw); err == nil {
t.Fatalf("validateNavigationURL(%q) returned nil error", raw)
}
})
}
}
func TestCdp_normalisedPort_Good(t *testing.T) {
tests := []struct {
raw string
want string
}{
{"http://localhost", "80"},
{"ws://localhost", "80"},
{"https://localhost", "443"},
{"wss://localhost", "443"},
{"http://localhost:1234", "1234"},
{"ws://localhost:5678", "5678"},
}
for _, tc := range tests {
t.Run(tc.raw, func(t *testing.T) {
u, err := url.Parse(tc.raw)
if err != nil {
t.Fatalf("url.Parse returned error: %v", err)
}
if got := normalisedPort(u); got != tc.want {
t.Fatalf("normalisedPort(%q) = %q, want %q", tc.raw, got, tc.want)
}
})
}
}
func TestCdp_normalisedPort_Ugly(t *testing.T) {
u, err := url.Parse("ftp://localhost")
if err != nil {
t.Fatalf("url.Parse returned error: %v", err)
}
if got := normalisedPort(u); got != "" {
t.Fatalf("normalisedPort(ftp://localhost) = %q, want empty", got)
}
}
func TestCdp_targetIDFromWebSocketURL_Good(t *testing.T) {
got, err := targetIDFromWebSocketURL("ws://localhost:9222/devtools/page/target-1")
if err != nil {
t.Fatalf("targetIDFromWebSocketURL returned error: %v", err)
}
if got != "target-1" {
t.Fatalf("targetIDFromWebSocketURL = %q, want %q", got, "target-1")
}
}
func TestCdp_targetIDFromWebSocketURL_Bad(t *testing.T) {
for _, raw := range []string{
"ws://localhost:9222/",
"ws://localhost:9222",
} {
t.Run(raw, func(t *testing.T) {
if _, err := targetIDFromWebSocketURL(raw); err == nil {
t.Fatalf("targetIDFromWebSocketURL(%q) returned nil error", raw)
}
})
}
}
func TestCdp_isTerminalReadError_Good(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{name: "nil", err: nil, want: false},
{name: "net closed", err: net.ErrClosed, want: true},
{name: "ws close sent", err: websocket.ErrCloseSent, want: true},
{name: "close error", err: &websocket.CloseError{Code: 1000, Text: "bye"}, want: true},
{name: "other", err: context.DeadlineExceeded, want: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := isTerminalReadError(tc.err); got != tc.want {
t.Fatalf("isTerminalReadError(%v) = %v, want %v", tc.err, got, tc.want)
}
})
}
}
func TestCdp_cloneHelpers_Good(t *testing.T) {
original := map[string]any{
"nested": map[string]any{"count": float64(1)},
"items": []any{map[string]any{"id": "alpha"}},
"value": "original",
}
cloned := cloneMapAny(original)
cloned["value"] = "changed"
cloned["nested"].(map[string]any)["count"] = float64(2)
cloned["items"].([]any)[0].(map[string]any)["id"] = "beta"
if got := original["value"]; got != "original" {
t.Fatalf("original scalar mutated: %v", got)
}
if got := original["nested"].(map[string]any)["count"]; got != float64(1) {
t.Fatalf("original nested map mutated: %v", got)
}
if got := original["items"].([]any)[0].(map[string]any)["id"]; got != "alpha" {
t.Fatalf("original nested slice mutated: %v", got)
}
if cloneMapAny(nil) != nil {
t.Fatal("cloneMapAny(nil) = non-nil")
}
if cloneSliceAny(nil) != nil {
t.Fatal("cloneSliceAny(nil) = non-nil")
}
if got := cloneAny(original).(map[string]any)["value"]; got != "original" {
t.Fatalf("cloneAny(map) = %v, want original value", got)
}
}
func TestCdp_ListTargets_Good(t *testing.T) {
server := newFakeCDPServer(t)
targets, err := ListTargets(server.DebugURL())
if err != nil {
t.Fatalf("ListTargets returned error: %v", err)
}
if len(targets) != 1 {
t.Fatalf("ListTargets returned %d targets, want 1", len(targets))
}
if targets[0].Type != "page" {
t.Fatalf("ListTargets type = %q, want page", targets[0].Type)
}
got := make([]TargetInfo, 0)
for target := range ListTargetsAll(server.DebugURL()) {
got = append(got, target)
}
if len(got) != 1 {
t.Fatalf("ListTargetsAll yielded %d targets, want 1", len(got))
}
}
func TestCdp_GetVersion_Good(t *testing.T) {
server := newFakeCDPServer(t)
version, err := GetVersion(server.DebugURL())
if err != nil {
t.Fatalf("GetVersion returned error: %v", err)
}
if got := version["Browser"]; got != "Chrome/123.0" {
t.Fatalf("GetVersion Browser = %q, want Chrome/123.0", got)
}
}
func TestCdp_NewCDPClient_Good_AutoCreatesTarget(t *testing.T) {
server := newFakeCDPServer(t)
server.mu.Lock()
server.targets = make(map[string]*fakeCDPTarget)
server.nextTarget = 0
server.mu.Unlock()
client, err := NewCDPClient(server.DebugURL())
if err != nil {
t.Fatalf("NewCDPClient returned error: %v", err)
}
t.Cleanup(func() {
_ = client.Close()
})
if client.DebugURL() != server.DebugURL() {
t.Fatalf("DebugURL() = %q, want %q", client.DebugURL(), server.DebugURL())
}
if client.WebSocketURL() == "" {
t.Fatal("WebSocketURL() returned empty string")
}
}
func TestCdp_NewCDPClient_Bad_RejectsInvalidDebugURL(t *testing.T) {
_, err := NewCDPClient("http://example.com:9222")
if err == nil {
t.Fatal("NewCDPClient succeeded for remote host")
}
}
func TestCdp_Send_Good(t *testing.T) {
server := newFakeCDPServer(t)
target := server.primaryTarget()
client := newConnectedCDPClient(t, target)
done := make(chan cdpMessage, 1)
target.onMessage = func(target *fakeCDPTarget, msg cdpMessage) {
done <- msg
}
if err := client.Send("Page.enable", map[string]any{"foo": "bar"}); err != nil {
t.Fatalf("Send returned error: %v", err)
}
select {
case msg := <-done:
if msg.Method != "Page.enable" {
t.Fatalf("Send method = %q, want Page.enable", msg.Method)
}
if got := msg.Params["foo"]; got != "bar" {
t.Fatalf("Send param foo = %v, want bar", got)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for sent message")
}
}
func TestCdp_NewTab_Good(t *testing.T) {
server := newFakeCDPServer(t)
target := server.primaryTarget()
client := newConnectedCDPClient(t, target)
tab, err := client.NewTab("about:blank")
if err != nil {
t.Fatalf("NewTab returned error: %v", err)
}
t.Cleanup(func() {
_ = tab.Close()
})
if tab.WebSocketURL() == "" {
t.Fatal("NewTab returned empty WebSocket URL")
}
}
func TestCdp_CloseTab_Bad_TargetCloseNotAcknowledged(t *testing.T) {
server := newFakeCDPServer(t)
target := server.primaryTarget()
target.onMessage = func(target *fakeCDPTarget, msg cdpMessage) {
if msg.Method != "Target.closeTarget" {
t.Fatalf("CloseTab sent %q, want Target.closeTarget", msg.Method)
}
target.reply(msg.ID, map[string]any{"success": false})
}
client := newConnectedCDPClient(t, target)
if err := client.CloseTab(); err == nil {
t.Fatal("CloseTab succeeded without target close acknowledgement")
}
}
func TestCdp_failPending_Good(t *testing.T) {
c1 := make(chan *cdpResponse, 1)
c2 := make(chan *cdpResponse, 1)
client := &CDPClient{
pending: map[int64]chan *cdpResponse{
1: c1,
2: c2,
},
}
client.failPending(errors.New("boom"))
for i, ch := range []chan *cdpResponse{c1, c2} {
select {
case resp := <-ch:
if resp.Error == nil || resp.Error.Message != "boom" {
t.Fatalf("pending response %d = %#v, want boom error", i+1, resp)
}
default:
t.Fatalf("pending response %d was not delivered", i+1)
}
}
}
func TestCdp_createTargetAt_Good(t *testing.T) {
server := newFakeCDPServer(t)
target, err := createTargetAt(context.Background(), mustParseURL(t, server.DebugURL()), "about:blank")
if err != nil {
t.Fatalf("createTargetAt returned error: %v", err)
}
if target == nil || target.WebSocketDebuggerURL == "" {
t.Fatalf("createTargetAt returned %#v", target)
}
}
func TestCdp_doDebugRequest_Bad_HTTPStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
}))
t.Cleanup(server.Close)
debugURL, err := parseDebugURL(server.URL)
if err != nil {
t.Fatalf("parseDebugURL returned error: %v", err)
}
if _, err := doDebugRequest(context.Background(), debugURL, "/json", ""); err == nil {
t.Fatal("doDebugRequest returned nil error for non-2xx status")
}
}
func mustParseURL(t *testing.T, raw string) *url.URL {
t.Helper()
u, err := url.Parse(raw)
if err != nil {
t.Fatalf("url.Parse returned error: %v", err)
}
return u
}