aboutsummaryrefslogtreecommitdiff
path: root/test/cdp/endpoint-test.ts
blob: 57d60880da0e7716f353f83061436acb708bd3dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
 * Unit tests for CDP target resolution — no browser and no network required.
 *
 * This is fork-specific logic (see FORK.md): upstream only ever drives a
 * browser on localhost. The precedence here is deliberately asymmetric — an
 * explicitly configured endpoint must never silently fall back to a different
 * browser, while the built-in default must, so a machine off the wire network
 * still gets the local browser the extension was originally written for.
 *
 * Run: npx tsx test/cdp/endpoint-test.ts
 */
import {
  DEFAULT_REMOTE_CDP,
  describeCdpTarget,
  isValidTargetValue,
  resolveCdpTarget,
} from "../../src/cdp/endpoint";

let passed = 0;
let failed = 0;
const check = (cond: boolean, label: string): void => {
  if (cond) {
    passed++;
    console.log(`  ✓ ${label}`);
  } else {
    failed++;
    console.error(`  ✗ ${label}`);
  }
};

const env = (value?: string): NodeJS.ProcessEnv => (value === undefined ? {} : { BU_CDP_HTTP: value });

function main(): void {
  // E1: the built-in default, used when nothing is configured.
  {
    const t = resolveCdpTarget(env());
    check(t.kind === "remote", "E1: unset BU_CDP_HTTP resolves to a remote target");
    if (t.kind === "remote") {
      check(t.source === "default", "E1: it is marked as the built-in default, not an instruction");
      check(`${t.host}:${t.port}` === DEFAULT_REMOTE_CDP, "E1: it matches DEFAULT_REMOTE_CDP");
    }
  }

  // E2: DEFAULT_REMOTE_CDP must itself be parseable, or the default silently
  // degrades to local for everyone.
  {
    const t = resolveCdpTarget(env());
    check(t.kind !== "local", "E2: the built-in default constant is well-formed");
  }

  // E3: an explicit endpoint is an instruction — source must reflect that, so
  // discovery fails loudly rather than falling back.
  {
    const t = resolveCdpTarget(env("10.0.0.9:9333"));
    check(t.kind === "remote", "E3: an explicit host:port resolves to a remote target");
    if (t.kind === "remote") {
      check(t.host === "10.0.0.9" && t.port === 9333, "E3: host and port are parsed");
      check(t.source === "env", "E3: it is marked as explicitly configured");
    }
  }

  // E4: opting back into upstream behaviour.
  for (const alias of ["local", "LOCAL", "off", "none", "0", "no"]) {
    const t = resolveCdpTarget(env(alias));
    check(t.kind === "local", `E4: "${alias}" selects this machine's own browser`);
  }

  // E5: empty or whitespace is treated as unset rather than as an error — a
  // shell exporting an empty var should not break the browser.
  for (const blank of ["", "   "]) {
    const t = resolveCdpTarget(env(blank));
    check(t.kind === "remote" && t.source === "default", `E5: ${JSON.stringify(blank)} falls back to the default`);
  }

  // E6: malformed values are rejected, not coerced. Silently reinterpreting
  // these would drive an unexpected browser.
  for (const bad of ["nonsense", "host:", ":9222", "host:0", "host:70000", "host:abc"]) {
    const t = resolveCdpTarget(env(bad));
    check(t.kind === "invalid", `E6: "${bad}" is rejected as malformed`);
  }

  // E7: IPv6 and hostnames still work — the port is taken from the LAST colon.
  {
    const t = resolveCdpTarget(env("castle.local:9223"));
    check(t.kind === "remote" && t.host === "castle.local", "E7: hostnames are accepted");
    const v6 = resolveCdpTarget(env("[::1]:9223"));
    check(v6.kind === "remote" && v6.host === "[::1]" && v6.port === 9223, "E7: bracketed IPv6 keeps its host");
  }

  // E8: descriptions are for humans in /browser-status; they must distinguish
  // a default from an instruction, since only one of them falls back.
  {
    const d = describeCdpTarget(resolveCdpTarget(env()));
    const e = describeCdpTarget(resolveCdpTarget(env("10.0.0.9:9333")));
    check(d.includes("default"), "E8: the default is labelled as such");
    check(e.includes("BU_CDP_HTTP"), "E8: an explicit endpoint names the variable that set it");
    check(describeCdpTarget(resolveCdpTarget(env("local"))).includes("own browser"), "E8: local is described plainly");
  }

  // E9: the stored choice (/browser-target) sits between env and the default.
  {
    const t = resolveCdpTarget({}, "local");
    check(t.kind === "local" && t.source === "stored", "E9: a stored value is used when the env is unset");

    const r = resolveCdpTarget({}, "10.2.3.4:9224");
    check(r.kind === "remote" && r.source === "stored", "E9: a stored host:port resolves as a remote target");
  }

  // E10: the environment wins, so a one-off `BU_CDP_HTTP=… pi` is never
  // silently overridden by a setting made days ago.
  {
    const t = resolveCdpTarget(env("local"), "10.2.3.4:9224");
    check(t.kind === "local" && t.source === "env", "E10: env beats the stored value");

    const r = resolveCdpTarget(env("10.9.9.9:1234"), "local");
    check(r.kind === "remote" && r.host === "10.9.9.9" && r.source === "env", "E10: an env endpoint beats stored local");
  }

  // E11: clearing the stored value falls back to the built-in default rather
  // than to local — clearing is "use the usual browser", not "go local".
  {
    const t = resolveCdpTarget({}, null);
    check(t.kind === "remote" && t.source === "default", "E11: no stored value falls back to the default");
    const blank = resolveCdpTarget({}, "   ");
    check(blank.kind === "remote" && blank.source === "default", "E11: a blank stored value is treated as cleared");
  }

  // E12: a corrupt stored value must be reported against the right source, or
  // the operator goes hunting through their shell config for an env var that
  // was never set.
  {
    const t = resolveCdpTarget({}, "garbage");
    check(t.kind === "invalid" && t.source === "stored", "E12: a bad stored value is blamed on /browser-target");
    if (t.kind === "invalid") {
      check(describeCdpTarget(t).includes("/browser-target"), "E12: the description names the stored source");
    }
    const e = resolveCdpTarget(env("garbage"), null);
    check(e.kind === "invalid" && e.source === "env", "E12: a bad env value is blamed on BU_CDP_HTTP");
  }

  // E13: the values /browser-target accepts must match what resolution can
  // actually use, or the command would happily store something unusable.
  {
    for (const good of ["local", "off", "1.2.3.4:9222", "castle.local:9223"]) {
      check(isValidTargetValue(good), `E13: "${good}" is accepted by the command`);
    }
    for (const bad of ["", "  ", "garbage", "host:", "host:99999"]) {
      check(!isValidTargetValue(bad), `E13: "${bad}" is rejected by the command`);
    }
    check(!isValidTargetValue("default"), "E13: \"default\" is handled as clear, not stored verbatim");
  }

  console.log(`\n${passed} passed, ${failed} failed`);
  if (failed > 0) process.exit(1);
}

main();