Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
407 lines
13 KiB
TypeScript
407 lines
13 KiB
TypeScript
import type { AdapterContext } from "@rakazo/adapter-kit";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
isPipedreamEnabled,
|
|
MAX_PIPEDREAM_RESPONSE_BYTES,
|
|
PipedreamConnector,
|
|
pipedreamConfigFromEnv,
|
|
} from "./pipedream-connector.js";
|
|
import { ThirdPartyConnectorEmulator } from "./third-party-connector-emulator.js";
|
|
|
|
const context: AdapterContext = {
|
|
operationId: "pipedream-test",
|
|
traceId: "pipedream-test",
|
|
spaceId: "workspace-example",
|
|
userId: "user-example",
|
|
signal: new AbortController().signal,
|
|
};
|
|
|
|
describe("pipedreamConfigFromEnv", () => {
|
|
it("maps shared environment values and normalizes unsupported environments", () => {
|
|
expect(
|
|
pipedreamConfigFromEnv({
|
|
pipedreamClientId: "client-id",
|
|
pipedreamClientSecret: "client-secret",
|
|
pipedreamProjectId: "project-id",
|
|
pipedreamEnvironment: "staging",
|
|
encryptionKey: "identity-secret",
|
|
}),
|
|
).toEqual({
|
|
clientId: "client-id",
|
|
clientSecret: "client-secret",
|
|
projectId: "project-id",
|
|
environment: "development",
|
|
identitySecret: "identity-secret",
|
|
});
|
|
});
|
|
|
|
it.each(["0", "false"])("does not treat VITEST=%s as an active test runner", (value) => {
|
|
vi.stubEnv("VITEST", value);
|
|
expect(
|
|
isPipedreamEnabled({
|
|
clientId: "client-id",
|
|
clientSecret: "client-secret",
|
|
projectId: "project-id",
|
|
environment: "production",
|
|
identitySecret: "identity-secret",
|
|
}),
|
|
).toBe(true);
|
|
vi.unstubAllEnvs();
|
|
});
|
|
});
|
|
|
|
describe("PipedreamConnector", () => {
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
it.each([
|
|
"javascript:alert(document.domain)",
|
|
"http://pipedream.example.test/connect",
|
|
"https://user:password@pipedream.example.test/connect",
|
|
])("rejects an unsafe provider connect URL: %s", async (connectUrl) => {
|
|
const fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
Response.json({ access_token: "fake-access-token", expires_in: 3_600 }),
|
|
)
|
|
.mockResolvedValueOnce(Response.json({ connect_link_url: connectUrl }));
|
|
const connector = new PipedreamConnector(
|
|
{
|
|
clientId: "fake-client-id",
|
|
clientSecret: "fake-client-secret",
|
|
projectId: "fake-project-id",
|
|
environment: "development",
|
|
identitySecret: "fake-identity-secret",
|
|
},
|
|
{ fetch },
|
|
);
|
|
|
|
await expect(
|
|
connector.begin(
|
|
{ provider: "gmail", redirectUrl: "https://rakazo.example.test/app" },
|
|
context,
|
|
),
|
|
).rejects.toThrow("secure HTTPS connect URL");
|
|
});
|
|
|
|
it("rejects an oversized token response before buffering it", async () => {
|
|
const response = new Response("oversized", {
|
|
headers: { "content-length": String(MAX_PIPEDREAM_RESPONSE_BYTES + 1) },
|
|
});
|
|
const cancel = vi.spyOn(response.body!, "cancel");
|
|
const connector = new PipedreamConnector(
|
|
{
|
|
clientId: "fake-client-id",
|
|
clientSecret: "fake-client-secret",
|
|
projectId: "fake-project-id",
|
|
environment: "development",
|
|
identitySecret: "fake-identity-secret",
|
|
},
|
|
{ fetch: vi.fn().mockResolvedValue(response) },
|
|
);
|
|
|
|
await expect(
|
|
connector.begin(
|
|
{ provider: "gmail", redirectUrl: "https://rakazo.example.test/app" },
|
|
context,
|
|
),
|
|
).rejects.toThrow("Pipedream response is too large.");
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("rejects oversized Content-Length without waiting on a hanging body cancel", async () => {
|
|
let cancelStarted = false;
|
|
const hangingBody = new ReadableStream<Uint8Array>({
|
|
start() {
|
|
// never enqueues or closes
|
|
},
|
|
cancel() {
|
|
cancelStarted = true;
|
|
return new Promise(() => {
|
|
// never settles
|
|
});
|
|
},
|
|
});
|
|
const abort = new AbortController();
|
|
const fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
Response.json({ access_token: "fake-access-token", expires_in: 3_600 }),
|
|
)
|
|
.mockImplementationOnce(async () => {
|
|
setTimeout(() => abort.abort(), 20);
|
|
return new Response(hangingBody, {
|
|
headers: { "content-length": String(MAX_PIPEDREAM_RESPONSE_BYTES + 1) },
|
|
});
|
|
});
|
|
const connector = new PipedreamConnector(
|
|
{
|
|
clientId: "fake-client-id",
|
|
clientSecret: "fake-client-secret",
|
|
projectId: "fake-project-id",
|
|
environment: "development",
|
|
identitySecret: "fake-identity-secret",
|
|
},
|
|
{ fetch },
|
|
);
|
|
|
|
const started = Date.now();
|
|
await expect(
|
|
connector.begin(
|
|
{ provider: "gmail", redirectUrl: "https://rakazo.example.test/app" },
|
|
{ ...context, signal: abort.signal },
|
|
),
|
|
).rejects.toThrow("Pipedream response is too large.");
|
|
expect(cancelStarted).toBe(true);
|
|
expect(Date.now() - started).toBeLessThan(500);
|
|
});
|
|
|
|
it("caps a chunked API response without a content length", async () => {
|
|
const fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
Response.json({ access_token: "fake-access-token", expires_in: 3_600 }),
|
|
)
|
|
.mockResolvedValueOnce(new Response(new Uint8Array(MAX_PIPEDREAM_RESPONSE_BYTES + 1)));
|
|
const connector = new PipedreamConnector(
|
|
{
|
|
clientId: "fake-client-id",
|
|
clientSecret: "fake-client-secret",
|
|
projectId: "fake-project-id",
|
|
environment: "development",
|
|
identitySecret: "fake-identity-secret",
|
|
},
|
|
{ fetch },
|
|
);
|
|
|
|
await expect(
|
|
connector.begin(
|
|
{ provider: "gmail", redirectUrl: "https://rakazo.example.test/app" },
|
|
context,
|
|
),
|
|
).rejects.toThrow("Pipedream response is too large.");
|
|
});
|
|
|
|
it("clears a rejected token before reading an oversized 401 response", async () => {
|
|
const fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(Response.json({ access_token: "rejected-token", expires_in: 3_600 }))
|
|
.mockResolvedValueOnce(
|
|
new Response("oversized", {
|
|
status: 401,
|
|
headers: { "content-length": String(MAX_PIPEDREAM_RESPONSE_BYTES + 1) },
|
|
}),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
Response.json({ access_token: "replacement-token", expires_in: 3_600 }),
|
|
)
|
|
.mockResolvedValueOnce(
|
|
Response.json({ connect_link_url: "https://pipedream.example.test/connect" }),
|
|
);
|
|
const connector = new PipedreamConnector(
|
|
{
|
|
clientId: "fake-client-id",
|
|
clientSecret: "fake-client-secret",
|
|
projectId: "fake-project-id",
|
|
environment: "development",
|
|
identitySecret: "fake-identity-secret",
|
|
},
|
|
{ fetch },
|
|
);
|
|
|
|
await expect(
|
|
connector.begin(
|
|
{ provider: "gmail", redirectUrl: "https://rakazo.example.test/app" },
|
|
context,
|
|
),
|
|
).rejects.toThrow("Pipedream response is too large.");
|
|
await expect(
|
|
connector.begin(
|
|
{ provider: "gmail", redirectUrl: "https://rakazo.example.test/app" },
|
|
context,
|
|
),
|
|
).resolves.toEqual({
|
|
authorizationUrl: "https://pipedream.example.test/connect?app=gmail",
|
|
state: "gmail",
|
|
});
|
|
expect(fetch).toHaveBeenCalledTimes(4);
|
|
expect(fetch.mock.calls[3]?.[1]?.headers).toEqual(
|
|
expect.objectContaining({ authorization: "Bearer replacement-token" }),
|
|
);
|
|
});
|
|
|
|
it("uses one opaque external identity across the app catalog and account flow", async () => {
|
|
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
|
const url = String(input);
|
|
requests.push({ url, init });
|
|
if (url.endsWith("/v1/oauth/token")) {
|
|
return Response.json({ access_token: "fake-access-token", expires_in: 3_600 });
|
|
}
|
|
if (url.includes("/v1/connect/apps?")) {
|
|
return Response.json({
|
|
data: [{ id: "app-1", name_slug: "gmail", name: "Gmail" }],
|
|
page_info: {},
|
|
});
|
|
}
|
|
if (url.includes("/accounts?")) {
|
|
return Response.json({
|
|
data: [
|
|
{
|
|
id: "account-1",
|
|
healthy: true,
|
|
app: { id: "app-1", name_slug: "gmail", name: "Gmail" },
|
|
},
|
|
],
|
|
page_info: {},
|
|
});
|
|
}
|
|
if (url.endsWith("/tokens")) {
|
|
return Response.json({ connect_link_url: "https://pipedream.example.test/connect" });
|
|
}
|
|
throw new Error(`Unexpected request ${url}`);
|
|
}),
|
|
);
|
|
const connector = new PipedreamConnector({
|
|
clientId: "fake-client-id",
|
|
clientSecret: "fake-client-secret",
|
|
projectId: "fake-project-id",
|
|
environment: "development",
|
|
identitySecret: "fake-identity-secret",
|
|
});
|
|
|
|
await expect(connector.catalog(context)).resolves.toEqual([
|
|
expect.objectContaining({
|
|
connectorId: "pipedream",
|
|
slug: "gmail",
|
|
connected: true,
|
|
}),
|
|
]);
|
|
const started = await connector.begin(
|
|
{ provider: "gmail", redirectUrl: "https://rakazo.example.test/app" },
|
|
context,
|
|
);
|
|
|
|
expect(started.authorizationUrl).toBe("https://pipedream.example.test/connect?app=gmail");
|
|
expect(requests.filter((request) => request.url.endsWith("/v1/oauth/token"))).toHaveLength(1);
|
|
expect(
|
|
requests.find((request) => request.url.endsWith("/v1/oauth/token"))?.init?.body,
|
|
).toContain('"scope":"connect:*"');
|
|
const accountUrl = requests.find((request) => request.url.includes("/accounts?"))?.url;
|
|
const connectRequest = requests.find((request) => request.url.endsWith("/tokens"));
|
|
const externalId = new URL(accountUrl!).searchParams.get("external_user_id");
|
|
expect(externalId).toMatch(/^rkz_[a-f0-9]{64}$/);
|
|
expect(externalId).not.toContain(context.userId);
|
|
expect(externalId).not.toContain(context.spaceId);
|
|
expect(JSON.parse(String(connectRequest?.init?.body))).toEqual(
|
|
expect.objectContaining({ external_user_id: externalId }),
|
|
);
|
|
});
|
|
|
|
it("marks account-list failures as pre-delete so revoke can restore local retry state", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (input: string | URL | Request) => {
|
|
const url = String(input);
|
|
if (url.endsWith("/v1/oauth/token")) {
|
|
return Response.json({ access_token: "fake-access-token", expires_in: 3_600 });
|
|
}
|
|
if (url.includes("/accounts?")) {
|
|
const error = new Error("network timeout listing accounts");
|
|
error.name = "TimeoutError";
|
|
throw error;
|
|
}
|
|
throw new Error(`Unexpected request ${url}`);
|
|
}),
|
|
);
|
|
const connector = new PipedreamConnector({
|
|
clientId: "fake-client-id",
|
|
clientSecret: "fake-client-secret",
|
|
projectId: "fake-project-id",
|
|
environment: "development",
|
|
identitySecret: "fake-identity-secret",
|
|
});
|
|
|
|
await expect(connector.revoke("account-1", context)).rejects.toMatchObject({
|
|
name: "TimeoutError",
|
|
remoteRevokePreDelete: true,
|
|
});
|
|
});
|
|
|
|
it("runs catalog, connection, discovery, execution, and revoke against the protocol emulator", async () => {
|
|
const emulator = new ThirdPartyConnectorEmulator();
|
|
const connector = new PipedreamConnector(
|
|
{
|
|
clientId: "fake-client-id",
|
|
clientSecret: "fake-client-secret",
|
|
projectId: "fake-project-id",
|
|
environment: "development",
|
|
identitySecret: "fake-identity-secret",
|
|
},
|
|
{ fetch: emulator.fetch, resolveHostname: emulator.resolveHostname },
|
|
);
|
|
|
|
await expect(connector.catalog(context)).resolves.toContainEqual(
|
|
expect.objectContaining({ connectorId: "pipedream", slug: "linear", connected: false }),
|
|
);
|
|
await expect(
|
|
connector.begin(
|
|
{ provider: "linear", redirectUrl: "https://rakazo.example.test/app" },
|
|
context,
|
|
),
|
|
).resolves.toEqual({
|
|
authorizationUrl: "https://pipedream.example.test/connect?app=linear",
|
|
state: "linear",
|
|
});
|
|
await expect(connector.connectionReady(context, "linear")).resolves.toBe(true);
|
|
|
|
const connectedContext = {
|
|
...context,
|
|
connectedConnections: [
|
|
{
|
|
id: "connection-linear",
|
|
connectorId: "pipedream",
|
|
externalId: "linear",
|
|
displayName: "Linear",
|
|
},
|
|
],
|
|
};
|
|
const tools = await connector.discoverTools(connectedContext);
|
|
expect(tools).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "notes.write",
|
|
route: expect.objectContaining({ connectorId: "pipedream", resourceId: "linear" }),
|
|
}),
|
|
);
|
|
const events = [];
|
|
for await (const event of connector.execute(
|
|
{
|
|
tool: "notes.write",
|
|
args: { text: "emulated-pipedream-ok" },
|
|
executionId: "pipedream-emulated-execution",
|
|
route: tools[0]?.route,
|
|
},
|
|
connectedContext,
|
|
)) {
|
|
events.push(event);
|
|
}
|
|
expect(events).toContainEqual(
|
|
expect.objectContaining({
|
|
type: "result",
|
|
data: expect.objectContaining({ isError: false }),
|
|
}),
|
|
);
|
|
expect(emulator.records).toContainEqual(
|
|
expect.objectContaining({
|
|
provider: "mcp",
|
|
operation: "notes.write",
|
|
args: { text: "emulated-pipedream-ok" },
|
|
}),
|
|
);
|
|
|
|
await connector.revoke("linear", context);
|
|
await expect(connector.connectionReady(context, "linear")).resolves.toBe(false);
|
|
});
|
|
});
|