diff --git a/src/proposals/dto/create-measurement-period.dto.ts b/src/proposals/dto/create-measurement-period.dto.ts index 03a9a75d2..f5e4638b8 100644 --- a/src/proposals/dto/create-measurement-period.dto.ts +++ b/src/proposals/dto/create-measurement-period.dto.ts @@ -2,6 +2,15 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsDateString, IsOptional, IsString } from "class-validator"; export class CreateMeasurementPeriodDto { + @ApiProperty({ + type: String, + description: + "Unique identifier (relative to the proposal) of a MeasurementPeriod", + }) + @IsString() + @IsOptional() + id?: string; + @ApiProperty({ type: String, required: true, diff --git a/src/proposals/dto/update-proposal.dto.ts b/src/proposals/dto/update-proposal.dto.ts index 1ce6ce0f8..2c23064a9 100644 --- a/src/proposals/dto/update-proposal.dto.ts +++ b/src/proposals/dto/update-proposal.dto.ts @@ -7,10 +7,12 @@ import { IsObject, IsOptional, IsString, + Validate, ValidateNested, } from "class-validator"; import { OwnableDto } from "../../common/dto/ownable.dto"; import { CreateMeasurementPeriodDto } from "./create-measurement-period.dto"; +import { UniqueMeasurementPeriodIdConstraint } from "./validators/unique-measurement-period-id"; @ApiTags("proposals") export class UpdateProposalDto extends OwnableDto { @@ -113,6 +115,7 @@ export class UpdateProposalDto extends OwnableDto { @IsOptional() @ValidateNested({ each: true }) @Type(() => CreateMeasurementPeriodDto) + @Validate(UniqueMeasurementPeriodIdConstraint) readonly MeasurementPeriodList?: CreateMeasurementPeriodDto[]; @ApiProperty({ diff --git a/src/proposals/dto/validators/unique-measurement-period-id.ts b/src/proposals/dto/validators/unique-measurement-period-id.ts new file mode 100644 index 000000000..0aa85d6a6 --- /dev/null +++ b/src/proposals/dto/validators/unique-measurement-period-id.ts @@ -0,0 +1,23 @@ +import { + ValidationArguments, + ValidatorConstraint, + ValidatorConstraintInterface, +} from "class-validator"; +import { MeasurementPeriodClass } from "src/proposals/schemas/measurement-period.schema"; + +@ValidatorConstraint({ async: false }) +export class UniqueMeasurementPeriodIdConstraint + implements ValidatorConstraintInterface +{ + validate( + measurementPeriods: MeasurementPeriodClass[], + args: ValidationArguments, + ) { + const ids = measurementPeriods.map((period) => period.id); + return ids.length === new Set(ids).size; + } + + defaultMessage(args: ValidationArguments) { + return "MeasurementPeriod id must be unique within the MeasurementPeriodList"; + } +} diff --git a/src/proposals/schemas/measurement-period.schema.ts b/src/proposals/schemas/measurement-period.schema.ts index 3ccd6df18..8dc1b5fee 100644 --- a/src/proposals/schemas/measurement-period.schema.ts +++ b/src/proposals/schemas/measurement-period.schema.ts @@ -2,11 +2,26 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { ApiProperty } from "@nestjs/swagger"; import { Document } from "mongoose"; import { QueryableClass } from "src/common/schemas/queryable.schema"; +import { v4 as uuidv4 } from "uuid"; export type MeasurementPeriodDocument = MeasurementPeriodClass & Document; @Schema() export class MeasurementPeriodClass extends QueryableClass { + @ApiProperty({ + type: String, + default: () => uuidv4(), + required: true, + description: + "Unique identifier (relative to the proposal) of a MeasurementPeriod", + }) + @Prop({ + type: String, + required: true, + default: () => uuidv4(), + }) + id: string; + @ApiProperty({ type: String, required: true, diff --git a/src/proposals/schemas/proposal.schema.ts b/src/proposals/schemas/proposal.schema.ts index 8e26a1692..d58eeccd7 100644 --- a/src/proposals/schemas/proposal.schema.ts +++ b/src/proposals/schemas/proposal.schema.ts @@ -156,6 +156,16 @@ export class ProposalClass extends OwnableClass { @Prop({ type: [MeasurementPeriodSchema], required: false, + validate: { + validator: function (measurementPeriodList: MeasurementPeriodClass[]) { + const ids = measurementPeriodList.map( + (measurementPeriod) => measurementPeriod.id, + ); + return ids.length === new Set(ids).size; + }, + message: + "MeasurementPeriod id must be unique within the MeasurementPeriodList", + }, }) MeasurementPeriodList?: MeasurementPeriodClass[]; diff --git a/test/Proposal.js b/test/Proposal.js index 411aa220f..0270898c3 100644 --- a/test/Proposal.js +++ b/test/Proposal.js @@ -163,7 +163,20 @@ describe("1500: Proposal: Simple Proposal", () => { }); }); - it("0080: adds a new complete proposal with an extra field, which should fail", async () => { + it("0080: check if complete proposal with two similar MeasurementPeriod.id is valid", async () => { + return request(appUrl) + .post("/api/v3/Proposals/isValid") + .send(TestData.ProposalWrong_2) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenProposalIngestor}` }) + .expect(TestData.EntryValidStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.have.property("valid").and.equal(false); + }); + }); + + it("0090: adds a new complete proposal with an extra field, which should fail", async () => { return request(appUrl) .post("/api/v3/Proposals") .send(TestData.ProposalWrong_1) @@ -176,7 +189,7 @@ describe("1500: Proposal: Simple Proposal", () => { }); }); - it("0090: should fetch this new proposal", async () => { + it("0100: should fetch this new proposal", async () => { return request(appUrl) .get("/api/v3/Proposals/" + proposalId) .set("Accept", "application/json") @@ -189,7 +202,7 @@ describe("1500: Proposal: Simple Proposal", () => { }); }); - it("0100: should add a new attachment to this proposal", async () => { + it("0110: should add a new attachment to this proposal", async () => { let testAttachment = { ...TestData.AttachmentCorrect }; testAttachment.proposalId = defaultProposalId; return request(appUrl) @@ -220,7 +233,7 @@ describe("1500: Proposal: Simple Proposal", () => { }); }); - it("0110: should fetch this proposal attachment", async () => { + it("0120: should fetch this proposal attachment", async () => { return request(appUrl) .get("/api/v3/Proposals/" + proposalId + "/attachments") .set("Accept", "application/json") @@ -233,7 +246,7 @@ describe("1500: Proposal: Simple Proposal", () => { }); }); - it("0120: should delete this proposal attachment", async () => { + it("0130: should delete this proposal attachment", async () => { return request(appUrl) .delete( "/api/v3/Proposals/" + proposalId + "/attachments/" + attachmentId, @@ -243,7 +256,7 @@ describe("1500: Proposal: Simple Proposal", () => { .expect(TestData.SuccessfulDeleteStatusCode); }); - it("0130: admin can remove all existing proposals", async () => { + it("0140: admin can remove all existing proposals", async () => { return await request(appUrl) .get("/api/v3/Proposals") .set("Accept", "application/json") diff --git a/test/TestData.js b/test/TestData.js index f5ca82fbd..9bb08d2a5 100644 --- a/test/TestData.js +++ b/test/TestData.js @@ -50,6 +50,7 @@ const TestData = { accessGroups: [], MeasurementPeriodList: [ { + id: "1", instrument: "ESS3-1", start: "2017-07-24T13:56:30.000Z", end: "2017-07-25T13:56:30.000Z", @@ -63,6 +64,29 @@ const TestData = { ], }, + ProposalCorrectComplete2: { + proposalId: "20170268", + pi_email: "pi@uni.edu", + pi_firstname: "principal", + pi_lastname: "investigator", + email: "proposer@uni.edu", + firstname: "proposal", + lastname: "proposer", + title: "A complete test proposal", + abstract: "Abstract of test proposal", + ownerGroup: "proposalingestor", + accessGroups: [], + MeasurementPeriodList: [ + { + id: "1", + instrument: "ESS3-1", + start: "2017-07-24T13:56:30.000Z", + end: "2017-07-25T13:56:30.000Z", + comment: "Some comment", + } + ], + }, + ProposalWrong_1: { proposalId: "20170267", pi_email: "pi@uni.edu", @@ -79,6 +103,35 @@ const TestData = { createdBy: "This should not be here", }, + ProposalWrong_2: { + proposalId: "20170267", + pi_email: "pi@uni.edu", + pi_firstname: "principal", + pi_lastname: "investigator", + email: "proposer@uni.edu", + firstname: "proposal", + lastname: "proposer", + title: "A complete test proposal with an extra field", + abstract: "Abstract of test proposal", + ownerGroup: "20170251-group", + accessGroups: [], + MeasurementPeriodList: [ + { + id: "1", + instrument: "ESS3-1", + start: "2017-07-24T13:56:30.000Z", + end: "2017-07-25T13:56:30.000Z", + comment: "Some comment", + }, + { + id: "1", + instrument: "ESS3-2", + start: "2017-07-28T13:56:30.000Z", + end: "2017-07-29T13:56:30.000Z", + }, + ], + }, + AttachmentCorrect: { thumbnail: "data/abc123", caption: "Some caption",