- Added DOKUMENTASI-FILTER-PER-SITE.md (site isolation docs) - Fixed start-with-env.js to force-load .env.production - Fixed MONGODB_URI hostname from mongodb-netify to mongodb.prod.proit.id - Updated .gitignore to exclude sensitive scripts and credential files - Minor UI and labeling improvements
240 lines
7.9 KiB
JavaScript
240 lines
7.9 KiB
JavaScript
// Test suite for the inactivity timeout logic (TDD approach)
|
|
// This script mocks React hooks and browser APIs to verify correct behavior of our session timeout logic.
|
|
|
|
const assert = require("assert");
|
|
|
|
console.log("=== RUNNING INACTIVITY TIMEOUT LOGIC TESTS ===");
|
|
|
|
// ─── Test Harness ─────────────────────────────────────────────────────────────
|
|
class MockDocument {
|
|
constructor() {
|
|
this.visibilityState = "visible";
|
|
this.listeners = {};
|
|
}
|
|
addEventListener(event, callback) {
|
|
if (!this.listeners[event]) this.listeners[event] = [];
|
|
this.listeners[event].push(callback);
|
|
}
|
|
removeEventListener(event, callback) {
|
|
if (!this.listeners[event]) return;
|
|
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
|
}
|
|
trigger(event) {
|
|
if (this.listeners[event]) {
|
|
this.listeners[event].forEach(cb => cb());
|
|
}
|
|
}
|
|
}
|
|
|
|
class MockWindow {
|
|
constructor() {
|
|
this.listeners = {};
|
|
this.location = { href: "" };
|
|
}
|
|
addEventListener(event, callback) {
|
|
if (!this.listeners[event]) this.listeners[event] = [];
|
|
this.listeners[event].push(callback);
|
|
}
|
|
removeEventListener(event, callback) {
|
|
if (!this.listeners[event]) return;
|
|
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
|
}
|
|
trigger(event) {
|
|
if (this.listeners[event]) {
|
|
this.listeners[event].forEach(cb => cb());
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Test Inactivity Logic Class ──────────────────────────────────────────────
|
|
class InactivityTimeoutTester {
|
|
constructor(options = {}) {
|
|
this.timeoutSeconds = options.timeoutSeconds || 3600; // 1 hour
|
|
this.warningSeconds = options.warningSeconds || 120; // 2 minutes
|
|
this.showWarning = false;
|
|
this.timeLeft = this.warningSeconds;
|
|
this.isLoggedOut = false;
|
|
this.lastActivity = Date.now();
|
|
this.visibilityState = "visible";
|
|
}
|
|
|
|
// Simulate user activity event
|
|
onUserActivity(currentTime) {
|
|
if (this.visibilityState === "visible") {
|
|
this.lastActivity = currentTime || Date.now();
|
|
this.showWarning = false;
|
|
}
|
|
}
|
|
|
|
// Simulate tick (every second or arbitrary delay)
|
|
tick(currentTime) {
|
|
const elapsed = Math.floor((currentTime - this.lastActivity) / 1000);
|
|
const remaining = this.timeoutSeconds - elapsed;
|
|
|
|
if (remaining <= 0) {
|
|
this.isLoggedOut = true;
|
|
this.showWarning = false;
|
|
} else if (remaining <= this.warningSeconds) {
|
|
this.timeLeft = remaining;
|
|
this.showWarning = true;
|
|
} else {
|
|
this.showWarning = false;
|
|
}
|
|
}
|
|
|
|
// Simulate returning to the tab
|
|
onVisibilityChange(newVisibilityState, currentTime) {
|
|
this.visibilityState = newVisibilityState;
|
|
if (newVisibilityState === "visible") {
|
|
const elapsed = Math.floor((currentTime - this.lastActivity) / 1000);
|
|
const remaining = this.timeoutSeconds - elapsed;
|
|
|
|
if (remaining <= 0) {
|
|
this.isLoggedOut = true;
|
|
this.showWarning = false;
|
|
} else if (remaining <= this.warningSeconds) {
|
|
this.timeLeft = remaining;
|
|
this.showWarning = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── TEST CASES ───────────────────────────────────────────────────────────────
|
|
|
|
function testResetActivityWhenVisible() {
|
|
console.log("- Test: Reset activity timer when tab is visible");
|
|
const tester = new InactivityTimeoutTester();
|
|
const initialActivity = tester.lastActivity;
|
|
|
|
// Wait 10 seconds
|
|
const timeAfter10s = initialActivity + 10000;
|
|
tester.onUserActivity(timeAfter10s);
|
|
|
|
// Last activity should be updated
|
|
assert.ok(tester.lastActivity >= initialActivity);
|
|
}
|
|
|
|
function testNoResetActivityWhenHidden() {
|
|
console.log("- Test: Do NOT reset activity timer when tab is hidden");
|
|
const tester = new InactivityTimeoutTester();
|
|
const initialActivity = tester.lastActivity;
|
|
|
|
// Change tab status to hidden
|
|
tester.visibilityState = "hidden";
|
|
|
|
// User activity should be ignored for reset
|
|
tester.onUserActivity(initialActivity + 10000);
|
|
assert.strictEqual(tester.lastActivity, initialActivity);
|
|
}
|
|
|
|
function testWarningThreshold() {
|
|
console.log("- Test: Trigger warning at 2 minutes remaining");
|
|
const tester = new InactivityTimeoutTester({ timeoutSeconds: 3600, warningSeconds: 120 });
|
|
const start = Date.now();
|
|
|
|
// Tick at 59 minutes (3540 seconds elapsed, 60 seconds remaining)
|
|
const warnTime = start + (3540 * 1000);
|
|
tester.tick(warnTime);
|
|
|
|
assert.strictEqual(tester.showWarning, true);
|
|
assert.strictEqual(tester.timeLeft, 60);
|
|
}
|
|
|
|
function testSafeZoneAfterActivity() {
|
|
console.log("- Test: Warning goes away when user becomes active");
|
|
const tester = new InactivityTimeoutTester({ timeoutSeconds: 3600, warningSeconds: 120 });
|
|
const start = Date.now();
|
|
|
|
// Tick at 3540 seconds (warning is shown)
|
|
const warnTime = start + (3540 * 1000);
|
|
tester.tick(warnTime);
|
|
assert.strictEqual(tester.showWarning, true);
|
|
|
|
// User performs activity at warnTime
|
|
tester.onUserActivity(warnTime);
|
|
|
|
// Tick immediately after activity (should be in safe zone)
|
|
tester.tick(warnTime);
|
|
assert.strictEqual(tester.showWarning, false);
|
|
}
|
|
|
|
function testAutoLogout() {
|
|
console.log("- Test: Auto-logout after 1 hour of total inactivity");
|
|
const tester = new InactivityTimeoutTester({ timeoutSeconds: 3600, warningSeconds: 120 });
|
|
const start = Date.now();
|
|
|
|
// Tick after 3601 seconds
|
|
const expireTime = start + (3601 * 1000);
|
|
tester.tick(expireTime);
|
|
|
|
assert.strictEqual(tester.isLoggedOut, true);
|
|
assert.strictEqual(tester.showWarning, false);
|
|
}
|
|
|
|
function testTabSwitchSilentlyTicks() {
|
|
console.log("- Test: Switching tabs continues counting down silently");
|
|
const tester = new InactivityTimeoutTester({ timeoutSeconds: 3600, warningSeconds: 120 });
|
|
const start = Date.now();
|
|
|
|
// Switch tab away to hidden
|
|
tester.onVisibilityChange("hidden", start);
|
|
|
|
// Tick 10 minutes (600s) later while hidden
|
|
const tenMinsLater = start + (600 * 1000);
|
|
tester.tick(tenMinsLater);
|
|
|
|
// Should still be in safe zone (no warning yet, no logout)
|
|
assert.strictEqual(tester.showWarning, false);
|
|
assert.strictEqual(tester.isLoggedOut, false);
|
|
}
|
|
|
|
function testTabReturnImmediateCheck() {
|
|
console.log("- Test: Tab return triggers immediate state check");
|
|
const tester = new InactivityTimeoutTester({ timeoutSeconds: 3600, warningSeconds: 120 });
|
|
const start = Date.now();
|
|
|
|
// User switches tab away
|
|
tester.onVisibilityChange("hidden", start);
|
|
|
|
// User is away for 59 minutes (3540s), then returns
|
|
const returnTime = start + (3540 * 1000);
|
|
tester.onVisibilityChange("visible", returnTime);
|
|
|
|
// Should immediately show warning with remaining time of 60 seconds
|
|
assert.strictEqual(tester.showWarning, true);
|
|
assert.strictEqual(tester.timeLeft, 60);
|
|
}
|
|
|
|
function testTabReturnExpiredLogout() {
|
|
console.log("- Test: Tab return logs out immediately if expired while away");
|
|
const tester = new InactivityTimeoutTester({ timeoutSeconds: 3600, warningSeconds: 120 });
|
|
const start = Date.now();
|
|
|
|
// User switches tab away
|
|
tester.onVisibilityChange("hidden", start);
|
|
|
|
// User is away for 61 minutes (3660s), then returns
|
|
const returnTime = start + (3660 * 1000);
|
|
tester.onVisibilityChange("visible", returnTime);
|
|
|
|
// Should immediately trigger logout
|
|
assert.strictEqual(tester.isLoggedOut, true);
|
|
}
|
|
|
|
// Execute tests
|
|
try {
|
|
testResetActivityWhenVisible();
|
|
testNoResetActivityWhenHidden();
|
|
testWarningThreshold();
|
|
testSafeZoneAfterActivity();
|
|
testAutoLogout();
|
|
testTabSwitchSilentlyTicks();
|
|
testTabReturnImmediateCheck();
|
|
testTabReturnExpiredLogout();
|
|
console.log("✓ ALL TESTS PASSED SUCCESSFULLY!");
|
|
} catch (error) {
|
|
console.error("❌ TEST FAILURE:", error);
|
|
process.exit(1);
|
|
}
|