import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartmongo from '../ts/index.js'; const { SessionEngine } = smartmongo.tsmdb; let sessionEngine: InstanceType; // ============================================================================ // Setup // ============================================================================ tap.test('session: should create SessionEngine instance', async () => { sessionEngine = new SessionEngine({ sessionTimeoutMs: 1000, // 1 second for testing cleanupIntervalMs: 10000, // 10 seconds to avoid cleanup during tests }); expect(sessionEngine).toBeTruthy(); }); // ============================================================================ // Session Lifecycle Tests // ============================================================================ tap.test('session: startSession should create session with auto-generated ID', async () => { const session = sessionEngine.startSession(); expect(session).toBeTruthy(); expect(session.id).toBeTruthy(); expect(session.id.length).toBeGreaterThanOrEqual(32); // UUID hex string (32 or 36 with hyphens) expect(session.createdAt).toBeGreaterThan(0); expect(session.lastActivityAt).toBeGreaterThan(0); expect(session.inTransaction).toBeFalse(); }); tap.test('session: startSession should create session with specified ID', async () => { const customId = 'custom-session-id-12345'; const session = sessionEngine.startSession(customId); expect(session.id).toEqual(customId); }); tap.test('session: startSession should create session with metadata', async () => { const metadata = { client: 'test-client', version: '1.0' }; const session = sessionEngine.startSession(undefined, metadata); expect(session.metadata).toBeTruthy(); expect(session.metadata!.client).toEqual('test-client'); expect(session.metadata!.version).toEqual('1.0'); }); tap.test('session: getSession should return session by ID', async () => { const created = sessionEngine.startSession('get-session-test'); const retrieved = sessionEngine.getSession('get-session-test'); expect(retrieved).toBeTruthy(); expect(retrieved!.id).toEqual('get-session-test'); expect(retrieved!.id).toEqual(created.id); }); tap.test('session: getSession should return undefined for non-existent session', async () => { const session = sessionEngine.getSession('non-existent-session-id'); expect(session).toBeUndefined(); }); tap.test('session: touchSession should update lastActivityAt', async () => { const session = sessionEngine.startSession('touch-test-session'); const originalLastActivity = session.lastActivityAt; // Wait a bit to ensure time difference await new Promise(resolve => setTimeout(resolve, 10)); const touched = sessionEngine.touchSession('touch-test-session'); expect(touched).toBeTrue(); const updated = sessionEngine.getSession('touch-test-session'); expect(updated!.lastActivityAt).toBeGreaterThanOrEqual(originalLastActivity); }); tap.test('session: touchSession should return false for non-existent session', async () => { const touched = sessionEngine.touchSession('non-existent-touch-session'); expect(touched).toBeFalse(); }); tap.test('session: endSession should remove the session', async () => { sessionEngine.startSession('end-session-test'); expect(sessionEngine.getSession('end-session-test')).toBeTruthy(); const ended = await sessionEngine.endSession('end-session-test'); expect(ended).toBeTrue(); expect(sessionEngine.getSession('end-session-test')).toBeUndefined(); }); tap.test('session: endSession should return false for non-existent session', async () => { const ended = await sessionEngine.endSession('non-existent-end-session'); expect(ended).toBeFalse(); }); // ============================================================================ // Session Expiry Tests // ============================================================================ tap.test('session: isSessionExpired should return false for fresh session', async () => { const session = sessionEngine.startSession('fresh-session'); const isExpired = sessionEngine.isSessionExpired(session); expect(isExpired).toBeFalse(); }); tap.test('session: isSessionExpired should return true for old session', async () => { // Create a session with old lastActivityAt const session = sessionEngine.startSession('old-session'); // Manually set lastActivityAt to old value (sessionTimeoutMs is 1000ms) (session as any).lastActivityAt = Date.now() - 2000; const isExpired = sessionEngine.isSessionExpired(session); expect(isExpired).toBeTrue(); }); tap.test('session: getSession should return undefined for expired session', async () => { const session = sessionEngine.startSession('expiring-session'); // Manually expire the session (session as any).lastActivityAt = Date.now() - 2000; const retrieved = sessionEngine.getSession('expiring-session'); expect(retrieved).toBeUndefined(); }); // ============================================================================ // Transaction Integration Tests // ============================================================================ tap.test('session: startTransaction should mark session as in transaction', async () => { sessionEngine.startSession('txn-session-1'); const started = sessionEngine.startTransaction('txn-session-1', 'txn-id-1', 1); expect(started).toBeTrue(); const session = sessionEngine.getSession('txn-session-1'); expect(session!.inTransaction).toBeTrue(); expect(session!.txnId).toEqual('txn-id-1'); expect(session!.txnNumber).toEqual(1); }); tap.test('session: startTransaction should return false for non-existent session', async () => { const started = sessionEngine.startTransaction('non-existent-txn-session', 'txn-id'); expect(started).toBeFalse(); }); tap.test('session: endTransaction should clear transaction state', async () => { sessionEngine.startSession('txn-session-2'); sessionEngine.startTransaction('txn-session-2', 'txn-id-2'); const ended = sessionEngine.endTransaction('txn-session-2'); expect(ended).toBeTrue(); const session = sessionEngine.getSession('txn-session-2'); expect(session!.inTransaction).toBeFalse(); expect(session!.txnId).toBeUndefined(); expect(session!.txnNumber).toBeUndefined(); }); tap.test('session: endTransaction should return false for non-existent session', async () => { const ended = sessionEngine.endTransaction('non-existent-end-txn-session'); expect(ended).toBeFalse(); }); tap.test('session: getTransactionId should return transaction ID', async () => { sessionEngine.startSession('txn-id-session'); sessionEngine.startTransaction('txn-id-session', 'my-txn-id'); const txnId = sessionEngine.getTransactionId('txn-id-session'); expect(txnId).toEqual('my-txn-id'); }); tap.test('session: getTransactionId should return undefined for session without transaction', async () => { sessionEngine.startSession('no-txn-session'); const txnId = sessionEngine.getTransactionId('no-txn-session'); expect(txnId).toBeUndefined(); }); tap.test('session: getTransactionId should return undefined for non-existent session', async () => { const txnId = sessionEngine.getTransactionId('non-existent-txn-id-session'); expect(txnId).toBeUndefined(); }); tap.test('session: isInTransaction should return correct state', async () => { sessionEngine.startSession('in-txn-check-session'); expect(sessionEngine.isInTransaction('in-txn-check-session')).toBeFalse(); sessionEngine.startTransaction('in-txn-check-session', 'txn-check'); expect(sessionEngine.isInTransaction('in-txn-check-session')).toBeTrue(); sessionEngine.endTransaction('in-txn-check-session'); expect(sessionEngine.isInTransaction('in-txn-check-session')).toBeFalse(); }); tap.test('session: isInTransaction should return false for non-existent session', async () => { expect(sessionEngine.isInTransaction('non-existent-in-txn-session')).toBeFalse(); }); // ============================================================================ // Session Listing Tests // ============================================================================ tap.test('session: listSessions should return all active sessions', async () => { // Close and recreate to have a clean slate sessionEngine.close(); sessionEngine = new SessionEngine({ sessionTimeoutMs: 10000, cleanupIntervalMs: 60000, }); sessionEngine.startSession('list-session-1'); sessionEngine.startSession('list-session-2'); sessionEngine.startSession('list-session-3'); const sessions = sessionEngine.listSessions(); expect(sessions.length).toEqual(3); }); tap.test('session: listSessions should not include expired sessions', async () => { const session = sessionEngine.startSession('expired-list-session'); // Expire the session (session as any).lastActivityAt = Date.now() - 20000; const sessions = sessionEngine.listSessions(); const found = sessions.find(s => s.id === 'expired-list-session'); expect(found).toBeUndefined(); }); tap.test('session: getSessionCount should return correct count', async () => { const count = sessionEngine.getSessionCount(); expect(count).toBeGreaterThanOrEqual(3); // We created 3 sessions above }); tap.test('session: getSessionsWithTransactions should filter correctly', async () => { // Clean slate sessionEngine.close(); sessionEngine = new SessionEngine({ sessionTimeoutMs: 10000, cleanupIntervalMs: 60000, }); sessionEngine.startSession('no-txn-1'); sessionEngine.startSession('no-txn-2'); sessionEngine.startSession('with-txn-1'); sessionEngine.startSession('with-txn-2'); sessionEngine.startTransaction('with-txn-1', 'txn-a'); sessionEngine.startTransaction('with-txn-2', 'txn-b'); const txnSessions = sessionEngine.getSessionsWithTransactions(); expect(txnSessions.length).toEqual(2); expect(txnSessions.every(s => s.inTransaction)).toBeTrue(); }); // ============================================================================ // getOrCreateSession Tests // ============================================================================ tap.test('session: getOrCreateSession should create if missing', async () => { const session = sessionEngine.getOrCreateSession('get-or-create-new'); expect(session).toBeTruthy(); expect(session.id).toEqual('get-or-create-new'); }); tap.test('session: getOrCreateSession should return existing session', async () => { const created = sessionEngine.startSession('get-or-create-existing'); const retrieved = sessionEngine.getOrCreateSession('get-or-create-existing'); expect(retrieved.id).toEqual(created.id); expect(retrieved.createdAt).toEqual(created.createdAt); }); tap.test('session: getOrCreateSession should touch existing session', async () => { const session = sessionEngine.startSession('get-or-create-touch'); const originalLastActivity = session.lastActivityAt; await new Promise(resolve => setTimeout(resolve, 10)); sessionEngine.getOrCreateSession('get-or-create-touch'); const updated = sessionEngine.getSession('get-or-create-touch'); expect(updated!.lastActivityAt).toBeGreaterThanOrEqual(originalLastActivity); }); // ============================================================================ // extractSessionId Static Method Tests // ============================================================================ tap.test('session: extractSessionId should handle UUID object', async () => { const { ObjectId } = smartmongo.tsmdb; const uuid = new smartmongo.tsmdb.plugins.bson.UUID(); const lsid = { id: uuid }; const extracted = SessionEngine.extractSessionId(lsid); expect(extracted).toEqual(uuid.toHexString()); }); tap.test('session: extractSessionId should handle string ID', async () => { const lsid = { id: 'string-session-id' }; const extracted = SessionEngine.extractSessionId(lsid); expect(extracted).toEqual('string-session-id'); }); tap.test('session: extractSessionId should handle binary format', async () => { const binaryData = Buffer.from('test-binary-uuid', 'utf8').toString('base64'); const lsid = { id: { $binary: { base64: binaryData } } }; const extracted = SessionEngine.extractSessionId(lsid); expect(extracted).toBeTruthy(); expect(typeof extracted).toEqual('string'); }); tap.test('session: extractSessionId should return undefined for null/undefined', async () => { expect(SessionEngine.extractSessionId(null)).toBeUndefined(); expect(SessionEngine.extractSessionId(undefined)).toBeUndefined(); }); tap.test('session: extractSessionId should return undefined for empty object', async () => { expect(SessionEngine.extractSessionId({})).toBeUndefined(); }); // ============================================================================ // refreshSession Tests // ============================================================================ tap.test('session: refreshSession should update lastActivityAt', async () => { const session = sessionEngine.startSession('refresh-session-test'); const originalLastActivity = session.lastActivityAt; await new Promise(resolve => setTimeout(resolve, 10)); const refreshed = sessionEngine.refreshSession('refresh-session-test'); expect(refreshed).toBeTrue(); const updated = sessionEngine.getSession('refresh-session-test'); expect(updated!.lastActivityAt).toBeGreaterThanOrEqual(originalLastActivity); }); tap.test('session: refreshSession should return false for non-existent session', async () => { const refreshed = sessionEngine.refreshSession('non-existent-refresh-session'); expect(refreshed).toBeFalse(); }); // ============================================================================ // Cleanup // ============================================================================ tap.test('session: close should clear all sessions', async () => { sessionEngine.startSession('close-test-session'); expect(sessionEngine.getSessionCount()).toBeGreaterThan(0); sessionEngine.close(); expect(sessionEngine.getSessionCount()).toEqual(0); }); export default tap.start();