Files
calendar/packages/app-store/_utils/oauth/OAuthManager.test.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f5d345b133 refactor: remove @calcom/web imports from @calcom/features and add @calcom/testing package (#26480)
* fix: remove @calcom/web imports from packages/features to eliminate circular dependency

- Migrate UserTableUser and MemberPermissions types to packages/features/users/types/user-table.ts
- Migrate useGeo hook to packages/features/geo/GeoContext.tsx
- Migrate buildLegacyRequest to packages/lib/buildLegacyCtx.ts
- Migrate Calendar component to packages/features/calendars/weeklyview/components/
- Move test utilities (bookingScenario, fixtures) to packages/features/test/
- Update all imports in packages/features to use new locations
- Add re-exports in apps/web for backward compatibility

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: delete original implementation files and fix type issues

- Delete original calendar component files in apps/web (keep only re-export stubs)
- Migrate OutOfOfficeInSlots to packages/features/bookings/components
- Convert apps/web OutOfOfficeInSlots to re-export stub
- Fix className vs class issue in Calendar.tsx
- Fix @calcom/trpc import violation in user-table.ts by using structural type

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing isGroup and contains fields to UserTableUser attributes type

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update customRole type to match actual Prisma Role model

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix build

* fix

* fix

* cleanup weeklyview

* fix

* refactor to mv MemberPermissions to types package

* add types dependency to features

* fix

* fix

* fix

* fix

* fix

* fix

* rename

* rename

* migrate

* migrate

* migrate

* fix

* fix

* fix

* refactor: move test utilities from packages/features/test to tests/libs

- Move bookingScenario utilities to tests/libs/bookingScenario
- Move fixtures to tests/libs/fixtures
- Update all imports in packages/features test files to use new location
- Update all imports in apps/web test files to use new location
- Eliminates duplication of test utilities between packages/features and apps/web

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: correct relative import paths for tests/libs in test files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: replace test utility implementations with re-exports to tests/libs

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: fix test import paths and move signup handler tests to apps/web

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: move buildLegacyCtx to packages/lib and restore handlers to packages/features

- Move buildLegacyCtx from apps/web/lib to packages/lib to break circular dependency
- Update apps/web/lib/buildLegacyCtx.ts to re-export from @calcom/lib
- Restore signup handlers and tests to packages/features/auth/signup/handlers
- Update handler imports to use @calcom/lib/buildLegacyCtx instead of @calcom/web/lib/buildLegacyCtx

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update recurring-event.test.ts imports to use tests/libs path

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: delete test re-export files and update imports to use tests/libs directly

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update remaining test imports to use tests/libs directly

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update handleRecurringEventBooking calls to match function signature (1 arg)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* remove

* migrate tests

* migrate tests

* refactor: update test mock imports by removing  and using async  for mock creators.

* fix type errors

* fix: add type assertion for MockUser in p2002.test-suite.ts

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: restore locale import in compareReminderBodyToTemplate.test.ts using relative path

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* feat: create @calcom/testing package and migrate tests from /tests directory

- Created new @calcom/testing package in /packages/testing
- Moved all files from /tests to /packages/testing
- Updated all imports across the codebase to use @calcom/testing alias
- Removed /tests directory at root level

This allows other packages like @calcom/features and @calcom/web to import
testing utilities using the @calcom/testing alias instead of relative paths.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* fix

* fix: add missing useBookings export to @calcom/atoms package

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add missing useCalendarsBusyTimes and useConnectedCalendars exports to @calcom/atoms

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* chore: add @calcom/testing as explicit devDependency to packages that use it

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move setupVitest.ts into @calcom/testing package

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* chore: add biome rules to restrict @calcom/testing imports

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* rename libs to lib

* rename libs to lib

* add rule

* add rule

* refactor: remove @calcom/features imports from @calcom/testing

- Move mockPaymentSuccessWebhookFromStripe to fresh-booking.test.ts
- Replace ProfileRepository.generateProfileUid() with uuidv4()
- Clone Tracking type into @calcom/testing/src/lib/types.ts
- Update imports in expects.ts and getMockRequestDataForBooking.ts
- Move source files into src/ folder
- Move CalendarManager mock to @calcom/features

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: add explicit exports for nested paths in @calcom/testing

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* improve

* improve

* fix

* fix

* fix type checks

* fix type checks

* fix type checks

* fix tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-08 16:59:11 +09:00

1552 lines
54 KiB
TypeScript

// import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock";
import { afterEach, expect, test, vi, describe } from "vitest";
import "vitest-fetch-mock";
import {
generateJsonResponse,
successResponse,
internalServerErrorResponse,
generateTextResponse,
} from "../testUtils";
import { OAuthManager, TokenStatus } from "./OAuthManager";
afterEach(() => {
vi.resetAllMocks();
});
const credentialSyncVariables = {
APP_CREDENTIAL_SHARING_ENABLED: false,
CREDENTIAL_SYNC_SECRET: "SECRET",
CREDENTIAL_SYNC_SECRET_HEADER_NAME: "calcom-credential-sync-secret",
CREDENTIAL_SYNC_ENDPOINT: "https://example.com/getToken",
};
function getDummyTokenObject(
token: { refresh_token?: string; expiry_date?: number; expires_in?: number } | null = null
) {
return {
access_token: "ACCESS_TOKEN",
...token,
};
}
function getExpiredTokenObject() {
return getDummyTokenObject({
// To make sure that existing token is used and thus refresh token doesn't happen
expiry_date: Date.now() - 10 * 1000,
});
}
describe("Credential Sync Disabled", () => {
const useCredentialSyncVariables = credentialSyncVariables;
describe("API: `getTokenObjectOrFetch`", () => {
describe("`fetchNewTokenObject` gets called with refresh_token arg", async () => {
test('refresh_token argument would be null if "refresh_token" is not present in the currentTokenObject', async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(successResponse({ json: getDummyTokenObject() }));
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth.getTokenObjectOrFetch();
expect(fetchNewTokenObject).toHaveBeenCalledWith({ refreshToken: null });
});
test('refresh_token would be the value if "refresh_token" is present in the currentTokenObject', async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).toHaveBeenCalledWith({ refreshToken: "REFRESH_TOKEN" });
});
});
describe("expiry_date based token refresh", () => {
describe("checking using expiry_date", () => {
test("fetchNewTokenObject is not called if token has not expired", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expiry_date: Date.now() + 10 * 1000,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).not.toHaveBeenCalled();
expect(updateTokenObject).not.toHaveBeenCalled();
});
test("`fetchNewTokenObject` is called if token has expired. Also, `updateTokenObject` is called with currentTokenObject and newTokenObject merged", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const currentTokenObject = getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expiry_date: Date.now() - 10 * 1000,
});
const newTokenObjectInResponse = getDummyTokenObject();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: newTokenObjectInResponse }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: currentTokenObject,
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).toHaveBeenCalledWith({ refreshToken: "REFRESH_TOKEN" });
expect(updateTokenObject).toHaveBeenCalledWith({
...currentTokenObject,
...newTokenObjectInResponse,
// Consider the token as expired as newTokenObjectInResponse didn't have expiry
expiry_date: 0,
});
});
test("`fetchNewTokenObject` is called if token is about to expire. Also, `updateTokenObject` is called with currentTokenObject and newTokenObject merged", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const currentTokenObject = getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expiry_date: Date.now() - 2 * 1000,
});
const newTokenObjectInResponse = getDummyTokenObject();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: newTokenObjectInResponse }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: currentTokenObject,
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).toHaveBeenCalledWith({ refreshToken: "REFRESH_TOKEN" });
expect(updateTokenObject).toHaveBeenCalledWith({
...currentTokenObject,
...newTokenObjectInResponse,
// Consider the token as expired as newTokenObjectInResponse didn't have expiry
expiry_date: 0,
});
});
});
describe("checking using expires_in", () => {
// eslint-disable-next-line playwright/max-nested-describe
describe("expires_in(relative to current time)", () => {
test("fetchNewTokenObject is called if expires_in is 0", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expires_in: 0,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).toHaveBeenCalledWith({ refreshToken: "REFRESH_TOKEN" });
});
test("`fetchNewTokenObject` is not called even if expires_in is any non zero positive value(that is not 'time since epoch')", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expires_in: 5,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).toHaveBeenCalledWith({ refreshToken: "REFRESH_TOKEN" });
});
});
// eslint-disable-next-line playwright/max-nested-describe
describe("expires_in(relative to epoch time)", () => {
test("fetchNewTokenObject is not called if token has not expired", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expires_in: Date.now() / 1000 + 10,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).not.toHaveBeenCalledWith({ refreshToken: "REFRESH_TOKEN" });
});
test("fetchNewTokenObject is called if token has expired", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expires_in: Date.now() / 1000 + 0,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).toHaveBeenCalledWith({ refreshToken: "REFRESH_TOKEN" });
});
});
});
});
test("If fetchNewTokenObject returns null then auth.getTokenObjectOrFetch would throw error", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject: async () => {
return null;
},
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
expect(async () => {
return auth.getTokenObjectOrFetch();
}).rejects.toThrowError("could not refresh the token");
});
test("if fetchNewTokenObject throws error that's not handled by isTokenObjectUnusable and isAccessTokenUnusable then auth.getTokenObjectOrFetch would still not throw error", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject: async () => {
throw new Error("fetchNewTokenObject error");
},
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
expect(async () => {
return auth.getTokenObjectOrFetch();
}).rejects.toThrowError("fetchNewTokenObject error");
});
test("if fetchNewTokenObject throws error that's handled by isTokenObjectUnusable then auth.getTokenObjectOrFetch would still throw error but a different one as access_token won't be available", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject: async () => {
throw new Error("fetchNewTokenObject error");
},
isTokenObjectUnusable: async () => {
return {
reason: "some reason",
};
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
expect(async () => {
return auth.getTokenObjectOrFetch();
}).rejects.toThrowError("fetchNewTokenObject error");
});
test("when currentTokenObject is not set, but getCurrentTokenObject is set", async () => {
const userId = 1;
const getCurrentTokenObject = vi.fn().mockResolvedValue(getDummyTokenObject());
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
isTokenExpiring: async () => {
return false;
},
isAccessTokenUnusable: async () => {
return null;
},
getCurrentTokenObject: getCurrentTokenObject,
fetchNewTokenObject: async () => {
return generateJsonResponse({ json: getDummyTokenObject() });
},
updateTokenObject: vi.fn(),
invalidateTokenObject: vi.fn(),
expireAccessToken: vi.fn(),
isTokenObjectUnusable: async () => {
return null;
},
});
const tokenObject = await auth.getTokenObjectOrFetch();
expect(getCurrentTokenObject).toHaveBeenCalled();
expect(tokenObject).toEqual({
token: expect.objectContaining(getDummyTokenObject()),
isUpdated: false,
});
});
});
describe("API: `request`", () => {
test("It would call fetch by adding Authorization and content header automatically", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
fetchMock.mockReturnValueOnce(Promise.resolve(generateJsonResponse({ json: { key: "value" } })));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({ tokenStatus: TokenStatus.VALID, json: { key: "value" } });
const fetchCallArguments = fetchMock.mock.calls[0];
expect(fetchCallArguments[0]).toBe("https://example.com");
// Verify that Authorization header is added automatically
// Along with other passed headers and other options
expect(fetchCallArguments[1]).toEqual(
expect.objectContaining({
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer ACCESS_TOKEN",
},
body: JSON.stringify({
key: "value",
}),
})
);
});
test("If `isTokenObjectUnusable` marks the response invalid, then `invalidateTokenObject` function is called", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = generateJsonResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
autoCheckTokenExpiryOnRequest: false,
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async (response) => {
const jsonRes = await response.json();
expect(jsonRes).toEqual(fakedFetchJsonResult);
return {
reason: "some reason",
};
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({
tokenStatus: TokenStatus.UNUSABLE_TOKEN_OBJECT,
json: fakedFetchJsonResult,
});
expect(invalidateTokenObject).toHaveBeenCalled();
expect(expireAccessToken).not.toHaveBeenCalled();
});
test("If `isAccessTokenUnusable` marks the response invalid, then `expireAccessToken` function is called", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = generateJsonResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
autoCheckTokenExpiryOnRequest: false,
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async (response) => {
const jsonRes = await response.json();
expect(jsonRes).toEqual(fakedFetchJsonResult);
return {
reason: "some reason",
};
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({
tokenStatus: TokenStatus.UNUSABLE_ACCESS_TOKEN,
json: fakedFetchJsonResult,
});
expect(invalidateTokenObject).not.toHaveBeenCalled();
expect(expireAccessToken).toHaveBeenCalled();
});
test("If the response is empty string make the json null(because empty string which is usually the case with 204 status is not a valid json). There shouldn't be any error even if `isTokenObjectUnusable` and `isAccessTokenUnusable` do json()", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const fakedFetchResponse = generateTextResponse({ text: "", status: 204 });
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async (response) => {
return await response.json();
},
isAccessTokenUnusable: async (response) => {
return await response.json();
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({ tokenStatus: TokenStatus.VALID, json: null });
expect(expireAccessToken).not.toHaveBeenCalled();
});
test("If status is not okay it throws error with statusText", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = generateJsonResponse({
json: fakedFetchJsonResult,
status: 500,
statusText: "Internal Server Error",
});
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const { json, tokenStatus } = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(json).toEqual(fakedFetchJsonResult);
expect(tokenStatus).toEqual(TokenStatus.INCONCLUSIVE);
});
test("if `customFetch` throws error that is handled by `isTokenObjectUnusable` then `request` would still throw error but also invalidate", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return {
reason: "some reason",
};
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await expect(
auth.request(() => {
throw new Error("Internal Server Error");
})
).rejects.toThrowError("Internal Server Error");
expect(invalidateTokenObject).toHaveBeenCalled();
});
});
describe("API: `requestRaw`", () => {
test("It would call fetch by adding Authorization and content header automatically", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
fetchMock.mockReturnValueOnce(Promise.resolve(generateJsonResponse({ json: { key: "value" } })));
const response = await auth.requestRaw({
url: "https://example.com",
options: {
method: "POST",
body: JSON.stringify({
key: "value",
}),
},
});
expect(await response.json()).toEqual({ key: "value" });
const fetchCallArguments = fetchMock.mock.calls[0];
expect(fetchCallArguments[0]).toBe("https://example.com");
// Verify that Authorization header is added automatically
// Along with other passed headers and other options
expect(fetchCallArguments[1]).toEqual(
expect.objectContaining({
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer ACCESS_TOKEN",
},
body: JSON.stringify({
key: "value",
}),
})
);
});
});
// Credentials might have no userId attached in production. They could instead have teamId
test("OAuthManager without resourceOwner.id is okay", async () => {
const userId = null;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn().mockResolvedValue(successResponse({ json: getDummyTokenObject() }));
expect(
() =>
new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
})
).not.toThrowError();
});
});
describe("Credential Sync Enabled", () => {
const useCredentialSyncVariables = {
...credentialSyncVariables,
APP_CREDENTIAL_SHARING_ENABLED: true,
};
describe("API: `getTokenObjectOrFetch`", () => {
test("CREDENTIAL_SYNC_ENDPOINT is hit if no expiry_date is set in the `currentTokenObject`", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
const fakedFetchResponse = generateJsonResponse({ json: getDummyTokenObject() });
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
await auth1.getTokenObjectOrFetch();
expectToBeTokenGetCall({
fetchCall: fetchMock.mock.calls[0],
useCredentialSyncVariables,
userId,
appSlug: "demo-app",
});
expect(fetchNewTokenObject).not.toHaveBeenCalled();
});
describe("expiry_date based token refresh", () => {
test("CREDENTIAL_SYNC_ENDPOINT is not hit if token has not expired", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expiry_date: Date.now() + 10 * 1000,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
await auth1.getTokenObjectOrFetch();
expect(fetchNewTokenObject).not.toHaveBeenCalled();
expect(fetchMock).not.toHaveBeenCalled();
});
test("CREDENTIAL_SYNC_ENDPOINT is hit if token has expired", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi
.fn()
.mockResolvedValue(generateJsonResponse({ json: getDummyTokenObject() }));
const auth1 = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
refresh_token: "REFRESH_TOKEN",
expiry_date: Date.now() - 10 * 1000,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
const fakedFetchResponse = generateJsonResponse({ json: getDummyTokenObject() });
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
await auth1.getTokenObjectOrFetch();
expectToBeTokenGetCall({
fetchCall: fetchMock.mock.calls[0],
useCredentialSyncVariables,
userId,
appSlug: "demo-app",
});
expect(fetchNewTokenObject).not.toHaveBeenCalled();
});
});
test("OAuthManager without resourceOwner.id should throw error as it is a requirement", async () => {
const userId = null;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn().mockResolvedValue(successResponse({ json: getDummyTokenObject() }));
expect(
() =>
new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
})
).toThrowError("resourceOwner should have id set");
});
});
describe("API: `request`", () => {
test("If `isTokenObjectUnusable` marks the response invalid, then `invalidateTokenObject` function is called", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn();
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = generateJsonResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
// To make sure that existing token is used and thus refresh token doesn't happen
expiry_date: Date.now() + 10 * 1000,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async (response) => {
const jsonRes = await response.json();
expect(jsonRes).toEqual(fakedFetchJsonResult);
return {
reason: "some reason",
};
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
// For fetch triggered by the actual request
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({
tokenStatus: TokenStatus.UNUSABLE_TOKEN_OBJECT,
json: fakedFetchJsonResult,
});
expect(invalidateTokenObject).not.toHaveBeenCalled();
expect(expireAccessToken).toHaveBeenCalled();
});
test("If neither of `isTokenObjectUnusable` and `isAccessTokenInvalid` mark the response invalid, but the response is still not OK then `markTokenExpired` is still called.", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn();
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = internalServerErrorResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
// To make sure that existing token is used and thus refresh token doesn't happen
expiry_date: Date.now() + 10 * 1000,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
// For fetch triggered by the actual request
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({
tokenStatus: TokenStatus.INCONCLUSIVE,
json: fakedFetchJsonResult,
});
expect(invalidateTokenObject).not.toHaveBeenCalled();
expect(expireAccessToken).toHaveBeenCalled();
});
test("If neither of `isTokenObjectUnusable` and `isAccessTokenInvalid` mark the response invalid, and the response is also OK then `markTokenExpired` is not called.", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn();
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = successResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
// To make sure that existing token is used and thus refresh token doesn't happen
expiry_date: Date.now() + 10 * 1000,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
// For fetch triggered by the actual request
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({
tokenStatus: TokenStatus.VALID,
json: fakedFetchJsonResult,
});
expect(invalidateTokenObject).not.toHaveBeenCalled();
expect(expireAccessToken).not.toHaveBeenCalled();
});
test("If `autoCheckTokenExpiryOnRequest` is true and token is expired, then token sync endpoint is hit", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn();
const currentTokenObject = getExpiredTokenObject();
const newTokenObjectInResponse = getDummyTokenObject();
const fakedTokenGetResponse = generateJsonResponse({ json: newTokenObjectInResponse });
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = successResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
autoCheckTokenExpiryOnRequest: true,
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject,
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
// For fetch triggered by the token sync request
fetchMock.mockReturnValueOnce(Promise.resolve(fakedTokenGetResponse));
// For fetch triggered by the request call fetch
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({
tokenStatus: TokenStatus.VALID,
json: fakedFetchJsonResult,
});
expect(updateTokenObject).toHaveBeenCalledWith(expect.objectContaining(newTokenObjectInResponse));
// In credential sync mode, the expiry date is set to next year as it is not explicitly set in newTokenObject
expectExpiryToBeNextYear(updateTokenObject.mock.calls[0][0].expiry_date);
expect(invalidateTokenObject).not.toHaveBeenCalled();
expect(expireAccessToken).not.toHaveBeenCalled();
});
test("If `autoCheckTokenExpiryOnRequest` is not set(default true is used) and token is expired, then token sync endpoint is hit", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn();
const fakedTokenGetJson = getDummyTokenObject();
const fakedTokenGetResponse = generateJsonResponse({ json: fakedTokenGetJson });
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = successResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getExpiredTokenObject(),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
// For fetch triggered by the token sync request
fetchMock.mockReturnValueOnce(Promise.resolve(fakedTokenGetResponse));
// For fetch triggered by the request call fetch
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.request({
url: "https://example.com",
options: {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "value",
}),
},
});
expect(response).toEqual({
tokenStatus: TokenStatus.VALID,
json: fakedFetchJsonResult,
});
expect(updateTokenObject).toHaveBeenCalled();
expect(invalidateTokenObject).not.toHaveBeenCalled();
expect(expireAccessToken).not.toHaveBeenCalled();
});
});
describe("API: `requestRaw`", () => {
test("Though `isTokenObjectUnusable` and `isAccessTokenInvalid` aren't applicable here, but if the response is not OK then `markTokenExpired` is still called.", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn();
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = internalServerErrorResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
autoCheckTokenExpiryOnRequest: false,
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
// To make sure that existing token is used and thus refresh token doesn't happen
expiry_date: Date.now() + 10 * 1000,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
// For fetch triggered by the actual request
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.requestRaw({
url: "https://example.com",
options: {
method: "POST",
body: JSON.stringify({
key: "value",
}),
},
});
expect(await response.json()).toEqual(fakedFetchJsonResult);
expect(invalidateTokenObject).not.toHaveBeenCalled();
expect(expireAccessToken).toHaveBeenCalled();
});
test("Though `isTokenObjectUnusable` and `isAccessTokenInvalid` aren't applicable here, and the response is also OK then `markTokenExpired` is not called.", async () => {
const userId = 1;
const invalidateTokenObject = vi.fn();
const expireAccessToken = vi.fn();
const updateTokenObject = vi.fn();
const fetchNewTokenObject = vi.fn();
const fakedFetchJsonResult = { key: "value" };
const fakedFetchResponse = successResponse({ json: fakedFetchJsonResult });
const auth = new OAuthManager({
autoCheckTokenExpiryOnRequest: false,
credentialSyncVariables: useCredentialSyncVariables,
resourceOwner: {
type: "user",
id: userId,
},
appSlug: "demo-app",
currentTokenObject: getDummyTokenObject({
// To make sure that existing token is used and thus refresh token doesn't happen
expiry_date: Date.now() + 10 * 1000,
}),
fetchNewTokenObject,
isTokenObjectUnusable: async () => {
return null;
},
isAccessTokenUnusable: async () => {
return null;
},
invalidateTokenObject: invalidateTokenObject,
updateTokenObject: updateTokenObject,
expireAccessToken: expireAccessToken,
});
// For fetch triggered by the actual request
fetchMock.mockReturnValueOnce(Promise.resolve(fakedFetchResponse));
const response = await auth.requestRaw({
url: "https://example.com",
options: {
method: "POST",
body: JSON.stringify({
key: "value",
}),
},
});
expect(await response.json()).toEqual(fakedFetchJsonResult);
expect(invalidateTokenObject).not.toHaveBeenCalled();
expect(expireAccessToken).not.toHaveBeenCalled();
});
});
});
function expectExpiryToBeNextYear(expiry_date: number) {
expect(new Date(expiry_date).getFullYear() - new Date().getFullYear()).toBe(1);
}
function expectToBeTokenGetCall({
fetchCall,
useCredentialSyncVariables,
userId,
appSlug,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetchCall: any[];
useCredentialSyncVariables: {
APP_CREDENTIAL_SHARING_ENABLED: boolean;
CREDENTIAL_SYNC_SECRET: string;
CREDENTIAL_SYNC_SECRET_HEADER_NAME: string;
CREDENTIAL_SYNC_ENDPOINT: string;
};
userId: number;
appSlug: string;
}) {
expect(fetchCall[0]).toBe("https://example.com/getToken");
expect(fetchCall[1]).toEqual(
expect.objectContaining({
method: "POST",
headers: {
[useCredentialSyncVariables.CREDENTIAL_SYNC_SECRET_HEADER_NAME]:
useCredentialSyncVariables.CREDENTIAL_SYNC_SECRET,
},
})
);
const fetchBody = fetchCall[1]?.body as unknown as URLSearchParams;
expect(fetchBody.get("calcomUserId")).toBe(userId.toString());
expect(fetchBody.get("appSlug")).toBe(appSlug);
}