Skip to content

Commit

Permalink
Content-publisher api: more validations (#518)
Browse files Browse the repository at this point in the history
# Problem

Closes #494
# Solution
- moved existed decorators into utils
- enabled base32 regex verification for ContentHash
- Added `ApiProperty` definitions on existing DTOs;
- minor bugfixes and refactors

## Steps to Verify:

1. Run the content-publishing-api
2. Open swagger
3. Try submitting wrong requests
  • Loading branch information
aramikm authored Sep 24, 2024
1 parent 8682999 commit 78af6f3
Show file tree
Hide file tree
Showing 23 changed files with 905 additions and 661 deletions.
46 changes: 23 additions & 23 deletions apps/content-publishing-api/k6-test/script.k6.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ export const options = {
};

export default function () {
group('/v1/content/{userDsnpId}', () => {
let userDsnpId = '1';
group('/v1/content/{msaId}', () => {
let msaId = '1';
// Request No. 1: ApiController_update with no assets
{
let url = BASE_URL + `/v1/content/${userDsnpId}`;
let url = BASE_URL + `/v1/content/${msaId}`;
const body = {
targetContentHash: '0x7653423447AF',
targetContentHash: 'bdyqdua4t4pxgy37mdmjyqv3dejp5betyqsznimpneyujsur23yubzna',
targetAnnouncementType: 'broadcast',
content: validContentNoUploadedAssets,
};
Expand All @@ -56,9 +56,9 @@ export default function () {
}
// Request No. 2: ApiController_update with assets
{
let url = BASE_URL + `/v1/content/${userDsnpId}`;
let url = BASE_URL + `/v1/content/${msaId}`;
const body = {
targetContentHash: '0x7653423447AF',
targetContentHash: 'bdyqdua4t4pxgy37mdmjyqv3dejp5betyqsznimpneyujsur23yubzna',
targetAnnouncementType: 'broadcast',
content: createContentWithAsset(BASE_URL),
};
Expand All @@ -72,9 +72,9 @@ export default function () {

// Request No. 3: ApiController_delete
{
let url = BASE_URL + `/v1/content/${userDsnpId}`;
let url = BASE_URL + `/v1/content/${msaId}`;
let body = {
targetContentHash: '0x7653423447AF',
targetContentHash: 'bdyqdua4t4pxgy37mdmjyqv3dejp5betyqsznimpneyujsur23yubzna',
targetAnnouncementType: 'broadcast',
};
let params = { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } };
Expand All @@ -99,12 +99,12 @@ export default function () {
}
});

group('/v1/profile/{userDsnpId}', () => {
let userDsnpId = '1'; // specify value as there is no example value for this parameter in OpenAPI spec
group('/v1/profile/{msaId}', () => {
let msaId = '1'; // specify value as there is no example value for this parameter in OpenAPI spec

// Request No. 1: ApiController_profile with no assets
{
let url = BASE_URL + `/v1/profile/${userDsnpId}`;
let url = BASE_URL + `/v1/profile/${msaId}`;
let body = { profile: validProfileNoUploadedAssets };
let params = { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } };
let request = http.put(url, JSON.stringify(body), params);
Expand All @@ -115,7 +115,7 @@ export default function () {
}
// Request No. 2: ApiController_profile with asset
{
let url = BASE_URL + `/v1/profile/${userDsnpId}`;
let url = BASE_URL + `/v1/profile/${msaId}`;
const referenceId = getReferenceId(BASE_URL);
let profile = Object.assign({}, validProfileNoUploadedAssets, {
icon: [
Expand All @@ -136,12 +136,12 @@ export default function () {
}
});

group('/v1/content/{userDsnpId}/broadcast', () => {
let userDsnpId = '1';
group('/v1/content/{msaId}/broadcast', () => {
let msaId = '1';

// Request No. 1: ApiController_broadcast no assets
{
let url = BASE_URL + `/v1/content/${userDsnpId}/broadcast`;
let url = BASE_URL + `/v1/content/${msaId}/broadcast`;
const body = {
content: validContentNoUploadedAssets,
};
Expand All @@ -154,7 +154,7 @@ export default function () {
}
// Request No. 2: ApiController_broadcast with assets
{
let url = BASE_URL + `/v1/content/${userDsnpId}/broadcast`;
let url = BASE_URL + `/v1/content/${msaId}/broadcast`;
const body = {
content: createContentWithAsset(BASE_URL),
};
Expand All @@ -167,12 +167,12 @@ export default function () {
}
});

group('/v1/content/{userDsnpId}/reaction', () => {
let userDsnpId = '1';
group('/v1/content/{msaId}/reaction', () => {
let msaId = '1';

// Request No. 1: ApiController_reaction
{
let url = BASE_URL + `/v1/content/${userDsnpId}/reaction`;
let url = BASE_URL + `/v1/content/${msaId}/reaction`;
let body = validReaction;
let params = { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } };
let request = http.post(url, JSON.stringify(body), params);
Expand All @@ -183,12 +183,12 @@ export default function () {
}
});

group('/v1/content/{userDsnpId}/reply', () => {
let userDsnpId = '1';
group('/v1/content/{msaId}/reply', () => {
let msaId = '1';

// Request No. 1: ApiController_reply no assets
{
let url = BASE_URL + `/v1/content/${userDsnpId}/reply`;
let url = BASE_URL + `/v1/content/${msaId}/reply`;
let body = validReplyNoUploadedAssets;
let params = { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } };
let request = http.post(url, JSON.stringify(body), params);
Expand All @@ -199,7 +199,7 @@ export default function () {
}
// Request No. 2: ApiController_reply with assets
{
let url = BASE_URL + `/v1/content/${userDsnpId}/reply`;
let url = BASE_URL + `/v1/content/${msaId}/reply`;
let body = Object.assign({}, validReplyNoUploadedAssets, { content: createContentWithAsset(BASE_URL) });
let params = { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } };
let request = http.post(url, JSON.stringify(body), params);
Expand Down
12 changes: 12 additions & 0 deletions apps/content-publishing-api/src/api.config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('Content Publishing API Config', () => {
API_PORT: undefined,
// API_TIMEOUT_MS: undefined,
FILE_UPLOAD_MAX_SIZE_IN_BYTES: undefined,
FILE_UPLOAD_COUNT_LIMIT: undefined,
};

beforeAll(() => {
Expand All @@ -28,8 +29,13 @@ describe('Content Publishing API Config', () => {

it('missing file upload limit should fail', async () => validateMissing(ALL_ENV, 'FILE_UPLOAD_MAX_SIZE_IN_BYTES'));

it('missing file upload count limit should fail', async () => validateMissing(ALL_ENV, 'FILE_UPLOAD_COUNT_LIMIT'));

it('invalid file upload limit should fail', async () =>
shouldFailBadValues(ALL_ENV, 'FILE_UPLOAD_MAX_SIZE_IN_BYTES', [-1]));

it('invalid file upload count limit should fail', async () =>
shouldFailBadValues(ALL_ENV, 'FILE_UPLOAD_COUNT_LIMIT', [-1]));
});

describe('valid environment', () => {
Expand All @@ -52,6 +58,12 @@ describe('Content Publishing API Config', () => {
);
});

it('should get file upload count limit', () => {
expect(contentPublishingServiceConfig.fileUploadCountLimit).toStrictEqual(
parseInt(ALL_ENV.FILE_UPLOAD_COUNT_LIMIT as string, 10),
);
});

// it('should get api timeout limit milliseconds', () => {
// expect(contentPublishingServiceConfig.apiTimeoutMs).toStrictEqual(parseInt(ALL_ENV.API_TIMEOUT_MS as string, 10));
// });
Expand Down
5 changes: 5 additions & 0 deletions apps/content-publishing-api/src/api.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface IContentPublishingApiConfig {
apiPort: number;
// apiTimeoutMs: number;
fileUploadMaxSizeBytes: number;
fileUploadCountLimit: number;
}

export default registerAs('content-publishing-api', (): IContentPublishingApiConfig => {
Expand All @@ -27,6 +28,10 @@ export default registerAs('content-publishing-api', (): IContentPublishingApiCon
value: process.env.FILE_UPLOAD_MAX_SIZE_IN_BYTES,
joi: Joi.number().min(1).required(),
},
fileUploadCountLimit: {
value: process.env.FILE_UPLOAD_COUNT_LIMIT,
joi: Joi.number().min(1).required(),
},
};

return JoiUtils.validate<IContentPublishingApiConfig>(configs);
Expand Down
1 change: 1 addition & 0 deletions apps/content-publishing-api/src/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import ipfsConfig from '#content-publishing-lib/config/ipfs.config';
useFactory: async (apiConf: IContentPublishingApiConfig) => ({
limits: {
fileSize: apiConf.fileUploadMaxSizeBytes,
files: apiConf.fileUploadCountLimit,
},
}),
inject: [apiConfig.KEY],
Expand Down
1 change: 1 addition & 0 deletions apps/content-publishing-api/src/build-openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ process.env.CAPACITY_LIMIT = '{"type":"amount","value":"80"}';
process.env.IPFS_ENDPOINT = 'http://127.0.0.1';
process.env.IPFS_GATEWAY_URL = 'http://127.0.0.1';
process.env.FILE_UPLOAD_MAX_SIZE_IN_BYTES = '100';
process.env.FILE_UPLOAD_COUNT_LIMIT = '10';
process.env.ASSET_EXPIRATION_INTERVAL_SECONDS = '100';
process.env.BATCH_INTERVAL_SECONDS = '100';
process.env.BATCH_MAX_COUNT = '100';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Body, Controller, Delete, HttpCode, Logger, Param, Post, Put } from '@n
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiService } from '../../api.service';
import {
DsnpUserIdParam,
BroadcastDto,
AnnouncementResponseDto,
AssetIncludedRequestDto,
Expand All @@ -12,6 +11,7 @@ import {
TombstoneDto,
} from '#types/dtos/content-publishing';
import { AnnouncementTypeName } from '#types/enums';
import { MsaIdDto } from '#types/dtos/common';

@Controller('v1/content')
@ApiTags('v1/content')
Expand All @@ -22,60 +22,46 @@ export class ContentControllerV1 {
this.logger = new Logger(this.constructor.name);
}

@Post(':userDsnpId/broadcast')
@Post(':msaId/broadcast')
@ApiOperation({ summary: 'Create DSNP Broadcast for user' })
@HttpCode(202)
@ApiResponse({ status: '2XX', type: AnnouncementResponseDto })
async broadcast(
@Param() userDsnpId: DsnpUserIdParam,
@Body() broadcastDto: BroadcastDto,
): Promise<AnnouncementResponseDto> {
async broadcast(@Param() { msaId }: MsaIdDto, @Body() broadcastDto: BroadcastDto): Promise<AnnouncementResponseDto> {
const metadata = await this.apiService.validateAssetsAndFetchMetadata(broadcastDto as AssetIncludedRequestDto);
return this.apiService.enqueueRequest(
AnnouncementTypeName.BROADCAST,
userDsnpId.userDsnpId,
broadcastDto,
metadata,
);
return this.apiService.enqueueRequest(AnnouncementTypeName.BROADCAST, msaId, broadcastDto, metadata);
}

@Post(':userDsnpId/reply')
@Post(':msaId/reply')
@ApiOperation({ summary: 'Create DSNP Reply for user' })
@HttpCode(202)
@ApiResponse({ status: '2XX', type: AnnouncementResponseDto })
async reply(@Param() userDsnpId: DsnpUserIdParam, @Body() replyDto: ReplyDto): Promise<AnnouncementResponseDto> {
async reply(@Param() { msaId }: MsaIdDto, @Body() replyDto: ReplyDto): Promise<AnnouncementResponseDto> {
const metadata = await this.apiService.validateAssetsAndFetchMetadata(replyDto as AssetIncludedRequestDto);
return this.apiService.enqueueRequest(AnnouncementTypeName.REPLY, userDsnpId.userDsnpId, replyDto, metadata);
return this.apiService.enqueueRequest(AnnouncementTypeName.REPLY, msaId, replyDto, metadata);
}

@Post(':userDsnpId/reaction')
@Post(':msaId/reaction')
@ApiOperation({ summary: 'Create DSNP Reaction for user' })
@HttpCode(202)
@ApiResponse({ status: '2XX', type: AnnouncementResponseDto })
async reaction(
@Param() userDsnpId: DsnpUserIdParam,
@Body() reactionDto: ReactionDto,
): Promise<AnnouncementResponseDto> {
return this.apiService.enqueueRequest(AnnouncementTypeName.REACTION, userDsnpId.userDsnpId, reactionDto);
async reaction(@Param() { msaId }: MsaIdDto, @Body() reactionDto: ReactionDto): Promise<AnnouncementResponseDto> {
return this.apiService.enqueueRequest(AnnouncementTypeName.REACTION, msaId, reactionDto);
}

@Put(':userDsnpId')
@Put(':msaId')
@ApiOperation({ summary: 'Update DSNP Content for user' })
@HttpCode(202)
@ApiResponse({ status: '2XX', type: AnnouncementResponseDto })
async update(@Param() userDsnpId: DsnpUserIdParam, @Body() updateDto: UpdateDto): Promise<AnnouncementResponseDto> {
async update(@Param() { msaId }: MsaIdDto, @Body() updateDto: UpdateDto): Promise<AnnouncementResponseDto> {
const metadata = await this.apiService.validateAssetsAndFetchMetadata(updateDto as AssetIncludedRequestDto);
return this.apiService.enqueueRequest(AnnouncementTypeName.UPDATE, userDsnpId.userDsnpId, updateDto, metadata);
return this.apiService.enqueueRequest(AnnouncementTypeName.UPDATE, msaId, updateDto, metadata);
}

@Delete(':userDsnpId')
@Delete(':msaId')
@ApiOperation({ summary: 'Delete DSNP Content for user' })
@HttpCode(202)
@ApiResponse({ status: '2XX', type: AnnouncementResponseDto })
async delete(
@Param() userDsnpId: DsnpUserIdParam,
@Body() tombstoneDto: TombstoneDto,
): Promise<AnnouncementResponseDto> {
return this.apiService.enqueueRequest(AnnouncementTypeName.TOMBSTONE, userDsnpId.userDsnpId, tombstoneDto);
async delete(@Param() { msaId }: MsaIdDto, @Body() tombstoneDto: TombstoneDto): Promise<AnnouncementResponseDto> {
return this.apiService.enqueueRequest(AnnouncementTypeName.TOMBSTONE, msaId, tombstoneDto);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { Body, Controller, HttpCode, Logger, Param, Put } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ApiService } from '../../api.service';
import {
DsnpUserIdParam,
ProfileDto,
AnnouncementResponseDto,
AssetIncludedRequestDto,
} from '#types/dtos/content-publishing';
import { ProfileDto, AnnouncementResponseDto, AssetIncludedRequestDto } from '#types/dtos/content-publishing';
import { AnnouncementTypeName } from '#types/enums';
import { MsaIdDto } from '#types/dtos/common';

@Controller('v1/profile')
@ApiTags('v1/profile')
Expand All @@ -18,15 +14,12 @@ export class ProfileControllerV1 {
this.logger = new Logger(this.constructor.name);
}

@Put(':userDsnpId')
@Put(':msaId')
@ApiOperation({ summary: "Update a user's Profile" })
@HttpCode(202)
@ApiResponse({ status: '2XX', type: AnnouncementResponseDto })
async profile(
@Param() userDsnpId: DsnpUserIdParam,
@Body() profileDto: ProfileDto,
): Promise<AnnouncementResponseDto> {
async profile(@Param() { msaId }: MsaIdDto, @Body() profileDto: ProfileDto): Promise<AnnouncementResponseDto> {
const metadata = await this.apiService.validateAssetsAndFetchMetadata(profileDto as AssetIncludedRequestDto);
return this.apiService.enqueueRequest(AnnouncementTypeName.PROFILE, userDsnpId.userDsnpId, profileDto, metadata);
return this.apiService.enqueueRequest(AnnouncementTypeName.PROFILE, msaId, profileDto, metadata);
}
}
Loading

0 comments on commit 78af6f3

Please sign in to comment.