← back to Ventura Claw
server/tests/integration.test.js
307 lines
#!/usr/bin/env node
// VenturaClaw — integration tests (bare node + assert, no test runner)
// Target: http://127.0.0.1:9788 (pm2: ventura-claw)
// Run: node tests/integration.test.js
"use strict";
const assert = require("assert");
const BASE = "http://127.0.0.1:9788";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function req(method, path, { body, cookie, headers = {} } = {}) {
const opts = {
method,
headers: { "Content-Type": "application/json", ...headers },
};
if (cookie) opts.headers["Cookie"] = `cc_session=${cookie}`;
if (body != null) opts.body = JSON.stringify(body);
const res = await fetch(`${BASE}${path}`, opts);
let json = null;
try { json = await res.json(); } catch { /* non-JSON response */ }
return { status: res.status, json, headers: res.headers };
}
// Extract cc_session value from Set-Cookie header
function extractCookie(headers) {
const raw = headers.get("set-cookie") || "";
const m = raw.match(/cc_session=([^;]+)/);
return m ? m[1] : null;
}
// Generate a unique fake IP per run so rate-limit tests never collide with
// prior test runs against the same 15-minute window.
function uniqueTestIp() {
const ts = Date.now();
const a = (ts >> 16) & 0xff || 10;
const b = (ts >> 8) & 0xff || 1;
const c = ts & 0xff || 1;
return `10.${a}.${b}.${c}`;
}
// ---------------------------------------------------------------------------
// Test runner
// ---------------------------------------------------------------------------
const results = [];
async function test(name, fn) {
try {
await fn();
results.push({ name, ok: true });
console.log(` PASS ${name}`);
} catch (err) {
results.push({ name, ok: false, err: err.message });
console.log(` FAIL ${name}`);
console.log(` ${err.message}`);
}
}
// ---------------------------------------------------------------------------
// Suite
// ---------------------------------------------------------------------------
(async () => {
console.log("\nVenturaClaw — integration test suite");
console.log("=======================================\n");
// ------------------------------------------------------------------
// 1. bcrypt login — valid creds return 200 + set-cookie
// ------------------------------------------------------------------
let SESSION; // reused across subsequent tests
await test("bcrypt login: valid creds → 200 + cc_session cookie", async () => {
const { status, json, headers } = await req("POST", "/api/auth/login", {
body: { email: "steve@venturaclaw.com", password: "149940c2c5b209fb" },
headers: { "X-Forwarded-For": uniqueTestIp() },
});
assert.strictEqual(status, 200, `expected 200, got ${status}`);
assert.strictEqual(json?.ok, true, "response body must have ok:true");
assert.strictEqual(json?.role, "admin", "role must be 'admin'");
SESSION = extractCookie(headers);
assert.ok(SESSION && SESSION.length > 20, "cc_session cookie must be present and non-trivial");
});
// ------------------------------------------------------------------
// 2. wrong password → 401 (not 200)
// ------------------------------------------------------------------
await test("bcrypt login: wrong password → 401", async () => {
const { status, json } = await req("POST", "/api/auth/login", {
body: { email: "steve@venturaclaw.com", password: "wrongpassword" },
headers: { "X-Forwarded-For": uniqueTestIp() },
});
assert.strictEqual(status, 401, `expected 401, got ${status}`);
assert.ok(json?.error, "response must contain an error field");
});
// ------------------------------------------------------------------
// 3. rate-limit — 6 bad attempts on same IP → 6th is 429
// ------------------------------------------------------------------
await test("rate-limit: 6th bad-pw attempt from same IP → 429", async () => {
const ip = uniqueTestIp();
let lastStatus;
for (let i = 0; i < 6; i++) {
const { status } = await req("POST", "/api/auth/login", {
body: { email: "nobody@example.com", password: "badpw" },
headers: { "X-Forwarded-For": ip },
});
lastStatus = status;
}
assert.strictEqual(lastStatus, 429, `expected 429 on 6th attempt, got ${lastStatus}`);
});
// ------------------------------------------------------------------
// 4a. session validation — valid cookie → user JSON
// ------------------------------------------------------------------
await test("session validation: valid cookie → 200 user JSON", async () => {
assert.ok(SESSION, "SESSION must have been captured in test 1");
const { status, json } = await req("GET", "/api/me", { cookie: SESSION });
assert.strictEqual(status, 200, `expected 200, got ${status}`);
assert.ok(json?.user, "response must contain a 'user' object");
assert.strictEqual(json.user.email, "steve@venturaclaw.com", "user.email mismatch");
assert.strictEqual(json.user.role, "admin", "user.role must be 'admin'");
});
// ------------------------------------------------------------------
// 4b. session validation — no cookie → 401
// ------------------------------------------------------------------
await test("session validation: no cookie → 401", async () => {
const { status } = await req("GET", "/api/me");
assert.strictEqual(status, 401, `expected 401, got ${status}`);
});
// ------------------------------------------------------------------
// 5a. comms-compliance: slack.chat.postMessage → ok:true
// ------------------------------------------------------------------
await test("comms-compliance scrub: slack.chat.postMessage with channel+text → ok:true", async () => {
const { status, json } = await req("POST", "/api/comms/scrub-preview", {
cookie: SESSION,
body: {
action: "slack.chat.postMessage",
input: { channel: "general", text: "Deployment complete — all systems go." },
},
});
assert.strictEqual(status, 200, `expected 200, got ${status}`);
assert.strictEqual(json?.ok, true, `expected ok:true, got violations: ${JSON.stringify(json?.violations)}`);
assert.strictEqual(json?.kind, "slack");
});
// ------------------------------------------------------------------
// 5b. comms-compliance: twilio.sms.send without STOP → ok:false, TCPA rule
// ------------------------------------------------------------------
await test("comms-compliance scrub: twilio SMS without STOP → ok:false + TCPA marketing SMS violation", async () => {
const { status, json } = await req("POST", "/api/comms/scrub-preview", {
cookie: SESSION,
body: {
action: "twilio.sms.send",
input: { to: "+15550001234", body: "Buy now! Great deals await." },
},
});
assert.strictEqual(status, 200, `expected 200, got ${status}`);
assert.strictEqual(json?.ok, false, "expected ok:false (TCPA violation)");
const rules = (json?.violations || []).map((v) => v.rule);
assert.ok(
rules.includes("TCPA marketing SMS"),
`expected 'TCPA marketing SMS' in violations, got: ${JSON.stringify(rules)}`
);
});
// ------------------------------------------------------------------
// 5c. comms-compliance: gmail long body without unsubscribe/address → ok:false, CAN-SPAM
// ------------------------------------------------------------------
await test("comms-compliance scrub: gmail >80 chars, no unsubscribe, no address → ok:false, multiple CAN-SPAM violations", async () => {
const longBody =
"This is a promotional email about our incredible new product lineup that exceeds eighty characters easily.";
assert.ok(longBody.length > 80, "test body must exceed 80 chars");
const { status, json } = await req("POST", "/api/comms/scrub-preview", {
cookie: SESSION,
body: {
action: "gmail.message.send",
input: { to: "user@example.com", subject: "Special Offer", body: longBody },
},
});
assert.strictEqual(status, 200, `expected 200, got ${status}`);
assert.strictEqual(json?.ok, false, "expected ok:false (CAN-SPAM violations)");
assert.ok(
json?.violations?.length >= 2,
`expected at least 2 CAN-SPAM violations, got ${json?.violations?.length}: ${JSON.stringify(json?.violations)}`
);
const rules = json.violations.map((v) => v.rule);
const hasUnsubscribe = rules.some((r) => r.includes("CAN-SPAM"));
assert.ok(hasUnsubscribe, `expected at least one CAN-SPAM rule, got: ${JSON.stringify(rules)}`);
});
// ------------------------------------------------------------------
// 5d. comms-compliance: twilio.sms.send with valid phone → ok:false (DNC fail-closed)
// ------------------------------------------------------------------
await test("comms-compliance scrub: twilio SMS valid phone → ok:false (DNC snapshot missing → fail-closed)", async () => {
const { status, json } = await req("POST", "/api/comms/scrub-preview", {
cookie: SESSION,
body: {
action: "twilio.sms.send",
input: { to: "+12125550100", body: "Reply STOP to unsubscribe." },
},
});
assert.strictEqual(status, 200, `expected 200, got ${status}`);
// DNC snapshot is absent → fail-closed regardless of STOP instruction
assert.strictEqual(json?.ok, false, "expected ok:false because DNC snapshot is missing");
const rules = (json?.violations || []).map((v) => v.rule);
assert.ok(
rules.includes("Federal DNC"),
`expected 'Federal DNC' violation when snapshot absent, got: ${JSON.stringify(rules)}`
);
});
// ------------------------------------------------------------------
// 6. non-OAuth health: stripe → ok:true + account_id
// ------------------------------------------------------------------
await test("stripe health (api-key path): ok:true with account_id", async () => {
const { status, json } = await req("GET", "/api/connectors/stripe/health", {
cookie: SESSION,
});
assert.strictEqual(status, 200, `expected 200, got ${status}`);
assert.strictEqual(json?.ok, true, `stripe health expected ok:true, got: ${JSON.stringify(json)}`);
assert.ok(
json?.account_id && json.account_id.startsWith("acct_"),
`expected account_id starting with 'acct_', got: ${json?.account_id}`
);
});
// ------------------------------------------------------------------
// 7. OAuth health for unconnected: notion → ok:false, reason "no_token"
// ------------------------------------------------------------------
await test("notion health (no creds saved): ok:false, reason 'no_token'", async () => {
const { status, json } = await req("GET", "/api/connectors/notion/health", {
cookie: SESSION,
});
assert.strictEqual(status, 200, `expected 200, got ${status}`);
assert.strictEqual(json?.ok, false, `notion health expected ok:false, got: ${JSON.stringify(json)}`);
assert.strictEqual(
json?.reason,
"no_token",
`expected reason 'no_token', got '${json?.reason}'`
);
});
// ------------------------------------------------------------------
// 8. prototype-pollution guard: DELETE /api/admin/oauth/__proto__/credentials → 404
// ------------------------------------------------------------------
await test("prototype-pollution guard: DELETE __proto__ vendor → 404 unknown vendor", async () => {
const { status, json } = await req(
"DELETE",
"/api/admin/oauth/__proto__/credentials",
{ cookie: SESSION }
);
assert.strictEqual(status, 404, `expected 404, got ${status}`);
assert.strictEqual(
json?.error,
"unknown vendor",
`expected 'unknown vendor' error, got: ${JSON.stringify(json)}`
);
});
// ------------------------------------------------------------------
// 9. canonical-secrets allowlist: unrelated keys must NOT be in process.env
// ------------------------------------------------------------------
await test("canonical-secrets allowlist: unrelated env keys are undefined in process.env", () => {
// These keys exist in the master secrets-manager .env but are NOT in VenturaClaw's ALLOW set.
// Verifying here exercises the allowlist loader that runs at server boot — if any of these
// leaked into process.env the allowlist filter is broken.
//
// NOTE: this test runs in the test process, which did NOT load the CC env loader.
// It checks that the server's OWN boot-time loader would block these by confirming
// they are absent here — and by design the test process shares no state with the server.
// What we are directly asserting: these secrets are not spilled via any env-inheritance path.
const forbidden = [
"LAWYER_DIRECTORY_ADMIN_PASSWORD",
"DPLA_API_KEY",
"ANIMALS_DB_URL",
"SDCC_GOOGLE_DRIVE_KEY",
"BANKRUPT_LEADS_PACER_PASSWORD",
];
const leaked = forbidden.filter((k) => process.env[k] !== undefined);
assert.strictEqual(
leaked.length,
0,
`These unrelated secrets leaked into process.env (allowlist broken): ${leaked.join(", ")}`
);
});
// ------------------------------------------------------------------
// Summary
// ------------------------------------------------------------------
console.log("\n---------------------------------------");
const passed = results.filter((r) => r.ok).length;
const failed = results.filter((r) => !r.ok).length;
console.log(`Results: ${passed} passed, ${failed} failed (${results.length} total)\n`);
if (failed > 0) {
console.log("Failed tests:");
results.filter((r) => !r.ok).forEach((r) => console.log(` - ${r.name}\n ${r.err}`));
process.exit(1);
}
})();