Skip to content

Commit 097fffa

Browse files
authored
Merge pull request #38 from muntact/feature/make-invalidate-called
Add invalidate function (WIP)
2 parents 80bbc4f + d7fd5a4 commit 097fffa

File tree

7 files changed

+164
-23
lines changed

7 files changed

+164
-23
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ const credentialsAuthenticator = createAuthenticator({
227227
authenticate(data) {
228228
// ...
229229
},
230+
invalidate(data) {
231+
// ...
232+
},
230233
restore(data) {
231234
// ...
232235
}
@@ -251,6 +254,16 @@ const credentialsAuthenticator = createAuthenticator({
251254
always returns a rejected promise resulting in an unauthenticated session. It
252255
is important that this function is defined when creating your authenticator.
253256

257+
* `invalidate(data)` (_function_): A function responsible for doing any
258+
additional cleanup of the authenticated data. This function will be invoked
259+
when the [`invalidateSession`](#invalidatesession) action is dispatched. It
260+
accepts a single argument with the data persisted to the session and must
261+
return a promise. A resolved promise will clear the authenticated session
262+
data and result in an unauthenticated session. A rejected promise will
263+
result in invalidation being interrupted, however session data will still be
264+
wiped. Note that a default implementation of this function is defined if none
265+
is given and always returns a resolved promise.
266+
254267
* `restore(data)` (_function_): A function used to restore the session,
255268
typically after a page refresh. This function will be invoked when the
256269
middleware is first created. It accepts an argument with the data persisted to
@@ -291,6 +304,9 @@ const credentialsAuthenticator = createAuthenticator({
291304
}
292305

293306
return Promise.reject()
307+
},
308+
invalidate(data) {
309+
return fetch('/api/invalidate', { method: 'DELETE' })
294310
}
295311
})
296312

packages/redux-simple-auth/src/actionTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const AUTHENTICATE_SUCCEEDED = `${prefix}/AUTHENTICATE_SUCCEEDED`
66
export const FETCH = `${prefix}/FETCH`
77
export const INITIALIZE = `${prefix}/INITIALIZE`
88
export const INVALIDATE_SESSION = `${prefix}/INVALIDATE_SESSION`
9+
export const INVALIDATE_SESSION_FAILED = `${prefix}/INVALIDATE_SESSION_FAILED`
910
export const UPDATE_SESSION = `${prefix}/UPDATE_SESSION`
1011
export const RESTORE = `${prefix}/RESTORE`
1112
export const RESTORE_FAILED = `${prefix}/RESTORE_FAILED`

packages/redux-simple-auth/src/actions.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FETCH,
66
INITIALIZE,
77
INVALIDATE_SESSION,
8+
INVALIDATE_SESSION_FAILED,
89
UPDATE_SESSION,
910
RESTORE,
1011
RESTORE_FAILED
@@ -36,6 +37,10 @@ export const invalidateSession = () => ({
3637
type: INVALIDATE_SESSION
3738
})
3839

40+
export const invalidateSessionFailed = () => ({
41+
type: INVALIDATE_SESSION_FAILED
42+
})
43+
3944
export const updateSession = payload => ({
4045
type: UPDATE_SESSION,
4146
payload
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
export default (
2-
{
3-
name,
4-
restore = () => Promise.reject(),
5-
authenticate = () => Promise.reject()
6-
} = {}
7-
) => {
1+
export default ({
2+
name,
3+
restore = () => Promise.reject(),
4+
authenticate = () => Promise.reject(),
5+
invalidate = (data) => Promise.resolve(data)
6+
} = {}) => {
87
if (name == null) {
98
throw new Error('Authenticators must define a `name` property')
109
}
@@ -18,6 +17,7 @@ export default (
1817
return {
1918
name,
2019
restore,
21-
authenticate
20+
authenticate,
21+
invalidate
2222
}
2323
}

packages/redux-simple-auth/src/middleware.js

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import defaultStorage from './storage/default'
22
import isPlainObject from 'lodash.isplainobject'
3-
import { AUTHENTICATE, FETCH } from './actionTypes'
3+
import { AUTHENTICATE, FETCH, INVALIDATE_SESSION } from './actionTypes'
44
import {
55
authenticateFailed,
66
authenticateSucceeded,
77
invalidateSession,
8+
invalidateSessionFailed,
89
restore,
910
restoreFailed,
1011
updateSession
1112
} from './actions'
13+
import {
14+
getSessionData,
15+
getAuthenticator,
16+
getIsAuthenticated
17+
} from './selectors'
1218

1319
const validateAuthenticatorsPresence = ({ authenticator, authenticators }) => {
1420
if (authenticator == null && authenticators == null) {
@@ -98,18 +104,18 @@ export default (config = {}) => {
98104
)
99105
}
100106
case FETCH: {
101-
const { session } = getState()
107+
const state = getState()
102108
const { url, options = {} } = action.payload
103109
const { headers = {} } = options
104110

105111
if (authorize) {
106-
authorize(session.data, (name, value) => {
112+
authorize(getSessionData(state), (name, value) => {
107113
headers[name] = value
108114
})
109115
}
110116

111117
return fetch(url, { ...options, headers }).then(response => {
112-
if (response.status === 401 && session.isAuthenticated) {
118+
if (response.status === 401 && getIsAuthenticated(state)) {
113119
dispatch(invalidateSession())
114120
} else if (refresh) {
115121
const result = refresh(response)
@@ -120,6 +126,44 @@ export default (config = {}) => {
120126
return response
121127
})
122128
}
129+
case INVALIDATE_SESSION: {
130+
const state = getState()
131+
132+
if (!state.session) {
133+
throw new Error(
134+
'No session data to invalidate. Be sure you authenticate the ' +
135+
'session before you try to invalidate it'
136+
)
137+
}
138+
139+
const authenticatorName = getAuthenticator(state)
140+
141+
if (!authenticatorName) {
142+
dispatch(invalidateSessionFailed())
143+
return Promise.reject(
144+
new Error(
145+
'No authenticated session. Be sure you authenticate the session ' +
146+
'before you try to invalidate it'
147+
)
148+
)
149+
}
150+
151+
const authenticator = findAuthenticator(authenticatorName)
152+
153+
if (!authenticator) {
154+
throw new Error(
155+
`No authenticator with name \`${authenticatorName}\` ` +
156+
'was found. Be sure you have defined it in the authenticators ' +
157+
'config.'
158+
)
159+
}
160+
161+
return authenticator.invalidate(getSessionData(state)).then(
162+
() => next(action),
163+
// TODO: what happens in this block:
164+
() => dispatch(invalidateSessionFailed())
165+
)
166+
}
123167
default: {
124168
const { session: prevSession } = getState()
125169
const result = next(action)

packages/redux-simple-auth/test/middleware.spec.js

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
authenticateSucceeded,
1010
fetch as fetchAction,
1111
invalidateSession,
12-
updateSession
12+
updateSession,
13+
invalidateSessionFailed
1314
} from '../src/actions'
1415
import {
1516
failAuthenticator,
@@ -238,6 +239,75 @@ describe('auth middleware', () => {
238239
})
239240
})
240241

242+
describe('when invalidating', () => {
243+
it('calls authenticators invalidate', () => {
244+
const middleware = configureMiddleware(spiedAuthenticator)
245+
const mockStore = configureStore([middleware])
246+
const data = { username: 'test', password: 'password' }
247+
const store = mockStore({ session: { authenticator: 'test', data } })
248+
const invalidateAction = invalidateSession()
249+
250+
store.dispatch(invalidateAction)
251+
expect(spiedAuthenticator.invalidate).toHaveBeenCalledWith(data)
252+
253+
spiedAuthenticator.authenticate.mockClear()
254+
spiedAuthenticator.invalidate.mockClear()
255+
})
256+
257+
describe('when there is no session', () => {
258+
it('throws error', () => {
259+
const authenticator = createAuthenticator({
260+
name: 'fake'
261+
})
262+
const middleware = configureMiddleware(authenticator)
263+
const mockStore = configureStore([middleware])
264+
const store = mockStore()
265+
const action = invalidateSession('not-real', {})
266+
267+
expect(() => store.dispatch(action)).toThrow(
268+
'No session data to invalidate. Be sure you authenticate the ' +
269+
'session before you try to invalidate it'
270+
)
271+
})
272+
})
273+
274+
describe('when redux store authenticator is not found', () => {
275+
it('returns a rejected promise and dispatches invalidate Session', async () => {
276+
const authenticator = createAuthenticator({
277+
name: 'unmatchable'
278+
})
279+
const middleware = configureMiddleware(authenticator)
280+
const mockStore = configureStore([middleware])
281+
const store = mockStore({ session: { } })
282+
await expect(store.dispatch(invalidateSession())).rejects.toEqual(
283+
new Error(
284+
'No authenticated session. Be sure you authenticate the session ' +
285+
'before you try to invalidate it'
286+
)
287+
)
288+
289+
expect(store.getActions()).toContainEqual(invalidateSessionFailed())
290+
})
291+
})
292+
293+
describe('when there is no authenticator', () => {
294+
it('throws error', () => {
295+
const authenticator = createAuthenticator({
296+
name: 'fake'
297+
})
298+
const middleware = configureMiddleware(authenticator)
299+
const mockStore = configureStore([middleware])
300+
const store = mockStore()
301+
const action = invalidateSession()
302+
303+
expect(() => store.dispatch(action)).toThrow(
304+
'No session data to invalidate. Be sure you authenticate the ' +
305+
'session before you try to invalidate it'
306+
)
307+
})
308+
})
309+
})
310+
241311
describe('session restoration', () => {
242312
it('hydrates session data from storage', () => {
243313
const middleware = configureMiddleware()
@@ -387,19 +457,21 @@ describe('auth middleware', () => {
387457
})
388458

389459
describe('when request returns 401 unauthorized', () => {
390-
it('dispatches invalidateSession', async () => {
460+
it('calls authenticators invalidate', async () => {
391461
fetch.mockResponse(JSON.stringify({ ok: true }), { status: 401 })
392-
const middleware = configureMiddleware()
462+
const middleware = configureMiddleware(spiedAuthenticator)
393463
const mockStore = configureStore([middleware])
394464
const data = { token: '1235' }
395-
const store = mockStore({ session: { data, isAuthenticated: true } })
396-
const invalidateAction = invalidateSession()
465+
const store = mockStore({
466+
session: { data, isAuthenticated: true, authenticator: 'test' }
467+
})
397468

398469
await store.dispatch(fetchAction('https://test.com'))
399470

400-
expect(store.getActions()).toEqual(
401-
expect.arrayContaining([invalidateAction])
402-
)
471+
expect(spiedAuthenticator.invalidate).toHaveBeenCalledWith(data)
472+
473+
spiedAuthenticator.authenticate.mockClear()
474+
spiedAuthenticator.invalidate.mockClear()
403475
})
404476

405477
it('does not dispatch if not authenticated', async () => {

packages/redux-simple-auth/test/utils/authenticators.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ import { createAuthenticator } from '../../src'
33
export const spiedAuthenticator = createAuthenticator({
44
name: 'test',
55
authenticate: jest.fn(data => Promise.resolve(data)),
6-
restore: jest.fn(() => Promise.resolve())
6+
restore: jest.fn(() => Promise.resolve()),
7+
invalidate: jest.fn(data => Promise.resolve(data))
78
})
89

910
export const successAuthenticator = createAuthenticator({
1011
name: 'test',
1112
authenticate: () => Promise.resolve({ token: 'abcdefg' }),
12-
restore: () => Promise.resolve()
13+
restore: () => Promise.resolve(),
14+
invalidate: () => Promise.resolve()
1315
})
1416

1517
export const failAuthenticator = createAuthenticator({
1618
name: 'test',
1719
authenticate: () => Promise.reject(),
18-
restore: () => Promise.reject()
20+
restore: () => Promise.reject(),
21+
invalidate: jest.fn(data => Promise.resolve(data))
1922
})

0 commit comments

Comments
 (0)