Skip to content

Commit 2cd95e6

Browse files
committed
feat: add exception handling for ValidationError, NotFoundError, and DatabaseError in AppSyncGraphQLResolver
1 parent 37a12ec commit 2cd95e6

File tree

1 file changed

+300
-0
lines changed

1 file changed

+300
-0
lines changed

packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import context from '@aws-lambda-powertools/testing-utils/context';
2+
import { AssertionError } from 'assert';
23
import type { AppSyncResolverEvent, Context } from 'aws-lambda';
34
import { beforeEach, describe, expect, it, vi } from 'vitest';
45
import { AppSyncGraphQLResolver } from '../../../src/appsync-graphql/AppSyncGraphQLResolver.js';
@@ -8,6 +9,27 @@ import {
89
} from '../../../src/appsync-graphql/index.js';
910
import { onGraphqlEventFactory } from '../../helpers/factories.js';
1011

12+
class ValidationError extends Error {
13+
constructor(message: string) {
14+
super(message);
15+
this.name = 'ValidationError';
16+
}
17+
}
18+
19+
class NotFoundError extends Error {
20+
constructor(message: string) {
21+
super(message);
22+
this.name = 'NotFoundError';
23+
}
24+
}
25+
26+
class DatabaseError extends Error {
27+
constructor(message: string) {
28+
super(message);
29+
this.name = 'DatabaseError';
30+
}
31+
}
32+
1133
describe('Class: AppSyncGraphQLResolver', () => {
1234
beforeEach(() => {
1335
vi.clearAllMocks();
@@ -706,4 +728,282 @@ describe('Class: AppSyncGraphQLResolver', () => {
706728
expect(resultMutation).toEqual(['scoped', 'scoped']);
707729
}
708730
);
731+
732+
// #region Exception Handling
733+
734+
it('should register and use an exception handler for specific error types', async () => {
735+
// Prepare
736+
const app = new AppSyncGraphQLResolver();
737+
738+
app.exceptionHandler(ValidationError, async (error) => {
739+
return {
740+
message: 'Validation failed',
741+
details: error.message,
742+
type: 'validation_error',
743+
};
744+
});
745+
746+
app.onQuery<{ id: string }>('getUser', async ({ id }) => {
747+
if (!id) {
748+
throw new ValidationError('User ID is required');
749+
}
750+
return { id, name: 'John Doe' };
751+
});
752+
753+
// Act
754+
const result = await app.resolve(
755+
onGraphqlEventFactory('getUser', 'Query', {}),
756+
context
757+
);
758+
759+
// Assess
760+
expect(result).toEqual({
761+
message: 'Validation failed',
762+
details: 'User ID is required',
763+
type: 'validation_error',
764+
});
765+
});
766+
767+
it('should handle multiple different error types with specific handlers', async () => {
768+
// Prepare
769+
const app = new AppSyncGraphQLResolver();
770+
771+
app.exceptionHandler(ValidationError, async (error) => {
772+
return {
773+
message: 'Validation failed',
774+
details: error.message,
775+
type: 'validation_error',
776+
};
777+
});
778+
779+
app.exceptionHandler(NotFoundError, async (error) => {
780+
return {
781+
message: 'Resource not found',
782+
details: error.message,
783+
type: 'not_found_error',
784+
};
785+
});
786+
787+
app.onQuery<{ id: string }>('getUser', async ({ id }) => {
788+
if (!id) {
789+
throw new ValidationError('User ID is required');
790+
}
791+
if (id === 'not-found') {
792+
throw new NotFoundError(`User with ID ${id} not found`);
793+
}
794+
return { id, name: 'John Doe' };
795+
});
796+
797+
// Act
798+
const validationResult = await app.resolve(
799+
onGraphqlEventFactory('getUser', 'Query', {}),
800+
context
801+
);
802+
const notFoundResult = await app.resolve(
803+
onGraphqlEventFactory('getUser', 'Query', { id: 'not-found' }),
804+
context
805+
);
806+
807+
// Asses
808+
expect(validationResult).toEqual({
809+
message: 'Validation failed',
810+
details: 'User ID is required',
811+
type: 'validation_error',
812+
});
813+
expect(notFoundResult).toEqual({
814+
message: 'Resource not found',
815+
details: 'User with ID not-found not found',
816+
type: 'not_found_error',
817+
});
818+
});
819+
820+
it('should prefer exact error class match over inheritance match', async () => {
821+
// Prepare
822+
const app = new AppSyncGraphQLResolver();
823+
824+
app.exceptionHandler(Error, async (error) => {
825+
return {
826+
message: 'Generic error occurred',
827+
details: error.message,
828+
type: 'generic_error',
829+
};
830+
});
831+
832+
app.exceptionHandler(ValidationError, async (error) => {
833+
return {
834+
message: 'Validation failed',
835+
details: error.message,
836+
type: 'validation_error',
837+
};
838+
});
839+
840+
app.onQuery('getUser', async () => {
841+
throw new ValidationError('Specific validation error');
842+
});
843+
844+
// Act
845+
const result = await app.resolve(
846+
onGraphqlEventFactory('getUser', 'Query', {}),
847+
context
848+
);
849+
850+
// Assess
851+
expect(result).toEqual({
852+
message: 'Validation failed',
853+
details: 'Specific validation error',
854+
type: 'validation_error',
855+
});
856+
});
857+
858+
it('should fall back to default error formatting when no exception handler is found', async () => {
859+
// Prepare
860+
const app = new AppSyncGraphQLResolver();
861+
862+
app.exceptionHandler(AssertionError, async (error) => {
863+
return {
864+
message: 'Validation failed',
865+
details: error.message,
866+
type: 'validation_error',
867+
};
868+
});
869+
870+
app.onQuery('getUser', async () => {
871+
throw new DatabaseError('Database connection failed');
872+
});
873+
874+
// Act
875+
const result = await app.resolve(
876+
onGraphqlEventFactory('getUser', 'Query', {}),
877+
context
878+
);
879+
880+
// Assess
881+
expect(result).toEqual({
882+
error: 'DatabaseError - Database connection failed',
883+
});
884+
});
885+
886+
it('should fall back to default error formatting when exception handler throws an error', async () => {
887+
// Prepare
888+
const app = new AppSyncGraphQLResolver({ logger: console });
889+
890+
app.exceptionHandler(ValidationError, async () => {
891+
throw new Error('Exception handler failed');
892+
});
893+
894+
app.onQuery('getUser', async () => {
895+
throw new ValidationError('Original error');
896+
});
897+
898+
// Act
899+
const result = await app.resolve(
900+
onGraphqlEventFactory('getUser', 'Query', {}),
901+
context
902+
);
903+
904+
// Assess
905+
expect(result).toEqual({
906+
error: 'ValidationError - Original error',
907+
});
908+
expect(console.error).toHaveBeenNthCalledWith(
909+
2,
910+
'Exception handler for ValidationError threw an error',
911+
new Error('Exception handler failed')
912+
);
913+
});
914+
915+
it('should work with async exception handlers', async () => {
916+
// Prepare
917+
const app = new AppSyncGraphQLResolver();
918+
919+
app.exceptionHandler(ValidationError, async (error) => {
920+
return {
921+
message: 'Async validation failed',
922+
details: error.message,
923+
type: 'async_validation_error',
924+
};
925+
});
926+
927+
app.onQuery('getUser', async () => {
928+
throw new ValidationError('Async error test');
929+
});
930+
931+
// Act
932+
const result = await app.resolve(
933+
onGraphqlEventFactory('getUser', 'Query', {}),
934+
context
935+
);
936+
937+
// Assess
938+
expect(result).toEqual({
939+
message: 'Async validation failed',
940+
details: 'Async error test',
941+
type: 'async_validation_error',
942+
});
943+
});
944+
945+
it('should not interfere with ResolverNotFoundException', async () => {
946+
// Prepare
947+
const app = new AppSyncGraphQLResolver();
948+
949+
app.exceptionHandler(RangeError, async (error) => {
950+
return {
951+
message: 'This should not be called',
952+
details: error.message,
953+
type: 'should_not_happen',
954+
};
955+
});
956+
957+
// Act & Assess
958+
await expect(
959+
app.resolve(
960+
onGraphqlEventFactory('nonExistentResolver', 'Query'),
961+
context
962+
)
963+
).rejects.toThrow('No resolver found for Query-nonExistentResolver');
964+
});
965+
966+
it('should work as a method decorator', async () => {
967+
// Prepare
968+
const app = new AppSyncGraphQLResolver();
969+
970+
class TestService {
971+
@app.exceptionHandler(ValidationError)
972+
async handleValidationError(error: ValidationError) {
973+
return {
974+
message: 'Decorator validation failed',
975+
details: error.message,
976+
type: 'decorator_validation_error',
977+
};
978+
}
979+
980+
@app.onQuery('getUser')
981+
async getUser() {
982+
throw new ValidationError('Decorator error test');
983+
}
984+
985+
async handler(event: unknown, context: Context) {
986+
return app.resolve(event, context, {
987+
scope: this,
988+
});
989+
}
990+
}
991+
992+
const service = new TestService();
993+
994+
// Act
995+
const result = await service.handler(
996+
onGraphqlEventFactory('getUser', 'Query', {}),
997+
context
998+
);
999+
1000+
// Assess
1001+
expect(result).toEqual({
1002+
message: 'Decorator validation failed',
1003+
details: 'Decorator error test',
1004+
type: 'decorator_validation_error',
1005+
});
1006+
});
1007+
1008+
// #endregion Exception handling
7091009
});

0 commit comments

Comments
 (0)