From 5831edef1f4d4a2f86adde1cc8630e86ca54cafc Mon Sep 17 00:00:00 2001 From: jannisvisser Date: Wed, 15 Jan 2025 13:39:36 +0100 Subject: [PATCH 1/6] chore: set up autogenerated module dependency diagram AB#32535 --- services/API-service/module-dependencies.md | 43 +++++++++++++++++ services/API-service/package-lock.json | 49 +++++++++++++++++++ services/API-service/package.json | 1 + services/API-service/src/main.ts | 52 ++++++++++++++++++++- 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 services/API-service/module-dependencies.md diff --git a/services/API-service/module-dependencies.md b/services/API-service/module-dependencies.md new file mode 100644 index 000000000..91899ed4a --- /dev/null +++ b/services/API-service/module-dependencies.md @@ -0,0 +1,43 @@ +# Module Dependencies Graph + +```mermaid +graph LR + EapActionsModule-->UserModule + UserModule-->LookupModule + WaterpointsModule-->UserModule + WaterpointsModule-->CountryModule + CountryModule-->UserModule + AdminAreaModule-->UserModule + AdminAreaModule-->EventModule + EventModule-->UserModule + EventModule-->CountryModule + EventModule-->EapActionsModule + EventModule-->TyphoonTrackModule + TyphoonTrackModule-->UserModule + AdminAreaModule-->CountryModule + AdminAreaDynamicDataModule-->UserModule + AdminAreaDynamicDataModule-->EventModule + AdminAreaDynamicDataModule-->CountryModule + AdminAreaDynamicDataModule-->AdminAreaModule + MetadataModule-->UserModule + MetadataModule-->CountryModule + MetadataModule-->EventModule + PointDataModule-->UserModule + PointDataModule-->WhatsappModule + WhatsappModule-->LookupModule + WhatsappModule-->EventModule + WhatsappModule-->NotificationContentModule + NotificationContentModule-->EventModule + NotificationContentModule-->AdminAreaDynamicDataModule + NotificationContentModule-->AdminAreaDataModule + AdminAreaDataModule-->UserModule + NotificationContentModule-->AdminAreaModule + LinesDataModule-->UserModule + NotificationModule-->UserModule + NotificationModule-->EventModule + NotificationModule-->AdminAreaDynamicDataModule + NotificationModule-->WhatsappModule + NotificationModule-->NotificationContentModule + NotificationModule-->TyphoonTrackModule + CronjobModule-->AdminAreaDynamicDataModule +``` diff --git a/services/API-service/package-lock.json b/services/API-service/package-lock.json index 50e9f798e..cea71113c 100644 --- a/services/API-service/package-lock.json +++ b/services/API-service/package-lock.json @@ -26,6 +26,7 @@ "mailchimp-api-v3": "^1.15.0", "mjml": "^4.15.3", "mysql": "^2.15.0", + "nestjs-spelunker": "^1.3.1", "pg": "^8.13.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0", @@ -2349,6 +2350,19 @@ "node": ">=8" } }, + "node_modules/@ogma/common": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ogma/common/-/common-1.2.0.tgz", + "integrity": "sha512-y2GHkT4t3B9CoI7ELfgZXeMHiiOC/X4XvdeqYLbP5A3keop7fANzjd5pQdtyYQvdQ0lPzcHJiMvOGkIINZHZHQ==" + }, + "node_modules/@ogma/styler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ogma/styler/-/styler-1.1.0.tgz", + "integrity": "sha512-OtwJ8Ump3sKEAv2DIUi+R2G2Y5v3cEzqRO/sH11MTWikAhac31GZR/xzU+3BoPRT0u8oIob7N5dtOKTdPhqq3w==", + "dependencies": { + "@ogma/common": "^1.1.1" + } + }, "node_modules/@one-ini/wasm": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", @@ -9237,6 +9251,20 @@ "node": ">= 0.6" } }, + "node_modules/nestjs-spelunker": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/nestjs-spelunker/-/nestjs-spelunker-1.3.1.tgz", + "integrity": "sha512-Vvye5jPdhKF+wyUDFlAxoEqXqWaXvpuyIIWN0/2OmJ3WNP1g7G2ljGM8pTnlWNsC8DGDdJcWB0OZ3y9IM07n7Q==", + "license": "MIT", + "dependencies": { + "@ogma/styler": "^1.0.0" + }, + "peerDependencies": { + "@nestjs/common": ">6.11.0", + "@nestjs/core": ">6.11.0", + "reflect-metadata": "^0.1.0" + } + }, "node_modules/no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -13536,6 +13564,19 @@ } } }, + "@ogma/common": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ogma/common/-/common-1.2.0.tgz", + "integrity": "sha512-y2GHkT4t3B9CoI7ELfgZXeMHiiOC/X4XvdeqYLbP5A3keop7fANzjd5pQdtyYQvdQ0lPzcHJiMvOGkIINZHZHQ==" + }, + "@ogma/styler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ogma/styler/-/styler-1.1.0.tgz", + "integrity": "sha512-OtwJ8Ump3sKEAv2DIUi+R2G2Y5v3cEzqRO/sH11MTWikAhac31GZR/xzU+3BoPRT0u8oIob7N5dtOKTdPhqq3w==", + "requires": { + "@ogma/common": "^1.1.1" + } + }, "@one-ini/wasm": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", @@ -18710,6 +18751,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "nestjs-spelunker": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/nestjs-spelunker/-/nestjs-spelunker-1.3.1.tgz", + "integrity": "sha512-Vvye5jPdhKF+wyUDFlAxoEqXqWaXvpuyIIWN0/2OmJ3WNP1g7G2ljGM8pTnlWNsC8DGDdJcWB0OZ3y9IM07n7Q==", + "requires": { + "@ogma/styler": "^1.0.0" + } + }, "no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", diff --git a/services/API-service/package.json b/services/API-service/package.json index 2d23b939a..8ae5b1514 100644 --- a/services/API-service/package.json +++ b/services/API-service/package.json @@ -44,6 +44,7 @@ "mailchimp-api-v3": "^1.15.0", "mjml": "^4.15.3", "mysql": "^2.15.0", + "nestjs-spelunker": "^1.3.1", "pg": "^8.13.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0", diff --git a/services/API-service/src/main.ts b/services/API-service/src/main.ts index 5e6bbc7b9..c53ccbc4b 100644 --- a/services/API-service/src/main.ts +++ b/services/API-service/src/main.ts @@ -1,4 +1,9 @@ -import { BadRequestException, ValidationPipe } from '@nestjs/common'; +import fs from 'fs'; +import { + BadRequestException, + INestApplication, + ValidationPipe, +} from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, @@ -8,9 +13,47 @@ import { } from '@nestjs/swagger'; import * as bodyParser from 'body-parser'; +import { SpelunkerModule } from 'nestjs-spelunker'; import { ApplicationModule } from './app.module'; -import { EXTERNAL_API, PORT } from './config'; +import { DEBUG, EXTERNAL_API, PORT } from './config'; + +/** + * A visualization of module dependencies is generated using `nestjs-spelunker` + * The file can be vied with [Mermaid](https://mermaid.live) or the VSCode extention "bierner.markdown-mermaid" + * See: https://github.com/jmcdo29/nestjs-spelunker + */ +function generateModuleDependencyGraph(app: INestApplication): void { + const tree = SpelunkerModule.explore(app); + const root = SpelunkerModule.graph(tree); + const edges = SpelunkerModule.findGraphEdges(root); + const genericModules = [ + // Sorted alphabetically + 'ApplicationModule', + 'HealthModule', + 'HttpModule', + 'ScheduleModule', + 'ScriptsModule', + 'TerminusModule', + 'TypeOrmCoreModule', + 'TypeOrmModule', + ]; + const mermaidEdges = edges + .filter( + ({ from, to }) => + !genericModules.includes(from.module.name) && + !genericModules.includes(to.module.name), + ) + .map(({ from, to }) => ` ${from.module.name}-->${to.module.name}`); + const mermaidGraph = + '# Module Dependencies Graph\n\n```mermaid\ngraph LR\n' + + mermaidEdges.join('\n') + + '\n```\n'; + + fs.writeFile('module-dependencies.md', mermaidGraph, 'utf8', (err) => { + if (err) console.warn(`Writing API-graph failed!`, err); + }); +} async function bootstrap(): Promise { const appOptions = { cors: true }; @@ -67,6 +110,11 @@ async function bootstrap(): Promise { extended: true, }), ); + + if (DEBUG) { + generateModuleDependencyGraph(app); + } + await app.listen(process.env.PORT || PORT); } bootstrap(); From 6b5d98e54348da6d8ed5abad6f9165d1bbe830b8 Mon Sep 17 00:00:00 2001 From: jannisvisser Date: Wed, 15 Jan 2025 14:53:58 +0100 Subject: [PATCH 2/6] chore: remove red crescent references --- interfaces/IBF-dashboard/src/app/services/map.service.ts | 6 +----- interfaces/IBF-dashboard/src/app/types/ibf-layer.ts | 2 -- .../API-service/src/api/metadata/dto/add-indicators.dto.ts | 3 +-- services/API-service/src/api/metadata/dto/add-layers.dto.ts | 3 +-- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/interfaces/IBF-dashboard/src/app/services/map.service.ts b/interfaces/IBF-dashboard/src/app/services/map.service.ts index 7eb62b37a..b1128495d 100644 --- a/interfaces/IBF-dashboard/src/app/services/map.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/map.service.ts @@ -632,14 +632,10 @@ export class MapService { .pipe(shareReplay(1)); } else if (layer.type === IbfLayerType.point) { // NOTE: any non-standard point layers should be placed above this 'else if'! - const layerName = - layer.name === IbfLayerName.redCrescentBranches - ? IbfLayerName.redCrossBranches - : layer.name; layerData = this.apiService .getPointData( this.country.countryCodeISO3, - layerName, + layer.name, this.disasterType.disasterType, ) .pipe(shareReplay(1)); diff --git a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts index 405760572..216a3963b 100644 --- a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts +++ b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts @@ -106,7 +106,6 @@ export enum IbfLayerName { rainfall = 'rainfall', rainfallExtent = 'rainfall_extent', rainfallForecast = 'rainfall_forecast', - redCrescentBranches = 'red_crescent_branches', redCrossBranches = 'red_cross_branches', roads = 'roads', roof_type = 'roof_type', @@ -142,7 +141,6 @@ export enum IbfLayerLabel { population = 'Population', populationTotal = 'Total Population', rainfallExtent = 'Rainfall extent', - redCrescentBranches = 'Red Crescent branches', redCrossBranches = 'Red Cross branches', typhoonTrack = 'Typhoon track', waterpoints = 'Waterpoints', diff --git a/services/API-service/src/api/metadata/dto/add-indicators.dto.ts b/services/API-service/src/api/metadata/dto/add-indicators.dto.ts index 6bedca843..3cb6865d7 100644 --- a/services/API-service/src/api/metadata/dto/add-indicators.dto.ts +++ b/services/API-service/src/api/metadata/dto/add-indicators.dto.ts @@ -82,8 +82,7 @@ export class IndicatorDto { @ApiProperty({ example: { UGA: { - floods: - 'This layer represents the locations of the local branches, the source of this data comes from the National Society and may need updating.

Source link: Egyptian Red Crescent Society (ERCS). Year: 2020.', + floods: 'description', }, }, }) diff --git a/services/API-service/src/api/metadata/dto/add-layers.dto.ts b/services/API-service/src/api/metadata/dto/add-layers.dto.ts index 568dc799e..eba9aaffe 100644 --- a/services/API-service/src/api/metadata/dto/add-layers.dto.ts +++ b/services/API-service/src/api/metadata/dto/add-layers.dto.ts @@ -46,8 +46,7 @@ export class LayerDto { @ApiProperty({ example: { UGA: { - 'heavy-rain': - 'This layer represents the locations of the local branches, the source of this data comes from the National Society and may need updating.

Source link: Egyptian Red Crescent Society (ERCS). Year: 2020.', + 'heavy-rain': 'description', }, }, }) From f35dd069b3f6c781efd772988979654cf03149b0 Mon Sep 17 00:00:00 2001 From: jannisvisser Date: Wed, 15 Jan 2025 15:59:59 +0100 Subject: [PATCH 3/6] test: integration test on POST notification-info AB#32534 --- .../src/api/country/country.service.ts | 9 +++- .../src/scripts/json/notification-info.json | 5 +- .../fixtures/notification-info.const.ts | 43 ++++++++++++++++ .../API-service/dto/create-country.dto.ts | 30 +++++++++++ .../dto/create-notification-info.dto.ts | 12 +++++ tests/integration/helpers/country.helper.ts | 34 +++++++++++++ .../country/create-notification-info.test.ts | 51 +++++++++++++++++++ 7 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 tests/integration/fixtures/notification-info.const.ts create mode 100644 tests/integration/helpers/API-service/dto/create-country.dto.ts create mode 100644 tests/integration/helpers/API-service/dto/create-notification-info.dto.ts create mode 100644 tests/integration/helpers/country.helper.ts create mode 100644 tests/integration/tests/country/create-notification-info.test.ts diff --git a/services/API-service/src/api/country/country.service.ts b/services/API-service/src/api/country/country.service.ts index 71a7b8162..44594ef2c 100644 --- a/services/API-service/src/api/country/country.service.ts +++ b/services/API-service/src/api/country/country.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; @@ -222,6 +222,13 @@ export class CountryService { where: { countryCodeISO3: notificationInfoCountry.countryCodeISO3 }, relations: ['notificationInfo'], }); + if (!existingCountry) { + // It is not ideal to throw an error halfway, but it's at least better than the 500 error that currently would occur + throw new HttpException( + `Country with code ${notificationInfoCountry.countryCodeISO3} not found. If multiple countries passed, then earlier countries have processed correctly, later countries have not.`, + HttpStatus.NOT_FOUND, + ); + } if (existingCountry.notificationInfo) { existingCountry.notificationInfo = await this.createNotificationInfo( diff --git a/services/API-service/src/scripts/json/notification-info.json b/services/API-service/src/scripts/json/notification-info.json index fef4f3708..fcd40e14e 100644 --- a/services/API-service/src/scripts/json/notification-info.json +++ b/services/API-service/src/scripts/json/notification-info.json @@ -112,8 +112,8 @@ "triggerStatement": { "drought": "TBD" }, - "linkSocialMediaType": "WhatsApp", - "linkSocialMediaUrl": "https://chat.whatsapp.com/FfVimuGRHHiJSk0BU7nGQT", + "linkSocialMediaType": null, + "linkSocialMediaUrl": null, "linkVideo": "https://bit.ly/IBF-video-Zimbabwe", "linkPdf": "https://510ibfsystem.blob.core.windows.net/manuals/IBF%20Manual-Zimbabwe-Published.pdf" }, @@ -131,7 +131,6 @@ "linkSocialMediaUrl": "https://chat.whatsapp.com/Kjh3qxURJOQImAgUUmbmbR/", "linkVideo": "https://bit.ly/IBF-video-Malawi", "linkPdf": "https://510ibfsystem.blob.core.windows.net/manuals/IBF%20Manual-Malawi-Published.pdf", - "useWhatsapp": { "flash-floods": true, "floods": false diff --git a/tests/integration/fixtures/notification-info.const.ts b/tests/integration/fixtures/notification-info.const.ts new file mode 100644 index 000000000..d9fdbac53 --- /dev/null +++ b/tests/integration/fixtures/notification-info.const.ts @@ -0,0 +1,43 @@ +import { CreateNotificationInfoDto } from '../helpers/API-service/dto/create-notification-info.dto'; + +export const notificationInfoData: CreateNotificationInfoDto[] = [ + { + countryCodeISO3: 'MWI', + logo: { + floods: + 'https://mcusercontent.com/e71f3b134823403aa6fe0c6dc/images/31600ce7-b5e8-992f-8f53-e58f1b5dc955.png', + 'flash-floods': + 'https://mcusercontent.com/e71f3b134823403aa6fe0c6dc/images/d628fa5d-f4fa-bc51-9977-d6b464ff003b.png', + }, + triggerStatement: { + floods: + 'An administrative area is triggered based on two parameters from the 6-days GloFAS forecast on a daily basis: the return period of the forecasted flood and the probability of occurrence. The trigger will activate when GloFAS issues a forecast of at least 60% probability of occurrence of a 5 year return period flood within the next 6 days. The GloFAS flood forecast triggers except in the Traditional Areas where the False Alarm Ratio (FAR) exceeds the predetermined maximum value which is 0.5.

Be aware that if a flood alert is issued with less than 6 days of lead time, the EAP may not be activated, please consider alternative response options.', + 'flash-floods': + 'A notification is issued when the model predicts that the rainfall forecasted can potentially lead to a flood, exposing at least 20 people. The trigger model updates every 6 hours based on rainfall forecasts. A warning notification will be issued based on the rainfall forecast, with a lead time of up to 48h. A trigger notification will be issued if the threshold is exceeded, with a lead time of 12 hours or less.', + }, + linkSocialMediaType: 'WhatsApp', + linkSocialMediaUrl: 'https://chat.whatsapp.com/Kjh3qxURJOQImAgUUmbmbR/', + linkVideo: 'https://bit.ly/IBF-video-Malawi', + linkPdf: + 'https://510ibfsystem.blob.core.windows.net/manuals/IBF%20Manual-Malawi-Published.pdf', + useWhatsapp: { + 'flash-floods': true, + floods: false, + }, + whatsappMessage: { + 'flash-floods': { + 'initial-single-event': + "*IBF [triggerState] notification*\n\nA [triggerState] for flash floods is forecasted in *[eventName]* for: *[startTimeEvent]*.\n\nTo receive more detailed information reply 'yes' to this message.", + 'initial-multi-event': + "*IBF notification*\n\nThere are *[nrEvents]* notifications issued for flash floods. The first notification is forecasted for: *[startTimeFirstEvent]*.\n\nTo receive more detailed information reply 'yes' to this message.", + 'follow-up': + '*IBF [triggerState] notification*\n\nA [triggerState] for flash floods is forecasted in *[eventName]*: *[startTimeEvent]*.\n\nThere are *[nrTriggeredAreas]* [adminAreaLabel] listed below in order of potentially exposed population.\n[areaList]\nOpen the IBF Portal on a computer to get more information about this [triggerState].', + 'whatsapp-group': + 'Please use the designated WhatsApp group ([whatsappGroupLink]) to communicate about this trigger.', + 'no-trigger-old-event': + 'The trigger warning formerly activated on *[startDate]* is now below trigger threshold.\n\n', + 'no-trigger': 'There is *no trigger* currently.', + }, + }, + }, +]; diff --git a/tests/integration/helpers/API-service/dto/create-country.dto.ts b/tests/integration/helpers/API-service/dto/create-country.dto.ts new file mode 100644 index 000000000..eb3c78e65 --- /dev/null +++ b/tests/integration/helpers/API-service/dto/create-country.dto.ts @@ -0,0 +1,30 @@ +export class CountryDto { + countryCodeISO3: string; + countryName: string; + countryDisasterSettings: CountryDisasterSettingsDto[]; + adminRegionLabels: object; + countryLogos: object; + countryBoundingBox: BoundingBox; + disasterTypes: string[]; +} + +export class CountryDisasterSettingsDto { + disasterType: string; + adminLevels: AdminLevel[]; + defaultAdminLevel: AdminLevel; + activeLeadTimes: string[]; + droughtSeasonRegions?: object; + droughtEndOfMonthPipeline?: boolean; + showMonthlyEapActions?: boolean; + enableEarlyActions?: boolean; + enableStopTrigger?: boolean; + monthlyForecastInfo?: object; + eapLink: string; + eapAlertClasses?: object; + droughtRegions?: object; + isEventBased?: boolean; +} + +export class AddCountriesDto { + countries: CountryDto[]; +} diff --git a/tests/integration/helpers/API-service/dto/create-notification-info.dto.ts b/tests/integration/helpers/API-service/dto/create-notification-info.dto.ts new file mode 100644 index 000000000..cc517f53a --- /dev/null +++ b/tests/integration/helpers/API-service/dto/create-notification-info.dto.ts @@ -0,0 +1,12 @@ +export interface CreateNotificationInfoDto { + countryCodeISO3: string; + logo: object; + triggerStatement: object; + linkSocialMediaType: string; + linkSocialMediaUrl: string; + linkVideo: string; + linkPdf: string; + useWhatsapp?: object; + whatsappMessage?: object; + externalEarlyActionForm?: string; +} diff --git a/tests/integration/helpers/country.helper.ts b/tests/integration/helpers/country.helper.ts new file mode 100644 index 000000000..848945c1f --- /dev/null +++ b/tests/integration/helpers/country.helper.ts @@ -0,0 +1,34 @@ +import * as request from 'supertest'; + +import { CreateNotificationInfoDto } from './API-service/dto/create-notification-info.dto'; +import { getServer } from './utility.helper'; + +export function addOrUpdateNotificationInfo( + notificationInfoData: CreateNotificationInfoDto[], + accessToken: string, +): Promise { + return getServer() + .post(`/country/notification-info`) + .set('Authorization', `Bearer ${accessToken}`) + .send(notificationInfoData); +} + +export function getCountries( + countryCodeISO3SArray: string[], + accessToken: string, +): Promise { + return getServer() + .get(`/country`) + .set('Authorization', `Bearer ${accessToken}`) + .query({ countryCodesISO3: countryCodeISO3SArray.join(',') }); +} + +export function addOrUpdateCountries( + notificationInfoData: CreateNotificationInfoDto[], + accessToken: string, +): Promise { + return getServer() + .post(`/country/notification-info`) + .set('Authorization', `Bearer ${accessToken}`) + .send(notificationInfoData); +} diff --git a/tests/integration/tests/country/create-notification-info.test.ts b/tests/integration/tests/country/create-notification-info.test.ts new file mode 100644 index 000000000..313d244b2 --- /dev/null +++ b/tests/integration/tests/country/create-notification-info.test.ts @@ -0,0 +1,51 @@ +import { notificationInfoData } from '../../fixtures/notification-info.const'; +import { + addOrUpdateNotificationInfo, + getCountries, +} from '../../helpers/country.helper'; +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; + +describe('create or update notification info', () => { + let accessToken: string; + + beforeAll(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('should update existing data successfully', async () => { + // Arrange + const newNotificationInfoData = structuredClone(notificationInfoData); + const newLinkPdf = 'https://test-changed-link.com'; + newNotificationInfoData[0].linkPdf = newLinkPdf; + const countryCodeISO3 = 'MWI'; + + // Act + const postResult = await addOrUpdateNotificationInfo( + newNotificationInfoData, + accessToken, + ); + + const getResult = await getCountries([countryCodeISO3], accessToken); + + // Assert + expect(postResult.status).toBe(201); + expect(getResult.status).toBe(200); + expect(getResult.body[0].notificationInfo.linkPdf).toEqual(newLinkPdf); + }); + + it('should fail on unkown countryCodeISO3', async () => { + // Arrange + const newNotificationInfoData = structuredClone(notificationInfoData); + newNotificationInfoData[0].countryCodeISO3 = 'XXX'; + + // Act + const postResult = await addOrUpdateNotificationInfo( + newNotificationInfoData, + accessToken, + ); + + // Assert + expect(postResult.status).toBe(404); + }); +}); From abefaa66d21bed2f18aae600680945633a0de542 Mon Sep 17 00:00:00 2001 From: jannisvisser Date: Fri, 17 Jan 2025 14:27:06 +0100 Subject: [PATCH 4/6] test: add create country tests AB#32534 --- tests/integration/fixtures/country.const.ts | 101 ++++++++++++++++++ .../API-service/dto/create-country.dto.ts | 10 +- .../API-service/enum/admin-level.enum.ts | 6 ++ tests/integration/helpers/country.helper.ts | 7 +- .../tests/country/create-country.test.ts | 92 ++++++++++++++++ .../country/create-notification-info.test.ts | 51 --------- 6 files changed, 209 insertions(+), 58 deletions(-) create mode 100644 tests/integration/fixtures/country.const.ts create mode 100644 tests/integration/helpers/API-service/enum/admin-level.enum.ts create mode 100644 tests/integration/tests/country/create-country.test.ts delete mode 100644 tests/integration/tests/country/create-notification-info.test.ts diff --git a/tests/integration/fixtures/country.const.ts b/tests/integration/fixtures/country.const.ts new file mode 100644 index 000000000..95a0a6576 --- /dev/null +++ b/tests/integration/fixtures/country.const.ts @@ -0,0 +1,101 @@ +import { CountryDto } from '../helpers/API-service/dto/create-country.dto'; + +export const countryData: CountryDto[] = [ + { + countryCodeISO3: 'MWI', + countryName: 'Malawi', + disasterTypes: ['floods', 'flash-floods'], + countryDisasterSettings: [ + { + disasterType: 'floods', + adminLevels: [3], + defaultAdminLevel: 3, + activeLeadTimes: [ + '0-day', + '1-day', + '2-day', + '3-day', + '4-day', + '5-day', + '6-day', + '7-day', + ], + eapLink: + 'https://510ibfsystem.blob.core.windows.net/about-trigger/MWI-EAP-document.pdf', + eapAlertClasses: { + no: { + label: 'No action', + color: 'ibf-no-alert-primary', + value: 0, + }, + max: { + label: 'Trigger issued', + color: 'ibf-glofas-trigger', + value: 1, + }, + }, + isEventBased: true, + }, + { + disasterType: 'flash-floods', + adminLevels: [3], + defaultAdminLevel: 3, + activeLeadTimes: [ + '0-hour', + '1-hour', + '2-hour', + '3-hour', + '4-hour', + '5-hour', + '6-hour', + '7-hour', + '8-hour', + '9-hour', + '10-hour', + '11-hour', + '12-hour', + '15-hour', + '18-hour', + '21-hour', + '24-hour', + '48-hour', + ], + eapLink: + 'https://510ibfsystem.blob.core.windows.net/about-trigger/MWI-flashfloods-about.pdf', + enableEarlyActions: false, + enableStopTrigger: false, + isEventBased: true, + }, + ], + adminRegionLabels: { + '1': { + singular: 'Region', + plural: 'Regions', + }, + '2': { + singular: 'District', + plural: 'Districts', + }, + '3': { + singular: 'Traditional Authority', + plural: 'Traditional Authorities', + }, + }, + countryLogos: { + floods: ['MWI-mrcs.png', 'ZWE-DanishRedCross.png'], + 'flash-floods': ['MWI-government.jpeg'], + }, + countryBoundingBox: { + type: 'Polygon', + coordinates: [ + [ + [35.7719047381, -16.8012997372], + [35.7719047381, -9.23059905359], + [32.6881653175, -9.23059905359], + [32.6881653175, -16.8012997372], + [35.7719047381, -16.8012997372], + ], + ], + }, + }, +]; diff --git a/tests/integration/helpers/API-service/dto/create-country.dto.ts b/tests/integration/helpers/API-service/dto/create-country.dto.ts index eb3c78e65..6109069e9 100644 --- a/tests/integration/helpers/API-service/dto/create-country.dto.ts +++ b/tests/integration/helpers/API-service/dto/create-country.dto.ts @@ -1,14 +1,16 @@ -export class CountryDto { +import { AdminLevel } from '../enum/admin-level.enum'; + +export interface CountryDto { countryCodeISO3: string; countryName: string; countryDisasterSettings: CountryDisasterSettingsDto[]; adminRegionLabels: object; countryLogos: object; - countryBoundingBox: BoundingBox; + countryBoundingBox: any; //BoundingBox; disasterTypes: string[]; } -export class CountryDisasterSettingsDto { +export interface CountryDisasterSettingsDto { disasterType: string; adminLevels: AdminLevel[]; defaultAdminLevel: AdminLevel; @@ -25,6 +27,6 @@ export class CountryDisasterSettingsDto { isEventBased?: boolean; } -export class AddCountriesDto { +export interface AddCountriesDto { countries: CountryDto[]; } diff --git a/tests/integration/helpers/API-service/enum/admin-level.enum.ts b/tests/integration/helpers/API-service/enum/admin-level.enum.ts new file mode 100644 index 000000000..d84459476 --- /dev/null +++ b/tests/integration/helpers/API-service/enum/admin-level.enum.ts @@ -0,0 +1,6 @@ +export enum AdminLevel { + adminLevel1 = 1, + adminLevel2 = 2, + adminLevel3 = 3, + adminLevel4 = 4, +} diff --git a/tests/integration/helpers/country.helper.ts b/tests/integration/helpers/country.helper.ts index 848945c1f..158020b13 100644 --- a/tests/integration/helpers/country.helper.ts +++ b/tests/integration/helpers/country.helper.ts @@ -1,5 +1,6 @@ import * as request from 'supertest'; +import { AddCountriesDto } from './API-service/dto/create-country.dto'; import { CreateNotificationInfoDto } from './API-service/dto/create-notification-info.dto'; import { getServer } from './utility.helper'; @@ -24,11 +25,11 @@ export function getCountries( } export function addOrUpdateCountries( - notificationInfoData: CreateNotificationInfoDto[], + countryData: AddCountriesDto, accessToken: string, ): Promise { return getServer() - .post(`/country/notification-info`) + .post(`/country`) .set('Authorization', `Bearer ${accessToken}`) - .send(notificationInfoData); + .send(countryData); } diff --git a/tests/integration/tests/country/create-country.test.ts b/tests/integration/tests/country/create-country.test.ts new file mode 100644 index 000000000..820af3523 --- /dev/null +++ b/tests/integration/tests/country/create-country.test.ts @@ -0,0 +1,92 @@ +import { countryData } from '../../fixtures/country.const'; +import { notificationInfoData } from '../../fixtures/notification-info.const'; +import { + addOrUpdateCountries, + addOrUpdateNotificationInfo, + getCountries, +} from '../../helpers/country.helper'; +import { getAccessToken, resetDB } from '../../helpers/utility.helper'; + +describe('create or update country and notification info', () => { + let accessToken: string; + + beforeAll(async () => { + accessToken = await getAccessToken(); + await resetDB(accessToken); + }); + + it('should update existing country and notification-info successfully', async () => { + // Arrange + const countryCodeISO3 = 'MWI'; + const newLinkPdf = 'https://test-changed-link.com'; + const newCountryName = 'Malawi-different-name'; + + const newCountryData = structuredClone(countryData); + const newNotificationInfoData = structuredClone(notificationInfoData); + newNotificationInfoData[0].linkPdf = newLinkPdf; + newCountryData[0].countryName = newCountryName; + + // Act + const postCountryResult = await addOrUpdateCountries( + { countries: newCountryData }, + accessToken, + ); + const postNotificationInfoResult = await addOrUpdateNotificationInfo( + newNotificationInfoData, + accessToken, + ); + + const getResult = await getCountries([countryCodeISO3], accessToken); + + // Assert + expect(postCountryResult.status).toBe(201); + expect(postNotificationInfoResult.status).toBe(201); + expect(getResult.status).toBe(200); + expect(getResult.body[0].countryName).toEqual(newCountryName); + expect(getResult.body[0].notificationInfo.linkPdf).toEqual(newLinkPdf); + }); + + it('should add new country and notification-info successfully', async () => { + // Arrange + const newCountryCodeISO3 = 'MWI-new-country'; + const newCountryName = 'Malawi-new-country'; + + const newCountryData = structuredClone(countryData); + const newNotificationInfoData = structuredClone(notificationInfoData); + newCountryData[0].countryCodeISO3 = newCountryCodeISO3; + newCountryData[0].countryName = newCountryName; + newNotificationInfoData[0].countryCodeISO3 = newCountryCodeISO3; + + // Act + const postCountryResult = await addOrUpdateCountries( + { countries: newCountryData }, + accessToken, + ); + const postNotificationInfoResult = await addOrUpdateNotificationInfo( + newNotificationInfoData, + accessToken, + ); + const getResult = await getCountries([newCountryCodeISO3], accessToken); + + // Assert + expect(postCountryResult.status).toBe(201); + expect(postNotificationInfoResult.status).toBe(201); + expect(getResult.status).toBe(200); + expect(getResult.body[0].countryName).toEqual(newCountryName); + }); + + it('should fail to create notification-info on unkown countryCodeISO3', async () => { + // Arrange + const newNotificationInfoData = structuredClone(notificationInfoData); + newNotificationInfoData[0].countryCodeISO3 = 'XXX'; + + // Act + const postResult = await addOrUpdateNotificationInfo( + newNotificationInfoData, + accessToken, + ); + + // Assert + expect(postResult.status).toBe(404); + }); +}); diff --git a/tests/integration/tests/country/create-notification-info.test.ts b/tests/integration/tests/country/create-notification-info.test.ts deleted file mode 100644 index 313d244b2..000000000 --- a/tests/integration/tests/country/create-notification-info.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { notificationInfoData } from '../../fixtures/notification-info.const'; -import { - addOrUpdateNotificationInfo, - getCountries, -} from '../../helpers/country.helper'; -import { getAccessToken, resetDB } from '../../helpers/utility.helper'; - -describe('create or update notification info', () => { - let accessToken: string; - - beforeAll(async () => { - accessToken = await getAccessToken(); - await resetDB(accessToken); - }); - - it('should update existing data successfully', async () => { - // Arrange - const newNotificationInfoData = structuredClone(notificationInfoData); - const newLinkPdf = 'https://test-changed-link.com'; - newNotificationInfoData[0].linkPdf = newLinkPdf; - const countryCodeISO3 = 'MWI'; - - // Act - const postResult = await addOrUpdateNotificationInfo( - newNotificationInfoData, - accessToken, - ); - - const getResult = await getCountries([countryCodeISO3], accessToken); - - // Assert - expect(postResult.status).toBe(201); - expect(getResult.status).toBe(200); - expect(getResult.body[0].notificationInfo.linkPdf).toEqual(newLinkPdf); - }); - - it('should fail on unkown countryCodeISO3', async () => { - // Arrange - const newNotificationInfoData = structuredClone(notificationInfoData); - newNotificationInfoData[0].countryCodeISO3 = 'XXX'; - - // Act - const postResult = await addOrUpdateNotificationInfo( - newNotificationInfoData, - accessToken, - ); - - // Assert - expect(postResult.status).toBe(404); - }); -}); From 5fdf7a12437518358550dd8cf7424eca7155f9bc Mon Sep 17 00:00:00 2001 From: jannisvisser Date: Fri, 17 Jan 2025 14:33:17 +0100 Subject: [PATCH 5/6] chore: rm red crescent reference --- .../IBF-dashboard/src/app/services/map.service.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/interfaces/IBF-dashboard/src/app/services/map.service.ts b/interfaces/IBF-dashboard/src/app/services/map.service.ts index b1128495d..191d36100 100644 --- a/interfaces/IBF-dashboard/src/app/services/map.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/map.service.ts @@ -255,40 +255,35 @@ export class MapService { layer: IbfLayerMetadata, layerActive: boolean, ) => { - const layerName = - layer.name === IbfLayerName.redCrescentBranches - ? IbfLayerName.redCrossBranches - : layer.name; if (this.country) { if (layerActive) { this.apiService .getPointData( this.country.countryCodeISO3, - layerName, + layer.name, this.disasterType.disasterType, ) .subscribe((pointData) => { - this.addPointDataLayer(layer, layerName, pointData); + this.addPointDataLayer(layer, pointData); }); } else { - this.addPointDataLayer(layer, layerName, null); + this.addPointDataLayer(layer, null); } } }; private addPointDataLayer = ( layer: IbfLayerMetadata, - layerName: IbfLayerName, pointData: GeoJSON.FeatureCollection, ) => { this.addLayer({ - name: layerName, + name: layer.name, label: layer.label, type: IbfLayerType.point, group: IbfLayerGroup.point, description: this.getPopoverText(layer), active: this.adminLevelService.activeLayerNames.length - ? this.adminLevelService.activeLayerNames.includes(layerName) + ? this.adminLevelService.activeLayerNames.includes(layer.name) : this.getActiveState(layer), show: true, data: pointData, From ae4aba4c8c41068247086a2fe55175f0acd67b28 Mon Sep 17 00:00:00 2001 From: jannisvisser Date: Fri, 17 Jan 2025 14:36:29 +0100 Subject: [PATCH 6/6] chore: rm unused disputed-border + unused method --- .../src/app/components/map/map.component.ts | 36 ------------------- .../src/app/services/map.service.ts | 20 ++--------- 2 files changed, 2 insertions(+), 54 deletions(-) diff --git a/interfaces/IBF-dashboard/src/app/components/map/map.component.ts b/interfaces/IBF-dashboard/src/app/components/map/map.component.ts index 6c589bf75..7dbf8b319 100644 --- a/interfaces/IBF-dashboard/src/app/components/map/map.component.ts +++ b/interfaces/IBF-dashboard/src/app/components/map/map.component.ts @@ -741,42 +741,6 @@ export class MapComponent implements AfterViewInit, OnDestroy { ); } - private createDefaultPopupAdminRegions( - activeAggregateLayer: IbfLayer, - feature, - ): string { - feature = activeAggregateLayer.data?.features.find( - (f) => f.properties?.['placeCode'] === feature.properties.placeCode, - ); - return ( - '' + - feature.properties.name + - (feature.properties.placeCode.includes('Disputed') - ? ' (Disputed borders)' - : '') + - '' + - (feature.properties.nameParent - ? ` (${feature.properties.nameParent})` - : '') + - '
' + - (!activeAggregateLayer - ? '' - : activeAggregateLayer.label + - ': ' + - this.numberFormat( - typeof feature.properties[activeAggregateLayer.colorProperty] !== - 'undefined' - ? feature.properties[activeAggregateLayer.colorProperty] - : feature.properties.indicators[ - activeAggregateLayer.colorProperty - ], - activeAggregateLayer, - ) + - ' ' + - (activeAggregateLayer.unit || '')) - ); - } - private createAdminRegionsLayer(layer: IbfLayer): GeoJSON { if (!layer.data) { return; diff --git a/interfaces/IBF-dashboard/src/app/services/map.service.ts b/interfaces/IBF-dashboard/src/app/services/map.service.ts index 191d36100..d5094ce71 100644 --- a/interfaces/IBF-dashboard/src/app/services/map.service.ts +++ b/interfaces/IBF-dashboard/src/app/services/map.service.ts @@ -44,15 +44,6 @@ export class MapService { public layers = [] as IbfLayer[]; private triggeredAreaColor = 'var(--ion-color-ibf-outline-red)'; private nonTriggeredAreaColor = 'var(--ion-color-ibf-no-alert-primary)'; - private disputedBorderStyle: { - weight: number; - dashArray: string; - color: string; - } = { - weight: 2, - dashArray: '5 5', - color: null, - }; private layerDataCache: Record = {}; public state = { @@ -934,20 +925,13 @@ export class MapService { colorThreshold, ); const fillOpacity = this.getAdminRegionFillOpacity(layer); - let weight = this.getAdminRegionWeight(layer, placeCode); - let color = this.getAdminRegionColor(layer); - let dashArray: string; - if (placeCode?.includes('Disputed')) { - dashArray = this.disputedBorderStyle.dashArray; - weight = this.disputedBorderStyle.weight; - color = this.disputedBorderStyle.color; - } + const weight = this.getAdminRegionWeight(layer, placeCode); + const color = this.getAdminRegionColor(layer); return { fillColor, fillOpacity, weight, color, - dashArray, }; } };