UNPKG

@fiqch/n8n-nodes-fiq-opcua

Version:

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

594 lines 28.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ConnectionPool_1 = require("../nodes/FactoryIQ/OpcUa/ConnectionPool"); jest.mock('../vendor', () => ({ OPCUAClient: { create: jest.fn(() => ({ connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue({ close: jest.fn().mockResolvedValue(undefined), isChannelValid: jest.fn().mockReturnValue(true), isReconnecting: false, }), })), }, 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("OpcUaConnectionPool", () => { let pool; let mockVendor; const mockCredentials = { endpointUrl: "opc.tcp://localhost:4840", securityPolicy: "None", securityMode: "None", authenticationType: "anonymous", }; const mockCredentials2 = { endpointUrl: "opc.tcp://localhost:4841", securityPolicy: "None", securityMode: "None", authenticationType: "anonymous", }; beforeEach(() => { ConnectionPool_1.OpcUaConnectionPool.instance = undefined; pool = ConnectionPool_1.OpcUaConnectionPool.getInstance(); mockVendor = require('../vendor'); jest.clearAllMocks(); mockVendor.OPCUAClient.create.mockImplementation(() => ({ connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue({ close: jest.fn().mockResolvedValue(undefined), isChannelValid: jest.fn().mockReturnValue(true), isReconnecting: false, }), })); }); afterEach(async () => { try { await pool.shutdown(); } catch (error) { } ConnectionPool_1.OpcUaConnectionPool.instance = undefined; }); describe("Singleton Pattern", () => { it("should return the same instance on multiple calls", () => { const pool1 = ConnectionPool_1.OpcUaConnectionPool.getInstance(); const pool2 = ConnectionPool_1.OpcUaConnectionPool.getInstance(); expect(pool1).toBe(pool2); }); it("should maintain state across getInstance calls", async () => { const pool1 = ConnectionPool_1.OpcUaConnectionPool.getInstance(); const connection1 = await pool1.getConnection(mockCredentials); const pool2 = ConnectionPool_1.OpcUaConnectionPool.getInstance(); expect(pool1).toBe(pool2); pool1.releaseConnection(connection1); const connection2 = await pool2.getConnection(mockCredentials); expect(connection2.client).toBe(connection1.client); }); }); describe("Connection Creation", () => { it("should create a new connection on first request", async () => { const connection = await pool.getConnection(mockCredentials); expect(connection).toBeDefined(); expect(connection.client).toBeDefined(); expect(connection.session).toBeDefined(); expect(connection.inUse).toBe(true); expect(connection.key).toBeDefined(); }); it("should create connections for different credentials", async () => { const connection1 = await pool.getConnection(mockCredentials); const connection2 = await pool.getConnection(mockCredentials2); expect(connection1.client).not.toBe(connection2.client); expect(connection1.key).not.toBe(connection2.key); pool.releaseConnection(connection1); pool.releaseConnection(connection2); }); it("should create connection key correctly", async () => { const connection = await pool.getConnection(mockCredentials); expect(connection.key).toBe("opc.tcp://localhost:4840|anonymous|"); }); it("should include username in connection key", async () => { const credentialsWithUser = { ...mockCredentials, authenticationType: "usernamePassword", username: "testuser", password: "testpass" }; const connection = await pool.getConnection(credentialsWithUser); expect(connection.key).toBe("opc.tcp://localhost:4840|usernamePassword|testuser"); }); }); describe("Connection Reuse", () => { it("should reuse connection when released and requested again", async () => { const connection1 = await pool.getConnection(mockCredentials); const originalClient = connection1.client; pool.releaseConnection(connection1); expect(connection1.inUse).toBe(false); const connection2 = await pool.getConnection(mockCredentials); expect(connection2.client).toBe(originalClient); expect(connection2.inUse).toBe(true); }); it("should not reuse connection that is still in use", async () => { const connection1 = await pool.getConnection(mockCredentials); const connection2 = await pool.getConnection(mockCredentials); expect(connection2.client).not.toBe(connection1.client); pool.releaseConnection(connection1); pool.releaseConnection(connection2); }); it("should find available connection among multiple connections", async () => { const connection1 = await pool.getConnection(mockCredentials); const connection2 = await pool.getConnection(mockCredentials); const connection3 = await pool.getConnection(mockCredentials); pool.releaseConnection(connection2); const connection4 = await pool.getConnection(mockCredentials); expect(connection4.client).toBe(connection2.client); pool.releaseConnection(connection1); pool.releaseConnection(connection3); pool.releaseConnection(connection4); }); }); describe("Pool Size Limits", () => { it("should create up to 3 connections per credential", async () => { const connection1 = await pool.getConnection(mockCredentials); const connection2 = await pool.getConnection(mockCredentials); const connection3 = await pool.getConnection(mockCredentials); expect(connection1.client).not.toBe(connection2.client); expect(connection2.client).not.toBe(connection3.client); expect(connection1.client).not.toBe(connection3.client); pool.releaseConnection(connection1); pool.releaseConnection(connection2); pool.releaseConnection(connection3); }); it("should reuse first connection when pool is full", async () => { const connection1 = await pool.getConnection(mockCredentials); const connection2 = await pool.getConnection(mockCredentials); const connection3 = await pool.getConnection(mockCredentials); const connection4 = await pool.getConnection(mockCredentials); expect(connection4.client).toBe(connection1.client); expect(connection4.inUse).toBe(true); pool.releaseConnection(connection2); pool.releaseConnection(connection3); pool.releaseConnection(connection4); }); }); describe("Session Validation", () => { it("should validate session before reuse", async () => { const mockSession = { isChannelValid: jest.fn().mockReturnValue(true), isReconnecting: false, close: jest.fn().mockResolvedValue(undefined), }; const mockClient = { connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue(mockSession), }; require('../vendor').OPCUAClient.create.mockImplementation(() => mockClient); const connection1 = await pool.getConnection(mockCredentials); pool.releaseConnection(connection1); const connection2 = await pool.getConnection(mockCredentials); expect(mockSession.isChannelValid).toHaveBeenCalled(); pool.releaseConnection(connection2); }); it("should remove invalid session from pool", async () => { const mockSession = { isChannelValid: jest.fn().mockReturnValue(false), isReconnecting: false, close: jest.fn().mockResolvedValue(undefined), }; const mockClient = { connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue(mockSession), }; require('../vendor').OPCUAClient.create.mockImplementation(() => ({ ...mockClient, createSession: jest.fn().mockResolvedValue({ ...mockSession, isChannelValid: jest.fn().mockReturnValue(false), }), })); const connection1 = await pool.getConnection(mockCredentials); const originalClient = connection1.client; pool.releaseConnection(connection1); require('../vendor').OPCUAClient.create.mockImplementation(() => ({ connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue({ isChannelValid: jest.fn().mockReturnValue(false), isReconnecting: false, close: jest.fn().mockResolvedValue(undefined), }), })); const connection2 = await pool.getConnection(mockCredentials); expect(connection2.client).not.toBe(originalClient); }); it("should handle session validation errors", async () => { const mockSession = { isChannelValid: jest.fn().mockImplementation(() => { throw new Error("Session validation failed"); }), isReconnecting: false, close: jest.fn().mockResolvedValue(undefined), }; const mockClient = { connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue(mockSession), }; require('../vendor').OPCUAClient.create.mockImplementation(() => mockClient); const connection1 = await pool.getConnection(mockCredentials); pool.releaseConnection(connection1); const connection2 = await pool.getConnection(mockCredentials); expect(connection2).toBeDefined(); }); it("should skip reconnecting sessions", async () => { const mockSession = { isChannelValid: jest.fn().mockReturnValue(true), isReconnecting: true, close: jest.fn().mockResolvedValue(undefined), }; const mockClient = { connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue(mockSession), }; require('../vendor').OPCUAClient.create.mockImplementation(() => mockClient); const connection1 = await pool.getConnection(mockCredentials); pool.releaseConnection(connection1); require('../vendor').OPCUAClient.create.mockImplementation(() => ({ connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockResolvedValue({ isChannelValid: jest.fn().mockReturnValue(true), isReconnecting: false, close: jest.fn().mockResolvedValue(undefined), }), })); const connection2 = await pool.getConnection(mockCredentials); expect(connection2.client).not.toBe(connection1.client); pool.releaseConnection(connection1); pool.releaseConnection(connection2); }); }); describe("Error Handling", () => { it("should throw error when connection creation fails", async () => { const mockClient = { connect: jest.fn().mockRejectedValue(new Error("Connection failed")), disconnect: jest.fn().mockResolvedValue(undefined), }; require('../vendor').OPCUAClient.create.mockImplementation(() => mockClient); await expect(pool.getConnection(mockCredentials)).rejects.toThrow("Connection failed"); }); it("should throw error when session creation fails", async () => { const mockClient = { connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockRejectedValue(new Error("Session creation failed")), }; require('../vendor').OPCUAClient.create.mockImplementation(() => mockClient); await expect(pool.getConnection(mockCredentials)).rejects.toThrow("Session creation failed"); }); }); describe("Security Configuration", () => { it("should handle different security policies", async () => { const secureCredentials = { ...mockCredentials, securityPolicy: "Basic256Sha256", securityMode: "SignAndEncrypt", }; const connection = await pool.getConnection(secureCredentials); expect(connection).toBeDefined(); }); it("should handle X509 authentication", async () => { const x509Credentials = { ...mockCredentials, authenticationType: "x509", certificate: "dummy-cert", privateKey: "dummy-key", }; const connection = await pool.getConnection(x509Credentials); expect(connection).toBeDefined(); }); it("should throw error for X509 without certificate", async () => { const invalidX509Credentials = { ...mockCredentials, authenticationType: "x509", }; await expect(pool.getConnection(invalidX509Credentials)).rejects.toThrow("X509 authentication requires both certificate and private key"); }); it("should throw error for X509 without private key", async () => { const invalidX509Credentials = { ...mockCredentials, authenticationType: "x509", certificate: "dummy-cert", }; await expect(pool.getConnection(invalidX509Credentials)).rejects.toThrow("X509 authentication requires both certificate and private key"); }); }); describe("Shutdown", () => { it("should close all connections on shutdown", async () => { const connection1 = await pool.getConnection(mockCredentials); const connection2 = await pool.getConnection(mockCredentials2); const mockSession1 = connection1.session; const mockClient1 = connection1.client; const mockSession2 = connection2.session; const mockClient2 = connection2.client; await pool.shutdown(); expect(mockSession1.close).toHaveBeenCalled(); expect(mockClient1.disconnect).toHaveBeenCalled(); expect(mockSession2.close).toHaveBeenCalled(); expect(mockClient2.disconnect).toHaveBeenCalled(); }); it("should handle errors during shutdown gracefully", async () => { const connection = await pool.getConnection(mockCredentials); connection.session.close.mockRejectedValue(new Error("Close failed")); connection.client.disconnect.mockRejectedValue(new Error("Disconnect failed")); await expect(pool.shutdown()).resolves.not.toThrow(); }); it("should clear pools after shutdown", async () => { await pool.getConnection(mockCredentials); await pool.shutdown(); const newConnection = await pool.getConnection(mockCredentials); expect(newConnection).toBeDefined(); }); }); describe("Connection Release", () => { it("should handle release of null connection", () => { expect(() => pool.releaseConnection(null)).not.toThrow(); }); it("should handle release of undefined connection", () => { expect(() => pool.releaseConnection(undefined)).not.toThrow(); }); it("should mark connection as not in use when released", async () => { const connection = await pool.getConnection(mockCredentials); expect(connection.inUse).toBe(true); pool.releaseConnection(connection); expect(connection.inUse).toBe(false); }); }); it('should create a new connection when pool is empty', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; const connection = await pool.getConnection(credentials); expect(connection).toBeDefined(); expect(connection.inUse).toBe(true); expect(mockVendor.OPCUAClient.create).toHaveBeenCalled(); }); it('should reuse available connection', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; const connection1 = await pool.getConnection(credentials); pool.releaseConnection(connection1); const connection2 = await pool.getConnection(credentials); expect(connection1).toBe(connection2); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledTimes(1); }); it('should handle session validation failure and remove invalid connection', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; const connection1 = await pool.getConnection(credentials); pool.releaseConnection(connection1); connection1.session.isChannelValid = jest.fn(() => { throw new Error('Session validation failed'); }); const connection2 = await pool.getConnection(credentials); expect(connection2).not.toBe(connection1); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledTimes(2); }); it('should handle reconnecting session', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; const connection1 = await pool.getConnection(credentials); pool.releaseConnection(connection1); connection1.session.isReconnecting = true; const connection2 = await pool.getConnection(credentials); expect(connection2).not.toBe(connection1); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledTimes(2); }); it('should limit pool size to 3 connections and reuse first when full', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; const conn1 = await pool.getConnection(credentials); await pool.getConnection(credentials); await pool.getConnection(credentials); const conn4 = await pool.getConnection(credentials); expect(conn4).toBe(conn1); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledTimes(3); }); it('should handle connection creation failure', async () => { const credentials = { endpointUrl: 'opc.tcp://invalid-server', authenticationType: 'anonymous', }; mockVendor.OPCUAClient.create.mockReturnValueOnce({ connect: jest.fn().mockRejectedValue(new Error('Connection failed')), disconnect: jest.fn().mockResolvedValue(undefined), }); await expect(pool.getConnection(credentials)).rejects.toThrow('Connection failed'); }); it('should create different pools for different credentials', async () => { const credentials1 = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; const credentials2 = { endpointUrl: 'opc.tcp://localhost:4841', authenticationType: 'anonymous', }; const conn1 = await pool.getConnection(credentials1); const conn2 = await pool.getConnection(credentials2); expect(conn1).not.toBe(conn2); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledTimes(2); }); it('should handle username/password authentication', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'usernamePassword', username: 'testuser', password: 'testpass', }; const connection = await pool.getConnection(credentials); expect(connection).toBeDefined(); expect(mockVendor.OPCUAClient.create).toHaveBeenCalled(); }); it('should handle x509 authentication with valid certificate', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'x509', certificate: 'test-cert-data', privateKey: 'test-key-data', }; const connection = await pool.getConnection(credentials); expect(connection).toBeDefined(); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledWith(expect.objectContaining({ certificateData: expect.any(Buffer), privateKeyData: expect.any(Buffer), })); }); it('should throw error for x509 authentication without certificate or private key', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'x509', }; await expect(pool.getConnection(credentials)).rejects.toThrow('X509 authentication requires both certificate and private key.'); }); it('should handle all security policies', async () => { const policies = ['Basic256Sha256', 'Basic256', 'Basic128Rsa15', 'Aes128_Sha256_RsaOaep', 'Aes256_Sha256_RsaPss', 'None']; for (const policy of policies) { const credentials = { endpointUrl: `opc.tcp://localhost:484${policies.indexOf(policy)}`, authenticationType: 'anonymous', securityPolicy: policy, }; const connection = await pool.getConnection(credentials); expect(connection).toBeDefined(); } }); it('should handle all security modes', async () => { const modes = ['Sign', 'SignAndEncrypt', 'Sign & Encrypt', 'None']; for (const mode of modes) { const credentials = { endpointUrl: `opc.tcp://localhost:485${modes.indexOf(mode)}`, authenticationType: 'anonymous', securityMode: mode, }; const connection = await pool.getConnection(credentials); expect(connection).toBeDefined(); } }); it('should release connection properly', () => { const mockConnection = { client: {}, session: {}, inUse: true, key: 'test-key', }; pool.releaseConnection(mockConnection); expect(mockConnection.inUse).toBe(false); }); it('should handle null connection in releaseConnection', () => { expect(() => pool.releaseConnection(null)).not.toThrow(); }); it('should shutdown gracefully and cleanup all connections', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; await pool.getConnection(credentials); await pool.getConnection(credentials); const mockSession = { close: jest.fn().mockResolvedValue(undefined) }; const mockClient = { disconnect: jest.fn().mockResolvedValue(undefined) }; const pools = pool.pools; for (const [, connections] of pools.entries()) { connections.forEach((conn) => { conn.session = mockSession; conn.client = mockClient; }); } await pool.shutdown(); expect(mockSession.close).toHaveBeenCalled(); expect(mockClient.disconnect).toHaveBeenCalled(); }); it('should handle errors during shutdown gracefully', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; await pool.getConnection(credentials); const mockSession = { close: jest.fn().mockRejectedValue(new Error('Close failed')) }; const mockClient = { disconnect: jest.fn().mockRejectedValue(new Error('Disconnect failed')) }; const pools = pool.pools; for (const [, connections] of pools.entries()) { connections.forEach((conn) => { conn.session = mockSession; conn.client = mockClient; }); } await expect(pool.shutdown()).resolves.toBeUndefined(); }); it('should create proper client name with random suffix', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; await pool.getConnection(credentials); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledWith(expect.objectContaining({ clientName: expect.stringMatching(/^n8n-opcua-pool-[a-z0-9]{8}$/) })); }); it('should handle missing session in connection during validation', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; const connection1 = await pool.getConnection(credentials); pool.releaseConnection(connection1); connection1.session = null; const connection2 = await pool.getConnection(credentials); expect(connection2).not.toBe(connection1); expect(mockVendor.OPCUAClient.create).toHaveBeenCalledTimes(2); }); it('should handle session creation failure', async () => { const credentials = { endpointUrl: 'opc.tcp://localhost:4840', authenticationType: 'anonymous', }; mockVendor.OPCUAClient.create.mockReturnValueOnce({ connect: jest.fn().mockResolvedValue(undefined), disconnect: jest.fn().mockResolvedValue(undefined), createSession: jest.fn().mockRejectedValue(new Error('Session creation failed')), }); await expect(pool.getConnection(credentials)).rejects.toThrow('Session creation failed'); }); }); //# sourceMappingURL=ConnectionPool.unit.test.js.map