Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions lib/utils/switchOrg.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { IssuerRouteTypes, PromptTypes } from "../types";

vi.mock("./generateAuthUrl", () => {
return {
generateAuthUrl: vi.fn().mockResolvedValue({
url: new URL(
"https://auth.example.com/oauth2/auth?client_id=mock&response_type=code&redirect_uri=https%3A%2F%2Fredirect.example.com&audience=&scope=openid+profile&prompt=none&state=mockstate&nonce=mocknonce&code_challenge=mockchallenge&code_challenge_method=S256&org_code=org_123",
),
state: "mockstate",
nonce: "mocknonce",
codeChallenge: "mockchallenge",
codeVerifier: "mockverifier",
}),
};
});

import { generateAuthUrl } from "./generateAuthUrl";
import { switchOrg } from "./switchOrg";

describe("switchOrg", () => {
afterEach(() => {
vi.clearAllMocks();
});

it("throws when domain is missing", () => {
expect(() => {
// @ts-expect-error testing runtime validation
switchOrg({
orgCode: "org_123",
redirectURL: "https://redirect.example.com",
});
}).toThrow("domain is required for switchOrg");
});

it("throws when orgCode is missing", () => {
expect(() => {
// @ts-expect-error testing runtime validation
switchOrg({
domain: "https://auth.example.com",
redirectURL: "https://redirect.example.com",
});
}).toThrow("orgCode is required for switchOrg");
});

it("throws when redirectURL is missing", () => {
expect(() => {
// @ts-expect-error testing runtime validation
switchOrg({ domain: "https://auth.example.com", orgCode: "org_123" });
}).toThrow("redirectURL is required for switchOrg");
});

it("calls generateAuthUrl with correct args and returns its result", async () => {
const domain = "https://auth.example.com";
const orgCode = "org_123";
const redirectURL = "https://redirect.example.com";

const result = await switchOrg({ domain, orgCode, redirectURL });

expect(generateAuthUrl).toHaveBeenCalledTimes(1);
expect(generateAuthUrl).toHaveBeenCalledWith(
domain,
IssuerRouteTypes.login,
expect.objectContaining({
prompt: PromptTypes.none,
orgCode,
redirectURL,
}),
);

expect(result.state).toBe("mockstate");
expect(result.nonce).toBe("mocknonce");
expect(result.codeChallenge).toBe("mockchallenge");
expect(result.codeVerifier).toBe("mockverifier");

const prompt = result.url.searchParams.get("prompt");
const orgParam = result.url.searchParams.get("org_code");
const redirectParam = result.url.searchParams.get("redirect_uri");
expect(prompt).toBe("none");
expect(orgParam).toBe("org_123");
expect(redirectParam).toBe("https://redirect.example.com");
});
});
43 changes: 43 additions & 0 deletions lib/utils/switchOrg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
IssuerRouteTypes,
OrgCode,
PromptTypes,
type LoginOptions,
} from "../types";
import { generateAuthUrl } from "./generateAuthUrl";

export interface SwitchOrgParams {
domain: string;
orgCode: OrgCode;
redirectURL: string;
}

/**
* Switch the active organization without prompting.
* @param params - { domain, orgCode, redirectURL }
* @returns OAuth URL bundle to redirect to (auto-switches active org).
* @throws Error when domain, orgCode, or redirectURL are missing.
*/
export const switchOrg = ({
domain,
orgCode,
redirectURL,
}: SwitchOrgParams): ReturnType<typeof generateAuthUrl> => {
if (!domain) {
throw new Error("domain is required for switchOrg");
}
if (!orgCode) {
throw new Error("orgCode is required for switchOrg");
}

if (!redirectURL) {
throw new Error("redirectURL is required for switchOrg");
}
const loginOptions: LoginOptions = {
prompt: PromptTypes.none,
orgCode,
redirectURL,
};

return generateAuthUrl(domain, IssuerRouteTypes.login, loginOptions);
};