Skip to content

Ref(backend): OccupationGroup & SkillGroup module index.test.ts refactor and coverage tests for other uncovered logics#497

Merged
irumvanselme merged 2 commits into
mainfrom
fix/refactoring_the_root_test_and_coverage_test
May 21, 2026
Merged

Ref(backend): OccupationGroup & SkillGroup module index.test.ts refactor and coverage tests for other uncovered logics#497
irumvanselme merged 2 commits into
mainfrom
fix/refactoring_the_root_test_and_coverage_test

Conversation

@C5rogers
Copy link
Copy Markdown

@C5rogers C5rogers commented May 18, 2026

  • This pull request currently have change on refactor on OccupationGroup index.test.ts for testing endpoint functions and also refactoring the other endpoint tests for coverage test
  • now also it have SkillGroup index.test.ts refactored and distributed over the submodules

@C5rogers C5rogers requested a review from Copilot May 18, 2026 11:48
@C5rogers C5rogers self-assigned this May 18, 2026
@C5rogers C5rogers added the enhancement New feature or request label May 18, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the occupationGroup module test suite. The previously monolithic backend/src/esco/occupationGroup/index.test.ts (~2042 lines covering POST, GET list, GET by ID, GET parent, GET children behaviors via the top-level router) is replaced with a thin router-level test that mocks each sub-handler, while the actual controller behavior is split into per-endpoint test files. Additional coverage tests are also added to the per-endpoint files, and an unused helper is deleted from _shared/testDataHelper.ts.

Changes:

  • Replace occupationGroup/index.test.ts with mock-based router routing tests.
  • Migrate POST/GET/GET-by-id/parent/children controller behavior tests to their respective index.test.ts files and add new edge-case coverage (cursor decode failure, repo failures, schema-invalid path, missing modelId, etc.).
  • Remove unused getIOccupationGroupMockDataWithOccupationGroupChildren helper.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
backend/src/esco/occupationGroup/POST/index.ts Removes a blank line between schema validation and modelId extraction.
backend/src/esco/occupationGroup/POST/index.test.ts Adds many POST coverage tests migrated from root index.test.ts and new ones; introduces handler wrappers and a global ParseValidationError mock.
backend/src/esco/occupationGroup/index.test.ts Replaces controller tests with mocked sub-handler routing tests.
backend/src/esco/occupationGroup/GET/index.test.ts Adds cursor/next-cursor, model-validation, repo-failure coverage tests.
backend/src/esco/occupationGroup/GET/query.test.ts Adds path-parameter parsing and cursor round-trip tests.
backend/src/esco/occupationGroup/[id]/GET/index.test.ts Adds INTERNAL_SERVER_ERROR on fetch failure and BAD_REQUEST on path-validation failure tests.
backend/src/esco/occupationGroup/[id]/parent/GET/index.test.ts Adds NOT_FOUND for missing model and INTERNAL_SERVER_ERROR for repo failure tests.
backend/src/esco/occupationGroup/[id]/children/GET/index.test.ts Adds INTERNAL_SERVER_ERROR for repo failure test.
backend/src/esco/occupationGroup/_shared/testDataHelper.ts Removes unused getIOccupationGroupMockDataWithOccupationGroupChildren helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

} as never);

const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup.findById = jest.fn().mockResolvedValue(Promise.reject(new Error("DB error")));
mockGetOccupationGroupParentPathParameters.mockReturnValue({ modelId: "model-1", id: "group-1" } as never);

