From b05ad1f6f74d6860060609f69dfc169897b25a15 Mon Sep 17 00:00:00 2001 From: Justin Dalrymple Date: Wed, 30 Oct 2024 09:20:21 -0400 Subject: [PATCH 1/4] Debugging --- packages/rest/src/Requester.ts | 2 +- packages/rest/test/unit/Requester.ts | 49 +++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/rest/src/Requester.ts b/packages/rest/src/Requester.ts index 9fc346e14..67ba9741d 100644 --- a/packages/rest/src/Requester.ts +++ b/packages/rest/src/Requester.ts @@ -57,7 +57,7 @@ async function throwFailedRequestError( if (contentType?.includes('application/json')) { const output = JSON.parse(content); - description = output.message; + description = typeof output.message === 'string' ? output.message : JSON.stringify(output); } else { description = content; } diff --git a/packages/rest/test/unit/Requester.ts b/packages/rest/test/unit/Requester.ts index 0279e8a52..f80cb3cb4 100644 --- a/packages/rest/test/unit/Requester.ts +++ b/packages/rest/test/unit/Requester.ts @@ -98,7 +98,54 @@ describe('processBody', () => { }); describe('defaultRequestHandler', () => { - it('should return an error with the statusText as the primary message and a description derived from a error property when response has an error property', async () => { + it.only('should return an error with the statusText as the primary message', async () => { + const responseContent = { error: 'msg' }; + + MockFetch.mockReturnValueOnce( + Promise.resolve({ + ok: false, + status: 501, + statusText: 'Really Bad Error', + headers: new Headers({ + 'content-type': 'application/json', + }), + json: () => Promise.resolve(responseContent), + text: () => Promise.resolve(JSON.stringify(responseContent)), + }), + ); + + await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ + message: 'Really Bad Error', + name: 'GitbeakerRequestError', + }); + }); + + it('should return an error with a description property derived from the error property when response has an error property', async () => { + const stringBody = { error: 'msg' }; + + MockFetch.mockReturnValueOnce( + Promise.resolve({ + ok: false, + status: 501, + statusText: 'Really Bad Error', + headers: new Headers({ + 'content-type': 'application/json', + }), + json: () => Promise.resolve(stringBody), + text: () => Promise.resolve(JSON.stringify(stringBody)), + }), + ); + + await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ + message: 'Really Bad Error', + name: 'GitbeakerRequestError', + cause: { + description: 'msg', + }, + }); + }); + + it('should return an error with a description property derived from the error property when response has an error property', async () => { const stringBody = { error: 'msg' }; MockFetch.mockReturnValueOnce( From f7e75422b237f97f1eaef9569e51b9f5c00aa1e9 Mon Sep 17 00:00:00 2001 From: Justin Dalrymple Date: Sat, 2 Nov 2024 01:08:41 -0400 Subject: [PATCH 2/4] Improved tests that actually validate Error subproperties Fix weird handling of output json. .error and .message are inferred --- packages/rest/src/Requester.ts | 4 +- packages/rest/test/unit/Requester.ts | 489 ++++++++++++++++----------- 2 files changed, 295 insertions(+), 198 deletions(-) diff --git a/packages/rest/src/Requester.ts b/packages/rest/src/Requester.ts index 67ba9741d..06e624772 100644 --- a/packages/rest/src/Requester.ts +++ b/packages/rest/src/Requester.ts @@ -56,8 +56,10 @@ async function throwFailedRequestError( if (contentType?.includes('application/json')) { const output = JSON.parse(content); + const contentProperty = output.error || output.message; - description = typeof output.message === 'string' ? output.message : JSON.stringify(output); + description = + typeof contentProperty === 'string' ? contentProperty : JSON.stringify(contentProperty); } else { description = content; } diff --git a/packages/rest/test/unit/Requester.ts b/packages/rest/test/unit/Requester.ts index f80cb3cb4..692e3a49b 100644 --- a/packages/rest/test/unit/Requester.ts +++ b/packages/rest/test/unit/Requester.ts @@ -1,4 +1,9 @@ import type { RequestOptions } from '@gitbeaker/requester-utils'; +import { + GitbeakerRequestError, + GitbeakerRetryError, + GitbeakerTimeoutError, +} from '@gitbeaker/requester-utils'; import { defaultRequestHandler, processBody } from '../../src/Requester'; global.fetch = jest.fn(); @@ -98,101 +103,160 @@ describe('processBody', () => { }); describe('defaultRequestHandler', () => { - it.only('should return an error with the statusText as the primary message', async () => { + it('should return an error with the statusText as the Error message', async () => { const responseContent = { error: 'msg' }; MockFetch.mockReturnValueOnce( - Promise.resolve({ - ok: false, - status: 501, - statusText: 'Really Bad Error', - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify(responseContent), { + status: 501, + statusText: 'Really Bad Error', + headers: { + 'content-type': 'application/json', + }, }), - json: () => Promise.resolve(responseContent), - text: () => Promise.resolve(JSON.stringify(responseContent)), - }), + ), ); - await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ - message: 'Really Bad Error', - name: 'GitbeakerRequestError', - }); + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.message).toBe('Really Bad Error'); + expect(e).toBeInstanceOf(GitbeakerRequestError); + } }); - it('should return an error with a description property derived from the error property when response has an error property', async () => { - const stringBody = { error: 'msg' }; + it('should return an error with the response included in the cause', async () => { + const responseContent = { error: 'msg' }; MockFetch.mockReturnValueOnce( - Promise.resolve({ - ok: false, - status: 501, - statusText: 'Really Bad Error', - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify(responseContent), { + status: 501, + statusText: 'Really Bad Error', + headers: { + 'content-type': 'application/json', + }, }), - json: () => Promise.resolve(stringBody), - text: () => Promise.resolve(JSON.stringify(stringBody)), - }), + ), ); - await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ - message: 'Really Bad Error', - name: 'GitbeakerRequestError', - cause: { - description: 'msg', - }, - }); + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.message).toBe('Really Bad Error'); + expect(e.cause.response).toBeInstanceOf(Response); + } }); - it('should return an error with a description property derived from the error property when response has an error property', async () => { - const stringBody = { error: 'msg' }; + it('should return an error with the request included in the cause', async () => { + const responseContent = { error: 'msg' }; MockFetch.mockReturnValueOnce( - Promise.resolve({ - ok: false, - status: 501, - statusText: 'Really Bad Error', - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify(responseContent), { + status: 501, + statusText: 'Really Bad Error', + headers: { + 'content-type': 'application/json', + }, }), - json: () => Promise.resolve(stringBody), - text: () => Promise.resolve(JSON.stringify(stringBody)), - }), + ), ); - await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ - message: 'Really Bad Error', - name: 'GitbeakerRequestError', - cause: { - description: 'msg', - }, - }); + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.message).toBe('Really Bad Error'); + expect(e.cause.request).toBeInstanceOf(Request); + } }); - it('should return an error the content of the error message if response is not JSON', async () => { - const stringBody = 'Bad things happened'; + it("should return an error with a description property derived from the response's error property when response is JSON", async () => { + const responseContent = { error: 'msg' }; MockFetch.mockReturnValueOnce( - Promise.resolve({ - ok: false, - status: 501, - statusText: 'Really Bad Error', - headers: new Headers({ - 'content-type': 'text/plain', + Promise.resolve( + new Response(JSON.stringify(responseContent), { + status: 501, + statusText: 'Really Bad Error', + headers: { + 'content-type': 'application/json', + }, }), - json: () => Promise.resolve(stringBody), - text: () => Promise.resolve(stringBody), - }), + ), ); - await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ - message: 'Really Bad Error', - name: 'GitbeakerRequestError', - cause: { - description: stringBody, - }, - }); + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.cause.description).toBe('msg'); + expect(e).toBeInstanceOf(GitbeakerRequestError); + } + }); + + it("should return an error with a description property derived from the response's message property when response is JSON", async () => { + const responseContent = { message: 'msg' }; + + MockFetch.mockReturnValueOnce( + Promise.resolve( + new Response(JSON.stringify(responseContent), { + status: 501, + statusText: 'Really Bad Error', + headers: { + 'content-type': 'application/json', + }, + }), + ), + ); + + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.cause.description).toBe('msg'); + expect(e).toBeInstanceOf(GitbeakerRequestError); + } + }); + + it('should return an error with the plain response text if response is not JSON', async () => { + const responseContent = 'Bad things happened'; + + MockFetch.mockReturnValueOnce( + Promise.resolve( + new Response(responseContent, { + status: 500, + statusText: 'Really Bad Error', + headers: { + 'content-type': 'text/plain', + }, + }), + ), + ); + + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.cause.description).toBe(responseContent); + expect(e).toBeInstanceOf(GitbeakerRequestError); + } }); it('should return an error with a message "Query timeout was reached" if fetch throws a TimeoutError', async () => { @@ -205,10 +269,15 @@ describe('defaultRequestHandler', () => { MockFetch.mockRejectedValueOnce(new TimeoutError('Hit timeout')); - await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ - message: 'Query timeout was reached', - name: 'GitbeakerTimeoutError', - }); + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.message).toBe('Query timeout was reached'); + expect(e).toBeInstanceOf(GitbeakerTimeoutError); + } }); it('should return an error with a message "Query timeout was reached" if fetch throws a AbortError', async () => { @@ -221,10 +290,15 @@ describe('defaultRequestHandler', () => { MockFetch.mockRejectedValueOnce(new AbortError('Abort signal triggered')); - await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ - message: 'Query timeout was reached', - name: 'GitbeakerTimeoutError', - }); + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.message).toBe('Query timeout was reached'); + expect(e).toBeInstanceOf(GitbeakerTimeoutError); + } }); it('should return an unchanged error if fetch throws an error thats not an AbortError or TimeoutError', async () => { @@ -237,36 +311,44 @@ describe('defaultRequestHandler', () => { MockFetch.mockRejectedValueOnce(new RandomError('Random Error')); - await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ - message: 'Random Error', - name: 'RandomError', - }); + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.message).toBe('Random Error'); + expect(e).toBeInstanceOf(RandomError); + } }); it('should retry request if a 429 retry code is returned', async () => { - const stringBody = { error: 'msg' }; - const fakeFailedReturnValue = Promise.resolve({ - ok: false, - status: 429, - statusText: 'Retry Code', - headers: new Headers({ - 'content-type': 'application/json', - }), - json: () => Promise.resolve(stringBody), - text: () => Promise.resolve(JSON.stringify(stringBody)), - }); + const responseContent = { error: 'msg' }; + const fakeFailedReturnValue = Promise.resolve( + Promise.resolve( + new Response(JSON.stringify(responseContent), { + status: 429, + statusText: 'Retry Code', + headers: { + 'content-type': 'application/json', + }, + }), + ), + ); - const fakeSuccessfulReturnValue = Promise.resolve({ - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', - }), - }); + const fakeSuccessfulReturnValue = Promise.resolve( + Promise.resolve( + new Response(JSON.stringify({}), { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, + }), + ), + ); - // Mock return 10 times + // Mock return twice MockFetch.mockReturnValue(fakeFailedReturnValue); MockFetch.mockReturnValue(fakeSuccessfulReturnValue); @@ -280,29 +362,32 @@ describe('defaultRequestHandler', () => { }); it('should retry request if a 502 retry code is returned', async () => { - const stringBody = { error: 'msg' }; - const fakeFailedReturnValue = Promise.resolve({ - ok: false, - status: 502, - statusText: 'Retry Code', - headers: new Headers({ - 'content-type': 'application/json', - }), - json: () => Promise.resolve(stringBody), - text: () => Promise.resolve(JSON.stringify(stringBody)), - }); + const responseContent = { error: 'msg' }; + const fakeFailedReturnValue = Promise.resolve( + Promise.resolve( + new Response(JSON.stringify(responseContent), { + status: 502, + statusText: 'Retry Code', + headers: { + 'content-type': 'application/json', + }, + }), + ), + ); - const fakeSuccessfulReturnValue = Promise.resolve({ - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', - }), - }); + const fakeSuccessfulReturnValue = Promise.resolve( + Promise.resolve( + new Response(JSON.stringify({}), { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, + }), + ), + ); - // Mock return 10 times + // Mock return twice MockFetch.mockReturnValue(fakeFailedReturnValue); MockFetch.mockReturnValue(fakeSuccessfulReturnValue); @@ -316,41 +401,48 @@ describe('defaultRequestHandler', () => { }); it('should return a default error if retries are unsuccessful', async () => { - const stringBody = { error: 'msg' }; - const fakeReturnValue = Promise.resolve({ - ok: false, - status: 429, - statusText: 'Retry Code', - headers: new Headers({ - 'content-type': 'application/json', - }), - json: () => Promise.resolve(stringBody), - text: () => Promise.resolve(JSON.stringify(stringBody)), - }); + const responseContent = { error: 'msg' }; + const fakeReturnValue = Promise.resolve( + Promise.resolve( + new Response(JSON.stringify(responseContent), { + status: 429, + statusText: 'Retry Code', + headers: { + 'content-type': 'application/json', + }, + }), + ), + ); - // Mock return 10 times + // Mock return MockFetch.mockReturnValue(fakeReturnValue); - await expect(defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow({ - message: + // Ensure an assertion is made (the error is thrown) + expect.assertions(2); + + try { + await defaultRequestHandler('http://test.com', {} as RequestOptions); + } catch (e) { + expect(e.message).toBe( 'Could not successfully complete this request after 10 retries, last status code: 429. Check the applicable rate limits for this endpoint.', - name: 'GitbeakerRetryError', - }); + ); + expect(e).toBeInstanceOf(GitbeakerRetryError); + } MockFetch.mockRestore(); }); it('should return correct properties if request is valid', async () => { MockFetch.mockReturnValueOnce( - Promise.resolve({ - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify({}), { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, }), - }), + ), ); const output = await defaultRequestHandler('http://test.com', {} as RequestOptions); @@ -362,18 +454,17 @@ describe('defaultRequestHandler', () => { }); }); - it('should return correct properties as stream if request is valid', async () => { + it.only('should return correct properties as stream if request is valid', async () => { MockFetch.mockReturnValueOnce( - Promise.resolve({ - body: 'text', - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response('text', { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, }), - }), + ), ); const output = await defaultRequestHandler('http://test.com', { @@ -381,23 +472,27 @@ describe('defaultRequestHandler', () => { } as RequestOptions); expect(output).toMatchObject({ - body: 'text', + body: expect.any(ReadableStream), headers: { 'content-type': 'application/json' }, status: 200, }); + + const outputContent = await new Response(output.body as ReadableStream).text(); + + expect(outputContent).toBe('text'); }); it('should handle a prefix url correctly', async () => { MockFetch.mockReturnValueOnce( - Promise.resolve({ - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify({}), { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, }), - }), + ), ); await defaultRequestHandler('testurl', { @@ -411,15 +506,15 @@ describe('defaultRequestHandler', () => { it('should handle a searchParams correctly', async () => { MockFetch.mockReturnValueOnce( - Promise.resolve({ - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify({}), { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, }), - }), + ), ); await defaultRequestHandler('testurl/123', { @@ -434,15 +529,15 @@ describe('defaultRequestHandler', () => { it('should add same-origin mode for repository/archive endpoint', async () => { MockFetch.mockReturnValueOnce( - Promise.resolve({ - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify({}), { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, }), - }), + ), ); await defaultRequestHandler('http://test.com/repository/archive'); @@ -456,15 +551,15 @@ describe('defaultRequestHandler', () => { it('should use default mode (cors) for non-repository/archive endpoints', async () => { MockFetch.mockReturnValueOnce( - Promise.resolve({ - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify({}), { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, }), - }), + ), ); await defaultRequestHandler('http://test.com/test/something'); @@ -476,15 +571,15 @@ describe('defaultRequestHandler', () => { it('should handle multipart prefixUrls correctly', async () => { MockFetch.mockReturnValue( - Promise.resolve({ - json: () => Promise.resolve({}), - text: () => Promise.resolve(JSON.stringify({})), - ok: true, - status: 200, - headers: new Headers({ - 'content-type': 'application/json', + Promise.resolve( + new Response(JSON.stringify({}), { + status: 200, + statusText: 'Good', + headers: { + 'content-type': 'application/json', + }, }), - }), + ), ); await defaultRequestHandler('testurl/123', { From 2a4d0a9ca30e4474121a52d61d154a3dac716315 Mon Sep 17 00:00:00 2001 From: Justin Dalrymple Date: Sat, 2 Nov 2024 01:19:10 -0400 Subject: [PATCH 3/4] Remove focused test --- packages/rest/test/unit/Requester.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rest/test/unit/Requester.ts b/packages/rest/test/unit/Requester.ts index 692e3a49b..bb05a68f2 100644 --- a/packages/rest/test/unit/Requester.ts +++ b/packages/rest/test/unit/Requester.ts @@ -454,7 +454,7 @@ describe('defaultRequestHandler', () => { }); }); - it.only('should return correct properties as stream if request is valid', async () => { + it('should return correct properties as stream if request is valid', async () => { MockFetch.mockReturnValueOnce( Promise.resolve( new Response('text', { From ca595666962b22474823c9759d9d71dfd147989b Mon Sep 17 00:00:00 2001 From: Justin Dalrymple Date: Thu, 7 Nov 2024 10:14:11 -0500 Subject: [PATCH 4/4] Save progress --- global.d.ts | 21 ++++++++ jest.config.base.mjs | 6 ++- jest.setup.ts | 75 ++++++++++++++++++++++++++++ packages/rest/test/unit/Requester.ts | 31 +++++++++--- 4 files changed, 123 insertions(+), 10 deletions(-) diff --git a/global.d.ts b/global.d.ts index b68ae0770..e51d91254 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1 +1,22 @@ import 'jest-extended' + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface AsymmetricMatchers { + toThrowWith(cb: (error: Error) => void): void; + } + interface Matchers { + toThrowWith(cb: (error: Error) => void): R; + } + } +} + +declare module "expect" { + interface AsymmetricMatchers { + toThrowWith(cb: (error: Error) => void): void; + } + interface Matchers { + toThrowWith(cb: (error: Error) => void): R; + } +} \ No newline at end of file diff --git a/jest.config.base.mjs b/jest.config.base.mjs index 02cc1bdeb..197b137e5 100644 --- a/jest.config.base.mjs +++ b/jest.config.base.mjs @@ -1,4 +1,5 @@ export default { + rootDir: '../../', testEnvironment: 'node', testRegex: 'test\\/.*\\.ts$', coverageDirectory: 'coverage', @@ -9,10 +10,11 @@ export default { ['jest-junit', { outputDirectory: 'reports', outputName: 'nodejs_junit.xml' }], ], moduleNameMapper: { - '^@gitbeaker/(.*)$': '/../$1/src', + '^@gitbeaker/core/map.json': '/packages/core/dist/map.json', + '^@gitbeaker/(.*)$': '/packages/$1/src', }, transform: { '^.+\\.(t|j)sx?$': '@swc/jest', }, - setupFilesAfterEnv: ['jest-extended/all'], + setupFilesAfterEnv: ['/jest.setup.ts'], }; diff --git a/jest.setup.ts b/jest.setup.ts index e1fd2b7a9..47337a8c6 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1,2 +1,77 @@ import 'jest-extended'; import 'jest-extended/all'; + +import { expect } from '@jest/globals'; +import type {MatcherFunction} from 'expect'; + +const toThrowWith: MatcherFunction<[cb:unknown]> = async function (expectCallbackOrPromiseReturn, matcherCallback) { + const isFromReject = this && this.promise === 'rejects'; // See https://github.com/facebook/jest/pull/7621#issue-244312550 + if ((!expectCallbackOrPromiseReturn || typeof expectCallbackOrPromiseReturn !== 'function') && !isFromReject) { + return { + pass: false, + message: () => + `Received value must be a function but instead "${expectCallbackOrPromiseReturn}" was found`, + }; + } + + if ((!matcherCallback || typeof matcherCallback !== 'function')) { + return { + pass: false, + message: () => + `matcherCallback value must be a function but instead "${matcherCallback}" was found`, + }; + } + + + let error; + if (isFromReject) { + error = expectCallbackOrPromiseReturn; + } else { + try { + if (typeof expectCallbackOrPromiseReturn === 'function') { + await expectCallbackOrPromiseReturn(); + } + } catch (e) { + error = e; + } + } + + await matcherCallback(error) + + if (!error) { + return { + pass: false, + message: () => 'Expected the function to throw an error.\n' + "But it didn't throw anything.", + }; + } else { + return { + pass: true, + message: () => 'Expected the function not to throw an error" + }; + } +} + +expect.extend({ + toThrowWith, +}); + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface AsymmetricMatchers { + toThrowWith(cb: (error: Error) => void): void; + } + interface Matchers { + toThrowWith(cb: (error: Error) => void): R; + } + } +} + +declare module "expect" { + interface AsymmetricMatchers { + toThrowWith(cb: (error: Error) => void): void; + } + interface Matchers { + toThrowWith(cb: (error: Error) => void): R; + } +} \ No newline at end of file diff --git a/packages/rest/test/unit/Requester.ts b/packages/rest/test/unit/Requester.ts index bb05a68f2..77c235d91 100644 --- a/packages/rest/test/unit/Requester.ts +++ b/packages/rest/test/unit/Requester.ts @@ -1,3 +1,4 @@ +import { expect } from '@jest/globals'; import type { RequestOptions } from '@gitbeaker/requester-utils'; import { GitbeakerRequestError, @@ -129,7 +130,7 @@ describe('defaultRequestHandler', () => { } }); - it('should return an error with the response included in the cause', async () => { + it.only('should return an error with the response included in the cause', async () => { const responseContent = { error: 'msg' }; MockFetch.mockReturnValueOnce( @@ -145,14 +146,28 @@ describe('defaultRequestHandler', () => { ); // Ensure an assertion is made (the error is thrown) - expect.assertions(2); - try { - await defaultRequestHandler('http://test.com', {} as RequestOptions); - } catch (e) { - expect(e.message).toBe('Really Bad Error'); - expect(e.cause.response).toBeInstanceOf(Response); - } + // await expect(() => defaultRequestHandler('http://test.com', {} as RequestOptions)).rejects.toThrow( + // expect.objectContaining({ + // name: "GitbeakerRequestError", + // message: 'Really Bad Error', + // cause: { + // response: expect.any(Response), + // }, + // }), + // ); + + await expect(() => Promise.reject(new Error(''))).rejects.toThrowWithMessage(Error, 'test'); + + // await expect(() => Promise.reject(new Error(''))).rejects.toThrowWith((e: GitbeakerRequestError) => { + // expect(e.message).toBe('Really Bad Error'); + // expect(e?.cause?.response).toBeInstanceOf(Response); + // }); + + // await expect(() => defaultRequestHandler('http://test.com')).rejects.toThrowWith((e: GitbeakerRequestError) => { + // expect(e.message).toBe('Really Bad Error'); + // expect(e?.cause?.response).toBeInstanceOf(Response); + // }); }); it('should return an error with the request included in the cause', async () => {