Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.tap/
/node_modules/
/package-lock.json
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@eslint/js": "^9.17.0",
"eslint": "^9.17.0",
"globals": "^15.14.0",
"tap": "^16.0.1"
"tap": "^21.7.0"
},
"dependencies": {
"@muze-nl/assert": "^0.5.1",
Expand Down
18 changes: 8 additions & 10 deletions src/oauth2.popup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function handleRedirect(origin = null) {

let params = new URLSearchParams(window.location.search)
if (!params.has('code') && window.location.hash) {
let query = window.location.hash.substr(1)
let query = window.location.hash.substring(1)
params = new URLSearchParams('?'+query)
}

Expand All @@ -23,20 +23,18 @@ export function handleRedirect(origin = null) {
// There is no parent, either the page was opened directly or the user closed the calling window.
console.error('No parent window found, cannot post authorization code (or error)')
} else {
let message

if (params.has('code')) {
parent.postMessage({
authorization_code: params.get('code')
}, origin)
success = true
message = { authorization_code: params.get('code') }
} else if (params.has('error')) {
parent.postMessage({
error: params.get('error')
}, origin)
message = { error: params.get('error') }
} else {
parent.postMessage({
error: 'Could not find an authorization_code',
}, origin)
message = { error: 'Could not find an authorization_code' }
}

parent.postMessage(message, origin)
}

return success
Expand Down
155 changes: 155 additions & 0 deletions test/oauth2.popup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import tap from 'tap'
import { handleRedirect, authorizePopup } from '../src/oauth2.popup.mjs'

tap.test('handleRedirect SHOULD complain WHEN called without Window existing', async t => {
globalThis.window = undefined

const actual = t.throws(handleRedirect)

t.equal(actual.message, "Cannot read properties of undefined (reading 'location')")
t.end()
})

tap.test('handleRedirect SHOULD complain WHEN called without parent existing', async t => {
globalThis.window = { location: { origin: 'mock origin' } }

const logs = t.capture(console, 'error')
const actual = handleRedirect()

t.match(logs(), [
{ args: [ 'No parent window found, cannot post authorization code (or error)' ], returned: undefined },
])
t.notOk(actual)
t.end()
})

tap.test('handleRedirect SHOULD post Error message WHEN called with parent without query params', async t => {
globalThis.window = { location: { origin: 'mock origin' } }
globalThis.window.parent = {
postMessage: createPostMessage(t, 'mock origin', { error: 'Could not find an authorization_code' })
}

const actual = handleRedirect()

t.notOk(actual)
t.end()
})

tap.test('handleRedirect SHOULD post Error message WHEN called with window opener', async t => {
globalThis.window = { location: { origin: 'mock origin' } }
globalThis.window.parent = globalThis.window
globalThis.window.opener = {
postMessage: createPostMessage(t, 'mock origin', { error: 'Could not find an authorization_code' })
}

const actual = handleRedirect()

t.notOk(actual)
t.end()
})

tap.test('handleRedirect SHOULD post received error WHEN called with parent with error query param', async t => {
globalThis.window = { location: { origin: 'mock origin', search: '?error=mockError' } }
globalThis.window.parent = {
postMessage: createPostMessage(t, 'mock origin', { error: 'mockError' })
}

const actual = handleRedirect()

t.notOk(actual)
t.end()
})

tap.test('handleRedirect SHOULD post received error WHEN called with parent with error hash param', async t => {
globalThis.window = { location: { origin: 'mock origin', search: '', hash: '#error=mockError' } }
globalThis.window.parent = {
postMessage: createPostMessage(t, 'mock origin', { error: 'mockError' })
}

const actual = handleRedirect()

t.notOk(actual)
t.end()
})

tap.test('handleRedirect SHOULD post received code WHEN called with parent with code query param', async t => {
globalThis.window = { location: { origin: 'mock origin', search: '?code=mockCode' } }
globalThis.window.parent = {
postMessage: createPostMessage(t, 'mock origin', { authorization_code: 'mockCode' })
}

const actual = handleRedirect()

t.ok(actual)
t.end()
})

tap.test('handleRedirect SHOULD post received code WHEN called with opener with code query param', async t => {
globalThis.window = { location: { origin: 'mock origin', search: '?code=mockCode' } }
globalThis.window.parent = globalThis.window
globalThis.window.opener = {
postMessage: createPostMessage(t, 'mock origin', { authorization_code: 'mockCode' })
}

const actual = handleRedirect()

t.ok(actual)
t.end()
})

tap.test('handleRedirect SHOULD post received code WHEN called with parent with code hash param', async t => {
globalThis.window = { location: { origin: 'mock origin', search: '', hash: '#code=mockCode' } }
globalThis.window.parent = {
postMessage: createPostMessage(t, 'mock origin', { authorization_code: 'mockCode' })
}

const actual = handleRedirect()

t.ok(actual)
t.end()
})

tap.test('authorizePopup SHOULD reject with error message WHEN message posted without error or code', async t => {
globalThis.window = { open: (authorizationCodeURL) => t.equal(authorizationCodeURL, 'mockUrl') }
globalThis.addEventListener = createEventListener(t, { })

const actual = await t.rejects(authorizePopup('mockUrl'))

t.equal(actual, 'Unknown authorization error')
t.end()
})

tap.test('authorizePopup SHOULD reject with received error message WHEN message posted with error', async t => {
globalThis.window = { open: (authorizationCodeURL) => t.equal(authorizationCodeURL, 'mockUrl') }
globalThis.addEventListener = createEventListener(t, { error: 'mockError' })

const actual = await t.rejects(authorizePopup('mockUrl'))

t.equal(actual, 'mockError')
t.end()
})

tap.test('authorizePopup SHOULD resolve with received code WHEN message posted with code', async t => {
globalThis.window = { open: (authorizationCodeURL) => t.equal(authorizationCodeURL, 'mockUrl') }
globalThis.addEventListener = createEventListener(t, { authorization_code: 'mockCode' })

const actual = await authorizePopup('mockUrl')

t.equal(actual, 'mockCode')
t.end()
})

function createEventListener(t, data) {
return function eventListener(event, callback, options) {
t.equal(event, 'message')
t.ok(options.once)
callback({ data: data })
}
}

function createPostMessage(t, expectedOrigin, expectedMessage) {
return function postMessage(message, origin) {
t.equal(origin, expectedOrigin)
t.match(message, expectedMessage)
}
}