const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup.findParent = jest.fn().mockResolvedValue(Promise.reject(new Error("DB error")));
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup.findChildren = jest
.fn()
.mockResolvedValue(Promise.reject(new Error("DB error")));
import { handler as getByIdHandler } from "./[id]/GET/index";
import { handler as getParentHandler } from "./[id]/parent/GET/index";
import { handler as getChildrenHandler } from "./[id]/children/GET/index";
describe("Occupations Router", () => {
Comment on lines +50 to +56
const postOccupationGroupHandler = async (event: APIGatewayProxyEvent) => {
return new OccupationGroupCreateController().postOccupationGroup(event);
};

const postOccupationGroupSchemaInvalidHandler = async (event: APIGatewayProxyEvent) => {
getMockGetSchema().mockReturnValue(jest.fn().mockReturnValue(false) as never);
return new OccupationGroupCreateController().postOccupationGroup(event);
Comment on lines +50 to +56
const postOccupationGroupHandler = async (event: APIGatewayProxyEvent) => {
return new OccupationGroupCreateController().postOccupationGroup(event);
};

const postOccupationGroupSchemaInvalidHandler = async (event: APIGatewayProxyEvent) => {
getMockGetSchema().mockReturnValue(jest.fn().mockReturnValue(false) as never);
return new OccupationGroupCreateController().postOccupationGroup(event);
Comment on lines 122 to 123
}

const { modelId: resolvedModelId } = parsePath<{ modelId?: string }>(Routes.OCCUPATION_GROUPS_ROUTE, event.path);
Comment on lines +27 to +56
test("should route POST to postHandler", async () => {
const event = { httpMethod: HTTP_VERBS.POST } as APIGatewayProxyEvent;
const response = await handler(event);
expect(postHandler).toHaveBeenCalledWith(event);
expect(response.body).toBe("POST");
});

describe("GET", () => {
// GIVEN a valid GET request (method & header)
const givenModelId = getMockStringId(1);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString() },
};

// AND a configured base path for resource
const givenResourcesBaseUrl = "https://some/path/to/api/resources";
jest.spyOn(config, "getResourcesBaseUrl").mockReturnValue(givenResourcesBaseUrl);

test("GET should return only the occupationGroups for the given modelId", async () => {
// AND GIVEN a repository that will successfully get an arbitrary number (N) of models
const givenOccupationGroups: Array<IOccupationGroup> = [
{
...getIOccupationGroupMockData(1, givenModelId),
UUID: "foo",
UUIDHistory: ["foo"],
importId: randomUUID(),
},
{
...getIOccupationGroupMockData(2, givenModelId),
UUID: "bar",
UUIDHistory: ["bar"],
importId: randomUUID(),
},
{
...getIOccupationGroupMockData(3, givenModelId),
UUID: "baz",
UUIDHistory: ["baz"],
importId: randomUUID(),
},
];

const firstPageOccupationGroups = givenOccupationGroups.slice(-2);

const limit = 2;
const firstPageCursor = Buffer.from(
JSON.stringify({ id: givenOccupationGroups[2].id, createdAt: givenOccupationGroups[2].createdAt })
).toString("base64");

const expectedNextCursor = null;

// AND the user is not model manager
checkRole.mockResolvedValueOnce(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findParent: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: firstPageOccupationGroups, nextCursor: null }),
findChildren: jest.fn().mockResolvedValue([]),
validateModelForOccupationGroup: jest.fn().mockResolvedValue(null),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event and the modelId as path parameter
const actualResponse = await occupationGroupHandler({
...givenEvent,
queryStringParameters: { limit: limit.toString(), cursor: firstPageCursor },
path: `/models/${givenModelId}/occupationGroups`,
} as never);

const expectedFirstPageOccupationGroups = {
data: firstPageOccupationGroups,
limit: limit,
nextCursor: expectedNextCursor,
};

// THEN expect the handler to return the OK status
expect(actualResponse.statusCode).toEqual(StatusCodes.OK);
expect(actualResponse.headers).toMatchObject({
"Content-Type": "application/json",
});

// AND the response body contains only the first page OccupationGroups
expect(JSON.parse(actualResponse.body)).toMatchObject({
...expectedFirstPageOccupationGroups,
data: expect.arrayContaining(
expectedFirstPageOccupationGroups.data.map((og) =>
expect.objectContaining({
UUID: og.UUID,
UUIDHistory: og.UUIDHistory,
code: og.code,
originUri: og.originUri,
preferredLabel: og.preferredLabel,
altLabels: og.altLabels,
groupType: og.groupType,
description: og.description,
id: og.id,
modelId: og.modelId,
createdAt: og.createdAt.toISOString(),
updatedAt: og.updatedAt.toISOString(),
path: `${givenResourcesBaseUrl}/models/${og.modelId}/occupationGroups/${og.id}`,
tabiyaPath: `${givenResourcesBaseUrl}/models/${og.modelId}/occupationGroups/${og.UUID}`,
})
)
),
});
});

test("GET should return nextCursor when nextCursor is present in the paginated occupation group result", async () => {
// GIVEN role check passes for anonymous access
checkRole.mockResolvedValueOnce(true);

const limit = 1;
const givenOccupationGroups: Array<IOccupationGroup> = [
{
...getIOccupationGroupMockData(1, givenModelId),
UUID: "foo",
UUIDHistory: ["foo"],
importId: randomUUID(),
},
{
...getIOccupationGroupMockData(2, givenModelId),
UUID: "bar",
UUIDHistory: ["bar"],
importId: randomUUID(),
},
];

// AND a service that will successfully get the occupation groups (returns 2 items for limit 1)
const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findParent: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({
items: [givenOccupationGroups[0]],
nextCursor: { _id: givenOccupationGroups[1].id, createdAt: givenOccupationGroups[0].createdAt },
}),
validateModelForOccupationGroup: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event and limit 1
const actualResponse = await occupationGroupHandler({
...givenEvent,
queryStringParameters: { limit: limit.toString() },
path: `/models/${givenModelId}/occupationGroups`,
} as never);

// THEN expect the handler to return the OK status
expect(actualResponse.statusCode).toEqual(StatusCodes.OK);
expect(actualResponse.headers).toMatchObject({
"Content-Type": "application/json",
});

// verify the service was called correctly
expect(getServiceRegistry().occupationGroup.findPaginated).toHaveBeenCalledWith(givenModelId, undefined, limit);
// AND the response body contains a nextCursor (base64 encoded)
const responseBody = JSON.parse(actualResponse.body);
expect(responseBody.nextCursor).toBeDefined();
expect(typeof responseBody.nextCursor).toBe("string");

// Verify it's a valid base64 string by decoding it
const decodedCursor = Buffer.from(responseBody.nextCursor, "base64").toString("utf-8");
expect(JSON.parse(decodedCursor)).toHaveProperty("id");
expect(JSON.parse(decodedCursor)).toHaveProperty("createdAt");
});

test("GET should respond with the BAD_REQUEST status code if the modelId is not passed as a path parameter", async () => {
// AND GIVEN the repository fails to get the occupationGroups
const firstPageCursorObject = {
id: getMockStringId(1),
createdAt: new Date(),
};
const firstPageCursor = Buffer.from(JSON.stringify(firstPageCursorObject)).toString("base64");

const limit = 2;

const givenBadEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
queryStringParameters: {},
};
// AND the user is not model manager
checkRole.mockResolvedValueOnce(true);

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler({
...givenBadEvent,
queryStringParameters: { limit: limit.toString(), cursor: firstPageCursor },
path: `/models//occupationGroups`,
} as never);

// THEN expect the handler to return the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);
// AND the response body contains the error information
const parsedMissing = JSON.parse(actualResponse.body);
expect(parsedMissing).toMatchObject({
errorCode:
OccupationGroupAPISpecs.GET.Enums.Response.Status500.ErrorCodes.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUPS,
message: "modelId is missing in the path",
});
expect(typeof parsedMissing.details).toBe("string");
});
test("GET should respond with the BAD_REQUEST status code if the modelId is not correct model id", async () => {
// AND GIVEN the repository fails to get the occupationGroups
const firstPageCursorObject = {
id: getMockStringId(1),
createdAt: new Date(),
};
const firstPageCursor = Buffer.from(JSON.stringify(firstPageCursorObject)).toString("base64");

const limit = 2;

const givenBadEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: "foo" },
queryStringParameters: {},
path: `/models/foo/occupationGroups`,
};

// AND the user is not model manager
checkRole.mockResolvedValueOnce(true);

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler({
...givenBadEvent,
queryStringParameters: { limit: limit.toString(), cursor: firstPageCursor },
} as never);

// THEN expect the handler to return the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);
// AND the response body contains the error information
const parsedInvalidModel = JSON.parse(actualResponse.body);
expect(parsedInvalidModel).toMatchObject({
errorCode: ErrorAPISpecs.Constants.ErrorCodes.INVALID_JSON_SCHEMA,
message: ErrorAPISpecs.Constants.ReasonPhrases.INVALID_JSON_SCHEMA,
});
expect(typeof parsedInvalidModel.details).toBe("string");
});
test("GET should respond with the BAD_REQUEST status code if the query parameter is not valid query parameter", async () => {
// GIVEN the repository fails to get the occupationGroups
const firstPageCursorObject = {
id: getMockStringId(1),
createdAt: new Date(),
};
const firstPageCursor = Buffer.from(JSON.stringify(firstPageCursorObject)).toString("base64");

// AND the user is not model manager
checkRole.mockResolvedValueOnce(true);

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler({
...givenEvent,
queryStringParameters: { limit: "foo", cursor: firstPageCursor },
path: `/models/${givenModelId}/occupationGroups`,
} as never);

// THEN expect the handler to return the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);
// AND the response body contains the error information
const parsedInvalidQuery = JSON.parse(actualResponse.body);
expect(parsedInvalidQuery).toMatchObject({
errorCode: ErrorAPISpecs.Constants.GET.ErrorCodes.INVALID_QUERY_PARAMETER,
message: ErrorAPISpecs.Constants.ReasonPhrases.INVALID_JSON_SCHEMA,
});
expect(typeof parsedInvalidQuery.details).toBe("string");
});

