paperclip/scripts/smoke/notion-generic-live.test.mjs

287 lines
11 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
assertAutomaticRegistrationSource,
assertSanitizedEvidence,
connectionRemovalFacts,
extractNotionIdentity,
extractNotionVerificationCode,
inspectAuthorizationUrl,
isFreshNotionVerificationMessage,
NotionGenericLivePreflightError,
notionVerificationAuthenticationPassed,
parseRuntimeAbsenceProof,
parseSanitizedAgentProof,
persistedOAuthStartResult,
preflightNotionGenericLive,
prepareNotionGenericLiveSmoke,
safeEndpointSummary,
} from "./notion-generic-live-lib.mjs";
const COMPLETE_ENV = {
PAPERCLIP_E2E_BASE_URL: "https://paperclip.example.test",
PAPERCLIP_E2E_EMAIL: "operator@example.test",
PAPERCLIP_DEV_LOGIN_PASSWORD: "not-a-real-password",
PAPERCLIP_API_URL: "https://paperclip.example.test/api",
PAPERCLIP_API_KEY: "not-a-real-agent-key",
PAPERCLIP_RUN_ID: "run-123",
PAPERCLIP_TASK_ID: "issue-123",
};
test("preflight reports binding names without exposing supplied values", () => {
assert.throws(
() => preflightNotionGenericLive({ PAPERCLIP_DEV_LOGIN_PASSWORD: "present" }),
(error) => {
assert.ok(error instanceof NotionGenericLivePreflightError);
assert.equal(error.code, "missing_environment");
assert.deepEqual(error.details.missing, [
"PAPERCLIP_E2E_BASE_URL",
"PAPERCLIP_E2E_EMAIL",
"PAPERCLIP_API_URL",
"PAPERCLIP_API_KEY",
"PAPERCLIP_RUN_ID",
"PAPERCLIP_TASK_ID",
]);
assert.doesNotMatch(error.message, /present/);
return true;
},
);
});
test("preflight requires explicit credential-free HTTPS target and control-plane URLs", () => {
for (const baseUrl of [
"http://127.0.0.1:3100",
"http://paperclip.example.test",
"https://user:secret@paperclip.example.test",
"https://paperclip.example.test/?code=secret",
]) {
assert.throws(
() => preflightNotionGenericLive({ ...COMPLETE_ENV, PAPERCLIP_E2E_BASE_URL: baseUrl }),
(error) => error instanceof NotionGenericLivePreflightError && error.code === "unsafe_base_url",
);
}
const split = preflightNotionGenericLive({
...COMPLETE_ENV,
PAPERCLIP_API_URL: "https://control-plane.example.test/api",
});
assert.equal(split.baseUrl, "https://paperclip.example.test");
assert.equal(split.apiBaseUrl, "https://control-plane.example.test/api");
});
test("health and binding metadata pass before browser loading, without fetching the value", async () => {
const requests = [];
let browserLoaded = false;
const prepared = await prepareNotionGenericLiveSmoke({
environment: COMPLETE_ENV,
fetchImpl: async (url, init = {}) => {
requests.push({ url: String(url), method: init.method ?? "GET" });
if (String(url).endsWith("/api/health")) {
return { ok: true, json: async () => ({ status: "ok" }) };
}
return {
ok: true,
json: async () => ({ secrets: [{ key: "generic-flow-test-account", delivery: "api" }] }),
};
},
loadBrowser: async () => {
browserLoaded = true;
return { chromium: {} };
},
});
assert.equal(prepared.config.callbackUrl, "https://paperclip.example.test/api/tools/oauth/callback");
assert.equal(browserLoaded, true);
assert.deepEqual(requests.map((entry) => entry.method), ["GET", "GET"]);
assert.equal(requests.some((entry) => entry.url.includes("/value")), false);
});
test("an unavailable secret binding fails before browser or credential entry", async () => {
let browserLoaded = false;
await assert.rejects(
prepareNotionGenericLiveSmoke({
environment: COMPLETE_ENV,
fetchImpl: async (url) => String(url).endsWith("/api/health")
? { ok: true, json: async () => ({ status: "ok" }) }
: { ok: true, json: async () => ({ secrets: [] }) },
loadBrowser: async () => {
browserLoaded = true;
},
}),
(error) => error instanceof NotionGenericLivePreflightError && error.code === "secret_binding_unavailable",
);
assert.equal(browserLoaded, false);
});
test("selects only fresh authenticated Notion verification mail and extracts one code", () => {
const notBefore = new Date("2026-08-18T12:00:00.000Z");
const message = {
timestamp: new Date("2026-08-18T12:00:05.000Z"),
from: "Notion <login@mail.notion.so>",
subject: "Your Notion login code",
extractedText: "Your temporary login code is 123 456.",
headers: {
"authentication-results": "dkim=pass; spf=pass; dmarc=pass",
},
};
assert.equal(isFreshNotionVerificationMessage(message, { notBefore }), true);
assert.equal(notionVerificationAuthenticationPassed(message), true);
assert.equal(extractNotionVerificationCode(message), "123456");
assert.equal(isFreshNotionVerificationMessage({
...message,
timestamp: new Date("2026-08-18T11:59:59.000Z"),
}, { notBefore }), false);
assert.equal(isFreshNotionVerificationMessage({
...message,
from: "Notion <login@example.test>",
}, { notBefore }), false);
assert.equal(notionVerificationAuthenticationPassed({
...message,
headers: { "authentication-results": "dkim=fail; spf=pass" },
}), false);
assert.equal(extractNotionVerificationCode({ ...message, extractedText: "Codes 123456 and 654321" }), null);
});
test("authorization proof requires automatic registration, PKCE, callback, resource, and safe endpoints", () => {
assert.equal(assertAutomaticRegistrationSource("cimd"), "cimd");
assert.equal(assertAutomaticRegistrationSource("dcr"), "dcr");
for (const source of ["manual", "preconfigured", null]) {
assert.throws(
() => assertAutomaticRegistrationSource(source),
(error) => error instanceof NotionGenericLivePreflightError && error.code === "unexpected_registration_source",
);
}
const baseUrl = "https://paperclip.example.test";
const callbackUrl = `${baseUrl}/api/tools/oauth/callback`;
const resource = "https://mcp.notion.com/mcp";
const url = new URL("https://mcp.notion.com/authorize");
url.searchParams.set("response_type", "code");
url.searchParams.set("client_id", `${baseUrl}/api/tools/oauth/client-metadata`);
url.searchParams.set("redirect_uri", callbackUrl);
url.searchParams.set("state", "not-recorded");
url.searchParams.set("code_challenge", "not-recorded");
url.searchParams.set("code_challenge_method", "S256");
url.searchParams.set("resource", resource);
assert.deepEqual(inspectAuthorizationUrl(url.toString(), {
callbackUrl,
resource,
registrationSource: "cimd",
baseUrl,
}), {
endpoint: { origin: "https://mcp.notion.com", path: "/authorize" },
parameters: { clientId: true, state: true, pkceS256: true, callbackUri: true, resource: true },
});
assert.deepEqual(safeEndpointSummary("https://mcp.notion.com/token", "token"), {
origin: "https://mcp.notion.com",
path: "/token",
});
assert.throws(
() => safeEndpointSummary("http://mcp.notion.com/token", "token"),
(error) => error instanceof NotionGenericLivePreflightError && error.code === "unsafe_token_endpoint",
);
});
test("authorization proof rejects a provider login page after OAuth parameters were consumed", () => {
assert.throws(
() => inspectAuthorizationUrl("https://id.notion.test/login", {
callbackUrl: "https://paperclip.example/api/tools/oauth/callback",
resource: "https://mcp.notion.com/mcp",
registrationSource: "dcr",
baseUrl: "https://paperclip.example",
}),
(error) => error instanceof NotionGenericLivePreflightError
&& error.code === "authorization_parameter_missing",
);
});
test("reconstructs the inline OAuth start from durable connection state and provider navigation", () => {
assert.deepEqual(persistedOAuthStartResult({
id: "connection-123",
config: {
oauth: {
clientRegistrationSource: "cimd",
issuer: "https://mcp.notion.com",
resource: "https://mcp.notion.com/mcp",
},
},
}, " https://mcp.notion.com/authorize?state=not-recorded "), {
connectionId: "connection-123",
authorizationUrl: "https://mcp.notion.com/authorize?state=not-recorded",
registrationSource: "cimd",
issuer: "https://mcp.notion.com",
resource: "https://mcp.notion.com/mcp",
});
assert.equal(persistedOAuthStartResult({ id: "connection-123", config: {} }, "https://example.test"), null);
});
test("accepts zero-count cleanup before setup but requires full revocation after install", () => {
const partial = {
installsRemoved: 0,
appProfileBindingsRemoved: 0,
credentialRefsCleared: 0,
secretsRevoked: 0,
secretBindingsRemoved: 0,
grantsRevoked: 0,
oauthStatesDiscarded: 1,
runtimeSlotsStopped: 0,
appProfile: "absent",
};
assert.deepEqual(connectionRemovalFacts(partial), {
credentialsRemoved: 0,
secretBindingsRemoved: 0,
grantsRevoked: 0,
accessBindingsRemoved: 0,
installsRemoved: 0,
oauthStatesDiscarded: 1,
runtimeSlotsStopped: 0,
appProfile: "absent",
});
assert.equal(connectionRemovalFacts(partial, { requireInstalled: true }), null);
assert.ok(connectionRemovalFacts({
...partial,
installsRemoved: 1,
appProfileBindingsRemoved: 1,
secretsRevoked: 1,
appProfile: "deleted",
}, { requireInstalled: true }));
});
test("workspace proof extraction and fresh-run comments retain only sanitized identity", () => {
const identity = extractNotionIdentity({
result: {
content: [{
type: "text",
text: JSON.stringify({
id: "bot-123",
type: "bot",
bot: { workspace_id: "workspace-123", workspace_name: "Paperclip" },
token: "discard-me",
}),
}],
},
});
assert.deepEqual(identity, {
workspaceId: "workspace-123",
workspaceName: "Paperclip",
botId: "bot-123",
});
assert.deepEqual(
parseSanitizedAgentProof(
'{"workspaceId":"workspace-123","workspaceName":"Paperclip","invocationId":"inv-123"}',
identity,
),
{ workspaceId: "workspace-123", workspaceName: "Paperclip", invocationId: "inv-123" },
);
assert.deepEqual(
parseRuntimeAbsenceProof('{"connectionId":"conn-123","toolPresent":false}', "conn-123"),
{ connectionId: "conn-123", toolPresent: false },
);
});
test("sanitized evidence rejects credential fields, sessions, and OAuth query values", () => {
assert.doesNotThrow(() => assertSanitizedEvidence({ workspaceId: "workspace-123", invocationId: "inv-123" }));
assert.throws(() => assertSanitizedEvidence({ accessToken: "secret" }), /unsafe_evidence_key/);
assert.throws(() => assertSanitizedEvidence({ sessionId: "secret" }), /unsafe_evidence_key/);
assert.throws(() => assertSanitizedEvidence({ note: "callback?code=secret" }), /unsafe_evidence_text/);
});