Skip to content

Commit

Permalink
[TM-1401] Specs for the entity service.
Browse files Browse the repository at this point in the history
  • Loading branch information
roguenet committed Dec 5, 2024
1 parent acd04e8 commit e4ae654
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 14 deletions.
53 changes: 53 additions & 0 deletions apps/entity-service/src/trees/research.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ResearchService } from "./research.service";
import { Test } from "@nestjs/testing";
import { TreeSpeciesResearch } from "@terramatch-microservices/database/entities";
import { TreeSpeciesResearchFactory } from "@terramatch-microservices/database/factories/tree-species-research.factory";
import { pick } from "lodash";
import { faker } from "@faker-js/faker";

describe("ResearchService", () => {
let service: ResearchService;

beforeAll(async () => {
await TreeSpeciesResearch.truncate();
});

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [ResearchService]
}).compile();

service = module.get(ResearchService);
});

afterEach(() => {
jest.restoreAllMocks();
});

it("should return an empty array with no matches", async () => {
const result = await service.searchScientificNames("test");
expect(result.length).toBe(0);
});

it("should return the matching entries", async () => {
const tree1 = await TreeSpeciesResearchFactory.create({ scientificName: "Lorem asdfium" });
const tree2 = await TreeSpeciesResearchFactory.create({ scientificName: "Lorem qasdium" });
const tree3 = await TreeSpeciesResearchFactory.create({ scientificName: "Ipsum loremium" });
await TreeSpeciesResearchFactory.create({ scientificName: "Alorem ipsium" });
await TreeSpeciesResearchFactory.create({ scientificName: "Fakem ipslorem" });
const result = await service.searchScientificNames("lore");
expect(result.length).toBe(3);
expect(result).toContainEqual(pick(tree1, ["taxonId", "scientificName"]));
expect(result).toContainEqual(pick(tree2, ["taxonId", "scientificName"]));
expect(result).toContainEqual(pick(tree3, ["taxonId", "scientificName"]));
});

it("should return 10 entries maximum", async () => {
for (let ii = 0; ii < 12; ii++) {
await TreeSpeciesResearchFactory.create({ scientificName: `Tree${faker.word.words()}` });
}

const result = await service.searchScientificNames("tree");
expect(result.length).toBe(10);
});
});
28 changes: 15 additions & 13 deletions apps/entity-service/src/trees/research.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import { Injectable } from "@nestjs/common";

@Injectable()
export class ResearchService {
async searchScientificNames(search: string): Promise<{ taxonId: string; scientificName: string }[]> {
return await TreeSpeciesResearch.findAll({
where: {
[Op.or]: [
// By checking these two, we're limiting the search term to only occurrences at the
// beginning of a word in the scientific name, which tends to lead to better results.
{ scientificName: { [Op.like]: `${search}%` } },
{ scientificName: { [Op.like]: `% ${search}%` } }
]
},
attributes: ["taxonId", "scientificName"],
limit: 10
});
async searchScientificNames(search: string) {
return (
await TreeSpeciesResearch.findAll({
where: {
[Op.or]: [
// By checking these two, we're limiting the search term to only occurrences at the
// beginning of a word in the scientific name, which tends to lead to better results.
{ scientificName: { [Op.like]: `${search}%` } },
{ scientificName: { [Op.like]: `% ${search}%` } }
]
},
attributes: ["taxonId", "scientificName"],
limit: 10
})
).map(({ taxonId, scientificName }) => ({ taxonId, scientificName }));
}
}
44 changes: 44 additions & 0 deletions apps/entity-service/src/trees/trees.conroller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { TreesController } from "./trees.controller";
import { ResearchService } from "./research.service";
import { Test } from "@nestjs/testing";
import { createMock, DeepMocked } from "@golevelup/ts-jest";
import { BadRequestException } from "@nestjs/common";
import { Resource } from "@terramatch-microservices/common/util";

describe("TreesController", () => {
let controller: TreesController;
let researchService: DeepMocked<ResearchService>;

beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [TreesController],
providers: [{ provide: ResearchService, useValue: (researchService = createMock<ResearchService>()) }]
}).compile();

controller = module.get(TreesController);
});

afterEach(() => {
jest.restoreAllMocks();
});

it("should throw if the search param is missing", async () => {
await expect(controller.searchScientificNames("")).rejects.toThrow(BadRequestException);
await expect(controller.searchScientificNames(null)).rejects.toThrow(BadRequestException);
});

it("should return the tree species from the service in order", async () => {
researchService.searchScientificNames.mockResolvedValue([
{ taxonId: "wfo-0000002583", scientificName: "Cirsium carolinianum" },
{ taxonId: "wfo-0000003963", scientificName: "Cirsium carniolicum" }
]);
const result = await controller.searchScientificNames("cirs");
expect(researchService.searchScientificNames).toHaveBeenCalledWith("cirs");
const data = result.data as Resource[];
expect(data.length).toBe(2);
expect(data[0].id).toBe("wfo-0000002583");
expect(data[0].attributes.scientificName).toBe("Cirsium carolinianum");
expect(data[1].id).toBe("wfo-0000003963");
expect(data[1].attributes.scientificName).toBe("Cirsium carniolicum");
});
});
3 changes: 2 additions & 1 deletion apps/entity-service/src/trees/trees.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { buildJsonApi } from "@terramatch-microservices/common/util";
import { ScientificNameDto } from "./dto/scientific-name.dto";
import { ApiOperation } from "@nestjs/swagger";
import { JsonApiResponse } from "@terramatch-microservices/common/decorators";
import { isEmpty } from "lodash";

@Controller("trees/v3")
export class TreesController {
Expand All @@ -16,7 +17,7 @@ export class TreesController {
})
@JsonApiResponse({ data: { type: ScientificNameDto } })
async searchScientificNames(@Query("search") search: string) {
if (search == null) throw new BadRequestException("search query param is required");
if (isEmpty(search)) throw new BadRequestException("search query param is required");

const document = buildJsonApi();
for (const treeSpecies of await this.researchService.searchScientificNames(search)) {
Expand Down
22 changes: 22 additions & 0 deletions libs/database/src/lib/factories/tree-species-research.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FactoryGirl } from "factory-girl-ts";
import { TreeSpeciesResearch } from "../entities";
import { faker } from "@faker-js/faker";

export const TreeSpeciesResearchFactory = FactoryGirl.define(TreeSpeciesResearch, async () => ({
taxonId: await generateUniqueTaxonId(),
scientificName: faker.word.words(2),
family: faker.word.words(1),
genus: faker.word.words(1),
specificEpithet: faker.word.words()
}));

async function generateUniqueTaxonId() {
let taxonId = generateTaxonId();
while ((await TreeSpeciesResearch.findByPk(taxonId)) != null) taxonId = generateTaxonId();
return taxonId;
}

const generateTaxonId = () => {
const taxonId = `000000000${faker.number.int(9999999999)}`;
return `wfo-${taxonId.substring(taxonId.length - 10)}`;
};

0 comments on commit e4ae654

Please sign in to comment.