@embeddable.com/sdk-core
Version:
Core Embeddable SDK module responsible for web-components bundling and publishing.
228 lines (185 loc) • 6.14 kB
text/typescript
import { server } from "./../../../mocks/server";
import { vi } from "vitest";
import { mkdir, access, writeFile, readFile } from "fs/promises";
import { CREDENTIALS_DIR, CREDENTIALS_FILE } from "./credentials";
import { http, HttpResponse } from "msw";
import { AxiosError } from "axios";
vi.mock("./logger", () => ({
initLogger: vi.fn(),
logError: vi.fn(),
}));
vi.mock("fs/promises", () => ({
readFile: vi.fn(),
writeFile: vi.fn(),
access: vi.fn(),
mkdir: vi.fn(),
}));
const startMock = {
succeed: vi.fn(),
fail: vi.fn(),
};
vi.mock("ora", () => ({
default: () => ({
start: vi.fn().mockImplementation(() => startMock),
info: vi.fn(),
}),
}));
vi.mock("./rollbar.mjs", () => ({
default: vi.fn(),
}));
vi.mock("open", () => ({
default: vi.fn(),
}));
vi.mock("node:fs", () => ({
existsSync: vi.fn(),
}));
// Mock provideConfig directly to avoid file:// URL mocking issues
vi.mock("./provideConfig", () => ({
default: vi.fn().mockResolvedValue({
authDomain: "test-domain.com",
authClientId: "test-client-id",
audienceUrl: "test-audience-url",
}),
}));
// Import after mocks are set up
import login, { resolveFiles, getToken } from "./login";
const makeJwt = (payload: Record<string, unknown>) => {
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }))
.toString("base64url");
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
return `${header}.${body}.signature`;
};
describe("login", () => {
beforeEach(async () => {
vi.resetModules();
// Re-mock provideConfig after resetModules
const provideConfigModule = await import("./provideConfig");
vi.mocked(provideConfigModule.default).mockResolvedValue({
authDomain: "test-domain.com",
authClientId: "test-client-id",
audienceUrl: "test-audience-url",
} as any);
vi.mocked(readFile).mockImplementation(async () =>
Buffer.from(`{"access_token":"mocked-token"}`),
);
vi.mocked(access).mockImplementation(async () => {
throw new Error();
});
vi.mocked(mkdir).mockImplementation(async () => "mocked");
vi.mocked(writeFile).mockImplementation(async () => undefined);
});
vi.mock("./reportErrorToRollbar", () => vi.fn());
vi.mock("./sleep", () => vi.fn());
it("should resolve files", async () => {
await resolveFiles();
expect(access).toHaveBeenCalledWith(CREDENTIALS_DIR);
expect(mkdir).toHaveBeenCalledWith(CREDENTIALS_DIR);
expect(writeFile).toHaveBeenCalledWith(CREDENTIALS_FILE, "");
});
it("should get token", async () => {
const token = await getToken();
expect(token).toBe("mocked-token");
});
it("should return a non-expired JWT access token without refreshing", async () => {
const futureExp = Math.floor(Date.now() / 1000) + 60 * 60;
const jwt = makeJwt({ exp: futureExp });
vi.mocked(readFile).mockResolvedValue(
Buffer.from(
JSON.stringify({ access_token: jwt, refresh_token: "rt-A" }),
),
);
const token = await getToken();
expect(token).toBe(jwt);
expect(writeFile).not.toHaveBeenCalled();
});
it("should refresh and persist a new access token when the cached one is expired", async () => {
const pastExp = Math.floor(Date.now() / 1000) - 60;
const expiredJwt = makeJwt({ exp: pastExp });
vi.mocked(readFile).mockResolvedValue(
Buffer.from(
JSON.stringify({
access_token: expiredJwt,
refresh_token: "rt-A",
}),
),
);
server.use(
http.post("**/oauth/token", () =>
HttpResponse.json({
access_token: "fresh-access-token",
refresh_token: "rt-B",
expires_in: 1800,
}),
),
);
const token = await getToken();
expect(token).toBe("fresh-access-token");
expect(writeFile).toHaveBeenCalledWith(
CREDENTIALS_FILE,
JSON.stringify({
access_token: "fresh-access-token",
refresh_token: "rt-B",
expires_in: 1800,
}),
);
});
it("should return the stale access token when refresh fails", async () => {
const pastExp = Math.floor(Date.now() / 1000) - 60;
const expiredJwt = makeJwt({ exp: pastExp });
vi.mocked(readFile).mockResolvedValue(
Buffer.from(
JSON.stringify({
access_token: expiredJwt,
refresh_token: "rt-A",
}),
),
);
server.use(
http.post("**/oauth/token", () =>
HttpResponse.json(
{ error: "invalid_grant" },
{ status: 403 },
),
),
);
const token = await getToken();
expect(token).toBe(expiredJwt);
expect(writeFile).not.toHaveBeenCalled();
});
it("should return the stale access token when no refresh_token is stored", async () => {
const pastExp = Math.floor(Date.now() / 1000) - 60;
const expiredJwt = makeJwt({ exp: pastExp });
vi.mocked(readFile).mockResolvedValue(
Buffer.from(JSON.stringify({ access_token: expiredJwt })),
);
const token = await getToken();
expect(token).toBe(expiredJwt);
expect(writeFile).not.toHaveBeenCalled();
});
it("should login by saving token in the credentials file", async () => {
await login();
expect(startMock.succeed).toHaveBeenCalledWith(
"You are successfully authenticated now!",
);
expect(writeFile).toHaveBeenCalledWith(
CREDENTIALS_FILE,
JSON.stringify({ access_token: "mocked-token " }),
);
});
it("should fail to login when the response returns 500 error", async () => {
vi.spyOn(console, "log").mockImplementation(() => undefined);
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
server.use(
http.post("**/oauth/device/code", () => {
return new HttpResponse(null, { status: 500 });
}),
);
await login();
expect(startMock.fail).toHaveBeenCalledWith(
"Authentication failed. Please try again.",
);
expect((console.log as any).mock.calls[0][0]).toMatchInlineSnapshot(
`[AxiosError: Request failed with status code 500]`,
);
});
});