mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* fix(javascript): reject a tls protocol version Node refuses at the call site
`tls.connect` resolves `minVersion`/`maxVersion` before it opens a socket, so a bad literal
throws from the call itself and no socket exists. The acquisition filter decided the port
only, so it reported a complete built-in resource for a call that throws.
Measured on Node 24.22.1:
| `minVersion` / `maxVersion` | outcome |
| --- | --- |
| `"TLSv1"`, `"TLSv1.1"`, `"TLSv1.2"`, `"TLSv1.3"` | socket created |
| `null`, `undefined`, absent | socket created |
| `"TLSv1.0"`, `"tlsv1"`, `"tlsv1.2"`, `"TLSv1.4"`, `"nope"`, `""` | THREW `ERR_TLS_INVALID_PROTOCOL_VERSION` |
| `5`, `true`, `{}`, `[]` | THREW `ERR_TLS_INVALID_PROTOCOL_VERSION` |
| `1n` | THREW `TypeError` |
The accepted set is exact and case-sensitive, so it is a closed list rather than a pattern.
`net` ignores these members, so only `tls` is consulted. The member is read in write order,
which is why `{minVersion: "nope", minVersion: "TLSv1.2"}` keeps its acquisition; a spread,
a computed key or an accessor leaves the answer undecided rather than decided against.
22 cases, 11 failing before this commit. `src/domain/javascript`: 5040 to 5062 passing.
`tsc` 0; `build` 0; `npm run format:check` 0; `npm run lint` 0 errors; `knip` 0.
* fix(javascript): read every tls options slot and check both version members
Four review findings, each measured on Node 24.22.1. All four were real gaps in the first
version of this check.
**An accepted `minVersion` returned before `maxVersion` was read.** `tls.connect({port: 80,
minVersion: "TLSv1.2", maxVersion: "nope"})` throws, because both members are resolved
before the socket opens. The loop now continues past an accepted version and returns only
for a rejected one.
**Only argument zero was read.** `tls` also accepts `(port, options)`, `(port, host,
options)` and `(path, options)`, and all three throw on a bad version:
`tls.connect(443, {minVersion: "nope"})`, `tls.connect(443, "127.0.0.1", {minVersion:
"nope"})` and `tls.connect("/tmp/x.sock", {minVersion: "nope"})`. Every object literal in
the argument list is now read; a spread argument shifts those positions, so it leaves the
answer undecided instead of picking a wrong slot.
**A signed number was left unknown.** `minVersion: -1`, `+1` and `-0` all throw, but they
arrive as a `UnaryExpression` and matched none of the literal arms. `readExactJavaScriptLiteral`
already normalises `-1` and `+1`; `-0` is the one case it excludes, so a sign over a numeric
literal is decided explicitly here rather than treated as unknown.
13 cases added; reverting the slot and loop changes fails 4 of them.
`src/domain/javascript`: 5062 to 5075 passing. `tsc` 0; `build` 0;
`npm run format:check` 0; `npm run lint` 0 errors; `knip` 0.
* fix(javascript): merge the tls arguments and honour a supplied secure context
Four review findings, each measured on Node 24.22.1. All four were real: the previous
reader checked each object argument independently and ignored the context.
**Several object arguments merge in order, so the later one wins.** The reader rejected the
first object and stopped:
| call | Node 24.22.1 |
| --- | --- |
| `tls.connect({port: 443, minVersion: "nope"}, {minVersion: "TLSv1.2"})` | SOCKET |
| `tls.connect({port: 443, minVersion: "TLSv1.2"}, {minVersion: "nope"})` | THREW |
| `tls.connect({port: 443}, {minVersion: "nope"})` | THREW |
The members are now resolved across the whole argument list in write order, so a later
argument replaces an earlier one the way Node's merge does.
**A supplied `secureContext` bypasses version parsing entirely.** Node uses
`options.secureContext || tls.createSecureContext(options)`, so
`tls.connect({port: 80, secureContext: ctx, minVersion: "nope"})` connects while a `null`
context throws. An unreadable context leaves the answer undecided.
**An ordinary method member cannot install a version.** The reader skipped any object
containing a method, so `{checkServerIdentity() {}, minVersion: "nope"}` kept a socket
Node never creates. Only an accessor runs a body and leaves the member open; a plain method
is a value Node never reads as a version name. A `minVersion() {}` member is itself such a
value, so it rejects.
**Signed BigInt and function-shaped versions were left unknown.** `-1n`, `() => {}`,
`function () {}` and `class {}` are all truthy non-strings that Node refuses; none matched
an existing arm. They are now decided.
12 cases added; reverting the merge and context changes fails 2 of them and reverting the
method arm fails 6. `src/domain/javascript`: 5075 to 5087 passing. `tsc` 0; `build` 0;
`npm run format:check` 0; `npm run lint` 0 errors; `knip` 0.
* fix(javascript): read only the options slot tls.connect copies, and its accessors
Six review findings, each measured on Node 24.22.1. All six were real.
**A falsy `secureContext` does not bypass version parsing.** Node tests
`options.secureContext` for truth, so `{secureContext: false, minVersion: "nope"}`, and `0`
and `""` with it, all throw. The guard only recognised nullish syntax, so it dropped real
throws. `staticTruthiness` now decides the context: a known-falsy value keeps checking, a
known-truthy one ends the check, and an unreadable one stays undecided.
**Node copies at most one options object.** Measured:
| call | result |
| --- | --- |
| `(443, {minVersion: "TLSv1.2"}, {minVersion: "nope"})` | SOCKET — the third argument is ignored |
| `(443, {minVersion: "nope"}, {minVersion: "TLSv1.2"})` | THREW |
| `(443, "127.0.0.1", {minVersion: "nope"})` | THREW — the host fills slot 1 |
| `({port: 443, minVersion: "nope"}, {minVersion: "TLSv1.2"})` | SOCKET — the second object wins |
The reader treated every object argument as merged in order, which let an ignored object
decide. `tlsOptionsObjects` now models the two overloads: an object in slot 0 is merged with
slot 1, and a port or path in slot 0 makes the first object among slots 1 and 2 the only one
read.
**An exact computed key names the member.** `{["minVersion"]: "nope"}` throws, but the
reader marked every computed key unknown before the shared name resolver saw it. Only an
unresolvable key is unknown now, and the case that asserted the old behaviour uses a
binding key so it still covers the unknown path.
**An accessor can write the member first.** `{get side() { this.minVersion = "TLSv1.2"; },
minVersion: "nope"}` connects, because Node's own copy runs the getter before it reads
`minVersion`; and a `set minVersion` in an earlier argument absorbs the later write. Both
are bodies this reader does not run, so an accessor anywhere in the merged objects now
leaves every version member undecided. A plain method still rejects, since Node never reads
it as a version name.
**An unshadowed `NaN` or `Infinity` is a non-string version.** Both throw, and neither
matched an existing arm. They are decided the same way the adjacent port filter decides
them, through `isUnmodifiedGlobalReference`, so a shadowed binding stays unknown.
16 cases added; reverting the truthiness, accessor and computed-key changes fails 2 of them.
`src/domain/javascript`: 5075 to 5188 passing. `tsc` 0; `build` 0;
`npm run format:check` 0; `npm run lint` 0 errors.
* fix(javascript): use the nullable key owner and settle unary versions
Six review findings, each measured on Node 24.22.1. All six were real, and three of them
were removing real sockets.
**`semanticStaticPropertyName` encodes an unresolved key as `""`, never `undefined`.** The
guard tested `undefined`, so it never fired and an unresolved computed key was read as a
member literally named empty. `const k = "minVersion";
tls.connect({port: 443, minVersion: "nope", [k]: "TLSv1.2"})` connects on Node 24.22.1
because the later computed member wins, while the analysis filtered on the stale `"nope"`
and dropped the socket. `semanticStaticPropertyKey` is the nullable owner and is now used,
with `null` treated as unknown.
**A getter can replace a plain version method before Node copies it.** The method branch
ignored `accessorWritten`, so `{get side() { this.minVersion = "TLSv1.2"; }, minVersion()
{}}` still returned `"rejects"` although it connects. The same unknown result now applies to
methods when an accessor is present.
**A non-literal second argument is a merged options object, not an absent one.**
`const o = {minVersion: "TLSv1.2"}; tls.connect({port: 443, minVersion: "nope"}, o)`
connects. The slot reader mapped every non-object to `undefined`, so only the stale first
object was examined. A non-object in a copied slot now stays unknown.
**`-0` is falsy, so a signed-zero context does not bypass parsing.** The local truthiness
helper did not fold a unary `-`, so `{secureContext: -0, minVersion: "nope"}` was treated as
a possible bypass although it throws. The shared `staticJavaScriptTruthiness` owner already
handles signed zero and replaces the local helper, which is deleted.
**A unary operator over a settled operand is a settled non-string version.** `!0`, `!1`,
`~~0`, `!!0` and `typeof 1` all throw `ERR_TLS_INVALID_PROTOCOL_VERSION`; the previous arm
recognised only `+` and `-`, and the exact-literal and shape arms did not reach them.
`void <expr>` stays accepted, because it is `undefined`.
The slot reader also had to distinguish a host string in slot 1 from an options object, since
`tls.connect(443, "127.0.0.1", {minVersion: "nope"})` throws while
`tls.connect(443, {minVersion: "nope"})` is the two-slot form.
`src/domain/javascript`: 5188 to 5197 passing. Reverting the key owner fails 1 case and
reverting the unary arms fails 2. `tsc` 0; `build` 0; `npm run format:check` 0;
`npm run lint` 0 errors.
* test(javascript): distinguish package scope in route fixtures
* test(apple): separate identity and refusal fixture groups
* fix(javascript): share TLS option facts and preserve merge uncertainty
* fix(javascript): separate TLS assignment authority from copy facts
---------
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
390 lines
15 KiB
TypeScript
390 lines
15 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
|
|
|
/**
|
|
* `tls.connect` resolves `minVersion`/`maxVersion` before it opens a socket, so a bad
|
|
* literal throws from the call itself and no socket exists.
|
|
*
|
|
* Measured on Node 24.22.1, accepted: `"TLSv1"`, `"TLSv1.1"`, `"TLSv1.2"`, `"TLSv1.3"`,
|
|
* `null`, `undefined`, and the member being absent. Threw
|
|
* `ERR_TLS_INVALID_PROTOCOL_VERSION`: `"TLSv1.0"`, `"tlsv1"`, `"tlsv1.2"`, `"TLSv1.4"`,
|
|
* `"nope"`, `""`, `5`, `true`, `{}`, `[]`; a BigInt throws `TypeError`.
|
|
*
|
|
* The set is exact and case-sensitive, so it is a closed list rather than a pattern.
|
|
* `net` ignores these members entirely.
|
|
*/
|
|
const acquisitionsFor = (body: string) =>
|
|
analyzeJavaScriptSemantics(
|
|
`import tls from "node:tls";\nimport net from "node:net";\nfunction g() { ${body}; }`,
|
|
)
|
|
.resourceOperations.filter(({ kind }) => kind === "acquire")
|
|
.map(({ kind, method }) => ({ kind, method }));
|
|
|
|
it.each([
|
|
[
|
|
"a rejected maximum beside an accepted minimum",
|
|
`tls.connect({ port: 80, minVersion: "TLSv1.2", maxVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a rejected minimum beside an accepted maximum",
|
|
`tls.connect({ port: 80, minVersion: "nope", maxVersion: "TLSv1.3" })`,
|
|
],
|
|
[
|
|
"an options object in the port slot",
|
|
`tls.connect(443, { minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"an options object after port and host",
|
|
`tls.connect(443, "127.0.0.1", { minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"an options object after a Unix path",
|
|
`tls.connect("/tmp/x.sock", { minVersion: "nope" })`,
|
|
],
|
|
["a negative version", `tls.connect({ port: 80, minVersion: -1 })`],
|
|
["a unary-plus version", `tls.connect({ port: 80, minVersion: +1 })`],
|
|
["a negative-zero version", `tls.connect({ port: 80, minVersion: -0 })`],
|
|
["a negative maximum", `tls.connect({ port: 80, maxVersion: -1 })`],
|
|
] as const)("emits no socket acquisition for %s", (_name, call) => {
|
|
// Both members are resolved before the socket opens, so an accepted minimum does not
|
|
// make a rejected maximum irrelevant. `tls` also reads the options object from every
|
|
// positional overload. Measured on Node 24.22.1.
|
|
expect(acquisitionsFor(call)).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"both versions accepted",
|
|
`tls.connect({ port: 80, minVersion: "TLSv1.2", maxVersion: "TLSv1.3" })`,
|
|
],
|
|
["a port-slot options object", `tls.connect(443, { minVersion: "TLSv1.2" })`],
|
|
[
|
|
"a path-slot options object",
|
|
`tls.connect("/tmp/x.sock", { minVersion: "TLSv1.3" })`,
|
|
],
|
|
[
|
|
"a spread argument",
|
|
`const o = { minVersion: "nope" }; tls.connect(443, ...[o])`,
|
|
],
|
|
] as const)("keeps the socket acquisition for %s", (_name, call) => {
|
|
expect(acquisitionsFor(call)).toEqual([
|
|
{ kind: "acquire", method: "connect" },
|
|
]);
|
|
});
|
|
it.each([
|
|
[
|
|
"a later accepted version over a rejected one",
|
|
`tls.connect({ port: 443, minVersion: "nope" }, { minVersion: "TLSv1.2" })`,
|
|
],
|
|
[
|
|
"a supplied secure context beside a bad version",
|
|
`tls.connect({ port: 80, secureContext: ctx, minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"an accessor version with no explicit one",
|
|
`tls.connect({ port: 80, get minVersion() { return "TLSv1.2"; } })`,
|
|
],
|
|
] as const)("keeps the socket acquisition for %s", (_name, call) => {
|
|
// Several object arguments merge in order, so a later one wins; a supplied
|
|
// `secureContext` bypasses version parsing entirely; and an accessor runs a body, so its
|
|
// result is not settled by this literal. Measured on Node 24.22.1.
|
|
expect(acquisitionsFor(call)).toEqual([
|
|
{ kind: "acquire", method: "connect" },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"a rejected version over an accepted one",
|
|
`tls.connect({ port: 443, minVersion: "TLSv1.2" }, { minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a later rejected version with no earlier one",
|
|
`tls.connect({ port: 443 }, { minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a null secure context beside a bad version",
|
|
`tls.connect({ port: 80, secureContext: null, minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"an ordinary method beside a bad version",
|
|
`tls.connect({ port: 80, checkServerIdentity() {}, minVersion: "nope" })`,
|
|
],
|
|
["an arrow version", `tls.connect({ port: 80, minVersion: () => {} })`],
|
|
[
|
|
"a function version",
|
|
`tls.connect({ port: 80, minVersion: function () {} })`,
|
|
],
|
|
["a class version", `tls.connect({ port: 80, minVersion: class {} })`],
|
|
["a negative BigInt version", `tls.connect({ port: 80, minVersion: -1n })`],
|
|
["a version method member", `tls.connect({ port: 80, minVersion() {} })`],
|
|
] as const)("emits no socket acquisition for %s", (_name, call) => {
|
|
// An ordinary method cannot install a version, and an arrow, function, class or signed
|
|
// BigInt version is a truthy non-string, so each of these throws at the call site.
|
|
expect(acquisitionsFor(call)).toEqual([]);
|
|
});
|
|
it.each([
|
|
[
|
|
"a falsy boolean secure context",
|
|
`tls.connect({ port: 443, secureContext: false, minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a zero secure context",
|
|
`tls.connect({ port: 443, secureContext: 0, minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"an empty-string secure context",
|
|
`tls.connect({ port: 443, secureContext: "", minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a rejected version in the first of three objects",
|
|
`tls.connect(443, { minVersion: "nope" }, { minVersion: "TLSv1.2" })`,
|
|
],
|
|
[
|
|
"a rejected version after port and host",
|
|
`tls.connect(443, "127.0.0.1", { minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a rejected version written after a good one",
|
|
`tls.connect({ port: 443, minVersion: "TLSv1.2" }, { minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"an exact computed key",
|
|
`tls.connect({ port: 80, ["minVersion"]: "nope" })`,
|
|
],
|
|
["an unmodified NaN version", `tls.connect({ port: 80, minVersion: NaN })`],
|
|
[
|
|
"an unmodified Infinity version",
|
|
`tls.connect({ port: 80, minVersion: Infinity })`,
|
|
],
|
|
] as const)("emits no socket acquisition for %s", (_name, call) => {
|
|
// A falsy secure context does not bypass version parsing; Node copies at most one options
|
|
// object so a third argument is ignored; an exact computed key names the same member; and
|
|
// an unshadowed NaN or Infinity is a non-string version. Measured on Node 24.22.1.
|
|
expect(acquisitionsFor(call)).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"a supplied secure context",
|
|
`tls.connect({ port: 443, secureContext: {}, minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"an unreadable secure context",
|
|
`const f = {}; tls.connect({ port: 443, secureContext: f, minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a good version in the first of three objects",
|
|
`tls.connect(443, { minVersion: "TLSv1.2" }, { minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a good version written after a rejected one",
|
|
`tls.connect({ port: 443, minVersion: "nope" }, { minVersion: "TLSv1.2" })`,
|
|
],
|
|
[
|
|
"an accessor that writes the version",
|
|
`tls.connect({ port: 80, get side() { this.minVersion = "TLSv1.2"; }, minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a setter that absorbs a later version",
|
|
`tls.connect({ port: 80, set minVersion(_) {} }, { minVersion: "nope" })`,
|
|
],
|
|
[
|
|
"a shadowed NaN version",
|
|
`const NaN = {}; tls.connect({ port: 80, minVersion: NaN })`,
|
|
],
|
|
[
|
|
"a spread beside a rejected version",
|
|
`const o = {}; tls.connect({ port: 80, ...o, minVersion: "nope" })`,
|
|
],
|
|
] as const)("keeps the socket acquisition for %s", (_name, call) => {
|
|
// Only a truthy context bypasses parsing, the second object wins when slot 0 is an
|
|
// object, and an accessor runs a body this reader does not replay.
|
|
expect(acquisitionsFor(call)).toEqual([
|
|
{ kind: "acquire", method: "connect" },
|
|
]);
|
|
});
|
|
it.each([
|
|
[
|
|
"an unreadable computed key that may re-write the version",
|
|
`const k = "minVersion"; tls.connect({ port: 443, minVersion: "nope", [k]: "TLSv1.2" })`,
|
|
],
|
|
[
|
|
"a getter that shadows a version method",
|
|
`tls.connect({ port: 443, get side() { this.minVersion = "TLSv1.2"; }, minVersion() {} })`,
|
|
],
|
|
[
|
|
"an unreadable second options object",
|
|
`const o = { minVersion: "TLSv1.2" }; tls.connect({ port: 443, minVersion: "nope" }, o)`,
|
|
],
|
|
] as const)("keeps the socket acquisition for %s", (_name, call) => {
|
|
// `semanticStaticPropertyKey` is the nullable owner: an unresolved key is `null`, not
|
|
// `""`. An accessor can also replace a plain method before Node copies it, and a
|
|
// non-literal second argument is a merged options object rather than an absent one.
|
|
expect(acquisitionsFor(call)).toEqual([
|
|
{ kind: "acquire", method: "connect" },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"a signed-zero secure context",
|
|
`tls.connect({ port: 443, secureContext: -0, minVersion: "nope" })`,
|
|
],
|
|
["a logical-not version", `tls.connect({ port: 443, minVersion: !0 })`],
|
|
["a double-not version", `tls.connect({ port: 443, minVersion: !!0 })`],
|
|
[
|
|
"a double-bitwise-not version",
|
|
`tls.connect({ port: 443, minVersion: ~~0 })`,
|
|
],
|
|
["a typeof version", `tls.connect({ port: 443, minVersion: typeof 1 })`],
|
|
] as const)("emits no socket acquisition for %s", (_name, call) => {
|
|
// `-0` is falsy, so the context does not bypass parsing; and a unary operator over a
|
|
// settled operand is a settled non-string primitive, which Node refuses.
|
|
expect(acquisitionsFor(call)).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["a void version", `tls.connect({ port: 443, minVersion: void 0 })`],
|
|
] as const)("keeps the socket acquisition for %s", (_name, call) => {
|
|
// `void` is `undefined`, which Node reads as "not given", and a plain method is a value
|
|
// Node never reads as a version name when no accessor can replace it.
|
|
expect(acquisitionsFor(call)).toEqual([
|
|
{ kind: "acquire", method: "connect" },
|
|
]);
|
|
});
|
|
it.each([
|
|
["an unknown version name", `tls.connect({ port: 80, minVersion: "nope" })`],
|
|
["the TLSv1.0 spelling", `tls.connect({ port: 80, minVersion: "TLSv1.0" })`],
|
|
["a lower-case name", `tls.connect({ port: 80, minVersion: "tlsv1.2" })`],
|
|
["an empty version", `tls.connect({ port: 80, minVersion: "" })`],
|
|
["a numeric version", `tls.connect({ port: 80, minVersion: 5 })`],
|
|
["a boolean version", `tls.connect({ port: 80, minVersion: true })`],
|
|
["an object version", `tls.connect({ port: 80, minVersion: {} })`],
|
|
["an array version", `tls.connect({ port: 80, minVersion: [] })`],
|
|
["a BigInt version", `tls.connect({ port: 80, minVersion: 1n })`],
|
|
[
|
|
"an unknown maximum version",
|
|
`tls.connect({ port: 80, maxVersion: "TLSv1.4" })`,
|
|
],
|
|
[
|
|
"a bad version written last",
|
|
`tls.connect({ port: 80, minVersion: "TLSv1.2", minVersion: "nope" })`,
|
|
],
|
|
] as const)("emits no socket acquisition for %s", (_name, call) => {
|
|
expect(acquisitionsFor(call)).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["TLSv1", `tls.connect({ port: 80, minVersion: "TLSv1" })`],
|
|
["TLSv1.1", `tls.connect({ port: 80, minVersion: "TLSv1.1" })`],
|
|
["TLSv1.2", `tls.connect({ port: 80, minVersion: "TLSv1.2" })`],
|
|
["TLSv1.3 as a maximum", `tls.connect({ port: 80, maxVersion: "TLSv1.3" })`],
|
|
["a null version", `tls.connect({ port: 80, minVersion: null })`],
|
|
["an undefined version", `tls.connect({ port: 80, minVersion: undefined })`],
|
|
["no version member", `tls.connect({ port: 80 })`],
|
|
[
|
|
"a good version written last",
|
|
`tls.connect({ port: 80, minVersion: "nope", minVersion: "TLSv1.2" })`,
|
|
],
|
|
[
|
|
"a version supplied by a spread",
|
|
`const o = { minVersion: "nope" }; tls.connect({ port: 80, ...o })`,
|
|
],
|
|
[
|
|
"a version supplied by an unresolvable computed key",
|
|
`const k = "minVersion"; tls.connect({ port: 80, [k]: "nope" })`,
|
|
],
|
|
["net ignoring the member", `net.connect({ port: 80, minVersion: "nope" })`],
|
|
] as const)("keeps the socket acquisition for %s", (_name, call) => {
|
|
expect(acquisitionsFor(call)).toEqual([
|
|
{ kind: "acquire", method: "connect" },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
`tls.connect({port:443,minVersion:"nope"},()=>{})`,
|
|
`tls.connect({port:443,minVersion:"nope",...{}})`,
|
|
`tls.connect({port:443,...{minVersion:"nope"}})`,
|
|
`tls.connect({port:443,...{...{minVersion:"nope"}}})`,
|
|
`tls.connect({port:443,minVersion:"nope",...0})`,
|
|
`tls.connect({port:443,minVersion:"nope",secureContext:undefined})`,
|
|
`tls.connect({port:443,minVersion:"nope",secureContext:void 0})`,
|
|
`tls.connect({port:443,minVersion:-value})`,
|
|
`tls.connect({port:443,minVersion:delete options.version})`,
|
|
])("keeps TLS rejection and linked release consistent: %s", (call) => {
|
|
const semantics = analyzeJavaScriptSemantics(
|
|
`import tls from "node:tls";function g(){const socket=${call};socket.destroy();}`,
|
|
);
|
|
expect(semantics.resourceOperations).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
`tls.connect(443,null,{minVersion:"nope"})`,
|
|
`tls.connect(443,()=>{}, {minVersion:"nope"})`,
|
|
])("reads TLS override after a known non-object argument: %s", (call) => {
|
|
expect(acquisitionsFor(call)).toEqual([]);
|
|
});
|
|
it.each([
|
|
`tls.connect({port:443,minVersion:"nope"},()=>{}, {minVersion:"TLSv1.2"})`,
|
|
`tls.connect({port:443,minVersion:"TLSv1.2"},[], {minVersion:"nope"})`,
|
|
`tls.connect(options,{minVersion:"nope"})`,
|
|
])("preserves effective accepted or unresolved TLS options: %s", (call) => {
|
|
expect(acquisitionsFor(call)).toHaveLength(1);
|
|
});
|
|
|
|
it("keeps a TLS version override unknown when the merge target may absorb it", () => {
|
|
expect(
|
|
acquisitionsFor(
|
|
`tls.connect({port:443,secureContext:false,__proto__:{set minVersion(value){}}},{minVersion:"nope"})`,
|
|
),
|
|
).toHaveLength(1);
|
|
});
|
|
it.each(["null", "0"])(
|
|
"rejects the override when prototype %s cannot absorb it",
|
|
(prototype) => {
|
|
expect(
|
|
acquisitionsFor(
|
|
`tls.connect({port:443,secureContext:false,__proto__:${prototype}},{minVersion:"nope"})`,
|
|
),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
`class O {port=443;set minVersion(value){}};const socket=tls.connect(new O(),{secureContext:false,minVersion:"nope"});socket.destroy();`,
|
|
`const socket=tls.connect({port:443,minVersion:"nope"},null,class {static minVersion="TLSv1.2"});socket.destroy();`,
|
|
`const socket=tls.connect({port:443,secureContext:false},{["__proto__"]:{set minVersion(value){}},minVersion:"nope"});socket.destroy();`,
|
|
])(
|
|
"preserves uncertain target assignments and enumerable sources: %s",
|
|
(body) => {
|
|
const ir = analyzeJavaScriptSemantics(
|
|
`import tls from "node:tls";function g(){${body}}`,
|
|
);
|
|
expect(
|
|
ir.resourceOperations.filter((value) => value.kind === "acquire"),
|
|
).toHaveLength(1);
|
|
expect(
|
|
ir.resourceOperations.filter((value) => value.kind === "release"),
|
|
).toHaveLength(1);
|
|
},
|
|
);
|
|
it.each([
|
|
`tls.connect({port:443,minVersion:"nope"},class {static minVersion="TLSv1.2"})`,
|
|
`tls.connect({port:443,secureContext:false},{["__proto__"]:null,minVersion:"nope"})`,
|
|
`tls.connect({port:443,secureContext:false},{["__proto__"]:0,minVersion:"nope"})`,
|
|
])("retains definite rejection for the actual selected source: %s", (call) => {
|
|
expect(acquisitionsFor(call)).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
'tls.connect({port:443,minVersion:"nope"},null,null)',
|
|
'tls.connect({port:443,minVersion:"nope"},null,5)',
|
|
'tls.connect({port:443,minVersion:"nope"},null,"TLSv1.2")',
|
|
'tls.connect({port:443,minVersion:"nope"},null,()=>{})',
|
|
'tls.connect({port:443,minVersion:"nope"},{__proto__:{minVersion:"TLSv1.2"}})',
|
|
])("copies only enumerable own option properties: %s", (call) => {
|
|
expect(acquisitionsFor(call)).toEqual([]);
|
|
});
|