362 lines
14 KiB
TypeScript
362 lines
14 KiB
TypeScript
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
|
import * as smartmongo from '../ts/index.js';
|
||
|
|
|
||
|
|
const { SessionEngine } = smartmongo.tsmdb;
|
||
|
|
|
||
|
|
let sessionEngine: InstanceType<typeof SessionEngine>;
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// 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();
|