test("GET should respond with the NOT_FOUND if the model does not exist", async () => {
// GIVEN a service that model does not exists
const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
findPaginated: jest.fn(),
validateModelForOccupationGroup: jest
.fn()
.mockResolvedValue(ModelForOccupationGroupValidationErrorCode.MODEL_NOT_FOUND_BY_ID),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;
checkRole.mockResolvedValueOnce(true);
// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler({
...givenEvent,
path: `/models/${givenModelId}/occupationGroups`,
} as never);

// THEN expect the handler to return the NOT_FOUND status
expect(actualResponse.statusCode).toEqual(StatusCodes.NOT_FOUND);

// AND the response body contains the error information
const expectedErrorBody = {
errorCode: OccupationGroupAPISpecs.GET.Enums.Response.Status404.ErrorCodes.MODEL_NOT_FOUND,
message: "Model not found",
details: `No model found with id: ${givenModelId}`,
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET should respond with the INTERNAL_SERVER_ERROR if the model validation failed to fetch data from database", async () => {
// GIVEN a service that model does not exists
const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findParent: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn(),
findChildren: jest.fn(),
validateModelForOccupationGroup: jest
.fn()
.mockResolvedValue(ModelForOccupationGroupValidationErrorCode.FAILED_TO_FETCH_FROM_DB),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;
checkRole.mockResolvedValueOnce(true);
// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler({
...givenEvent,
path: `/models/${givenModelId}/occupationGroups`,
} as never);

// THEN expect the handler to return the INTERNAL_SERVER_ERROR status
expect(actualResponse.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);

