mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
* fix(js): resolve literal URL from Request constructor in fetch calls * fix(js): gate Request URL extraction on fetch and require new operator
377 lines
8.7 KiB
TypeScript
377 lines
8.7 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
|
|
|
const endpoints = (source: string) =>
|
|
analyzeJavaScriptStaticSource(source).endpoints.map((endpoint) => ({
|
|
kind: endpoint.kind,
|
|
value: endpoint.value,
|
|
mechanism: endpoint.mechanism,
|
|
resolution: endpoint.resolution,
|
|
}));
|
|
|
|
it("resolves unchanged lexical strings and object properties", () => {
|
|
expect(
|
|
endpoints(`
|
|
const target = "/items";
|
|
const config = { url: "/accounts" };
|
|
fetch(target);
|
|
fetch("/direct");
|
|
fetch(config.url);
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "/items",
|
|
mechanism: "call:fetch",
|
|
resolution: undefined,
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "/direct",
|
|
mechanism: "call:fetch",
|
|
resolution: undefined,
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "/accounts",
|
|
mechanism: "call:fetch",
|
|
resolution: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("resolves unchanged URL objects and aliases using URL serialization", () => {
|
|
expect(
|
|
endpoints(`
|
|
const socket = new URL("HTTPS://EXAMPLE.COM:443");
|
|
const alias = socket;
|
|
const relative = new URL("/items", "https://api.example.test/base");
|
|
fetch(alias);
|
|
fetch(relative);
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "https://example.com/",
|
|
mechanism: "call:fetch",
|
|
resolution: undefined,
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "https://api.example.test/items",
|
|
mechanism: "call:fetch",
|
|
resolution: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps an interpolated request expression verbatim", () => {
|
|
expect(
|
|
endpoints("fetch(`https://${hosts['a@b']}/items/${id}`);"),
|
|
).toContainEqual({
|
|
kind: "network",
|
|
value: "`https://${hosts['a@b']}/items/${id}`",
|
|
mechanism: "call:fetch",
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("keeps writes to a config property partial instead of resolving its initializer", () => {
|
|
expect(
|
|
endpoints(`
|
|
const config = { url: "https://old.example.test" };
|
|
config.url = "https://new.example.test";
|
|
fetch(config.url);
|
|
`),
|
|
).toContainEqual({
|
|
kind: "network",
|
|
value: "config.url",
|
|
mechanism: "call:fetch",
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("keeps a reassigned endpoint binding partial", () => {
|
|
expect(
|
|
endpoints(`
|
|
let endpoint = "https://old.example.test";
|
|
endpoint = "https://new.example.test";
|
|
fetch(endpoint);
|
|
`),
|
|
).toContainEqual({
|
|
kind: "network",
|
|
value: "endpoint",
|
|
mechanism: "call:fetch",
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("does not resolve an endpoint from a declaration after the request", () => {
|
|
expect(
|
|
endpoints(`
|
|
fetch(endpoint);
|
|
const endpoint = "https://api.example.test/items";
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "endpoint",
|
|
mechanism: "call:fetch",
|
|
resolution: "partial",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps URLSearchParams mutations partial", () => {
|
|
expect(
|
|
endpoints(`
|
|
const socket = new URL("wss://old.example.test/socket");
|
|
socket.searchParams.set("token", token);
|
|
new WebSocket(socket);
|
|
`),
|
|
).toContainEqual({
|
|
kind: "network",
|
|
value: "socket",
|
|
mechanism: "call:WebSocket",
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("resolves a URL object against a literal base", () => {
|
|
expect(
|
|
endpoints(`
|
|
const socket = new URL("/socket", "wss://api.example.test");
|
|
new WebSocket(socket);
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "wss://api.example.test/socket",
|
|
mechanism: "call:WebSocket",
|
|
resolution: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("resolves an unchanged URL object's toString target", () => {
|
|
expect(
|
|
endpoints(`
|
|
const socket = new URL("wss://api.example.test/socket");
|
|
new WebSocket(socket.toString());
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "wss://api.example.test/socket",
|
|
mechanism: "call:WebSocket",
|
|
resolution: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps a URL object partial when its base is unresolved", () => {
|
|
expect(
|
|
endpoints(`
|
|
const socket = new URL("/socket", apiOrigin);
|
|
new WebSocket(socket);
|
|
`),
|
|
).toContainEqual({
|
|
kind: "network",
|
|
value: "socket",
|
|
mechanism: "call:WebSocket",
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("keeps URL aliases partial after a member mutation", () => {
|
|
expect(
|
|
endpoints(`
|
|
const socket = new URL("wss://old.example.test/socket");
|
|
const alias = socket;
|
|
alias.hostname = "new.example.test";
|
|
fetch(socket);
|
|
`),
|
|
).toContainEqual({
|
|
kind: "network",
|
|
value: "socket",
|
|
mechanism: "call:fetch",
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("keeps an escaped URL object partial", () => {
|
|
expect(
|
|
endpoints(`
|
|
const socket = new URL("wss://old.example.test/socket");
|
|
configure(socket);
|
|
new WebSocket(socket);
|
|
`),
|
|
).toContainEqual({
|
|
kind: "network",
|
|
value: "socket",
|
|
mechanism: "call:WebSocket",
|
|
resolution: "partial",
|
|
});
|
|
});
|
|
|
|
it("retains dynamic targets on known global fetch receivers", () => {
|
|
expect(
|
|
endpoints(`
|
|
window.fetch(endpoint);
|
|
globalThis.fetch(endpoint);
|
|
self.fetch(endpoint);
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "endpoint",
|
|
mechanism: "call:window.fetch",
|
|
resolution: "partial",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "endpoint",
|
|
mechanism: "call:globalThis.fetch",
|
|
resolution: "partial",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "endpoint",
|
|
mechanism: "call:self.fetch",
|
|
resolution: "partial",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("does not treat a locally bound fetch function as a network API", () => {
|
|
expect(
|
|
endpoints(`
|
|
function fetch(key) { return records[key]; }
|
|
fetch(key);
|
|
`),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("keeps exact literal targets exact", () => {
|
|
expect(endpoints('fetch("https://api.example.test/items");')).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "https://api.example.test/items",
|
|
mechanism: "call:fetch",
|
|
resolution: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("resolves inline Request constructor targets with literal URLs", () => {
|
|
expect(
|
|
endpoints(`
|
|
fetch(new Request("/api/chat", { method: "POST" }));
|
|
window.fetch(new Request("/api/window"));
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "/api/chat",
|
|
mechanism: "call:fetch",
|
|
resolution: undefined,
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "/api/window",
|
|
mechanism: "call:window.fetch",
|
|
resolution: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps Request calls without new partial", () => {
|
|
expect(endpoints('fetch(Request("/api/stream"));')).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: 'Request("/api/stream")',
|
|
mechanism: "call:fetch",
|
|
resolution: "partial",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("resolves unchanged Request objects and aliases", () => {
|
|
expect(
|
|
endpoints(`
|
|
const req = new Request("/api/chat", { method: "POST" });
|
|
const alias = req;
|
|
fetch(alias);
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "/api/chat",
|
|
mechanism: "call:fetch",
|
|
resolution: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps Request objects partial when called with toString", () => {
|
|
expect(
|
|
endpoints(`
|
|
const req = new Request("/api/chat");
|
|
fetch(req.toString());
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "req.toString()",
|
|
mechanism: "call:fetch",
|
|
resolution: "partial",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps Request targets partial on non-fetch network APIs", () => {
|
|
expect(
|
|
endpoints(`
|
|
const socketReq = new Request("wss://host/socket");
|
|
new WebSocket(new Request("wss://host/direct"));
|
|
new WebSocket(socketReq);
|
|
navigator.sendBeacon(new Request("https://api.example.test/beacon"));
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: 'new Request("wss://host/direct")',
|
|
mechanism: "call:WebSocket",
|
|
resolution: "partial",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "socketReq",
|
|
mechanism: "call:WebSocket",
|
|
resolution: "partial",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: 'new Request("https://api.example.test/beacon")',
|
|
mechanism: "call:navigator.sendBeacon",
|
|
resolution: "partial",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps Request objects partial when Request is locally shadowed", () => {
|
|
expect(
|
|
endpoints(`
|
|
function test(Request) {
|
|
fetch(new Request("/api/shadowed"));
|
|
}
|
|
`),
|
|
).toContainEqual({
|
|
kind: "network",
|
|
value: 'new Request("/api/shadowed")',
|
|
mechanism: "call:fetch",
|
|
resolution: "partial",
|
|
});
|
|
});
|