session-manager.test.tsā¢6.92 kB
import { SessionManager } from '../session-manager.js';
import { DatabaseManager } from '../database.js';
import { existsSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
describe('SessionManager', () => {
let dbPath: string;
let dbManager: DatabaseManager;
let sessionManager: SessionManager;
beforeEach(() => {
const uniqueId = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 9)}`;
dbPath = join(tmpdir(), `test-clipboard-${uniqueId}.db`);
dbManager = new DatabaseManager(dbPath);
sessionManager = new SessionManager(dbManager);
});
afterEach(() => {
if (dbManager) {
dbManager.close();
const keyPath = dbManager.getEncryptionKeyPath();
if (keyPath && existsSync(keyPath)) {
rmSync(keyPath);
}
}
if (existsSync(dbPath)) {
rmSync(dbPath);
}
});
describe('Session Creation - RED', () => {
it('should create session with unique ID', () => {
const sessionId = sessionManager.createSession();
expect(sessionId).toBeDefined();
expect(typeof sessionId).toBe('string');
expect(sessionId.length).toBeGreaterThan(0);
});
it('should store session in database', () => {
const sessionId = sessionManager.createSession();
const db = dbManager.getConnection();
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId) as
| { session_id: string; created_at: number; last_activity: number }
| undefined;
expect(session).toBeDefined();
expect(session?.session_id).toBe(sessionId);
});
it('should track creation timestamp', () => {
const beforeCreate = Date.now();
const sessionId = sessionManager.createSession();
const afterCreate = Date.now();
const db = dbManager.getConnection();
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId) as
| { created_at: number }
| undefined;
expect(session?.created_at).toBeGreaterThanOrEqual(beforeCreate);
expect(session?.created_at).toBeLessThanOrEqual(afterCreate);
});
it('should generate cryptographically secure session IDs', () => {
const ids = new Set<string>();
for (let i = 0; i < 100; i++) {
ids.add(sessionManager.createSession());
}
// All 100 IDs should be unique
expect(ids.size).toBe(100);
});
});
describe('Session Retrieval - RED', () => {
it('should retrieve existing session', () => {
const sessionId = sessionManager.createSession();
const session = sessionManager.getSession(sessionId);
expect(session).toBeDefined();
expect(session?.sessionId).toBe(sessionId);
});
it('should return null for non-existent session', () => {
const session = sessionManager.getSession('non-existent-id');
expect(session).toBeNull();
});
it('should update last_activity timestamp on retrieval', () => {
const sessionId = sessionManager.createSession();
// Wait a bit to ensure timestamp difference
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
return wait(10).then(() => {
const beforeGet = Date.now();
sessionManager.getSession(sessionId);
const afterGet = Date.now();
const db = dbManager.getConnection();
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId) as
| { last_activity: number }
| undefined;
expect(session?.last_activity).toBeGreaterThanOrEqual(beforeGet);
expect(session?.last_activity).toBeLessThanOrEqual(afterGet);
});
});
});
describe('Session Cleanup - RED', () => {
it('should remove sessions older than timeout', () => {
const sessionId = sessionManager.createSession();
// Manually set old timestamp
const db = dbManager.getConnection();
const oneDayAgo = Date.now() - 25 * 60 * 60 * 1000; // 25 hours ago
db.prepare('UPDATE sessions SET last_activity = ? WHERE session_id = ?').run(
oneDayAgo,
sessionId
);
const removed = sessionManager.cleanupExpiredSessions();
expect(removed).toBeGreaterThan(0);
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId);
expect(session).toBeUndefined();
});
it('should preserve active sessions', () => {
const sessionId = sessionManager.createSession();
sessionManager.cleanupExpiredSessions();
const db = dbManager.getConnection();
const session = db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId);
expect(session).toBeDefined();
});
it('should cascade delete related data', () => {
const sessionId = sessionManager.createSession();
const db = dbManager.getConnection();
// Add some related data
db.prepare(
`INSERT INTO clipboard_buffer
(session_id, content, source_file, start_line, end_line, copied_at, operation_type)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).run(sessionId, 'test content', 'test.ts', 1, 5, Date.now(), 'copy');
// Set old timestamp
const oneDayAgo = Date.now() - 25 * 60 * 60 * 1000;
db.prepare('UPDATE sessions SET last_activity = ? WHERE session_id = ?').run(
oneDayAgo,
sessionId
);
sessionManager.cleanupExpiredSessions();
// Check cascade delete
const clipboard = db
.prepare('SELECT * FROM clipboard_buffer WHERE session_id = ?')
.get(sessionId);
expect(clipboard).toBeUndefined();
});
it('should return count of removed sessions', () => {
// Create multiple expired sessions
const db = dbManager.getConnection();
const oneDayAgo = Date.now() - 25 * 60 * 60 * 1000;
for (let i = 0; i < 3; i++) {
const sessionId = sessionManager.createSession();
db.prepare('UPDATE sessions SET last_activity = ? WHERE session_id = ?').run(
oneDayAgo,
sessionId
);
}
const removed = sessionManager.cleanupExpiredSessions();
expect(removed).toBe(3);
});
});
describe('Custom timeout - RED', () => {
it('should use custom timeout when provided', () => {
const customTimeout = 1000; // 1 second
const customSessionManager = new SessionManager(dbManager, customTimeout);
const sessionId = customSessionManager.createSession();
// Set timestamp to 2 seconds ago
const db = dbManager.getConnection();
const twoSecondsAgo = Date.now() - 2000;
db.prepare('UPDATE sessions SET last_activity = ? WHERE session_id = ?').run(
twoSecondsAgo,
sessionId
);
const removed = customSessionManager.cleanupExpiredSessions();
expect(removed).toBe(1);
});
});
});