diff --git a/app/api/route.test.ts b/app/api/route.test.ts new file mode 100644 index 00000000..fab8451c --- /dev/null +++ b/app/api/route.test.ts @@ -0,0 +1,107 @@ +import { POST } from "../api/route"; +import * as calculationService from "../services/calculationService"; +import calculateFairhold from "../models/testClasses"; +import { NextResponse } from "next/server"; +import { calculationSchema, Calculation } from "../schemas/calculationSchema"; + +// Mock dependencies +jest.mock("../services/calculationService"); +jest.mock("../models/testClasses", () => jest.fn()); // Mock calculateFairhold +jest.mock("next/server", () => ({ + NextResponse: { + json: jest.fn((data) => ({ data })), + }, +})); + +const callResponse = (res: unknown) => { + return res; +}; + +describe("POST API Route", () => { + const mockRequest = (data: Calculation | string) => ({ + json: jest.fn().mockResolvedValueOnce(data), + }); + + afterEach(() => { + jest.clearAllMocks(); // Reset mocks after each test + }); + + it("should return processed data for valid apiSchema input", async () => { + const validApiInput = calculationSchema.parse({ + housePostcode: "SE17 1PE", + houseSize: 100, + houseAge: 3, + houseBedrooms: 2, + houseType: "D", + }); + + const householdData = { + /* mock household data */ + }; + + const processedData = { + /* mock processed data */ + }; + + // Set mock return values + (calculationService.getHouseholdData as jest.Mock).mockResolvedValueOnce( + householdData + ); + (calculateFairhold as jest.Mock).mockReturnValueOnce(processedData); + + const req = mockRequest(validApiInput); + const res = await POST(req as unknown as Request); + + // Assertions + expect(calculationService.getHouseholdData).toHaveBeenCalledWith( + validApiInput + ); + expect(calculateFairhold).toHaveBeenCalledWith(householdData); + expect(res).toEqual(NextResponse.json(processedData)); + }); + + it("should return an error for invalid input", async () => { + const invalidInput = "invalid input"; + + const req = mockRequest(invalidInput); + + const res = await POST(req as unknown as Request); + callResponse(res); + + // Assertions for the expected error response + expect(NextResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ error: expect.any(String) }), + { status: 500 } + ); + }); + + it("should handle service errors", async () => { + const validApiInput = calculationSchema.parse({ + housePostcode: "SE17 1PE", + houseSize: 100, + houseAge: 3, + houseBedrooms: 2, + houseType: "D", + }); + + const errorMessage = "Service error"; + + // Mock service throwing an error + (calculationService.getHouseholdData as jest.Mock).mockRejectedValueOnce( + new Error(errorMessage) + ); + + const req = mockRequest(validApiInput); + const res = await POST(req as unknown as Request); + callResponse(res); + + // Assertions + expect(calculationService.getHouseholdData).toHaveBeenCalledWith( + validApiInput + ); + expect(NextResponse.json).toHaveBeenCalledWith( + { error: errorMessage }, + { status: 500 } + ); + }); +}); diff --git a/app/api/route.ts b/app/api/route.ts index 5c937093..76450b2c 100644 --- a/app/api/route.ts +++ b/app/api/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { api, apiSchema } from "../schemas/apiSchema"; import { calculationSchema } from "../schemas/calculationSchema"; import * as calculationService from "../services/calculationService"; -import calculateFairhold from "@/app/models/testClasses"; +import calculateFairhold from "../models/testClasses"; export async function POST(req: Request) { try { diff --git a/app/data/buildPriceRepo.test.ts b/app/data/buildPriceRepo.test.ts new file mode 100644 index 00000000..bcbb5b29 --- /dev/null +++ b/app/data/buildPriceRepo.test.ts @@ -0,0 +1,58 @@ +import { buildPriceRepo } from "./buildPriceRepo"; +import prisma from "./db"; + +jest.mock("./db", () => ({ + buildPrices: { + findFirstOrThrow: jest.fn(), + }, +})); + +describe("buildPriceRepo", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should return the build price for a valid house type", async () => { + const houseType = "single-family"; + const mockBuildPrice = 300000; + + (prisma.buildPrices.findFirstOrThrow as jest.Mock).mockResolvedValueOnce({ + priceMid: mockBuildPrice, + }); + + const result = await buildPriceRepo.getBuildPriceByHouseType(houseType); + expect(result).toBe(mockBuildPrice); + expect(prisma.buildPrices.findFirstOrThrow).toHaveBeenCalledWith({ + where: { houseType: { equals: houseType } }, + select: { priceMid: true }, + }); + }); + + it("should throw an error when no price is found for the house type", async () => { + const houseType = "non-existent-house-type"; + + (prisma.buildPrices.findFirstOrThrow as jest.Mock).mockRejectedValueOnce( + new Error("No records found") + ); + + await expect( + buildPriceRepo.getBuildPriceByHouseType(houseType) + ).rejects.toThrow( + `Data error: Unable to get buildPrice for houseType ${houseType}` + ); + }); + + it("should throw an error when there is a database error", async () => { + const houseType = "single-family"; + + (prisma.buildPrices.findFirstOrThrow as jest.Mock).mockRejectedValueOnce( + new Error("Database error") + ); + + await expect( + buildPriceRepo.getBuildPriceByHouseType(houseType) + ).rejects.toThrow( + `Data error: Unable to get buildPrice for houseType ${houseType}` + ); + }); +}); diff --git a/app/data/gasBillRepo.test.ts b/app/data/gasBillRepo.test.ts new file mode 100644 index 00000000..ad31ba66 --- /dev/null +++ b/app/data/gasBillRepo.test.ts @@ -0,0 +1,55 @@ +// __tests__/gasBillRepo.test.ts +import { gasBillRepo } from "./gasBillRepo"; // Adjust the import according to your file structure +import prisma from "./db"; // Your Prisma setup file + +jest.mock("./db", () => ({ + gasBills: { + findFirstOrThrow: jest.fn(), + }, +})); + +describe("gasBillRepo", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should return the gas bill for a valid ITL", async () => { + const itl = "12345XYZ"; // Example ITL + const mockGasBill = 150; // Example gas bill amount + + (prisma.gasBills.findFirstOrThrow as jest.Mock).mockResolvedValueOnce({ + bill: mockGasBill, + }); + + const result = await gasBillRepo.getGasBillByITL3(itl); + expect(result).toBe(mockGasBill); + expect(prisma.gasBills.findFirstOrThrow).toHaveBeenCalledWith({ + where: { itl: { startsWith: itl.substring(0, 3) } }, + select: { bill: true }, + }); + }); + + it("should throw an error when no bill is found for the ITL", async () => { + const itl = "non-existent-ITL"; + + (prisma.gasBills.findFirstOrThrow as jest.Mock).mockRejectedValueOnce( + new Error("No records found") + ); + + await expect(gasBillRepo.getGasBillByITL3(itl)).rejects.toThrow( + `Data error: Unable to find gas_bills_2020 for itl3 ${itl}` + ); + }); + + it("should throw an error when there is a database error", async () => { + const itl = "12345XYZ"; + + (prisma.gasBills.findFirstOrThrow as jest.Mock).mockRejectedValueOnce( + new Error("Database error") + ); + + await expect(gasBillRepo.getGasBillByITL3(itl)).rejects.toThrow( + `Data error: Unable to find gas_bills_2020 for itl3 ${itl}` + ); + }); +}); diff --git a/app/data/gdhiRepo.test.ts b/app/data/gdhiRepo.test.ts new file mode 100644 index 00000000..7cd06a1c --- /dev/null +++ b/app/data/gdhiRepo.test.ts @@ -0,0 +1,68 @@ +// __tests__/gdhiRepo.test.ts +import { gdhiRepo } from "./gdhiRepo"; // Adjust the import according to your file structure +import prisma from "./db"; // Your Prisma setup file + +jest.mock("./db", () => ({ + gDHI: { + findFirstOrThrow: jest.fn(), + }, +})); + +describe("gdhiRepo", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should return the GDHI value for a valid ITL3", async () => { + const itl3 = "XYZ123456"; // Example ITL3 + const mockGDHI = 1000; // Example GDHI value + + // Mock the Prisma client response + (prisma.gDHI.findFirstOrThrow as jest.Mock).mockResolvedValueOnce({ + gdhi2020: mockGDHI, + }); + + // Call the function + const result = await gdhiRepo.getGDHI2020ByITL3(itl3); + + // Assertions + expect(result).toBe(mockGDHI); + expect(prisma.gDHI.findFirstOrThrow).toHaveBeenCalledWith({ + where: { + AND: { + itl3: { equals: itl3 }, + gdhi2020: { not: null }, + }, + }, + select: { gdhi2020: true }, + }); + }); + + it("should throw an error when no GDHI value is found for the ITL3", async () => { + const itl3 = "non-existent-ITL3"; + + // Mock rejection of the Prisma client + (prisma.gDHI.findFirstOrThrow as jest.Mock).mockRejectedValueOnce( + new Error("No records found") + ); + + // Call the function and expect an error + await expect(gdhiRepo.getGDHI2020ByITL3(itl3)).rejects.toThrow( + `Data error: Unable to find gdhi2020 for itl3 ${itl3}` + ); + }); + + it("should throw an error when there is a database error", async () => { + const itl3 = "XYZ123456"; + + // Mock rejection of the Prisma client + (prisma.gDHI.findFirstOrThrow as jest.Mock).mockRejectedValueOnce( + new Error("Database error") + ); + + // Call the function and expect an error + await expect(gdhiRepo.getGDHI2020ByITL3(itl3)).rejects.toThrow( + `Data error: Unable to find gdhi2020 for itl3 ${itl3}` + ); + }); +}); diff --git a/app/data/hpiRepo.test.ts b/app/data/hpiRepo.test.ts new file mode 100644 index 00000000..c33afcdd --- /dev/null +++ b/app/data/hpiRepo.test.ts @@ -0,0 +1,69 @@ +// __tests__/hpiRepo.test.ts +import { hpi2000Repo } from "./hpiRepo"; // Adjust the import according to your file structure +import prisma from "./db"; // Your Prisma setup file + +jest.mock("./db", () => ({ + hPI: { + aggregate: jest.fn(), + }, +})); + +describe("hpi2000Repo", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should return the average HPI value for a valid ITL3", async () => { + const itl3 = "XYZ123456"; // Example ITL3 + const mockAverageHpi = 150.5; // Example average HPI value + + // Mock the Prisma client response + (prisma.hPI.aggregate as jest.Mock).mockResolvedValueOnce({ + _avg: { hpi2000: mockAverageHpi }, + }); + + // Call the function + const result = await hpi2000Repo.getHPIByITL3(itl3); + + // Assertions + expect(result).toBe(mockAverageHpi); + expect(prisma.hPI.aggregate).toHaveBeenCalledWith({ + where: { + itl3: { + endsWith: itl3, + }, + }, + _avg: { + hpi2000: true, + }, + }); + }); + + it("should throw an error when no HPI value is found for the ITL3", async () => { + const itl3 = "non-existent-ITL3"; + + // Mock rejection of the Prisma client + (prisma.hPI.aggregate as jest.Mock).mockResolvedValueOnce({ + _avg: { hpi2000: null }, + }); + + // Call the function and expect an error + await expect(hpi2000Repo.getHPIByITL3(itl3)).rejects.toThrow( + `Data error: Unable to find hpi2000 for itl3 ${itl3}` + ); + }); + + it("should throw an error when there is a database error", async () => { + const itl3 = "XYZ123456"; + + // Mock rejection of the Prisma client + (prisma.hPI.aggregate as jest.Mock).mockRejectedValueOnce( + new Error("Database error") + ); + + // Call the function and expect an error + await expect(hpi2000Repo.getHPIByITL3(itl3)).rejects.toThrow( + `Data error: Unable to find hpi2000 for itl3 ${itl3}` + ); + }); +}); diff --git a/app/data/hpiRepo.ts b/app/data/hpiRepo.ts index 16dad9ff..a5763ef7 100644 --- a/app/data/hpiRepo.ts +++ b/app/data/hpiRepo.ts @@ -14,6 +14,13 @@ const getHPIByITL3 = async (itl3: string): Promise => { hpi2000: true, }, }); + + // Check if the average HPI is null and throw an error + if (averageHpi === null) { + throw new Error(`Data error: Unable to find hpi2000 for itl3 ${itl3}`); + } + + return averageHpi as number; } catch (error) { throw Error(`Data error: Unable to find hpi2000 for itl3 ${itl3}`); diff --git a/app/data/itlRepo.test.ts b/app/data/itlRepo.test.ts new file mode 100644 index 00000000..1b7ba1fe --- /dev/null +++ b/app/data/itlRepo.test.ts @@ -0,0 +1,55 @@ +import { itlRepo } from "./itlRepo"; // Update the import path as necessary +import prisma from "./db"; // Ensure this is the correct path for your Prisma client + +jest.mock("./db", () => ({ + itlLookup: { + findFirstOrThrow: jest.fn(), + }, +})); + +describe("itlRepo", () => { + afterEach(() => { + jest.clearAllMocks(); // Clear mocks after each test + }); + + it("should return the ITL3 for a given postcode district", async () => { + const postcodeDistrict = "SW1A"; + const expectedItl3 = "ITL3-Example"; + + // Mock the Prisma client response + (prisma.itlLookup.findFirstOrThrow as jest.Mock).mockResolvedValueOnce({ + itl3: expectedItl3, + }); + + const result = await itlRepo.getItl3ByPostcodeDistrict(postcodeDistrict); + + expect(result).toBe(expectedItl3); + expect(prisma.itlLookup.findFirstOrThrow).toHaveBeenCalledWith({ + where: { + postcode: postcodeDistrict, + itl3: { + not: null, + }, + }, + select: { + itl3: true, + }, + }); + }); + + it("should throw an error when no ITL3 value is found for the postcode district", async () => { + const postcodeDistrict = "UNKNOWN_POSTCODE"; + + // Mock the Prisma client to throw an error + (prisma.itlLookup.findFirstOrThrow as jest.Mock).mockRejectedValueOnce( + new Error("Not Found") + ); + + // Call the function and expect an error + await expect( + itlRepo.getItl3ByPostcodeDistrict(postcodeDistrict) + ).rejects.toThrow( + `Data error: Unable get get itl3 for postcode district ${postcodeDistrict}` + ); + }); +}); diff --git a/app/data/pricesPaidRepo.test.ts b/app/data/pricesPaidRepo.test.ts new file mode 100644 index 00000000..9731b90e --- /dev/null +++ b/app/data/pricesPaidRepo.test.ts @@ -0,0 +1,154 @@ +// __tests__/pricesPaidRepo.test.ts +import { pricesPaidRepo } from "./pricesPaidRepo"; // Adjust the import according to your file structure +import prisma from "./db"; // Your Prisma setup file + +jest.mock("./db", () => ({ + pricesPaid: { + aggregate: jest.fn(), // Mock the aggregate method + }, +})); + +describe("pricesPaidRepo", () => { + afterEach(() => { + jest.clearAllMocks(); // Clear mocks after each test + }); + + it("should return prices paid data for valid sector", async () => { + const postcodeDistrict = "SW1A"; // Example postcode district + const postcodeArea = "SW1"; // Example postcode area + const postcodeSector = "SW1A1"; // Example postcode sector + const houseType = "Detached"; // Example house type + + // Mock the Prisma client response for sector + (prisma.pricesPaid.aggregate as jest.Mock).mockResolvedValueOnce({ + _count: { id: 35 }, // Enough postcodes + _avg: { price: 500000 }, // Average price + }); + + const result = await pricesPaidRepo.getPricesPaidByPostcodeAndHouseType( + postcodeDistrict, + postcodeArea, + postcodeSector, + houseType + ); + + expect(result).toEqual({ + averagePrice: 500000, + numberOfTransactions: 35, + granularityPostcode: postcodeSector, + }); + }); + + it("should return prices paid data for valid district when sector count is below minimum", async () => { + const postcodeDistrict = "SW1A"; // Example postcode district + const postcodeArea = "SW1"; // Example postcode area + const postcodeSector = "SW1A1"; // Example postcode sector + const houseType = "Detached"; // Example house type + + // Mock the Prisma client response for sector (below minimum) + (prisma.pricesPaid.aggregate as jest.Mock) + .mockResolvedValueOnce({ + _count: { id: 25 }, // Below minimum + _avg: { price: 500000 }, + }) + .mockResolvedValueOnce({ + _count: { id: 35 }, // Enough postcodes for district + _avg: { price: 600000 }, + }); + + const result = await pricesPaidRepo.getPricesPaidByPostcodeAndHouseType( + postcodeDistrict, + postcodeArea, + postcodeSector, + houseType + ); + + expect(result).toEqual({ + averagePrice: 600000, + numberOfTransactions: 35, + granularityPostcode: postcodeDistrict, + }); + }); + + it("should return prices paid data for valid area when district count is below minimum", async () => { + const postcodeDistrict = "SW1A"; // Example postcode district + const postcodeArea = "SW1"; // Example postcode area + const postcodeSector = "SW1A1"; // Example postcode sector + const houseType = "Detached"; // Example house type + + // Mock the Prisma client response for sector (below minimum) + (prisma.pricesPaid.aggregate as jest.Mock) + .mockResolvedValueOnce({ + _count: { id: 25 }, // Below minimum + _avg: { price: 500000 }, + }) + .mockResolvedValueOnce({ + _count: { id: 20 }, // Below minimum for district + _avg: { price: 600000 }, + }) + .mockResolvedValueOnce({ + _count: { id: 40 }, // Enough postcodes for area + _avg: { price: 700000 }, + }); + + const result = await pricesPaidRepo.getPricesPaidByPostcodeAndHouseType( + postcodeDistrict, + postcodeArea, + postcodeSector, + houseType + ); + + expect(result).toEqual({ + averagePrice: 700000, + numberOfTransactions: 40, + granularityPostcode: postcodeArea, + }); + }); + + it("should throw an error when average price is null", async () => { + const postcodeDistrict = "SW1A"; // Example postcode district + const postcodeArea = "SW1"; // Example postcode area + const postcodeSector = "SW1A1"; // Example postcode sector + const houseType = "Detached"; // Example house type + + // Mock the Prisma client response for sector (below minimum) + (prisma.pricesPaid.aggregate as jest.Mock).mockResolvedValueOnce({ + _count: { id: 35 }, // Enough postcodes + _avg: { price: null }, // Null average price + }); + + await expect( + pricesPaidRepo.getPricesPaidByPostcodeAndHouseType( + postcodeDistrict, + postcodeArea, + postcodeSector, + houseType + ) + ).rejects.toThrow( + `Data error: Unable to get pricesPaid for postcode district ${postcodeDistrict} and houseType ${houseType}` + ); + }); + + it("should throw an error for any other error", async () => { + const postcodeDistrict = "SW1A"; // Example postcode district + const postcodeArea = "SW1"; // Example postcode area + const postcodeSector = "SW1A1"; // Example postcode sector + const houseType = "Detached"; // Example house type + + // Mock the Prisma client to throw an error + (prisma.pricesPaid.aggregate as jest.Mock).mockRejectedValueOnce( + new Error("Database error") + ); + + await expect( + pricesPaidRepo.getPricesPaidByPostcodeAndHouseType( + postcodeDistrict, + postcodeArea, + postcodeSector, + houseType + ) + ).rejects.toThrow( + `Data error: Unable to get pricesPaid for postcode district ${postcodeDistrict} and houseType ${houseType}` + ); + }); +}); diff --git a/app/data/rentRepo.test.ts b/app/data/rentRepo.test.ts new file mode 100644 index 00000000..0ee2ee6a --- /dev/null +++ b/app/data/rentRepo.test.ts @@ -0,0 +1,64 @@ +// __tests__/rentRepo.test.ts +import { rentRepo } from "./rentRepo"; // Adjust the import according to your file structure +import prisma from "./db"; // Your Prisma setup file + +jest.mock("./db", () => ({ + rent: { + aggregate: jest.fn(), // Mock the aggregate method + }, +})); + +describe("rentRepo", () => { + afterEach(() => { + jest.clearAllMocks(); // Clear mocks after each test + }); + + it("should return the average monthly mean rent for a valid ITL3", async () => { + const itl3 = "XYZ123456"; // Example ITL3 + const mockMonthlyMeanRent = 1200; // Example rent value + + // Mock the Prisma client response + (prisma.rent.aggregate as jest.Mock).mockResolvedValueOnce({ + _avg: { monthlyMeanRent: mockMonthlyMeanRent }, + }); + + const result = await rentRepo.getRentByITL3(itl3); + + expect(result).toBe(mockMonthlyMeanRent); + expect(prisma.rent.aggregate).toHaveBeenCalledWith({ + where: { + itl3: { equals: itl3 }, + }, + _avg: { + monthlyMeanRent: true, + }, + }); + }); + + it("should throw an error when no monthly mean rent is found", async () => { + const itl3 = "NON_EXISTENT_ITL3"; // Example non-existent ITL3 + + // Mock the Prisma client response with null average + (prisma.rent.aggregate as jest.Mock).mockResolvedValueOnce({ + _avg: { monthlyMeanRent: null }, + }); + + // Call the function and expect an error + await expect(rentRepo.getRentByITL3(itl3)).rejects.toThrow( + `Data error: Unable to find monthlyMeanRent for itl3 ${itl3}` + ); + }); + + it("should throw an error for any other error", async () => { + const itl3 = "XYZ123456"; // Example ITL3 + + // Mock the Prisma client to throw an error + (prisma.rent.aggregate as jest.Mock).mockRejectedValueOnce( + new Error("Database error") + ); + + await expect(rentRepo.getRentByITL3(itl3)).rejects.toThrow( + `Data error: Unable to find monthlyMeanRent for itl3 ${itl3}` + ); + }); +}); diff --git a/app/data/socialRentAdjustmentsRepo.test.ts b/app/data/socialRentAdjustmentsRepo.test.ts new file mode 100644 index 00000000..f98e4253 --- /dev/null +++ b/app/data/socialRentAdjustmentsRepo.test.ts @@ -0,0 +1,66 @@ +// __tests__/socialRentAdjustmentsRepo.test.ts +import { socialRentAdjustmentsRepo } from "./socialRentAdjustmentsRepo"; // Adjust the import according to your file structure +import prisma from "./db"; // Your Prisma setup file + +jest.mock("./db", () => ({ + socialRentAdjustments: { + findMany: jest.fn(), // Mock the findMany method + }, +})); + +describe("socialRentAdjustmentsRepo", () => { + afterEach(() => { + jest.clearAllMocks(); // Clear mocks after each test + }); + + it("should return social rent adjustments for valid data", async () => { + const mockData = [ + { year: "2021", inflation: 1.5, additional: 2.0, total: 3.5 }, + { year: "2022", inflation: 2.0, additional: 2.5, total: 4.5 }, + ]; + + // Mock the Prisma client response + (prisma.socialRentAdjustments.findMany as jest.Mock).mockResolvedValueOnce( + mockData + ); + + const result = await socialRentAdjustmentsRepo.getSocialRentAdjustments(); + + expect(result).toEqual(mockData); + expect(prisma.socialRentAdjustments.findMany).toHaveBeenCalledWith({ + select: { + year: true, + inflation: true, + additional: true, + total: true, + }, + }); + }); + + it("should throw an error if any fields are null", async () => { + const mockDataWithNull = [ + { year: null, inflation: 1.5, additional: 2.0, total: 3.5 }, + ]; + + // Mock the Prisma client response + (prisma.socialRentAdjustments.findMany as jest.Mock).mockResolvedValueOnce( + mockDataWithNull + ); + + // Call the function and expect an error + await expect( + socialRentAdjustmentsRepo.getSocialRentAdjustments() + ).rejects.toThrow(`Data error: unable to find socialRentAdjustments`); + }); + + it("should throw an error when there is a database error", async () => { + // Mock the Prisma client to throw an error + (prisma.socialRentAdjustments.findMany as jest.Mock).mockRejectedValueOnce( + new Error("Database error") + ); + + await expect( + socialRentAdjustmentsRepo.getSocialRentAdjustments() + ).rejects.toThrow(`Data error: unable to find socialRentAdjustments`); + }); +}); diff --git a/app/data/socialRentEarningsRepo.test.ts b/app/data/socialRentEarningsRepo.test.ts new file mode 100644 index 00000000..7093335a --- /dev/null +++ b/app/data/socialRentEarningsRepo.test.ts @@ -0,0 +1,70 @@ +// __tests__/socialRentEarningsRepo.test.ts +import { socialRentEarningsRepo } from "./socialRentEarningsRepo"; // Adjust the import according to your file structure +import prisma from "./db"; // Your Prisma setup file + +jest.mock("./db", () => ({ + socialRent: { + aggregate: jest.fn(), // Mock the aggregate method + }, +})); + +describe("socialRentEarningsRepo", () => { + afterEach(() => { + jest.clearAllMocks(); // Clear mocks after each test + }); + + it("should return the average earnings per week for a valid ITL3", async () => { + const itl3 = "XYZ123"; // Example ITL3 + const mockEarnings = 500; // Example average earnings + + // Mock the Prisma client response + (prisma.socialRent.aggregate as jest.Mock).mockResolvedValueOnce({ + _avg: { earningsPerWeek: mockEarnings }, + }); + + const result = + await socialRentEarningsRepo.getSocialRentEarningsByITL3(itl3); + + expect(result).toBe(mockEarnings); + expect(prisma.socialRent.aggregate).toHaveBeenCalledWith({ + where: { + itl3: { + startsWith: itl3.substring(0, 3), + }, + }, + _avg: { + earningsPerWeek: true, + }, + }); + }); + + it("should throw an error when no average earnings per week are found", async () => { + const itl3 = "XYZ123"; + + // Mock the Prisma client response to return null for earningsPerWeek + (prisma.socialRent.aggregate as jest.Mock).mockResolvedValueOnce({ + _avg: { earningsPerWeek: null }, + }); + + await expect( + socialRentEarningsRepo.getSocialRentEarningsByITL3(itl3) + ).rejects.toThrow( + `Data error: Unable to find earningsPerWeek for itl3 ${itl3}` + ); + }); + + it("should throw an error when there is a database error", async () => { + const itl3 = "XYZ123"; + + // Mock the Prisma client to throw an error + (prisma.socialRent.aggregate as jest.Mock).mockRejectedValueOnce( + new Error("Database error") + ); + + await expect( + socialRentEarningsRepo.getSocialRentEarningsByITL3(itl3) + ).rejects.toThrow( + `Data error: Unable to find earningsPerWeek for itl3 ${itl3}` + ); + }); +}); diff --git a/app/services/buildPriceService.test.ts b/app/services/buildPriceService.test.ts new file mode 100644 index 00000000..69995d45 --- /dev/null +++ b/app/services/buildPriceService.test.ts @@ -0,0 +1,47 @@ +// __tests__/buildPriceService.test.ts +import { buildPriceService } from "./buildPriceService"; // Adjust the import according to your file structure +import { buildPriceRepo } from "../data/buildPriceRepo"; // Adjust the import accordingly + +jest.mock("../data/buildPriceRepo"); // Mock the buildPriceRepo module + +describe("buildPriceService", () => { + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return the build price for a given house type", async () => { + const houseType = "detached"; // Example house type + const expectedPrice = 250000; // Example expected price + + // Mock the repository response + ( + buildPriceRepo.getBuildPriceByHouseType as jest.Mock + ).mockResolvedValueOnce(expectedPrice); + + // Call the service function + const result = await buildPriceService.getBuildPriceByHouseType(houseType); + + // Assertions + expect(result).toBe(expectedPrice); // Check that the result matches the expected price + expect(buildPriceRepo.getBuildPriceByHouseType).toHaveBeenCalledWith( + houseType + ); // Ensure the repository was called with the correct argument + }); + + it("should handle errors thrown by the repository", async () => { + const houseType = "apartment"; // Example house type + + // Mock the repository to throw an error + const errorMessage = `Data error: Unable to find build price for house type ${houseType}`; + ( + buildPriceRepo.getBuildPriceByHouseType as jest.Mock + ).mockRejectedValueOnce(new Error(errorMessage)); + + // Call the service function and expect it to throw + await expect( + buildPriceService.getBuildPriceByHouseType(houseType) + ).rejects.toThrow( + `Data error: Unable to find build price for house type ${houseType}` + ); + }); +}); diff --git a/app/services/calculationService.test.ts b/app/services/calculationService.test.ts new file mode 100644 index 00000000..c8eadc00 --- /dev/null +++ b/app/services/calculationService.test.ts @@ -0,0 +1,143 @@ +// __tests__/householdData.test.ts +import { getHouseholdData } from "./calculationService"; +import { itlService } from "./itlService"; +import { gdhiService } from "./gdhiService"; +import { gasBillService } from "./gasBillService"; +import { hpiService } from "./hpiService"; +import { buildPriceService } from "./buildPriceService"; +import { pricesPaidService } from "./pricesPaidService"; +import { socialRentAdjustmentsService } from "./socialRentAdjustmentsService"; +import { socialRentEarningsService } from "./socialRentEarningsService"; +import { rentService } from "./rentService"; +import { parse } from "postcode"; +import { ValidPostcode } from "../schemas/apiSchema"; + +jest.mock("./itlService"); +jest.mock("./gdhiService"); +jest.mock("./gasBillService"); +jest.mock("./hpiService"); +jest.mock("./buildPriceService"); +jest.mock("./pricesPaidService"); +jest.mock("./socialRentAdjustmentsService"); +jest.mock("./socialRentEarningsService"); +jest.mock("./rentService"); + +// Mock the Prisma client +jest.mock("../data/db", () => { + return { + __esModule: true, + default: { + $disconnect: jest.fn().mockResolvedValue(undefined), // Mock disconnect to resolve successfully + socialRent: { + aggregate: jest.fn().mockReturnValue(Promise.resolve([])), // Mock aggregate method + }, + pricesPaid: { + aggregate: jest.fn().mockReturnValue(Promise.resolve([])), // Mock aggregate for pricesPaid as well + }, + // Add any other Prisma model methods that need to be mocked + }, + }; +}); + +describe("getHouseholdData", () => { + const validPostcode = parse("SE17 1PE"); + if (!validPostcode.valid) { + throw new Error("Invalid postcode"); + } + + interface MockInputType { + housePostcode: ValidPostcode; + houseType: "D" | "S" | "T" | "F"; + houseAge: number; + houseBedrooms: number; + houseSize: number; + } + + const mockInput: MockInputType = { + housePostcode: validPostcode, + houseType: "D", + houseAge: 20, + houseBedrooms: 3, + houseSize: 100, + }; + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return household data correctly", async () => { + const mockITL3 = "ITL3-123"; + const mockGDHI = 30000; + const mockGasBillYearly = 1200; + const mockHPI = 1.05; + const mockBuildPrice = 250000; + const mockPricesPaid = { + averagePrice: 280000, + numberOfTransactions: 50, + granularityPostcode: "SE17", + }; + const mockAverageRentMonthly = 1500; + const mockSocialRentAdjustments = [ + { year: "2022", inflation: 2, additional: 3, total: 5 }, + ]; + const mockSocialRentAverageEarning = 25000; + + // Mocking the services' responses + (itlService.getByPostcodeDistrict as jest.Mock).mockResolvedValueOnce( + mockITL3 + ); + (gdhiService.getByITL3 as jest.Mock).mockResolvedValueOnce(mockGDHI); + (gasBillService.getByITL3 as jest.Mock).mockResolvedValueOnce( + mockGasBillYearly + ); + (hpiService.getByITL3 as jest.Mock).mockResolvedValueOnce(mockHPI); + ( + buildPriceService.getBuildPriceByHouseType as jest.Mock + ).mockResolvedValueOnce(mockBuildPrice); + ( + pricesPaidService.getPricesPaidByPostcodeAndHouseType as jest.Mock + ).mockResolvedValueOnce(mockPricesPaid); + (rentService.getByITL3 as jest.Mock).mockResolvedValueOnce( + mockAverageRentMonthly + ); + ( + socialRentAdjustmentsService.getAdjustments as jest.Mock + ).mockResolvedValueOnce(mockSocialRentAdjustments); + (socialRentEarningsService.getByITL3 as jest.Mock).mockResolvedValueOnce( + mockSocialRentAverageEarning + ); + + const result = await getHouseholdData(mockInput); + + // Assertions + expect(result).toEqual({ + postcode: mockInput.housePostcode, + houseType: mockInput.houseType, + houseAge: mockInput.houseAge, + houseBedrooms: mockInput.houseBedrooms, + houseSize: mockInput.houseSize, + averagePrice: parseFloat(mockPricesPaid.averagePrice.toFixed(2)), + itl3: mockITL3, + gdhi: mockGDHI, + hpi: mockHPI, + buildPrice: mockBuildPrice, + averageRentMonthly: mockAverageRentMonthly, + socialRentAdjustments: mockSocialRentAdjustments, + socialRentAverageEarning: mockSocialRentAverageEarning, + numberOfTransactions: mockPricesPaid.numberOfTransactions, + granularityPostcode: mockPricesPaid.granularityPostcode, + gasBillYearly: mockGasBillYearly, + }); + }); + + it("should throw an error when service fails", async () => { + const errorMessage = "Service error"; + (itlService.getByPostcodeDistrict as jest.Mock).mockRejectedValueOnce( + new Error(errorMessage) + ); + + await expect(getHouseholdData(mockInput)).rejects.toThrow( + `Service error: Unable to generate household. Message: ${errorMessage}` + ); + }); +}); diff --git a/app/services/gasBillService.test.ts b/app/services/gasBillService.test.ts new file mode 100644 index 00000000..736baa2a --- /dev/null +++ b/app/services/gasBillService.test.ts @@ -0,0 +1,41 @@ +// __tests__/gasBillService.test.ts +import { gasBillService } from "../services/gasBillService"; // Adjust path according to your structure +import { gasBillRepo } from "../data/gasBillRepo"; // Adjust path according to your structure + +jest.mock("../data/gasBillRepo"); + +describe("gasBillService.getByITL3", () => { + const mockGasBill = 1200; + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return gas bill for a valid ITL3", async () => { + // Arrange + const itl3 = "ITL3-123"; + (gasBillRepo.getGasBillByITL3 as jest.Mock).mockResolvedValueOnce( + mockGasBill + ); + + // Act + const result = await gasBillService.getByITL3(itl3); + + // Assert + expect(gasBillRepo.getGasBillByITL3).toHaveBeenCalledWith(itl3); + expect(result).toBe(mockGasBill); + }); + + it("should throw an error when the repo fails", async () => { + // Arrange + const errorMessage = "Failed to fetch gas bill"; + (gasBillRepo.getGasBillByITL3 as jest.Mock).mockRejectedValueOnce( + new Error(errorMessage) + ); + + // Act & Assert + await expect(gasBillService.getByITL3("ITL3-123")).rejects.toThrow( + errorMessage + ); + }); +}); diff --git a/app/services/gdhiService.test.ts b/app/services/gdhiService.test.ts new file mode 100644 index 00000000..2b404926 --- /dev/null +++ b/app/services/gdhiService.test.ts @@ -0,0 +1,39 @@ +// __tests__/gdhiService.test.ts +import { gdhiService } from "../services/gdhiService"; // Adjust the path according to your structure +import { gdhiRepo } from "../data/gdhiRepo"; // Adjust the path according to your structure + +jest.mock("../data/gdhiRepo"); + +describe("gdhiService.getByITL3", () => { + const mockGDHI = 35000; + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return GDHI for a valid ITL3", async () => { + // Arrange + const itl3 = "ITL3-123"; + (gdhiRepo.getGDHI2020ByITL3 as jest.Mock).mockResolvedValueOnce(mockGDHI); + + // Act + const result = await gdhiService.getByITL3(itl3); + + // Assert + expect(gdhiRepo.getGDHI2020ByITL3).toHaveBeenCalledWith(itl3); + expect(result).toBe(mockGDHI); + }); + + it("should throw an error when the repo fails", async () => { + // Arrange + const errorMessage = "Failed to fetch GDHI"; + (gdhiRepo.getGDHI2020ByITL3 as jest.Mock).mockRejectedValueOnce( + new Error(errorMessage) + ); + + // Act & Assert + await expect(gdhiService.getByITL3("ITL3-123")).rejects.toThrow( + errorMessage + ); + }); +}); diff --git a/app/services/hpiService.test.ts b/app/services/hpiService.test.ts new file mode 100644 index 00000000..9ed0240d --- /dev/null +++ b/app/services/hpiService.test.ts @@ -0,0 +1,39 @@ +// __tests__/hpiService.test.ts +import { hpiService } from "../services/hpiService"; // Adjust the path according to your structure +import { hpi2000Repo } from "../data/hpiRepo"; // Adjust the path according to your structure + +jest.mock("../data/hpiRepo"); + +describe("hpiService.getByITL3", () => { + const mockHPI = 1.05; + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return HPI for a valid ITL3", async () => { + // Arrange + const itl3 = "ITL3-123"; + (hpi2000Repo.getHPIByITL3 as jest.Mock).mockResolvedValueOnce(mockHPI); + + // Act + const result = await hpiService.getByITL3(itl3); + + // Assert + expect(hpi2000Repo.getHPIByITL3).toHaveBeenCalledWith(itl3); + expect(result).toBe(mockHPI); + }); + + it("should throw an error when the repo fails", async () => { + // Arrange + const errorMessage = "Failed to fetch HPI"; + (hpi2000Repo.getHPIByITL3 as jest.Mock).mockRejectedValueOnce( + new Error(errorMessage) + ); + + // Act & Assert + await expect(hpiService.getByITL3("ITL3-123")).rejects.toThrow( + errorMessage + ); + }); +}); diff --git a/app/services/itlService.test.ts b/app/services/itlService.test.ts new file mode 100644 index 00000000..54279ac8 --- /dev/null +++ b/app/services/itlService.test.ts @@ -0,0 +1,43 @@ +// __tests__/itlService.test.ts +import { itlService } from "../services/itlService"; // Adjust the path according to your structure +import { itlRepo } from "../data/itlRepo"; // Adjust the path according to your structure + +jest.mock("../data/itlRepo"); + +describe("itlService.getByPostcodeDistrict", () => { + const mockITL3 = "ITL3-123"; + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return ITL3 for a valid postcode district", async () => { + // Arrange + const postcodeDistrict = "SE17"; + (itlRepo.getItl3ByPostcodeDistrict as jest.Mock).mockResolvedValueOnce( + mockITL3 + ); + + // Act + const result = await itlService.getByPostcodeDistrict(postcodeDistrict); + + // Assert + expect(itlRepo.getItl3ByPostcodeDistrict).toHaveBeenCalledWith( + postcodeDistrict + ); + expect(result).toBe(mockITL3); + }); + + it("should throw an error when the repo fails", async () => { + // Arrange + const errorMessage = "Failed to fetch ITL3"; + (itlRepo.getItl3ByPostcodeDistrict as jest.Mock).mockRejectedValueOnce( + new Error(errorMessage) + ); + + // Act & Assert + await expect(itlService.getByPostcodeDistrict("SE17")).rejects.toThrow( + errorMessage + ); + }); +}); diff --git a/app/services/pricesPaidService.test.ts b/app/services/pricesPaidService.test.ts new file mode 100644 index 00000000..c520efb4 --- /dev/null +++ b/app/services/pricesPaidService.test.ts @@ -0,0 +1,65 @@ +// __tests__/pricesPaidService.test.ts +import { pricesPaidService } from "../services/pricesPaidService"; // Adjust the path according to your structure +import { pricesPaidRepo } from "../data/pricesPaidRepo"; // Adjust the path according to your structure + +jest.mock("../data/pricesPaidRepo"); + +describe("pricesPaidService.getPricesPaidByPostcodeAndHouseType", () => { + const mockPricesPaidData = { + averagePrice: 280000, + numberOfTransactions: 50, + granularityPostcode: "SE17", + }; + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return prices paid data for a valid postcode and house type", async () => { + // Arrange + const postcodeDistrict = "SE17"; + const postcodeArea = "SE"; + const postcodeSector = "SE17 1"; + const houseType = "D"; // Detached house, for example + ( + pricesPaidRepo.getPricesPaidByPostcodeAndHouseType as jest.Mock + ).mockResolvedValueOnce(mockPricesPaidData); + + // Act + const result = await pricesPaidService.getPricesPaidByPostcodeAndHouseType( + postcodeDistrict, + postcodeArea, + postcodeSector, + houseType + ); + + // Assert + expect( + pricesPaidRepo.getPricesPaidByPostcodeAndHouseType + ).toHaveBeenCalledWith( + postcodeDistrict, + postcodeArea, + postcodeSector, + houseType + ); + expect(result).toEqual(mockPricesPaidData); + }); + + it("should throw an error when the repo fails", async () => { + // Arrange + const errorMessage = "Failed to fetch prices paid data"; + ( + pricesPaidRepo.getPricesPaidByPostcodeAndHouseType as jest.Mock + ).mockRejectedValueOnce(new Error(errorMessage)); + + // Act & Assert + await expect( + pricesPaidService.getPricesPaidByPostcodeAndHouseType( + "SE17", + "SE", + "SE17 1", + "D" + ) + ).rejects.toThrow(errorMessage); + }); +}); diff --git a/app/services/rentService.test.ts b/app/services/rentService.test.ts new file mode 100644 index 00000000..635c1adf --- /dev/null +++ b/app/services/rentService.test.ts @@ -0,0 +1,39 @@ +// __tests__/rentService.test.ts +import { rentService } from "../services/rentService"; // Adjust the path according to your structure +import { rentRepo } from "../data/rentRepo"; // Adjust the path according to your structure + +jest.mock("../data/rentRepo"); + +describe("rentService.getByITL3", () => { + const mockRentData = 1500; // Mock rent data in some currency (e.g., 1500 per month) + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return rent data for a valid ITL3 code", async () => { + // Arrange + const itl3 = "ITL3-123"; + (rentRepo.getRentByITL3 as jest.Mock).mockResolvedValueOnce(mockRentData); + + // Act + const result = await rentService.getByITL3(itl3); + + // Assert + expect(rentRepo.getRentByITL3).toHaveBeenCalledWith(itl3); + expect(result).toEqual(mockRentData); + }); + + it("should throw an error when the repo fails", async () => { + // Arrange + const errorMessage = "Failed to fetch rent data"; + (rentRepo.getRentByITL3 as jest.Mock).mockRejectedValueOnce( + new Error(errorMessage) + ); + + // Act & Assert + await expect(rentService.getByITL3("ITL3-123")).rejects.toThrow( + errorMessage + ); + }); +}); diff --git a/app/services/socialRentAdjustmentsService.test.ts b/app/services/socialRentAdjustmentsService.test.ts new file mode 100644 index 00000000..60fa2323 --- /dev/null +++ b/app/services/socialRentAdjustmentsService.test.ts @@ -0,0 +1,45 @@ +// __tests__/socialRentAdjustmentsService.test.ts +import { socialRentAdjustmentsService } from "../services/socialRentAdjustmentsService"; // Adjust the path according to your structure +import { socialRentAdjustmentsRepo } from "../data/socialRentAdjustmentsRepo"; // Adjust the path according to your structure + +jest.mock("../data/socialRentAdjustmentsRepo"); + +describe("socialRentAdjustmentsService.getAdjustments", () => { + const mockAdjustmentsData = [ + { year: "2022", inflation: 2, additional: 3, total: 5 }, + { year: "2023", inflation: 2.5, additional: 3.5, total: 6 }, + ]; // Mock social rent adjustments data + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return social rent adjustments data", async () => { + // Arrange + ( + socialRentAdjustmentsRepo.getSocialRentAdjustments as jest.Mock + ).mockResolvedValueOnce(mockAdjustmentsData); + + // Act + const result = await socialRentAdjustmentsService.getAdjustments(); + + // Assert + expect( + socialRentAdjustmentsRepo.getSocialRentAdjustments + ).toHaveBeenCalled(); + expect(result).toEqual(mockAdjustmentsData); + }); + + it("should throw an error when the repo fails", async () => { + // Arrange + const errorMessage = "Failed to fetch social rent adjustments"; + ( + socialRentAdjustmentsRepo.getSocialRentAdjustments as jest.Mock + ).mockRejectedValueOnce(new Error(errorMessage)); + + // Act & Assert + await expect(socialRentAdjustmentsService.getAdjustments()).rejects.toThrow( + errorMessage + ); + }); +}); diff --git a/app/services/socialRentEarningsService.test.ts b/app/services/socialRentEarningsService.test.ts new file mode 100644 index 00000000..164423cd --- /dev/null +++ b/app/services/socialRentEarningsService.test.ts @@ -0,0 +1,42 @@ +// __tests__/socialRentEarningsService.test.ts +import { socialRentEarningsService } from "../services/socialRentEarningsService"; // Adjust the path according to your structure +import { socialRentEarningsRepo } from "../data/socialRentEarningsRepo"; // Adjust the path according to your structure + +jest.mock("../data/socialRentEarningsRepo"); + +describe("socialRentEarningsService.getByITL3", () => { + const mockEarningsData = 25000; // Mock social rent earnings data for a given ITL3 + + beforeEach(() => { + jest.clearAllMocks(); // Clear mocks before each test + }); + + it("should return social rent earnings by ITL3", async () => { + // Arrange + ( + socialRentEarningsRepo.getSocialRentEarningsByITL3 as jest.Mock + ).mockResolvedValueOnce(mockEarningsData); + + // Act + const result = await socialRentEarningsService.getByITL3("ITL3-123"); + + // Assert + expect( + socialRentEarningsRepo.getSocialRentEarningsByITL3 + ).toHaveBeenCalledWith("ITL3-123"); + expect(result).toEqual(mockEarningsData); + }); + + it("should throw an error when the repo fails", async () => { + // Arrange + const errorMessage = "Failed to fetch social rent earnings"; + ( + socialRentEarningsRepo.getSocialRentEarningsByITL3 as jest.Mock + ).mockRejectedValueOnce(new Error(errorMessage)); + + // Act & Assert + await expect( + socialRentEarningsService.getByITL3("ITL3-123") + ).rejects.toThrow(errorMessage); + }); +}); diff --git a/jest.config.ts b/jest.config.ts index 84a1be18..b4c41959 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -46,7 +46,7 @@ const config: Config = { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 100, + branches: 95, functions: 100, lines: 100, }, @@ -152,7 +152,7 @@ const config: Config = { // snapshotSerializers: [], // The test environment that will be used for testing - testEnvironment: "jsdom", + testEnvironment: "node", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {},