Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ import AuthHTTPProvider from "@cadolabs/auth-http-provider"
First you need to create a factory

```js
const factory = AuthHTTPProvider.make({ getToken, saveToken, refreshToken, onError })
const factory = AuthHTTPProvider.make({ getAccessToken, saveTokens, refreshTokens, onError })
```

Options:

- `getToken` – `void => Promise<string>` – returns saved auth token
- `saveToken` – `token => Promise<void>` – save refreshed token
- `refreshToken` – `() => Promise<string>` – refresh current token
- `getAccessToken` – `() => Promise<string>` – returns saved access token
- `saveTokens` – `(tokens: { accessToken: string, refreshToken: string }) => Promise<void>` – save tokens
- `refreshTokens` – `() => Promise<{ accessToken: string, refreshToken: string }>` – refresh current tokens
- `onError` – `Error => void` – calls on error (Error object is just a request from `fetch`)

And after that you can create a http provider:
Expand All @@ -63,11 +63,11 @@ Request options:

## How does it work

On each request performing it calls callback `getToken` to get the auth token and makes the request with auth header `Authorization: Bearer <token>`.
On each request performing it calls callback `getAccessToken` to get the auth token and makes the request with auth header `Authorization: Bearer <token>`.

When any request you made fails with 401 error code, it tries to refresh the token using callback `refreshToken` and perform it one more time with the new token. If it fails again, it calls `onError` callback and throws an error.
When any request you made fails with 401 error code, it tries to refresh the tokens using callback `refreshTokens` and perform it one more time with the new access token. If it fails again, it calls `onError` callback and throws an error.

If request complited successfully with new token, it calls `saveToken` to make your code save it somewhere.
If request complited successfully with new token, it calls `saveTokens` to make your code save new tokens somewhere.

In other cases it behaves like a regular request-performing library.

Expand Down
16 changes: 11 additions & 5 deletions src/Provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,24 @@ export default class Provider {
})

#request = async request => {
const token = await this.factory.getToken()
const accessToken = await this.factory.getAccessToken()

const response = await this.#perform(request, token)
const response = await this.#perform(request, accessToken)

if (response.ok) return response

