Add tests for OAuthProvider class
This commit is contained in:
@@ -24,8 +24,9 @@ import {
|
||||
setNonceCookie,
|
||||
TEN_MINUTES_MS,
|
||||
verifyNonce,
|
||||
OAuthProvider,
|
||||
} from './OAuthProvider';
|
||||
import { AuthResponse } from './types';
|
||||
import { AuthResponse, OAuthProviderHandlers } from './types';
|
||||
|
||||
describe('OAuthProvider Utils', () => {
|
||||
describe('verifyNonce', () => {
|
||||
@@ -196,3 +197,168 @@ describe('OAuthProvider Utils', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuthProvider', () => {
|
||||
class MyAuthProvider implements OAuthProviderHandlers {
|
||||
async start() {
|
||||
return {
|
||||
url: '/url',
|
||||
status: 301,
|
||||
};
|
||||
}
|
||||
async handler() {
|
||||
return {
|
||||
user: {},
|
||||
info: {
|
||||
refreshToken: 'token',
|
||||
},
|
||||
};
|
||||
}
|
||||
async refresh() {
|
||||
return {
|
||||
accessToken: 'token',
|
||||
};
|
||||
}
|
||||
}
|
||||
const providerInstance = new MyAuthProvider();
|
||||
const providerId = 'test-provider';
|
||||
|
||||
it('sets the correct headers in start', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId);
|
||||
const mockRequest = ({
|
||||
query: {
|
||||
scope: 'user',
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const mockResponse = ({
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
statusCode: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
await oauthProvider.start(mockRequest, mockResponse);
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
|
||||
expect(mockResponse.statusCode).toEqual(301);
|
||||
expect(mockResponse.end).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets the refresh cookie if refresh is enabled', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId);
|
||||
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
'test-provider-nonce': 'nonce',
|
||||
},
|
||||
query: {
|
||||
state: 'nonce',
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const mockResponse = ({
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
expect.stringContaining('test-provider-refresh-token'),
|
||||
expect.stringContaining('token'),
|
||||
expect.objectContaining({ path: '/auth/test-provider' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does no set the refresh cookie if refresh is disabled', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId, true);
|
||||
|
||||
const mockRequest = ({
|
||||
cookies: {
|
||||
'test-provider-nonce': 'nonce',
|
||||
},
|
||||
query: {
|
||||
state: 'nonce',
|
||||
},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const mockResponse = ({
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('removes refresh cookie when logging out', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId);
|
||||
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const mockResponse = ({
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
send: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
await oauthProvider.logout(mockRequest, mockResponse);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
expect.stringContaining('test-provider-refresh-token'),
|
||||
'',
|
||||
expect.objectContaining({ path: '/auth/test-provider' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('gets new access-token when refreshing', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId);
|
||||
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: {
|
||||
'test-provider-refresh-token': 'token',
|
||||
},
|
||||
query: {},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const mockResponse = ({
|
||||
send: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
await oauthProvider.refresh(mockRequest, mockResponse);
|
||||
expect(mockResponse.send).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accessToken: 'token',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles refresh without capabilities', async () => {
|
||||
const oauthProvider = new OAuthProvider(providerInstance, providerId, true);
|
||||
|
||||
const mockRequest = ({
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: {
|
||||
'test-provider-refresh-token': 'token',
|
||||
},
|
||||
query: {},
|
||||
} as unknown) as express.Request;
|
||||
|
||||
const mockResponse = ({
|
||||
send: jest.fn().mockReturnThis(),
|
||||
} as unknown) as express.Response;
|
||||
|
||||
await oauthProvider.refresh(mockRequest, mockResponse);
|
||||
expect(mockResponse.send).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.send).toHaveBeenCalledWith(
|
||||
'Refresh token not supported for provider: test-provider',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
@@ -5,19 +21,18 @@ import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRefreshTokenStrategy,
|
||||
} from './PassportStrategyHelper';
|
||||
import { RedirectInfo } from './types';
|
||||
|
||||
const mockRequest = ({} as unknown) as express.Request;
|
||||
|
||||
describe('PassportStrategyHelper', () => {
|
||||
class MyCustomRedirectStrategy extends passport.Strategy {
|
||||
authenticate(_req: express.Request, options: any) {
|
||||
authenticate() {
|
||||
this.redirect('a', 302);
|
||||
}
|
||||
}
|
||||
|
||||
describe('executeRedirectStrategy', () => {
|
||||
it('should call authenticate and resolve with RedirectInfo', () => {
|
||||
it('should call authenticate and resolve with RedirectInfo', async () => {
|
||||
const mockStrategy = new MyCustomRedirectStrategy();
|
||||
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
|
||||
const redirectStrategyPromise = executeRedirectStrategy(
|
||||
@@ -26,7 +41,7 @@ describe('PassportStrategyHelper', () => {
|
||||
{},
|
||||
);
|
||||
expect(spyAuthenticate).toBeCalledTimes(1);
|
||||
expect(redirectStrategyPromise).resolves.toStrictEqual(
|
||||
await expect(redirectStrategyPromise).resolves.toStrictEqual(
|
||||
expect.objectContaining({ url: 'a', status: 302 }),
|
||||
);
|
||||
});
|
||||
@@ -34,7 +49,7 @@ describe('PassportStrategyHelper', () => {
|
||||
|
||||
describe('executeFrameHandlerStrategy', () => {
|
||||
class MyCustomAuthSuccessStrategy extends passport.Strategy {
|
||||
authenticate(_req: express.Request, options: any) {
|
||||
authenticate() {
|
||||
this.success(
|
||||
{ accessToken: 'ACCESS_TOKEN' },
|
||||
{ refreshToken: 'REFRESH_TOKEN' },
|
||||
@@ -42,22 +57,22 @@ describe('PassportStrategyHelper', () => {
|
||||
}
|
||||
}
|
||||
class MyCustomAuthErrorStrategy extends passport.Strategy {
|
||||
authenticate(_req: express.Request, options: any) {
|
||||
authenticate() {
|
||||
this.error(new Error('MyCustomAuth error'));
|
||||
}
|
||||
}
|
||||
class MyCustomAuthRedirectStrategy extends passport.Strategy {
|
||||
authenticate(_req: express.Request, options: any) {
|
||||
authenticate() {
|
||||
this.redirect('URL', 302);
|
||||
}
|
||||
}
|
||||
class MyCustomAuthFailStrategy extends passport.Strategy {
|
||||
authenticate(_req: express.Request, options: any) {
|
||||
authenticate() {
|
||||
this.fail('challenge', 302);
|
||||
}
|
||||
}
|
||||
|
||||
it('should resolve with user and info on success', () => {
|
||||
it('should resolve with user and info on success', async () => {
|
||||
const mockStrategy = new MyCustomAuthSuccessStrategy();
|
||||
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
|
||||
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
|
||||
@@ -65,7 +80,7 @@ describe('PassportStrategyHelper', () => {
|
||||
mockStrategy,
|
||||
);
|
||||
expect(spyAuthenticate).toBeCalledTimes(1);
|
||||
expect(frameHandlerStrategyPromise).resolves.toStrictEqual(
|
||||
await expect(frameHandlerStrategyPromise).resolves.toStrictEqual(
|
||||
expect.objectContaining({
|
||||
user: { accessToken: 'ACCESS_TOKEN' },
|
||||
info: { refreshToken: 'REFRESH_TOKEN' },
|
||||
@@ -73,7 +88,7 @@ describe('PassportStrategyHelper', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject on error', () => {
|
||||
it('should reject on error', async () => {
|
||||
const mockStrategy = new MyCustomAuthErrorStrategy();
|
||||
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
|
||||
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
|
||||
@@ -81,12 +96,12 @@ describe('PassportStrategyHelper', () => {
|
||||
mockStrategy,
|
||||
);
|
||||
expect(spyAuthenticate).toBeCalledTimes(1);
|
||||
expect(frameHandlerStrategyPromise).rejects.toThrowError(
|
||||
await expect(frameHandlerStrategyPromise).rejects.toThrow(
|
||||
'Authentication failed, Error: MyCustomAuth error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject on redirect', () => {
|
||||
it('should reject on redirect', async () => {
|
||||
const mockStrategy = new MyCustomAuthRedirectStrategy();
|
||||
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
|
||||
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
|
||||
@@ -94,12 +109,12 @@ describe('PassportStrategyHelper', () => {
|
||||
mockStrategy,
|
||||
);
|
||||
expect(spyAuthenticate).toBeCalledTimes(1);
|
||||
expect(frameHandlerStrategyPromise).rejects.toThrowError(
|
||||
await expect(frameHandlerStrategyPromise).rejects.toThrow(
|
||||
'Unexpected redirect',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject on fail', () => {
|
||||
it('should reject on fail', async () => {
|
||||
const mockStrategy = new MyCustomAuthFailStrategy();
|
||||
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
|
||||
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
|
||||
@@ -107,14 +122,13 @@ describe('PassportStrategyHelper', () => {
|
||||
mockStrategy,
|
||||
);
|
||||
expect(spyAuthenticate).toBeCalledTimes(1);
|
||||
expect(frameHandlerStrategyPromise).rejects.toThrow();
|
||||
await expect(frameHandlerStrategyPromise).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeRefreshTokenStrategy', () => {
|
||||
it('should resolve with a new access token, scope and expiry', () => {
|
||||
it('should resolve with a new access token, scope and expiry', async () => {
|
||||
class MyCustomOAuth2Success {
|
||||
constructor() {}
|
||||
getOAuthAccessToken(
|
||||
_refreshToken: string,
|
||||
_options: any,
|
||||
@@ -127,6 +141,7 @@ describe('PassportStrategyHelper', () => {
|
||||
}
|
||||
}
|
||||
class MyCustomRefreshTokenSuccess extends passport.Strategy {
|
||||
// @ts-ignore
|
||||
private _oauth2 = new MyCustomOAuth2Success();
|
||||
}
|
||||
|
||||
@@ -136,7 +151,7 @@ describe('PassportStrategyHelper', () => {
|
||||
'REFRESH_TOKEN',
|
||||
'a',
|
||||
);
|
||||
expect(refreshTokenPromise).resolves.toStrictEqual(
|
||||
await expect(refreshTokenPromise).resolves.toStrictEqual(
|
||||
expect.objectContaining({
|
||||
accessToken: 'ACCESS_TOKEN',
|
||||
params: expect.objectContaining({ scope: 'a', expires_in: 10 }),
|
||||
@@ -144,9 +159,8 @@ describe('PassportStrategyHelper', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject with an error if refresh failed', () => {
|
||||
it('should reject with an error if refresh failed', async () => {
|
||||
class MyCustomOAuth2Error {
|
||||
constructor() {}
|
||||
getOAuthAccessToken(
|
||||
_refreshToken: string,
|
||||
_options: any,
|
||||
@@ -156,6 +170,7 @@ describe('PassportStrategyHelper', () => {
|
||||
}
|
||||
}
|
||||
class MyCustomRefreshTokenSuccess extends passport.Strategy {
|
||||
// @ts-ignore
|
||||
private _oauth2 = new MyCustomOAuth2Error();
|
||||
}
|
||||
|
||||
@@ -165,14 +180,13 @@ describe('PassportStrategyHelper', () => {
|
||||
'REFRESH_TOKEN',
|
||||
'a',
|
||||
);
|
||||
expect(refreshTokenPromise).rejects.toThrow(
|
||||
await expect(refreshTokenPromise).rejects.toThrow(
|
||||
'Failed to refresh access token Error: Unknown error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject with an error if access token missing in refresh callback', () => {
|
||||
it('should reject with an error if access token missing in refresh callback', async () => {
|
||||
class MyCustomOAuth2AccessTokenMissing {
|
||||
constructor() {}
|
||||
getOAuthAccessToken(
|
||||
_refreshToken: string,
|
||||
_options: any,
|
||||
@@ -182,6 +196,7 @@ describe('PassportStrategyHelper', () => {
|
||||
}
|
||||
}
|
||||
class MyCustomRefreshTokenSuccess extends passport.Strategy {
|
||||
// @ts-ignore
|
||||
private _oauth2 = new MyCustomOAuth2AccessTokenMissing();
|
||||
}
|
||||
|
||||
@@ -191,7 +206,7 @@ describe('PassportStrategyHelper', () => {
|
||||
'REFRESH_TOKEN',
|
||||
'a',
|
||||
);
|
||||
expect(refreshTokenPromise).rejects.toThrow(
|
||||
await expect(refreshTokenPromise).rejects.toThrow(
|
||||
'Failed to refresh access token, no access token received',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export const executeRedirectStrategy = async (
|
||||
providerStrategy: passport.Strategy,
|
||||
options: any,
|
||||
): Promise<RedirectInfo> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(resolve => {
|
||||
const strategy = Object.create(providerStrategy);
|
||||
strategy.redirect = (url: string, status?: number) => {
|
||||
resolve({ url, status: status ?? undefined });
|
||||
|
||||
Reference in New Issue
Block a user