Skip to content

Commit aa6d05a

Browse files
authored
test(attachmentV4): add api test for attachment v4 (#1853)
<!-- Follow semantic-release guidelines for the PR title, which is used in the changelog. Title should follow the format `<type>(<scope>): <subject>`, where - Type is one of: build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test|BREAKING CHANGE - Scope (optional) describes the place of the change (eg a particular milestone) and is usually omitted - subject should be a non-capitalized one-line description in present imperative tense and not ending with a period See https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines for more details. --> ## Description 1. added API tests for attachments v4 service. 2. added isValid endpoint for attachment input validation ## Motivation <!-- Background on use case, changes needed --> ## Fixes <!-- Please provide a list of the issues fixed by this PR --> * Bug fixed (#X) ## Changes: <!-- Please provide a list of the changes implemented by this PR --> * changes made ## Tests included - [ ] Included for each change/fix? - [ ] Passing? <!-- Merge will not be approved unless tests pass --> ## Documentation - [ ] swagger documentation updated (required for API changes) - [ ] official documentation updated ### official documentation info <!-- If you have updated the official documentation, please provide PR # and URL of the updated pages -->
2 parents 45c9fec + c5dc7fd commit aa6d05a

10 files changed

+410
-12
lines changed

src/attachments/attachments.v4.controller.ts

+101-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import {
1313
ForbiddenException,
1414
NotFoundException,
1515
Patch,
16+
Put,
17+
ValidationError,
18+
HttpCode,
1619
} from "@nestjs/common";
1720
import {
1821
ApiBearerAuth,
@@ -40,9 +43,15 @@ import { getSwaggerAttachmentFilterContent } from "./types/attachment-filter-con
4043
import { AttachmentFilterValidationPipe } from "./pipes/attachment-filter-validation.pipe";
4144
import { CreateAttachmentV4Dto } from "./dto/create-attachment.v4.dto";
4245
import { OutputAttachmentV4Dto } from "./dto/output-attachment.v4.dto";
43-
import { PartialUpdateAttachmentV4Dto } from "./dto/update-attachment.v4.dto";
46+
import {
47+
PartialUpdateAttachmentV4Dto,
48+
UpdateAttachmentV4Dto,
49+
} from "./dto/update-attachment.v4.dto";
4450
import { AttachmentsV4Service as AttachmentService } from "./attachments.v4.service";
4551
import { AllowAny } from "src/auth/decorators/allow-any.decorator";
52+
import { validate, ValidatorOptions } from "class-validator";
53+
import { plainToInstance } from "class-transformer";
54+
import { IsValidResponse } from "src/common/types";
4655

4756
@ApiBearerAuth()
4857
@ApiTags("attachments v4")
@@ -350,6 +359,49 @@ export class AttachmentsV4Controller {
350359
);
351360
}
352361

362+
// PUT /attachments/:aid
363+
@UseGuards(PoliciesGuard)
364+
@CheckPolicies("attachments", (ability: AppAbility) =>
365+
ability.can(Action.AttachmentUpdateEndpoint, Attachment),
366+
)
367+
@ApiOperation({
368+
summary: "It updates the attachment.",
369+
description: `It updates the attachment specified through the id specified. If optional fields are not provided they will be removed.
370+
The PUT method is responsible for modifying an existing entity. The crucial part about it is that it is supposed to replace an entity.
371+
Therefore, if we don’t send a field of an entity when performing a PUT request, the missing field should be removed from the document.
372+
(Caution: This operation could result with data loss if all the attachment fields are not provided)`,
373+
})
374+
@ApiParam({
375+
name: "aid",
376+
description: "ID of the attachment to modify",
377+
type: String,
378+
})
379+
@ApiBody({
380+
type: UpdateAttachmentV4Dto,
381+
})
382+
@ApiResponse({
383+
status: HttpStatus.OK,
384+
type: Attachment,
385+
description:
386+
"Update an existing attachment. The whole attachment object with updated fields have to be passed in.",
387+
})
388+
@Put("/:aid")
389+
async findOneAndReplace(
390+
@Req() request: Request,
391+
@Param("aid") aid: string,
392+
@Body() updateAttachmentDto: UpdateAttachmentV4Dto,
393+
): Promise<OutputAttachmentV4Dto | null> {
394+
await this.checkPermissionsForAttachment(
395+
request,
396+
aid,
397+
Action.AttachmentUpdateEndpoint,
398+
);
399+
return this.attachmentsService.findOneAndReplace(
400+
{ _id: aid },
401+
updateAttachmentDto,
402+
);
403+
}
404+
353405
// POST /attachments
354406
@UseGuards(PoliciesGuard)
355407
@CheckPolicies("attachments", (ability: AppAbility) =>
@@ -374,7 +426,6 @@ export class AttachmentsV4Controller {
374426
@Req() request: Request,
375427
@Body() createAttachmentDto: CreateAttachmentV4Dto,
376428
): Promise<OutputAttachmentV4Dto> {
377-
//TODO: check user permission
378429
this.checkPermissionsForAttachmentCreate(
379430
request,
380431
createAttachmentDto,
@@ -383,6 +434,54 @@ export class AttachmentsV4Controller {
383434
return this.attachmentsService.create(createAttachmentDto);
384435
}
385436

437+
@UseGuards(PoliciesGuard)
438+
@CheckPolicies("attachments", (ability: AppAbility) =>
439+
ability.can(Action.AttachmentCreateEndpoint, Attachment),
440+
)
441+
@Post("/isValid")
442+
@HttpCode(HttpStatus.OK)
443+
@ApiOperation({
444+
summary: "It validates the attachment provided as input.",
445+
description:
446+
"It validates the attachment provided as input, and returns true if the information is a valid attachment",
447+
})
448+
@ApiBody({
449+
type: CreateAttachmentV4Dto,
450+
})
451+
@ApiResponse({
452+
status: HttpStatus.OK,
453+
type: IsValidResponse,
454+
description:
455+
"Check if the attachment provided pass validation. It return true if the validation is passed",
456+
})
457+
async isValid(
458+
@Req() request: Request,
459+
@Body() createAttachmentDto: object,
460+
): Promise<IsValidResponse> {
461+
const validatorOptions: ValidatorOptions = {
462+
whitelist: true,
463+
forbidNonWhitelisted: true,
464+
};
465+
const CreateAttachmentDtoInstance = plainToInstance(
466+
CreateAttachmentV4Dto,
467+
createAttachmentDto,
468+
);
469+
470+
this.checkPermissionsForAttachmentCreate(
471+
request,
472+
CreateAttachmentDtoInstance,
473+
Action.AttachmentCreateEndpoint,
474+
);
475+
const errorsAttachment = await validate(
476+
CreateAttachmentDtoInstance,
477+
validatorOptions,
478+
);
479+
480+
const valid = errorsAttachment.length == 0;
481+
482+
return { valid: valid, reason: errorsAttachment };
483+
}
484+
386485
// DELETE /attachments/:aid
387486
@UseGuards(PoliciesGuard)
388487
@CheckPolicies("attachments", (ability: AppAbility) =>

src/attachments/attachments.v4.service.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Inject, Injectable, Scope } from "@nestjs/common";
1+
import { Inject, Injectable, NotFoundException, Scope } from "@nestjs/common";
22
import { REQUEST } from "@nestjs/core";
33
import { Request } from "express";
44
import { InjectModel } from "@nestjs/mongoose";
@@ -90,6 +90,33 @@ export class AttachmentsV4Service {
9090
return result;
9191
}
9292

93+
async findOneAndReplace(
94+
filter: FilterQuery<AttachmentDocument>,
95+
updateAttachmentDto: PartialUpdateAttachmentV4Dto,
96+
): Promise<Attachment | null> {
97+
const username = (this.request?.user as JWTUser).username;
98+
const existingAttachment = await this.attachmentModel
99+
.findOne({ aid: filter._id })
100+
.exec();
101+
if (!existingAttachment) {
102+
throw new NotFoundException(`Attachment: ${filter._id} not found`);
103+
}
104+
const updatedAttachmentInput = {
105+
...updateAttachmentDto,
106+
aid: existingAttachment.aid,
107+
createdBy: existingAttachment.createdBy,
108+
};
109+
const result = await this.attachmentModel
110+
.findOneAndReplace(
111+
filter,
112+
addUpdatedByField(updatedAttachmentInput, username),
113+
{ new: true },
114+
)
115+
.exec();
116+
117+
return result;
118+
}
119+
93120
async findOneAndDelete(
94121
filter: FilterQuery<AttachmentDocument>,
95122
): Promise<unknown> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ApiPropertyOptional, ApiProperty } from "@nestjs/swagger";
2+
import { IsString, IsEnum, IsOptional } from "class-validator";
3+
import { AttachmentRelationTargetType } from "../types/relationship-filter.enum";
4+
5+
export class AttachmentRelationshipsV4Dto {
6+
@IsString()
7+
@ApiProperty({
8+
type: String,
9+
description: "Array of entity target IDs.",
10+
})
11+
targetId: string;
12+
13+
@IsEnum(AttachmentRelationTargetType)
14+
@ApiProperty({
15+
enum: Object.values(AttachmentRelationTargetType),
16+
description:
17+
"Type of entity target. Can be one of the following: 'dataset','proposal','sample'.",
18+
})
19+
targetType: AttachmentRelationTargetType;
20+
21+
@IsOptional()
22+
@IsString()
23+
@ApiPropertyOptional({
24+
type: String,
25+
description: "Relationship type (defaults to 'is attached to').",
26+
default: "is attached to",
27+
})
28+
relationType?: string;
29+
}

src/attachments/dto/update-attachment.v4.dto.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { IsBoolean, IsOptional, IsString } from "class-validator";
1+
import {
2+
IsBoolean,
3+
IsOptional,
4+
IsString,
5+
ValidateNested,
6+
} from "class-validator";
27
import { OwnableDto } from "../../common/dto/ownable.dto";
38
import { PartialType } from "@nestjs/swagger";
4-
import { AttachmentRelationshipClass } from "../schemas/relationship.schema";
9+
import { Type } from "class-transformer";
10+
import { AttachmentRelationshipsV4Dto } from "./attachment-relationships.v4.dto";
511

612
export class UpdateAttachmentV4Dto extends OwnableDto {
713
@IsOptional()
@@ -12,7 +18,9 @@ export class UpdateAttachmentV4Dto extends OwnableDto {
1218
readonly caption: string;
1319

1420
@IsOptional()
15-
readonly relationships?: AttachmentRelationshipClass[];
21+
@ValidateNested({ each: true })
22+
@Type(() => AttachmentRelationshipsV4Dto)
23+
readonly relationships?: AttachmentRelationshipsV4Dto[];
1624

1725
@IsBoolean()
1826
isPublished: boolean;

src/attachments/interfaces/attachment-filters.interface.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { FilterQuery } from "mongoose";
22
import { ILimitsFilter } from "src/common/interfaces/common.interface";
3-
import { AttachmentRelationshipClass } from "../schemas/relationship.schema";
3+
import { AttachmentRelationshipsV4Dto } from "../dto/attachment-relationships.v4.dto";
44

55
export interface IAttachmentFields {
66
aid?: string;
77
_id: string;
88
thumbnail?: string;
99
caption: string;
10-
relationships?: AttachmentRelationshipClass[];
10+
relationships?: AttachmentRelationshipsV4Dto[];
1111
ownerGroup: string;
1212
accessGroups: string[];
1313
isPublished: boolean;

src/attachments/pipes/attachment-filter-validation.pipe.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { PipeTransform, Injectable } from "@nestjs/common";
22
import { BadRequestException } from "@nestjs/common/exceptions";
33
import { flattenObject } from "src/common/utils";
4-
import { AttachmentRelationshipClass } from "../schemas/relationship.schema";
4+
import { AttachmentRelationshipsV4Dto } from "../dto/attachment-relationships.v4.dto";
55
import { OutputAttachmentV4Dto } from "../dto/output-attachment.v4.dto";
66

77
// Attachment specific keys that are allowed
88
const ALLOWED_ATTACHMENT_KEYS = [
99
...Object.keys(new OutputAttachmentV4Dto()),
10-
...Object.keys(new AttachmentRelationshipClass()),
10+
...Object.keys(new AttachmentRelationshipsV4Dto()),
1111
];
1212

1313
// Allowed keys taken from mongoose QuerySelector.

src/common/types.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
2+
import { ValidationError } from "class-validator";
23
import { IFullFacets } from "src/elastic-search/interfaces/es-common.type";
34

45
export class FullFacetFilters {
@@ -49,5 +50,7 @@ export class CountApiResponse {
4950

5051
export class IsValidResponse {
5152
@ApiProperty({ type: Boolean })
52-
isvalid: boolean;
53+
valid: boolean;
54+
@ApiPropertyOptional()
55+
reason?: ValidationError[];
5356
}

src/datasets/datasets.v4.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ export class DatasetsV4Controller {
320320
@Req() request: Request,
321321
@Body(PidValidationPipe)
322322
createDatasetDto: object,
323-
) {
323+
): Promise<IsValidResponse> {
324324
const createDatasetDtoInstance = plainToInstance(
325325
CreateDatasetDto,
326326
createDatasetDto,

0 commit comments

Comments
 (0)