Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(CostSurface): Adds GET endpoint to retrieve one and all Cost Surfaces for a Project [MRXN23-108] [MRXN23-109] #1531

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,11 @@ export class CostSurfaceResult {
@ApiProperty()
data!: JSONAPICostSurface;
}

export class CostSurfaceResultPlural {
@ApiProperty({
isArray: true,
type: JSONAPICostSurface,
})
data!: JSONAPICostSurface[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ export class CostSurfaceSerializer {
'isDefault',
'createdAt',
'lastModifiedAt',
'scenarios',
'scenarioUsageCount',
],
keyForAttribute: 'camelCase',
scenarios: {
ref: 'id',
attributes: ['id', 'name'],
},
project: {
ref: 'id',
attributes: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@ import {
import { ensureShapefileHasRequiredFiles } from '@marxan-api/utils/file-uploads.utils';
import { UpdateCostSurfaceDto } from '@marxan-api/modules/cost-surface/dto/update-cost-surface.dto';
import { CostSurfaceSerializer } from '@marxan-api/modules/cost-surface/dto/cost-surface.serializer';
import { JSONAPICostSurface } from '@marxan-api/modules/cost-surface/cost-surface.api.entity';
import {
costSurfaceResource,
CostSurfaceResult,
CostSurfaceResultPlural,
JSONAPICostSurface,
} from '@marxan-api/modules/cost-surface/cost-surface.api.entity';

//@Todo refactor all endpoints to use cost-surfaces instead of singular cost-surface
@ApiTags(projectResource.className)
@Controller(`${apiGlobalPrefixes.v1}/projects`)
export class ProjectCostSurfaceController {
Expand All @@ -58,6 +64,56 @@ export class ProjectCostSurfaceController {
public readonly costSurfaceSeralizer: CostSurfaceSerializer,
) {}

@ImplementsAcl()
@UseGuards(JwtAuthGuard)
@Get(`:projectId/cost-surface/:costSurfaceId`)
@ApiOkResponse({ type: CostSurfaceResult })
async getCostSurfaces(
@Param('projectId') projectId: string,
@Param('costSurfaceId') costSurfaceId: string,
@Req() req: RequestWithAuthenticatedUser,
): Promise<void> {
const result = await this.costSurfaceService.getCostSurface(
req.user.id,
projectId,
costSurfaceId,
);

if (isLeft(result)) {
throw mapAclDomainToHttpError(result.left, {
projectId,
userId: req.user.id,
resourceType: costSurfaceResource.name.plural,
});
}

return this.costSurfaceSeralizer.serialize(result.right);
}

@ImplementsAcl()
@UseGuards(JwtAuthGuard)
@Get(`:projectId/cost-surface/`)
@ApiOkResponse({ type: CostSurfaceResultPlural })
async getCostSurface(
@Param('projectId') projectId: string,
@Req() req: RequestWithAuthenticatedUser,
): Promise<void> {
const result = await this.costSurfaceService.getCostSurfaces(
req.user.id,
projectId,
);

if (isLeft(result)) {
throw mapAclDomainToHttpError(result.left, {
projectId,
userId: req.user.id,
resourceType: costSurfaceResource.name.plural,
});
}

return this.costSurfaceSeralizer.serialize(result.right);
}

@ImplementsAcl()
@UseGuards(JwtAuthGuard)
@ApiOperation({ description: 'To be implemented' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,56 @@ import { getProjectCostSurfaceFixtures } from './project-cost-surface.fixtures';

let fixtures: FixtureType<typeof getProjectCostSurfaceFixtures>;

describe('Get Project Cost Surface', () => {
beforeEach(async () => {
fixtures = await getProjectCostSurfaceFixtures();
});

it(`should not return the CostSurface if the user doesn't have permissions to view the project`, async () => {
// ARRANGE
const projectId = await fixtures.GivenProject('someProject', []);
const costSurface = await fixtures.GivenCostSurfaceMetadataForProject(
projectId,
'costSurface',
);
await fixtures.GivenScenario(projectId, costSurface.id);
await fixtures.GivenScenario(projectId, costSurface.id);
// ACT
const response = await fixtures.WhenGettingCostSurfaceForProject(
projectId,
costSurface.id,
);

// ASSERT
await fixtures.ThenProjectNotViewableErrorWasReturned(response);
});

it(`should not return list of CostSurface if the user doesn't have permissions to view the project`, async () => {
// ARRANGE
const projectId = await fixtures.GivenProject('someProject', []);
const costSurface1 = await fixtures.GivenCostSurfaceMetadataForProject(
projectId,
'costSurface 1',
);
const costSurface2 = await fixtures.GivenCostSurfaceMetadataForProject(
projectId,
'costSurface 2',
);
await fixtures.GivenScenario(projectId, costSurface1.id);
await fixtures.GivenScenario(projectId, costSurface1.id);
await fixtures.GivenScenario(projectId, costSurface2.id);
await fixtures.GivenScenario(projectId, costSurface2.id);
await fixtures.GivenScenario(projectId, costSurface2.id);
// ACT
const response = await fixtures.WhenGettingCostSurfacesForProject(
projectId,
);

// ASSERT
await fixtures.ThenProjectNotViewableErrorWasReturned(response);
});
});

describe('Upload Cost Surface Shapefile', () => {
beforeEach(async () => {
fixtures = await getProjectCostSurfaceFixtures();
Expand Down
139 changes: 139 additions & 0 deletions api/apps/api/test/project/project-cost-surface.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,145 @@ describe('Cost Surface', () => {
await fixtures.ThenADefaultCostSurfaceWasCreated(projectId);
});
});
describe('Getting Cost Surfaces for Project', () => {
it(`should return the costSurface for the given id, along with its scenarioUsageCount`, async () => {
// ARRANGE
const projectId1 = await fixtures.GivenProject('someProject 1');
const projectId2 = await fixtures.GivenProject('the REAL project');
const default2 = await fixtures.GivenDefaultCostSurfaceForProject(
projectId1,
);
const costSurface11 = await fixtures.GivenCostSurfaceMetadataForProject(
projectId1,
'costSurface 1 1',
);
const costSurface21 = await fixtures.GivenCostSurfaceMetadataForProject(
projectId2,
'costSurface 2 1',
);
const costSurface22 = await fixtures.GivenCostSurfaceMetadataForProject(
projectId2,
'costSurface 2 2',
);
await fixtures.GivenScenario(projectId1, costSurface11.id);
await fixtures.GivenScenario(projectId1, costSurface11.id);
await fixtures.GivenScenario(projectId2, costSurface21.id);
const scenario22 = await fixtures.GivenScenario(
projectId2,
costSurface22.id,
);
const scenario23 = await fixtures.GivenScenario(
projectId2,
costSurface22.id,
);
const scenario24 = await fixtures.GivenScenario(
projectId2,
costSurface22.id,
);

// ACT
const response = await fixtures.WhenGettingCostSurfaceForProject(
projectId2,
costSurface22.id,
);

// ASSERT
await fixtures.ThenResponseHasCostSurface(response, {
...costSurface22,
scenarioUsageCount: 3,
scenarios: [scenario22, scenario23, scenario24],
});
});

it(`should return all the Project's CostSurfaces along their corresponding scenarioUsageCount`, async () => {
// ARRANGE
const projectId1 = await fixtures.GivenProject('someProject 1');
const projectId2 = await fixtures.GivenProject('the REAL project');
const default1 = await fixtures.GivenDefaultCostSurfaceForProject(
projectId1,
);
const default2 = await fixtures.GivenDefaultCostSurfaceForProject(
projectId2,
);
const costSurface11 = await fixtures.GivenCostSurfaceMetadataForProject(
projectId1,
'costSurface 1 1',
);
const costSurface21 = await fixtures.GivenCostSurfaceMetadataForProject(
projectId2,
'costSurface 2 1',
);
const costSurface22 = await fixtures.GivenCostSurfaceMetadataForProject(
projectId2,
'costSurface 2 2',
);
const scenario11 = await fixtures.GivenScenario(
projectId1,
costSurface11.id,
);
const scenario12 = await fixtures.GivenScenario(
projectId1,
costSurface11.id,
);
const scenario21 = await fixtures.GivenScenario(
projectId2,
costSurface21.id,
);
const scenario22 = await fixtures.GivenScenario(
projectId2,
costSurface22.id,
);
const scenario23 = await fixtures.GivenScenario(
projectId2,
costSurface22.id,
);
const scenario24 = await fixtures.GivenScenario(
projectId2,
costSurface22.id,
);
const scenario25 = await fixtures.GivenScenario(
projectId2,
costSurface22.id,
);

const expectedResponse1 = [
{ ...default1, scenarioUsageCount: 0, scenarios: [] },
{
...costSurface11,
scenarioUsageCount: 2,
scenarios: [scenario11, scenario12],
},
];

const expectedResponse2 = [
{ ...default2, scenarioUsageCount: 0, scenarios: [] },
{ ...costSurface21, scenarioUsageCount: 1, scenarios: [scenario21] },
{
...costSurface22,
scenarioUsageCount: 4,
scenarios: [scenario22, scenario23, scenario24, scenario25],
},
];

// ACT
const response1 = await fixtures.WhenGettingCostSurfacesForProject(
projectId1,
);
const response2 = await fixtures.WhenGettingCostSurfacesForProject(
projectId2,
);

// ASSERT
await fixtures.ThenReponseHasCostSurfaceList(
response1,
expectedResponse1,
);
await fixtures.ThenReponseHasCostSurfaceList(
response2,
expectedResponse2,
);
});
});
describe('Upload Cost Surface Shapefile', () => {
it(`should create CostSurface API entity with the provided name`, async () => {
// ARRANGE
Expand Down
61 changes: 61 additions & 0 deletions api/apps/api/test/project/project-cost-surface.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ export const getProjectCostSurfaceFixtures = async () => {
return __dirname + `/../upload-feature/import-files/wetlands.zip`;
},

WhenGettingCostSurfacesForProject: async (projectId: string) => {
return request(app.getHttpServer())
.get(`/api/v1/projects/${projectId}/cost-surface`)
.set('Authorization', `Bearer ${token}`);
},
WhenGettingCostSurfaceForProject: async (projectId: string, id: string) => {
return request(app.getHttpServer())
.get(`/api/v1/projects/${projectId}/cost-surface/${id}`)
.set('Authorization', `Bearer ${token}`);
},
WhenUploadingCostSurfaceShapefileForProject: async (
projectId: string,
costSurfaceName: string,
Expand Down Expand Up @@ -204,6 +214,50 @@ export const getProjectCostSurfaceFixtures = async () => {
expect(savedCostSurface?.name).toEqual(name);
},

ThenResponseHasCostSurface: async (
response: request.Response,
costSurface: CostSurface,
) => {
const jsonAPICostSurfaces = response.body.data;

expect(costSurface.id).toEqual(jsonAPICostSurfaces.id);
expect(costSurface.name).toEqual(jsonAPICostSurfaces.attributes.name);
expect(costSurface.scenarioUsageCount).toEqual(
jsonAPICostSurfaces.attributes.scenarioUsageCount,
);
expect(jsonAPICostSurfaces.attributes.scenarioUsageCount).toEqual(
jsonAPICostSurfaces.relationships.scenarios.data.length,
);
expect(costSurface.scenarios.length).toEqual(
jsonAPICostSurfaces.relationships.scenarios.data.length,
);
},
ThenReponseHasCostSurfaceList: async (
response: request.Response,
costSurfaces: CostSurface[],
) => {
const jsonAPICostSurfaces = response.body.data;
const expected = costSurfaces.sort((a, b) => b.id.localeCompare(a.id));
const received = jsonAPICostSurfaces.sort((a: any, b: any) =>
b.id.localeCompare(a.id),
);

expect(expected.length).toEqual(received.length);
for (let i = 0; i < expected.length; i++) {
expect(expected[i].id).toEqual(received[i].id);
expect(expected[i].name).toEqual(received[i].attributes.name);
expect(expected[i].scenarioUsageCount).toEqual(
received[i].attributes.scenarioUsageCount,
);
expect(received[i].attributes.scenarioUsageCount).toEqual(
received[i].relationships.scenarios.data.length,
);
expect(expected[i].scenarios.length).toEqual(
received[i].relationships.scenarios.data.length,
);
}
},

ThenCostSurfaceAPIEntityWasProperlyUpdated: async (
response: request.Response,
name: string,
Expand All @@ -226,6 +280,13 @@ export const getProjectCostSurfaceFixtures = async () => {
expect(savedCostSurface.max).toEqual(costSurface.max);
},

ThenProjectNotViewableErrorWasReturned: (response: request.Response) => {
const error: any = response.body.errors[0].title;
expect(response.status).toBe(HttpStatus.FORBIDDEN);
expect(error).toContain(
`User with ID: ${userId} is not allowed to perform this action on costSurfaces.`,
);
},
ThenProjectNotEditableErrorWasReturned: (
response: request.Response,
projectId: string,
Expand Down