moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,198 @@
/**
* Session Timeout Middleware Tests
*
* Verifies:
* - SessionTimeoutTracker returns expired after inactivity
* - Timer resets on activity
* - Disabled in solo mode (no CLERK_SECRET_KEY)
* - Exempt paths (/health, /api/vault) bypass timeout
* - Cleanup prevents memory leak
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { SessionTimeoutTracker } from '../../src/local/security-middleware.js';
describe('SessionTimeoutTracker', () => {
let tracker: SessionTimeoutTracker;
afterEach(() => {
tracker?.destroy();
});
it('does not expire on first request', () => {
tracker = new SessionTimeoutTracker(30_000); // 30 seconds for testing
const expired = tracker.check('192.168.1.1');
expect(expired).toBe(false);
});
it('does not expire when activity is within timeout', () => {
tracker = new SessionTimeoutTracker(30_000);
tracker.check('192.168.1.1');
// Immediately check again — should not be expired
const expired = tracker.check('192.168.1.1');
expect(expired).toBe(false);
});
it('expires after inactivity exceeds timeout', () => {
tracker = new SessionTimeoutTracker(100); // 100ms timeout for testing
// First request — sets last activity
tracker.check('192.168.1.1');
// Simulate time passing beyond the timeout
vi.useFakeTimers();
vi.advanceTimersByTime(150); // 150ms > 100ms timeout
const expired = tracker.check('192.168.1.1');
expect(expired).toBe(true);
vi.useRealTimers();
});
it('resets timer on every request', () => {
tracker = new SessionTimeoutTracker(200); // 200ms timeout
vi.useFakeTimers();
// First request
tracker.check('192.168.1.1');
// 100ms later — still within timeout
vi.advanceTimersByTime(100);
let expired = tracker.check('192.168.1.1');
expect(expired).toBe(false);
// Another 100ms — still within timeout because timer was reset
vi.advanceTimersByTime(100);
expired = tracker.check('192.168.1.1');
expect(expired).toBe(false);
// 250ms without any request — should expire
vi.advanceTimersByTime(250);
expired = tracker.check('192.168.1.1');
expect(expired).toBe(true);
vi.useRealTimers();
});
it('tracks different IPs independently', () => {
tracker = new SessionTimeoutTracker(100);
vi.useFakeTimers();
// Both IPs make a request
tracker.check('192.168.1.1');
tracker.check('192.168.1.2');
// 50ms later, only IP1 makes another request
vi.advanceTimersByTime(50);
tracker.check('192.168.1.1');
// 60ms more — IP2's last activity was 110ms ago (expired), IP1's was 60ms ago (not expired)
vi.advanceTimersByTime(60);
const ip1Expired = tracker.check('192.168.1.1');
expect(ip1Expired).toBe(false);
const ip2Expired = tracker.check('192.168.1.2');
expect(ip2Expired).toBe(true);
vi.useRealTimers();
});
it('allows fresh request after expiration', () => {
tracker = new SessionTimeoutTracker(100);
vi.useFakeTimers();
tracker.check('192.168.1.1');
// Expire
vi.advanceTimersByTime(150);
const expired = tracker.check('192.168.1.1');
expect(expired).toBe(true);
// Next request should succeed (entry was cleared on expiration)
const freshRequest = tracker.check('192.168.1.1');
expect(freshRequest).toBe(false);
vi.useRealTimers();
});
it('returns configured timeout value', () => {
tracker = new SessionTimeoutTracker(45_000);
expect(tracker.getTimeoutMs()).toBe(45_000);
});
it('defaults to 30 minutes when no timeout specified', () => {
// Clear env var to ensure default
const original = process.env.WAGGLE_SESSION_TIMEOUT_MS;
delete process.env.WAGGLE_SESSION_TIMEOUT_MS;
tracker = new SessionTimeoutTracker();
expect(tracker.getTimeoutMs()).toBe(30 * 60 * 1000);
process.env.WAGGLE_SESSION_TIMEOUT_MS = original;
});
it('reads timeout from WAGGLE_SESSION_TIMEOUT_MS env var', () => {
const original = process.env.WAGGLE_SESSION_TIMEOUT_MS;
process.env.WAGGLE_SESSION_TIMEOUT_MS = '60000';
tracker = new SessionTimeoutTracker();
expect(tracker.getTimeoutMs()).toBe(60_000);
process.env.WAGGLE_SESSION_TIMEOUT_MS = original;
});
it('destroy clears all state', () => {
tracker = new SessionTimeoutTracker(30_000);
tracker.check('192.168.1.1');
tracker.check('192.168.1.2');
tracker.destroy();
// After destroy, a new check should work (fresh state)
// We verify destroy doesn't throw and a new tracker works
const newTracker = new SessionTimeoutTracker(30_000);
const expired = newTracker.check('192.168.1.1');
expect(expired).toBe(false);
newTracker.destroy();
});
});
describe('Session timeout in solo vs team mode', () => {
it('session timeout is only enabled when CLERK_SECRET_KEY is set', () => {
// This test verifies the design contract:
// The securityMiddlewarePlugin checks `process.env.CLERK_SECRET_KEY`
// and only creates a SessionTimeoutTracker when it's present.
//
// In solo mode (no CLERK_SECRET_KEY), session timeout is null/disabled.
const original = process.env.CLERK_SECRET_KEY;
delete process.env.CLERK_SECRET_KEY;
const isTeamMode = !!process.env.CLERK_SECRET_KEY;
expect(isTeamMode).toBe(false);
// Restore for other tests
if (original) process.env.CLERK_SECRET_KEY = original;
});
it('exempt paths are correctly defined', () => {
// Verify the exempt path patterns match the spec
const TIMEOUT_EXEMPT_PATHS = ['/health', '/api/vault'];
// /health should be exempt
expect(TIMEOUT_EXEMPT_PATHS.some(p => '/health'.startsWith(p))).toBe(true);
// /api/vault/* should be exempt
expect(TIMEOUT_EXEMPT_PATHS.some(p => '/api/vault/my-secret/reveal'.startsWith(p))).toBe(true);
// /api/chat should NOT be exempt
expect(TIMEOUT_EXEMPT_PATHS.some(p => '/api/chat'.startsWith(p))).toBe(false);
// /api/feedback/stats should NOT be exempt
expect(TIMEOUT_EXEMPT_PATHS.some(p => '/api/feedback/stats'.startsWith(p))).toBe(false);
});
});