Skip to content

Commit

Permalink
test: fix unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielRivers committed Nov 19, 2024
1 parent c2c875f commit fe8e4c0
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 100 deletions.
4 changes: 4 additions & 0 deletions lib/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ describe("index exports", () => {
"getActiveStorage",
"hasActiveStorage",
"clearActiveStorage",
"clearInsecureStorage",
"getInsecureStorage",
"hasInsecureStorage",
"setInsecureStorage",
"getClaim",
"getClaims",
"getCurrentOrganization",
Expand Down
12 changes: 12 additions & 0 deletions lib/utils/base64UrlEncode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,16 @@ describe("base64UrlEncode", () => {
const result = base64UrlEncode(input);
expect(result).toBe(expectedOutput);
});

it("should encode when passed an ArrayBuffer", () => {
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
for (let i = 0; i < view.length; i++) {
view[i] = i + 1;
}

const expectedOutput = "AQIDBAUGBwg";
const result = base64UrlEncode(buffer);
expect(result).toBe(expectedOutput);
});
});
24 changes: 11 additions & 13 deletions lib/utils/base64UrlEncode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@
* @param str String to encode
* @returns encoded string
*/
export const base64UrlEncode = (str: string | ArrayBuffer): string => {
if (str instanceof ArrayBuffer) {
const numberArray = Array.from<number>(new Uint8Array(str));
return btoa(String.fromCharCode.apply(null, numberArray))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
export const base64UrlEncode = (input: string | ArrayBuffer): string => {
const toBase64Url = (str: string): string =>
btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

if (input instanceof ArrayBuffer) {
const uint8Array = new Uint8Array(input);
const binaryString = String.fromCharCode(...uint8Array);
return toBase64Url(binaryString);
}

const encoder = new TextEncoder();
const uintArray = encoder.encode(str);
const charArray = Array.from(uintArray);
return btoa(String.fromCharCode.apply(null, charArray))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const uint8Array = encoder.encode(input);
const binaryString = String.fromCharCode(...uint8Array);
return toBase64Url(binaryString);
};
70 changes: 63 additions & 7 deletions lib/utils/exchangeAuthCode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ import { MemoryStorage, StorageKeys } from "../sessionManager";
import { setActiveStorage } from "./token";
import createFetchMock from "vitest-fetch-mock";
import { frameworkSettings } from "./exchangeAuthCode";
import * as refreshTokenTimer from "./refreshTimer";
import * as main from "../main";

const fetchMock = createFetchMock(vi);

describe("exchangeAuthCode", () => {
beforeEach(() => {
fetchMock.enableMocks();
vi.spyOn(refreshTokenTimer, "setRefreshTimer");
vi.spyOn(main, "refreshToken");
vi.useFakeTimers();
});

afterEach(() => {
fetchMock.resetMocks();
vi.useRealTimers();
});

it("missing state param", async () => {
Expand Down Expand Up @@ -142,10 +148,14 @@ describe("exchangeAuthCode", () => {
expect(url).toBe("http://test.kinde.com/oauth2/token");
expect(options).toMatchObject({
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
});
expect((options?.headers as Headers).get("Content-type")).toEqual(
"application/x-www-form-urlencoded; charset=UTF-8",
);
expect((options?.headers as Headers).get("Cache-Control")).toEqual(
"no-store",
);
expect((options?.headers as Headers).get("Pragma")).toEqual("no-cache");
});

it("set the framework and version on header", async () => {
Expand Down Expand Up @@ -173,6 +183,7 @@ describe("exchangeAuthCode", () => {
access_token: "access_token",
refresh_token: "refresh_token",
id_token: "id_token",
expires_in: 3600,
}),
);

Expand All @@ -188,11 +199,10 @@ describe("exchangeAuthCode", () => {
expect(url).toBe("http://test.kinde.com/oauth2/token");
expect(options).toMatchObject({
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"Kinde-SDK": "Framework/Version",
},
});
expect((options?.headers as Headers).get("Kinde-SDK")).toEqual(
"Framework/Version",
);
});

it("should handle token exchange failure", async () => {
Expand Down Expand Up @@ -226,4 +236,50 @@ describe("exchangeAuthCode", () => {
error: "Token exchange failed: 500 - error",
});
});

it("should set the refresh timer", async () => {
const store = new MemoryStorage();
setActiveStorage(store);

const state = "state";

await store.setItems({
[StorageKeys.state]: state,
});

frameworkSettings.framework = "Framework";
frameworkSettings.frameworkVersion = "Version";

const input = "hello";

const urlParams = new URLSearchParams();
urlParams.append("code", input);
urlParams.append("state", state);
urlParams.append("client_id", "test");

fetchMock.mockResponseOnce(
JSON.stringify({
access_token: "access_token",
refresh_token: "refresh_token",
id_token: "id_token",
expires_in: 3600,
}),
);

await exchangeAuthCode({
urlParams,
domain: "http://test.kinde.com",
clientId: "test",
redirectURL: "http://test.kinde.com",
autoReferesh: true,
});

expect(refreshTokenTimer.setRefreshTimer).toHaveBeenCalledOnce();
expect(refreshTokenTimer.setRefreshTimer).toHaveBeenCalledWith(
3600,
expect.any(Function),
);
vi.advanceTimersByTime(3600 * 1000);
expect(main.refreshToken).toHaveBeenCalledTimes(1);
});
});
5 changes: 2 additions & 3 deletions lib/utils/exchangeAuthCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export const exchangeAuthCode = async ({
StorageKeys.codeVerifier,
)) as string;


const headers: {
"Content-type": string;
"Cache-Control": string;
Expand Down Expand Up @@ -115,7 +114,7 @@ export const exchangeAuthCode = async ({
error: `Token exchange failed: ${response.status} - ${errorText}`,
};
}
clearRefreshTimer()
clearRefreshTimer();

const data: {
access_token: string;
Expand All @@ -132,7 +131,7 @@ export const exchangeAuthCode = async ({
});

if (autoReferesh) {
setRefreshTimer(data.expires_in * 1000, async () => {
setRefreshTimer(data.expires_in, async () => {
refreshToken(domain, clientId);
});
}
Expand Down
6 changes: 3 additions & 3 deletions lib/utils/generateAuthUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("generateAuthUrl", () => {
expect(nonce!.length).toBe(16);
result.url.searchParams.delete("nonce");
const codeChallenge = result.url.searchParams.get("code_challenge");
expect(codeChallenge!.length).toBeGreaterThan(43);
expect(codeChallenge!.length).toBeGreaterThanOrEqual(27);
result.url.searchParams.delete("code_challenge");
expect(result.url.toString()).toBe(expectedUrl);
});
Expand Down Expand Up @@ -88,7 +88,7 @@ describe("generateAuthUrl", () => {
result.url.searchParams.delete("nonce");

const codeChallenge = result.url.searchParams.get("code_challenge");
expect(codeChallenge!.length).toBeGreaterThan(43);
expect(codeChallenge!.length).toBeGreaterThanOrEqual(27);
result.url.searchParams.delete("code_challenge");

expect(result.url.toString()).toBe(expectedUrl);
Expand Down Expand Up @@ -117,7 +117,7 @@ describe("generateAuthUrl", () => {
expect(state).not.toBeNull();
expect(state!.length).toBe(32);
const codeChallenge = result.url.searchParams.get("code_challenge");
expect(codeChallenge!.length).toBeGreaterThan(43);
expect(codeChallenge!.length).toBeGreaterThanOrEqual(27);
result.url.searchParams.delete("code_challenge");
result.url.searchParams.delete("nonce");
result.url.searchParams.delete("state");
Expand Down
2 changes: 0 additions & 2 deletions lib/utils/generateAuthUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ export async function generatePKCEPair(): Promise<{
const codeVerifier = generateRandomString(52);
const data = new TextEncoder().encode(codeVerifier);
const hashed = await crypto.subtle.digest("SHA-256", data);
// const hashArray = Array.from(new Uint8Array(hashed));
// const hashString = hashArray.map((b) => String.fromCharCode(b)).join("");
const codeChallenge = base64UrlEncode(hashed);
return { codeVerifier, codeChallenge };
}
4 changes: 2 additions & 2 deletions lib/utils/refreshTimer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
export let refreshTimer: number | undefined;

export function setRefreshTimer(timer: number, callback: () => void) {
window.setTimeout(callback, timer);
refreshTimer = window.setTimeout(callback, timer * 1000 - 10000);
}

export function clearRefreshTimer() {
if (refreshTimer !== undefined) {
window.clearTimeout(refreshTimer);
refreshTimer = undefined;
}
}
}
2 changes: 1 addition & 1 deletion lib/utils/token/getDecodedToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const getDecodedToken = async <
const decodedToken = jwtDecoder<T>(token);

if (!decodedToken) {
console.log("No decoded token found");
console.warn("No decoded token found");
}

return decodedToken;
Expand Down
40 changes: 39 additions & 1 deletion lib/utils/token/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, beforeEach } from "vitest";
import { MemoryStorage } from "../../sessionManager";
import {
getActiveStorage,
hasActiveStorage,
setActiveStorage,
clearActiveStorage,
setInsecureStorage,
hasInsecureStorage,
clearInsecureStorage,
getInsecureStorage,
} from ".";

describe("token index", () => {
beforeEach(() => {
clearActiveStorage();
clearInsecureStorage();
});

it("hasActiveStorage returns true when storage is set", async () => {
const storage = new MemoryStorage();
setActiveStorage(storage);
Expand All @@ -28,4 +38,32 @@ describe("token index", () => {
setActiveStorage(storage);
expect(getActiveStorage()).toBe(storage);
});

it("hasInsecureStorage returns true when insecure storage is set", async () => {
const storage = new MemoryStorage();
setInsecureStorage(storage);
expect(hasInsecureStorage()).toStrictEqual(true);
});

it("hasInsecureStorage returns false when insecure storage is cleared", async () => {
clearInsecureStorage();
expect(hasInsecureStorage()).toStrictEqual(false);
});

it("getInsecureStorage returns null when no insecure storage is set", async () => {
clearInsecureStorage();
clearActiveStorage();
expect(getInsecureStorage()).toBeNull();
});

it("getInsecureStorage returns active storage when no insecure storage is set", async () => {
clearInsecureStorage();
expect(getInsecureStorage()).toBeNull();
});

it("getInsecureStorage returns storage instance when set", async () => {
const storage = new MemoryStorage();
setInsecureStorage(storage);
expect(getInsecureStorage()).toBe(storage);
});
});
8 changes: 4 additions & 4 deletions lib/utils/token/isAuthenticated.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("isAuthenticated", () => {
});
const mockRefreshToken = vi
.spyOn(tokenUtils, "refreshToken")
.mockResolvedValue(true);
.mockResolvedValue({ success: true });

const result = await isAuthenticated({
useRefreshToken: true,
Expand All @@ -67,7 +67,7 @@ describe("isAuthenticated", () => {
vi.spyOn(tokenUtils, "getDecodedToken").mockResolvedValue({
exp: mockCurrentTime - 3600,
});
vi.spyOn(tokenUtils, "refreshToken").mockResolvedValue(false);
vi.spyOn(tokenUtils, "refreshToken").mockResolvedValue({ success: false });

const result = await isAuthenticated({
useRefreshToken: true,
Expand Down Expand Up @@ -96,9 +96,9 @@ describe("isAuthenticated", () => {
vi.spyOn(tokenUtils, "getDecodedToken").mockResolvedValue({
// Missing 'exp' field
});

const result = await isAuthenticated();

expect(result).toBe(false);
});
});
3 changes: 2 additions & 1 deletion lib/utils/token/isAuthenticated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const isAuthenticated = async (
const isExpired = token.exp < Math.floor(Date.now() / 1000);

if (isExpired && props?.useRefreshToken) {
return refreshToken(props.domain, props.clientId);
const refreshResult = await refreshToken(props.domain, props.clientId);
return refreshResult.success;
}
return !isExpired;
} catch (error) {
Expand Down
Loading

0 comments on commit fe8e4c0

Please sign in to comment.