// AND the response body contains the error information
const expectedErrorBody = {
errorCode:
OccupationGroupAPISpecs.GET.Enums.Response.Status500.ErrorCodes.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUPS,
message: "Failed to fetch the model details from the DB",
details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET should respond with the BAD_REQUEST if the cursor decoding failed", async () => {
// GIVEN a service that model does not exists
const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn(),
findParent: jest.fn().mockResolvedValue(null),
validateModelForOccupationGroup: jest.fn(),
findChildren: jest.fn(),
decodeCursor: jest.fn().mockImplementation(() => {
throw new Error("Failed to decode cursor");
}),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;
checkRole.mockResolvedValueOnce(true);
const cursor = Buffer.from(getRandomString(10)).toString("base64");
// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler({
...givenEvent,
queryStringParameters: { cursor },
path: `/models/${givenModelId}/occupationGroups`,
} as never);

// THEN expect the handler to return the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);

// AND the response body contains the error information
const expectedErrorBody = {
errorCode: ErrorAPISpecs.Constants.GET.ErrorCodes.INVALID_QUERY_PARAMETER,
message: "Invalid cursor parameter",

details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});

test("GET should respond with the INTERNAL_SERVER_ERROR status code if the repository fails to get the occupationGroups", async () => {
// AND GIVEN the repository fails to get the occupationGroups
const firstPageCursorObject = {
id: getMockStringId(1),
createdAt: new Date(),
};
const firstPageCursor = Buffer.from(JSON.stringify(firstPageCursorObject)).toString("base64");

const givenOccupationGroupRepositoryMock = {
Model: undefined as never,
hierarchyModel: undefined as never,
create: jest.fn().mockResolvedValue(null),
createMany: jest.fn().mockResolvedValue([]),
findById: jest.fn().mockResolvedValue(null),
findAll: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockRejectedValue(new Error("foo")),
getOccupationGroupByUUID: jest.fn().mockResolvedValue(null),
getHistory: jest.fn().mockResolvedValue([]),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
};
jest.spyOn(getRepositoryRegistry(), "OccupationGroup", "get").mockReturnValue(givenOccupationGroupRepositoryMock);
const limit = 2;

// AND the user is not model manager
checkRole.mockResolvedValueOnce(true);

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler({
...givenEvent,
queryStringParameters: { limit: limit.toString(), cursor: firstPageCursor },
path: `/models/${givenModelId}/occupationGroups`,
} as never);

// THEN expect the handler to return the INTERNAL_SERVER_ERROR status
expect(actualResponse.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.GET.Enums.Response.Status500.ErrorCodes.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUPS,
message: "Failed to retrieve the occupation groups from the DB",
details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
testMethodsNotAllowed(
[HTTP_VERBS.PUT, HTTP_VERBS.DELETE, HTTP_VERBS.OPTIONS, HTTP_VERBS.PATCH],
occupationGroupHandler
);
test("should route GET occupationGroups list to getHandler", async () => {
const event = { httpMethod: HTTP_VERBS.GET, path: "/models/1/occupationGroups" } as APIGatewayProxyEvent;
const response = await handler(event);
expect(getHandler).toHaveBeenCalledWith(event);
expect(response.body).toBe("GET");
});

describe("GET individual occupation group", () => {
test("GET /models/{modelId}/occupationGroups/{id} should return the occupation group for a valid ID", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

// AND a configured base path for resource
const givenResourcesBaseUrl = "https://some/path/to/api/resources";
jest.spyOn(config, "getResourcesBaseUrl").mockReturnValueOnce(givenResourcesBaseUrl);

// AND a repository that will successfully get the occupation group
const givenOccupationGroup: IOccupationGroup = {
...getIOccupationGroupMockData(1, givenModelId),
id: givenOccupationGroupId,
UUID: "test-uuid",
UUIDHistory: ["test-uuid"],
importId: randomUUID(),
};

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(givenOccupationGroup),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest.fn(),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// THEN respond with the OK status code
expect(actualResponse.statusCode).toEqual(StatusCodes.OK);
// AND the handler to return the correct headers
expect(actualResponse.headers).toMatchObject({
"Content-Type": "application/json",
});
// AND the transformation function is called correctly
expect(transformModule.transform).toHaveBeenCalledWith(givenOccupationGroup, givenResourcesBaseUrl);
// AND the handler to return the expected result
expect(JSON.parse(actualResponse.body)).toMatchObject(transformSpy.mock.results[0].value);
});

test("GET /models/{modelId}/occupationGroups/{id} should respond with NOT_FOUND if model does not exist", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn(),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest
.fn()
.mockResolvedValue(ModelForOccupationGroupValidationErrorCode.MODEL_NOT_FOUND_BY_ID),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// AND respond with the NOT_FOUND status
expect(actualResponse.statusCode).toEqual(StatusCodes.NOT_FOUND);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode: OccupationGroupAPISpecs.OccupationGroup.GET.Enums.Response.Status404.ErrorCodes.MODEL_NOT_FOUND,
message: "Model not found",
details: `No model found with id: ${givenModelId}`,
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET /models/{modelId}/occupationGroups/{id} should respond with INTERNAL_SERVER_ERROR if model validator function failed to fetch from db", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn(),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest
.fn()
.mockResolvedValue(ModelForOccupationGroupValidationErrorCode.FAILED_TO_FETCH_FROM_DB),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// AND respond with the INTERNAL_SERVER_ERROR status
expect(actualResponse.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.GET.Enums.Response.Status500.ErrorCodes
.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUP_DETAIL,
message: "Failed to fetch the model details from the DB",
details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});

test("GET /models/{modelId}/occupationGroups/{id} should response with BAD_REQUEST if the path validation failed ", async () => {
jest.doMock("validator", () => ({
ajvInstance: {
getSchema: jest.fn(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const validateFn: any = jest.fn().mockReturnValue(false);

validateFn.errors = [{ instancePath: "/id", message: "invalid id" }];

return validateFn;
}),
},
}));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let handler: any;

jest.isolateModules(() => {
({ handler = handler } = require("./index"));
});

const givenModelId = getMockStringId(100);
const givenOccupationGroupId = getMockStringId(100);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}`,
} as never;
// AND User has the required role
checkRole.mockResolvedValue(true);

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await handler(givenEvent);

// AND respond with the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode: ErrorAPISpecs.Constants.ErrorCodes.INVALID_JSON_SCHEMA,
message: ErrorAPISpecs.Constants.ReasonPhrases.INVALID_JSON_SCHEMA,
details: expect.stringContaining("modelId"),
};

expect(JSON.parse(actualResponse.body)).toMatchObject({
errorCode: ErrorAPISpecs.Constants.ErrorCodes.INVALID_JSON_SCHEMA,
message: ErrorAPISpecs.Constants.ReasonPhrases.INVALID_JSON_SCHEMA,
});
expect(typeof JSON.parse(actualResponse.body).details).toBe("string");
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});

test("GET /models/{modelId}/occupationGroups/{id} should respond with NOT_FOUND if occupation group is not found", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModel: IModelInfo = {
...getIModelInfoMockData(1),
UUID: "foo",
UUIDHistory: ["foo"],
released: false,
};
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModel.id.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModel.id}/occupationGroups/${givenOccupationGroupId}`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findParent: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest.fn(),
findChildren: jest.fn().mockResolvedValue([]),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);
// THEN respond with the NOT_FOUND status
expect(actualResponse.statusCode).toEqual(StatusCodes.NOT_FOUND);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.GET.Enums.Response.Status404.ErrorCodes.OCCUPATION_GROUP_NOT_FOUND,
message: "Occupation group not found",
details: `No occupation group found with id: ${givenOccupationGroupId}`,
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});

test("GET /models/{modelId}/occupationGroups/{id} should respond with BAD_REQUEST if modelId is missing", async () => {
const givenOccupationGroupId = getMockStringId(2);

const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models//occupationGroups/${givenOccupationGroupId}`,
} as never;

// AND role check passes for anonymous access
checkRole.mockResolvedValueOnce(true);

// WHEN the occupation group handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent as never);

// THEN expect the handler to return the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);
// AND the response body contains the error information
const parsedMissing = JSON.parse(actualResponse.body);
expect(parsedMissing).toMatchObject({
errorCode:
OccupationGroupAPISpecs.GET.Enums.Response.Status500.ErrorCodes.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUPS,
message: "modelId is missing in the path",
});
expect(typeof parsedMissing.details).toBe("string");
});

