File size: 3,769 Bytes
529090e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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');
        }
    });
});