diff --git a/src/__test__/popup.spec.js b/src/__test__/popup.spec.js index 5701bd9..c936412 100644 --- a/src/__test__/popup.spec.js +++ b/src/__test__/popup.spec.js @@ -1,11 +1,6 @@ // @flow /* eslint-env jest */ -import { - appOriginHandler, - loginHandler, - storageHandler, - startPopupServer -} from '../popup' +import { obtainSession, popupHandler } from '../popup' import { polyfillWindow, polyunfillWindow } from './spec-helpers' import { defaultStorage } from '../storage' @@ -13,13 +8,49 @@ beforeEach(polyfillWindow) afterEach(polyunfillWindow) -describe('storageHandler', () => { +describe('obtainSession', () => { + it('resolves to the captured session once it captures the "foundSession" method', async () => { + expect.assertions(1) + const store = defaultStorage() + const session = { + idp: 'https://localhost', + webId: 'https://localhost/profile#me' + } + const sessionPromise = obtainSession(store, window, { + popupUri: 'https://app.biz/select-idp', + callbackUri: 'https://app.biz/callback', + storage: store + }) + window.postMessage( + { + 'solid-auth-client': { + id: '12345', + method: 'foundSession', + args: [session] + } + }, + window.location.origin + ) + const resolvedSession = await sessionPromise + expect(resolvedSession).toEqual(session) + }) +}) + +describe('popupHandler', () => { let store let handler + const mockCallback = jest.fn() + + const options = { + popupUri: 'https://localhost/select-idp', + callbackUri: 'https://localhost/callback', + storage: defaultStorage() + } beforeEach(() => { store = defaultStorage() - handler = storageHandler(store) + handler = popupHandler(store, options, mockCallback) + mockCallback.mockReset() }) it('implements getItem', async () => { @@ -44,23 +75,8 @@ describe('storageHandler', () => { expect(await store.getItem('foo')).toEqual(null) }) - it('ignores unknown methods', async () => { - expect.assertions(1) - const resp = await handler('unknown_method', 'a', 'b', 'c') - expect(resp).toBeNull() - }) -}) - -describe('loginHandler', () => { - const options = { - popupUri: 'https://localhost/select-idp', - callbackUri: 'https://localhost/callback', - storage: defaultStorage() - } - it('returns the loginOptions', async () => { expect.assertions(1) - const handler = loginHandler(options, () => {}) const _options = await handler('getLoginOptions') expect(_options).toEqual({ popupUri: options.popupUri, @@ -70,76 +86,25 @@ describe('loginHandler', () => { it('captures a found session', async () => { expect.assertions(3) - const mockCallback = jest.fn() - const handler = loginHandler(options, mockCallback) const session = { idp: 'https://example.com', webId: 'https://me.example.com/profile#me' } const _sessionResp = await handler('foundSession', session) - expect(_sessionResp).toEqual(null) + expect(_sessionResp).toBeUndefined() expect(mockCallback.mock.calls.length).toBe(1) expect(mockCallback.mock.calls[0][0]).toEqual(session) }) - it('ignores unknown methods', async () => { - expect.assertions(1) - const handler = loginHandler(options, () => {}) - const resp = await handler('unknown_method', 'a', 'b', 'c') - expect(resp).toBeNull() - }) -}) - -describe('appOriginHandler', () => { it('responds with the window origin', async () => { expect.assertions(1) - const resp = await appOriginHandler('getAppOrigin') + const resp = await handler('getAppOrigin') expect(resp).toEqual('https://app.biz') }) it('ignores unknown methods', async () => { expect.assertions(1) - const resp = await appOriginHandler('unknown_method') - expect(resp).toBeNull() - }) -}) - -describe('startPopupServer', () => { - it('rejects if loginOptions does not include both popupUri and callbackUri', async () => { - expect.assertions(1) - const store = defaultStorage() - await expect( - startPopupServer(store, window, { - popupUri: '', - callbackUri: '', - storage: store - }) - ).rejects.toBeInstanceOf(Error) - }) - - it('resolves to the captured session once it captures the "foundSession" method', async () => { - expect.assertions(1) - const store = defaultStorage() - const session = { - idp: 'https://localhost', - webId: 'https://localhost/profile#me' - } - const sessionPromise = startPopupServer(store, window, { - popupUri: 'https://app.biz/select-idp', - callbackUri: 'https://app.biz/callback', - storage: store - }) - window.postMessage( - { - 'solid-auth-client': { - id: '12345', - method: 'foundSession', - args: [session] - } - }, - window.location.origin - ) - const resolvedSession = await sessionPromise - expect(resolvedSession).toEqual(session) + const resp = await handler('unknown_method') + expect(resp).toBeUndefined() }) }) diff --git a/src/ipc.js b/src/ipc.js index 909dba8..47aefa4 100644 --- a/src/ipc.js +++ b/src/ipc.js @@ -105,11 +105,3 @@ export class Client { }) } } - -export const combineHandlers = (...handlers: handler[]) => ( - method: string, - ...args: any[] -): ?Promise => - handlers - .map(handler => handler(method, ...args)) - .find(promise => promise !== null) diff --git a/src/popup.js b/src/popup.js index 689b344..3dc5b29 100644 --- a/src/popup.js +++ b/src/popup.js @@ -1,78 +1,29 @@ // @flow import type { loginOptions } from './solid-auth-client' -import { combineHandlers, Server } from './ipc' +import { Server } from './ipc' import type { Session } from './session' import type { AsyncStorage } from './storage' import { originOf } from './url-util' -const popupAppRequestHandler = ( - store: AsyncStorage, - options: loginOptions, - foundSessionCb: Session => void -) => - combineHandlers( - storageHandler(store), - loginHandler(options, foundSessionCb), - appOriginHandler - ) - -export const storageHandler = (store: AsyncStorage) => ( - method: string, - ...args: any[] -): ?Promise => { - switch (method) { - case 'storage/getItem': - return store.getItem(...args) - case 'storage/setItem': - return store.setItem(...args) - case 'storage/removeItem': - return store.removeItem(...args) - default: - return null - } -} - -export const loginHandler = ( - options: loginOptions, - foundSessionCb: Session => void -) => (method: string, ...args: any[]): ?Promise => { - switch (method) { - case 'getLoginOptions': - return Promise.resolve({ - popupUri: options.popupUri, - callbackUri: options.callbackUri - }) - case 'foundSession': - foundSessionCb(...args) - return Promise.resolve(null) - default: - return null - } -} - -export const appOriginHandler = (method: string): ?Promise => { - return method === 'getAppOrigin' - ? Promise.resolve(window.location.origin) - : null +export function openIdpPopup(popupUri: string): window { + const width = 650 + const height = 400 + const left = window.screenX + (window.innerWidth - width) / 2 + const top = window.screenY + (window.innerHeight - height) / 2 + const settings = `width=${width},height=${height},left=${left},top=${top}` + return window.open(popupUri, 'solid-auth-client', settings) } -export const startPopupServer = ( +export function obtainSession( store: AsyncStorage, - childWindow: window, + popup: window, options: loginOptions -): Promise => { +): Promise { return new Promise((resolve, reject) => { - if (!(options.popupUri && options.callbackUri)) { - return reject( - new Error( - 'Cannot serve a popup without both "options.popupUri" and "options.callbackUri"' - ) - ) - } const popupServer = new Server( - childWindow, + popup, originOf(options.popupUri || ''), - popupAppRequestHandler(store, options, (session: Session) => { + popupHandler(store, options, (session: Session) => { popupServer.stop() resolve(session) }) @@ -81,19 +32,30 @@ export const startPopupServer = ( }) } -export const openIdpSelector = (options: loginOptions): window => { - if (!(options.popupUri && options.callbackUri)) { - throw new Error( - 'Cannot open IDP select UI. Must provide both "options.popupUri" and "options.callbackUri".' - ) +export function popupHandler( + store: AsyncStorage, + { popupUri, callbackUri }: loginOptions, + foundSessionCb: Session => void +) { + return async (method: string, ...args: any[]) => { + switch (method) { + // Origin + case 'getAppOrigin': + return window.location.origin + + // Storage + case 'storage/getItem': + return store.getItem(...args) + case 'storage/setItem': + return store.setItem(...args) + case 'storage/removeItem': + return store.removeItem(...args) + + // Login + case 'getLoginOptions': + return { popupUri, callbackUri } + case 'foundSession': + foundSessionCb(...args) + } } - const width = 650 - const height = 400 - const w = window.open( - options.popupUri, - '_blank', - `width=${width},height=${height},left=${(window.innerWidth - width) / - 2},top=${(window.innerHeight - height) / 2}` - ) - return w } diff --git a/src/solid-auth-client.js b/src/solid-auth-client.js index d40defc..e40e89d 100644 --- a/src/solid-auth-client.js +++ b/src/solid-auth-client.js @@ -2,7 +2,7 @@ /* global fetch */ import EventEmitter from 'events' import { authnFetch } from './authn-fetch' -import { openIdpSelector, startPopupServer } from './popup' +import { openIdpPopup, obtainSession } from './popup' import type { Session } from './session' import { getSession, saveSession, clearSession } from './session' import type { AsyncStorage } from './storage' @@ -40,12 +40,8 @@ export default class SolidAuthClient extends EventEmitter { if (!options.callbackUri) { options.callbackUri = options.popupUri } - const childWindow = openIdpSelector(options) - const session = await startPopupServer( - options.storage, - childWindow, - options - ) + const popup = openIdpPopup(options.popupUri) + const session = await obtainSession(options.storage, popup, options) this.emit('login', session) this.emit('session', session) return session