test("GET /models/{modelId}/occupationGroups/{id} should respond with INTERNAL_SERVER_ERROR if repository throws an error", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModel: IModelInfo = {
...getIModelInfoMockData(1),
UUID: "foo",
UUIDHistory: ["foo"],
released: false,
};
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModel.id.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModel.id}/occupationGroups/${givenOccupationGroupId}`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

// AND a repository that will throw an error
const givenOccupationGroupRepositoryMock = {
Model: undefined as never,
hierarchyModel: undefined as never,
create: jest.fn().mockResolvedValue(null),
createMany: jest.fn().mockResolvedValue([]),
findById: jest.fn().mockRejectedValue(new Error("Database connection failed")),
findAll: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
getOccupationGroupByUUID: jest.fn().mockResolvedValue(null),
getHistory: jest.fn().mockResolvedValue([]),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
};
jest.spyOn(getRepositoryRegistry(), "OccupationGroup", "get").mockReturnValue(givenOccupationGroupRepositoryMock);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockRejectedValue(new Error("foo")),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest.fn(),
findChildren: jest.fn().mockResolvedValue([]),
findParent: jest.fn().mockResolvedValue(null),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// THEN respond with the INTERNAL_SERVER_ERROR status
expect(actualResponse.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.GET.Enums.Response.Status500.ErrorCodes
.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUP_DETAIL,
message: "Failed to retrieve the occupation group from the DB",
details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("should route GET occupationGroup by ID to getByIdHandler", async () => {
const event = { httpMethod: HTTP_VERBS.GET, path: "/models/1/occupationGroups/2" } as APIGatewayProxyEvent;
const response = await handler(event);
expect(getByIdHandler).toHaveBeenCalledWith(event);
expect(response.body).toBe("GET_BY_ID");
});

describe("GET parent occupation group", () => {
test("GET /models/{modelId}/occupationGroups/{id}/parent should return the parent occupation group for a valid ID", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}/parent`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);
// AND a configured base path for resource
const givenResourcesBaseUrl = "https://some/path/to/api/resources";
jest.spyOn(config, "getResourcesBaseUrl").mockReturnValueOnce(givenResourcesBaseUrl);

