From 27ba6d6c139dc93cf17468e4e548c86b5426ed0d Mon Sep 17 00:00:00 2001 From: thehighestprimenumber Date: Fri, 6 Sep 2024 09:57:21 -0300 Subject: [PATCH] (chore) partial migration to jest --- app/jest.config.ts | 2 +- app/jest.setup.ts | 2 +- app/package-lock.json | 117 +++-- app/package.json | 6 +- app/src/util/api.ts | 2 - app/tests/api/activity_value.jest.ts | 421 ++++++++++++++++++ app/tests/api/activity_value.test.ts | 61 --- .../api/{admin.test.ts => admin.jest.ts} | 41 +- app/tests/api/{city.test.ts => city.jest.ts} | 0 ...{datasource.test.ts => datasource.jest.ts} | 47 +- app/tests/api/inventory.jest.ts | 421 ++++++++++++++++++ app/tests/api/inventory.test.ts | 257 ++--------- app/tests/api/inventory_value.jest.ts | 312 +++++++++++++ app/tests/api/inventory_value.test.ts | 127 ++---- ...{population.test.ts => population.jest.ts} | 58 ++- app/tests/api/{user.test.ts => user.jest.ts} | 23 +- app/tests/cdpservice.test.ts | 12 +- app/tests/helpers.ts | 48 ++ app/tests/{models.test.ts => models.jest.ts} | 0 app/tests/{series.test.ts => series.jest.ts} | 49 +- 20 files changed, 1482 insertions(+), 524 deletions(-) create mode 100644 app/tests/api/activity_value.jest.ts rename app/tests/api/{admin.test.ts => admin.jest.ts} (75%) rename app/tests/api/{city.test.ts => city.jest.ts} (100%) rename app/tests/api/{datasource.test.ts => datasource.jest.ts} (80%) create mode 100644 app/tests/api/inventory.jest.ts create mode 100644 app/tests/api/inventory_value.jest.ts rename app/tests/api/{population.test.ts => population.jest.ts} (71%) rename app/tests/api/{user.test.ts => user.jest.ts} (77%) rename app/tests/{models.test.ts => models.jest.ts} (100%) rename app/tests/{series.test.ts => series.jest.ts} (64%) diff --git a/app/jest.config.ts b/app/jest.config.ts index 60a0f28fc..b18339042 100644 --- a/app/jest.config.ts +++ b/app/jest.config.ts @@ -181,7 +181,7 @@ const config: JestConfigWithTsJest = { // "**/__tests__/**/*.[jt]s?(x)", // "**/?(*.)+(spec|test).[tj]s?(x)" // ], - + testMatch: ["**/?(*.)+(jest).[tj]s?(x)"], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ // "/node_modules/" diff --git a/app/jest.setup.ts b/app/jest.setup.ts index e16be11be..8e3054011 100644 --- a/app/jest.setup.ts +++ b/app/jest.setup.ts @@ -1 +1 @@ -import "@/util/big_int_json.ts"; +import "@/util/big_int_json"; diff --git a/app/package-lock.json b/app/package-lock.json index 10141d87f..92494053e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -99,7 +99,7 @@ "sequelize-auto": "^0.8.8", "start-server-and-test": "^2.0.3", "storybook": "^8.2.7", - "ts-jest": "^29.1.2", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.7.0" }, @@ -13670,6 +13670,21 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.783", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz", @@ -15547,6 +15562,36 @@ "ramda": "0.29.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -17834,6 +17879,24 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -26192,28 +26255,30 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, "node_modules/ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", @@ -26223,6 +26288,9 @@ "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -26234,26 +26302,11 @@ } } }, - "node_modules/ts-jest/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -26261,12 +26314,6 @@ "node": ">=10" } }, - "node_modules/ts-jest/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/ts-jest/node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/app/package.json b/app/package.json index 49301c53b..a2ad11554 100644 --- a/app/package.json +++ b/app/package.json @@ -9,7 +9,7 @@ "start": "next start", "lint": "next lint", "test": "npm run api:test & npm run e2e:test", - "api:test": "glob -c \"tsx --no-warnings --test\" \"./tests/**/*.test.ts\"", + "api:test": "glob -c \"tsx --no-warnings --test\" \"./tests/**/*.test.ts\" & npm run jest", "e2e:test": "npx playwright test", "e2e:debug": "playwright test --debug", "e2e:test:head": "npx playwright test -- --headed", @@ -25,7 +25,7 @@ "db:gen-migration": "sequelize-cli migration:generate --name", "db:gen-seed": "sequelize-cli seed:generate --name", "sync-catalogue": "tsx scripts/catalogue-sync.ts", - "ci:test": "tsx --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.test.ts", + "ci:test": "tsx --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info tests/**/*.test.ts & npm run jest", "prettier": "npx prettier . --write", "email": "email dev --dir src/lib/emails", "create-admin": "tsx scripts/create-admin.ts" @@ -122,7 +122,7 @@ "sequelize-auto": "^0.8.8", "start-server-and-test": "^2.0.3", "storybook": "^8.2.7", - "ts-jest": "^29.1.2", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.7.0" }, diff --git a/app/src/util/api.ts b/app/src/util/api.ts index 69c0c633f..4840ec1a6 100644 --- a/app/src/util/api.ts +++ b/app/src/util/api.ts @@ -12,8 +12,6 @@ import { db } from "@/models"; import { ValidationError } from "sequelize"; import { ManualInputValidationError } from "@/lib/custom-errors/manual-input-error"; -import "@/util/big_int_json.ts"; - export type ApiResponse = NextResponse | StreamingTextResponse; export type NextHandler = ( diff --git a/app/tests/api/activity_value.jest.ts b/app/tests/api/activity_value.jest.ts new file mode 100644 index 000000000..6fbfd61ac --- /dev/null +++ b/app/tests/api/activity_value.jest.ts @@ -0,0 +1,421 @@ +import { + DELETE as deleteActivityValue, + GET as getActivityValue, + PATCH as updateActivityValue, +} from "@/app/api/v0/inventory/[inventory]/activity-value/[id]/route"; + +import { + DELETE as deleteAllActivitiesInSubsector, + POST as createActivityValue, +} from "@/app/api/v0/inventory/[inventory]/activity-value/route"; + +import { db } from "@/models"; +import { CreateActivityValueRequest } from "@/util/validation"; +import { randomUUID } from "node:crypto"; +import { mockRequest, setupTests, testUserID } from "../helpers"; +import { City } from "@/models/City"; +import { Inventory } from "@/models/Inventory"; +import { SubCategory } from "@/models/SubCategory"; +import { SubSector } from "@/models/SubSector"; +import { InventoryValue } from "@/models/InventoryValue"; +import { ActivityValue } from "@/models/ActivityValue"; +import { Sector } from "@/models/Sector"; +import { describe, it, afterAll, beforeAll, expect } from "@jest/globals"; + +const ReferenceNumber = "I.1.1"; + +const validCreateActivity: CreateActivityValueRequest = { + activityData: { + co2_amount: 100, + ch4_amount: 100, + n2o_amount: 100, + "residential-building-type": "building-type-all", + "residential-building-fuel-type": "fuel-type-charcoal", + "residential-buildings-fuel-source": "source", + }, + metadata: { + active_selection: "test1", + }, + inventoryValue: { + inputMethodology: "direct-measure", + gpcReferenceNumber: ReferenceNumber, + unavailableReason: "Reason for unavailability", + unavailableExplanation: "Explanation for unavailability", + }, + dataSource: { + sourceType: "", + dataQuality: "high", + notes: "Some notes regarding the data source", + }, + gasValues: [ + { + id: "123e4567-e89b-12d3-a456-426614174001", + gas: "CO2", + gasAmount: 1000n, + emissionsFactor: { + emissionsPerActivity: 50.5, + gas: "CO2", + units: "kg", + }, + }, + { + id: "123e4567-e89b-12d3-a456-426614174003", + gas: "CH4", + gasAmount: 2000n, + emissionsFactor: { + emissionsPerActivity: 25.0, + gas: "CH4", + units: "kg", + }, + }, + ], +}; + +const updatedActivityValue: CreateActivityValueRequest = { + activityData: { + co2_amount: 120, + ch4_amount: 160, + n2o_amount: 100, + "residential-building-type": "building-type-all", + "residential-building-fuel-type": "fuel-type-anthracite", + "residential-buildings-fuel-source": "source-edit", + }, + metadata: { + "active-selection": "test1", + }, + inventoryValue: { + inputMethodology: "direct-measure", + gpcReferenceNumber: ReferenceNumber, + unavailableReason: "Reason for unavailability", + unavailableExplanation: "Explanation for unavailability", + }, + dataSource: { + sourceType: "updated-type", + dataQuality: "high", + notes: "Some notes regarding the data source", + }, + gasValues: [ + { + id: "123e4567-e89b-12d3-a456-426614174001", + gas: "CO2", + gasAmount: 1000n, + emissionsFactor: { + emissionsPerActivity: 50.5, + gas: "CO2", + units: "kg", + }, + }, + { + id: "123e4567-e89b-12d3-a456-426614174003", + gas: "CH4", + gasAmount: 4000n, + emissionsFactor: { + emissionsPerActivity: 25.0, + gas: "CH4", + units: "kg", + }, + }, + ], +}; + +const invalidCreateActivity: CreateActivityValueRequest = { + activityData: { + "form-test-input1": 40.4, + "form-test-input2": "132894729485739867398473321", + "form-test-input3": "agriculture-forestry", + }, + metadata: { + "active-selection": "test1", + }, + dataSource: { + sourceType: "", + dataQuality: "high", + notes: "Some notes regarding the data source", + }, + gasValues: [ + { + id: "123e4567-e89b-12d3-a456-426614174001", + gas: "CO2", + gasAmount: 1000n, + emissionsFactor: { + emissionsPerActivity: 50.5, + gas: "CO2", + units: "kg", + }, + }, + { + id: "123e4567-e89b-12d3-a456-426614174003", + gas: "CH4", + gasAmount: 2000n, + emissionsFactor: { + emissionsPerActivity: 25.0, + gas: "CH4", + units: "kg", + }, + }, + ], +}; + +const activityUnits = "UNITS"; +const activityValue = 1000; +const co2eq = 44000n; +const locode = "XX_INVENTORY_CITY_ACTIVITY_VALUE"; +// Matches name given by CDP for API testing +const cityName = "Open Earth Foundation API City Discloser activity value"; +const cityCountry = "United Kingdom of Great Britain and Northern Ireland"; +const inventoryName = "TEST_INVENTORY_INVENTORY_ACTIVITY_VALUE"; +const sectorName = "XX_INVENTORY_TEST_SECTOR_ACTIVITY_VALUE"; +const subcategoryName = "XX_INVENTORY_TEST_SUBCATEGORY_ACTIVITY_VALUE"; +const subsectorName = "XX_INVENTORY_TEST_SUBSECTOR_1_ACTIVITY_VALUE"; + +/** skipped tests are running with the with node test runner **/ +describe("Activity Value API", () => { + let city: City; + let inventory: Inventory; + let sector: Sector; + let subCategory: SubCategory; + let subSector: SubSector; + let inventoryValue: InventoryValue; + let createdActivityValue: ActivityValue; + let createdActivityValue2: ActivityValue; + + beforeAll(async () => { + setupTests(); + await db.initialize(); + + // Perform model cleanup and creation + await db.models.Sector.destroy({ where: { sectorName } }); + await db.models.SubCategory.destroy({ where: { subcategoryName } }); + await db.models.SubSector.destroy({ where: { subsectorName } }); + await db.models.City.destroy({ where: { locode } }); + + const prevInventory = await db.models.Inventory.findOne({ + where: { inventoryName }, + }); + if (prevInventory) { + await db.models.InventoryValue.destroy({ + where: { inventoryId: prevInventory.inventoryId }, + }); + await db.models.Inventory.destroy({ where: { inventoryName } }); + } + + city = await db.models.City.create({ + cityId: randomUUID(), + name: cityName, + country: cityCountry, + locode, + }); + + await db.models.User.upsert({ userId: testUserID, name: "TEST_USER" }); + await city.addUser(testUserID); + + // create an inventory + inventory = await db.models.Inventory.create({ + inventoryId: randomUUID(), + inventoryName: inventoryName, + cityId: city.cityId, + }); + + sector = await db.models.Sector.create({ + sectorId: randomUUID(), + sectorName, + }); + + subSector = await db.models.SubSector.create({ + subsectorId: randomUUID(), + sectorId: sector.sectorId, + referenceNumber: ReferenceNumber, + subsectorName, + }); + + subCategory = await db.models.SubCategory.create({ + subcategoryId: randomUUID(), + subsectorId: subSector.subsectorId, + referenceNumber: ReferenceNumber, + subcategoryName, + }); + + await db.models.InventoryValue.destroy({ + where: { inventoryId: inventory.inventoryId }, + }); + + inventoryValue = await db.models.InventoryValue.create({ + inventoryId: inventory.inventoryId, + id: randomUUID(), + subCategoryId: subCategory.subcategoryId, + subSectorId: subSector.subsectorId, + gpcReferenceNumber: ReferenceNumber, + co2eq, + activityUnits, + inputMethodology: "direct-measure", + activityValue, + }); + }); + + afterAll(async () => { + await db.models.City.destroy({ where: { locode } }); + await db.models.Inventory.destroy({ + where: { inventoryId: inventory.inventoryId }, + }); + await db.models.ActivityValue.destroy({ + where: { id: createdActivityValue2.id }, + }); + await db.models.Sector.destroy({ where: { sectorName } }); + await db.models.SubCategory.destroy({ where: { subcategoryName } }); + await db.models.SubSector.destroy({ where: { subsectorName } }); + await db.models.InventoryValue.destroy({ + where: { inventoryId: inventory.inventoryId }, + }); + + if (db.sequelize) await db.sequelize.close(); + }); + + it("should not create an activity value with invalid data", async () => { + const req = mockRequest(invalidCreateActivity); + const res = await createActivityValue(req, { + params: { inventory: inventory.inventoryId }, + }); + expect(res.status).toBe(400); + }); + + it.skip("should create an activity, creating an inventory value with inventoryValue params", async () => { + const findInventory = await db.models.Inventory.findOne({ + where: { inventoryName }, + }); + expect(findInventory?.inventoryId).toBe(inventory.inventoryId); + + const req = mockRequest(validCreateActivity); + const res = await createActivityValue(req, { + params: { inventory: inventory.inventoryId }, + }); + + expect(res.status).toBe(200); + const { data } = await res.json(); + createdActivityValue2 = data; + expect(data.activityData.co2_amount).toBe( + validCreateActivity.activityData.co2_amount, + ); + expect(data.inventoryValueId).not.toBeNull(); + }); + + it.skip("should create an activity value with inventoryValueId", async () => { + const findInventory = await db.models.Inventory.findOne({ + where: { inventoryName }, + }); + expect(findInventory?.inventoryId).toBe(inventory.inventoryId); + + const req = mockRequest({ + ...validCreateActivity, + inventoryValueId: inventoryValue.id, + inventoryValue: undefined, + }); + const res = await createActivityValue(req, { + params: { inventory: inventory.inventoryId }, + }); + + expect(res.status).toBe(200); + const { data } = await res.json(); + createdActivityValue = data; + expect(data.activityData.co2_amount).toBe( + validCreateActivity.activityData.co2_amount, + ); + expect(data.inventoryValueId).not.toBeNull(); + }); + + it.skip("should get an activity value", async () => { + const req = mockRequest(); + const res = await getActivityValue(req, { + params: { + inventory: inventory.inventoryId, + id: createdActivityValue.id, + }, + }); + + const { data } = await res.json(); + expect(res.status).toBe(200); + expect(data.co2eq).toBe(createdActivityValue.co2eq); + expect(data.co2eqYears).toBe(createdActivityValue.co2eqYears); + expect(data.inventoryValueId).toBe(inventoryValue.id); + }); + + it("should not get an activity value with invalid id", async () => { + const fakeId = randomUUID(); + const req = mockRequest(); + const res = await getActivityValue(req, { + params: { inventory: inventory.inventoryId, id: fakeId }, + }); + + const { data } = await res.json(); + expect(data).toBeNull(); + }); + + it.skip("should update an activity value", async () => { + const req = mockRequest({ + ...createdActivityValue, + activityData: updatedActivityValue.activityData, + metaData: updatedActivityValue.metadata, + }); + const res = await updateActivityValue(req, { + params: { + inventory: inventory.inventoryId, + id: createdActivityValue.id, + }, + }); + + const { data } = await res.json(); + expect(res.status).toBe(200); + expect(data.activityData.co2_amount).toBe( + updatedActivityValue.activityData.co2_amount, + ); + }); + + it.skip("should delete an activity value", async () => { + const req = mockRequest(); + const res = await deleteActivityValue(req, { + params: { + inventory: inventory.inventoryId, + id: createdActivityValue.id, + }, + }); + + const { data } = await res.json(); + expect(res.status).toBe(200); + expect(data).toBe(true); + }); + + it.skip("should delete all activities in a subsector", async () => { + const findInventory = await db.models.Inventory.findOne({ + where: { inventoryName }, + }); + expect(findInventory?.inventoryId).toBe(inventory.inventoryId); + + const req1 = mockRequest({ + ...validCreateActivity, + inventoryValueId: inventoryValue.id, + inventoryValue: undefined, + }); + const req2 = mockRequest({ + ...validCreateActivity, + inventoryValueId: inventoryValue.id, + inventoryValue: undefined, + }); + + const res1 = await createActivityValue(req1, { + params: { inventory: inventory.inventoryId }, + }); + const res2 = await createActivityValue(req2, { + params: { inventory: inventory.inventoryId }, + }); + + expect(res1.status).toBe(200); + expect(res2.status).toBe(200); + + const req3 = mockRequest(null, { subSectorId: subSector.subsectorId }); + const res3 = await deleteAllActivitiesInSubsector(req3, { + params: { inventory: inventory.inventoryId }, + }); + + const { data } = await res3.json(); + expect(res3.status).toBe(200); + }); +}); diff --git a/app/tests/api/activity_value.test.ts b/app/tests/api/activity_value.test.ts index c25b7fed4..06b5171cc 100644 --- a/app/tests/api/activity_value.test.ts +++ b/app/tests/api/activity_value.test.ts @@ -120,44 +120,6 @@ const updatedActivityValue: CreateActivityValueRequest = { ], }; -const invalidCreateActivity: CreateActivityValueRequest = { - activityData: { - "form-test-input1": 40.4, - "form-test-input2": "132894729485739867398473321", - "form-test-input3": "agriculture-forestry", - }, - metadata: { - "active-selection": "test1", - }, - dataSource: { - sourceType: "", - dataQuality: "high", - notes: "Some notes regarding the data source", - }, - gasValues: [ - { - id: "123e4567-e89b-12d3-a456-426614174001", - gas: "CO2", - gasAmount: 1000n, - emissionsFactor: { - emissionsPerActivity: 50.5, - gas: "CO2", - units: "kg", - }, - }, - { - id: "123e4567-e89b-12d3-a456-426614174003", - gas: "CH4", - gasAmount: 2000n, - emissionsFactor: { - emissionsPerActivity: 25.0, - gas: "CH4", - units: "kg", - }, - }, - ], -}; - const activityUnits = "UNITS"; const activityValue = 1000; const co2eq = 44000n; @@ -293,16 +255,6 @@ describe("Activity Value API", () => { if (db.sequelize) await db.sequelize.close(); }); - it("should not create an activity value with invalid data", async () => { - const req = mockRequest(invalidCreateActivity); - const res = await createActivityValue(req, { - params: { - inventory: inventory.inventoryId, - }, - }); - assert.equal(res.status, 400); - }); - it("should create an activity, creating an inventory value with inventoryValue params", async () => { const findInventory = await db.models.Inventory.findOne({ where: { @@ -383,19 +335,6 @@ describe("Activity Value API", () => { assert.equal(data.inventoryValueId, inventoryValue.id); }); - it("should not get an activity value with invalid id", async () => { - const fakeId = randomUUID(); - const req = mockRequest(); - const res = await getActivityValue(req, { - params: { - inventory: inventory.inventoryId, - id: fakeId, - }, - }); - const { data } = await res.json(); - assert.equal(data, null); - }); - // test patch, break patch it("should update an activity value", async () => { const req = mockRequest({ diff --git a/app/tests/api/admin.test.ts b/app/tests/api/admin.jest.ts similarity index 75% rename from app/tests/api/admin.test.ts rename to app/tests/api/admin.jest.ts index d8e47f731..102b12d8e 100644 --- a/app/tests/api/admin.test.ts +++ b/app/tests/api/admin.jest.ts @@ -1,7 +1,14 @@ import { POST as changeRole } from "@/app/api/v0/auth/role/route"; import { db } from "@/models"; -import assert from "node:assert"; -import { after, before, beforeEach, describe, it, mock } from "node:test"; +import { + beforeAll, + afterAll, + beforeEach, + describe, + it, + expect, + jest, +} from "@jest/globals"; import { mockRequest, setupTests, testUserData, testUserID } from "../helpers"; import { AppSession, Auth, Roles } from "@/lib/auth"; @@ -17,18 +24,18 @@ const mockAdminSession: AppSession = { describe("Admin API", () => { let prevGetServerSession = Auth.getServerSession; - before(async () => { + beforeAll(async () => { setupTests(); await db.initialize(); }); - after(async () => { + afterAll(async () => { Auth.getServerSession = prevGetServerSession; if (db.sequelize) await db.sequelize.close(); }); beforeEach(async () => { - Auth.getServerSession = mock.fn(() => Promise.resolve(mockSession)); + Auth.getServerSession = jest.fn(() => Promise.resolve(mockSession)); await db.models.User.upsert({ userId: testUserID, name: testUserData.name, @@ -39,27 +46,27 @@ describe("Admin API", () => { it("should change the user role when logged in as admin", async () => { const req = mockRequest({ email: testUserData.email, role: Roles.Admin }); - Auth.getServerSession = mock.fn(() => Promise.resolve(mockAdminSession)); + Auth.getServerSession = jest.fn(() => Promise.resolve(mockAdminSession)); const res = await changeRole(req, { params: {} }); - assert.equal(res.status, 200); + expect(res.status).toBe(200); const body = await res.json(); - assert.equal(body.success, true); + expect(body.success).toBe(true); const user = await db.models.User.findOne({ where: { email: testUserData.email }, }); - assert.equal(user?.role, Roles.Admin); + expect(user?.role).toBe(Roles.Admin); }); it("should not change the user role when logged in as normal user", async () => { const req = mockRequest({ email: testUserData.email, role: Roles.Admin }); const res = await changeRole(req, { params: {} }); - assert.equal(res.status, 403); + expect(res.status).toBe(403); const user = await db.models.User.findOne({ where: { email: testUserData.email }, }); - assert.equal(user?.role, Roles.User); + expect(user?.role).toBe(Roles.User); }); it("should return a 404 error when user does not exist", async () => { @@ -67,24 +74,24 @@ describe("Admin API", () => { email: "not-existing@example.com", role: Roles.Admin, }); - Auth.getServerSession = mock.fn(() => Promise.resolve(mockAdminSession)); + Auth.getServerSession = jest.fn(() => Promise.resolve(mockAdminSession)); const res = await changeRole(req, { params: {} }); - assert.equal(res.status, 404); + expect(res.status).toBe(404); }); it("should validate the request", async () => { - Auth.getServerSession = mock.fn(() => Promise.resolve(mockAdminSession)); + Auth.getServerSession = jest.fn(() => Promise.resolve(mockAdminSession)); const req = mockRequest({ email: testUserData.email, role: "invalid" }); const res = await changeRole(req, { params: {} }); - assert.equal(res.status, 400); + expect(res.status).toBe(400); const req2 = mockRequest({ email: "not-an-email", role: "Admin" }); const res2 = await changeRole(req2, { params: {} }); - assert.equal(res2.status, 400); + expect(res2.status).toBe(400); const req3 = mockRequest({}); const res3 = await changeRole(req3, { params: {} }); - assert.equal(res3.status, 400); + expect(res3.status).toBe(400); }); }); diff --git a/app/tests/api/city.test.ts b/app/tests/api/city.jest.ts similarity index 100% rename from app/tests/api/city.test.ts rename to app/tests/api/city.jest.ts diff --git a/app/tests/api/datasource.test.ts b/app/tests/api/datasource.jest.ts similarity index 80% rename from app/tests/api/datasource.test.ts rename to app/tests/api/datasource.jest.ts index 757cb5cca..3c803fb2f 100644 --- a/app/tests/api/datasource.test.ts +++ b/app/tests/api/datasource.jest.ts @@ -1,17 +1,15 @@ import { GET as getDataSourcesForSector } from "@/app/api/v0/datasource/[inventoryId]/[sectorId]/route"; import { GET as getAllDataSources } from "@/app/api/v0/datasource/[inventoryId]/route"; - import { db } from "@/models"; -import assert from "node:assert"; import { randomUUID } from "node:crypto"; -import { after, before, describe, it } from "node:test"; import { literal, Op } from "sequelize"; -import { mockRequest, setupTests } from "../helpers"; +import { cascadeDeleteDataSource, mockRequest, setupTests } from "../helpers"; import { City } from "@/models/City"; import { CreateInventoryRequest } from "@/util/validation"; import { Sector } from "@/models/Sector"; import { Inventory } from "@/models/Inventory"; import fetchMock from "fetch-mock"; +import { beforeAll, afterAll, describe, it, expect } from "@jest/globals"; const locode = "XX_DATASOURCE_CITY"; const sectorName = "XX_DATASOURCE_TEST_1"; @@ -55,17 +53,14 @@ describe("DataSource API", () => { let city: City; let inventory: Inventory; let sector: Sector; - before(async () => { + + beforeAll(async () => { setupTests(); await db.initialize(); - // this also deletes all Sector/SubSectorValue instances associated with it (cascade) - await db.models.Inventory.destroy({ - where: { year: inventoryData.year }, - }); - await db.models.DataSource.destroy({ - where: { - [Op.or]: [literal(`dataset_name ->> 'en' LIKE 'XX_INVENTORY_TEST_%'`)], - }, + + await db.models.Inventory.destroy({ where: { year: inventoryData.year } }); + await cascadeDeleteDataSource({ + [Op.or]: [literal(`dataset_name ->> 'en' LIKE 'XX_INVENTORY_TEST_%'`)], }); await db.models.City.destroy({ where: { locode } }); city = await db.models.City.create({ @@ -73,6 +68,7 @@ describe("DataSource API", () => { locode, name: "CC_", }); + await db.models.SubCategory.destroy({ where: { subcategoryName } }); await db.models.SubSector.destroy({ where: { subsectorName } }); await db.models.Sector.destroy({ where: { sectorName } }); @@ -82,17 +78,20 @@ describe("DataSource API", () => { inventoryId: randomUUID(), cityId: city.cityId, }); + sector = await db.models.Sector.create({ sectorId: randomUUID(), referenceNumber: "X", sectorName, }); + const subSector = await db.models.SubSector.create({ subsectorId: randomUUID(), sectorId: sector.sectorId, referenceNumber: "X.9", subsectorName, }); + const subCategory = await db.models.SubCategory.create({ subcategoryId: randomUUID(), subsectorId: subSector.subsectorId, @@ -120,7 +119,7 @@ describe("DataSource API", () => { } }); - after(async () => { + afterAll(async () => { if (db.sequelize) await db.sequelize.close(); }); @@ -129,16 +128,16 @@ describe("DataSource API", () => { const res = await getDataSourcesForSector(req, { params: { inventoryId: inventory.inventoryId, sectorId: sector.sectorId }, }); - assert.equal(res.status, 200); + expect(res.status).toBe(200); const { data } = await res.json(); - assert.equal(data.length, 1); + expect(data.length).toBe(1); const { source } = data[0]; - assert.equal(source.datasetName.en, "XX_DATASOURCE_TEST_0"); - assert.equal(source.sectorId, sector.sectorId); - assert.equal(source.apiEndpoint, apiEndpoint); - assert.equal(source.geographicalLocation, "EARTH"); - assert.equal(source.startYear, 4000); - assert.equal(source.endYear, 4010); + expect(source.datasetName.en).toBe("XX_DATASOURCE_TEST_0"); + expect(source.sectorId).toBe(sector.sectorId); + expect(source.apiEndpoint).toBe(apiEndpoint); + expect(source.geographicalLocation).toBe("EARTH"); + expect(source.startYear).toBe(4000); + expect(source.endYear).toBe(4010); }); it("should get the data sources for all sectors", async () => { @@ -146,9 +145,9 @@ describe("DataSource API", () => { const res = await getAllDataSources(req, { params: { inventoryId: inventory.inventoryId }, }); - assert.equal(res.status, 200); + expect(res.status).toBe(200); const { data } = await res.json(); - assert.equal(data.length, 2); + expect(data.length).toBe(2); }); it.todo("should apply data sources"); diff --git a/app/tests/api/inventory.jest.ts b/app/tests/api/inventory.jest.ts new file mode 100644 index 000000000..3ea296537 --- /dev/null +++ b/app/tests/api/inventory.jest.ts @@ -0,0 +1,421 @@ +import { + DELETE as deleteInventory, + GET as findInventory, + PATCH as updateInventory, +} from "@/app/api/v0/inventory/[inventory]/route"; +import { GET as calculateProgress } from "@/app/api/v0/inventory/[inventory]/progress/route"; +import { POST as createInventory } from "@/app/api/v0/city/[city]/inventory/route"; +import { POST as submitInventory } from "@/app/api/v0/inventory/[inventory]/cdp/route"; +import { db } from "@/models"; +import { CreateInventoryRequest } from "@/util/validation"; +import { randomUUID } from "node:crypto"; +import { literal, Op } from "sequelize"; +import { + cascadeDeleteDataSource, + createRequest, + expectStatusCode, + mockRequest, + setupTests, + testUserID, +} from "../helpers"; +import { SubSector, SubSectorAttributes } from "@/models/SubSector"; +import { City } from "@/models/City"; +import { Inventory } from "@/models/Inventory"; +import { Sector } from "@/models/Sector"; +import { SubCategory } from "@/models/SubCategory"; +import { + expect, + jest, + describe, + beforeAll, + afterAll, + beforeEach, + it, +} from "@jest/globals"; + +jest.useFakeTimers(); + +const locode = "XX_INVENTORY_CITY"; +// Matches name given by CDP for API testing +const cityName = "Open Earth Foundation API City Discloser"; +const cityCountry = "United Kingdom of Great Britain and Northern Ireland"; +const inventoryName = "TEST_INVENTORY_INVENTORY"; +const sectorName = "XX_INVENTORY_TEST_SECTOR"; +const subcategoryName = "XX_INVENTORY_TEST_SUBCATEGORY"; +const subsectorName = "XX_INVENTORY_TEST_SUBSECTOR_1"; +const subSectorName2 = "XX_INVENTORY_TEST_SUBSECTOR_2"; + +process.env.CDP_MODE = "test"; + +const inventoryData: CreateInventoryRequest = { + inventoryName, + year: 3000, + totalEmissions: 1337, +}; + +const inventoryData2: CreateInventoryRequest = { + inventoryName, + year: 3001, + totalEmissions: 1338, +}; + +const invalidInventory = { + inventoryName: "", + year: 0, + totalEmissions: "246kg co2eq", +}; + +const inventoryValue = { + activityValue: 20, + activityUnits: "km", + emissionFactorValue: 20, + totalEmissions: 400, +}; + +describe("Inventory API", () => { + let city: City; + let inventory: Inventory; + let sector: Sector; + let subCategory: SubCategory; + let subSector: SubSector; + let subSector2: SubSector; + + beforeAll(async () => { + setupTests(); + await db.initialize(); + // this also deletes all Sector/SubSectorValue instances associated with it (cascade) + await db.models.Inventory.destroy({ + where: { inventoryName }, + }); + + await cascadeDeleteDataSource({ + [Op.or]: [literal(`dataset_name ->> 'en' LIKE 'XX_INVENTORY_TEST_%'`)], + }); + + await db.models.Sector.destroy({ + where: { sectorName: { [Op.like]: "XX_INVENTORY_TEST%" } }, + }); + await db.models.City.destroy({ where: { locode } }); + await db.models.SubCategory.destroy({ + where: { subcategoryName }, + }); + await db.models.SubSector.destroy({ + where: { subsectorName: { [Op.like]: "XX_INVENTORY_%" } }, + }); + await db.models.Sector.destroy({ + where: { sectorName: { [Op.like]: "XX_INVENTORY_TEST%" } }, + }); + await db.models.Sector.destroy({ where: { sectorName } }); + await db.models.Sector.destroy({ + where: { sectorName: { [Op.like]: "XX_INVENTORY_PROGRESS_TEST%" } }, + }); + city = await db.models.City.create({ + cityId: randomUUID(), + name: cityName, + country: cityCountry, + locode, + }); + await db.models.User.upsert({ userId: testUserID, name: "TEST_USER" }); + await city.addUser(testUserID); + sector = await db.models.Sector.create({ + sectorId: randomUUID(), + sectorName, + }); + subSector = await db.models.SubSector.create({ + subsectorId: randomUUID(), + sectorId: sector.sectorId, + subsectorName: subsectorName, + }); + subSector2 = await db.models.SubSector.create({ + subsectorId: randomUUID(), + sectorId: sector.sectorId, + subsectorName: subSectorName2, + }); + subCategory = await db.models.SubCategory.create({ + subcategoryId: randomUUID(), + subsectorId: subSector2.subsectorId, + }); + }); + + beforeEach(async () => { + await db.models.Inventory.destroy({ where: { inventoryName } }); + inventory = await db.models.Inventory.create({ + inventoryId: randomUUID(), + cityId: city.cityId, + ...inventoryData, + }); + await db.models.InventoryValue.create({ + id: randomUUID(), + inventoryId: inventory.inventoryId, + subCategoryId: subCategory.subcategoryId, + ...inventoryValue, + }); + }); + + afterAll(async () => { + await db.models.SubCategory.destroy({ + where: { subcategoryId: subCategory.subcategoryId }, + }); + await db.models.SubSector.destroy({ + where: { subsectorId: subSector.subsectorId }, + }); + await db.models.SubSector.destroy({ + where: { subsectorId: subSector2.subsectorId }, + }); + await db.models.Sector.destroy({ where: { sectorId: sector.sectorId } }); + await db.models.City.destroy({ where: { locode } }); + if (db.sequelize) await db.sequelize.close(); + }); + + it("should create an inventory", async () => { + await db.models.Inventory.destroy({ + where: { inventoryName }, + }); + const req = mockRequest(inventoryData); + const res = await createInventory(req, { + params: { city: city.cityId }, + }); + await db.models.Population.create({ + cityId: city.cityId!, + year: 2020, + population: 1000, + }); + await expectStatusCode(res, 200); + const { data } = await res.json(); + expect(data.inventoryName).toEqual(inventory.inventoryName); + expect(data.year).toEqual(inventory.year); + expect(data.totalEmissions).toEqual(inventory.totalEmissions); + }); + + it("should not create an inventory with invalid data", async () => { + const req = mockRequest(invalidInventory); + const res = await createInventory(req, { + params: { city: locode }, + }); + expect(res.status).toEqual(400); + const { + error: { issues }, + } = await res.json(); + expect(issues.length).toEqual(3); + }); + + it("should find an inventory", async () => { + const req = mockRequest(); + const res = await findInventory(req, { + params: { inventory: inventory.inventoryId }, + }); + expect(res.status).toEqual(200); + const { data } = await res.json(); + expect(data.inventoryName).toEqual(inventory.inventoryName); + expect(data.year).toEqual(inventory.year); + expect(data.totalEmissions).toEqual(inventory.totalEmissions); + }); + + it.skip("should download an inventory in csv format", async () => { + const url = `http://localhost:3000/api/v0/inventory/${inventory.inventoryId}?format=csv`; + const req = createRequest(url); + const res = await findInventory(req, { + params: { inventory: inventory.inventoryId }, + }); + expect(res.status).toEqual(200); + expect(res.headers.get("content-type")).toEqual("text/csv"); + expect( + res.headers.get("content-disposition")?.startsWith("attachment"), + ).toBeTruthy(); + const csv = await res.text(); + const lines = csv.split("\n"); + expect(lines.length > 0).toBeTruthy(); + const headers = lines[0].split(","); + expect(headers.length).toEqual(7); + expect(headers).toEqual([ + "Inventory Reference", + "GPC Reference Number", + "Total Emissions", + "Activity Units", + "Activity Value", + "Emission Factor Value", + "Datasource ID", + ]); + expect(lines.length > 1).toBeTruthy(); + expect(lines.length).toStrictEqual(2); + expect( + lines.slice(1).every((line) => line.split(",").length == 6), + ).toBeTruthy(); + }); + + // TODO this test is very slow. use "CIRIS Light" spreadsheet instead (for download as well anyways) + it.skip("should download an inventory in xls format", async () => { + const url = `http://localhost:3000/api/v0/inventory/${inventory.inventoryId}?format=xls`; + const req = createRequest(url); + const res = await findInventory(req, { + params: { inventory: inventory.inventoryId }, + }); + expect(res.status).toEqual(200); + expect(res.headers.get("content-type")).toEqual("application/vnd.ms-excel"); + expect( + res.headers.get("content-disposition")?.startsWith("attachment"), + ).toBeTruthy(); + const body = await res.blob(); + }); + + it("should not find non-existing inventories", async () => { + const req = mockRequest(invalidInventory); + const res = await findInventory(req, { + params: { inventory: randomUUID() }, + }); + expect(res.status).toEqual(404); + }); + + it("should update an inventory", async () => { + const req = mockRequest(inventoryData2); + const res = await updateInventory(req, { + params: { inventory: inventory.inventoryId }, + }); + expect(res.status).toEqual(200); + const { data } = await res.json(); + expect(data.inventoryName).toEqual(inventoryData2.inventoryName); + expect(data.year).toEqual(inventoryData2.year); + expect(data.totalEmissions).toEqual(inventoryData2.totalEmissions); + }); + + it("should not update an inventory with invalid data", async () => { + const req = mockRequest(invalidInventory); + const res = await updateInventory(req, { + params: { inventory: inventory.inventoryId }, + }); + expect(res.status).toEqual(400); + const { + error: { issues }, + } = await res.json(); + expect(issues.length).toEqual(3); + }); + + it("should delete an inventory", async () => { + const req = mockRequest(); + const res = await deleteInventory(req, { + params: { inventory: inventory.inventoryId }, + }); + expect(res.status).toEqual(200); + const { data, deleted } = await res.json(); + expect(deleted).toEqual(true); + expect(data.inventoryName).toEqual(inventory.inventoryName); + expect(data.year).toEqual(inventory.year); + expect(data.totalEmissions).toEqual(inventory.totalEmissions); + }); + + it("should not delete a non-existing inventory", async () => { + const req = mockRequest(); + const res = await deleteInventory(req, { + params: { inventory: randomUUID() }, + }); + expect(res.status).toEqual(404); + }); + + it("should calculate progress for an inventory", async () => { + // setup mock data + const existingInventory = await db.models.Inventory.findOne({ + where: { inventoryName }, + }); + expect(existingInventory).not.toEqual(null); + const sectorNames = ["PROGRESS_TEST1", "PROGRESS_TEST2", "PROGRESS_TEST3"]; + const userSource = await db.models.DataSource.create({ + datasourceId: randomUUID(), + sourceType: "user", + datasetName: { + en: "XX_INVENTORY_TEST_USE", + }, + }); + const thirdPartySource = await db.models.DataSource.create({ + datasourceId: randomUUID(), + sourceType: "third_party", + datasetName: { en: "XX_INVENTORY_TEST_THIRD_PARTY" }, + }); + const sources = [userSource, thirdPartySource, null]; + + for (const sectorName of sectorNames) { + const sectorId = randomUUID(); + await db.models.Sector.create({ + sectorId, + sectorName: "XX_INVENTORY_" + sectorName, + }); + for (let i = 0; i < sectorNames.length; i++) { + const subSectorId = randomUUID(); + await db.models.SubSector.create({ + subsectorId: subSectorId, + sectorId, + subsectorName: "XX_INVENTORY_" + sectorName + "_" + sectorNames[i], + }); + for (let j = 0; j < sectorNames.length; j++) { + const subCategoryId = randomUUID(); + await db.models.SubCategory.create({ + subcategoryId: subCategoryId, + subsectorId: subSectorId, + subcategoryName: `XX_INVENTORY_${sectorName}_${sectorNames[i]}_${sectorNames[j]}`, + }); + if (sources[i] != null) { + await db.models.InventoryValue.create({ + id: randomUUID(), + sectorId, + subSectorId, + subCategoryId, + datasourceId: sources[i]?.datasourceId, + inventoryId: existingInventory!.inventoryId, + }); + } + } + } + } + + const req = mockRequest(); + const res = await calculateProgress(req, { + params: { inventory: inventory.inventoryId }, + }); + + expect(res.status).toEqual(200); + const { totalProgress, sectorProgress } = (await res.json()).data; + const cleanedSectorProgress = sectorProgress + .filter(({ sector: checkSector }: { sector: { sectorName: string } }) => { + return checkSector.sectorName.startsWith("XX_INVENTORY_PROGRESS_TEST"); + }) + .map( + ({ + sector, + subSectors, + ...progress + }: { + sector: { sectorName: string; sectorId: string; completed: boolean }; + subSectors: Array; + }) => { + expect(sector.sectorId).not.toEqual(null); + expect(subSectors.length).toEqual(3); + for (const subSector of subSectors) { + expect(subSector.completed).not.toEqual(null); + } + return { sector: { sectorName: sector.sectorName }, ...progress }; + }, + ); + expect(cleanedSectorProgress.length).toEqual(3); + for (const sector of cleanedSectorProgress) { + expect(sector.total).toEqual(9); + expect(sector.thirdParty).toEqual(3); + expect(sector.uploaded).toEqual(3); + expect( + sector.sector.sectorName.startsWith("XX_INVENTORY_PROGRESS_TEST"), + ).toBeTruthy(); + } + expect(totalProgress.thirdParty).toEqual(9); + expect(totalProgress.uploaded).toEqual(9); + // TODO the route counts subsectors created by other tests/ seeders + // expect(totalProgress.total).toEqual(27); + }); + + it.skip("should submit an inventory to the CDP test API", async () => { + const req = mockRequest({}); + const res = await submitInventory(req, { + params: { inventory: inventory.inventoryId }, + }); + await expectStatusCode(res, 200); + const json = await res.json(); + expect(json.success).toBe(true); + }); +}); diff --git a/app/tests/api/inventory.test.ts b/app/tests/api/inventory.test.ts index 8738d61e1..8ba8c7f62 100644 --- a/app/tests/api/inventory.test.ts +++ b/app/tests/api/inventory.test.ts @@ -8,26 +8,22 @@ import { POST as createInventory } from "@/app/api/v0/city/[city]/inventory/rout import { POST as submitInventory } from "@/app/api/v0/inventory/[inventory]/cdp/route"; import { db } from "@/models"; import { CreateInventoryRequest } from "@/util/validation"; +import assert from "node:assert"; import { randomUUID } from "node:crypto"; import { after, before, beforeEach, describe, it } from "node:test"; import { literal, Op } from "sequelize"; -import { createRequest, mockRequest, setupTests, testUserID } from "../helpers"; +import { + cascadeDeleteDataSource, + createRequest, + mockRequest, + setupTests, + testUserID, +} from "../helpers"; import { SubSector, SubSectorAttributes } from "@/models/SubSector"; import { City } from "@/models/City"; import { Inventory } from "@/models/Inventory"; import { Sector } from "@/models/Sector"; import { SubCategory } from "@/models/SubCategory"; -import { - expect, - jest, - describe, - beforeAll, - afterAll, - beforeEach, - it, -} from "@jest/globals"; - -jest.useFakeTimers(); const locode = "XX_INVENTORY_CITY"; // Matches name given by CDP for API testing @@ -74,18 +70,18 @@ describe("Inventory API", () => { let subSector: SubSector; let subSector2: SubSector; - beforeAll(async () => { + before(async () => { setupTests(); await db.initialize(); // this also deletes all Sector/SubSectorValue instances associated with it (cascade) await db.models.Inventory.destroy({ where: { inventoryName }, }); - await db.models.DataSource.destroy({ - where: { - [Op.or]: [literal(`dataset_name ->> 'en' LIKE 'XX_INVENTORY_TEST_%'`)], - }, + + await cascadeDeleteDataSource({ + [Op.or]: [literal(`dataset_name ->> 'en' LIKE 'XX_INVENTORY_TEST_%'`)], }); + await db.models.Sector.destroy({ where: { sectorName: { [Op.like]: "XX_INVENTORY_TEST%" } }, }); @@ -146,7 +142,7 @@ describe("Inventory API", () => { }); }); - afterAll(async () => { + after(async () => { await db.models.SubCategory.destroy({ where: { subcategoryId: subCategory.subcategoryId }, }); @@ -161,67 +157,21 @@ describe("Inventory API", () => { if (db.sequelize) await db.sequelize.close(); }); - it("should create an inventory", async () => { - await db.models.Inventory.destroy({ - where: { inventoryName }, - }); - const req = mockRequest(inventoryData); - const res = await createInventory(req, { - params: { city: city.cityId }, - }); - await db.models.Population.create({ - cityId: city.cityId!, - year: 2020, - population: 1000, - }); - assert.equal(res.status, 200); - const { data } = await res.json(); - expect(data.inventoryName).toEqual(inventory.inventoryName); - expect(data.year).toEqual(inventory.year); - expect(data.totalEmissions).toEqual(inventory.totalEmissions); - }); - - it("should not create an inventory with invalid data", async () => { - const req = mockRequest(invalidInventory); - const res = await createInventory(req, { - params: { city: locode }, - }); - expect(res.status).toEqual(400); - const { - error: { issues }, - } = await res.json(); - expect(issues.length).toEqual(3); - }); - - it("should find an inventory", async () => { - const req = mockRequest(); - const res = await findInventory(req, { - params: { inventory: inventory.inventoryId }, - }); - expect(res.status).toEqual(200); - const { data } = await res.json(); - expect(data.inventoryName).toEqual(inventory.inventoryName); - expect(data.year).toEqual(inventory.year); - expect(data.totalEmissions).toEqual(inventory.totalEmissions); - }); - it.skip("should download an inventory in csv format", async () => { const url = `http://localhost:3000/api/v0/inventory/${inventory.inventoryId}?format=csv`; const req = createRequest(url); const res = await findInventory(req, { params: { inventory: inventory.inventoryId }, }); - expect(res.status).toEqual(200); - expect(res.headers.get("content-type")).toEqual("text/csv"); - expect( - res.headers.get("content-disposition")?.startsWith("attachment"), - ).toBeTruthy(); + assert.equal(res.status, 200); + assert.equal(res.headers.get("content-type"), "text/csv"); + assert.ok(res.headers.get("content-disposition")?.startsWith("attachment")); const csv = await res.text(); const lines = csv.split("\n"); - expect(lines.length > 0).toBeTruthy(); + assert.ok(lines.length > 0); const headers = lines[0].split(","); - expect(headers.length).toEqual(7); - expect(headers).toEqual([ + assert.equal(headers.length, 7); + assert.deepEqual(headers, [ "Inventory Reference", "GPC Reference Number", "Total Emissions", @@ -230,11 +180,9 @@ describe("Inventory API", () => { "Emission Factor Value", "Datasource ID", ]); - expect(lines.length > 1).toBeTruthy(); - expect(lines.length).toStrictEqual(2); - expect( - lines.slice(1).every((line) => line.split(",").length == 6), - ).toBeTruthy(); + assert.ok(lines.length > 1, csv); + assert.strictEqual(lines.length, 2); + assert.ok(lines.slice(1).every((line) => line.split(",").length == 6)); }); // TODO this test is very slow. use "CIRIS Light" spreadsheet instead (for download as well anyways) @@ -244,165 +192,12 @@ describe("Inventory API", () => { const res = await findInventory(req, { params: { inventory: inventory.inventoryId }, }); - expect(res.status).toEqual(200); - expect(res.headers.get("content-type")).toEqual("application/vnd.ms-excel"); - expect( - res.headers.get("content-disposition")?.startsWith("attachment"), - ).toBeTruthy(); + assert.equal(res.status, 200); + assert.equal(res.headers.get("content-type"), "application/vnd.ms-excel"); + assert.ok(res.headers.get("content-disposition")?.startsWith("attachment")); const body = await res.blob(); }); - it("should not find non-existing inventories", async () => { - const req = mockRequest(invalidInventory); - const res = await findInventory(req, { - params: { inventory: randomUUID() }, - }); - expect(res.status).toEqual(404); - }); - - it("should update an inventory", async () => { - const req = mockRequest(inventoryData2); - const res = await updateInventory(req, { - params: { inventory: inventory.inventoryId }, - }); - expect(res.status).toEqual(200); - const { data } = await res.json(); - expect(data.inventoryName).toEqual(inventoryData2.inventoryName); - expect(data.year).toEqual(inventoryData2.year); - expect(data.totalEmissions).toEqual(inventoryData2.totalEmissions); - }); - - it("should not update an inventory with invalid data", async () => { - const req = mockRequest(invalidInventory); - const res = await updateInventory(req, { - params: { inventory: inventory.inventoryId }, - }); - expect(res.status).toEqual(400); - const { - error: { issues }, - } = await res.json(); - expect(issues.length).toEqual(3); - }); - - it("should delete an inventory", async () => { - const req = mockRequest(); - const res = await deleteInventory(req, { - params: { inventory: inventory.inventoryId }, - }); - expect(res.status).toEqual(200); - const { data, deleted } = await res.json(); - expect(deleted).toEqual(true); - expect(data.inventoryName).toEqual(inventory.inventoryName); - expect(data.year).toEqual(inventory.year); - expect(data.totalEmissions).toEqual(inventory.totalEmissions); - }); - - it("should not delete a non-existing inventory", async () => { - const req = mockRequest(); - const res = await deleteInventory(req, { - params: { inventory: randomUUID() }, - }); - expect(res.status).toEqual(404); - }); - - it("should calculate progress for an inventory", async () => { - // setup mock data - const existingInventory = await db.models.Inventory.findOne({ - where: { inventoryName }, - }); - expect(existingInventory).not.toEqual(null); - const sectorNames = ["PROGRESS_TEST1", "PROGRESS_TEST2", "PROGRESS_TEST3"]; - const userSource = await db.models.DataSource.create({ - datasourceId: randomUUID(), - sourceType: "user", - datasetName: { - en: "XX_INVENTORY_TEST_USE", - }, - }); - const thirdPartySource = await db.models.DataSource.create({ - datasourceId: randomUUID(), - sourceType: "third_party", - datasetName: { en: "XX_INVENTORY_TEST_THIRD_PARTY" }, - }); - const sources = [userSource, thirdPartySource, null]; - - for (const sectorName of sectorNames) { - const sectorId = randomUUID(); - await db.models.Sector.create({ - sectorId, - sectorName: "XX_INVENTORY_" + sectorName, - }); - for (let i = 0; i < sectorNames.length; i++) { - const subSectorId = randomUUID(); - await db.models.SubSector.create({ - subsectorId: subSectorId, - sectorId, - subsectorName: "XX_INVENTORY_" + sectorName + "_" + sectorNames[i], - }); - for (let j = 0; j < sectorNames.length; j++) { - const subCategoryId = randomUUID(); - await db.models.SubCategory.create({ - subcategoryId: subCategoryId, - subsectorId: subSectorId, - subcategoryName: `XX_INVENTORY_${sectorName}_${sectorNames[i]}_${sectorNames[j]}`, - }); - if (sources[i] != null) { - await db.models.InventoryValue.create({ - id: randomUUID(), - sectorId, - subSectorId, - subCategoryId, - datasourceId: sources[i]?.datasourceId, - inventoryId: existingInventory!.inventoryId, - }); - } - } - } - } - - const req = mockRequest(); - const res = await calculateProgress(req, { - params: { inventory: inventory.inventoryId }, - }); - - expect(res.status).toEqual(200); - const { totalProgress, sectorProgress } = (await res.json()).data; - const cleanedSectorProgress = sectorProgress - .filter(({ sector: checkSector }: { sector: { sectorName: string } }) => { - return checkSector.sectorName.startsWith("XX_INVENTORY_PROGRESS_TEST"); - }) - .map( - ({ - sector, - subSectors, - ...progress - }: { - sector: { sectorName: string; sectorId: string; completed: boolean }; - subSectors: Array; - }) => { - expect(sector.sectorId).not.toEqual(null); - expect(subSectors.length).toEqual(3); - for (const subSector of subSectors) { - expect(subSector.completed).not.toEqual(null); - } - return { sector: { sectorName: sector.sectorName }, ...progress }; - }, - ); - expect(cleanedSectorProgress.length).toEqual(3); - for (const sector of cleanedSectorProgress) { - expect(sector.total).toEqual(9); - expect(sector.thirdParty).toEqual(3); - expect(sector.uploaded).toEqual(3); - expect( - sector.sector.sectorName.startsWith("XX_INVENTORY_PROGRESS_TEST"), - ).toBeTruthy(); - } - expect(totalProgress.thirdParty).toEqual(9); - expect(totalProgress.uploaded).toEqual(9); - // TODO the route counts subsectors created by other tests/ seeders - // expect(totalProgress.total).toEqual(27); - }); - it( "should submit an inventory to the CDP test API", { skip: true }, diff --git a/app/tests/api/inventory_value.jest.ts b/app/tests/api/inventory_value.jest.ts new file mode 100644 index 000000000..90ff3e486 --- /dev/null +++ b/app/tests/api/inventory_value.jest.ts @@ -0,0 +1,312 @@ +import { + DELETE as deleteInventoryValue, + GET as findInventoryValue, + PATCH as upsertInventoryValue, +} from "@/app/api/v0/inventory/[inventory]/value/[subcategory]/route"; +import { GET as batchFindInventoryValues } from "@/app/api/v0/inventory/[inventory]/value/route"; + +import { db } from "@/models"; +import { CreateInventoryValueRequest } from "@/util/validation"; +import { randomUUID } from "node:crypto"; +import { + describe, + expect, + beforeAll, + beforeEach, + afterAll, + it, +} from "@jest/globals"; + +import { + expectStatusCode, + expectToBeLooselyEqual, + mockRequest, + setupTests, + testUserID, +} from "../helpers"; + +import { Inventory } from "@/models/Inventory"; +import { InventoryValue } from "@/models/InventoryValue"; +import { SubCategory } from "@/models/SubCategory"; +import { SubSector } from "@/models/SubSector"; + +const locode = "XX_SUBCATEGORY_CITY"; +const co2eq = 44000n; +const activityUnits = "UNITS"; +const activityValue = 1000; +const inventoryName = "TEST_SUBCATEGORY_INVENTORY"; +const subcategoryName = "TEST_SUBCATEGORY_SUBCATEGORY"; +const subsectorName = "TEST_SUBCATEGORY_SUBSECTOR"; + +const baseInventory = { + cityPopulation: 0, + regionPopulation: 0, + countryPopulation: 0, + cityPopulationYear: 0, + regionPopulationYear: 0, + countryPopulationYear: 0, +}; +const inventoryValue1: CreateInventoryValueRequest = { + ...baseInventory, + activityUnits, + activityValue, + co2eq, +}; + +const inventoryValue2: CreateInventoryValueRequest = { + ...baseInventory, + activityUnits, + activityValue, + co2eq: 700000n, +}; + +const invalidInventoryValue = { + ...baseInventory, + activityUnits: activityUnits, + activityValue: 1000000, + co2eq: -1n, +}; + +describe("Inventory Value API", () => { + let inventory: Inventory; + let subCategory: SubCategory; + let subSector: SubSector; + let inventoryValue: InventoryValue; + + beforeAll(async () => { + setupTests(); + await db.initialize(); + + await db.models.SubCategory.destroy({ + where: { subcategoryName }, + }); + await db.models.SubSector.destroy({ + where: { subsectorName }, + }); + await db.models.City.destroy({ where: { locode } }); + + const prevInventory = await db.models.Inventory.findOne({ + where: { inventoryName }, + }); + if (prevInventory) { + await db.models.InventoryValue.destroy({ + where: { inventoryId: prevInventory.inventoryId }, + }); + await db.models.Inventory.destroy({ + where: { inventoryName }, + }); + } + + const city = await db.models.City.create({ + cityId: randomUUID(), + locode, + }); + await db.models.User.upsert({ userId: testUserID, name: "TEST_USER" }); + await city.addUser(testUserID); + inventory = await db.models.Inventory.create({ + inventoryId: randomUUID(), + inventoryName: "TEST_SUBCATEGORY_INVENTORY", + cityId: city.cityId, + }); + + subSector = await db.models.SubSector.create({ + subsectorId: randomUUID(), + subsectorName, + }); + + subCategory = await db.models.SubCategory.create({ + subcategoryId: randomUUID(), + subsectorId: subSector.subsectorId, + subcategoryName, + }); + }); + + beforeEach(async () => { + await db.models.InventoryValue.destroy({ + where: { inventoryId: inventory.inventoryId }, + }); + inventoryValue = await db.models.InventoryValue.create({ + inventoryId: inventory.inventoryId, + id: randomUUID(), + subCategoryId: subCategory.subcategoryId, + co2eq, + activityUnits, + activityValue, + }); + }); + + afterAll(async () => { + if (db.sequelize) await db.sequelize.close(); + }); + + it("Should create an inventory value", async () => { + await db.models.InventoryValue.destroy({ + where: { id: inventoryValue.id }, + }); + const req = mockRequest(inventoryValue1); + const res = await upsertInventoryValue(req, { + params: { + inventory: inventory.inventoryId, + subcategory: subCategory.subcategoryId, + }, + }); + await expectStatusCode(res, 200); + const { data } = await res.json(); + + expect(data.activityUnits).toEqual(inventoryValue1.activityUnits); + expectToBeLooselyEqual(data.activityValue, inventoryValue1.activityValue); + expectToBeLooselyEqual(data.co2eq, inventoryValue1.co2eq); + }); + + it.skip("Should not create an inventory value with invalid data", async () => { + const req = mockRequest(invalidInventoryValue); + const res = await upsertInventoryValue(req, { + params: { + inventory: inventory.inventoryId, + subcategory: subCategory.subcategoryId, + }, + }); + await expectStatusCode(res, 400); + const { + error: { issues }, + } = await res.json(); + expect(issues.length).toEqual(3); + }); + + it("Should find an inventory value", async () => { + const req = mockRequest(); + const res = await findInventoryValue(req, { + params: { + inventory: inventory.inventoryId, + subcategory: subCategory.subcategoryId, + }, + }); + + const { data } = await res.json(); + + await expectStatusCode(res, 200); + expectToBeLooselyEqual(data.co2eq, co2eq); + expect(data.activityUnits).toEqual(activityUnits); + expectToBeLooselyEqual(data.activityValue, activityValue); + }); + + it("Should find multiple inventory values", async () => { + // prepare data + const subCategory2 = await db.models.SubCategory.create({ + subcategoryId: randomUUID(), + subsectorId: subSector.subsectorId, + subcategoryName: subcategoryName + "2", + }); + await db.models.InventoryValue.create({ + inventoryId: inventory.inventoryId, + id: randomUUID(), + subCategoryId: subCategory2.subcategoryId, + co2eq: inventoryValue2.co2eq, + activityUnits: inventoryValue2.activityUnits, + activityValue: inventoryValue2.activityValue, + }); + + const subCategoryIds = [ + subCategory.subcategoryId, + subCategory2.subcategoryId, + ].join(","); + const req = mockRequest(null, { subCategoryIds }); + const res = await batchFindInventoryValues(req, { + params: { + inventory: inventory.inventoryId, + }, + }); + + const { data } = await res.json(); + + await expectStatusCode(res, 200); + expect(data.length).toEqual(2); + + // database returns results in random order + data.sort((a: InventoryValue, b: InventoryValue) => { + if (a?.created && b?.created) { + return new Date(a.created).getTime() - new Date(b.created).getTime(); + } else { + return 0; + } + }); + + expectToBeLooselyEqual(data[0].co2eq, co2eq); + expectToBeLooselyEqual(data[1].co2eq, inventoryValue2.co2eq); + expect(data[0].activityUnits).toEqual(activityUnits); + expect(data[1].activityUnits).toEqual(inventoryValue2.activityUnits); + expectToBeLooselyEqual(data[0].activityValue, activityValue); + expectToBeLooselyEqual( + data[1].activityValue, + inventoryValue2.activityValue, + ); + }); + + it("Should not find a non-existing sub category", async () => { + const req = mockRequest(invalidInventoryValue); + const res = await findInventoryValue(req, { + params: { + inventory: inventory.inventoryId, + subcategory: randomUUID(), + }, + }); + await expectStatusCode(res, 404); + }); + + it.skip("Should update an inventory value", async () => { + const req = mockRequest(inventoryValue1); + const res = await upsertInventoryValue(req, { + params: { + inventory: inventory.inventoryId, + subcategory: subCategory.subcategoryId, + }, + }); + await expectStatusCode(res, 200); + const { data } = await res.json(); + expect(data.co2eq).toEqual(inventoryValue1.co2eq); + expect(data.activityUnits).toEqual(inventoryValue1.activityUnits); + expect(data.activityValue).toEqual(inventoryValue1.activityValue); + }); + + it.skip("Should not update an inventory value with invalid data", async () => { + const req = mockRequest(invalidInventoryValue); + const res = await upsertInventoryValue(req, { + params: { + inventory: inventory.inventoryId, + subcategory: subCategory.subcategoryId, + }, + }); + await expectStatusCode(res, 400); + const { + error: { issues }, + } = await res.json(); + expect(issues.length).toEqual(3); + }); + + it("Should delete an inventory value", async () => { + const req = mockRequest(inventoryValue2); + const res = await deleteInventoryValue(req, { + params: { + inventory: inventory.inventoryId, + subcategory: subCategory.subcategoryId, + }, + }); + await expectStatusCode(res, 200); + const { data, deleted } = await res.json(); + expect(deleted).toEqual(true); + expectToBeLooselyEqual(data.co2eq, co2eq); + expect(data.activityUnits).toEqual(activityUnits); + expectToBeLooselyEqual(data.activityValue, activityValue); + }); + + it("Should not delete a non-existing inventory value", async () => { + const req = mockRequest(inventoryValue2); + const res = await deleteInventoryValue(req, { + params: { + inventory: randomUUID(), + subcategory: randomUUID(), + }, + }); + await expectStatusCode(res, 404); + }); +}); diff --git a/app/tests/api/inventory_value.test.ts b/app/tests/api/inventory_value.test.ts index 4a4c36c14..a816fb297 100644 --- a/app/tests/api/inventory_value.test.ts +++ b/app/tests/api/inventory_value.test.ts @@ -7,22 +7,11 @@ import { GET as batchFindInventoryValues } from "@/app/api/v0/inventory/[invento import { db } from "@/models"; import { CreateInventoryValueRequest } from "@/util/validation"; +import assert from "node:assert"; import { randomUUID } from "node:crypto"; -import { - describe, - expect, - beforeAll, - beforeEach, - afterAll, - it, -} from "@jest/globals"; +import { after, before, beforeEach, describe, it } from "node:test"; -import { - expectStatusCode, - mockRequest, - setupTests, - testUserID, -} from "../helpers"; +import { mockRequest, setupTests, testUserID } from "../helpers"; import { Inventory } from "@/models/Inventory"; import { InventoryValue } from "@/models/InventoryValue"; @@ -37,21 +26,32 @@ const inventoryName = "TEST_SUBCATEGORY_INVENTORY"; const subcategoryName = "TEST_SUBCATEGORY_SUBCATEGORY"; const subsectorName = "TEST_SUBCATEGORY_SUBSECTOR"; +const baseInventory = { + cityPopulation: 0, + regionPopulation: 0, + countryPopulation: 0, + cityPopulationYear: 0, + regionPopulationYear: 0, + countryPopulationYear: 0, +}; const inventoryValue1: CreateInventoryValueRequest = { + ...baseInventory, activityUnits, activityValue, co2eq, }; const inventoryValue2: CreateInventoryValueRequest = { + ...baseInventory, activityUnits, activityValue, co2eq: 700000n, }; const invalidInventoryValue = { - activityUnits: 0, - activityValue: "1000s", + ...baseInventory, + activityUnits: activityUnits, + activityValue: 1000000, co2eq: -1n, }; @@ -61,7 +61,7 @@ describe("Inventory Value API", () => { let subSector: SubSector; let inventoryValue: InventoryValue; - beforeAll(async () => { + before(async () => { setupTests(); await db.initialize(); @@ -123,29 +123,10 @@ describe("Inventory Value API", () => { }); }); - afterAll(async () => { + after(async () => { if (db.sequelize) await db.sequelize.close(); }); - it("Should create an inventory value", async () => { - await db.models.InventoryValue.destroy({ - where: { id: inventoryValue.id }, - }); - const req = mockRequest(inventoryValue1); - const res = await upsertInventoryValue(req, { - params: { - inventory: inventory.inventoryId, - subcategory: subCategory.subcategoryId, - }, - }); - await expectStatusCode(res, 200); - const { data } = await res.json(); - - expect(data.activityUnits).toEqual(inventoryValue1.activityUnits); - expect(data.activityValue).toEqual(inventoryValue1.activityValue); - expect(data.co2eq).toEqual(inventoryValue1.co2eq); - }); - it("Should not create an inventory value with invalid data", async () => { const req = mockRequest(invalidInventoryValue); const res = await upsertInventoryValue(req, { @@ -154,11 +135,11 @@ describe("Inventory Value API", () => { subcategory: subCategory.subcategoryId, }, }); - await expectStatusCode(res, 400); + assert.equal(res.status, 400); const { error: { issues }, } = await res.json(); - expect(issues.length).toEqual(3); + assert.equal(issues.length, 3); }); it("Should find an inventory value", async () => { @@ -172,10 +153,10 @@ describe("Inventory Value API", () => { const { data } = await res.json(); - await expectStatusCode(res, 200); - expect(data.co2eq).toEqual(co2eq); - expect(data.activityUnits).toEqual(activityUnits); - expect(data.activityValue).toEqual(activityValue); + assert.equal(res.status, 200); + assert.equal(data.co2eq, co2eq); + assert.equal(data.activityUnits, activityUnits); + assert.equal(data.activityValue, activityValue); }); it("Should find multiple inventory values", async () => { @@ -207,8 +188,8 @@ describe("Inventory Value API", () => { const { data } = await res.json(); - await expectStatusCode(res, 200); - expect(data.length).toEqual(2); + assert.equal(res.status, 200); + assert.equal(data.length, 2); // database returns results in random order data.sort((a: InventoryValue, b: InventoryValue) => { @@ -219,23 +200,12 @@ describe("Inventory Value API", () => { } }); - expect(data[0].co2eq).toEqual(co2eq); - expect(data[1].co2eq).toEqual(inventoryValue2.co2eq); - expect(data[0].activityUnits).toEqual(activityUnits); - expect(data[1].activityUnits).toEqual(inventoryValue2.activityUnits); - expect(data[0].activityValue).toEqual(activityValue); - expect(data[1].activityValue).toEqual(inventoryValue2.activityValue); - }); - - it("Should not find a non-existing sub category", async () => { - const req = mockRequest(invalidInventoryValue); - const res = await findInventoryValue(req, { - params: { - inventory: inventory.inventoryId, - subcategory: randomUUID(), - }, - }); - await expectStatusCode(res, 404); + assert.equal(data[0].co2eq, co2eq); + assert.equal(data[1].co2eq, inventoryValue2.co2eq); + assert.equal(data[0].activityUnits, activityUnits); + assert.equal(data[1].activityUnits, inventoryValue2.activityUnits); + assert.equal(data[0].activityValue, activityValue); + assert.equal(data[1].activityValue, inventoryValue2.activityValue); }); it("Should update an inventory value", async () => { @@ -246,11 +216,11 @@ describe("Inventory Value API", () => { subcategory: subCategory.subcategoryId, }, }); - await expectStatusCode(res, 200); const { data } = await res.json(); - expect(data.co2eq).toEqual(inventoryValue1.co2eq); - expect(data.activityUnits).toEqual(inventoryValue1.activityUnits); - expect(data.activityValue).toEqual(inventoryValue1.activityValue); + assert.equal(res.status, 200); + assert.equal(data.co2eq, inventoryValue1.co2eq); + assert.equal(data.activityUnits, inventoryValue1.activityUnits); + assert.equal(data.activityValue, inventoryValue1.activityValue); }); it("Should not update an inventory value with invalid data", async () => { @@ -261,11 +231,11 @@ describe("Inventory Value API", () => { subcategory: subCategory.subcategoryId, }, }); - await expectStatusCode(res, 400); + assert.equal(res.status, 400); const { error: { issues }, } = await res.json(); - expect(issues.length).toEqual(3); + assert.equal(issues.length, 3); }); it("Should delete an inventory value", async () => { @@ -276,22 +246,11 @@ describe("Inventory Value API", () => { subcategory: subCategory.subcategoryId, }, }); - await expectStatusCode(res, 200); + assert.equal(res.status, 200); const { data, deleted } = await res.json(); - expect(deleted).toEqual(true); - expect(data.co2eq).toEqual(co2eq); - expect(data.activityUnits).toEqual(activityUnits); - expect(data.activityValue).toEqual(activityValue); - }); - - it("Should not delete a non-existing inventory value", async () => { - const req = mockRequest(inventoryValue2); - const res = await deleteInventoryValue(req, { - params: { - inventory: randomUUID(), - subcategory: randomUUID(), - }, - }); - await expectStatusCode(res, 404); + assert.equal(deleted, true); + assert.equal(data.co2eq, co2eq); + assert.equal(data.activityUnits, activityUnits); + assert.equal(data.activityValue, activityValue); }); }); diff --git a/app/tests/api/population.test.ts b/app/tests/api/population.jest.ts similarity index 71% rename from app/tests/api/population.test.ts rename to app/tests/api/population.jest.ts index 3b2256351..2744bb8cd 100644 --- a/app/tests/api/population.test.ts +++ b/app/tests/api/population.jest.ts @@ -1,10 +1,15 @@ import { POST as savePopulations } from "@/app/api/v0/city/[city]/population/route"; import { db } from "@/models"; -import { mockRequest, setupTests, testUserID } from "../helpers"; +import { + expectToBeLooselyEqual, + mockRequest, + setupTests, + testUserID, +} from "../helpers"; import { CreatePopulationRequest } from "@/util/validation"; import { Op } from "sequelize"; import { keyBy } from "@/util/helpers"; -import { describe, expect, beforeAll, afterAll, it } from "@jest/globals"; +import { afterAll, beforeAll, describe, expect, it } from "@jest/globals"; const cityId = "76bb1ab7-5177-45a1-a61f-cfdee9c448e8"; @@ -61,23 +66,28 @@ describe("Population API", () => { const res = await savePopulations(req, { params: { city: cityId } }); expect(res.status).toEqual(200); const data = await res.json(); - - expect(data.data.cityPopulation.population).toEqual( + expectToBeLooselyEqual( + data.data.cityPopulation.population, validPopulationUpdate.cityPopulation, ); - expect(data.data.cityPopulation.year).toEqual( + expectToBeLooselyEqual( + data.data.cityPopulation.year, validPopulationUpdate.cityPopulationYear, ); - expect(data.data.regionPopulation.regionPopulation).toEqual( + expectToBeLooselyEqual( + data.data.regionPopulation.regionPopulation, validPopulationUpdate.regionPopulation, ); - expect(data.data.regionPopulation.year).toEqual( + expectToBeLooselyEqual( + data.data.regionPopulation.year, validPopulationUpdate.regionPopulationYear, ); - expect(data.data.countryPopulation.countryPopulation).toEqual( + expectToBeLooselyEqual( + data.data.countryPopulation.countryPopulation, validPopulationUpdate.countryPopulation, ); - expect(data.data.countryPopulation.year).toEqual( + expectToBeLooselyEqual( + data.data.countryPopulation.year, validPopulationUpdate.countryPopulationYear, ); @@ -86,9 +96,9 @@ describe("Population API", () => { }); expect(populations.length).toEqual(3); const populationByYear = keyBy(populations, (p) => p.year.toString()); - expect(populationByYear["1337"].population).toEqual(1); - expect(populationByYear["1338"].regionPopulation).toEqual(2); - expect(populationByYear["1339"].countryPopulation).toEqual(3); + expectToBeLooselyEqual(populationByYear["1337"].population, 1); + expectToBeLooselyEqual(populationByYear["1338"].regionPopulation, 2); + expectToBeLooselyEqual(populationByYear["1339"].countryPopulation, 3); }); it("should correctly save population information for the same year", async () => { @@ -97,22 +107,28 @@ describe("Population API", () => { expect(res.status).toEqual(200); const data = await res.json(); - expect(data.data.cityPopulation.population).toEqual( + expectToBeLooselyEqual( + data.data.cityPopulation.population, overlappingPopulationUpdate.cityPopulation, ); - expect(data.data.cityPopulation.year).toEqual( + expectToBeLooselyEqual( + data.data.cityPopulation.year, overlappingPopulationUpdate.cityPopulationYear, ); - expect(data.data.regionPopulation.regionPopulation).toEqual( + expectToBeLooselyEqual( + data.data.regionPopulation.regionPopulation, overlappingPopulationUpdate.regionPopulation, ); - expect(data.data.regionPopulation.year).toEqual( + expectToBeLooselyEqual( + data.data.regionPopulation.year, overlappingPopulationUpdate.regionPopulationYear, ); - expect(data.data.countryPopulation.countryPopulation).toEqual( + expectToBeLooselyEqual( + data.data.countryPopulation.countryPopulation, overlappingPopulationUpdate.countryPopulation, ); - expect(data.data.countryPopulation.year).toEqual( + expectToBeLooselyEqual( + data.data.countryPopulation.year, overlappingPopulationUpdate.countryPopulationYear, ); @@ -120,9 +136,9 @@ describe("Population API", () => { where: { cityId, year: 1340 }, }); expect(populations.length).toEqual(1); - expect(populations[0].population).toEqual(4); - expect(populations[0].regionPopulation).toEqual(5); - expect(populations[0].countryPopulation).toEqual(6); + expectToBeLooselyEqual(populations[0].population, 4); + expectToBeLooselyEqual(populations[0].regionPopulation, 5); + expectToBeLooselyEqual(populations[0].countryPopulation, 6); }); it("should not save invalid population information", async () => { diff --git a/app/tests/api/user.test.ts b/app/tests/api/user.jest.ts similarity index 77% rename from app/tests/api/user.test.ts rename to app/tests/api/user.jest.ts index 9cec2d2d7..61cb820b9 100644 --- a/app/tests/api/user.test.ts +++ b/app/tests/api/user.jest.ts @@ -1,10 +1,10 @@ import { PATCH as updateUser } from "@/app/api/v0/user/route"; import { db } from "@/models"; import { UserAttributes } from "@/models/User"; -import assert from "node:assert"; -import { after, before, describe, it } from "node:test"; +import { beforeAll, afterAll, describe, it, expect } from "@jest/globals"; import { mockRequest, setupTests, testUserID } from "../helpers"; +// Test Data const inventoryId = "dab66377-a4fc-46d2-9782-5a87282d39fa"; const userData: UserAttributes = { @@ -21,7 +21,7 @@ const invalidUserUpdate = { }; describe("User API", () => { - before(async () => { + beforeAll(async () => { setupTests(); await db.initialize(); await db.models.Inventory.destroy({ @@ -34,33 +34,32 @@ describe("User API", () => { }); }); - after(async () => { + afterAll(async () => { if (db.sequelize) await db.sequelize.close(); }); it("should update a user", async () => { const req = mockRequest(userUpdate); const res = await updateUser(req, { params: {} }); - assert.equal(res.status, 200); + expect(res.status).toBe(200); const data = await res.json(); - assert.ok(data.success); + expect(data.success).toBeTruthy(); const user = await db.models.User.findOne({ where: { userId: userData.userId }, }); - assert.ok(user != null); - assert.equal(user.defaultInventoryId, inventoryId); + expect(user).not.toBeNull(); + expect(user!.defaultInventoryId).toBe(inventoryId); }); it("should not update a user with invalid data", async () => { const req = mockRequest(invalidUserUpdate); const res = await updateUser(req, { params: {} }); - assert.equal(res.status, 400); + expect(res.status).toBe(400); const user = await db.models.User.findOne({ where: { userId: userData.userId }, }); - assert.ok(user != null); - assert.notEqual( - user.defaultInventoryId, + expect(user).not.toBeNull(); + expect(user!.defaultInventoryId).not.toBe( invalidUserUpdate.defaultInventoryId, ); }); diff --git a/app/tests/cdpservice.test.ts b/app/tests/cdpservice.test.ts index 033a0d89f..d579afb10 100644 --- a/app/tests/cdpservice.test.ts +++ b/app/tests/cdpservice.test.ts @@ -1,7 +1,7 @@ import { after, before, beforeEach, describe, it, mock } from "node:test"; -import { logger } from '@/services/logger'; -import CDPService from '@/backend/CDPService'; +import { logger } from "@/services/logger"; +import CDPService from "@/backend/CDPService"; import assert from "node:assert"; @@ -11,7 +11,7 @@ const TEST_QUESTION = "1"; const TEST_RESPONSE = ["Test response"]; describe.skip("CDPService", () => { - let cityID: string|null = null; + let cityID: string | null = null; it("should be in test mode", () => { assert.equal(CDPService.mode, "test"); @@ -19,7 +19,7 @@ describe.skip("CDPService", () => { before(async () => { cityID = await CDPService.getCityID(TEST_CITY, TEST_COUNTRY); - }) + }); it("should get a city ID from CDP", async () => { const testCityID = await CDPService.getCityID(TEST_CITY, TEST_COUNTRY); @@ -35,8 +35,8 @@ describe.skip("CDPService", () => { const response = await CDPService.submitMatrix( cityID!, TEST_QUESTION, - TEST_RESPONSE + TEST_RESPONSE, ); assert.notEqual(response, null); }); -}); \ No newline at end of file +}); diff --git a/app/tests/helpers.ts b/app/tests/helpers.ts index fc67e4bc7..bd1ee22cc 100644 --- a/app/tests/helpers.ts +++ b/app/tests/helpers.ts @@ -9,6 +9,9 @@ import fs from "fs"; import path from "path"; import { ApiResponse } from "@/util/api"; import { expect } from "@jest/globals"; +import { db } from "@/models"; +import { Op, WhereOptions } from "sequelize"; +import { DataSourceI18nAttributes } from "@/models/DataSourceI18n"; const mockUrl = "http://localhost:3000/api/v0"; @@ -113,3 +116,48 @@ export async function expectStatusCode( throw err; } } + +export const expectToBeLooselyEqual = (received: any, expected: any) => { + const pass = received == expected; + if (pass) { + return { + message: () => + `expected ${received} not to be loosely equal to ${expected}`, + pass: true, + }; + } else { + return { + message: () => `expected ${received} to be loosely equal to ${expected}`, + pass: false, + }; + } +}; + +/** deletes DataSources and their associated InventoryValues **/ +export const cascadeDeleteDataSource = async ( + where: WhereOptions, +) => { + const dataSources = await db.models.DataSource.findAll({ + where, + }); + + const dataSourceIds = dataSources.map((ds) => ds.datasourceId); + + if (dataSourceIds.length > 0) { + await db.models.InventoryValue.destroy({ + where: { + datasourceId: { + [Op.in]: dataSourceIds, + }, + }, + }); + + await db.models.DataSource.destroy({ + where: { + datasourceId: { + [Op.in]: dataSourceIds, + }, + }, + }); + } +}; diff --git a/app/tests/models.test.ts b/app/tests/models.jest.ts similarity index 100% rename from app/tests/models.test.ts rename to app/tests/models.jest.ts diff --git a/app/tests/series.test.ts b/app/tests/series.jest.ts similarity index 64% rename from app/tests/series.test.ts rename to app/tests/series.jest.ts index 76da43a60..dd16b5c1b 100644 --- a/app/tests/series.test.ts +++ b/app/tests/series.jest.ts @@ -1,60 +1,57 @@ -import { describe, it } from "node:test" -import assert from "node:assert/strict" -import { estimate } from "@/util/series" +import { estimate } from "@/util/series"; +import { describe, it, expect } from "@jest/globals"; describe("Series", () => { - let series0 = [ - { year: 2000, value: 100 } - ]; + let series0 = [{ year: 2000, value: 100 }]; let series = [ { year: 2000, value: 100 }, { year: 2010, value: 200 }, - { year: 2020, value: 400 } + { year: 2020, value: 400 }, ]; it("should return exact value for small series on match", () => { let result = estimate(series0, 2000); - assert.strictEqual(result, 100); - }) + expect(result).toBe(100); + }); it("should return null for small series on no match", () => { let result = estimate(series0, 2001); - assert.strictEqual(result, null); - }) + expect(result).toBeNull(); + }); it("should return exact value", () => { let result = estimate(series, 2010); - assert.strictEqual(result, 200); - }) + expect(result).toBe(200); + }); it("should return interpolated value at low rate", () => { let result = estimate(series, 2005); - assert.strictEqual(result, 150); - }) + expect(result).toBe(150); + }); it("should return interpolated value at high rate", () => { let result = estimate(series, 2015); - assert.strictEqual(result, 300); - }) + expect(result).toBe(300); + }); it("should return extrapolated value at low rate", () => { let result = estimate(series, 1995); - assert.strictEqual(result, 50); - }) + expect(result).toBe(50); + }); it("should return extrapolated value at high rate", () => { let result = estimate(series, 2025); - assert.strictEqual(result, 500); - }) + expect(result).toBe(500); + }); it("should return null if below safe extrapolation range", () => { let result = estimate(series, 1979); - assert.strictEqual(result, null); - }) + expect(result).toBeNull(); + }); it("should return null if above safe extrapolation range", () => { let result = estimate(series, 2041); - assert.strictEqual(result, null); - }) -}) \ No newline at end of file + expect(result).toBeNull(); + }); +});