Skip to content

Commit 23c1bae

Browse files
authored
feat(error): handle network connection errors (#1030)
1 parent 96058b1 commit 23c1bae

File tree

8 files changed

+259
-80
lines changed

8 files changed

+259
-80
lines changed

src/components/Oops.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('components/Oops.tsx', () => {
66
it('should render itself & its children', () => {
77
const mockError = {
88
title: 'Error title',
9-
description: 'Error description',
9+
descriptions: ['Error description'],
1010
emojis: ['🔥'],
1111
};
1212
const tree = TestRenderer.create(<Oops error={mockError} />);

src/components/Oops.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ export const Oops: FC<IProps> = ({ error }) => {
1919
<h2 className="font-semibold text-xl mb-2 text-semibold">
2020
{error.title}
2121
</h2>
22-
<div>{error.description}</div>
22+
{error.descriptions.map((description, i) => {
23+
return (
24+
// biome-ignore lint/suspicious/noArrayIndexKey: using index for key to keep the error constants clean
25+
<div className="text-center mb-2" key={`error_description_${i}`}>
26+
{description}
27+
</div>
28+
);
29+
})}
2330
</div>
2431
);
2532
};

src/components/__snapshots__/Oops.test.tsx.snap

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/hooks/useNotifications.test.ts

+207-66
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { act, renderHook, waitFor } from '@testing-library/react';
2-
import axios from 'axios';
2+
import axios, { AxiosError } from 'axios';
33
import nock from 'nock';
44

55
import { mockAccounts, mockSettings } from '../__mocks__/mock-state';
@@ -50,17 +50,20 @@ describe('hooks/useNotifications.ts', () => {
5050
});
5151

5252
describe('should fetch notifications with failures - github.com & enterprise', () => {
53-
it('bad credentials', async () => {
54-
const status = 401;
55-
const message = 'Bad credentials';
53+
it('network error', async () => {
54+
const code = AxiosError.ERR_NETWORK;
5655

5756
nock('https://api.github.com/')
5857
.get('/notifications?participating=false')
59-
.reply(status, { message });
58+
.replyWithError({
59+
code: code,
60+
});
6061

6162
nock('https://github.gitify.io/api/v3/')
6263
.get('/notifications?participating=false')
63-
.reply(status, { message });
64+
.replyWithError({
65+
code: code,
66+
});
6467

6568
const { result } = renderHook(() => useNotifications());
6669

@@ -70,107 +73,245 @@ describe('hooks/useNotifications.ts', () => {
7073

7174
await waitFor(() => {
7275
expect(result.current.requestFailed).toBe(true);
73-
expect(result.current.errorDetails).toBe(Errors.BAD_CREDENTIALS);
76+
expect(result.current.errorDetails).toBe(Errors.NETWORK);
7477
});
7578
});
7679

77-
it('missing scopes', async () => {
78-
const status = 403;
79-
const message = "Missing the 'notifications' scope";
80-
81-
nock('https://api.github.com/')
82-
.get('/notifications?participating=false')
83-
.reply(status, { message });
80+
describe('bad request errors', () => {
81+
it('bad credentials', async () => {
82+
const code = AxiosError.ERR_BAD_REQUEST;
83+
const status = 401;
84+
const message = 'Bad credentials';
85+
86+
nock('https://api.github.com/')
87+
.get('/notifications?participating=false')
88+
.replyWithError({
89+
code,
90+
response: {
91+
status,
92+
data: {
93+
message,
94+
},
95+
},
96+
});
97+
98+
nock('https://github.gitify.io/api/v3/')
99+
.get('/notifications?participating=false')
100+
.replyWithError({
101+
code,
102+
response: {
103+
status,
104+
data: {
105+
message,
106+
},
107+
},
108+
});
84109

85-
nock('https://github.gitify.io/api/v3/')
86-
.get('/notifications?participating=false')
87-
.reply(status, { message });
110+
const { result } = renderHook(() => useNotifications());
88111

89-
const { result } = renderHook(() => useNotifications());
112+
act(() => {
113+
result.current.fetchNotifications(mockAccounts, mockSettings);
114+
});
90115

91-
act(() => {
92-
result.current.fetchNotifications(mockAccounts, mockSettings);
116+
await waitFor(() => {
117+
expect(result.current.requestFailed).toBe(true);
118+
expect(result.current.errorDetails).toBe(Errors.BAD_CREDENTIALS);
119+
});
93120
});
94121

95-
await waitFor(() => {
96-
expect(result.current.requestFailed).toBe(true);
97-
expect(result.current.errorDetails).toBe(Errors.MISSING_SCOPES);
98-
});
99-
});
122+
it('missing scopes', async () => {
123+
const code = AxiosError.ERR_BAD_REQUEST;
124+
const status = 403;
125+
const message = "Missing the 'notifications' scope";
126+
127+
nock('https://api.github.com/')
128+
.get('/notifications?participating=false')
129+
.replyWithError({
130+
code,
131+
response: {
132+
status,
133+
data: {
134+
message,
135+
},
136+
},
137+
});
138+
139+
nock('https://github.gitify.io/api/v3/')
140+
.get('/notifications?participating=false')
141+
.replyWithError({
142+
code,
143+
response: {
144+
status,
145+
data: {
146+
message,
147+
},
148+
},
149+
});
100150

101-
it('rate limited - primary', async () => {
102-
const status = 403;
103-
const message = 'API rate limit exceeded';
151+
const { result } = renderHook(() => useNotifications());
104152

105-
nock('https://api.github.com/')
106-
.get('/notifications?participating=false')
107-
.reply(status, { message });
153+
act(() => {
154+
result.current.fetchNotifications(mockAccounts, mockSettings);
155+
});
108156

109-
nock('https://github.gitify.io/api/v3/')
110-
.get('/notifications?participating=false')
111-
.reply(status, { message });
157+
await waitFor(() => {
158+
expect(result.current.requestFailed).toBe(true);
159+
expect(result.current.errorDetails).toBe(Errors.MISSING_SCOPES);
160+
});
161+
});
112162

113-
const { result } = renderHook(() => useNotifications());
163+
it('rate limited - primary', async () => {
164+
const code = AxiosError.ERR_BAD_REQUEST;
165+
const status = 403;
166+
const message = 'API rate limit exceeded';
167+
168+
nock('https://api.github.com/')
169+
.get('/notifications?participating=false')
170+
.replyWithError({
171+
code,
172+
response: {
173+
status,
174+
data: {
175+
message,
176+
},
177+
},
178+
});
179+
180+
nock('https://github.gitify.io/api/v3/')
181+
.get('/notifications?participating=false')
182+
.replyWithError({
183+
code,
184+
response: {
185+
status,
186+
data: {
187+
message,
188+
},
189+
},
190+
});
114191

115-
act(() => {
116-
result.current.fetchNotifications(mockAccounts, mockSettings);
192+
const { result } = renderHook(() => useNotifications());
193+
194+
act(() => {
195+
result.current.fetchNotifications(mockAccounts, mockSettings);
196+
});
197+
198+
await waitFor(() => {
199+
expect(result.current.requestFailed).toBe(true);
200+
expect(result.current.errorDetails).toBe(Errors.RATE_LIMITED);
201+
});
117202
});
118203

119-
await waitFor(() => {
120-
expect(result.current.requestFailed).toBe(true);
121-
expect(result.current.errorDetails).toBe(Errors.RATE_LIMITED);
204+
it('rate limited - secondary', async () => {
205+
const code = AxiosError.ERR_BAD_REQUEST;
206+
const status = 403;
207+
const message = 'You have exceeded a secondary rate limit';
208+
209+
nock('https://api.github.com/')
210+
.get('/notifications?participating=false')
211+
.replyWithError({
212+
code,
213+
response: {
214+
status,
215+
data: {
216+
message,
217+
},
218+
},
219+
});
220+
221+
nock('https://github.gitify.io/api/v3/')
222+
.get('/notifications?participating=false')
223+
.replyWithError({
224+
code,
225+
response: {
226+
status,
227+
data: {
228+
message,
229+
},
230+
},
231+
});
232+
233+
const { result } = renderHook(() => useNotifications());
234+
235+
act(() => {
236+
result.current.fetchNotifications(mockAccounts, mockSettings);
237+
});
238+
239+
await waitFor(() => {
240+
expect(result.current.requestFailed).toBe(true);
241+
expect(result.current.errorDetails).toBe(Errors.RATE_LIMITED);
242+
});
122243
});
123-
});
124244

125-
it('rate limited - secondary', async () => {
126-
const status = 403;
127-
const message = 'You have exceeded a secondary rate limit';
245+
it('unhandled bad request error', async () => {
246+
const code = AxiosError.ERR_BAD_REQUEST;
247+
const status = 400;
248+
const message = 'Oops! Something went wrong.';
249+
250+
nock('https://api.github.com/')
251+
.get('/notifications?participating=false')
252+
.replyWithError({
253+
code,
254+
response: {
255+
status,
256+
data: {
257+
message,
258+
},
259+
},
260+
});
261+
262+
nock('https://github.gitify.io/api/v3/')
263+
.get('/notifications?participating=false')
264+
.replyWithError({
265+
code,
266+
response: {
267+
status,
268+
data: {
269+
message,
270+
},
271+
},
272+
});
128273

129-
nock('https://api.github.com/')
130-
.get('/notifications?participating=false')
131-
.reply(status, { message });
274+
const { result } = renderHook(() => useNotifications());
132275

133-
nock('https://github.gitify.io/api/v3/')
134-
.get('/notifications?participating=false')
135-
.reply(status, { message });
276+
act(() => {
277+
result.current.fetchNotifications(mockAccounts, mockSettings);
278+
});
136279

137-
const { result } = renderHook(() => useNotifications());
280+
expect(result.current.isFetching).toBe(true);
138281

139-
act(() => {
140-
result.current.fetchNotifications(mockAccounts, mockSettings);
141-
});
282+
await waitFor(() => {
283+
expect(result.current.isFetching).toBe(false);
284+
});
142285

143-
await waitFor(() => {
144286
expect(result.current.requestFailed).toBe(true);
145-
expect(result.current.errorDetails).toBe(Errors.RATE_LIMITED);
146287
});
147288
});
148289

149-
it('default error', async () => {
150-
const status = 400;
151-
const message = 'Oops! Something went wrong.';
290+
it('unknown error', async () => {
291+
const code = 'anything';
152292

153293
nock('https://api.github.com/')
154294
.get('/notifications?participating=false')
155-
.reply(status, { message });
295+
.replyWithError({
296+
code: code,
297+
});
156298

157299
nock('https://github.gitify.io/api/v3/')
158300
.get('/notifications?participating=false')
159-
.reply(status, { message });
301+
.replyWithError({
302+
code: code,
303+
});
160304

161305
const { result } = renderHook(() => useNotifications());
162306

163307
act(() => {
164308
result.current.fetchNotifications(mockAccounts, mockSettings);
165309
});
166310

167-
expect(result.current.isFetching).toBe(true);
168-
169311
await waitFor(() => {
170-
expect(result.current.isFetching).toBe(false);
312+
expect(result.current.requestFailed).toBe(true);
313+
expect(result.current.errorDetails).toBe(Errors.UNKNOWN);
171314
});
172-
173-
expect(result.current.requestFailed).toBe(true);
174315
});
175316
});
176317
});

src/hooks/useNotifications.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios, { type AxiosError, type AxiosPromise } from 'axios';
1+
import axios, { AxiosError, type AxiosPromise } from 'axios';
22
import { useCallback, useState } from 'react';
33

44
import type {
@@ -405,6 +405,16 @@ export const useNotifications = (): NotificationsState => {
405405
};
406406

407407
function determineFailureType(err: AxiosError<GithubRESTError>): GitifyError {
408+
const code = err.code;
409+
410+
if (code === AxiosError.ERR_NETWORK) {
411+
return Errors.NETWORK;
412+
}
413+
414+
if (code !== AxiosError.ERR_BAD_REQUEST) {
415+
return Errors.UNKNOWN;
416+
}
417+
408418
const status = err.response.status;
409419
const message = err.response.data.message;
410420

0 commit comments

Comments
 (0)