// AND a repository that will successfully get the occupation group parent
const givenOccupationGroup: IOccupationGroup = {
...getIOccupationGroupMockData(1, givenModelId),
id: getMockStringId(3),
UUID: "test-uuid",
UUIDHistory: ["test-uuid"],
importId: randomUUID(),
};
const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest.fn(),
findChildren: jest.fn().mockResolvedValue([]),
findParent: jest.fn().mockResolvedValue(givenOccupationGroup),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);
// THEN respond with the OK status code
expect(actualResponse.statusCode).toEqual(StatusCodes.OK);
// AND the handler to return the correct headers
expect(actualResponse.headers).toMatchObject({
"Content-Type": "application/json",
});
// AND the handler to return the expected result
expect(JSON.parse(actualResponse.body)).toMatchObject({
id: givenOccupationGroup.id,
UUID: givenOccupationGroup.UUID,
});
});
test("GET /models/{modelId}/occupationGroups/{id}/parent should respond with NOT_FOUND if model does not exist", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}/parent`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn(),
findParent: jest.fn(),
findChildren: jest.fn(),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest
.fn()
.mockResolvedValue(ModelForOccupationGroupValidationErrorCode.MODEL_NOT_FOUND_BY_ID),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// AND respond with the NOT_FOUND status
expect(actualResponse.statusCode).toEqual(StatusCodes.NOT_FOUND);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.Parent.GET.Enums.Response.Status404.ErrorCodes.MODEL_NOT_FOUND,
message: "Model not found",
details: `No model found with id: ${givenModelId}`,
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET /models/{modelId}/occupationGroups/{id}/parent should respond with INTERNAL_SERVER_ERROR if model validator function failed to fetch from db", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}/parent`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn(),
findParent: jest.fn(),
findChildren: jest.fn(),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest
.fn()
.mockResolvedValue(ModelForOccupationGroupValidationErrorCode.FAILED_TO_FETCH_FROM_DB),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// AND respond with the INTERNAL_SERVER_ERROR status
expect(actualResponse.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.Parent.GET.Enums.Response.Status500.ErrorCodes
.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUP_PARENT,
message: "Failed to fetch the model details from the DB",
details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET /models/{modelId}/occupationGroups/{id}/parent should response with BAD_REQUEST if the path validation failed ", async () => {
jest.doMock("validator", () => ({
ajvInstance: {
getSchema: jest.fn(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const validateFn: any = jest.fn().mockReturnValue(false);

validateFn.errors = [{ instancePath: "/id", message: "invalid id" }];

return validateFn;
}),
},
}));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let handler: any;

jest.isolateModules(() => {
({ handler = handler } = require("./index"));
});
const givenModelId = getMockStringId(100);
const givenOccupationGroupId = getMockStringId(100);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}/parent`,
} as never;
// AND User has the required role
checkRole.mockResolvedValue(true);

// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode: ErrorAPISpecs.Constants.ErrorCodes.INVALID_JSON_SCHEMA,
message: ErrorAPISpecs.Constants.ReasonPhrases.INVALID_JSON_SCHEMA,
details: expect.stringContaining("modelId"),
};

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await handler(givenEvent);

// AND respond with the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);

expect(JSON.parse(actualResponse.body)).toMatchObject({
errorCode: ErrorAPISpecs.Constants.ErrorCodes.INVALID_JSON_SCHEMA,
message: ErrorAPISpecs.Constants.ReasonPhrases.INVALID_JSON_SCHEMA,
});
expect(typeof JSON.parse(actualResponse.body).details).toBe("string");
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET /models/{modelId}/occupationGroups/{id}/parent should respond with NOT_FOUND if occupation group is not found", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModel: IModelInfo = {
...getIModelInfoMockData(1),
UUID: "foo",
UUIDHistory: ["foo"],
released: false,
};
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModel.id.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModel.id}/occupationGroups/${givenOccupationGroupId}/parent`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest.fn(),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);
// THEN respond with the NOT_FOUND status
expect(actualResponse.statusCode).toEqual(StatusCodes.NOT_FOUND);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.Parent.GET.Enums.Response.Status404.ErrorCodes
.OCCUPATION_GROUP_PARENT_NOT_FOUND,
message: "Occupation group or parent not found",
details: `No occupation group or parent found with occupation group id: ${givenOccupationGroupId}`,
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET /models/{modelId}/occupationGroups/{id}/parent should respond with BAD_REQUEST if modelId is missing", async () => {
const givenOccupationGroupId = getMockStringId(2);

const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models//occupationGroups/${givenOccupationGroupId}/parent`,
} as never;

// AND role check passes for anonymous access
checkRole.mockResolvedValueOnce(true);

// WHEN the occupation group handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent as never);

