mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
test(mitm): bypass matching + manager bypass-json writer
Cover the new CJS routing primitives and the bypass-JSON manager write
path so the C1/C2 contracts cannot regress silently.
- `mitm-server-connect.test.ts` (27 tests): exercises the
`_internal/bypass.cjs` shim used by `server.cjs`:
- DEFAULT_BYPASS_PATTERNS shape (≥4 regexes, all RegExp)
- Default bank / gov / okta / auth0 → bypass
- Bypass beats target match (precedence)
- Known target hostname → target
- Unknown hostname → passthrough
- User glob pattern → bypass
- Empty / undefined hostname → passthrough
- Set-vs-Array shape for targetHosts
- Case-insensitive hostname matching
- bypassGlobMatch wildcard semantics + ReDoS-safe linear walk
- parseBypassJson with valid / empty / malformed / wrong-shape inputs
- Spec assertions on server.cjs source — header injection (C2),
sanitizeErrorMessage wrapping (Hard Rule #12), and CONNECT handler
registration (C1). These three guard against future regressions
that would silently re-introduce the bugs the C1/C2 fixes closed.
- `mitm-manager-bypass-json.test.ts` (5 tests): exercises
`writeBypassJson()`:
- Creates the `mitm/` dir + valid JSON file shape
- Empty array roundtrips as empty patterns
- Falls back to `getUserBypassPatterns()` when no arg passed
- Default patterns from the DB are NOT written to the file
- Overwrites prior content
This commit is contained in:
104
tests/unit/mitm-manager-bypass-json.test.ts
Normal file
104
tests/unit/mitm-manager-bypass-json.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Tests for `writeBypassJson()` in src/mitm/manager.ts.
|
||||
*
|
||||
* The function persists user bypass patterns to `<DATA_DIR>/mitm/bypass.json`
|
||||
* which is read by `src/mitm/server.cjs` at boot to drive the CONNECT
|
||||
* handler's bypass routing decision.
|
||||
*
|
||||
* Plan reference: 11-agent-bridge.plan.md §4.6 + master-plan-group-A.md §3.5.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "omniroute-mitm-bypass-json-")
|
||||
);
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const bypassDb = await import("../../src/lib/db/agentBridgeBypass.ts");
|
||||
const manager = await import("../../src/mitm/manager.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
try {
|
||||
if (fs.existsSync(TEST_DATA_DIR)) {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
}
|
||||
break;
|
||||
} catch (error: unknown) {
|
||||
const code = (error as { code?: string } | null)?.code;
|
||||
if ((code === "EBUSY" || code === "EPERM") && attempt < 9) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("writeBypassJson — creates mitm/ dir and writes JSON file", () => {
|
||||
manager.writeBypassJson(["custom.example.com"]);
|
||||
const file = path.join(TEST_DATA_DIR, "mitm", "bypass.json");
|
||||
assert.ok(fs.existsSync(file), "bypass.json must exist after write");
|
||||
const payload = JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||
assert.equal(payload.version, 1);
|
||||
assert.ok(typeof payload.generatedAt === "string");
|
||||
assert.deepEqual(payload.patterns, ["custom.example.com"]);
|
||||
});
|
||||
|
||||
test("writeBypassJson — empty array writes empty patterns", () => {
|
||||
manager.writeBypassJson([]);
|
||||
const file = path.join(TEST_DATA_DIR, "mitm", "bypass.json");
|
||||
const payload = JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||
assert.deepEqual(payload.patterns, []);
|
||||
});
|
||||
|
||||
test("writeBypassJson — pulls from DB when no patterns argument passed", () => {
|
||||
// Seed user patterns via the DB module.
|
||||
bypassDb.replaceUserBypassPatterns(["*.from-db.example.com", "literal.com"]);
|
||||
manager.writeBypassJson();
|
||||
const file = path.join(TEST_DATA_DIR, "mitm", "bypass.json");
|
||||
const payload = JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||
assert.deepEqual(
|
||||
payload.patterns.sort(),
|
||||
["*.from-db.example.com", "literal.com"].sort()
|
||||
);
|
||||
});
|
||||
|
||||
test("writeBypassJson — does NOT write default patterns (those live in server.cjs)", () => {
|
||||
// Seed defaults via the DB module — these should NOT appear in the JSON.
|
||||
bypassDb.seedDefaultBypassPatterns([
|
||||
"*.bank.test",
|
||||
"*.gov.test",
|
||||
"okta.com",
|
||||
"auth0.com",
|
||||
]);
|
||||
manager.writeBypassJson();
|
||||
const file = path.join(TEST_DATA_DIR, "mitm", "bypass.json");
|
||||
const payload = JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||
// No user patterns were set → user-only output is empty.
|
||||
assert.deepEqual(payload.patterns, []);
|
||||
});
|
||||
|
||||
test("writeBypassJson — JSON is well-formed and overwrites previous file", () => {
|
||||
manager.writeBypassJson(["first.com"]);
|
||||
manager.writeBypassJson(["second.com", "third.com"]);
|
||||
const file = path.join(TEST_DATA_DIR, "mitm", "bypass.json");
|
||||
const payload = JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||
assert.deepEqual(payload.patterns, ["second.com", "third.com"]);
|
||||
});
|
||||
275
tests/unit/mitm-server-connect.test.ts
Normal file
275
tests/unit/mitm-server-connect.test.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Tests for the bypass / passthrough / target routing primitives used by
|
||||
* the CJS proxy in `src/mitm/server.cjs`.
|
||||
*
|
||||
* The CJS proxy itself cannot be required in tests (it spawns a TLS server
|
||||
* and exits when ROUTER_API_KEY is missing). Instead, we exercise the
|
||||
* `_internal/bypass.cjs` shim that `server.cjs` depends on. That shim
|
||||
* carries all the routing logic — `server.cjs` is now a thin wiring layer
|
||||
* around it.
|
||||
*
|
||||
* Plan reference:
|
||||
* - 11-agent-bridge.plan.md §4.6
|
||||
* - master-plan-group-A.md §3.5 / §12 #16
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const requireCjs = createRequire(import.meta.url);
|
||||
const shim = requireCjs("../../src/mitm/_internal/bypass.cjs") as {
|
||||
DEFAULT_BYPASS_PATTERNS: RegExp[];
|
||||
bypassGlobMatch: (h: string, p: string) => boolean;
|
||||
routeBypass: (
|
||||
h: string,
|
||||
targetHosts: Set<string> | string[] | undefined,
|
||||
userPatterns: string[]
|
||||
) => "bypass" | "target" | "passthrough";
|
||||
parseBypassJson: (raw: string) => string[];
|
||||
};
|
||||
|
||||
const TARGETS = new Set([
|
||||
"daily-cloudcode-pa.googleapis.com",
|
||||
"api.githubcopilot.com",
|
||||
]);
|
||||
|
||||
test("DEFAULT_BYPASS_PATTERNS — at least the 4 mandatory regexes", () => {
|
||||
assert.ok(shim.DEFAULT_BYPASS_PATTERNS.length >= 4);
|
||||
for (const re of shim.DEFAULT_BYPASS_PATTERNS) {
|
||||
assert.ok(re instanceof RegExp);
|
||||
}
|
||||
});
|
||||
|
||||
test("routeBypass — default bank pattern → bypass", () => {
|
||||
assert.equal(shim.routeBypass("my.bank.example", TARGETS, []), "bypass");
|
||||
assert.equal(shim.routeBypass("secure.bank.com", TARGETS, []), "bypass");
|
||||
});
|
||||
|
||||
test("routeBypass — default gov pattern → bypass", () => {
|
||||
assert.equal(shim.routeBypass("portal.gov.br", TARGETS, []), "bypass");
|
||||
assert.equal(shim.routeBypass("tax.gov", TARGETS, []), "bypass");
|
||||
});
|
||||
|
||||
test("routeBypass — default okta pattern → bypass", () => {
|
||||
assert.equal(shim.routeBypass("mycorp.okta.com", TARGETS, []), "bypass");
|
||||
assert.equal(shim.routeBypass("okta.com", TARGETS, []), "bypass");
|
||||
});
|
||||
|
||||
test("routeBypass — default auth0 pattern → bypass", () => {
|
||||
assert.equal(shim.routeBypass("myapp.auth0.com", TARGETS, []), "bypass");
|
||||
});
|
||||
|
||||
test("routeBypass — bypass beats target match (precedence)", () => {
|
||||
// Hypothetical: user has an okta-hosted Copilot account. Bypass wins.
|
||||
const targets = new Set(["my.okta.com"]);
|
||||
assert.equal(shim.routeBypass("my.okta.com", targets, []), "bypass");
|
||||
});
|
||||
|
||||
test("routeBypass — known target hostname → target", () => {
|
||||
assert.equal(
|
||||
shim.routeBypass("daily-cloudcode-pa.googleapis.com", TARGETS, []),
|
||||
"target"
|
||||
);
|
||||
assert.equal(shim.routeBypass("api.githubcopilot.com", TARGETS, []), "target");
|
||||
});
|
||||
|
||||
test("routeBypass — unknown hostname → passthrough", () => {
|
||||
assert.equal(shim.routeBypass("example.com", TARGETS, []), "passthrough");
|
||||
assert.equal(shim.routeBypass("api.openai.com", TARGETS, []), "passthrough");
|
||||
});
|
||||
|
||||
test("routeBypass — user glob pattern → bypass", () => {
|
||||
const userPatterns = ["*.internal.example.com"];
|
||||
assert.equal(
|
||||
shim.routeBypass("admin.internal.example.com", TARGETS, userPatterns),
|
||||
"bypass"
|
||||
);
|
||||
assert.equal(
|
||||
shim.routeBypass("external.example.com", TARGETS, userPatterns),
|
||||
"passthrough"
|
||||
);
|
||||
});
|
||||
|
||||
test("routeBypass — empty hostname → passthrough", () => {
|
||||
assert.equal(shim.routeBypass("", TARGETS, []), "passthrough");
|
||||
assert.equal(
|
||||
shim.routeBypass(undefined as unknown as string, TARGETS, []),
|
||||
"passthrough"
|
||||
);
|
||||
});
|
||||
|
||||
test("routeBypass — targetHosts may be an array (not just Set)", () => {
|
||||
const targetsArr = [
|
||||
"daily-cloudcode-pa.googleapis.com",
|
||||
"api.githubcopilot.com",
|
||||
];
|
||||
assert.equal(
|
||||
shim.routeBypass("api.githubcopilot.com", targetsArr, []),
|
||||
"target"
|
||||
);
|
||||
assert.equal(shim.routeBypass("example.com", targetsArr, []), "passthrough");
|
||||
});
|
||||
|
||||
test("routeBypass — case-insensitive on hostname", () => {
|
||||
assert.equal(shim.routeBypass("MyApp.Okta.COM", TARGETS, []), "bypass");
|
||||
assert.equal(
|
||||
shim.routeBypass(
|
||||
"DAILY-cloudcode-pa.googleapis.com".toLowerCase(),
|
||||
TARGETS,
|
||||
[]
|
||||
),
|
||||
"target"
|
||||
);
|
||||
});
|
||||
|
||||
test("bypassGlobMatch — exact match (no wildcard)", () => {
|
||||
assert.ok(shim.bypassGlobMatch("api.openai.com", "api.openai.com"));
|
||||
assert.ok(!shim.bypassGlobMatch("api.openai.com", "api.anthropic.com"));
|
||||
});
|
||||
|
||||
test("bypassGlobMatch — single wildcard at start", () => {
|
||||
assert.ok(shim.bypassGlobMatch("foo.example.com", "*.example.com"));
|
||||
assert.ok(shim.bypassGlobMatch("bar.example.com", "*.example.com"));
|
||||
assert.ok(!shim.bypassGlobMatch("example.org", "*.example.com"));
|
||||
});
|
||||
|
||||
test("bypassGlobMatch — wildcard at end", () => {
|
||||
assert.ok(shim.bypassGlobMatch("api.example.com", "api.*"));
|
||||
assert.ok(!shim.bypassGlobMatch("svc.example.com", "api.*"));
|
||||
});
|
||||
|
||||
test("bypassGlobMatch — too many wildcards → rejected", () => {
|
||||
// 10 wildcards → segments.length === 11, exceeds the cap.
|
||||
const pat = "a*b*c*d*e*f*g*h*i*j*k";
|
||||
assert.equal(shim.bypassGlobMatch("abcdefghijk", pat), false);
|
||||
});
|
||||
|
||||
test("bypassGlobMatch — case-insensitive", () => {
|
||||
assert.ok(shim.bypassGlobMatch("FOO.EXAMPLE.COM", "*.example.com"));
|
||||
});
|
||||
|
||||
test("bypassGlobMatch — does not throw on regex-special chars in pattern", () => {
|
||||
// No regex compilation happens — the helper is a linear string walk.
|
||||
assert.doesNotThrow(() => shim.bypassGlobMatch("test.com", "(invalid["));
|
||||
});
|
||||
|
||||
test("parseBypassJson — valid JSON with patterns array", () => {
|
||||
const raw = JSON.stringify({
|
||||
version: 1,
|
||||
patterns: ["*.internal.example.com", "Custom.Host.COM"],
|
||||
});
|
||||
const parsed = shim.parseBypassJson(raw);
|
||||
assert.deepEqual(parsed, ["*.internal.example.com", "custom.host.com"]);
|
||||
});
|
||||
|
||||
test("parseBypassJson — empty input → []", () => {
|
||||
assert.deepEqual(shim.parseBypassJson(""), []);
|
||||
});
|
||||
|
||||
test("parseBypassJson — malformed JSON → []", () => {
|
||||
assert.deepEqual(shim.parseBypassJson("not json"), []);
|
||||
});
|
||||
|
||||
test("parseBypassJson — missing patterns property → []", () => {
|
||||
assert.deepEqual(shim.parseBypassJson(JSON.stringify({ version: 1 })), []);
|
||||
});
|
||||
|
||||
test("parseBypassJson — patterns not an array → []", () => {
|
||||
assert.deepEqual(
|
||||
shim.parseBypassJson(JSON.stringify({ patterns: "foo" })),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test("parseBypassJson — filters out non-string and empty entries", () => {
|
||||
const raw = JSON.stringify({
|
||||
patterns: ["valid.com", "", null, 42, "Another.COM"],
|
||||
});
|
||||
const parsed = shim.parseBypassJson(raw);
|
||||
assert.deepEqual(parsed, ["valid.com", "another.com"]);
|
||||
});
|
||||
|
||||
test("C2 header contract — server.cjs intercept must inject x-omniroute-source and x-omniroute-agent", async () => {
|
||||
// This is a documentation/spec assertion: the exact header names that
|
||||
// server.cjs::intercept must inject per master plan §3.5. If anyone
|
||||
// edits server.cjs to remove or rename them, this test fails and
|
||||
// flags the regression.
|
||||
const fs = await import("node:fs");
|
||||
const path = await import("node:path");
|
||||
const url = await import("node:url");
|
||||
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const serverPath = path.resolve(here, "../../src/mitm/server.cjs");
|
||||
const src = fs.readFileSync(serverPath, "utf-8");
|
||||
assert.match(
|
||||
src,
|
||||
/"x-omniroute-source":\s*"agent-bridge"/,
|
||||
'server.cjs must inject "x-omniroute-source: agent-bridge"'
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/"x-omniroute-agent":\s*agentId/,
|
||||
'server.cjs must inject "x-omniroute-agent: <id>" from the host→agent map'
|
||||
);
|
||||
// Antigravity non-regression: the historical host must still resolve to
|
||||
// the antigravity agent id, so the existing flow continues to work.
|
||||
assert.match(
|
||||
src,
|
||||
/TARGET_HOST_AGENT\.set\(lower,\s*id\)/,
|
||||
"server.cjs must populate TARGET_HOST_AGENT from targets.json"
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/TARGET_HOST_AGENT\.set\(h,\s*"antigravity"\)/,
|
||||
"server.cjs must seed antigravity baseline in TARGET_HOST_AGENT"
|
||||
);
|
||||
});
|
||||
|
||||
test("Hard Rule #12 — server.cjs intercept error path uses sanitizeErrorMessage", async () => {
|
||||
// Spec assertion: error responses must NOT leak raw err.message.
|
||||
const fs = await import("node:fs");
|
||||
const path = await import("node:path");
|
||||
const url = await import("node:url");
|
||||
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const serverPath = path.resolve(here, "../../src/mitm/server.cjs");
|
||||
const src = fs.readFileSync(serverPath, "utf-8");
|
||||
// sanitizeErrorMessage must wrap the error body.
|
||||
assert.match(
|
||||
src,
|
||||
/sanitizeErrorMessage\(error\s*&&\s*error\.message\)/,
|
||||
"intercept() error path must route through sanitizeErrorMessage()"
|
||||
);
|
||||
// The historical raw-leak pattern must be gone from the error body literal.
|
||||
const errorBodyRegion = src.match(
|
||||
/res\.end\(\s*JSON\.stringify\(\{\s*error[\s\S]*?type:\s*"mitm_error"[\s\S]*?\}\)\s*\)/
|
||||
);
|
||||
assert.ok(errorBodyRegion, "intercept() must build a JSON error body");
|
||||
assert.doesNotMatch(
|
||||
errorBodyRegion[0],
|
||||
/message:\s*error\.message[^a-zA-Z_]/,
|
||||
"intercept() error body must not contain raw error.message"
|
||||
);
|
||||
});
|
||||
|
||||
test("C1 contract — server.cjs registers a CONNECT handler", async () => {
|
||||
const fs = await import("node:fs");
|
||||
const path = await import("node:path");
|
||||
const url = await import("node:url");
|
||||
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const serverPath = path.resolve(here, "../../src/mitm/server.cjs");
|
||||
const src = fs.readFileSync(serverPath, "utf-8");
|
||||
assert.match(
|
||||
src,
|
||||
/server\.on\(\s*"connect"/,
|
||||
"server.cjs must register a CONNECT handler"
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/net\.connect\(/,
|
||||
"server.cjs must dial upstream via net.connect for bypass/passthrough"
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/HTTP\/1\.1\s+200\s+Connection Established/,
|
||||
"server.cjs CONNECT path must reply with 200 Connection Established"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user