Skip to content

Commit 586e98a

Browse files
committed
update error handler
Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent 525e6ef commit 586e98a

3 files changed

Lines changed: 219 additions & 146 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,4 @@
157157
"*": "biome check --no-errors-on-unmatched",
158158
"*.{js,ts,tsx}": "pnpm test:changed --passWithNoTests --updateSnapshot"
159159
}
160-
}
160+
}
Lines changed: 183 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,197 @@
1-
// import { RequestError } from '@octokit/request-error';
1+
import { GraphqlResponseError } from '@octokit/graphql';
2+
import { RequestError } from '@octokit/request-error';
23

3-
import type { GraphqlResponseError } from '@octokit/graphql';
4+
import { EVENTS } from '../../../shared/events';
45

5-
import type { DeepPartial } from '../../__helpers__/test-utils';
6+
import { Errors } from '../errors';
7+
import * as rendererLogger from '../logger';
8+
import { determineFailureType, handleGraphQLResponseError } from './errors';
69

7-
// import { EVENTS } from '../../../shared/events';
10+
describe('renderer/utils/api/errors.ts', () => {
11+
describe('determineFailureType', () => {
12+
describe('generic errors', () => {
13+
it('bad credentials - safe storage decryption error', () => {
14+
const mockError = new Error(
15+
`Error invoking remote method '${EVENTS.SAFE_STORAGE_DECRYPT}': Error: Error while decrypting the ciphertext provided to safeStorage.decryptString. Ciphertext does not appear to be encrypted.`,
16+
);
17+
const result = determineFailureType(mockError);
18+
expect(result).toBe(Errors.BAD_CREDENTIALS);
19+
});
820

9-
// import type { Link } from '../../types';
21+
it('unknown error - generic error', () => {
22+
const mockError = new Error('Something went wrong');
23+
const result = determineFailureType(mockError);
24+
expect(result).toBe(Errors.UNKNOWN);
25+
});
26+
});
1027

11-
// import { Errors } from '../errors';
12-
import * as rendererLogger from '../logger';
13-
import { handleGraphQLResponseError } from './errors';
28+
describe('REST API errors (RequestError)', () => {
29+
it('bad credentials - 401 status', () => {
30+
const mockError = new RequestError('Bad credentials', 401, {
31+
request: {
32+
method: 'GET',
33+
url: 'https://api.github.com',
34+
headers: {},
35+
},
36+
});
37+
const result = determineFailureType(mockError);
38+
expect(result).toBe(Errors.BAD_CREDENTIALS);
39+
});
1440

15-
describe('renderer/utils/api/errors.ts', () => {
16-
// describe('determineFailureType', () => {
17-
// it('network error', async () => {
18-
// const mockError: Partial<RequestError> = {
19-
// code: AxiosError.ERR_NETWORK,
20-
// };
21-
// const result = determineFailureType(
22-
// mockError as AxiosError<GitHubRESTError>,
23-
// );
24-
// expect(result).toBe(Errors.NETWORK);
25-
// });
26-
// describe('bad request errors', () => {
27-
// it('bad credentials', async () => {
28-
// const mockError: Partial<AxiosError<GitHubRESTError>> = {
29-
// code: AxiosError.ERR_BAD_REQUEST,
30-
// status: 401,
31-
// response: createMockResponse(401, 'Bad credentials'),
32-
// };
33-
// const result = determineFailureType(
34-
// mockError as AxiosError<GitHubRESTError>,
35-
// );
36-
// expect(result).toBe(Errors.BAD_CREDENTIALS);
37-
// });
38-
// it('missing scopes', async () => {
39-
// const mockError: Partial<AxiosError<GitHubRESTError>> = {
40-
// code: AxiosError.ERR_BAD_REQUEST,
41-
// status: 403,
42-
// response: createMockResponse(
43-
// 403,
44-
// "Missing the 'notifications' scope",
45-
// ),
46-
// };
47-
// const result = determineFailureType(
48-
// mockError as AxiosError<GitHubRESTError>,
49-
// );
50-
// expect(result).toBe(Errors.MISSING_SCOPES);
51-
// });
52-
// it('rate limited - primary', async () => {
53-
// const mockError: Partial<AxiosError<GitHubRESTError>> = {
54-
// code: AxiosError.ERR_BAD_REQUEST,
55-
// status: 403,
56-
// response: createMockResponse(403, 'API rate limit exceeded'),
57-
// };
58-
// const result = determineFailureType(
59-
// mockError as AxiosError<GitHubRESTError>,
60-
// );
61-
// expect(result).toBe(Errors.RATE_LIMITED);
62-
// });
63-
// it('rate limited - secondary', async () => {
64-
// const mockError: Partial<AxiosError<GitHubRESTError>> = {
65-
// code: AxiosError.ERR_BAD_REQUEST,
66-
// status: 403,
67-
// response: createMockResponse(
68-
// 403,
69-
// 'You have exceeded a secondary rate limit',
70-
// ),
71-
// };
72-
// const result = determineFailureType(
73-
// mockError as AxiosError<GitHubRESTError>,
74-
// );
75-
// expect(result).toBe(Errors.RATE_LIMITED);
76-
// });
77-
// it('unhandled bad request error', async () => {
78-
// const mockError: Partial<AxiosError<GitHubRESTError>> = {
79-
// code: AxiosError.ERR_BAD_REQUEST,
80-
// status: 400,
81-
// response: createMockResponse(403, 'Oops! Something went wrong.'),
82-
// };
83-
// const result = determineFailureType(
84-
// mockError as AxiosError<GitHubRESTError>,
85-
// );
86-
// expect(result).toBe(Errors.UNKNOWN);
87-
// });
88-
// });
89-
// it('bad credentials - safe storage', async () => {
90-
// const mockError: Partial<AxiosError<GitHubRESTError>> = {
91-
// message: `Error invoking remote method '${EVENTS.SAFE_STORAGE_DECRYPT}': Error: Error while decrypting the ciphertext provided to safeStorage.decryptString. Ciphertext does not appear to be encrypted.`,
92-
// };
93-
// const result = determineFailureType(
94-
// mockError as AxiosError<GitHubRESTError>,
95-
// );
96-
// expect(result).toBe(Errors.BAD_CREDENTIALS);
97-
// });
98-
// it('unknown error', async () => {
99-
// const mockError: Partial<AxiosError<GitHubRESTError>> = {
100-
// code: 'anything',
101-
// };
102-
// const result = determineFailureType(
103-
// mockError as AxiosError<GitHubRESTError>,
104-
// );
105-
// expect(result).toBe(Errors.UNKNOWN);
106-
// });
107-
// });
108-
});
41+
it('missing scopes - 403 with scope message', () => {
42+
const mockError = new RequestError(
43+
"Missing the 'notifications' scope",
44+
403,
45+
{
46+
request: {
47+
method: 'GET',
48+
url: 'https://api.github.com',
49+
headers: {},
50+
},
51+
},
52+
);
53+
const result = determineFailureType(mockError);
54+
expect(result).toBe(Errors.MISSING_SCOPES);
55+
});
10956

110-
describe('handleGraphQLResponseError', () => {
111-
it('throws and logs when GraphQL errors are present', () => {
112-
const rendererLogErrorSpy = jest
113-
.spyOn(rendererLogger, 'rendererLogError')
114-
.mockImplementation();
57+
it('rate limited - primary rate limit', () => {
58+
const mockError = new RequestError('API rate limit exceeded', 403, {
59+
request: {
60+
method: 'GET',
61+
url: 'https://api.github.com',
62+
headers: {},
63+
},
64+
});
65+
const result = determineFailureType(mockError);
66+
expect(result).toBe(Errors.RATE_LIMITED);
67+
});
11568

116-
const payload: GraphqlResponseError<unknown> = {
117-
data: {},
118-
errors: [
69+
it('rate limited - secondary rate limit', () => {
70+
const mockError = new RequestError(
71+
'You have exceeded a secondary rate limit',
72+
403,
73+
{
74+
request: {
75+
method: 'GET',
76+
url: 'https://api.github.com',
77+
headers: {},
78+
},
79+
},
80+
);
81+
const result = determineFailureType(mockError);
82+
expect(result).toBe(Errors.RATE_LIMITED);
83+
});
84+
85+
it('network error - no status', () => {
86+
const mockError = new RequestError('Network error', 0, {
87+
request: {
88+
method: 'GET',
89+
url: 'https://api.github.com',
90+
headers: {},
91+
},
92+
});
93+
const result = determineFailureType(mockError);
94+
expect(result).toBe(Errors.NETWORK);
95+
});
96+
97+
it('unknown error - unhandled 403', () => {
98+
const mockError = new RequestError('Forbidden', 403, {
99+
request: {
100+
method: 'GET',
101+
url: 'https://api.github.com',
102+
headers: {},
103+
},
104+
});
105+
const result = determineFailureType(mockError);
106+
expect(result).toBe(Errors.UNKNOWN);
107+
});
108+
});
109+
110+
describe('GraphQL API errors (GraphqlResponseError)', () => {
111+
it('bad credentials', () => {
112+
const mockError = createGraphQLResponseError('Bad credentials');
113+
const result = determineFailureType(mockError);
114+
expect(result).toBe(Errors.BAD_CREDENTIALS);
115+
});
116+
117+
it('rate limited - primary rate limit', () => {
118+
const mockError = createGraphQLResponseError('API rate limit exceeded');
119+
const result = determineFailureType(mockError);
120+
expect(result).toBe(Errors.RATE_LIMITED);
121+
});
122+
123+
it('rate limited - secondary rate limit', () => {
124+
const mockError = createGraphQLResponseError(
125+
'You have exceeded a secondary rate limit',
126+
);
127+
const result = determineFailureType(mockError);
128+
expect(result).toBe(Errors.RATE_LIMITED);
129+
});
130+
131+
it('unknown error', () => {
132+
const mockError = createGraphQLResponseError('Something went wrong');
133+
const result = determineFailureType(mockError);
134+
expect(result).toBe(Errors.UNKNOWN);
135+
});
136+
});
137+
});
138+
139+
describe('handleGraphQLResponseError', () => {
140+
it('throws and logs when GraphQL errors are present', () => {
141+
const rendererLogErrorSpy = jest
142+
.spyOn(rendererLogger, 'rendererLogError')
143+
.mockImplementation();
144+
145+
const payload = createGraphQLResponseError('Something went wrong');
146+
147+
expect(() => handleGraphQLResponseError('test-context', payload)).toThrow(
148+
'GraphQL request returned errors: Something went wrong',
149+
);
150+
151+
expect(rendererLogErrorSpy).toHaveBeenCalledWith(
152+
'test-context',
153+
'GraphQL errors present in response',
154+
expect.any(Error),
155+
);
156+
});
157+
158+
it('throws with multiple error messages joined', () => {
159+
const payload = new GraphqlResponseError(
119160
{
120-
message: 'Something went wrong',
121-
locations: [{ line: 1, column: 1 }],
161+
method: 'POST',
162+
url: 'https://api.github.com/graphql',
163+
headers: {},
122164
},
123-
],
124-
} as DeepPartial<
125-
GraphqlResponseError<unknown>
126-
> as GraphqlResponseError<unknown>;
127-
128-
expect(() => handleGraphQLResponseError('test-context', payload)).toThrow(
129-
'GraphQL request returned errors',
130-
);
165+
{},
166+
{
167+
data: {},
168+
errors: [
169+
{ message: 'Error 1' },
170+
{ message: 'Error 2' },
171+
] as unknown as GraphqlResponseError<unknown>['errors'],
172+
},
173+
);
131174

132-
expect(rendererLogErrorSpy).toHaveBeenCalled();
175+
expect(() => handleGraphQLResponseError('test-context', payload)).toThrow(
176+
'GraphQL request returned errors: Error 1; Error 2',
177+
);
178+
});
133179
});
134180
});
135181

136-
// function createMockResponse(
137-
// status: number,
138-
// message: string,
139-
// ): AxiosResponse<GitHubRESTError> {
140-
// return {
141-
// data: {
142-
// message,
143-
// documentation_url: 'https://some-url.com' as Link,
144-
// },
145-
// status,
146-
// statusText: 'Some status text',
147-
// headers: {},
148-
// config: {
149-
// headers: undefined,
150-
// },
151-
// };
152-
// }
182+
function createGraphQLResponseError(
183+
message: string,
184+
): GraphqlResponseError<unknown> {
185+
return new GraphqlResponseError(
186+
{
187+
method: 'POST',
188+
url: 'https://api.github.com/graphql',
189+
headers: {},
190+
},
191+
{},
192+
{
193+
data: {},
194+
errors: [{ message }] as GraphqlResponseError<unknown>['errors'],
195+
},
196+
);
197+
}

0 commit comments

Comments
 (0)