forked from OctoLinker/OctoLinker
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add rate limit notification component (OctoLinker#541)
* Add rate limit notification component * Update packages/ratelimit-notification/__tests__/__snapshots__/messages.js.snap Co-Authored-By: stefanbuck <[email protected]> * Update packages/ratelimit-notification/__tests__/__snapshots__/messages.js.snap Co-Authored-By: stefanbuck <[email protected]> * Update packages/ratelimit-notification/messages.js Co-Authored-By: stefanbuck <[email protected]> * Update packages/ratelimit-notification/messages.js Co-Authored-By: stefanbuck <[email protected]>
- Loading branch information
1 parent
f575e2d
commit 9ba8c38
Showing
9 changed files
with
327 additions
and
2 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
packages/ratelimit-notification/__tests__/__snapshots__/messages.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`ratelimit-notification needsTokenForPrivate 1`] = ` | ||
Object { | ||
"body": "OctoLinker needs a GitHub API token to retrieve repository metadata for private repositories. <a href=\\"#\\" class=\\"js-octolinker-open-settings\\">Create a token</a> to enable OctoLinker for all your private repositories.", | ||
"type": "info", | ||
} | ||
`; | ||
|
||
exports[`ratelimit-notification rateLimitExceeded for authenticated requests 1`] = ` | ||
Object { | ||
"body": "OctoLinker exceed the GitHub API hourly limit. The rate limit will be reset in 5 seconds.", | ||
"type": "warn", | ||
} | ||
`; | ||
|
||
exports[`ratelimit-notification rateLimitExceeded for unauthenticated requests 1`] = ` | ||
Object { | ||
"body": "OctoLinker exceed the GitHub API hourly limit for unauthenticated requests. You probably want to <a href=\\"#\\" class=\\"js-octolinker-open-settings\\">create a token</a> or wait 5 minutes.", | ||
"type": "warn", | ||
} | ||
`; | ||
|
||
exports[`ratelimit-notification tokenIsInvalid 1`] = ` | ||
Object { | ||
"body": "The token you provided is invalid. You must <a href=\\"#\\" class=\\"js-octolinker-open-settings\\">create a new token</a> before you can continue using OctoLinker.", | ||
"type": "error", | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { | ||
removeAllNotifications, | ||
isPrivateRepository, | ||
} from '@octolinker/user-interface'; | ||
import ratelimitNotification from '../index'; | ||
import { | ||
tokenIsInvalid, | ||
needsTokenForPrivate, | ||
rateLimitExceeded, | ||
} from '../messages'; | ||
import { parse } from '../parse-github-header'; | ||
|
||
jest.mock('../parse-github-header', () => ({ | ||
parse: jest.fn(), | ||
})); | ||
|
||
jest.mock('../messages'); | ||
|
||
jest.mock('@octolinker/user-interface'); | ||
|
||
parse.mockImplementation(() => ({ | ||
rateLimitTotal: 10, | ||
rateLimitRemaining: 20, | ||
rateLimitReset: 30000, | ||
})); | ||
|
||
describe('ratelimitNotification', () => { | ||
it('removes all notifications', () => { | ||
ratelimitNotification(); | ||
expect(removeAllNotifications).toHaveBeenCalled(); | ||
}); | ||
|
||
it('parse github header', () => { | ||
ratelimitNotification('fakeHeader'); | ||
expect(parse).toHaveBeenCalledWith('fakeHeader'); | ||
}); | ||
|
||
describe('when status code is 401', () => { | ||
it('shows invalid token message', () => { | ||
ratelimitNotification('fakeHeader', 401); | ||
|
||
expect(tokenIsInvalid).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('when status code is 404', () => { | ||
it('shows needs private token message when repository is private', () => { | ||
isPrivateRepository.mockImplementation(() => true); | ||
ratelimitNotification('fakeHeader', 404); | ||
|
||
expect(needsTokenForPrivate).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('when status code is 403', () => { | ||
describe('when requests is unauthenticated', () => { | ||
it('shows needs private token message when repository is private', () => { | ||
isPrivateRepository.mockImplementation(() => true); | ||
parse.mockImplementation(() => ({ | ||
rateLimitTotal: 60, | ||
})); | ||
|
||
ratelimitNotification('fakeHeader', 403); | ||
|
||
expect(needsTokenForPrivate).toHaveBeenCalled(); | ||
}); | ||
|
||
it('shows rate limit exceeded message', () => { | ||
isPrivateRepository.mockImplementation(() => false); | ||
parse.mockImplementation(() => ({ | ||
rateLimitTotal: 60, | ||
rateLimitRemaining: 0, | ||
})); | ||
ratelimitNotification('fakeHeader', 403); | ||
|
||
expect(rateLimitExceeded).toHaveBeenCalled(); | ||
expect(rateLimitExceeded).toHaveBeenCalledWith({ | ||
isUnauthenticated: true, | ||
remainingTime: expect.any(Number), | ||
}); | ||
}); | ||
}); | ||
|
||
describe('when requests is authenticated', () => { | ||
it('shows rate limit exceeded message', () => { | ||
isPrivateRepository.mockImplementation(() => false); | ||
parse.mockImplementation(() => ({ | ||
rateLimitTotal: 5000, | ||
rateLimitRemaining: 0, | ||
rateLimitReset: 30000, | ||
})); | ||
|
||
ratelimitNotification('fakeHeader', 403); | ||
|
||
expect(rateLimitExceeded).toHaveBeenCalled(); | ||
expect(rateLimitExceeded).toHaveBeenCalledWith({ | ||
isUnauthenticated: false, | ||
remainingTime: expect.any(Number), | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { showNotification } from '@octolinker/user-interface'; | ||
import { | ||
tokenIsInvalid, | ||
needsTokenForPrivate, | ||
rateLimitExceeded, | ||
} from '../messages'; | ||
|
||
jest.mock('@octolinker/user-interface'); | ||
|
||
describe('ratelimit-notification', () => { | ||
it('tokenIsInvalid', () => { | ||
const showNotificationStub = jest.fn(); | ||
showNotification.mockImplementation(showNotificationStub); | ||
|
||
tokenIsInvalid(); | ||
expect(showNotificationStub.mock.calls[0][0]).toMatchSnapshot(); | ||
}); | ||
it('needsTokenForPrivate', () => { | ||
const showNotificationStub = jest.fn(); | ||
showNotification.mockImplementation(showNotificationStub); | ||
|
||
needsTokenForPrivate(); | ||
expect(showNotificationStub.mock.calls[0][0]).toMatchSnapshot(); | ||
}); | ||
|
||
describe('rateLimitExceeded', () => { | ||
it('for authenticated requests', () => { | ||
const showNotificationStub = jest.fn(); | ||
showNotification.mockImplementation(showNotificationStub); | ||
|
||
rateLimitExceeded({ isUnauthenticated: false, remainingTime: 5000 }); | ||
expect(showNotificationStub.mock.calls[0][0]).toMatchSnapshot(); | ||
}); | ||
|
||
it('for unauthenticated requests', () => { | ||
const showNotificationStub = jest.fn(); | ||
showNotification.mockImplementation(showNotificationStub); | ||
|
||
rateLimitExceeded({ isUnauthenticated: true, remainingTime: 300000 }); | ||
expect(showNotificationStub.mock.calls[0][0]).toMatchSnapshot(); | ||
}); | ||
}); | ||
}); |
17 changes: 17 additions & 0 deletions
17
packages/ratelimit-notification/__tests__/parse-github-header.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { parse } from '../parse-github-header'; | ||
|
||
describe('parse-github-header', () => { | ||
it('parse', () => { | ||
const fakeHeader = new Map([ | ||
['x-ratelimit-limit', '10'], | ||
['x-ratelimit-remaining', '20'], | ||
['x-ratelimit-reset', '30'], | ||
]); | ||
|
||
expect(parse(fakeHeader)).toEqual({ | ||
rateLimitTotal: 10, | ||
rateLimitRemaining: 20, | ||
rateLimitReset: 30000, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { | ||
removeAllNotifications, | ||
isPrivateRepository, | ||
} from '@octolinker/user-interface'; | ||
|
||
import { | ||
tokenIsInvalid, | ||
needsTokenForPrivate, | ||
rateLimitExceeded, | ||
} from './messages'; | ||
import { parse } from './parse-github-header'; | ||
|
||
const MAX_UNAUTHENTICATED_REQUESTS = 60; | ||
|
||
document.body.addEventListener('click', event => { | ||
if (event.target.classList.contains('js-octolinker-open-settings')) { | ||
chrome.runtime.sendMessage({ action: 'openSettings' }); | ||
} | ||
}); | ||
|
||
export default (headers, statusCode) => { | ||
const { rateLimitTotal, rateLimitRemaining, rateLimitReset } = parse(headers); | ||
|
||
removeAllNotifications(); | ||
|
||
const isUnauthenticated = rateLimitTotal === MAX_UNAUTHENTICATED_REQUESTS; | ||
const isPrivate = isPrivateRepository(); | ||
|
||
if (statusCode === 401) { | ||
return tokenIsInvalid(); | ||
} | ||
|
||
if (isPrivate && statusCode === 404) { | ||
return needsTokenForPrivate(); | ||
} | ||
|
||
if (statusCode === 403) { | ||
if (isPrivate && isUnauthenticated) { | ||
return needsTokenForPrivate(); | ||
} | ||
const remainingTime = rateLimitReset - new Date().getTime(); | ||
|
||
if (rateLimitRemaining === 0) { | ||
return rateLimitExceeded({ isUnauthenticated, remainingTime }); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { showNotification } from '@octolinker/user-interface'; | ||
|
||
const prettyTime = ms => { | ||
const sec = ms / 1000; | ||
|
||
if (sec > 120) { | ||
return `${Math.round(sec / 60)} minutes`; | ||
} | ||
if (sec > 60) { | ||
return `${Math.round(sec / 60)} minute`; | ||
} | ||
|
||
return `${sec} seconds`; | ||
}; | ||
|
||
export const rateLimitExceeded = ({ isUnauthenticated, remainingTime }) => { | ||
const timeLeft = prettyTime(remainingTime); | ||
|
||
if (isUnauthenticated) { | ||
showNotification({ | ||
type: 'warn', | ||
body: `OctoLinker exceed the GitHub API hourly limit for unauthenticated requests. You probably want to <a href="#" class="js-octolinker-open-settings">create a token</a> or wait ${timeLeft}.`, | ||
}); | ||
return; | ||
} | ||
|
||
showNotification({ | ||
type: 'warn', | ||
body: `OctoLinker exceed the GitHub API hourly limit. The rate limit will be reset in ${timeLeft}.`, | ||
}); | ||
}; | ||
|
||
export const needsTokenForPrivate = () => { | ||
showNotification({ | ||
type: 'info', | ||
body: | ||
'OctoLinker needs a GitHub API token to retrieve repository metadata for private repositories. <a href="#" class="js-octolinker-open-settings">Create a token</a> to enable OctoLinker for all your private repositories.', | ||
}); | ||
}; | ||
|
||
export const tokenIsInvalid = () => { | ||
showNotification({ | ||
type: 'error', | ||
body: | ||
'The token you provided is invalid. You must <a href="#" class="js-octolinker-open-settings">create a new token</a> before you can continue using OctoLinker.', | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"name": "@octolinker/ratelimit-notification", | ||
"version": "1.0.0", | ||
"description": "", | ||
"repository": "https://github.com/octolinker/octolinker/tree/master/packages/ratelimit-notification", | ||
"license": "MIT", | ||
"main": "./index.js", | ||
"dependencies": { | ||
"@octolinker/user-interface": "1.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export const parse = headers => { | ||
const rateLimitTotal = parseInt(headers.get('x-ratelimit-limit'), 10) || 0; | ||
const rateLimitRemaining = | ||
parseInt(headers.get('x-ratelimit-remaining'), 10) || 0; | ||
const rateLimitReset = (headers.get('x-ratelimit-reset') || 0) * 1000; | ||
|
||
return { | ||
rateLimitTotal, | ||
rateLimitRemaining, | ||
rateLimitReset, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters