@appello/services
Version:
Services package with api / graphql
236 lines (235 loc) • 12.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refreshTokens_1 = require("./refreshTokens");
jest.mock('../setAuthorizationHeader', () => ({
setAuthorizationHeader: jest.fn((token, headers) => (Object.assign(Object.assign({}, headers), { Authorization: `Bearer ${token}` }))),
}));
const createMockAxiosError = (config = {}) => {
const error = new Error('Mock axios error');
error.isAxiosError = true;
error.config = Object.assign({ url: '/test', method: 'GET', headers: {} }, config);
return error;
};
const createMockAxiosInstance = () => {
const mockInstance = jest.fn(() => Promise.resolve({ data: 'success' }));
mockInstance.get = jest.fn();
mockInstance.post = jest.fn();
mockInstance.put = jest.fn();
mockInstance.patch = jest.fn();
mockInstance.delete = jest.fn();
mockInstance.request = jest.fn();
mockInstance.defaults = {};
mockInstance.interceptors = {
request: { use: jest.fn(), eject: jest.fn() },
response: { use: jest.fn(), eject: jest.fn() },
};
mockInstance.mockImplementation(() => Promise.resolve({ data: 'success' }));
mockInstance.request.mockImplementation(() => Promise.resolve({ data: 'success' }));
mockInstance.post.mockImplementation(() => Promise.resolve({ data: { accessToken: 'token' } }));
return mockInstance;
};
describe('refreshTokens', () => {
let mockInstance;
let mockConfig;
let mockError;
beforeEach(() => {
mockInstance = createMockAxiosInstance();
mockConfig = {
url: 'https://api.test.com',
getAccessToken: jest.fn().mockResolvedValue('old-access-token'),
getRefreshToken: jest.fn().mockResolvedValue('refresh-token'),
onTokenRefreshSuccess: jest.fn(),
onTokenRefreshError: jest.fn(),
refreshTokenUrl: '/auth/refresh',
};
mockError = createMockAxiosError({
headers: { 'Content-Type': 'application/json' },
});
jest.resetModules();
global.isRefreshing = false;
global.refreshQueues = [];
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Custom refresh token function', () => {
it('should use custom refreshTokens function when provided', async () => {
const customRefreshTokens = jest.fn().mockResolvedValue({
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
});
mockConfig.refreshTokens = customRefreshTokens;
const result = await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig);
expect(customRefreshTokens).toHaveBeenCalledWith(mockInstance, mockError);
expect(mockConfig.onTokenRefreshSuccess).toHaveBeenCalledWith({
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
});
expect(mockInstance).toHaveBeenCalled();
expect(result).toEqual({ data: 'success' });
});
it('should handle custom refresh token function failure', async () => {
const customError = new Error('Custom refresh failed');
mockConfig.refreshTokens = jest.fn().mockRejectedValue(customError);
await expect((0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig)).rejects.toThrow('Custom refresh failed');
expect(mockConfig.onTokenRefreshError).toHaveBeenCalledWith(customError);
expect(mockConfig.onTokenRefreshSuccess).not.toHaveBeenCalled();
});
});
describe('Default refresh token endpoint', () => {
it('should use default refresh endpoint when custom function not provided', async () => {
mockInstance.post.mockResolvedValue({
data: {
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
},
});
const result = await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig);
expect(mockInstance.post).toHaveBeenCalledWith('/auth/refresh', {
refreshToken: 'refresh-token',
});
expect(mockConfig.onTokenRefreshSuccess).toHaveBeenCalledWith({
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
});
expect(result).toEqual({ data: 'success' });
});
it('should handle default refresh endpoint failure', async () => {
const refreshError = new Error('Refresh endpoint failed');
mockInstance.post.mockRejectedValue(refreshError);
await expect((0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig)).rejects.toThrow('Refresh endpoint failed');
expect(mockConfig.onTokenRefreshError).toHaveBeenCalledWith(refreshError);
expect(mockConfig.onTokenRefreshSuccess).not.toHaveBeenCalled();
});
it('should use empty string as default refresh URL', async () => {
delete mockConfig.refreshTokenUrl;
mockInstance.post.mockResolvedValue({
data: { accessToken: 'new-token' },
});
await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig);
expect(mockInstance.post).toHaveBeenCalledWith('', {
refreshToken: 'refresh-token',
});
});
});
describe('Token extraction and header setting', () => {
it('should use getAccessTokenFromRefreshRequest when provided', async () => {
var _a;
const customTokenExtractor = jest.fn().mockReturnValue('extracted-token');
mockConfig.getAccessTokenFromRefreshRequest = customTokenExtractor;
mockConfig.refreshTokens = jest.fn().mockResolvedValue({
customField: 'custom-data',
accessToken: 'default-token',
});
await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig);
expect(customTokenExtractor).toHaveBeenCalledWith({
customField: 'custom-data',
accessToken: 'default-token',
});
const requestCall = mockInstance.mock
.calls[0][0];
expect((_a = requestCall.headers) === null || _a === void 0 ? void 0 : _a.Authorization).toBe('Bearer extracted-token');
});
it('should fallback to data.accessToken when custom extractor not provided', async () => {
var _a;
mockConfig.refreshTokens = jest.fn().mockResolvedValue({
accessToken: 'fallback-token',
otherField: 'other-data',
});
await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig);
const requestCall = mockInstance.mock
.calls[0][0];
expect((_a = requestCall.headers) === null || _a === void 0 ? void 0 : _a.Authorization).toBe('Bearer fallback-token');
});
});
describe('Request configuration handling', () => {
it('should set refreshRetry flag on the request config', async () => {
mockConfig.refreshTokens = jest.fn().mockResolvedValue({
accessToken: 'new-token',
});
await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig);
const requestCall = mockInstance.mock
.calls[0][0];
expect(requestCall.refreshRetry).toBe(true);
});
it('should preserve original request configuration', async () => {
var _a, _b;
const originalConfig = {
url: '/test-endpoint',
method: 'POST',
data: { test: 'data' },
headers: { 'Custom-Header': 'custom-value' },
};
mockError = createMockAxiosError(originalConfig);
mockConfig.refreshTokens = jest.fn().mockResolvedValue({
accessToken: 'new-token',
});
await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig);
const requestCall = mockInstance.mock
.calls[0][0];
expect(requestCall.url).toBe('/test-endpoint');
expect(requestCall.method).toBe('POST');
expect(requestCall.data).toEqual({ test: 'data' });
expect((_a = requestCall.headers) === null || _a === void 0 ? void 0 : _a['Custom-Header']).toBe('custom-value');
expect((_b = requestCall.headers) === null || _b === void 0 ? void 0 : _b.Authorization).toBe('Bearer new-token');
});
});
describe('Callback handling', () => {
it('should call onTokenRefreshSuccess with new tokens', async () => {
const newTokens = {
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
expiresIn: 3600,
};
mockConfig.refreshTokens = jest.fn().mockResolvedValue(newTokens);
await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig);
expect(mockConfig.onTokenRefreshSuccess).toHaveBeenCalledWith(newTokens);
});
it('should call onTokenRefreshError on failure', async () => {
const refreshError = new Error('Token refresh failed');
mockConfig.refreshTokens = jest.fn().mockRejectedValue(refreshError);
await expect((0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig)).rejects.toThrow('Token refresh failed');
expect(mockConfig.onTokenRefreshError).toHaveBeenCalledWith(refreshError);
});
it('should handle missing callback functions gracefully', async () => {
const configWithoutCallbacks = Object.assign({}, mockConfig);
delete configWithoutCallbacks.onTokenRefreshSuccess;
delete configWithoutCallbacks.onTokenRefreshError;
configWithoutCallbacks.refreshTokens = jest.fn().mockResolvedValue({
accessToken: 'new-token',
});
await expect((0, refreshTokens_1.refreshTokens)(mockInstance, mockError, configWithoutCallbacks)).resolves.toBeDefined();
});
});
describe('Generic type handling', () => {
it('should work with custom token types', async () => {
const customConfig = Object.assign(Object.assign({}, mockConfig), { refreshTokens: jest.fn().mockResolvedValue({
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
customField: 'custom-value',
}) });
const result = await (0, refreshTokens_1.refreshTokens)(mockInstance, mockError, customConfig);
expect(customConfig.onTokenRefreshSuccess).toHaveBeenCalledWith({
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
customField: 'custom-value',
});
expect(result).toBeDefined();
});
});
describe('Error propagation', () => {
it('should re-throw refresh errors after handling', async () => {
const originalError = new Error('Original refresh error');
mockConfig.refreshTokens = jest.fn().mockRejectedValue(originalError);
await expect((0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig)).rejects.toBe(originalError);
});
it('should handle errors in the finally block', async () => {
mockConfig.refreshTokens = jest.fn().mockResolvedValue({
accessToken: 'new-token',
});
mockInstance.mockRejectedValue(new Error('Request failed'));
await expect((0, refreshTokens_1.refreshTokens)(mockInstance, mockError, mockConfig)).rejects.toThrow('Request failed');
expect(mockConfig.onTokenRefreshSuccess).toHaveBeenCalled();
});
});
});