Skip to content

Commit

Permalink
add tests: check that double-spending of user-uploaded planning areas…
Browse files Browse the repository at this point in the history
… is not allowed
  • Loading branch information
hotzevzl committed Nov 16, 2023
1 parent 8755133 commit 4f30507
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 3 deletions.
15 changes: 15 additions & 0 deletions api/apps/api/test/projects/crud/projects.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import { TestClientApi } from '../../utils/test-client/test-client-api';
import { Repository } from 'typeorm';
import { CostSurface } from '@marxan-api/modules/cost-surface/cost-surface.api.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
import { FixtureType } from '@marxan/utils/tests/fixture-type';
import { getFixtures } from '../projects.fixtures';

let fixtures: FixtureType<typeof getFixtures>;

describe('ProjectsModule (e2e)', () => {
let api: TestClientApi;

beforeEach(async () => {
api = await TestClientApi.initialize();
fixtures = await getFixtures();
});

describe('when creating a project', () => {
Expand Down Expand Up @@ -123,7 +128,17 @@ describe('ProjectsModule (e2e)', () => {
'When a regular planning grid is requested (hexagon or square) either a custom planning area or a GADM area gid must be provided',
);
});

it('should fail when creating a second project using the same custom planning area as another project', async () => {
await fixtures.GivenCustomPlanningAreaWasCreated();
await fixtures.GivenPrivateProjectWithCustomPlanningAreaWasCreated();
const result = await fixtures.WhenCreatingAnotherProjectWithSameCustomPlanningArea();
fixtures.ThenCreationOfProjectWithAlreadySpentCustomPlanningAreaShouldFail(
result,
);
});
});

describe('when listing projects', () => {
it('should be able to get a list of the projects the user have a role in', async () => {
const userToken = await api.utils.createWorkingUser();
Expand Down
174 changes: 171 additions & 3 deletions api/apps/api/test/projects/projects.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { CloningFilesRepository } from '@marxan/cloning-files-repository';
import { ComponentId, ComponentLocation } from '@marxan/cloning/domain';
import { HttpStatus } from '@nestjs/common';
import { CommandBus, CqrsModule } from '@nestjs/cqrs';
import { getRepositoryToken } from '@nestjs/typeorm';
import { TypeOrmModule, getEntityManagerToken, getRepositoryToken } from '@nestjs/typeorm';
import { isLeft } from 'fp-ts/lib/Either';
import { Readable } from 'stream';
import * as request from 'supertest';
import { Repository } from 'typeorm';
import { validate, version } from 'uuid';
import { EntityManager, Repository } from 'typeorm';
import { v4, validate, version } from 'uuid';
import { ApiEventsService } from '../../src/modules/api-events';
import { ApiEventByTopicAndKind } from '../../src/modules/api-events/api-event.topic+kind.api.entity';
import { CompleteExportPiece } from '../../src/modules/clone/export/application/complete-export-piece.command';
Expand All @@ -25,6 +25,17 @@ import { bootstrapApplication } from '../utils/api-application';
import { EventBusTestUtils } from '../utils/event-bus.test.utils';
import { ScenariosTestUtils } from '../utils/scenarios.test.utils';
import { ScenarioType } from '@marxan-api/modules/scenarios/scenario.api.entity';
import { MultiPolygon } from 'geojson';
import { PlanningUnitGridShape } from '@marxan/scenarios-planning-unit';
import { ProjectsTestUtils } from '../utils/projects.test.utils';
import { apiConnections } from '@marxan-api/ormconfig';

type ApiErrorResponse = {
errors: {
status: number;
title: string;
}[];
};

export const getFixtures = async () => {
const app = await bootstrapApplication([CqrsModule], [EventBusTestUtils]);
Expand All @@ -35,6 +46,13 @@ export const getFixtures = async () => {
const publishedProjectsRepo: Repository<PublishedProject> = app.get(
getRepositoryToken(PublishedProject),
);
const apiEntityManager: EntityManager = app.get(
getEntityManagerToken(apiConnections.default),
);
const geoEntityManager: EntityManager = app.get(
getEntityManagerToken(apiConnections.geoprocessingDB),
);

const cleanups: (() => Promise<void>)[] = [];

const apiEvents = app.get(ApiEventsService);
Expand All @@ -43,6 +61,41 @@ export const getFixtures = async () => {
const commandBus = app.get(CommandBus);
const eventBusTestUtils = app.get(EventBusTestUtils);

const projectId = v4();
const organizationId = v4();
const planningAreaId = v4();

const expectedGeom: MultiPolygon = {
type: 'MultiPolygon',
coordinates: [
[
[
[102.0, 2.0],
[103.0, 2.0],
[103.0, 3.0],
[102.0, 3.0],
[102.0, 2.0],
],
],
[
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0],
],
[
[100.2, 0.2],
[100.8, 0.2],
[100.8, 0.8],
[100.2, 0.8],
[100.2, 0.2],
],
],
],
};

eventBusTestUtils.startInspectingEvents();

return {
Expand All @@ -51,6 +104,46 @@ export const getFixtures = async () => {
await Promise.all(cleanups.map((clean) => clean()));
await app.close();
},
GivenPrivateProjectWithCustomPlanningAreaWasCreated: async () => {
await GivenProjectExistsV3(apiEntityManager, projectId, organizationId);
},
GivenCustomPlanningAreaWasCreated: async (): Promise<void> => {
await geoEntityManager.query(
`
insert into planning_areas (id, project_id, the_geom)
values
($1, $2, ST_GeomFromGeoJSON($3));
`,
[planningAreaId, projectId, expectedGeom],
);
},
WhenCreatingAnotherProjectWithSameCustomPlanningArea:
async (): Promise<ApiErrorResponse> => {
// Here we create the project via an API request, so that the full project
// creation lifecycle is triggered, including an attempt to link the
// project to an existing planning area.
const result = (await ProjectsTestUtils.createProject(
app,
randomUserToken,
{
name: 'Test',
organizationId,
metadata: {},
planningAreaId,
planningUnitGridShape: PlanningUnitGridShape.FromShapefile,
planningUnitAreakm2: 10000,
},
)) as unknown as ApiErrorResponse;
return result;
},
ThenCreationOfProjectWithAlreadySpentCustomPlanningAreaShouldFail: (
projectCreationResult: ApiErrorResponse,
) => {
expect(projectCreationResult.errors[0].status).toEqual(500);
expect(projectCreationResult.errors[0].title).toMatch(
/Planning area [a-f0-9-]{36} is already linked to a project: no new project can be created using it as its own planning area./,
);
},

WhenComparisonMapIsRequested: async (
scenarioIdA: string,
Expand Down Expand Up @@ -547,3 +640,78 @@ export const getFixtures = async () => {
},
};
};

/**
* @debt Copy of same function from api/apps/geoprocessing/test/integration/cloning/fixtures.ts
*/
export function GivenOrganizationExists(
em: EntityManager,
organizationId: string,
) {
return em
.createQueryBuilder()
.insert()
.into(`organizations`)
.values({
id: organizationId,
name: `test organization - ${organizationId}`,
})
.execute();
}

/**
* @debt Copy of same function from api/apps/geoprocessing/test/integration/cloning/fixtures.ts
*/
export async function GivenProjectExistsV3(
em: EntityManager,
projectId: string,
organizationId: string,
projectData: Record<string, any> = {},
costSurfaceId = v4(),
) {
await GivenOrganizationExists(em, organizationId);

const insertResult = await em
.createQueryBuilder()
.insert()
.into(`projects`)
.values({
id: projectId,
name: `test project - ${projectId}`,
organizationId,
planningUnitGridShape: PlanningUnitGridShape.Square,
...projectData,
})
.execute();

await GivenDefaultCostSurfaceForProject(em, projectId, costSurfaceId);

return insertResult;
}

/**
* @debt Copy of same function from api/apps/geoprocessing/test/integration/cloning/fixtures.ts
*/
async function GivenDefaultCostSurfaceForProject(
em: EntityManager,
projectId: string,
id: string,
name?: string,
) {
const nameForCostSurface = name || projectId;
return em
.createQueryBuilder()
.insert()
.into(`cost_surfaces`)
.values({
id,
name: `${
nameForCostSurface ? nameForCostSurface + ' - ' : ''
}Default Cost Surface`,
projectId: projectId,
min: 0,
max: 0,
isDefault: true,
})
.execute();
}

0 comments on commit 4f30507

Please sign in to comment.