From 9c228bcf58e81fb53547d870fd52d0b10903853c Mon Sep 17 00:00:00 2001 From: K Gajowy Date: Mon, 5 Jul 2021 11:12:41 +0200 Subject: [PATCH 1/2] feat(marxan-run): expose endpoints for starting/cancelling marxan execution --- .../modules/scenarios/scenarios.controller.ts | 43 +++++++++++++++++++ .../modules/scenarios/scenarios.service.ts | 20 ++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/api/apps/api/src/modules/scenarios/scenarios.controller.ts b/api/apps/api/src/modules/scenarios/scenarios.controller.ts index 3ba61f08bb..f7428fd440 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.controller.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.controller.ts @@ -36,6 +36,7 @@ import { ApiUnauthorizedResponse, ApiForbiddenResponse, ApiParam, + ApiAcceptedResponse, } from '@nestjs/swagger'; import { apiGlobalPrefixes } from '@marxan-api/api.config'; import { JwtAuthGuard } from '@marxan-api/guards/jwt-auth.guard'; @@ -64,6 +65,9 @@ import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; const basePath = `${apiGlobalPrefixes.v1}/scenarios`; const solutionsSubPath = `:id/marxan/run/:runId/solutions`; +const marxanRunTag = 'Marxan Run'; +const marxanFilesTag = 'Marxan Run - Files'; + @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiTags(scenarioResource.className) @@ -245,6 +249,7 @@ export class ScenariosController { ); } + @ApiTags(marxanFilesTag) @ApiOperation({ description: `Resolve scenario's input parameter file.` }) @Get(':id/marxan/dat/input.dat') @ApiProduces('text/plain') @@ -255,6 +260,7 @@ export class ScenariosController { return await this.service.getInputParameterFile(id); } + @ApiTags(marxanFilesTag) @ApiOperation({ description: `Resolve scenario's spec file.` }) @Get(':id/marxan/dat/spec.dat') @ApiProduces('text/plain') @@ -310,6 +316,41 @@ export class ScenariosController { ); } + @ApiOperation({ + description: `Request start of the Marxan execution.`, + summary: `Request start of the Marxan execution.`, + }) + @ApiTags(marxanRunTag) + @ApiQuery({ + name: `blm`, + required: false, + type: Number, + }) + @ApiAcceptedResponse({ + description: `No content.`, + }) + @Post(`:id/marxan`) + async executeMarxanRun( + @Param(`id`, ParseUUIDPipe) id: string, + @Query(`blm`) blm?: number, + ) { + await this.service.run(id, blm); + } + + @ApiOperation({ + description: `Cancel running Marxan execution.`, + summary: `Cancel running Marxan execution.`, + }) + @ApiTags(marxanRunTag) + @ApiAcceptedResponse({ + description: `No content.`, + }) + @Delete(`:id/marxan`) + async cancelMaraxnRun(@Param(`id`, ParseUUIDPipe) id: string) { + await this.service.cancel(id); + } + + @ApiTags(marxanRunTag) @ApiOkResponse({ type: ScenarioSolutionResultDto, }) @@ -324,6 +365,7 @@ export class ScenariosController { ); } + @ApiTags(marxanRunTag) @ApiOkResponse({ type: ScenarioSolutionResultDto, }) @@ -345,6 +387,7 @@ export class ScenariosController { ); } + @ApiTags(marxanFilesTag) @Header('Content-Type', 'text/csv') @ApiOkResponse({ schema: { diff --git a/api/apps/api/src/modules/scenarios/scenarios.service.ts b/api/apps/api/src/modules/scenarios/scenarios.service.ts index f9823462fb..74a4cb8e97 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.service.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.service.ts @@ -1,4 +1,9 @@ -import { BadRequestException, HttpService, Injectable } from '@nestjs/common'; +import { + BadRequestException, + HttpService, + Injectable, + NotImplementedException, +} from '@nestjs/common'; import { FetchSpecification } from 'nestjs-base-service'; import { classToClass } from 'class-transformer'; import * as stream from 'stream'; @@ -150,6 +155,19 @@ export class ScenariosService { return this.specDatService.getSpecDatContent(scenarioId); } + async run(scenarioId: string, _blm?: number): Promise { + await this.assertScenario(scenarioId); + // TODO ensure not running yet + // TODO submit + throw new NotImplementedException(); + } + + async cancel(scenarioId: string): Promise { + await this.assertScenario(scenarioId); + // TODO ensure it is running + throw new NotImplementedException(); + } + private async assertScenario(scenarioId: string) { await this.crudService.getById(scenarioId); } From 6a1e6a6ef5cef0eea4865687069a9fb170cab044 Mon Sep 17 00:00:00 2001 From: K Gajowy Date: Mon, 5 Jul 2021 14:05:40 +0200 Subject: [PATCH 2/2] spec(marxan-run): baseground for e2e spec of marxan run --- .../marxan-run/execute-marxan-run.e2e-spec.ts | 28 ++++++++ .../business-critical/marxan-run/fixtures.ts | 66 +++++++++++++++++++ api/apps/api/test/steps/given-project.ts | 7 +- .../api/test/steps/given-scenario-exists.ts | 2 + 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 api/apps/api/test/business-critical/marxan-run/execute-marxan-run.e2e-spec.ts create mode 100644 api/apps/api/test/business-critical/marxan-run/fixtures.ts diff --git a/api/apps/api/test/business-critical/marxan-run/execute-marxan-run.e2e-spec.ts b/api/apps/api/test/business-critical/marxan-run/execute-marxan-run.e2e-spec.ts new file mode 100644 index 0000000000..94bded0fa8 --- /dev/null +++ b/api/apps/api/test/business-critical/marxan-run/execute-marxan-run.e2e-spec.ts @@ -0,0 +1,28 @@ +import { PromiseType } from 'utility-types'; +import { getFixtures } from './fixtures'; + +let fixtures: PromiseType>; + +beforeAll(async () => { + fixtures = await getFixtures(); +}); + +afterAll(async () => { + await fixtures.cleanup(); +}); + +describe(`Marxan run`, () => { + beforeAll(async () => { + await fixtures.GivenUserIsLoggedIn(); + await fixtures.GivenProjectOrganizationExists(); + await fixtures.GivenScenarioExists(`Mouse`); + await fixtures.GivenCostSurfaceTemplateFilled(); + await fixtures.WhenMarxanExecutionIsRequested(); + await fixtures.WhenMarxanExecutionIsCompleted(); + await fixtures.ThenResultsAreAvailable(); + }); + + it(`should work in near future`, () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/api/apps/api/test/business-critical/marxan-run/fixtures.ts b/api/apps/api/test/business-critical/marxan-run/fixtures.ts new file mode 100644 index 0000000000..f1ab8ef387 --- /dev/null +++ b/api/apps/api/test/business-critical/marxan-run/fixtures.ts @@ -0,0 +1,66 @@ +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { bootstrapApplication } from '../../utils/api-application'; +import { GivenUserIsLoggedIn } from '../../steps/given-user-is-logged-in'; +import { GivenProjectExists } from '../../steps/given-project'; +import { GivenScenarioExists } from '../../steps/given-scenario-exists'; +import { ScenarioType } from '@marxan-api/modules/scenarios/scenario.api.entity'; +import { ScenariosTestUtils } from '../../utils/scenarios.test.utils'; + +export const getFixtures = async () => { + const app: INestApplication = await bootstrapApplication(); + const cleanups: (() => Promise)[] = []; + + let authToken: string; + let project: string; + let scenario: string; + + return { + GivenUserIsLoggedIn: async () => { + authToken = await GivenUserIsLoggedIn(app); + }, + GivenProjectOrganizationExists: async () => { + const organizationProject = await GivenProjectExists(app, authToken, { + countryCode: 'AGO', + adminAreaLevel1Id: 'AGO.15_1', + adminAreaLevel2Id: 'AGO.15.4_1', + }); + + project = organizationProject.projectId; + cleanups.push(organizationProject.cleanup); + }, + GivenScenarioExists: async (name: string) => { + scenario = ( + await GivenScenarioExists(app, project, authToken, { + name, + type: ScenarioType.marxan, + }) + ).id; + cleanups.push(() => + ScenariosTestUtils.deleteScenario(app, authToken, scenario), + ); + }, + GivenCostSurfaceTemplateFilled: async () => { + const template = await request(app.getHttpServer()) + .get(`/api/v1/scenarios/${scenario}/cost-surface/shapefile-template`) + .set('Authorization', `Bearer ${authToken}`); + console.log(template.body); + }, + WhenMarxanExecutionIsRequested: async () => { + // TODO currently not implemented yet: 501 + await request(app.getHttpServer()) + .post(`/api/v1/scenarios/${scenario}/marxan`) + .set('Authorization', `Bearer ${authToken}`); + }, + WhenMarxanExecutionIsCompleted: async () => { + return void 0; + }, + ThenResultsAreAvailable: async () => { + return void 0; + }, + cleanup: async () => { + await Promise.all(cleanups.map((c) => c())); + await app.close(); + }, + }; +}; diff --git a/api/apps/api/test/steps/given-project.ts b/api/apps/api/test/steps/given-project.ts index 9801f44a4d..0e9f60f348 100644 --- a/api/apps/api/test/steps/given-project.ts +++ b/api/apps/api/test/steps/given-project.ts @@ -6,6 +6,11 @@ import { E2E_CONFIG } from '../e2e.config'; export const GivenProjectExists = async ( app: INestApplication, jwt: string, + adminArea?: { + countryCode?: string; + adminAreaLevel1Id?: string; + adminAreaLevel2Id?: string; + }, ): Promise<{ projectId: string; organizationId: string; @@ -20,7 +25,7 @@ export const GivenProjectExists = async ( ).data.id; const projectId = ( await ProjectsTestUtils.createProject(app, jwt, { - ...E2E_CONFIG.projects.valid.minimal(), + ...E2E_CONFIG.projects.valid.minimalInGivenAdminArea(adminArea), organizationId, }) ).data.id; diff --git a/api/apps/api/test/steps/given-scenario-exists.ts b/api/apps/api/test/steps/given-scenario-exists.ts index 7ee27c3b4a..644511d6b9 100644 --- a/api/apps/api/test/steps/given-scenario-exists.ts +++ b/api/apps/api/test/steps/given-scenario-exists.ts @@ -7,9 +7,11 @@ export const GivenScenarioExists = async ( app: INestApplication, projectId: string, jwtToken: string, + options?: Partial, ) => { const createScenarioDTO: Partial = { ...E2E_CONFIG.scenarios.valid.minimal(), + ...options, projectId, }; return (