Skip to content

Commit

Permalink
Simplify popup calling code.
Browse files Browse the repository at this point in the history
  • Loading branch information
RubenVerborgh committed Oct 13, 2018
1 parent c393f40 commit 268b3e0
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 169 deletions.
121 changes: 43 additions & 78 deletions src/__test__/popup.spec.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
// @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'

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 () => {
Expand All @@ -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,
Expand All @@ -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()
})
})
8 changes: 0 additions & 8 deletions src/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,3 @@ export class Client {
})
}
}

export const combineHandlers = (...handlers: handler[]) => (
method: string,
...args: any[]
): ?Promise<any> =>
handlers
.map(handler => handler(method, ...args))
.find(promise => promise !== null)
114 changes: 38 additions & 76 deletions src/popup.js
Original file line number Diff line number Diff line change
@@ -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<any> => {
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<any> => {
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<any> => {
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<?Session> => {
): Promise<?Session> {
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)
})
Expand All @@ -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
}
10 changes: 3 additions & 7 deletions src/solid-auth-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 268b3e0

Please sign in to comment.