// THEN expect the handler to return the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);
// AND the response body contains the error information
const parsedMissing = JSON.parse(actualResponse.body);
expect(parsedMissing).toMatchObject({
errorCode:
OccupationGroupAPISpecs.GET.Enums.Response.Status500.ErrorCodes.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUPS,
message: "modelId is missing in the path",
});
expect(typeof parsedMissing.details).toBe("string");
});
test("GET /models/{modelId}/occupationGroups/{id}/parent should respond with INTERNAL_SERVER_ERROR if repository throws an error", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModel: IModelInfo = {
...getIModelInfoMockData(1),
UUID: "foo",
UUIDHistory: ["foo"],
released: false,
};
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModel.id.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModel.id}/occupationGroups/${givenOccupationGroupId}/parent`,
} as never;

// AND User has the required role
checkRole.mockResolvedValue(true);

// AND a repository that will throw an error
const givenOccupationGroupRepositoryMock = {
Model: undefined as never,
hierarchyModel: undefined as never,
create: jest.fn().mockResolvedValue(null),
createMany: jest.fn().mockResolvedValue([]),
findById: jest.fn().mockRejectedValue(new Error("Database connection failed")),
findAll: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
getOccupationGroupByUUID: jest.fn().mockResolvedValue(null),
getHistory: jest.fn().mockResolvedValue([]),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue([]),
};
jest.spyOn(getRepositoryRegistry(), "OccupationGroup", "get").mockReturnValue(givenOccupationGroupRepositoryMock);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest.fn(),
findChildren: jest.fn().mockResolvedValue([]),
findParent: jest.fn().mockRejectedValue(new Error("foo")),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// THEN respond with the INTERNAL_SERVER_ERROR status
expect(actualResponse.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.Parent.GET.Enums.Response.Status500.ErrorCodes
.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUP_PARENT,
message: "Failed to retrieve the parent occupation group from the DB",
details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("should route GET parent to getParentHandler", async () => {
const event = { httpMethod: HTTP_VERBS.GET, path: "/models/1/occupationGroups/2/parent" } as APIGatewayProxyEvent;
const response = await handler(event);
expect(getParentHandler).toHaveBeenCalledWith(event);
expect(response.body).toBe("GET_PARENT");
});

describe("GET children of occupation group", () => {
test("GET /models/{modelId}/occupationGroups/{id}/children should return the children of a given occupation group id", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}/children`,
} as never;
// AND User has the required role
checkRole.mockResolvedValue(true);
// AND a configured base path for resource
const givenResourcesBaseUrl = "https://some/path/to/api/resources";
jest.spyOn(config, "getResourcesBaseUrl").mockReturnValueOnce(givenResourcesBaseUrl);

// AND a repository that will successfully get the occupation group children
const givenOccupationGroup: IOccupationGroupChild = {
...getIOccupationGroupMockData(1, givenModelId),
id: getMockStringId(3),
UUID: "test-uuid",
UUIDHistory: ["test-uuid"],
parentId: "",
objectType: ObjectTypes.ISCOGroup,
};
const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest.fn(),
findChildren: jest.fn().mockResolvedValue([givenOccupationGroup]),
findParent: jest.fn().mockResolvedValue(null),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;
// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);
// THEN respond with the OK status code
expect(actualResponse.statusCode).toEqual(StatusCodes.OK);
// AND the handler to return the correct headers
expect(actualResponse.headers).toMatchObject({
"Content-Type": "application/json",
});
// AND the handler to return the expected result
expect(JSON.parse(actualResponse.body)).toMatchObject({
data: expect.any(Array),
limit: null,
nextCursor: null,
});
});
test("GET /models/{modelId}/occupationGroups/{id}/children should respond with NOT_FOUND if model does not exist", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}/children`,
} as never;
// AND User has the required role
checkRole.mockResolvedValue(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn(),
findParent: jest.fn(),
findChildren: jest.fn(),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest
.fn()
.mockResolvedValue(ModelForOccupationGroupValidationErrorCode.MODEL_NOT_FOUND_BY_ID),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// AND respond with the NOT_FOUND status
expect(actualResponse.statusCode).toEqual(StatusCodes.NOT_FOUND);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.Parent.GET.Enums.Response.Status404.ErrorCodes.MODEL_NOT_FOUND,
message: "Model not found",
details: `No model found with id: ${givenModelId}`,
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET /models/{modelId}/occupationGroups/{id}/children should respond with INTERNAL_SERVER_ERROR if model validator function failed to fetch from db", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModelId = getMockStringId(1);
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}/children`,
} as never;
// AND User has the required role
checkRole.mockResolvedValue(true);

const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn(),
findParent: jest.fn(),
findChildren: jest.fn(),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest
.fn()
.mockResolvedValue(ModelForOccupationGroupValidationErrorCode.FAILED_TO_FETCH_FROM_DB),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// AND respond with the INTERNAL_SERVER_ERROR status
expect(actualResponse.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.Children.GET.Enums.Response.Status500.ErrorCodes
.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUP_CHILDREN,
message: "Failed to fetch the model details from the DB",
details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET /models/{modelId}/occupationGroups/{id}/children should response with BAD_REQUEST if the path validation failed", async () => {
jest.doMock("validator", () => ({
ajvInstance: {
getSchema: jest.fn(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const validateFn: any = jest.fn().mockReturnValue(false);

validateFn.errors = [{ instancePath: "/id", message: "invalid id" }];

return validateFn;
}),
},
}));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let handler: any;

jest.isolateModules(() => {
({ handler = handler } = require("./index"));
});

