Skip to content
This repository has been archived by the owner on Aug 18, 2024. It is now read-only.

Commit

Permalink
Improve API Error Handling (#21)
Browse files Browse the repository at this point in the history
* Make the API client throw `HttpException` on errors

* Improve API Error Handling

---------

Co-authored-by: yasaichi <[email protected]>
  • Loading branch information
yasaichi and yasaichi authored Mar 20, 2024
1 parent fc2bf98 commit e0dabce
Show file tree
Hide file tree
Showing 13 changed files with 639 additions and 50 deletions.
3 changes: 2 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"singleQuote": true
},
"imports": {
"@app/fake-api-kiota": "./libs/fake-api-kiota/src/index.ts"
"@app/fake-api-kiota": "./libs/fake-api-kiota/src/index.ts",
"@app/nestjs-kiota": "./libs/nestjs-kiota/src/index.ts"
},
"lint": {
"include": ["src/", "apps/", "libs/", "test/"],
Expand Down
25 changes: 4 additions & 21 deletions libs/fake-api-kiota/src/fake-api-kiota.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { apiClientServiceFactory, NestRequestAdapter } from '@app/nestjs-kiota';
import { AnonymousAuthenticationProvider } from '@microsoft/kiota-abstractions';
import {
FetchRequestAdapter,
Expand All @@ -12,18 +13,6 @@ import {
import { createApiClient } from './generated/apiClient.ts';

export class FakeApiKiotaModule extends ConfigurableModuleClass {
private static nestLifecycleHooks: string[] = [
'onModuleInit',
'onApplicationBootstrap',
'onModuleDestroy',
'beforeApplicationShutdown',
'onApplicationShutdown',
];
private static apiServicePropsCalledByNest = new Set([
'then',
...FakeApiKiotaModule.nestLifecycleHooks,
]);

static register(
{ baseUrl, customFetch, ...restOptions }: typeof OPTIONS_TYPE,
): DynamicModule {
Expand All @@ -41,15 +30,9 @@ export class FakeApiKiotaModule extends ConfigurableModuleClass {
...super.register(restOptions),
providers: [{
provide: FAKE_API_KIOTA_SERVICE_TOKEN,
// NOTE: This is a workaround to use the API client with NestJS dependency injection mechanism.
// For further details, please refer to the following comment:
// https://github.com/microsoft/kiota-typescript/issues/1075#issuecomment-1987042257
useValue: new Proxy(createApiClient(requestAdapter), {
get: (target, prop) =>
FakeApiKiotaModule.apiServicePropsCalledByNest.has(prop.toString())
? undefined
: Reflect.get(target, prop),
}),
useValue: apiClientServiceFactory(
createApiClient(new NestRequestAdapter(requestAdapter)),
),
}],
exports: [FAKE_API_KIOTA_SERVICE_TOKEN],
};
Expand Down
263 changes: 263 additions & 0 deletions libs/nestjs-kiota/src/NestRequestAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import {
AnonymousAuthenticationProvider,
HttpMethod,
type RequestAdapter,
RequestInformation,
} from '@microsoft/kiota-abstractions';
import { FetchRequestAdapter } from '@microsoft/kiota-http-fetchlibrary';
import { HttpException } from '@nestjs/common';
import { beforeEach, describe, it } from '@std/testing/bdd';
import assert from 'node:assert';
import { fake, type SinonStubbedInstance, stub } from 'sinon';
import { NestRequestAdapter } from './NestRequestAdapter.ts';

describe(NestRequestAdapter.name, () => {
let requestAdapter: SinonStubbedInstance<RequestAdapter>;
let nestRequestAdapter: NestRequestAdapter;

const apiError = {
additionalData: {},
responseHeaders: {},
responseStatusCode: 404,
bar: 'bar',
};
const baseUrl = 'https://example.com';
const primitiveResponseModel = 42;
const responseModel = { foo: 'foo' };

beforeEach(() => {
// TODO: Stop using FetchRequestAdapter
requestAdapter = stub(
new FetchRequestAdapter(new AnonymousAuthenticationProvider()),
);
nestRequestAdapter = new NestRequestAdapter(requestAdapter);
});

describe('baseUrl', () => {
it('should return the same `baseUrl` value as the wrapped adapter has', () => {
requestAdapter.baseUrl = baseUrl;
assert.strictEqual(nestRequestAdapter.baseUrl, baseUrl);
});
});

describe('baseUrl=', () => {
it('should change the `baseUrl` value of the wrapped adapter', () => {
nestRequestAdapter.baseUrl = baseUrl;
assert.strictEqual(requestAdapter.baseUrl, baseUrl);
});
});

describe('send', () => {
const args = [
new RequestInformation(HttpMethod.GET),
fake(),
undefined,
] as const;

describe('when the wrapped method call succeeds', () => {
beforeEach(() => {
requestAdapter.send.returns(Promise.resolve(responseModel));
});

it('should return a response model the wrapped method returns', async () => {
assert.deepEqual(await nestRequestAdapter.send(...args), responseModel);
});
});

describe('when the wrapped method call fails', () => {
beforeEach(() => {
requestAdapter.send.throwsException(apiError);
});

it('should throw `HttpException`', () => {
assert.rejects(
nestRequestAdapter.send(...args),
(error) => {
assert(requestAdapter.send.calledOnceWith(...args));

assert(error instanceof HttpException);
assert.strictEqual(error.getStatus(), apiError.responseStatusCode);
assert.deepEqual(error.getResponse(), { bar: apiError.bar });

return true;
},
);
});
});
});

describe('sendPrimitive', () => {
const args = [
new RequestInformation(HttpMethod.GET),
'number',
undefined,
] as const;

describe('when the wrapped method call succeeds', () => {
beforeEach(() => {
requestAdapter.sendPrimitive.returns(
Promise.resolve(primitiveResponseModel),
);
});

it('should return a primitive response model the wrapped method returns', async () => {
assert.strictEqual(
await nestRequestAdapter.sendPrimitive(...args),
primitiveResponseModel,
);
});
});

describe('when the wrapped method call fails', () => {
beforeEach(() => {
requestAdapter.sendPrimitive.throwsException(apiError);
});

it('should throw `HttpException`', () => {
assert.rejects(
nestRequestAdapter.sendPrimitive(...args),
(error) => {
assert(requestAdapter.sendPrimitive.calledOnceWith(...args));

assert(error instanceof HttpException);
assert.strictEqual(error.getStatus(), apiError.responseStatusCode);
assert.deepEqual(error.getResponse(), { bar: apiError.bar });

return true;
},
);
});
});
});

describe('sendCollection', () => {
const args = [
new RequestInformation(HttpMethod.GET),
fake(),
undefined,
] as const;

describe('when the wrapped method call succeeds', () => {
beforeEach(() => {
requestAdapter.sendCollection.returns(Promise.resolve([responseModel]));
});

it('should return a response model collection the wrapped method returns', async () => {
assert.deepEqual(
await nestRequestAdapter.sendCollection(...args),
[responseModel],
);
});
});

describe('when the wrapped method call fails', () => {
beforeEach(() => {
requestAdapter.sendCollection.throwsException(apiError);
});

it('should throw `HttpException`', () => {
assert.rejects(
nestRequestAdapter.sendCollection(...args),
(error) => {
assert(requestAdapter.sendCollection.calledOnceWith(...args));

assert(error instanceof HttpException);
assert.strictEqual(error.getStatus(), apiError.responseStatusCode);
assert.deepEqual(error.getResponse(), { bar: apiError.bar });

return true;
},
);
});
});
});

describe('sendCollectionOfPrimitive', () => {
const args = [
new RequestInformation(HttpMethod.GET),
'number',
undefined,
] as const;

describe('when the wrapped method call succeeds', () => {
beforeEach(() => {
requestAdapter.sendCollectionOfPrimitive.returns(
Promise.resolve([primitiveResponseModel]),
);
});

it('should return a primitive response model the wrapped method returns', async () => {
assert.deepEqual(
await nestRequestAdapter.sendCollectionOfPrimitive(...args),
[primitiveResponseModel],
);
});
});

describe('when the wrapped method call fails', () => {
beforeEach(() => {
requestAdapter.sendCollectionOfPrimitive.throwsException(apiError);
});

it('should throw `HttpException`', () => {
assert.rejects(
nestRequestAdapter.sendCollectionOfPrimitive(...args),
(error) => {
assert(
requestAdapter.sendCollectionOfPrimitive.calledOnceWith(...args),
);

assert(error instanceof HttpException);
assert.strictEqual(error.getStatus(), apiError.responseStatusCode);
assert.deepEqual(error.getResponse(), { bar: apiError.bar });

return true;
},
);
});
});
});

describe('sendNoResponseContent', () => {
const args = [
new RequestInformation(HttpMethod.DELETE),
undefined,
] as const;

describe('when the wrapped method call succeeds', () => {
beforeEach(() => {
requestAdapter.sendNoResponseContent.returns(Promise.resolve());
});

it('should return nothing', async () => {
assert.strictEqual(
await nestRequestAdapter.sendNoResponseContent(...args),
undefined,
);
});
});

describe('when the wrapped method call fails', () => {
beforeEach(() => {
requestAdapter.sendNoResponseContent.throwsException(apiError);
});

it('should throw `HttpException`', () => {
assert.rejects(
nestRequestAdapter.sendNoResponseContent(...args),
(error) => {
assert(
requestAdapter.sendNoResponseContent.calledOnceWith(...args),
);

assert(error instanceof HttpException);
assert.strictEqual(error.getStatus(), apiError.responseStatusCode);
assert.deepEqual(error.getResponse(), { bar: apiError.bar });

return true;
},
);
});
});
});
});
Loading

0 comments on commit e0dabce

Please sign in to comment.