if (response.status === 401) {
const newToken = await this.factory.refreshToken()
const newResponse = await this.#perform(request, newToken)
const newTokens = await this.factory.refreshTokens()

if (!newTokens?.accessToken) {
this.factory.onError(response)
throw response
}

const newResponse = await this.#perform(request, newTokens.accessToken)

if (newResponse.ok) {
await this.factory.saveToken(newToken)
await this.factory.saveTokens(newTokens)
return newResponse
}

Expand Down
8 changes: 4 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ class Factory {
return new Factory(params)
}

constructor ({ getToken, saveToken, refreshToken, onError }) {
this.getToken = getToken
this.saveToken = saveToken
this.refreshToken = refreshToken
constructor ({ getAccessToken, saveTokens, refreshTokens, onError }) {
this.getAccessToken = getAccessToken
this.saveTokens = saveTokens
this.refreshTokens = refreshTokens
this.onError = onError
}

Expand Down
64 changes: 46 additions & 18 deletions tests/request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ fetchMock.enableMocks()

const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"]

const getToken = jest.fn(() => Promise.resolve("current-token"))
const saveToken = jest.fn(_token => Promise.resolve())
const refreshToken = jest.fn(() => Promise.resolve("new-token"))
const getAccessToken = jest.fn(() => Promise.resolve("current-token"))
const saveTokens = jest.fn(_tokens => Promise.resolve())
const refreshTokens = jest.fn(() => Promise.resolve({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
}))
const onError = jest.fn(_error => Promise.resolve())

const createProvider = () => {
const createProvider = params => {
return Provider
.make({ getToken, saveToken, refreshToken, onError })
.make({ getAccessToken, saveTokens, refreshTokens, onError, ...params })
.create({ baseURL: "http://localhost" })
}

Expand Down Expand Up @@ -57,7 +60,7 @@ describe("making requests", () => {

expect(response.status).toEqual(200)
expect(response.json()).resolves.toEqual({ success: true })
expect(getToken).toHaveBeenCalled()
expect(getAccessToken).toHaveBeenCalled()
expect(fetch).toHaveBeenCalledWith(expectedUrl, {
method,
body: expectedBody,
Expand All @@ -79,7 +82,7 @@ describe("making requests", () => {

expect(response.status).toEqual(200)
expect(response.json()).resolves.toEqual({ success: true })
expect(getToken).toHaveBeenCalled()
expect(getAccessToken).toHaveBeenCalled()
expect(fetch).toHaveBeenCalledWith("http://localhost/route", {
method,
body: expect.any(FormData),
Expand All @@ -103,13 +106,16 @@ describe("refreshing token", () => {

expect(response.status).toEqual(200)
expect(response.json()).resolves.toEqual({ success: true })
expect(getToken).toHaveBeenCalled()
expect(saveToken).toHaveBeenCalledWith("new-token")
expect(refreshToken).toHaveBeenCalled()
expect(getAccessToken).toHaveBeenCalled()
expect(saveTokens).toHaveBeenCalledWith({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
})
expect(refreshTokens).toHaveBeenCalled()

expect(fetchMock).toHaveBeenCalledTimes(2)
expect(fetchMock).toHaveBeenNthCalledWith(1, ...makeCallingMatcher("current-token"))
expect(fetchMock).toHaveBeenNthCalledWith(2, ...makeCallingMatcher("new-token"))
expect(fetchMock).toHaveBeenNthCalledWith(2, ...makeCallingMatcher("new-access-token"))
})
})
})
Expand All @@ -128,14 +134,36 @@ describe("errors", () => {
expect(e.status).toEqual(401)
}

expect(getToken).toHaveBeenCalled()
expect(refreshToken).toHaveBeenCalled()
expect(saveToken).not.toHaveBeenCalled()
expect(getAccessToken).toHaveBeenCalled()
expect(refreshTokens).toHaveBeenCalled()
expect(saveTokens).not.toHaveBeenCalled()
expect(onError).toHaveBeenCalled()

expect(fetchMock).toHaveBeenCalledTimes(2)
expect(fetchMock).toHaveBeenNthCalledWith(1, ...makeCallingMatcher("current-token"))
expect(fetchMock).toHaveBeenNthCalledWith(2, ...makeCallingMatcher("new-token"))
expect(fetchMock).toHaveBeenNthCalledWith(2, ...makeCallingMatcher("new-access-token"))
})

it(`${method} | no re-request occurs if the token is not received`, async () => {
fetchMock.mockResponse("", { status: 401 })

const refreshTokensReturnedNothing = jest.fn(() => Promise.resolve())
const provider = createProvider({ refreshTokens: refreshTokensReturnedNothing })

try {
await provider.get("/route")
}
catch (e) {
expect(e.status).toEqual(401)
}

expect(getAccessToken).toHaveBeenCalled()
expect(refreshTokensReturnedNothing).toHaveBeenCalled()
expect(saveTokens).not.toHaveBeenCalled()
expect(onError).toHaveBeenCalled()

expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenNthCalledWith(1, ...makeCallingMatcher("current-token"))
})

it(`${method} | throws an error on non-401 statuses`, async () => {
Expand All @@ -150,9 +178,9 @@ describe("errors", () => {
expect(e.status).toEqual(500)
}

expect(getToken).toHaveBeenCalled()
expect(refreshToken).not.toHaveBeenCalled()
expect(saveToken).not.toHaveBeenCalled()
expect(getAccessToken).toHaveBeenCalled()
expect(refreshTokens).not.toHaveBeenCalled()
expect(saveTokens).not.toHaveBeenCalled()
expect(onError).not.toHaveBeenCalled()

expect(fetchMock).toHaveBeenCalledTimes(1)
Expand Down