const givenModelId = getMockStringId(100);
const givenOccupationGroupId = getMockStringId(100);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModelId.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModelId}/occupationGroups/${givenOccupationGroupId}/children`,
} as never;
// AND User has the required role
checkRole.mockResolvedValue(true);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode: ErrorAPISpecs.Constants.ErrorCodes.INVALID_JSON_SCHEMA,
message: ErrorAPISpecs.Constants.ReasonPhrases.INVALID_JSON_SCHEMA,
details: expect.stringContaining("modelId"),
};

// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await handler(givenEvent);
// AND respond with the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);

expect(JSON.parse(actualResponse.body)).toMatchObject({
errorCode: ErrorAPISpecs.Constants.ErrorCodes.INVALID_JSON_SCHEMA,
message: ErrorAPISpecs.Constants.ReasonPhrases.INVALID_JSON_SCHEMA,
});
expect(typeof JSON.parse(actualResponse.body).details).toBe("string");
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("GET /models/{modelId}/occupationGroups/{id}/children should respond with BAD_REQUEST if modelId is missing", async () => {
const givenOccupationGroupId = getMockStringId(2);

const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models//occupationGroups/${givenOccupationGroupId}/children`,
} as never;
// AND role check passes for anonymous access
checkRole.mockResolvedValueOnce(true);

// WHEN the occupation group handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent as never);

// THEN expect the handler to return the BAD_REQUEST status
expect(actualResponse.statusCode).toEqual(StatusCodes.BAD_REQUEST);
// AND the response body contains the error information
const parsedMissing = JSON.parse(actualResponse.body);
expect(parsedMissing).toMatchObject({
errorCode:
OccupationGroupAPISpecs.GET.Enums.Response.Status500.ErrorCodes.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUPS,
message: "modelId is missing in the path",
});
expect(typeof parsedMissing.details).toBe("string");
});
test("GET /models/{modelId}/occupationGroups/{id}/children should respond with INTERNAL_SERVER_ERROR if repository throws an error", async () => {
// GIVEN a valid request with modelId and occupationGroup ID
const givenModel: IModelInfo = {
...getIModelInfoMockData(1),
UUID: "foo",
UUIDHistory: ["foo"],
released: false,
};
const givenOccupationGroupId = getMockStringId(2);
const givenEvent = {
httpMethod: HTTP_VERBS.GET,
headers: {},
pathParameters: { modelId: givenModel.id.toString(), id: givenOccupationGroupId.toString() },
queryStringParameters: {},
path: `/models/${givenModel.id}/occupationGroups/${givenOccupationGroupId}/children`,
} as never;
// AND User has the required role
checkRole.mockResolvedValue(true);

// AND a repository that will throw an error
const givenOccupationGroupRepositoryMock = {
Model: undefined as never,
hierarchyModel: undefined as never,
create: jest.fn().mockResolvedValue(null),
createMany: jest.fn().mockResolvedValue([]),
findById: jest.fn().mockRejectedValue(null),
findAll: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
getOccupationGroupByUUID: jest.fn().mockResolvedValue(null),
getHistory: jest.fn().mockResolvedValue([]),
findParent: jest.fn().mockResolvedValue(null),
findChildren: jest.fn().mockResolvedValue(new Error("Database connection failed")),
};
jest.spyOn(getRepositoryRegistry(), "OccupationGroup", "get").mockReturnValue(givenOccupationGroupRepositoryMock);
const givenOccupationGroupServiceMock = {
create: jest.fn(),
findById: jest.fn().mockResolvedValue(null),
findPaginated: jest.fn().mockResolvedValue({ items: [], nextCursor: null }),
validateModelForOccupationGroup: jest.fn(),
findChildren: jest.fn().mockResolvedValue(new Error("foo")),
findParent: jest.fn().mockRejectedValue(null),
} as IOccupationGroupService;
const mockServiceRegistry = mockGetServiceRegistry();
mockServiceRegistry.occupationGroup = givenOccupationGroupServiceMock;
// WHEN the occupationGroup handler is invoked with the given event
const actualResponse = await occupationGroupHandler(givenEvent);

// THEN respond with the INTERNAL_SERVER_ERROR status
expect(actualResponse.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR);
// AND the response body contains the error information
const expectedErrorBody: ErrorAPISpecs.Types.Payload = {
errorCode:
OccupationGroupAPISpecs.OccupationGroup.Children.GET.Enums.Response.Status500.ErrorCodes
.DB_FAILED_TO_RETRIEVE_OCCUPATION_GROUP_CHILDREN,
message: "Failed to retrieve the occupation group children from the DB",
details: "",
};
expect(JSON.parse(actualResponse.body)).toEqual(expectedErrorBody);
});
test("should route GET children to getChildrenHandler", async () => {
const event = { httpMethod: HTTP_VERBS.GET, path: "/models/1/occupationGroups/2/children" } as APIGatewayProxyEvent;
const response = await handler(event);
expect(getChildrenHandler).toHaveBeenCalledWith(event);
expect(response.body).toBe("GET_CHILDREN");
});
Comment on lines +62 to 67
test("should handle missing path in GET request", async () => {
const event = { httpMethod: HTTP_VERBS.GET } as unknown as APIGatewayProxyEvent;
const response = await handler(event);
expect(getHandler).toHaveBeenCalledWith(event);
expect(response.body).toBe("GET");
});
Comment on lines +22 to +23
createdAt,
});
@irumvanselme irumvanselme merged commit 13a3ee2 into main May 21, 2026
13 checks passed
@irumvanselme irumvanselme deleted the fix/refactoring_the_root_test_and_coverage_test branch May 21, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants