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 6c589bf75a..7dbf8b319e 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 7eb62b37ae..d5094ce71f 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 = { @@ -255,40 +246,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, @@ -632,14 +618,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)); @@ -943,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, }; } }; diff --git a/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts b/interfaces/IBF-dashboard/src/app/types/ibf-layer.ts index 4057605725..216a3963bb 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/module-dependencies.md b/services/API-service/module-dependencies.md new file mode 100644 index 0000000000..91899ed4a2 --- /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 50e9f798e9..cea71113cf 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 2d23b939a4..8ae5b15142 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/api/country/country.service.ts b/services/API-service/src/api/country/country.service.ts index 71a7b81628..44594ef2cf 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/api/metadata/dto/add-indicators.dto.ts b/services/API-service/src/api/metadata/dto/add-indicators.dto.ts index 6bedca8436..3cb6865d7d 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 568dc799ec..eba9aaffe7 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', }, }, }) diff --git a/services/API-service/src/main.ts b/services/API-service/src/main.ts index 5e6bbc7b91..c53ccbc4b7 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(); diff --git a/services/API-service/src/scripts/json/notification-info.json b/services/API-service/src/scripts/json/notification-info.json index fef4f37088..fcd40e14e0 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/country.const.ts b/tests/integration/fixtures/country.const.ts new file mode 100644 index 0000000000..95a0a65768 --- /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/fixtures/notification-info.const.ts b/tests/integration/fixtures/notification-info.const.ts new file mode 100644 index 0000000000..d9fdbac536 --- /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 0000000000..6109069e95 --- /dev/null +++ b/tests/integration/helpers/API-service/dto/create-country.dto.ts @@ -0,0 +1,32 @@ +import { AdminLevel } from '../enum/admin-level.enum'; + +export interface CountryDto { + countryCodeISO3: string; + countryName: string; + countryDisasterSettings: CountryDisasterSettingsDto[]; + adminRegionLabels: object; + countryLogos: object; + countryBoundingBox: any; //BoundingBox; + disasterTypes: string[]; +} + +export interface 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 interface 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 0000000000..cc517f53a0 --- /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/API-service/enum/admin-level.enum.ts b/tests/integration/helpers/API-service/enum/admin-level.enum.ts new file mode 100644 index 0000000000..d844594762 --- /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 new file mode 100644 index 0000000000..158020b131 --- /dev/null +++ b/tests/integration/helpers/country.helper.ts @@ -0,0 +1,35 @@ +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'; + +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( + countryData: AddCountriesDto, + accessToken: string, +): Promise { + return getServer() + .post(`/country`) + .set('Authorization', `Bearer ${accessToken}`) + .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 0000000000..820af35237 --- /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); + }); +});