SessionStore.test.ts•8.37 kB
/**
* SessionStore Unit Tests
*
* Tests encryption, decryption, and session management functionality
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import { SessionStore } from '../../../src/core/auth/SessionStore';
import type { Session } from '../../../src/core/types';
import { SessionError } from '../../../src/core/types';
describe('SessionStore', () => {
let testDir: string;
let sessionStore: SessionStore;
let mockSession: Session;
beforeEach(async () => {
// Create temporary test directory
testDir = path.join(os.tmpdir(), `whatap-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
sessionStore = new SessionStore(testDir);
// Mock session data
mockSession = {
email: 'test@whatap.io',
accountId: 12345,
cookies: {
wa: 'test-wa-cookie',
jsessionid: 'test-jsessionid',
},
apiToken: 'test-api-token',
serviceUrl: 'https://service.whatap.io',
createdAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
};
});
afterEach(async () => {
// Clean up test directory
try {
await fs.rm(testDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe('save and load', () => {
test('should save and load session successfully', async () => {
// Save session
await sessionStore.save(mockSession);
// Verify file exists
const exists = await sessionStore.exists();
expect(exists).toBe(true);
// Load session
const loaded = await sessionStore.load();
expect(loaded).not.toBeNull();
expect(loaded?.email).toBe(mockSession.email);
expect(loaded?.accountId).toBe(mockSession.accountId);
expect(loaded?.cookies.wa).toBe(mockSession.cookies.wa);
expect(loaded?.cookies.jsessionid).toBe(mockSession.cookies.jsessionid);
expect(loaded?.apiToken).toBe(mockSession.apiToken);
expect(loaded?.serviceUrl).toBe(mockSession.serviceUrl);
// Check dates are correctly parsed
expect(loaded?.createdAt).toBeInstanceOf(Date);
expect(loaded?.expiresAt).toBeInstanceOf(Date);
});
test('should return null when session file does not exist', async () => {
const loaded = await sessionStore.load();
expect(loaded).toBeNull();
});
test('should return null and clear expired session', async () => {
// Create expired session
const expiredSession: Session = {
...mockSession,
expiresAt: new Date(Date.now() - 1000), // Expired 1 second ago
};
await sessionStore.save(expiredSession);
// Try to load - should return null
const loaded = await sessionStore.load();
expect(loaded).toBeNull();
// Verify file was deleted
const exists = await sessionStore.exists();
expect(exists).toBe(false);
});
test('should overwrite existing session', async () => {
// Save first session
await sessionStore.save(mockSession);
// Save second session with different data
const updatedSession: Session = {
...mockSession,
accountId: 67890,
apiToken: 'updated-token',
};
await sessionStore.save(updatedSession);
// Load and verify updated data
const loaded = await sessionStore.load();
expect(loaded?.accountId).toBe(67890);
expect(loaded?.apiToken).toBe('updated-token');
});
});
describe('encryption', () => {
test('should encrypt session data (not plain text)', async () => {
await sessionStore.save(mockSession);
// Read raw file content
const sessionFile = sessionStore.getSessionFilePath();
const encrypted = await fs.readFile(sessionFile, 'utf-8');
// Verify it's not plain text
expect(encrypted).not.toContain('test@whatap.io');
expect(encrypted).not.toContain('test-wa-cookie');
expect(encrypted).not.toContain('test-api-token');
// Verify it contains encryption metadata
const encryptedData = JSON.parse(encrypted);
expect(encryptedData).toHaveProperty('iv');
expect(encryptedData).toHaveProperty('encrypted');
expect(encryptedData).toHaveProperty('authTag');
});
test('should use consistent encryption key', async () => {
// Save and load first time
await sessionStore.save(mockSession);
const loaded1 = await sessionStore.load();
// Create new SessionStore instance (same directory)
const sessionStore2 = new SessionStore(testDir);
const loaded2 = await sessionStore2.load();
// Should load same session data
expect(loaded1).toEqual(loaded2);
});
test('should generate new key if key file is deleted', async () => {
await sessionStore.save(mockSession);
// Delete key file
const keyFile = path.join(testDir, '.key');
await fs.unlink(keyFile);
// Create new instance - should generate new key
const sessionStore2 = new SessionStore(testDir);
// Should fail to load (wrong key)
await expect(sessionStore2.load()).rejects.toThrow(SessionError);
});
});
describe('clear', () => {
test('should clear session successfully', async () => {
await sessionStore.save(mockSession);
expect(await sessionStore.exists()).toBe(true);
await sessionStore.clear();
expect(await sessionStore.exists()).toBe(false);
const loaded = await sessionStore.load();
expect(loaded).toBeNull();
});
test('should not throw error when clearing non-existent session', async () => {
await expect(sessionStore.clear()).resolves.not.toThrow();
});
});
describe('file permissions', () => {
test('should create session file with restricted permissions (0600)', async () => {
await sessionStore.save(mockSession);
const sessionFile = sessionStore.getSessionFilePath();
const stats = await fs.stat(sessionFile);
// Check permissions (Unix only)
if (process.platform !== 'win32') {
const mode = stats.mode & 0o777;
expect(mode).toBe(0o600);
}
});
test('should create key file with restricted permissions (0600)', async () => {
await sessionStore.save(mockSession);
const keyFile = path.join(testDir, '.key');
const stats = await fs.stat(keyFile);
// Check permissions (Unix only)
if (process.platform !== 'win32') {
const mode = stats.mode & 0o777;
expect(mode).toBe(0o600);
}
});
});
describe('error handling', () => {
test('should throw SessionError on encryption failure', async () => {
// This is hard to test without mocking crypto, but we can test the wrapper
const invalidSession = { ...mockSession } as any;
delete invalidSession.email;
// Should still save (JSON.stringify handles it)
await expect(sessionStore.save(invalidSession)).resolves.not.toThrow();
});
test('should throw SessionError on corrupted session file', async () => {
await sessionStore.save(mockSession);
// Corrupt the session file
const sessionFile = sessionStore.getSessionFilePath();
await fs.writeFile(sessionFile, 'corrupted-data', 'utf-8');
// Should throw error on load
await expect(sessionStore.load()).rejects.toThrow(SessionError);
});
test('should throw SessionError on invalid encrypted data', async () => {
await sessionStore.save(mockSession);
// Write invalid JSON
const sessionFile = sessionStore.getSessionFilePath();
await fs.writeFile(
sessionFile,
JSON.stringify({
iv: 'invalid',
encrypted: 'invalid',
authTag: 'invalid',
}),
'utf-8'
);
// Should throw error on load
await expect(sessionStore.load()).rejects.toThrow(SessionError);
});
});
describe('exists', () => {
test('should return false when session does not exist', async () => {
const exists = await sessionStore.exists();
expect(exists).toBe(false);
});
test('should return true when session exists', async () => {
await sessionStore.save(mockSession);
const exists = await sessionStore.exists();
expect(exists).toBe(true);
});
});
});