diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts new file mode 100644 index 0000000000..d6346c688b --- /dev/null +++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts @@ -0,0 +1,104 @@ +/* + * 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 MockOAuthApi from './MockOAuthApi'; +import PowerIcon from '@material-ui/icons/Power'; +import { BasicOAuthScopes } from './BasicOAuthScopes'; + +describe('MockOAuthApi', () => { + it('should trigger all requests', async () => { + const popupResult = { is: 'done' }; + const mock = new MockOAuthApi(popupResult); + + const authHandler1 = jest + .fn() + .mockImplementation(() => mock.showLoginPopup()); + const requester1 = mock.createAuthRequester({ + provider: { icon: PowerIcon, title: 'Test' }, + onAuthRequest: authHandler1, + }); + + const authHandler2 = jest.fn().mockResolvedValue('other'); + const requester2 = mock.createAuthRequester({ + provider: { icon: PowerIcon, title: 'Test' }, + onAuthRequest: authHandler2, + }); + + const promises = [ + requester1(BasicOAuthScopes.from('a')), + requester1(BasicOAuthScopes.from('b')), + requester2(BasicOAuthScopes.from('a b')), + requester2(BasicOAuthScopes.from('b c')), + requester2(BasicOAuthScopes.from('c a')), + ]; + + await expect( + Promise.race([Promise.all(promises), 'waiting']), + ).resolves.toBe('waiting'); + + await mock.triggerAll(); + + await expect(Promise.all(promises)).resolves.toEqual([ + popupResult, + popupResult, + 'other', + 'other', + 'other', + ]); + + expect(authHandler1).toHaveBeenCalledTimes(1); + expect(authHandler1).toHaveBeenCalledWith(BasicOAuthScopes.from('a b')); + expect(authHandler2).toHaveBeenCalledTimes(1); + expect(authHandler2).toHaveBeenCalledWith(BasicOAuthScopes.from('a b c')); + }); + + it('should reject all requests', async () => { + const mock = new MockOAuthApi(); + + const authHandler1 = jest.fn(); + const requester1 = mock.createAuthRequester({ + provider: { icon: PowerIcon, title: 'Test' }, + onAuthRequest: authHandler1, + }); + + const authHandler2 = jest.fn(); + const requester2 = mock.createAuthRequester({ + provider: { icon: PowerIcon, title: 'Test' }, + onAuthRequest: authHandler2, + }); + + const promises = [ + requester1(BasicOAuthScopes.from('a')), + requester1(BasicOAuthScopes.from('b')), + requester2(BasicOAuthScopes.from('a b')), + requester2(BasicOAuthScopes.from('b c')), + requester2(BasicOAuthScopes.from('c a')), + ]; + + await expect( + Promise.race([Promise.all(promises), 'waiting']), + ).resolves.toBe('waiting'); + + await mock.rejectAll(); + + for (const promise of promises) { + await expect(promise).rejects.toMatchObject({ name: 'RejectedError' }); + } + + expect(authHandler1).not.toHaveBeenCalled(); + expect(authHandler2).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.ts b/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.ts new file mode 100644 index 0000000000..f52c50dbe5 --- /dev/null +++ b/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.ts @@ -0,0 +1,66 @@ +/* + * 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 { + OAuthRequestApi, + AuthRequesterOptions, + LoginPopupOptions, +} from '../../definitions'; +import { OAuthRequestManager } from './OAuthRequestManager'; + +export default class MockOAuthApi implements OAuthRequestApi { + private readonly real = new OAuthRequestManager(); + + constructor(private readonly popupResult = {}) {} + + createAuthRequester(options: AuthRequesterOptions) { + return this.real.createAuthRequester(options); + } + + authRequest$() { + return this.real.authRequest$(); + } + + async triggerAll() { + await Promise.resolve(); // Wait a tick to allow new requests to get forwarded + + return new Promise((resolve) => { + const subscription = this.authRequest$().subscribe((requests) => { + subscription.unsubscribe(); + Promise.all(requests.map((request) => request.trigger())).then(() => + resolve(), + ); + }); + }); + } + + async rejectAll() { + await Promise.resolve(); // Wait a tick to allow new requests to get forwarded + + return new Promise((resolve) => { + const subscription = this.authRequest$().subscribe((requests) => { + subscription.unsubscribe(); + requests.map((request) => request.reject()); + resolve(); + }); + }); + } + + async showLoginPopup(options?: LoginPopupOptions): Promise { + // Working around linter complaints, can't remove options since we want correct mock types + return options ? this.popupResult : this.popupResult; + } +}