Spaces:
Paused
Paused
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | |
| // Hoist mocks to ensure they are available for vi.mock | |
| const { mockSession, mockDriver } = vi.hoisted(() => { | |
| const mockRun = vi.fn(); | |
| const mockCloseSession = vi.fn(); | |
| const session = { | |
| run: mockRun, | |
| close: mockCloseSession | |
| }; | |
| const mockVerifyConnectivity = vi.fn(); | |
| const mockCloseDriver = vi.fn(); | |
| const driver = { | |
| session: vi.fn().mockReturnValue(session), | |
| verifyConnectivity: mockVerifyConnectivity, | |
| close: mockCloseDriver | |
| }; | |
| return { | |
| mockSession: session, | |
| mockDriver: driver | |
| }; | |
| }); | |
| vi.mock('neo4j-driver', () => ({ | |
| default: { | |
| driver: vi.fn(() => mockDriver), | |
| auth: { | |
| basic: vi.fn() | |
| }, | |
| session: { | |
| READ: 'READ', | |
| WRITE: 'WRITE' | |
| }, | |
| isInt: (val: any) => val && typeof val.toNumber === 'function', | |
| int: (val: number) => ({ toNumber: () => val }) | |
| } | |
| })); | |
| // Import after mocking | |
| import { Neo4jAdapter } from '../adapters/Neo4jAdapter'; | |
| describe('Neo4jAdapter (Immunforsvar)', () => { | |
| let adapter: Neo4jAdapter; | |
| beforeEach(() => { | |
| // Reset mocks | |
| vi.clearAllMocks(); | |
| // Reset singleton (using a dirty cast because it's private/static) | |
| (Neo4jAdapter as any).instance = null; | |
| // Re-instantiate | |
| adapter = Neo4jAdapter.getInstance(); | |
| // Mock successful connection by default | |
| // We need to access the mocked functions directly | |
| mockDriver.verifyConnectivity.mockResolvedValue(undefined); | |
| mockSession.run.mockResolvedValue({ records: [] }); | |
| }); | |
| it('skal forbinde til Cloud Cortex ved start', async () => { | |
| // Connection is async in constructor, give it a microtask | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| expect(mockDriver.verifyConnectivity).toHaveBeenCalled(); | |
| }); | |
| it('skal execute query successfully', async () => { | |
| const mockRecord = { | |
| keys: ['n'], | |
| get: (key: string) => ({ labels: ['Test'], properties: { name: 'TestNode' }, identity: { toString: () => '1' } }) | |
| }; | |
| mockSession.run.mockResolvedValueOnce({ records: [mockRecord] }); | |
| const result = await adapter.executeQuery('MATCH (n) RETURN n'); | |
| expect(mockSession.run).toHaveBeenCalledWith('MATCH (n) RETURN n', {}); | |
| expect(result).toHaveLength(1); | |
| expect(result[0].n).toBeDefined(); | |
| }); | |
| it('skal håndtere connection failure (Circuit Breaker)', async () => { | |
| // Force failure | |
| mockDriver.verifyConnectivity.mockRejectedValue(new Error('Connection failed')); | |
| // Re-init to trigger connect() again | |
| (Neo4jAdapter as any).instance = null; | |
| adapter = Neo4jAdapter.getInstance(); | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| expect(adapter.isHealthy()).toBe(false); | |
| }); | |
| it('skal aktivere Circuit Breaker ved gentagne fejl', async () => { | |
| // Simulate 5 consecutive failures | |
| mockSession.run.mockRejectedValue(new Error('Database exploded')); | |
| // Run 5 queries that fail | |
| for (let i = 0; i < 5; i++) { | |
| try { await adapter.executeQuery('MATCH (n) RETURN n'); } catch (e) {} | |
| } | |
| // The 6th query should fail WITHOUT hitting the driver (Circuit Open) | |
| // The adapter checks the circuit breaker *before* ensuring connection/getting session | |
| try { | |
| await adapter.executeQuery('MATCH (n) RETURN n'); | |
| } catch (error: any) { | |
| expect(error.message).toContain('circuit OPEN'); | |
| } | |
| }); | |
| }); |