-
-
Notifications
You must be signed in to change notification settings - Fork 639
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Time out initial fetch and go to account-picker screen. #4754
Changes from all commits
424d1c9
2436ac2
a19108c
94b7444
bf28c95
93ec9d8
2db5f9d
e76252d
7664460
7ed53a2
3ca092f
06f07c0
ab62f73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,9 +19,9 @@ import type { ServerMessage } from '../../api/messages/getMessages'; | |
import { streamNarrow, HOME_NARROW, HOME_NARROW_STR, keyFromNarrow } from '../../utils/narrow'; | ||
import { GravatarURL } from '../../utils/avatar'; | ||
import * as eg from '../../__tests__/lib/exampleData'; | ||
import { ApiError, Server5xxError } from '../../api/apiErrors'; | ||
import { Server5xxError, NetworkError } from '../../api/apiErrors'; | ||
import { fakeSleep } from '../../__tests__/lib/fakeTimers'; | ||
import { BackoffMachine } from '../../utils/async'; | ||
import { TimeoutError, BackoffMachine } from '../../utils/async'; | ||
import * as logging from '../../utils/logging'; | ||
|
||
const mockStore = configureStore([thunk]); | ||
|
@@ -104,65 +104,129 @@ describe('fetchActions', () => { | |
jest.clearAllTimers(); | ||
}); | ||
|
||
test('resolves any promise, if there is no exception', async () => { | ||
const tryFetchFunc = jest.fn(async () => { | ||
await fakeSleep(10); | ||
return 'hello'; | ||
}); | ||
describe.each([false, true])( | ||
'whether or not asked to retry; was asked this time? %s', | ||
withRetry => { | ||
test('resolves any promise, if there is no exception', async () => { | ||
const tryFetchFunc = jest.fn(async () => { | ||
await fakeSleep(10); | ||
return 'hello'; | ||
}); | ||
|
||
await expect(tryFetch(tryFetchFunc)).resolves.toBe('hello'); | ||
await expect(tryFetch(tryFetchFunc, withRetry)).resolves.toBe('hello'); | ||
|
||
expect(tryFetchFunc).toHaveBeenCalledTimes(1); | ||
await expect(tryFetchFunc.mock.results[0].value).resolves.toBe('hello'); | ||
expect(tryFetchFunc).toHaveBeenCalledTimes(1); | ||
await expect(tryFetchFunc.mock.results[0].value).resolves.toBe('hello'); | ||
|
||
jest.runAllTimers(); | ||
}); | ||
jest.runAllTimers(); | ||
}); | ||
|
||
// TODO: test more errors, like regular `new Error()`s. Unexpected | ||
// errors should actually cause the retry loop to break; we'll fix | ||
// that soon. | ||
test('retries a call if there is a non-client error', async () => { | ||
const serverError = new Server5xxError(500); | ||
|
||
// fail on first call, succeed second time | ||
let callCount = 0; | ||
const thrower = jest.fn(() => { | ||
callCount++; | ||
if (callCount === 1) { | ||
throw serverError; | ||
} | ||
return 'hello'; | ||
}); | ||
test('Rethrows an unexpected error without retrying', async () => { | ||
const unexpectedError = new Error('You have displaced the mirth.'); | ||
|
||
const func = jest.fn(async () => { | ||
throw unexpectedError; | ||
}); | ||
|
||
await expect(tryFetch(func, withRetry)).rejects.toThrow(unexpectedError); | ||
expect(func).toHaveBeenCalledTimes(1); | ||
|
||
jest.runAllTimers(); | ||
}); | ||
|
||
test('times out after hanging on one request', async () => { | ||
const tryFetchPromise = tryFetch(async () => { | ||
await new Promise((resolve, reject) => {}); | ||
}, withRetry); | ||
|
||
await fakeSleep(60000); | ||
return expect(tryFetchPromise).rejects.toThrow(TimeoutError); | ||
}); | ||
}, | ||
); | ||
|
||
describe('if asked to retry', () => { | ||
test('retries a call if there is a recoverable error', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I appreciate your careful use of both an en-dash and a hyphen. Wouldn't want a reader to think we're talking about errors that are retryable in a non-known fashion :-P There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wish I still had access to the Chicago Manual of Style; it looks like this would be covered in Chapter 6: https://www.chicagomanualofstyle.org/16/ch06/ch06_toc.html 😛 (but I no longer have my college-sponsored subscription, alas). |
||
const serverError = new Server5xxError(500); | ||
|
||
// fail on first call, succeed second time | ||
let callCount = 0; | ||
const thrower = jest.fn(() => { | ||
callCount++; | ||
if (callCount === 1) { | ||
throw serverError; | ||
} | ||
return 'hello'; | ||
}); | ||
|
||
const tryFetchFunc = jest.fn(async () => { | ||
await fakeSleep(10); | ||
return thrower(); | ||
}); | ||
|
||
await expect(tryFetch(tryFetchFunc, true)).resolves.toBe('hello'); | ||
|
||
expect(tryFetchFunc).toHaveBeenCalledTimes(2); | ||
await expect(tryFetchFunc.mock.results[0].value).rejects.toThrow(serverError); | ||
await expect(tryFetchFunc.mock.results[1].value).resolves.toBe('hello'); | ||
|
||
const tryFetchFunc = jest.fn(async () => { | ||
await fakeSleep(10); | ||
return thrower(); | ||
jest.runAllTimers(); | ||
}); | ||
|
||
await expect(tryFetch(tryFetchFunc)).resolves.toBe('hello'); | ||
test('retries a call if there is a network error', async () => { | ||
const networkError = new NetworkError(); | ||
|
||
// fail on first call, succeed second time | ||
let callCount = 0; | ||
const thrower = jest.fn(() => { | ||
callCount++; | ||
if (callCount === 1) { | ||
throw networkError; | ||
} | ||
return 'hello'; | ||
}); | ||
|
||
expect(tryFetchFunc).toHaveBeenCalledTimes(2); | ||
await expect(tryFetchFunc.mock.results[0].value).rejects.toThrow(serverError); | ||
await expect(tryFetchFunc.mock.results[1].value).resolves.toBe('hello'); | ||
const tryFetchFunc = jest.fn(async () => { | ||
await fakeSleep(10); | ||
return thrower(); | ||
}); | ||
|
||
jest.runAllTimers(); | ||
}); | ||
await expect(tryFetch(tryFetchFunc)).resolves.toBe('hello'); | ||
|
||
test('Rethrows a 4xx error without retrying', async () => { | ||
const apiError = new ApiError(400, { | ||
code: 'BAD_REQUEST', | ||
msg: 'Bad Request', | ||
result: 'error', | ||
expect(tryFetchFunc).toHaveBeenCalledTimes(2); | ||
await expect(tryFetchFunc.mock.results[0].value).rejects.toThrow(networkError); | ||
await expect(tryFetchFunc.mock.results[1].value).resolves.toBe('hello'); | ||
|
||
jest.runAllTimers(); | ||
}); | ||
|
||
const func = jest.fn(async () => { | ||
throw apiError; | ||
test('times out after many short-duration 5xx errors', async () => { | ||
const func = jest.fn(async () => { | ||
await fakeSleep(50); | ||
throw new Server5xxError(500); | ||
}); | ||
|
||
await expect(tryFetch(func, true)).rejects.toThrow(TimeoutError); | ||
|
||
expect(func.mock.calls.length).toBeGreaterThan(50); | ||
}); | ||
}); | ||
|
||
await expect(tryFetch(func)).rejects.toThrow(apiError); | ||
expect(func).toHaveBeenCalledTimes(1); | ||
describe('if not asked to retry', () => { | ||
test('does not retry a call if there is a server error', async () => { | ||
const serverError = new Server5xxError(500); | ||
|
||
jest.runAllTimers(); | ||
const tryFetchFunc = jest.fn(async () => { | ||
await fakeSleep(10); | ||
throw serverError; | ||
}); | ||
|
||
await expect(tryFetch(tryFetchFunc, false)).rejects.toThrow(serverError); | ||
|
||
expect(tryFetchFunc).toHaveBeenCalledTimes(1); | ||
|
||
jest.runAllTimers(); | ||
}); | ||
}); | ||
}); | ||
|
||
|
@@ -245,6 +309,12 @@ describe('fetchActions', () => { | |
}); | ||
|
||
describe('failure', () => { | ||
beforeAll(() => { | ||
// suppress `logging.warn` output | ||
// $FlowFixMe[prop-missing]: Jest mock | ||
logging.warn.mockReturnValue(); | ||
}); | ||
|
||
test('rejects when user is not logged in, dispatches MESSAGE_FETCH_ERROR', async () => { | ||
const stateWithoutAccount = { | ||
...baseState, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably simpler as a
… ? … : …
expression, no?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I used
if
/else
because I thought there might be other cases to consider in the future, and this form would make it easier to expand on.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Yeah, that seems like a fine reason.