import { randomBytes } from "node:crypto"; import { expect } from "@effect/vitest";import { Effect } from "effect";import { composePluginApi } from "@executor-js/api/server";import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";import { AuthTemplateSlug, ConnectionName, IntegrationSlug, OAuthClientSlug,} from "@executor-js/sdk/shared";import { serveOAuthTestServer } from "@executor-js/sdk/testing"; import { scenario } from "../src/scenario";import { Api, Browser, Target } from "../src/services"; const api = composePluginApi([openApiHttpPlugin()] as const); const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; const oauthIntegrationSpec = (oauth: { readonly authorizationEndpoint: string; readonly tokenEndpoint: string;}) => ({ spec: { kind: "blob" as const, value: JSON.stringify({ openapi: "3.0.3", info: { title: "OAuth-protected API", version: "1.0.0" }, paths: { "/me": { get: { operationId: "getMe", tags: ["default"], responses: { "200": { description: "the caller" } }, }, }, }, }), }, baseUrl: "http://127.0.0.1:59999", authenticationTemplate: [ { slug: "oauth", kind: "oauth2" as const, authorizationUrl: oauth.authorizationEndpoint, tokenUrl: oauth.tokenEndpoint, scopes: ["read"], }, ], }) as const; scenario( "OAuth callback · a signed-out callback uses login returnTo and resumes the connection", {}, Effect.gen(function* () { const target = yield* Target; const { client: makeApiClient } = yield* Api; const browser = yield* Browser; const oauth = yield* serveOAuthTestServer(); const identity = yield* target.newIdentity(); const client = yield* makeApiClient(api, identity); const integration = IntegrationSlug.make(unique("signedoutcb")); yield* client.openapi.addSpec({ payload: { ...oauthIntegrationSpec(oauth), slug: integration }, }); const clientSlug = OAuthClientSlug.make(unique("signedoutc")); yield* client.oauth.createClient({ payload: { owner: "org", slug: clientSlug, authorizationUrl: oauth.authorizationEndpoint, tokenUrl: oauth.tokenEndpoint, grant: "authorization_code", clientId: "test-client", clientSecret: "test-secret", }, }); const started = yield* client.oauth.start({ payload: { client: clientSlug, clientOwner: "org", owner: "org", name: ConnectionName.make("main"), integration, template: AuthTemplateSlug.make("oauth"), }, }); expect(started.status, "oauth.start begins at the provider").toBe("redirect"); const authorizationUrl = started.status === "redirect" ? started.authorizationUrl : ""; const authorize = yield* Effect.promise(() => fetch(authorizationUrl, { redirect: "manual" })); expect(authorize.status, "the provider asks the user to log in").toBe(302); const consent = yield* Effect.promise(() => fetch(authorize.headers.get("location") ?? "", { method: "POST", redirect: "manual", headers: { authorization: `Basic ${Buffer.from("alice:password").toString("base64")}`, }, }), ); expect(consent.status, "provider consent redirects back to Executor").toBe(302); const callback = new URL(consent.headers.get("location") ?? ""); const callbackPath = `${callback.pathname}${callback.search}`; // No cookies: this mirrors a provider redirect reaching the callback after // the user's web session is gone, expired, or otherwise missing an org. const anonymous = { label: "anonymous" }; yield* browser.session(anonymous, async ({ page, step }) => { await step("Provider sends a signed-out browser to the OAuth callback", async () => { const response = await page.goto(callbackPath, { waitUntil: "commit" }); expect(response?.status(), "the callback redirects into the login flow").toBe(200); await page.getByText("Sign in to manage your tools and integrations").waitFor(); }); const loginUrl = new URL(page.url()); expect(loginUrl.pathname, "the signed-out callback lands on the sign-in page").toBe("/login"); expect( loginUrl.searchParams.get("returnTo"), "login preserves the callback so it can resume after sign-in", ).toBe(callbackPath); await step("Sign in resumes the original OAuth callback", async () => { await page.getByRole("link", { name: "Sign in" }).click(); await page.getByPlaceholder("new-user@example.com").fill(identity.credentials!.email); await page.getByRole("button", { name: /Continue/ }).click(); await page.waitForURL((url) => url.pathname === "/api/oauth/callback", { timeout: 30_000, }); await page.waitForFunction(() => document.body.innerText.includes("Connected"), null, { timeout: 30_000, }); }); const body = (await page.locator("body").textContent())?.trim() ?? ""; expect(new URL(page.url()).pathname, "the login returnTo lands back on the callback").toBe( "/api/oauth/callback", ); expect(body, "the callback completes after the sign-in recovery").toContain("Connected"); expect(body, "the raw protected API JSON is not shown").not.toContain( '"code":"no_organization"', ); }); }).pipe(Effect.scoped),);
import { randomBytes } from "node:crypto";
import { expect } from "@effect/vitest";
import { Effect } from "effect";
import { composePluginApi } from "@executor-js/api/server";
import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
import {
AuthTemplateSlug,
ConnectionName,
IntegrationSlug,
OAuthClientSlug,
} from "@executor-js/sdk/shared";
import { serveOAuthTestServer } from "@executor-js/sdk/testing";
import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";
const api = composePluginApi([openApiHttpPlugin()] as const);
const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`;
const oauthIntegrationSpec = (oauth: {
readonly authorizationEndpoint: string;
readonly tokenEndpoint: string;
}) =>
({
spec: {
kind: "blob" as const,
value: JSON.stringify({
openapi: "3.0.3",
info: { title: "OAuth-protected API", version: "1.0.0" },
paths: {
"/me": {
get: {
operationId: "getMe",
tags: ["default"],
responses: { "200": { description: "the caller" } },
},
},
},
}),
},
baseUrl: "http://127.0.0.1:59999",
authenticationTemplate: [
{
slug: "oauth",
kind: "oauth2" as const,
authorizationUrl: oauth.authorizationEndpoint,
tokenUrl: oauth.tokenEndpoint,
scopes: ["read"],
},
],
}) as const;
scenario(
"OAuth callback · a signed-out callback uses login returnTo and resumes the connection",
{},
Effect.gen(function* () {
const target = yield* Target;
const { client: makeApiClient } = yield* Api;
const browser = yield* Browser;
const oauth = yield* serveOAuthTestServer();
const identity = yield* target.newIdentity();
const client = yield* makeApiClient(api, identity);
const integration = IntegrationSlug.make(unique("signedoutcb"));
yield* client.openapi.addSpec({
payload: { ...oauthIntegrationSpec(oauth), slug: integration },
});
const clientSlug = OAuthClientSlug.make(unique("signedoutc"));
yield* client.oauth.createClient({
payload: {
owner: "org",
slug: clientSlug,
authorizationUrl: oauth.authorizationEndpoint,
tokenUrl: oauth.tokenEndpoint,
grant: "authorization_code",
clientId: "test-client",
clientSecret: "test-secret",
},
});
const started = yield* client.oauth.start({
payload: {
client: clientSlug,
clientOwner: "org",
owner: "org",
name: ConnectionName.make("main"),
integration,
template: AuthTemplateSlug.make("oauth"),
},
});
expect(started.status, "oauth.start begins at the provider").toBe("redirect");
const authorizationUrl = started.status === "redirect" ? started.authorizationUrl : "";
const authorize = yield* Effect.promise(() => fetch(authorizationUrl, { redirect: "manual" }));
expect(authorize.status, "the provider asks the user to log in").toBe(302);
const consent = yield* Effect.promise(() =>
fetch(authorize.headers.get("location") ?? "", {
method: "POST",
redirect: "manual",
headers: {
authorization: `Basic ${Buffer.from("alice:password").toString("base64")}`,
},
}),
);
expect(consent.status, "provider consent redirects back to Executor").toBe(302);
const callback = new URL(consent.headers.get("location") ?? "");
const callbackPath = `${callback.pathname}${callback.search}`;
// No cookies: this mirrors a provider redirect reaching the callback after
// the user's web session is gone, expired, or otherwise missing an org.
const anonymous = { label: "anonymous" };
yield* browser.session(anonymous, async ({ page, step }) => {
await step("Provider sends a signed-out browser to the OAuth callback", async () => {
const response = await page.goto(callbackPath, { waitUntil: "commit" });
expect(response?.status(), "the callback redirects into the login flow").toBe(200);
await page.getByText("Sign in to manage your tools and integrations").waitFor();
});
const loginUrl = new URL(page.url());
expect(loginUrl.pathname, "the signed-out callback lands on the sign-in page").toBe("/login");
expect(
loginUrl.searchParams.get("returnTo"),
"login preserves the callback so it can resume after sign-in",
).toBe(callbackPath);
await step("Sign in resumes the original OAuth callback", async () => {
await page.getByRole("link", { name: "Sign in" }).click();
await page.getByPlaceholder("new-user@example.com").fill(identity.credentials!.email);
await page.getByRole("button", { name: /Continue/ }).click();
await page.waitForURL((url) => url.pathname === "/api/oauth/callback", {
timeout: 30_000,
});
await page.waitForFunction(() => document.body.innerText.includes("Connected"), null, {
timeout: 30_000,
});
});
const body = (await page.locator("body").textContent())?.trim() ?? "";
expect(new URL(page.url()).pathname, "the login returnTo lands back on the callback").toBe(
"/api/oauth/callback",
);
expect(body, "the callback completes after the sign-in recovery").toContain("Connected");
expect(body, "the raw protected API JSON is not shown").not.toContain(
'"code":"no_organization"',
);
});
}).pipe(Effect.scoped),
);