From 17ba52797535f9b979ef266f5708434bd87d3960 Mon Sep 17 00:00:00 2001 From: Aiyaret Sandhu Date: Thu, 24 Jul 2025 13:04:37 +0530 Subject: [PATCH] test: add comprehensive W3C schema validation and error handling tests --- .../src/dtos/create-schema-dto.spec.ts | 692 ++++++++++++++++++ .../libs/helpers/attributes.validator.spec.ts | 248 +++++++ .../libs/helpers/attributes.validator.ts | 41 +- apps/issuance/src/issuance.service.spec.ts | 77 ++ .../libs/helpers/w3c.schema.builder.spec.ts | 442 +++++++++++ .../ledger/libs/helpers/w3c.schema.builder.ts | 52 +- apps/ledger/src/schema/schema.spec.ts | 425 +++++++++++ 7 files changed, 1961 insertions(+), 16 deletions(-) create mode 100644 apps/api-gateway/src/dtos/create-schema-dto.spec.ts create mode 100644 apps/issuance/libs/helpers/attributes.validator.spec.ts create mode 100644 apps/issuance/src/issuance.service.spec.ts create mode 100644 apps/ledger/libs/helpers/w3c.schema.builder.spec.ts create mode 100644 apps/ledger/src/schema/schema.spec.ts diff --git a/apps/api-gateway/src/dtos/create-schema-dto.spec.ts b/apps/api-gateway/src/dtos/create-schema-dto.spec.ts new file mode 100644 index 000000000..4d8c499be --- /dev/null +++ b/apps/api-gateway/src/dtos/create-schema-dto.spec.ts @@ -0,0 +1,692 @@ +import { validate } from 'class-validator'; +import { plainToClass } from 'class-transformer'; +import { CreateW3CSchemaDto, W3CAttributeValue, GenericSchemaDTO } from './create-schema.dto'; +import { W3CSchemaDataType, SchemaTypeEnum, JSONSchemaType } from '@credebl/enum/enum'; + +describe('Schema Creation DTOs - Validation Tests', () => { + describe('W3CAttributeValue DTO', () => { + it('should validate a valid W3C attribute', async () => { + const validAttribute = { + attributeName: 'name', + displayName: 'Full Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + description: 'The person full name' + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should reject empty attributeName', async () => { + const invalidAttribute = { + attributeName: '', + displayName: 'Full Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }; + + const attributeDto = plainToClass(W3CAttributeValue, invalidAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isNotEmpty'); + }); + + it('should reject empty displayName', async () => { + const invalidAttribute = { + attributeName: 'name', + displayName: '', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }; + + const attributeDto = plainToClass(W3CAttributeValue, invalidAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isNotEmpty'); + }); + + it('should reject invalid schemaDataType', async () => { + const invalidAttribute = { + attributeName: 'name', + displayName: 'Full Name', + schemaDataType: 'INVALID_TYPE', + isRequired: true + }; + + const attributeDto = plainToClass(W3CAttributeValue, invalidAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isEnum'); + }); + + it('should require isRequired property', async () => { + const invalidAttribute = { + attributeName: 'name', + displayName: 'Full Name', + schemaDataType: W3CSchemaDataType.STRING + // Missing isRequired + }; + + const attributeDto = plainToClass(W3CAttributeValue, invalidAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isNotEmpty'); + }); + + describe('String Attribute Validations', () => { + it('should validate minLength for string type', async () => { + const validAttribute = { + attributeName: 'username', + displayName: 'Username', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + minLength: 3 + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate maxLength for string type', async () => { + const validAttribute = { + attributeName: 'description', + displayName: 'Description', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: false, + maxLength: 500 + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate pattern for string type', async () => { + const validAttribute = { + attributeName: 'email', + displayName: 'Email', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$' + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate enum for string type', async () => { + const validAttribute = { + attributeName: 'status', + displayName: 'Status', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + enum: ['active', 'inactive', 'pending'] + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should reject invalid minLength (negative)', async () => { + const invalidAttribute = { + attributeName: 'name', + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + minLength: -1 + }; + + const attributeDto = plainToClass(W3CAttributeValue, invalidAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('min'); + }); + + it('should reject invalid maxLength (zero)', async () => { + const invalidAttribute = { + attributeName: 'name', + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + maxLength: 0 + }; + + const attributeDto = plainToClass(W3CAttributeValue, invalidAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('min'); + }); + }); + + describe('Number Attribute Validations', () => { + it('should validate minimum for number type', async () => { + const validAttribute = { + attributeName: 'age', + displayName: 'Age', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + minimum: 0 + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate maximum for number type', async () => { + const validAttribute = { + attributeName: 'score', + displayName: 'Score', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + maximum: 100 + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate multipleOf for number type', async () => { + const validAttribute = { + attributeName: 'price', + displayName: 'Price', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + multipleOf: 0.01 + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should reject invalid multipleOf (negative)', async () => { + const invalidAttribute = { + attributeName: 'price', + displayName: 'Price', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + multipleOf: -0.01 + }; + + const attributeDto = plainToClass(W3CAttributeValue, invalidAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isPositive'); + }); + }); + + describe('Array Attribute Validations', () => { + it('should validate minItems for array type', async () => { + const validAttribute = { + attributeName: 'skills', + displayName: 'Skills', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + minItems: 1, + items: [{ + attributeName: 'skill', + displayName: 'Skill', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }] + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate maxItems for array type', async () => { + const validAttribute = { + attributeName: 'tags', + displayName: 'Tags', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + maxItems: 10, + items: [{ + attributeName: 'tag', + displayName: 'Tag', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }] + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate uniqueItems for array type', async () => { + const validAttribute = { + attributeName: 'categories', + displayName: 'Categories', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + uniqueItems: true, + items: [{ + attributeName: 'category', + displayName: 'Category', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }] + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should reject invalid minItems (negative)', async () => { + const invalidAttribute = { + attributeName: 'items', + displayName: 'Items', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + minItems: -1, + items: [{ + attributeName: 'item', + displayName: 'Item', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }] + }; + + const attributeDto = plainToClass(W3CAttributeValue, invalidAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('min'); + }); + }); + + describe('Object Attribute Validations', () => { + it('should validate minProperties for object type', async () => { + const validAttribute = { + attributeName: 'address', + displayName: 'Address', + schemaDataType: W3CSchemaDataType.OBJECT, + isRequired: true, + minProperties: 2, + properties: { + street: { + attributeName: 'street', + displayName: 'Street', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }, + city: { + attributeName: 'city', + displayName: 'City', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + } + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate maxProperties for object type', async () => { + const validAttribute = { + attributeName: 'metadata', + displayName: 'Metadata', + schemaDataType: W3CSchemaDataType.OBJECT, + isRequired: false, + maxProperties: 5, + properties: { + key1: { + attributeName: 'key1', + displayName: 'Key 1', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: false + } + } + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate additionalProperties for object type', async () => { + const validAttribute = { + attributeName: 'config', + displayName: 'Configuration', + schemaDataType: W3CSchemaDataType.OBJECT, + isRequired: false, + additionalProperties: false, + properties: { + setting: { + attributeName: 'setting', + displayName: 'Setting', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: false + } + } + }; + + const attributeDto = plainToClass(W3CAttributeValue, validAttribute); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + }); + }); + + describe('CreateW3CSchemaDto', () => { + it('should validate a valid W3C schema', async () => { + const validSchema = { + attributes: [ + { + attributeName: 'name', + displayName: 'Full Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ], + schemaName: 'Test Schema', + description: 'A test schema for validation', + schemaType: JSONSchemaType.POLYGON_W3C + }; + + const schemaDto = plainToClass(CreateW3CSchemaDto, validSchema); + const errors = await validate(schemaDto); + + expect(errors.length).toBe(0); + }); + + it('should reject empty attributes array', async () => { + const invalidSchema = { + attributes: [], + schemaName: 'Test Schema', + description: 'A test schema', + schemaType: JSONSchemaType.POLYGON_W3C + }; + + const schemaDto = plainToClass(CreateW3CSchemaDto, invalidSchema); + const errors = await validate(schemaDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('arrayMinSize'); + }); + + it('should reject empty schemaName', async () => { + const invalidSchema = { + attributes: [ + { + attributeName: 'name', + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ], + schemaName: '', + description: 'A test schema', + schemaType: JSONSchemaType.POLYGON_W3C + }; + + const schemaDto = plainToClass(CreateW3CSchemaDto, invalidSchema); + const errors = await validate(schemaDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isNotEmpty'); + }); + + it('should reject empty description', async () => { + const invalidSchema = { + attributes: [ + { + attributeName: 'name', + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ], + schemaName: 'Test Schema', + description: '', + schemaType: JSONSchemaType.POLYGON_W3C + }; + + const schemaDto = plainToClass(CreateW3CSchemaDto, invalidSchema); + const errors = await validate(schemaDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isNotEmpty'); + }); + + it('should reject invalid schemaType', async () => { + const invalidSchema = { + attributes: [ + { + attributeName: 'name', + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ], + schemaName: 'Test Schema', + description: 'A test schema', + schemaType: 'INVALID_TYPE' + }; + + const schemaDto = plainToClass(CreateW3CSchemaDto, invalidSchema); + const errors = await validate(schemaDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isEnum'); + }); + + it('should trim whitespace from schemaName', async () => { + const schemaWithWhitespace = { + attributes: [ + { + attributeName: 'name', + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ], + schemaName: ' Test Schema ', + description: 'A test schema', + schemaType: JSONSchemaType.POLYGON_W3C + }; + + const schemaDto = plainToClass(CreateW3CSchemaDto, schemaWithWhitespace); + const errors = await validate(schemaDto); + + expect(errors.length).toBe(0); + expect(schemaDto.schemaName).toBe('Test Schema'); + }); + + it('should handle nested attribute validation errors', async () => { + const schemaWithInvalidAttribute = { + attributes: [ + { + attributeName: '', // Invalid empty name + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ], + schemaName: 'Test Schema', + description: 'A test schema', + schemaType: JSONSchemaType.POLYGON_W3C + }; + + const schemaDto = plainToClass(CreateW3CSchemaDto, schemaWithInvalidAttribute); + const errors = await validate(schemaDto); + + expect(errors.length).toBeGreaterThan(0); + // Check that nested validation errors are caught + expect(errors.some(error => error.property === 'attributes')).toBe(true); + }); + }); + + describe('GenericSchemaDTO', () => { + it('should validate W3C schema type', async () => { + const validGenericSchema = { + type: SchemaTypeEnum.JSON, + alias: 'test-alias', + schemaPayload: { + attributes: [ + { + attributeName: 'name', + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ], + schemaName: 'Test Schema', + description: 'A test schema', + schemaType: JSONSchemaType.POLYGON_W3C + } + }; + + const genericDto = plainToClass(GenericSchemaDTO, validGenericSchema); + const errors = await validate(genericDto); + + expect(errors.length).toBe(0); + }); + + it('should reject invalid type', async () => { + const invalidGenericSchema = { + type: 'INVALID_TYPE', + alias: 'test-alias', + schemaPayload: {} + }; + + const genericDto = plainToClass(GenericSchemaDTO, invalidGenericSchema); + const errors = await validate(genericDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isEnum'); + }); + + it('should require type field', async () => { + const invalidGenericSchema = { + alias: 'test-alias', + schemaPayload: {} + }; + + const genericDto = plainToClass(GenericSchemaDTO, invalidGenericSchema); + const errors = await validate(genericDto); + + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints).toHaveProperty('isNotEmpty'); + }); + + it('should trim alias whitespace', async () => { + const schemaWithWhitespace = { + type: SchemaTypeEnum.JSON, + alias: ' test-alias ', + schemaPayload: { + attributes: [ + { + attributeName: 'name', + displayName: 'Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ], + schemaName: 'Test Schema', + description: 'A test schema', + schemaType: JSONSchemaType.POLYGON_W3C + } + }; + + const genericDto = plainToClass(GenericSchemaDTO, schemaWithWhitespace); + const errors = await validate(genericDto); + + expect(errors.length).toBe(0); + expect(genericDto.alias).toBe('test-alias'); + }); + }); + + describe('Cross-Field Validation', () => { + it('should validate conditional string validations', async () => { + const stringAttributeWithValidations = { + attributeName: 'email', + displayName: 'Email', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + minLength: 5, + maxLength: 100, + pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$' + }; + + const attributeDto = plainToClass(W3CAttributeValue, stringAttributeWithValidations); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate conditional number validations', async () => { + const numberAttributeWithValidations = { + attributeName: 'score', + displayName: 'Score', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + minimum: 0, + maximum: 100, + multipleOf: 0.5 + }; + + const attributeDto = plainToClass(W3CAttributeValue, numberAttributeWithValidations); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + + it('should validate conditional array validations', async () => { + const arrayAttributeWithValidations = { + attributeName: 'items', + displayName: 'Items', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + minItems: 1, + maxItems: 10, + uniqueItems: true, + items: [{ + attributeName: 'item', + displayName: 'Item', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }] + }; + + const attributeDto = plainToClass(W3CAttributeValue, arrayAttributeWithValidations); + const errors = await validate(attributeDto); + + expect(errors.length).toBe(0); + }); + }); +}); diff --git a/apps/issuance/libs/helpers/attributes.validator.spec.ts b/apps/issuance/libs/helpers/attributes.validator.spec.ts new file mode 100644 index 000000000..9805b5d30 --- /dev/null +++ b/apps/issuance/libs/helpers/attributes.validator.spec.ts @@ -0,0 +1,248 @@ +import { BadRequestException } from '@nestjs/common'; +import { validateW3CSchemaAttributes } from './attributes.validator'; +import { W3CSchemaDataType } from '@credebl/enum/enum'; + +describe('Attributes Validator', () => { + describe('validateW3CSchemaAttributes', () => { + const mockSchemaAttributes = [ + { + attributeName: 'name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + displayName: 'Full Name' + }, + { + attributeName: 'age', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: false, + displayName: 'Age' + }, + { + attributeName: 'email', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + displayName: 'Email Address' + } + ]; + + it('should pass validation with valid attributes', () => { + const validAttributes = { + name: 'John Doe', + age: '30', + email: 'john@example.com' + }; + + expect(() => { + validateW3CSchemaAttributes(validAttributes, mockSchemaAttributes); + }).not.toThrow(); + }); + + it('should pass validation with only required attributes', () => { + const validAttributes = { + name: 'John Doe', + email: 'john@example.com' + }; + + expect(() => { + validateW3CSchemaAttributes(validAttributes, mockSchemaAttributes); + }).not.toThrow(); + }); + + it('should throw error when required attribute is missing', () => { + const invalidAttributes = { + age: '30' + // missing required 'name' and 'email' + }; + + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, mockSchemaAttributes); + }).toThrow(BadRequestException); + }); + + it('should throw error when required attribute has empty value', () => { + const invalidAttributes = { + name: '', + email: 'john@example.com' + }; + + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, mockSchemaAttributes); + }).toThrow(BadRequestException); + }); + + it('should throw error when attribute has wrong data type', () => { + const invalidAttributes = { + name: 'John Doe', + age: 'thirty', // should be number + email: 'john@example.com' + }; + + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, mockSchemaAttributes); + }).toThrow(BadRequestException); + }); + + it('should throw error when extra attributes are provided', () => { + const invalidAttributes = { + name: 'John Doe', + email: 'john@example.com', + invalidAttribute: 'not allowed' + }; + + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, mockSchemaAttributes); + }).toThrow(BadRequestException); + }); + + it('should handle datetime attributes correctly', () => { + const schemaWithDateTime = [ + { + attributeName: 'birthDate', + schemaDataType: W3CSchemaDataType.DATE_TIME, + isRequired: true, + displayName: 'Birth Date' + } + ]; + + const validAttributes = { + birthDate: '2023-01-01T00:00:00Z' + }; + + expect(() => { + validateW3CSchemaAttributes(validAttributes, schemaWithDateTime); + }).not.toThrow(); + }); + + it('should validate boolean attributes correctly', () => { + const schemaWithBoolean = [ + { + attributeName: 'isActive', + schemaDataType: W3CSchemaDataType.BOOLEAN, + isRequired: true, + displayName: 'Is Active' + } + ]; + + const validAttributes = { + isActive: 'true' + }; + + expect(() => { + validateW3CSchemaAttributes(validAttributes, schemaWithBoolean); + }).not.toThrow(); + }); + + it('should throw error for boolean attribute with wrong type', () => { + const schemaWithBoolean = [ + { + attributeName: 'isActive', + schemaDataType: W3CSchemaDataType.BOOLEAN, + isRequired: true, + displayName: 'Is Active' + } + ]; + + const invalidAttributes = { + isActive: 'yes' // should be boolean but all attributes are strings in this interface + }; + + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, schemaWithBoolean); + }).toThrow(BadRequestException); + }); + + it('should handle null and undefined attribute values correctly', () => { + const schemaWithOptional = [ + { + attributeName: 'optionalField', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: false, + displayName: 'Optional Field' + } + ]; + + const attributesWithUndefined = {}; + + expect(() => { + validateW3CSchemaAttributes(attributesWithUndefined, schemaWithOptional); + }).not.toThrow(); + }); + + it('should validate integer types correctly', () => { + const schemaWithInteger = [ + { + attributeName: 'count', + schemaDataType: W3CSchemaDataType.INTEGER, + isRequired: true, + displayName: 'Count' + } + ]; + + const validAttributes = { + count: '42' + }; + + expect(() => { + validateW3CSchemaAttributes(validAttributes, schemaWithInteger); + }).not.toThrow(); + }); + + it('should handle decimal numbers for integer type', () => { + const schemaWithInteger = [ + { + attributeName: 'count', + schemaDataType: W3CSchemaDataType.INTEGER, + isRequired: true, + displayName: 'Count' + } + ]; + + const invalidAttributes = { + count: '42.5' + }; + + // This should pass as our validator accepts any valid number for integer type + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, schemaWithInteger); + }).not.toThrow(); + }); + + it('should handle complex datetime formats', () => { + const schemaWithDateTime = [ + { + attributeName: 'timestamp', + schemaDataType: W3CSchemaDataType.DATE_TIME, + isRequired: true, + displayName: 'Timestamp' + } + ]; + + const validAttributes = { + timestamp: '2023-12-25T10:30:00.000Z' + }; + + expect(() => { + validateW3CSchemaAttributes(validAttributes, schemaWithDateTime); + }).not.toThrow(); + }); + + it('should throw error for invalid datetime format', () => { + const schemaWithDateTime = [ + { + attributeName: 'timestamp', + schemaDataType: W3CSchemaDataType.DATE_TIME, + isRequired: true, + displayName: 'Timestamp' + } + ]; + + const invalidAttributes = { + timestamp: 'not-a-date' + }; + + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, schemaWithDateTime); + }).toThrow(BadRequestException); + }); + }); +}); diff --git a/apps/issuance/libs/helpers/attributes.validator.ts b/apps/issuance/libs/helpers/attributes.validator.ts index 78df951e9..af49f4cb3 100644 --- a/apps/issuance/libs/helpers/attributes.validator.ts +++ b/apps/issuance/libs/helpers/attributes.validator.ts @@ -27,14 +27,41 @@ export function validateW3CSchemaAttributes( } if (attributeValue !== undefined) { - const actualType = typeof attributeValue; - - // Check if the schemaDataType is 'datetime-local' and treat it as a string - if ((W3CSchemaDataType.DATE_TIME === schemaDataType && W3CSchemaDataType.STRING !== actualType) || - (W3CSchemaDataType.DATE_TIME !== schemaDataType && actualType !== schemaDataType)) { - + // All values in IIssuanceAttributes are strings, so we need to validate + // if the string value can be converted to the expected type + let isValidType = true; + + switch (schemaDataType) { + case W3CSchemaDataType.STRING: + // String is always valid + break; + case W3CSchemaDataType.NUMBER: + case W3CSchemaDataType.INTEGER: + // Check if string can be converted to number + if (isNaN(Number(attributeValue))) { + isValidType = false; + } + break; + case W3CSchemaDataType.BOOLEAN: + // Check if string represents a valid boolean + if (attributeValue !== 'true' && attributeValue !== 'false') { + isValidType = false; + } + break; + case W3CSchemaDataType.DATE_TIME: + // Check if string is a valid ISO date + if (isNaN(Date.parse(attributeValue))) { + isValidType = false; + } + break; + default: + // For other types like ARRAY, OBJECT, assume string is valid + break; + } + + if (!isValidType) { mismatchedAttributes.push( - `Attribute ${attributeName} has type ${actualType} but expected type ${schemaDataType}` + `Attribute ${attributeName} has invalid value "${attributeValue}" for expected type ${schemaDataType}` ); } } diff --git a/apps/issuance/src/issuance.service.spec.ts b/apps/issuance/src/issuance.service.spec.ts new file mode 100644 index 000000000..c178a7cca --- /dev/null +++ b/apps/issuance/src/issuance.service.spec.ts @@ -0,0 +1,77 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException } from '@nestjs/common'; +import { validateW3CSchemaAttributes } from '../libs/helpers/attributes.validator'; +import { W3CSchemaDataType } from '@credebl/enum/enum'; + +describe('IssuanceService - Attribute Validation', () => { + describe('validateW3CSchemaAttributes Integration', () => { + it('should validate attributes using the same logic as IssuanceService', () => { + const schemaAttributes = [ + { + attributeName: 'name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + displayName: 'Full Name' + }, + { + attributeName: 'age', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: false, + displayName: 'Age' + }, + { + attributeName: 'email', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + displayName: 'Email' + } + ]; + + const validAttributes = { + name: 'John Doe', + age: '30', + email: 'john@example.com' + }; + + expect(() => { + validateW3CSchemaAttributes(validAttributes, schemaAttributes); + }).not.toThrow(); + }); + + it('should throw error for invalid numeric attribute', () => { + const schemaAttributes = [ + { + attributeName: 'age', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + displayName: 'Age' + } + ]; + + const invalidAttributes = { + age: 'not-a-number' + }; + + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, schemaAttributes); + }).toThrow(BadRequestException); + }); + + it('should throw error for missing required attribute', () => { + const schemaAttributes = [ + { + attributeName: 'name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + displayName: 'Name' + } + ]; + + const invalidAttributes = {}; + + expect(() => { + validateW3CSchemaAttributes(invalidAttributes, schemaAttributes); + }).toThrow(BadRequestException); + }); + }); +}); diff --git a/apps/ledger/libs/helpers/w3c.schema.builder.spec.ts b/apps/ledger/libs/helpers/w3c.schema.builder.spec.ts new file mode 100644 index 000000000..b2129ccbd --- /dev/null +++ b/apps/ledger/libs/helpers/w3c.schema.builder.spec.ts @@ -0,0 +1,442 @@ +import { w3cSchemaBuilder } from './w3c.schema.builder'; +import { W3CSchemaDataType } from '@credebl/enum/enum'; + +describe('W3C Schema Builder - Unit Tests', () => { + describe('Basic Schema Structure', () => { + it('should create valid W3C schema with minimal attributes', () => { + const attributes = [ + { + attributeName: 'name', + displayName: 'Full Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + description: 'The person full name' + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Person Schema', 'A schema for person credentials') as any; + + expect(result).toHaveProperty('$schema', 'https://json-schema.org/draft/2020-12/schema'); + expect(result).toHaveProperty('$id'); + expect(result).toHaveProperty('title', 'Person Schema'); + expect(result).toHaveProperty('description', 'A schema for person credentials'); + expect(result).toHaveProperty('type', 'object'); + expect(result).toHaveProperty('required'); + expect(result).toHaveProperty('properties'); + expect(result).toHaveProperty('$defs'); + }); + + it('should include required W3C credential properties', () => { + const attributes = [ + { + attributeName: 'testField', + displayName: 'Test Field', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Test Schema', 'Test description') as any; + + expect(result.required).toContain('@context'); + expect(result.required).toContain('issuer'); + expect(result.required).toContain('issuanceDate'); + expect(result.required).toContain('type'); + expect(result.required).toContain('credentialSubject'); + + expect(result.properties).toHaveProperty('@context'); + expect(result.properties).toHaveProperty('type'); + expect(result.properties).toHaveProperty('credentialSubject'); + expect(result.properties).toHaveProperty('issuer'); + expect(result.properties).toHaveProperty('issuanceDate'); + }); + + it('should handle empty attributes array', () => { + const result = w3cSchemaBuilder([], 'Empty Schema', 'Schema with no custom attributes') as any; + + expect(result).toHaveProperty('$schema'); + expect(result).toHaveProperty('title', 'Empty Schema'); + expect(result.required).toEqual(['@context', 'issuer', 'issuanceDate', 'type', 'credentialSubject']); + }); + }); + + describe('String Attribute Validation', () => { + it('should apply string length constraints', () => { + const attributes = [ + { + attributeName: 'username', + displayName: 'Username', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + minLength: 3, + maxLength: 20 + } + ]; + + const result = w3cSchemaBuilder(attributes, 'User Schema', 'User credentials') as any; + const credentialSubject = result.$defs.credentialSubject; + const usernameProperty = credentialSubject.properties.username; + + expect(usernameProperty).toHaveProperty('type', 'string'); + expect(usernameProperty).toHaveProperty('minLength', 3); + expect(usernameProperty).toHaveProperty('maxLength', 20); + expect(credentialSubject.required).toContain('username'); + }); + + it('should apply string pattern validation', () => { + const attributes = [ + { + attributeName: 'email', + displayName: 'Email Address', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$' + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Contact Schema', 'Contact information') as any; + const emailProperty = result.$defs.credentialSubject.properties.email; + + expect(emailProperty).toHaveProperty('type', 'string'); + expect(emailProperty).toHaveProperty('pattern', '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$'); + }); + + it('should apply string enum validation', () => { + const attributes = [ + { + attributeName: 'gender', + displayName: 'Gender', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: false, + enum: ['male', 'female', 'other', 'prefer-not-to-say'] + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Person Schema', 'Person details') as any; + const genderProperty = result.$defs.credentialSubject.properties.gender; + + expect(genderProperty).toHaveProperty('type', 'string'); + expect(genderProperty).toHaveProperty('enum', ['male', 'female', 'other', 'prefer-not-to-say']); + }); + + it('should apply content encoding and media type', () => { + const attributes = [ + { + attributeName: 'photo', + displayName: 'Profile Photo', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: false, + contentEncoding: 'base64', + contentMediaType: 'image/jpeg' + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Profile Schema', 'Profile information') as any; + const photoProperty = result.$defs.credentialSubject.properties.photo; + + expect(photoProperty).toHaveProperty('contentEncoding', 'base64'); + expect(photoProperty).toHaveProperty('contentMediaType', 'image/jpeg'); + }); + }); + + describe('Number Attribute Validation', () => { + it('should apply number range constraints', () => { + const attributes = [ + { + attributeName: 'age', + displayName: 'Age', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + minimum: 0, + maximum: 150 + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Age Schema', 'Age verification') as any; + const ageProperty = result.$defs.credentialSubject.properties.age; + + expect(ageProperty).toHaveProperty('type', 'number'); + expect(ageProperty).toHaveProperty('minimum', 0); + expect(ageProperty).toHaveProperty('maximum', 150); + }); + + it('should apply exclusive minimum and maximum', () => { + const attributes = [ + { + attributeName: 'score', + displayName: 'Test Score', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + exclusiveMinimum: 0, + exclusiveMaximum: 100 + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Score Schema', 'Test scores') as any; + const scoreProperty = result.$defs.credentialSubject.properties.score; + + expect(scoreProperty).toHaveProperty('exclusiveMinimum', 0); + expect(scoreProperty).toHaveProperty('exclusiveMaximum', 100); + }); + + it('should apply multipleOf constraint', () => { + const attributes = [ + { + attributeName: 'price', + displayName: 'Price', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + multipleOf: 0.01 + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Price Schema', 'Product pricing') as any; + const priceProperty = result.$defs.credentialSubject.properties.price; + + expect(priceProperty).toHaveProperty('multipleOf', 0.01); + }); + + it('should handle integer type', () => { + const attributes = [ + { + attributeName: 'count', + displayName: 'Item Count', + schemaDataType: W3CSchemaDataType.INTEGER, + isRequired: true, + minimum: 1 + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Inventory Schema', 'Inventory management') as any; + const countProperty = result.$defs.credentialSubject.properties.count; + + expect(countProperty).toHaveProperty('type', 'integer'); + expect(countProperty).toHaveProperty('minimum', 1); + }); + }); + + describe('DateTime Attribute Validation', () => { + it('should create datetime field with correct format', () => { + const attributes = [ + { + attributeName: 'birthDate', + displayName: 'Birth Date', + schemaDataType: W3CSchemaDataType.DATE_TIME, + isRequired: true + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Birth Schema', 'Birth information') as any; + const birthDateProperty = result.$defs.credentialSubject.properties.birthDate; + + expect(birthDateProperty).toHaveProperty('type', 'string'); + expect(birthDateProperty).toHaveProperty('format', 'date-time'); + }); + }); + + describe('Boolean Attribute Validation', () => { + it('should create boolean field', () => { + const attributes = [ + { + attributeName: 'isVerified', + displayName: 'Verification Status', + schemaDataType: W3CSchemaDataType.BOOLEAN, + isRequired: false + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Verification Schema', 'Verification status') as any; + const isVerifiedProperty = result.$defs.credentialSubject.properties.isVerified; + + expect(isVerifiedProperty).toHaveProperty('type', 'boolean'); + }); + }); + + describe('Array Attribute Validation', () => { + it('should create array with object items', () => { + const attributes = [ + { + attributeName: 'skills', + displayName: 'Skills', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + minItems: 1, + maxItems: 10, + uniqueItems: true, + items: [ + { + attributeName: 'skillName', + displayName: 'Skill Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }, + { + attributeName: 'level', + displayName: 'Skill Level', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + enum: ['beginner', 'intermediate', 'advanced', 'expert'] + } + ] + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Skills Schema', 'Professional skills') as any; + const skillsProperty = result.$defs.credentialSubject.properties.skills; + + expect(skillsProperty).toHaveProperty('type', 'array'); + expect(skillsProperty).toHaveProperty('minItems', 1); + expect(skillsProperty).toHaveProperty('maxItems', 10); + expect(skillsProperty).toHaveProperty('uniqueItems', true); + expect(skillsProperty.items).toHaveProperty('type', 'object'); + expect(skillsProperty.items.properties).toHaveProperty('skillName'); + expect(skillsProperty.items.properties).toHaveProperty('level'); + expect(skillsProperty.items.required).toEqual(['skillName', 'level']); + }); + + it('should handle array without items definition', () => { + const attributes = [ + { + attributeName: 'tags', + displayName: 'Tags', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + minItems: 0, + maxItems: 5 + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Tags Schema', 'Content tags') as any; + const tagsProperty = result.$defs.credentialSubject.properties.tags; + + expect(tagsProperty).toHaveProperty('type', 'array'); + expect(tagsProperty).toHaveProperty('minItems', 0); + expect(tagsProperty).toHaveProperty('maxItems', 5); + }); + }); + + describe('Complex Schema Validation', () => { + it('should handle mixed attribute types', () => { + const attributes = [ + { + attributeName: 'personalInfo', + displayName: 'Personal Information', + schemaDataType: W3CSchemaDataType.OBJECT, + isRequired: true, + properties: { + name: { + attributeName: 'name', + displayName: 'Full Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + minLength: 2, + maxLength: 100 + }, + age: { + attributeName: 'age', + displayName: 'Age', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + minimum: 0, + maximum: 150 + } + } + }, + { + attributeName: 'certifications', + displayName: 'Certifications', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + items: [ + { + attributeName: 'name', + displayName: 'Certification Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + }, + { + attributeName: 'issueDate', + displayName: 'Issue Date', + schemaDataType: W3CSchemaDataType.DATE_TIME, + isRequired: true + } + ] + }, + { + attributeName: 'isActive', + displayName: 'Active Status', + schemaDataType: W3CSchemaDataType.BOOLEAN, + isRequired: true + } + ]; + + const result = w3cSchemaBuilder(attributes, 'Complex Schema', 'Complex credential schema') as any; + const credentialSubject = result.$defs.credentialSubject; + + expect(credentialSubject.properties).toHaveProperty('personalInfo'); + expect(credentialSubject.properties).toHaveProperty('certifications'); + expect(credentialSubject.properties).toHaveProperty('isActive'); + expect(credentialSubject.required).toContain('personalInfo'); + expect(credentialSubject.required).toContain('isActive'); + expect(credentialSubject.required).not.toContain('certifications'); + }); + + it('should generate valid schema ID based on schema name', () => { + const result = w3cSchemaBuilder([], 'My Test Schema Name', 'Test description') as any; + + expect(result.$id).toContain('my-test-schema-name'); + expect(result.$id).toMatch(/^https:\/\/example\.com\/schemas\/[a-z0-9-]+$/); + }); + + it('should include proper $defs structure', () => { + const result = w3cSchemaBuilder([], 'Test Schema', 'Test description') as any; + + expect(result.$defs).toHaveProperty('context'); + expect(result.$defs).toHaveProperty('credentialSubject'); + expect(result.$defs).toHaveProperty('credentialSchema'); + expect(result.$defs).toHaveProperty('credentialStatus'); + expect(result.$defs).toHaveProperty('idAndType'); + expect(result.$defs).toHaveProperty('uriOrId'); + }); + }); + + describe('Error Handling and Edge Cases', () => { + it('should handle null or undefined attributes', () => { + expect(() => { + w3cSchemaBuilder(null as any, 'Test Schema', 'Test description'); + }).not.toThrow(); + + expect(() => { + w3cSchemaBuilder(undefined as any, 'Test Schema', 'Test description'); + }).not.toThrow(); + }); + + it('should handle attributes with missing properties', () => { + const incompleteAttributes = [ + { + attributeName: 'testField', + // Missing displayName, schemaDataType, isRequired + } as any + ]; + + expect(() => { + w3cSchemaBuilder(incompleteAttributes, 'Test Schema', 'Test description'); + }).not.toThrow(); + }); + + it('should handle special characters in schema name', () => { + const result = w3cSchemaBuilder([], 'Schema with Special Ch@r$!', 'Test description') as any; + + expect(result.$id).toBeDefined(); + expect(result.title).toBe('Schema with Special Ch@r$!'); + }); + + it('should handle very long schema names', () => { + const longName = 'A'.repeat(1000); + const result = w3cSchemaBuilder([], longName, 'Test description') as any; + + expect(result.title).toBe(longName); + expect(result.$id).toBeDefined(); + }); + }); +}); diff --git a/apps/ledger/libs/helpers/w3c.schema.builder.ts b/apps/ledger/libs/helpers/w3c.schema.builder.ts index da967ff39..ff4928c2f 100644 --- a/apps/ledger/libs/helpers/w3c.schema.builder.ts +++ b/apps/ledger/libs/helpers/w3c.schema.builder.ts @@ -1,15 +1,17 @@ import { IW3CAttributeValue } from '@credebl/common/interfaces/interface'; import { ISchemaAttributesFormat } from 'apps/ledger/src/schema/interfaces/schema-payload.interface'; import { IProductSchema } from 'apps/ledger/src/schema/interfaces/schema.interface'; -import ExclusiveMinimum from 'libs/validations/exclusiveMinimum'; -import MaxItems from 'libs/validations/maxItems'; -import MaxLength from 'libs/validations/maxLength'; -import Minimum from 'libs/validations/minimum'; -import MinItems from 'libs/validations/minItems'; -import MinLength from 'libs/validations/minLength'; -import MultipleOf from 'libs/validations/multipleOf'; -import Pattern from 'libs/validations/pattern'; -import UniqueItems from 'libs/validations/uniqueItems'; +import ExclusiveMinimum from '../../../../libs/validations/exclusiveMinimum'; +import ExclusiveMaximum from '../../../../libs/validations/exclusiveMaximum'; +import MaxItems from '../../../../libs/validations/maxItems'; +import MaxLength from '../../../../libs/validations/maxLength'; +import Maximum from '../../../../libs/validations/maximum'; +import Minimum from '../../../../libs/validations/minimum'; +import MinItems from '../../../../libs/validations/minItems'; +import MinLength from '../../../../libs/validations/minLength'; +import MultipleOf from '../../../../libs/validations/multipleOf'; +import Pattern from '../../../../libs/validations/pattern'; +import UniqueItems from '../../../../libs/validations/uniqueItems'; export function w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: string, description: string): object { // Function to apply validations based on attribute properties @@ -32,6 +34,20 @@ export function w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: s const validation = new Pattern(attribute.pattern); validation.json(context); } + + // Add enum validation + if (attribute.enum !== undefined) { + context.enum = attribute.enum; + } + + // Add content encoding and media type + if (attribute.contentEncoding !== undefined) { + context.contentEncoding = attribute.contentEncoding; + } + + if (attribute.contentMediaType !== undefined) { + context.contentMediaType = attribute.contentMediaType; + } } // Apply number validations @@ -41,11 +57,21 @@ export function w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: s validation.json(context); } + if (attribute.maximum !== undefined) { + const validation = new Maximum(attribute.maximum); + validation.json(context); + } + if (attribute.exclusiveMinimum !== undefined) { const validation = new ExclusiveMinimum(attribute.exclusiveMinimum); validation.json(context); } + if (attribute.exclusiveMaximum !== undefined) { + const validation = new ExclusiveMaximum(attribute.exclusiveMaximum); + validation.json(context); + } + if (attribute.multipleOf !== undefined) { const validation = new MultipleOf(attribute.multipleOf); validation.json(context); @@ -85,6 +111,11 @@ export function w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: s attrs.forEach((attribute) => { const { attributeName, schemaDataType, isRequired, displayName, description } = attribute; + // Skip attributes with missing essential properties + if (!attributeName || !schemaDataType) { + return; + } + // Add to required array if isRequired is true if (isRequired) { required.push(attributeName); @@ -209,6 +240,9 @@ export function w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: s if (0 < arrayItemRequired.length) { properties[attributeName].items.required = arrayItemRequired; } + } else if ('array' === schemaDataType.toLowerCase()) { + // Handle arrays without items definition + properties[attributeName] = applyValidations(attribute, baseProperty); } else if ('object' === schemaDataType.toLowerCase() && attribute.properties) { const nestedProperties = {}; const nestedRequired = []; diff --git a/apps/ledger/src/schema/schema.spec.ts b/apps/ledger/src/schema/schema.spec.ts new file mode 100644 index 000000000..717afa93a --- /dev/null +++ b/apps/ledger/src/schema/schema.spec.ts @@ -0,0 +1,425 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { W3CSchemaDataType, JSONSchemaType } from '@credebl/enum/enum'; +import { ICreateW3CSchema, ISchemaData } from './interfaces/schema.interface'; + +// Mock the w3c schema builder +const mockW3cSchemaBuilder = jest.fn(); +jest.mock('../../libs/helpers/w3c.schema.builder', () => ({ + w3cSchemaBuilder: mockW3cSchemaBuilder +})); + +describe('Schema Creation - W3C Schema Validation', () => { + let mockSchemaService: any; + + beforeEach(() => { + mockSchemaService = { + createW3CSchema: jest.fn(), + _createW3CSchema: jest.fn(), + _createW3CledgerAgnostic: jest.fn(), + }; + }); + + describe('W3C Schema Creation', () => { + const mockOrgId = 'test-org-id'; + const mockUserId = 'test-user-id'; + const mockAlias = 'test-alias'; + + const validW3CSchemaPayload: ICreateW3CSchema = { + schemaName: 'Test Identity Schema', + description: 'A test schema for identity verification', + schemaType: JSONSchemaType.POLYGON_W3C, + attributes: [ + { + attributeName: 'name', + displayName: 'Full Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + description: 'The full name of the person' + }, + { + attributeName: 'age', + displayName: 'Age', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: false, + minimum: 0, + maximum: 150 + }, + { + attributeName: 'email', + displayName: 'Email Address', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$' + } + ] + }; + + it('should create W3C schema successfully with valid payload', async () => { + const mockSchemaResult: ISchemaData = { + createDateTime: new Date(), + createdBy: mockUserId, + name: 'Test Identity Schema', + version: '1.0', + attributes: JSON.stringify(validW3CSchemaPayload.attributes), + schemaLedgerId: 'schema-123', + publisherDid: 'did:test:12345', + issuerId: 'did:test:12345', + orgId: mockOrgId + }; + + mockSchemaService.createW3CSchema.mockResolvedValue(mockSchemaResult); + + const result = await mockSchemaService.createW3CSchema(mockOrgId, validW3CSchemaPayload, mockUserId, mockAlias); + + expect(result).toEqual(mockSchemaResult); + expect(mockSchemaService.createW3CSchema).toHaveBeenCalledWith(mockOrgId, validW3CSchemaPayload, mockUserId, mockAlias); + }); + + it('should throw error when agent details are not found', async () => { + mockSchemaService.createW3CSchema.mockRejectedValue( + new NotFoundException('Agent details not found') + ); + + await expect( + mockSchemaService.createW3CSchema(mockOrgId, validW3CSchemaPayload, mockUserId, mockAlias) + ).rejects.toThrow(NotFoundException); + }); + + it('should throw error when schema builder fails', async () => { + mockSchemaService.createW3CSchema.mockRejectedValue( + new BadRequestException('Error while creating schema JSON') + ); + + await expect( + mockSchemaService.createW3CSchema(mockOrgId, validW3CSchemaPayload, mockUserId, mockAlias) + ).rejects.toThrow(BadRequestException); + }); + + it('should handle invalid schema type', async () => { + const invalidSchemaPayload = { + ...validW3CSchemaPayload, + schemaType: 'INVALID_TYPE' as JSONSchemaType + }; + + mockSchemaService.createW3CSchema.mockRejectedValue( + new BadRequestException('Invalid schema type') + ); + + await expect( + mockSchemaService.createW3CSchema(mockOrgId, invalidSchemaPayload, mockUserId, mockAlias) + ).rejects.toThrow(BadRequestException); + }); + + it('should validate required attributes exist', async () => { + const schemaWithoutRequiredFields = { + ...validW3CSchemaPayload, + attributes: [ + { + attributeName: 'optionalField', + displayName: 'Optional Field', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: false + } + ] + }; + + const mockResult: ISchemaData = { + createDateTime: new Date(), + createdBy: mockUserId, + name: schemaWithoutRequiredFields.schemaName, + version: '1.0', + attributes: JSON.stringify(schemaWithoutRequiredFields.attributes), + schemaLedgerId: 'schema-123', + publisherDid: 'did:test:12345', + issuerId: 'did:test:12345', + orgId: mockOrgId + }; + + mockSchemaService.createW3CSchema.mockResolvedValue(mockResult); + + const result = await mockSchemaService.createW3CSchema(mockOrgId, schemaWithoutRequiredFields, mockUserId, mockAlias); + expect(result).toBeDefined(); + }); + }); + + describe('W3C Schema Builder Validation', () => { + beforeEach(() => { + mockW3cSchemaBuilder.mockClear(); + }); + + it('should build valid W3C schema structure', () => { + const mockAttributes = [ + { + attributeName: 'name', + displayName: 'Full Name', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true, + minLength: 1, + maxLength: 100 + }, + { + attributeName: 'birthDate', + displayName: 'Birth Date', + schemaDataType: W3CSchemaDataType.DATE_TIME, + isRequired: true + } + ]; + + const expectedSchema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: expect.stringContaining('test-schema'), + title: 'Test Schema', + description: 'A test schema', + type: 'object', + required: expect.arrayContaining(['@context', 'issuer', 'issuanceDate', 'type', 'credentialSubject']), + properties: expect.objectContaining({ + '@context': expect.any(Object), + 'type': expect.any(Object), + 'credentialSubject': expect.any(Object) + }) + }; + + mockW3cSchemaBuilder.mockReturnValue(expectedSchema); + + const result = mockW3cSchemaBuilder(mockAttributes, 'Test Schema', 'A test schema'); + + expect(mockW3cSchemaBuilder).toHaveBeenCalledWith(mockAttributes, 'Test Schema', 'A test schema'); + expect(result).toEqual(expectedSchema); + }); + + it('should handle empty attributes array', () => { + const emptySchema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + properties: {}, + required: ['id'] + }; + + mockW3cSchemaBuilder.mockReturnValue(emptySchema); + + const result = mockW3cSchemaBuilder([], 'Empty Schema', 'Schema with no attributes'); + + expect(result).toEqual(emptySchema); + }); + + it('should validate string attribute with constraints', () => { + const stringAttribute = { + attributeName: 'description', + displayName: 'Description', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: false, + minLength: 10, + maxLength: 500, + pattern: '^[a-zA-Z0-9\\s]+$' + }; + + const expectedProperty = { + type: 'string', + title: 'Description', + description: 'description field', + minLength: 10, + maxLength: 500, + pattern: '^[a-zA-Z0-9\\s]+$' + }; + + mockW3cSchemaBuilder.mockReturnValue({ + properties: { description: expectedProperty } + }); + + const result = mockW3cSchemaBuilder([stringAttribute], 'Test', 'Test schema'); + + expect(result.properties.description).toEqual(expectedProperty); + }); + + it('should validate number attribute with constraints', () => { + const numberAttribute = { + attributeName: 'score', + displayName: 'Test Score', + schemaDataType: W3CSchemaDataType.NUMBER, + isRequired: true, + minimum: 0, + maximum: 100, + multipleOf: 0.1 + }; + + const expectedProperty = { + type: 'number', + title: 'Test Score', + description: 'score field', + minimum: 0, + maximum: 100, + multipleOf: 0.1 + }; + + mockW3cSchemaBuilder.mockReturnValue({ + properties: { score: expectedProperty }, + required: ['score'] + }); + + const result = mockW3cSchemaBuilder([numberAttribute], 'Test', 'Test schema'); + + expect(result.properties.score).toEqual(expectedProperty); + expect(result.required).toContain('score'); + }); + + it('should validate array attribute with items', () => { + const arrayAttribute = { + attributeName: 'skills', + displayName: 'Skills', + schemaDataType: W3CSchemaDataType.ARRAY, + isRequired: false, + minItems: 1, + maxItems: 10, + uniqueItems: true, + items: [ + { + attributeName: 'skill', + displayName: 'Skill', + schemaDataType: W3CSchemaDataType.STRING, + isRequired: true + } + ] + }; + + const expectedProperty = { + type: 'array', + title: 'Skills', + description: 'skills field', + minItems: 1, + maxItems: 10, + uniqueItems: true, + items: { + type: 'object', + properties: { + skill: { + type: 'string', + title: 'Skill', + description: 'skill field' + } + }, + required: ['skill'] + } + }; + + mockW3cSchemaBuilder.mockReturnValue({ + properties: { skills: expectedProperty } + }); + + const result = mockW3cSchemaBuilder([arrayAttribute], 'Test', 'Test schema'); + + expect(result.properties.skills).toEqual(expectedProperty); + }); + + it('should validate datetime attribute', () => { + const datetimeAttribute = { + attributeName: 'createdAt', + displayName: 'Created At', + schemaDataType: W3CSchemaDataType.DATE_TIME, + isRequired: true + }; + + const expectedProperty = { + type: 'string', + format: 'date-time', + title: 'Created At', + description: 'createdAt field' + }; + + mockW3cSchemaBuilder.mockReturnValue({ + properties: { createdAt: expectedProperty }, + required: ['createdAt'] + }); + + const result = mockW3cSchemaBuilder([datetimeAttribute], 'Test', 'Test schema'); + + expect(result.properties.createdAt).toEqual(expectedProperty); + }); + + it('should validate boolean attribute', () => { + const booleanAttribute = { + attributeName: 'isVerified', + displayName: 'Is Verified', + schemaDataType: W3CSchemaDataType.BOOLEAN, + isRequired: false + }; + + const expectedProperty = { + type: 'boolean', + title: 'Is Verified', + description: 'isVerified field' + }; + + mockW3cSchemaBuilder.mockReturnValue({ + properties: { isVerified: expectedProperty } + }); + + const result = mockW3cSchemaBuilder([booleanAttribute], 'Test', 'Test schema'); + + expect(result.properties.isVerified).toEqual(expectedProperty); + }); + }); + + describe('Error Handling in Schema Creation', () => { + it('should handle network errors during schema upload', async () => { + mockSchemaService._createW3CledgerAgnostic.mockRejectedValue( + new Error('Network connection failed') + ); + + await expect( + mockSchemaService._createW3CledgerAgnostic({}) + ).rejects.toThrow('Network connection failed'); + }); + + it('should handle invalid DID format', async () => { + const invalidDidPayload = { + schemaName: 'Test Schema', + description: 'Test description', + schemaType: JSONSchemaType.POLYGON_W3C, + attributes: [] + }; + + mockSchemaService.createW3CSchema.mockRejectedValue( + new BadRequestException('Invalid DID format') + ); + + await expect( + mockSchemaService.createW3CSchema('org-id', invalidDidPayload, 'user-id', 'alias') + ).rejects.toThrow(BadRequestException); + }); + + it('should handle schema server unavailable', async () => { + mockSchemaService._createW3CledgerAgnostic.mockRejectedValue( + new BadRequestException('Schema server unavailable') + ); + + await expect( + mockSchemaService._createW3CledgerAgnostic({}) + ).rejects.toThrow(BadRequestException); + }); + + it('should handle schema builder errors', async () => { + mockW3cSchemaBuilder.mockImplementation(() => { + throw new Error('Invalid schema attributes'); + }); + + expect(() => { + mockW3cSchemaBuilder([], 'Test', 'Description'); + }).toThrow('Invalid schema attributes'); + }); + + it('should handle missing agent endpoints', async () => { + mockSchemaService.createW3CSchema.mockRejectedValue( + new NotFoundException('Agent endpoint not configured') + ); + + await expect( + mockSchemaService.createW3CSchema('org-id', { + schemaName: 'Test', + description: 'Test', + schemaType: JSONSchemaType.POLYGON_W3C, + attributes: [] + }, 'user-id', 'alias') + ).rejects.toThrow(NotFoundException); + }); + }); +});