UNPKG

@fiqch/n8n-nodes-fiq-opcua

Version:

n8n custom node for reading and writing OPC UA data (FactoryIQ)

424 lines 27 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const OpcUaCredentialsApi_credentials_1 = require("../credentials/OpcUaCredentialsApi.credentials"); const FactoryiqOpcUa_node_1 = require("../nodes/FactoryIQ/OpcUa/FactoryiqOpcUa.node"); jest.mock('../vendor', () => { const mockClient = { connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue({ close: jest.fn().mockResolvedValue(undefined), }), }; return { OPCUAClient: { create: jest.fn(() => mockClient), }, SecurityPolicy: { None: 'None', Basic128Rsa15: 'Basic128Rsa15', Basic256: 'Basic256', Basic256Sha256: 'Basic256Sha256', Aes128_Sha256_RsaOaep: 'Aes128_Sha256_RsaOaep', Aes256_Sha256_RsaPss: 'Aes256_Sha256_RsaPss', }, MessageSecurityMode: { None: 'None', Sign: 'Sign', SignAndEncrypt: 'SignAndEncrypt', }, UserTokenType: { UserName: 'UserName', Certificate: 'Certificate', }, }; }); describe('OpcUaCredentialsApi', () => { let credentials; beforeEach(() => { credentials = new OpcUaCredentialsApi_credentials_1.OpcUaCredentialsApi(); jest.clearAllMocks(); }); it('should be defined and instantiable', () => { expect(credentials).toBeDefined(); expect(credentials).toBeInstanceOf(OpcUaCredentialsApi_credentials_1.OpcUaCredentialsApi); }); it('should have correct name', () => { expect(credentials.name).toBe('opcUaCredentialsApi'); }); it('should have correct displayName', () => { expect(credentials.displayName).toBe('FactoryIQ OPC UA Account API'); }); it('should have correct documentationUrl', () => { expect(credentials.documentationUrl).toBe('https://github.com/factoryiq-ch/n8n-nodes-fiq-opcua?tab=readme-ov-file#configuration'); }); it('should have correct icon', () => { expect(credentials.icon).toBe('file:FactoryIQ.svg'); }); it('should have properties array', () => { expect(credentials.properties).toBeDefined(); expect(Array.isArray(credentials.properties)).toBe(true); expect(credentials.properties.length).toBe(8); }); describe('properties', () => { it('should have endpointUrl property', () => { const endpointUrlProperty = credentials.properties.find(p => p.name === 'endpointUrl'); expect(endpointUrlProperty).toBeDefined(); expect(endpointUrlProperty === null || endpointUrlProperty === void 0 ? void 0 : endpointUrlProperty.displayName).toBe('Endpoint URL'); expect(endpointUrlProperty === null || endpointUrlProperty === void 0 ? void 0 : endpointUrlProperty.type).toBe('string'); expect(endpointUrlProperty === null || endpointUrlProperty === void 0 ? void 0 : endpointUrlProperty.default).toBe(''); expect(endpointUrlProperty === null || endpointUrlProperty === void 0 ? void 0 : endpointUrlProperty.placeholder).toBe('opc.tcp://host:port'); expect(endpointUrlProperty === null || endpointUrlProperty === void 0 ? void 0 : endpointUrlProperty.description).toContain('The OPC UA server endpoint URL'); }); it('should have securityPolicy property', () => { var _a; const securityPolicyProperty = credentials.properties.find(p => p.name === 'securityPolicy'); expect(securityPolicyProperty).toBeDefined(); expect(securityPolicyProperty === null || securityPolicyProperty === void 0 ? void 0 : securityPolicyProperty.displayName).toBe('Security Policy'); expect(securityPolicyProperty === null || securityPolicyProperty === void 0 ? void 0 : securityPolicyProperty.type).toBe('options'); expect(securityPolicyProperty === null || securityPolicyProperty === void 0 ? void 0 : securityPolicyProperty.default).toBe('None'); expect(securityPolicyProperty === null || securityPolicyProperty === void 0 ? void 0 : securityPolicyProperty.options).toBeDefined(); expect((_a = securityPolicyProperty === null || securityPolicyProperty === void 0 ? void 0 : securityPolicyProperty.options) === null || _a === void 0 ? void 0 : _a.length).toBe(6); const options = (securityPolicyProperty === null || securityPolicyProperty === void 0 ? void 0 : securityPolicyProperty.options) || []; expect(options.some(opt => opt.value === 'None')).toBe(true); expect(options.some(opt => opt.value === 'Basic128Rsa15')).toBe(true); expect(options.some(opt => opt.value === 'Basic256')).toBe(true); expect(options.some(opt => opt.value === 'Basic256Sha256')).toBe(true); expect(options.some(opt => opt.value === 'Aes128_Sha256_RsaOaep')).toBe(true); expect(options.some(opt => opt.value === 'Aes256_Sha256_RsaPss')).toBe(true); }); it('should have securityMode property', () => { var _a; const securityModeProperty = credentials.properties.find(p => p.name === 'securityMode'); expect(securityModeProperty).toBeDefined(); expect(securityModeProperty === null || securityModeProperty === void 0 ? void 0 : securityModeProperty.displayName).toBe('Security Mode'); expect(securityModeProperty === null || securityModeProperty === void 0 ? void 0 : securityModeProperty.type).toBe('options'); expect(securityModeProperty === null || securityModeProperty === void 0 ? void 0 : securityModeProperty.default).toBe('None'); expect(securityModeProperty === null || securityModeProperty === void 0 ? void 0 : securityModeProperty.options).toBeDefined(); expect((_a = securityModeProperty === null || securityModeProperty === void 0 ? void 0 : securityModeProperty.options) === null || _a === void 0 ? void 0 : _a.length).toBe(3); const options = (securityModeProperty === null || securityModeProperty === void 0 ? void 0 : securityModeProperty.options) || []; expect(options.some(opt => opt.value === 'None')).toBe(true); expect(options.some(opt => opt.value === 'Sign')).toBe(true); expect(options.some(opt => opt.value === 'SignAndEncrypt')).toBe(true); }); it('should have authenticationType property', () => { var _a; const authTypeProperty = credentials.properties.find(p => p.name === 'authenticationType'); expect(authTypeProperty).toBeDefined(); expect(authTypeProperty === null || authTypeProperty === void 0 ? void 0 : authTypeProperty.displayName).toBe('Authentication Type'); expect(authTypeProperty === null || authTypeProperty === void 0 ? void 0 : authTypeProperty.type).toBe('options'); expect(authTypeProperty === null || authTypeProperty === void 0 ? void 0 : authTypeProperty.default).toBe('anonymous'); expect(authTypeProperty === null || authTypeProperty === void 0 ? void 0 : authTypeProperty.options).toBeDefined(); expect((_a = authTypeProperty === null || authTypeProperty === void 0 ? void 0 : authTypeProperty.options) === null || _a === void 0 ? void 0 : _a.length).toBe(3); const options = (authTypeProperty === null || authTypeProperty === void 0 ? void 0 : authTypeProperty.options) || []; expect(options.some(opt => opt.value === 'anonymous')).toBe(true); expect(options.some(opt => opt.value === 'usernamePassword')).toBe(true); expect(options.some(opt => opt.value === 'x509')).toBe(true); }); it('should have username property with correct displayOptions', () => { var _a, _b; const usernameProperty = credentials.properties.find(p => p.name === 'username'); expect(usernameProperty).toBeDefined(); expect(usernameProperty === null || usernameProperty === void 0 ? void 0 : usernameProperty.displayName).toBe('Username'); expect(usernameProperty === null || usernameProperty === void 0 ? void 0 : usernameProperty.type).toBe('string'); expect(usernameProperty === null || usernameProperty === void 0 ? void 0 : usernameProperty.default).toBe(''); expect(usernameProperty === null || usernameProperty === void 0 ? void 0 : usernameProperty.displayOptions).toBeDefined(); expect((_b = (_a = usernameProperty === null || usernameProperty === void 0 ? void 0 : usernameProperty.displayOptions) === null || _a === void 0 ? void 0 : _a.show) === null || _b === void 0 ? void 0 : _b.authenticationType).toEqual(['usernamePassword']); }); it('should have password property with correct displayOptions and password type', () => { var _a, _b, _c; const passwordProperty = credentials.properties.find(p => p.name === 'password'); expect(passwordProperty).toBeDefined(); expect(passwordProperty === null || passwordProperty === void 0 ? void 0 : passwordProperty.displayName).toBe('Password'); expect(passwordProperty === null || passwordProperty === void 0 ? void 0 : passwordProperty.type).toBe('string'); expect(passwordProperty === null || passwordProperty === void 0 ? void 0 : passwordProperty.default).toBe(''); expect((_a = passwordProperty === null || passwordProperty === void 0 ? void 0 : passwordProperty.typeOptions) === null || _a === void 0 ? void 0 : _a.password).toBe(true); expect(passwordProperty === null || passwordProperty === void 0 ? void 0 : passwordProperty.displayOptions).toBeDefined(); expect((_c = (_b = passwordProperty === null || passwordProperty === void 0 ? void 0 : passwordProperty.displayOptions) === null || _b === void 0 ? void 0 : _b.show) === null || _c === void 0 ? void 0 : _c.authenticationType).toEqual(['usernamePassword']); }); it('should have certificate property with correct displayOptions', () => { var _a, _b, _c; const certificateProperty = credentials.properties.find(p => p.name === 'certificate'); expect(certificateProperty).toBeDefined(); expect(certificateProperty === null || certificateProperty === void 0 ? void 0 : certificateProperty.displayName).toBe('Certificate (PEM)'); expect(certificateProperty === null || certificateProperty === void 0 ? void 0 : certificateProperty.type).toBe('string'); expect(certificateProperty === null || certificateProperty === void 0 ? void 0 : certificateProperty.default).toBe(''); expect((_a = certificateProperty === null || certificateProperty === void 0 ? void 0 : certificateProperty.typeOptions) === null || _a === void 0 ? void 0 : _a.rows).toBe(4); expect(certificateProperty === null || certificateProperty === void 0 ? void 0 : certificateProperty.description).toContain('PEM-encoded X509 certificate'); expect(certificateProperty === null || certificateProperty === void 0 ? void 0 : certificateProperty.displayOptions).toBeDefined(); expect((_c = (_b = certificateProperty === null || certificateProperty === void 0 ? void 0 : certificateProperty.displayOptions) === null || _b === void 0 ? void 0 : _b.show) === null || _c === void 0 ? void 0 : _c.authenticationType).toEqual(['x509']); }); it('should have privateKey property with correct displayOptions and password type', () => { var _a, _b, _c, _d; const privateKeyProperty = credentials.properties.find(p => p.name === 'privateKey'); expect(privateKeyProperty).toBeDefined(); expect(privateKeyProperty === null || privateKeyProperty === void 0 ? void 0 : privateKeyProperty.displayName).toBe('Private Key (PEM)'); expect(privateKeyProperty === null || privateKeyProperty === void 0 ? void 0 : privateKeyProperty.type).toBe('string'); expect(privateKeyProperty === null || privateKeyProperty === void 0 ? void 0 : privateKeyProperty.default).toBe(''); expect((_a = privateKeyProperty === null || privateKeyProperty === void 0 ? void 0 : privateKeyProperty.typeOptions) === null || _a === void 0 ? void 0 : _a.rows).toBe(4); expect((_b = privateKeyProperty === null || privateKeyProperty === void 0 ? void 0 : privateKeyProperty.typeOptions) === null || _b === void 0 ? void 0 : _b.password).toBe(true); expect(privateKeyProperty === null || privateKeyProperty === void 0 ? void 0 : privateKeyProperty.description).toContain('PEM-encoded private key'); expect(privateKeyProperty === null || privateKeyProperty === void 0 ? void 0 : privateKeyProperty.displayOptions).toBeDefined(); expect((_d = (_c = privateKeyProperty === null || privateKeyProperty === void 0 ? void 0 : privateKeyProperty.displayOptions) === null || _c === void 0 ? void 0 : _c.show) === null || _d === void 0 ? void 0 : _d.authenticationType).toEqual(['x509']); }); }); it('should have all required ICredentialType properties', () => { expect(typeof credentials.name).toBe('string'); expect(typeof credentials.displayName).toBe('string'); expect(typeof credentials.documentationUrl).toBe('string'); expect(typeof credentials.icon).toBe('string'); expect(Array.isArray(credentials.properties)).toBe(true); }); it('should have consistent property structure', () => { credentials.properties.forEach((property, index) => { expect(property.name).toBeDefined(); expect(typeof property.name).toBe('string'); expect(property.displayName).toBeDefined(); expect(typeof property.displayName).toBe('string'); expect(property.type).toBeDefined(); expect(typeof property.type).toBe('string'); expect(property.default).toBeDefined(); }); }); it('should have properties with unique names', () => { const propertyNames = credentials.properties.map(p => p.name); const uniqueNames = [...new Set(propertyNames)]; expect(propertyNames.length).toBe(uniqueNames.length); }); describe('opcuaConnectionTest', () => { let opcuaNode; let mockCredentialTestFunctions; let mockVendor; let mockClient; beforeEach(() => { opcuaNode = new FactoryiqOpcUa_node_1.FactoryiqOpcUa(); mockCredentialTestFunctions = {}; mockVendor = require('../vendor'); mockClient = { connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue({ close: jest.fn().mockResolvedValue(undefined), }), }; mockVendor.OPCUAClient.create.mockReturnValue(mockClient); jest.clearAllMocks(); }); const testCredential = (data) => ({ data }); it('should return error when no credentials provided', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential(null)); expect(result).toEqual({ status: 'Error', message: 'No credentials provided.', }); }); it('should return error when endpointUrl is empty', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: '' })); expect(result).toEqual({ status: 'Error', message: 'Endpoint URL is required and cannot be empty.', }); }); it('should return error when endpointUrl is whitespace only', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: ' ' })); expect(result).toEqual({ status: 'Error', message: 'Endpoint URL is required and cannot be empty.', }); }); it('should return error when endpointUrl has invalid format', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'http://localhost:4840' })); expect(result).toEqual({ status: 'Error', message: 'Endpoint URL must start with opc.tcp:// or opc.https://. Received: "http://localhost:4840"', }); }); it('should successfully test connection with anonymous authentication', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', securityPolicy: 'None', securityMode: 'None', authenticationType: 'anonymous', })); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledWith({ securityPolicy: 'None', securityMode: 'None', connectionStrategy: { initialDelay: 1000, maxRetry: 1, maxDelay: 2000 }, clientName: 'n8n-opcua-credential-test', requestedSessionTimeout: 10000, endpointMustExist: false, securityOptions: { rejectUnauthorized: false }, }); expect(mockClient.connect).toHaveBeenCalledWith('opc.tcp://localhost:4840'); expect(mockClient.createSession).toHaveBeenCalledWith(undefined); expect(result).toEqual({ status: 'OK', message: 'Connection successful!', }); }); it('should successfully test connection with username/password authentication', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', securityPolicy: 'Basic256Sha256', securityMode: 'Sign', authenticationType: 'usernamePassword', username: 'testuser', password: 'testpass', })); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledWith({ securityPolicy: 'Basic256Sha256', securityMode: 'Sign', connectionStrategy: { initialDelay: 1000, maxRetry: 1, maxDelay: 2000 }, clientName: 'n8n-opcua-credential-test', requestedSessionTimeout: 10000, endpointMustExist: false, securityOptions: { rejectUnauthorized: false }, }); expect(mockClient.createSession).toHaveBeenCalledWith({ type: 'UserName', userName: 'testuser', password: 'testpass', }); expect(result).toEqual({ status: 'OK', message: 'Connection successful!', }); }); it('should successfully test connection with x509 authentication', async () => { const testCert = '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----'; const testKey = '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----'; const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', securityPolicy: 'Basic256', securityMode: 'SignAndEncrypt', authenticationType: 'x509', certificate: testCert, privateKey: testKey, })); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledWith({ securityPolicy: 'Basic256', securityMode: 'SignAndEncrypt', connectionStrategy: { initialDelay: 1000, maxRetry: 1, maxDelay: 2000 }, clientName: 'n8n-opcua-credential-test', requestedSessionTimeout: 10000, endpointMustExist: false, securityOptions: { rejectUnauthorized: false }, certificateData: Buffer.from(testCert), privateKeyData: Buffer.from(testKey), }); expect(mockClient.createSession).toHaveBeenCalledWith({ type: 'Certificate', certificateData: testCert, privateKey: testKey, }); expect(result).toEqual({ status: 'OK', message: 'Connection successful!', }); }); it('should return error for x509 authentication without certificate', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'x509', privateKey: 'test-key', })); expect(result.status).toBe('Error'); expect(result.message).toContain('X509 authentication requires both certificate and private key'); }); it('should return error for x509 authentication without private key', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'x509', certificate: 'test-cert', })); expect(result.status).toBe('Error'); expect(result.message).toContain('X509 authentication requires both certificate and private key'); }); it('should test all security policies', async () => { const policies = ['Basic128Rsa15', 'Basic256', 'Basic256Sha256', 'Aes128_Sha256_RsaOaep', 'Aes256_Sha256_RsaPss']; for (const policy of policies) { await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', securityPolicy: policy, securityMode: 'None', authenticationType: 'anonymous', })); expect(mockVendor.OPCUAClient.create).toHaveBeenLastCalledWith(expect.objectContaining({ securityPolicy: policy, })); } }); it('should test all security modes', async () => { const modes = ['Sign', 'SignAndEncrypt', 'Sign & Encrypt']; const expectedModes = ['Sign', 'SignAndEncrypt', 'SignAndEncrypt']; for (let i = 0; i < modes.length; i++) { await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', securityPolicy: 'None', securityMode: modes[i], authenticationType: 'anonymous', })); expect(mockVendor.OPCUAClient.create).toHaveBeenLastCalledWith(expect.objectContaining({ securityMode: expectedModes[i], })); } }); it('should handle connection errors gracefully', async () => { mockClient.connect.mockRejectedValueOnce(new Error('Connection failed')); const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', })); expect(result).toEqual({ status: 'Error', message: 'Connection failed', }); }); it('should handle session creation errors gracefully', async () => { mockClient.createSession.mockRejectedValueOnce(new Error('Session creation failed')); const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', })); expect(result).toEqual({ status: 'Error', message: 'Session creation failed', }); }); it('should disconnect client even when session close fails', async () => { const mockSession = { close: jest.fn().mockRejectedValue(new Error('Close failed')) }; mockClient.createSession.mockResolvedValueOnce(mockSession); const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', })); expect(mockClient.disconnect).toHaveBeenCalled(); expect(result).toEqual({ status: 'Error', message: 'Close failed', }); }); it('should handle opc.https:// protocol', async () => { const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.https://localhost:4840', authenticationType: 'anonymous', })); expect(mockClient.connect).toHaveBeenCalledWith('opc.https://localhost:4840'); expect(result.status).toBe('OK'); }); it('should clean up client on disconnect failure', async () => { mockClient.disconnect.mockRejectedValueOnce(new Error('Disconnect failed')); mockClient.connect.mockRejectedValueOnce(new Error('Connection failed')); const result = await opcuaNode.methods.credentialTest.opcuaConnectionTest.call(mockCredentialTestFunctions, testCredential({ endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', })); expect(result).toEqual({ status: 'Error', message: 'Connection failed', }); expect(mockClient.disconnect).toHaveBeenCalled(); }); }); }); //# sourceMappingURL=OpcUaCredentialsApi.unit.test.js.map