Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

338 lines
13 KiB
JavaScript

/**
* Tests for security/safe-logger.js
*
* Tests the SafeLogger's environment detection, data redaction
* in production mode, and passthrough in development mode.
*/
// Load SafeLogger — it self-registers on window
import '@js/security/safe-logger.js';
const SL = window.SafeLogger;
describe('SafeLogger', () => {
afterEach(() => {
SL.resetProductionMode();
});
describe('environment detection', () => {
it('detects localhost as development', () => {
// happy-dom defaults to localhost
SL.resetProductionMode();
expect(SL.isProduction()).toBe(false);
});
it('can be forced to production mode', () => {
SL.setProductionMode(true);
expect(SL.isProduction()).toBe(true);
});
it('can be forced to development mode', () => {
SL.setProductionMode(false);
expect(SL.isProduction()).toBe(false);
});
it('returns to auto-detection after reset', () => {
SL.setProductionMode(true);
expect(SL.isProduction()).toBe(true);
SL.resetProductionMode();
// localhost → development
expect(SL.isProduction()).toBe(false);
});
it('getProductionMode returns null when auto-detecting', () => {
expect(SL.getProductionMode()).toBeNull();
});
it('getProductionMode returns forced value', () => {
SL.setProductionMode(true);
expect(SL.getProductionMode()).toBe(true);
});
});
describe('development mode logging', () => {
beforeEach(() => {
SL.setProductionMode(false);
});
it('passes static message through', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('test message');
expect(spy).toHaveBeenCalledWith('test message');
spy.mockRestore();
});
it('passes dynamic data through in dev mode', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('query:', 'climate change');
expect(spy).toHaveBeenCalledWith('query:', 'climate change');
spy.mockRestore();
});
it('extracts Error properties in dev mode', () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
const err = new Error('test error');
SL.error('Failed:', err);
const secondArg = spy.mock.calls[0][1];
expect(secondArg).toHaveProperty('name', 'Error');
expect(secondArg).toHaveProperty('message', 'test error');
expect(secondArg).toHaveProperty('stack');
spy.mockRestore();
});
it('extracts .message from error-like objects in dev mode', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
SL.warn('Issue:', { message: 'not found', code: 404 });
expect(spy).toHaveBeenCalledWith('Issue:', 'not found');
spy.mockRestore();
});
it('stringifies plain objects in dev mode', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('Data:', { error: 'not found' });
expect(spy).toHaveBeenCalledWith('Data:', '{"error":"not found"}');
spy.mockRestore();
});
it('debug messages are emitted in dev mode', () => {
const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
SL.debug('debug info');
expect(spy).toHaveBeenCalledWith('debug info');
spy.mockRestore();
});
});
describe('production mode redaction', () => {
beforeEach(() => {
SL.setProductionMode(true);
});
it('passes first string argument (static message) through', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('User searched:', 'sensitive query');
expect(spy.mock.calls[0][0]).toBe('User searched:');
spy.mockRestore();
});
it('redacts string arguments after the first', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('User searched:', 'sensitive query');
expect(spy.mock.calls[0][1]).toBe('[redacted]');
spy.mockRestore();
});
it('redacts numbers', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('Research ID:', 42);
expect(spy.mock.calls[0][1]).toBe('[redacted]');
spy.mockRestore();
});
it('preserves booleans in production', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('Active:', true);
expect(spy.mock.calls[0][1]).toBe(true);
spy.mockRestore();
});
it('preserves null/undefined in production', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('Value:', null);
expect(spy.mock.calls[0][1]).toBeNull();
spy.mockRestore();
});
it('redacts Error message but keeps name in production', () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
SL.error('Failed:', new TypeError('secret details'));
const secondArg = spy.mock.calls[0][1];
expect(secondArg.name).toBe('TypeError');
expect(secondArg.message).toBe('[redacted]');
spy.mockRestore();
});
it('shows array structure but not contents in production', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('Items:', [1, 2, 3]);
expect(spy.mock.calls[0][1]).toBe('[Array(3)]');
spy.mockRestore();
});
it('shows [Object] for objects in production', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('Config:', { key: 'value' });
expect(spy.mock.calls[0][1]).toBe('[Object]');
spy.mockRestore();
});
it('suppresses debug messages entirely in production', () => {
const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
SL.debug('secret debug info');
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
});
describe('log methods call correct console methods', () => {
beforeEach(() => {
SL.setProductionMode(false);
});
it('log → console.log', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('test');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
it('info → console.info', () => {
const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
SL.info('test');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
it('warn → console.warn', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
SL.warn('test');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
it('error → console.error', () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
SL.error('test');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
describe('production redaction invariants (fuzz)', () => {
// Property-based test: in production mode, no string/number argument
// after the first should ever appear verbatim in console output.
// This is the most critical security property of SafeLogger — if
// it ever regresses, user search queries / API responses leak.
beforeEach(() => {
SL.setProductionMode(true);
});
const generateRandomString = () => {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%';
const len = Math.floor(Math.random() * 30) + 5;
let s = '';
for (let i = 0; i < len; i++) {
s += chars[Math.floor(Math.random() * chars.length)];
}
return s;
};
it('never leaks string arguments after the first (100 random inputs)', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
for (let i = 0; i < 100; i++) {
const secret = generateRandomString();
SL.log('Request:', secret);
const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
// The static message should pass through
expect(loggedArgs[0]).toBe('Request:');
// The secret should NOT appear in any logged argument
for (let j = 1; j < loggedArgs.length; j++) {
expect(loggedArgs[j]).not.toBe(secret);
expect(String(loggedArgs[j])).not.toContain(secret);
}
}
spy.mockRestore();
});
it('never leaks number arguments after the first', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
for (let i = 0; i < 50; i++) {
const secretId = Math.floor(Math.random() * 1e9);
SL.log('User ID:', secretId);
const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
for (let j = 1; j < loggedArgs.length; j++) {
expect(loggedArgs[j]).not.toBe(secretId);
}
}
spy.mockRestore();
});
it('never leaks object property values', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
for (let i = 0; i < 30; i++) {
const secret = generateRandomString();
SL.log('Data:', { token: secret, user: secret });
const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
// The serialized representation should not contain the secret
const serialized = JSON.stringify(loggedArgs);
expect(serialized).not.toContain(secret);
}
spy.mockRestore();
});
it('never leaks array contents', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
for (let i = 0; i < 30; i++) {
const secrets = [generateRandomString(), generateRandomString()];
SL.log('Items:', secrets);
const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
const serialized = JSON.stringify(loggedArgs);
for (const secret of secrets) {
expect(serialized).not.toContain(secret);
}
}
spy.mockRestore();
});
it('never leaks Error message contents', () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
for (let i = 0; i < 30; i++) {
const secretMsg = generateRandomString();
SL.error('Failed:', new Error(secretMsg));
const loggedArgs = spy.mock.calls[spy.mock.calls.length - 1];
const serialized = JSON.stringify(loggedArgs);
expect(serialized).not.toContain(secretMsg);
}
spy.mockRestore();
});
});
describe('edge cases', () => {
it('handles zero arguments', () => {
SL.setProductionMode(false);
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log();
expect(spy).toHaveBeenCalledWith();
spy.mockRestore();
});
it('redacts error-like objects in production', () => {
SL.setProductionMode(true);
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('Issue:', { message: 'secret', name: 'ApiError' });
const arg = spy.mock.calls[0][1];
expect(arg.name).toBe('ApiError');
expect(arg.message).toBe('[redacted]');
spy.mockRestore();
});
it('does not misclassify data objects with .message but no .name', () => {
// Regression: a summary object like {hasLogEntry, logType, message}
// was being treated as an Error in production, producing the
// misleading log line "Object { name: 'Error', message: '[redacted]' }".
SL.setProductionMode(true);
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
SL.log('Calling handlers with data:', {
hasLogEntry: true,
logType: 'milestone',
message: 'Phase complete...',
});
const arg = spy.mock.calls[0][1];
// Should fall through to the generic-object redaction, not the
// error-like branch.
expect(arg).toBe('[Object]');
spy.mockRestore();
});
});
});