diff --git a/govtool/metadata-validation/jest.config.js b/govtool/metadata-validation/jest.config.js new file mode 100644 index 000000000..7f87b8f22 --- /dev/null +++ b/govtool/metadata-validation/jest.config.js @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig'); + +module.exports = { + preset: 'ts-jest', + moduleFileExtensions: ['js', 'json', 'ts'], + testRegex: '.*\\.(spec|test)\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: '../coverage', + testEnvironment: 'node', + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths), + modulePaths: [''], +}; diff --git a/govtool/metadata-validation/package.json b/govtool/metadata-validation/package.json index 264d89c61..97c3e9f57 100644 --- a/govtool/metadata-validation/package.json +++ b/govtool/metadata-validation/package.json @@ -31,6 +31,7 @@ "blakejs": "^1.2.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "joi": "^17.12.3", "jsonld": "^8.3.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" @@ -57,22 +58,5 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" } } diff --git a/govtool/metadata-validation/src/app.controller.spec.ts b/govtool/metadata-validation/src/app.controller.spec.ts index a05c9b88a..f74ccb63d 100644 --- a/govtool/metadata-validation/src/app.controller.spec.ts +++ b/govtool/metadata-validation/src/app.controller.spec.ts @@ -1,9 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { HttpModule } from '@nestjs/axios'; +import { MetadataValidationStatus } from '@enums'; + import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { MetadataValidationStatus } from './enums/ValidationError'; // TODO: Mock HttpService describe('AppController', () => { @@ -24,28 +25,26 @@ describe('AppController', () => { appController = app.get(AppController); }); - describe('metadata validation', () => { - it('should throw invalid URL', async () => { - const result = await appController.validateMetadata({ - hash: 'hash', - url: 'url', - }); - expect(result).toEqual({ - status: MetadataValidationStatus.URL_NOT_FOUND, - valid: false, - }); + it('should throw invalid URL', async () => { + const result = await appController.validateMetadata({ + hash: 'hash', + url: 'url', + }); + expect(result).toEqual({ + status: MetadataValidationStatus.URL_NOT_FOUND, + valid: false, }); + }); - it('should throw invalid JSONLD', async () => { - const result = await appController.validateMetadata({ - hash: 'hash', - url: 'http://www.schema.org', - }); + it('should throw invalid JSONLD', async () => { + const result = await appController.validateMetadata({ + hash: 'hash', + url: 'http://www.schema.org', + }); - expect(result).toEqual({ - status: MetadataValidationStatus.INVALID_JSONLD, - valid: false, - }); + expect(result).toEqual({ + status: MetadataValidationStatus.INVALID_JSONLD, + valid: false, }); }); }); diff --git a/govtool/metadata-validation/src/app.controller.ts b/govtool/metadata-validation/src/app.controller.ts index 6b248a789..13e2a7986 100644 --- a/govtool/metadata-validation/src/app.controller.ts +++ b/govtool/metadata-validation/src/app.controller.ts @@ -1,7 +1,9 @@ import { Controller, Body, Post } from '@nestjs/common'; + +import { ValidateMetadataDTO } from '@dto'; +import { ValidateMetadataResult } from '@types'; + import { AppService } from './app.service'; -import { ValidateMetadataDTO } from './dto/validateMetadata.dto'; -import { ValidateMetadataResult } from './types/validateMetadata'; @Controller() export class AppController { diff --git a/govtool/metadata-validation/src/app.service.ts b/govtool/metadata-validation/src/app.service.ts index 22a774851..c29cf16fa 100644 --- a/govtool/metadata-validation/src/app.service.ts +++ b/govtool/metadata-validation/src/app.service.ts @@ -3,10 +3,10 @@ import { catchError, firstValueFrom } from 'rxjs'; import { HttpService } from '@nestjs/axios'; import * as blake from 'blakejs'; -import { ValidateMetadataDTO } from './dto/validateMetadata.dto'; -import { MetadataValidationStatus } from './enums/ValidationError'; -import { canonizeJSON } from './utils/canonizeJSON'; -import { ValidateMetadataResult } from './types/validateMetadata'; +import { ValidateMetadataDTO } from '@dto'; +import { MetadataValidationStatus } from '@enums'; +import { canonizeJSON, validateMetadataStandard } from '@utils'; +import { ValidateMetadataResult } from '@types'; @Injectable() export class AppService { @@ -15,6 +15,7 @@ export class AppService { async validateMetadata({ hash, url, + standard, }: ValidateMetadataDTO): Promise { let status: MetadataValidationStatus; try { @@ -26,6 +27,10 @@ export class AppService { ), ); + if (standard) { + await validateMetadataStandard(data, standard); + } + let canonizedMetadata; try { canonizedMetadata = await canonizeJSON(data); diff --git a/govtool/metadata-validation/src/dto/index.ts b/govtool/metadata-validation/src/dto/index.ts new file mode 100644 index 000000000..7e7770094 --- /dev/null +++ b/govtool/metadata-validation/src/dto/index.ts @@ -0,0 +1 @@ +export * from './validateMetadata.dto'; diff --git a/govtool/metadata-validation/src/dto/validateMetadata.dto.ts b/govtool/metadata-validation/src/dto/validateMetadata.dto.ts index c7f3cd4d1..966e1f377 100644 --- a/govtool/metadata-validation/src/dto/validateMetadata.dto.ts +++ b/govtool/metadata-validation/src/dto/validateMetadata.dto.ts @@ -1,4 +1,6 @@ -import { IsUrl, IsNotEmpty } from 'class-validator'; +import { IsUrl, IsNotEmpty, IsEnum, IsOptional } from 'class-validator'; + +import { MetadataStandard } from '@types'; export class ValidateMetadataDTO { @IsNotEmpty() @@ -6,4 +8,8 @@ export class ValidateMetadataDTO { @IsUrl() url: string; + + @IsOptional() + @IsEnum(MetadataStandard) + standard?: MetadataStandard; } diff --git a/govtool/metadata-validation/src/enums/ValidationError.ts b/govtool/metadata-validation/src/enums/ValidationError.ts index b691cc99b..3471773a8 100644 --- a/govtool/metadata-validation/src/enums/ValidationError.ts +++ b/govtool/metadata-validation/src/enums/ValidationError.ts @@ -2,4 +2,5 @@ export enum MetadataValidationStatus { URL_NOT_FOUND = 'URL_NOT_FOUND', INVALID_JSONLD = 'INVALID_JSONLD', INVALID_HASH = 'INVALID_HASH', + INCORRECT_FORMAT = 'INCORRECT_FORMAT', } diff --git a/govtool/metadata-validation/src/enums/index.ts b/govtool/metadata-validation/src/enums/index.ts new file mode 100644 index 000000000..118d3701d --- /dev/null +++ b/govtool/metadata-validation/src/enums/index.ts @@ -0,0 +1 @@ +export * from './ValidationError'; diff --git a/govtool/metadata-validation/src/health/health.module.ts b/govtool/metadata-validation/src/health/health.module.ts index 0208ef743..9ed184e8f 100644 --- a/govtool/metadata-validation/src/health/health.module.ts +++ b/govtool/metadata-validation/src/health/health.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { TerminusModule } from '@nestjs/terminus'; + import { HealthController } from './health.controller'; @Module({ diff --git a/govtool/metadata-validation/src/main.ts b/govtool/metadata-validation/src/main.ts index b61303f21..c27be2ac4 100644 --- a/govtool/metadata-validation/src/main.ts +++ b/govtool/metadata-validation/src/main.ts @@ -1,8 +1,8 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); diff --git a/govtool/metadata-validation/src/schemas/cipStandardSchema.ts b/govtool/metadata-validation/src/schemas/cipStandardSchema.ts new file mode 100644 index 000000000..913b9e952 --- /dev/null +++ b/govtool/metadata-validation/src/schemas/cipStandardSchema.ts @@ -0,0 +1,45 @@ +import * as Joi from 'joi'; + +import { MetadataStandard } from '@types'; + +type StandardSpecification = Record>; + +const CIP100_URL = + 'https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#'; +const CIP108_URL = + 'https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#'; + +export const cipStandardSchema: StandardSpecification = { + // Source of CIP-108: https://github.com/Ryun1/CIPs/blob/governance-metadata-actions/CIP-0108/README.md + [MetadataStandard.CIP108]: Joi.object({ + '@context': Joi.object({ + '@language': Joi.string().required(), + CIP100: Joi.string().valid(CIP100_URL).required(), + CIP108: Joi.string().valid(CIP108_URL).required(), + hashAlgorithm: Joi.string().valid('CIP100:hashAlgorithm').required(), + body: Joi.object(), + authors: Joi.object(), + }), + authors: Joi.array(), + hashAlgorithm: Joi.object({ + '@value': Joi.string().valid('blake2b-256').required(), + }), + body: Joi.object({ + title: Joi.object({ '@value': Joi.string().max(80).required() }), + abstract: Joi.object({ '@value': Joi.string().max(2500).required() }), + motivation: Joi.object({ '@value': Joi.string().required() }), + rationale: Joi.object({ '@value': Joi.string().required() }), + references: Joi.array().items( + Joi.object({ + '@type': Joi.string(), + label: Joi.object({ '@value': Joi.string().required() }), + uri: Joi.object({ '@value': Joi.string().uri().required() }), + referenceHash: Joi.object({ + hashDigest: Joi.string().required(), + hashAlgorithm: Joi.string().required(), + }), + }).required(), + ), + }), + }), +}; diff --git a/govtool/metadata-validation/src/schemas/index.ts b/govtool/metadata-validation/src/schemas/index.ts new file mode 100644 index 000000000..874d081db --- /dev/null +++ b/govtool/metadata-validation/src/schemas/index.ts @@ -0,0 +1 @@ +export * from './cipStandardSchema'; diff --git a/govtool/metadata-validation/src/types/index.ts b/govtool/metadata-validation/src/types/index.ts index e69de29bb..085381c12 100644 --- a/govtool/metadata-validation/src/types/index.ts +++ b/govtool/metadata-validation/src/types/index.ts @@ -0,0 +1 @@ +export * from './validateMetadata'; diff --git a/govtool/metadata-validation/src/types/validateMetadata.ts b/govtool/metadata-validation/src/types/validateMetadata.ts index 5225fd265..dcdf28363 100644 --- a/govtool/metadata-validation/src/types/validateMetadata.ts +++ b/govtool/metadata-validation/src/types/validateMetadata.ts @@ -1,4 +1,8 @@ -import { MetadataValidationStatus } from '../enums/ValidationError'; +import { MetadataValidationStatus } from '@enums'; + +export enum MetadataStandard { + CIP108 = 'CIP108', +} export type ValidateMetadataResult = { status?: MetadataValidationStatus; diff --git a/govtool/metadata-validation/src/utils/index.ts b/govtool/metadata-validation/src/utils/index.ts index e69de29bb..648150db2 100644 --- a/govtool/metadata-validation/src/utils/index.ts +++ b/govtool/metadata-validation/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './canonizeJSON'; +export * from './validateMetadataStandard'; diff --git a/govtool/metadata-validation/src/utils/validateMetadataStandard.test.ts b/govtool/metadata-validation/src/utils/validateMetadataStandard.test.ts new file mode 100644 index 000000000..b3b58fc17 --- /dev/null +++ b/govtool/metadata-validation/src/utils/validateMetadataStandard.test.ts @@ -0,0 +1,19 @@ +import { MetadataStandard } from '@types'; + +import { validateMetadataStandard } from './validateMetadataStandard'; + +describe('validateMetadataStandard', () => { + it('should throw MetadataValidationStatus.INCORRECT_FORMAT if validation fails', async () => { + try { + await validateMetadataStandard( + { + testValue: 'test', + anotherValue: 'another', + }, + MetadataStandard.CIP108, + ); + } catch (error) { + expect(error).toBe('INCORRECT_FORMAT'); + } + }); +}); diff --git a/govtool/metadata-validation/src/utils/validateMetadataStandard.ts b/govtool/metadata-validation/src/utils/validateMetadataStandard.ts new file mode 100644 index 000000000..702027769 --- /dev/null +++ b/govtool/metadata-validation/src/utils/validateMetadataStandard.ts @@ -0,0 +1,20 @@ +import { MetadataValidationStatus } from '@enums'; +import { cipStandardSchema } from '@schemas'; +import { MetadataStandard } from '@types'; + +/** + * Validates the metadata against a specific standard. + * @param data - The metadata to be validated. + * @param standard - The metadata standard to validate against. + * @throws {MetadataValidationStatus.INCORRECT_FORMAT} - If the metadata does not conform to the specified standard. + */ +export const validateMetadataStandard = async ( + data: Record, + standard: MetadataStandard, +) => { + try { + await cipStandardSchema[standard]?.validateAsync(data); + } catch (error) { + throw MetadataValidationStatus.INCORRECT_FORMAT; + } +}; diff --git a/govtool/metadata-validation/tsconfig.json b/govtool/metadata-validation/tsconfig.json index 95f5641cf..1044dc39f 100644 --- a/govtool/metadata-validation/tsconfig.json +++ b/govtool/metadata-validation/tsconfig.json @@ -16,6 +16,15 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "paths": { + "@/*": ["src/*"], + "@dto": ["src/dto"], + "@enums": ["src/enums"], + "@health": ["src/health"], + "@schemas": ["src/schemas"], + "@types": ["src/types"], + "@utils": ["src/utils"] + } } } diff --git a/govtool/metadata-validation/yarn.lock b/govtool/metadata-validation/yarn.lock index 3b456c21d..c39cc7697 100644 --- a/govtool/metadata-validation/yarn.lock +++ b/govtool/metadata-validation/yarn.lock @@ -396,6 +396,18 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== +"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -858,6 +870,23 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@sideway/address@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" + integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -3517,6 +3546,17 @@ jest@^29.5.0: import-local "^3.0.2" jest-cli "^29.7.0" +joi@^17.12.3: + version "17.12.3" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.12.3.tgz#944646979cd3b460178547b12ba37aca8482f63d" + integrity sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g== + dependencies: + "@hapi/hoek" "^9.3.0" + "@hapi/topo" "^5.1.0" + "@sideway/address" "^4.1.5" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"