Merge pull request #968 from spotify/auth-tests

Auth Backend: Tests
This commit is contained in:
Raghunandan Balachandran
2020-05-23 11:55:55 +02:00
committed by GitHub
8 changed files with 386 additions and 43 deletions
@@ -0,0 +1,51 @@
/*
* 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 { AuthProvider, AuthProviderRouteHandlers } from './types';
import { ProviderFactories } from './factories';
class MyAuthProvider implements AuthProvider, AuthProviderRouteHandlers {
strategy(): passport.Strategy {
return new passport.Strategy();
}
async start(_: express.Request, res: express.Response): Promise<any> {
res.send('start');
}
async frameHandler(_: express.Request, res: express.Response): Promise<any> {
res.send('frameHandler');
}
async logout(_: express.Request, res: express.Response): Promise<any> {
res.send('logout');
}
}
describe('getProviderFactory', () => {
it('makes a provider for MyAuthProvider', () => {
jest
.spyOn(ProviderFactories, 'getProviderFactory')
.mockReturnValueOnce(MyAuthProvider);
const provider = ProviderFactories.getProviderFactory('a');
expect(provider).toBeDefined();
});
it('throws an error when provider implementation does not exist', () => {
expect(() => {
ProviderFactories.getProviderFactory('b');
}).toThrow('Provider Implementation missing for : b auth provider');
});
});
@@ -0,0 +1,34 @@
/*
* 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 { AuthProviderFactories, AuthProviderFactory } from './types';
import { GoogleAuthProvider } from './google/provider';
export class ProviderFactories {
private static readonly providerFactories: AuthProviderFactories = {
google: GoogleAuthProvider,
};
public static getProviderFactory(providerId: string): AuthProviderFactory {
const ProviderImpl = ProviderFactories.providerFactories[providerId];
if (!ProviderImpl) {
throw Error(
`Provider Implementation missing for : ${providerId} auth provider`,
);
}
return ProviderImpl;
}
}
@@ -0,0 +1,162 @@
/*
* 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 { GoogleAuthProvider } from './provider';
import passport from 'passport';
import express from 'express';
import * as utils from './../utils';
const googleAuthProviderConfig = {
provider: 'google',
options: {
clientID: 'a',
clientSecret: 'b',
callbackURL: 'c',
},
};
const googleAuthProviderConfigInvalidOptions = {
provider: 'google',
options: {},
};
describe('GoogleAuthProvider', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('create a new provider', () => {
it('should succeed with valid config', () => {
const googleAuthProvider = new GoogleAuthProvider(
googleAuthProviderConfig,
);
expect(googleAuthProvider).toBeDefined();
expect(googleAuthProvider.start).toBeDefined();
expect(googleAuthProvider.logout).toBeDefined();
expect(googleAuthProvider.frameHandler).toBeDefined();
expect(googleAuthProvider.strategy).toBeDefined();
});
});
describe('start authentication handler', () => {
const mockResponse: any = ({} as unknown) as express.Response;
const mockNext: express.NextFunction = jest.fn();
it('should initiate authenticate request with provided scopes', () => {
const mockRequest = ({
query: {
scope: 'a,b',
},
} as unknown) as express.Request;
const spyPassport = jest
.spyOn(passport, 'authenticate')
.mockImplementation(() => jest.fn());
const googleAuthProvider = new GoogleAuthProvider(
googleAuthProviderConfig,
);
googleAuthProvider.start(mockRequest, mockResponse, mockNext);
expect(spyPassport).toBeCalledTimes(1);
expect(spyPassport).toBeCalledWith('google', {
scope: 'a,b',
accessType: 'offline',
prompt: 'consent',
});
});
it('should throw error if no scopes provided', () => {
const mockRequest = ({
query: {},
} as unknown) as express.Request;
const googleAuthProvider = new GoogleAuthProvider(
googleAuthProviderConfig,
);
expect(() => {
googleAuthProvider.start(mockRequest, mockResponse, mockNext);
}).toThrowError('missing scope parameter');
});
});
describe('logout handler', () => {
const mockRequest = ({} as unknown) as express.Request;
it('should perform logout and respond with 200', () => {
const mockResponse: any = ({
send: jest.fn(),
} as unknown) as express.Response;
const googleAuthProvider = new GoogleAuthProvider(
googleAuthProviderConfig,
);
const spyResponse = jest
.spyOn(mockResponse, 'send')
.mockImplementation(() => jest.fn());
googleAuthProvider.logout(mockRequest, mockResponse);
expect(spyResponse).toBeCalledTimes(1);
expect(spyResponse).toBeCalledWith('logout!');
});
});
describe('redirect frame handler', () => {
const mockRequest = ({} as unknown) as express.Request;
const mockResponse: any = ({
send: jest.fn(),
} as unknown) as express.Response;
const mockNext: express.NextFunction = jest.fn();
it('should call authenticate and post a response ', () => {
const spyPostMessage = jest
.spyOn(utils, 'postMessageResponse')
.mockImplementation(() => jest.fn());
const spyPassport = jest
.spyOn(passport, 'authenticate')
.mockImplementation(() => jest.fn());
const googleAuthProvider = new GoogleAuthProvider(
googleAuthProviderConfig,
);
googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext);
const callbackFunc = spyPassport.mock.calls[0][1] as Function;
callbackFunc();
expect(spyPassport).toBeCalledTimes(1);
expect(spyPostMessage).toBeCalledTimes(1);
});
});
describe('strategy handler', () => {
it('should return a valid passport strategy', () => {
const googleAuthProvider = new GoogleAuthProvider(
googleAuthProviderConfig,
);
expect(googleAuthProvider.strategy()).toBeInstanceOf(passport.Strategy);
});
it('should throw an error for invalid options', () => {
expect(() => {
const googleAuthProvider = new GoogleAuthProvider(
googleAuthProviderConfigInvalidOptions,
);
googleAuthProvider.strategy();
}).toThrow();
});
});
});
@@ -61,14 +61,12 @@ export class GoogleAuthProvider
})(req, res, next);
}
logout(_req: express.Request, res: express.Response) {
return new Promise((resolve) => {
res.send('logout!');
resolve();
});
async logout(_req: express.Request, res: express.Response) {
res.send('logout!');
}
strategy(): passport.Strategy {
// TODO: throw error if env variables not set?
return new GoogleStrategy(
{ ...this.providerConfig.options },
(
@@ -76,9 +74,9 @@ export class GoogleAuthProvider
refreshToken: any,
params: any,
profile: any,
cb: any,
done: any,
) => {
cb(undefined, {
done(undefined, {
profile,
idToken: params.id_token,
accessToken,
@@ -0,0 +1,103 @@
/*
* 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 passport from 'passport';
import express from 'express';
import { makeProvider, defaultRouter } from '.';
import {
AuthProvider,
AuthProviderRouteHandlers,
AuthProviderConfig,
} from './types';
import * as passportGoogleOAuth20 from 'passport-google-oauth20';
import { ProviderFactories } from './factories';
class MyAuthProvider implements AuthProvider, AuthProviderRouteHandlers {
private readonly providerConfig: AuthProviderConfig;
constructor(providerConfig: AuthProviderConfig) {
this.providerConfig = providerConfig;
}
strategy(): passport.Strategy {
return new passportGoogleOAuth20.Strategy(
this.providerConfig.options,
() => {},
);
}
async start(_: express.Request, res: express.Response): Promise<any> {
res.send('start');
}
async frameHandler(_: express.Request, res: express.Response): Promise<any> {
res.send('frameHandler');
}
async logout(_: express.Request, res: express.Response): Promise<any> {
res.send('logout');
}
}
class MyAuthProviderWithRefresh extends MyAuthProvider {
async refresh(_: express.Request, res: express.Response): Promise<any> {
res.send('logout');
}
}
const providerConfig = {
provider: 'a',
options: {
clientID: 'somevalue',
},
};
const providerConfigInvalid = {
provider: 'b',
options: {
clientID: 'somevalue',
},
};
describe('makeProvider', () => {
it('makes a provider for Myauthprovider', () => {
jest
.spyOn(ProviderFactories, 'getProviderFactory')
.mockReturnValueOnce(MyAuthProvider);
const provider = makeProvider(providerConfig);
expect(provider.providerId).toEqual('a');
expect(provider.strategy).toBeDefined();
expect(provider.providerRouter).toBeDefined();
});
it('throws an error when provider implementation does not exist', () => {
expect(() => {
makeProvider(providerConfigInvalid);
}).toThrow('Provider Implementation missing for : b auth provider');
});
});
describe('defaultRouter', () => {
it('make router for auth provider without refresh', () => {
expect(
defaultRouter(new MyAuthProvider({ provider: 'a', options: {} })),
).toBeDefined();
});
it('make router for auth provider with refresh', () => {
expect(
defaultRouter(
new MyAuthProviderWithRefresh({ provider: 'b', options: {} }),
),
).toBeDefined();
});
});
+3 -15
View File
@@ -15,17 +15,8 @@
*/
import Router from 'express-promise-router';
import {
AuthProviderRouteHandlers,
AuthProviderFactories,
AuthProviderConfig,
} from './types';
import { GoogleAuthProvider } from './google/provider';
const providerFactories: AuthProviderFactories = {
google: GoogleAuthProvider,
};
import { AuthProviderRouteHandlers, AuthProviderConfig } from './types';
import { ProviderFactories } from './factories';
export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
const router = Router();
@@ -40,10 +31,7 @@ export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
export const makeProvider = (config: AuthProviderConfig) => {
const providerId = config.provider;
const ProviderImpl = providerFactories[providerId];
if (!ProviderImpl) {
throw Error(`Provider Implementation missing for provider: ${providerId}`);
}
const ProviderImpl = ProviderFactories.getProviderFactory(providerId);
const providerInstance = new ProviderImpl(config);
const strategy = providerInstance.strategy();
const providerRouter = defaultRouter(providerInstance);
+5 -3
View File
@@ -51,9 +51,11 @@ export interface AuthProviderRouteHandlers {
}
export type AuthProviderFactories = {
[key: string]: {
new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers;
};
[key: string]: AuthProviderFactory;
};
export type AuthProviderFactory = {
new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers;
};
export type AuthInfo = {
+23 -18
View File
@@ -9,7 +9,7 @@
dependencies:
"@babel/highlight" "^7.0.0"
"@babel/code-frame@7.8.3", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3":
"@babel/code-frame@7.8.3", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
version "7.8.3"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==
@@ -3961,9 +3961,9 @@
"@types/uglify-js" "*"
"@types/html-webpack-plugin@*", "@types/html-webpack-plugin@^3.2.2":
version "3.2.3"
resolved "https://registry.npmjs.org/@types/html-webpack-plugin/-/html-webpack-plugin-3.2.3.tgz#865323e30e82560c0ca898dbf9f6f9d1c541cd7f"
integrity sha512-Y7dsVhTn75IaD4lMIY02UP1L8e0ou8KQu8DKPJAegEFKdJR28/8ejayDG8ykfR0DtYCx3dCEHIkdpN8AOB6txQ==
version "3.2.2"
resolved "https://registry.npmjs.org/@types/html-webpack-plugin/-/html-webpack-plugin-3.2.2.tgz#f552121f3c0a3972dda9a425de1e0029069b2907"
integrity sha512-KsL5cHtNWhOQF9Cu+Dpn7GemzQRxdKhe1/LgZUSku33B5L4Cx2/p3DX6YbeRNOoI552MNbB/VNbCDNEYU//iAw==
dependencies:
"@types/html-minifier" "*"
"@types/tapable" "*"
@@ -4069,9 +4069,9 @@
integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==
"@types/lodash@^4.14.151":
version "4.14.152"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c"
integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg==
version "4.14.151"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.151.tgz#7d58cac32bedb0ec37cb7f99094a167d6176c9d5"
integrity sha512-Zst90IcBX5wnwSu7CAS0vvJkTjTELY4ssKbHiTnGcJgi170uiS8yQDdc3v6S77bRqYQIN1App5a1Pc2lceE5/g==
"@types/mime@*":
version "2.0.1"
@@ -9749,11 +9749,11 @@ fork-ts-checker-webpack-plugin@3.1.1:
worker-rpc "^0.1.0"
fork-ts-checker-webpack-plugin@^4.0.5:
version "4.1.4"
resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.4.tgz#f0dc3ece19ec5b792d7b8ecd2a7f43509a5285ce"
integrity sha512-R0nTlZSyV0uCCzYe1kgR7Ve8mXyDvMm1pJwUFb6zzRVF5rTNb24G6gn2DFQy+W5aJYp2eq8aexpCOO+1SCyCSA==
version "4.1.0"
resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.0.tgz#62bffe704426770fee33f15f0c0d56c86297fefd"
integrity sha512-2DLwUVUR/AdNmMD2utfmSR8r4qHRFhnfL6QQDQS5q4g5uBZzXYDgg8MXPIbu0HzyLjyvbogqjBNKILG5fufwzg==
dependencies:
"@babel/code-frame" "^7.5.5"
babel-code-frame "^6.22.0"
chalk "^2.4.1"
micromatch "^3.1.10"
minimatch "^3.0.4"
@@ -20444,10 +20444,15 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^3.7.4, typescript@^3.9.2:
version "3.9.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a"
integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==
typescript@^3.7.4:
version "3.8.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
typescript@^3.9.2:
version "3.9.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9"
integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
@@ -20884,9 +20889,9 @@ uuid@^7.0.3:
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
uuid@^8.0.0:
version "8.1.0"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
version "8.0.0"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
v8-compile-cache@^2.0.3:
version "2.1.0"