import { ManifestValidator } from '../../src/validator/manifest-validator'; import { AllowedModule, Manifest } from '../../src/types/common'; import { CBFileSystem } from '../../src/node/file-system'; // Mock fs module jest.mock('fs'); type MockedCBFileSystem = CBFileSystem & { existsSync: jest.MockedFunction; readdirSync: jest.MockedFunction; mkdirSync: jest.MockedFunction; copyFileSync: jest.MockedFunction; statSync: jest.MockedFunction; readFileSync: jest.MockedFunction; writeFileSync: jest.MockedFunction; unlinkSync: jest.MockedFunction; rmdirSync: jest.MockedFunction; createWriteStream: jest.MockedFunction; }; // Mock internal logger jest.mock('../../src/logger/internal-logger', () => ({ __logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } })); // Test class that extends ManifestValidator to access protected methods class TestableManifestValidator extends ManifestValidator { public testValidate(manifest: Manifest): Manifest { this.validate(manifest); return manifest; } public testValidateBasicStructure(manifest: Manifest): void { this.validateBasicStructure(manifest); } public testValidateEvents(manifest: Manifest): void { this.validateEvents(manifest); } public testValidateDependencies(manifest: Manifest): void { this.validateDependencies(manifest); } public testValidateMetadata(manifest: Manifest): void { this.validateMetadata(manifest); } } describe('manifest-validator', () => { let validator: TestableManifestValidator; let allowedModules: AllowedModule[]; let mockFileSystem: MockedCBFileSystem; beforeEach(() => { allowedModules = [ { name: 'lodash', version: '^4.17.21', type: 'public' }, { name: 'axios', version: '^1.0.0', type: 'public' }, { name: 'express', version: '4.18.0', type: 'public' } ]; mockFileSystem = { existsSync: jest.fn() as any, mkdirSync: jest.fn() as any, writeFileSync: jest.fn() as any, readFileSync: jest.fn((_path: string, _encoding: string) => '{}') as any, readdirSync: jest.fn(() => []) as any, unlinkSync: jest.fn() as any, rmdirSync: jest.fn() as any, statSync: jest.fn(() => ({ isDirectory: () => true, size: 0 })) as any, createWriteStream: jest.fn(() => ({} as any)) as any, copyFileSync: jest.fn() as any, lstatSync: jest.fn(() => ({ isSymbolicLink: () => false } as any)) as any, renameSync: jest.fn() as any, cpSync: jest.fn() as any }; validator = new TestableManifestValidator(allowedModules, mockFileSystem); jest.clearAllMocks(); }); describe('ManifestValidator class', () => { describe('validate (protected method)', () => { it('should validate a complete valid manifest', () => { const manifest: Manifest = { events: { 'customer_created': { handler: 'handler/customerCreated.js' }, 'subscription_created': { handler: 'handler/subscriptionCreated.js' } }, dependencies: { 'lodash': '^4.17.21', 'axios': '1.0.0' } }; const result = validator.testValidate(manifest); expect(result).toEqual(manifest); }); it('should validate manifest without dependencies', () => { const manifest: Manifest = { events: { 'customer_created': { handler: 'handler/customerCreated.js' } } }; const result = validator.testValidate(manifest); expect(result).toEqual(manifest); }); it('should validate manifest with empty events', () => { const manifest: Manifest = { events: {} }; const result = validator.testValidate(manifest); expect(result).toEqual(manifest); }); }); describe('validateBasicStructure (protected method)', () => { it('should throw error when manifest is null', () => { expect(() => validator.testValidateBasicStructure(null as any)).toThrow( 'Manifest must be a valid object' ); }); it('should throw error when manifest is undefined', () => { expect(() => validator.testValidateBasicStructure(undefined as any)).toThrow( 'Manifest must be a valid object' ); }); it('should throw error when manifest is not an object', () => { expect(() => validator.testValidateBasicStructure('not an object' as any)).toThrow( 'Manifest must be a valid object' ); }); it('should throw error when events is not an object', () => { const manifest = { events: 'not an object' } as any; expect(() => validator.testValidateBasicStructure(manifest)).toThrow( 'Events mapping must be an object' ); }); it('should throw error when dependencies is not an object', () => { const manifest = { events: {}, dependencies: 'not an object' } as any; expect(() => validator.testValidateBasicStructure(manifest)).toThrow( 'Dependencies must be an object' ); }); it('should pass validation for valid basic structure', () => { const manifest: Manifest = { events: { 'customer_created': { handler: 'handler/customerCreated.js' } }, dependencies: { 'lodash': '^4.17.21' } }; expect(() => validator.testValidateBasicStructure(manifest)).not.toThrow(); }); }); describe('validateEvents (protected method)', () => { it('should throw error when events is missing', () => { const manifest = {} as Manifest; expect(() => validator.testValidateEvents(manifest)).toThrow( 'Invalid manifest: missing "events" mapping' ); }); it('should throw error when event handler is null', () => { const manifest = { events: { 'customer_created': null } } as any; expect(() => validator.testValidateEvents(manifest)).toThrow( 'Handler name for event "customer_created" must be a non-empty string' ); }); it('should throw error when event handler is not an object', () => { const manifest = { events: { 'customer_created': 'not an object' } } as any; expect(() => validator.testValidateEvents(manifest)).toThrow( 'Handler name for event "customer_created" must be a non-empty string' ); }); it('should pass validation for event handler with empty string (current implementation limitation)', () => { const manifest = { events: { 'customer_created': { handler: '' } } } as any; // Current implementation only checks if handler is an object, not the handler property expect(() => validator.testValidateEvents(manifest)).not.toThrow(); }); it('should validate valid events configuration', () => { const manifest: Manifest = { events: { 'customer_created': { handler: 'handler/customerCreated.js' }, 'subscription_created': { handler: 'handler/subscriptionCreated.js' } } }; expect(() => validator.testValidateEvents(manifest)).not.toThrow(); }); it('should validate events with complex handler paths', () => { const manifest: Manifest = { events: { 'customer_created': { handler: './src/handlers/customer/customerCreated.js' }, 'subscription_created': { handler: './dist/handlers/subscription/subscriptionCreated.js' } } }; expect(() => validator.testValidateEvents(manifest)).not.toThrow(); }); it('should validate events with various event types', () => { const manifest: Manifest = { events: { 'customer.created': { handler: 'handler/customerCreated.js' }, 'subscription.updated': { handler: 'handler/subscriptionUpdated.js' }, 'invoice.paid': { handler: 'handler/invoicePaid.js' } } }; expect(() => validator.testValidateEvents(manifest)).not.toThrow(); }); }); describe('validateDependencies (protected method)', () => { it('should handle manifest without dependencies', () => { const manifest: Manifest = { events: { 'customer_created': { handler: 'handler/customerCreated.js' } } }; expect(() => validator.testValidateDependencies(manifest)).not.toThrow(); }); it('should throw error when dependencies is not an object', () => { const manifest = { events: {}, dependencies: 'not an object' } as any; expect(() => validator.testValidateDependencies(manifest)).toThrow( 'Invalid manifest: "dependencies" must be an object' ); }); it('should throw error when dependency name is not a string', () => { const manifest = { events: {}, dependencies: { 123: '1.0.0' } } as any; expect(() => validator.testValidateDependencies(manifest)).toThrow( 'Dependency "123" is not allowed in this environment' ); }); it('should throw error when dependency version is not a string', () => { const manifest = { events: {}, dependencies: { 'lodash': 123 } } as any; expect(() => validator.testValidateDependencies(manifest)).toThrow( 'Invalid dependency in manifest: "lodash" must have string name and version' ); }); it('should throw error when dependency is not allowed', () => { const manifest: Manifest = { events: {}, dependencies: { 'forbidden-module': '1.0.0' } }; expect(() => validator.testValidateDependencies(manifest)).toThrow( 'Dependency "forbidden-module" is not allowed in this environment' ); }); it('should throw error when dependency version is invalid semver', () => { const manifest: Manifest = { events: {}, dependencies: { 'lodash': 'invalid-version' } }; expect(() => validator.testValidateDependencies(manifest)).toThrow( 'Invalid version range "invalid-version" for dependency "lodash"' ); }); it('should validate allowed dependencies', () => { const manifest: Manifest = { events: {}, dependencies: { 'lodash': '^4.17.21', 'axios': '1.0.0' } }; expect(() => validator.testValidateDependencies(manifest)).not.toThrow(); }); it('should throw error when allowed module has no version constraint', () => { const manifest: Manifest = { events: {}, dependencies: { 'axios': '1.0.0' } }; // Create a validator with axios without version constraint const validatorWithoutVersion = new TestableManifestValidator([ { name: 'axios', type: 'public' } // No version constraint ], mockFileSystem); expect(() => validatorWithoutVersion.testValidateDependencies(manifest)).toThrow( 'Configuration error: Allowed module "axios" does not have a version constraint' ); }); it('should validate dependencies with various semver ranges', () => { const manifest: Manifest = { events: {}, dependencies: { 'lodash': '^4.17.21', 'axios': '~1.0.0', 'express': '>=4.0.0 <5.0.0' } }; expect(() => validator.testValidateDependencies(manifest)).not.toThrow(); }); it('should handle empty dependencies object', () => { const manifest: Manifest = { events: {}, dependencies: {} }; expect(() => validator.testValidateDependencies(manifest)).not.toThrow(); }); }); describe('validateMetadata (protected method)', () => { it('should handle manifest with few dependencies', () => { const manifest: Manifest = { events: {}, dependencies: { 'lodash': '^4.17.21', 'axios': '1.0.0' } }; expect(() => validator.testValidateMetadata(manifest)).not.toThrow(); }); it('should handle manifest with many dependencies (logs warning)', () => { const manifest: Manifest = { events: {}, dependencies: { 'lodash': '^4.17.21', 'axios': '1.0.0', 'express': '4.18.0', 'dep4': '1.0.0', 'dep5': '1.0.0', 'dep6': '1.0.0', 'dep7': '1.0.0', 'dep8': '1.0.0', 'dep9': '1.0.0', 'dep10': '1.0.0', 'dep11': '1.0.0' // More than 10 dependencies } }; // validateMetadata only logs a warning, doesn't throw expect(() => validator.testValidateMetadata(manifest)).not.toThrow(); }); it('should handle manifest without dependencies', () => { const manifest: Manifest = { events: { 'customer_created': { handler: 'handler/customerCreated.js' } } }; expect(() => validator.testValidateMetadata(manifest)).not.toThrow(); }); it('should handle manifest with empty events and dependencies', () => { const manifest: Manifest = { events: {}, dependencies: {} }; expect(() => validator.testValidateMetadata(manifest)).not.toThrow(); }); }); }); describe('validateAndGetManifestFile', () => { it('should throw error when manifest file does not exist', () => { const manifestPath = '/test/manifest.json'; mockFileSystem.existsSync.mockReturnValue(false); expect(() => validator.validateAndGetManifestFile(manifestPath)).toThrow( 'manifest.json not found in user code directory' ); }); it('should successfully read and validate manifest file', () => { const manifestPath = '/test/manifest.json'; const manifestContent = JSON.stringify({ events: { 'customer_created': { handler: 'handler/customerCreated.js' } }, dependencies: { 'lodash': '^4.17.21' } }); mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(manifestContent); const result = validator.validateAndGetManifestFile(manifestPath); expect(mockFileSystem.existsSync).toHaveBeenCalledWith(manifestPath); expect(mockFileSystem.readFileSync).toHaveBeenCalledWith(manifestPath, 'utf8'); expect(result).toEqual({ events: { 'customer_created': { handler: 'handler/customerCreated.js' } }, dependencies: { 'lodash': '^4.17.21' } }); }); it('should throw error when manifest file contains invalid JSON', () => { const manifestPath = '/test/manifest.json'; const invalidJson = '{ "events": { "customer_created": { "handler": "handler/customerCreated.js" }'; // Missing closing brace mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(invalidJson); expect(() => validator.validateAndGetManifestFile(manifestPath)).toThrow( 'Failed to parse manifest file:' ); }); it('should throw error when manifest file contains invalid manifest structure', () => { const manifestPath = '/test/manifest.json'; const invalidManifest = JSON.stringify({ events: 'not an object' // Invalid structure }); mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(invalidManifest); expect(() => validator.validateAndGetManifestFile(manifestPath)).toThrow( 'Events mapping must be an object' ); }); it('should throw error when manifest file contains invalid dependencies', () => { const manifestPath = '/test/manifest.json'; const invalidManifest = JSON.stringify({ events: { 'customer_created': { handler: 'handler/customerCreated.js' } }, dependencies: { 'forbidden-module': '1.0.0' // Not in allowed modules } }); mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(invalidManifest); expect(() => validator.validateAndGetManifestFile(manifestPath)).toThrow( 'Dependency "forbidden-module" is not allowed in this environment' ); }); it('should handle file system read errors', () => { const manifestPath = '/test/manifest.json'; const readError = new Error('Permission denied'); mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockImplementation(() => { throw readError; }); expect(() => validator.validateAndGetManifestFile(manifestPath)).toThrow( 'Failed to read manifest file: Permission denied' ); }); it('should handle manifest with empty events and dependencies', () => { const manifestPath = '/test/manifest.json'; const manifestContent = JSON.stringify({ events: {}, dependencies: {} }); mockFileSystem.existsSync.mockReturnValue(true); mockFileSystem.readFileSync.mockReturnValue(manifestContent); const result = validator.validateAndGetManifestFile(manifestPath); expect(result).toEqual({ events: {}, dependencies: {} }); }); }); describe('constructor', () => { it('should create validator with allowed modules', () => { const validator = new TestableManifestValidator(allowedModules, mockFileSystem); expect(validator).toBeInstanceOf(ManifestValidator); }); it('should create validator with empty allowed modules', () => { const validator = new TestableManifestValidator([], mockFileSystem); expect(validator).toBeInstanceOf(ManifestValidator); }); it('should create validator with undefined allowed modules', () => { const validator = new TestableManifestValidator(undefined as any, mockFileSystem); expect(validator).toBeInstanceOf(ManifestValidator); }); }); describe('edge cases', () => { it('should handle manifest with complex event types', () => { const manifest: Manifest = { events: { 'customer.created': { handler: 'handler/customerCreated.js' }, 'subscription.updated': { handler: 'handler/subscriptionUpdated.js' }, 'invoice.paid': { handler: 'handler/invoicePaid.js' } } }; expect(() => validator.testValidate(manifest)).not.toThrow(); }); it('should handle manifest with complex handler paths', () => { const manifest: Manifest = { events: { 'customer_created': { handler: './src/handlers/customer/customerCreated.js' }, 'subscription_created': { handler: './dist/handlers/subscription/subscriptionCreated.js' } } }; expect(() => validator.testValidate(manifest)).not.toThrow(); }); it('should handle manifest with various semver ranges', () => { const manifest: Manifest = { events: {}, dependencies: { 'lodash': '^4.17.21', 'axios': '~1.0.0', 'express': '>=4.0.0 <5.0.0' } }; expect(() => validator.testValidate(manifest)).not.toThrow(); }); it('should handle manifest with special characters in event names', () => { const manifest: Manifest = { events: { 'customer_created_v2': { handler: 'handler/customerCreated.js' }, 'subscription-created': { handler: 'handler/subscriptionCreated.js' }, 'invoice.paid': { handler: 'handler/invoicePaid.js' } } }; expect(() => validator.testValidate(manifest)).not.toThrow(); }); it('should handle manifest with very long handler paths', () => { const manifest: Manifest = { events: { 'customer_created': { handler: './very/long/path/to/handlers/customer/created/handler.js' } } }; expect(() => validator.testValidate(manifest)).not.toThrow(); }); }); });