Skip to content

Commit d39ff57

Browse files
test: Add test suite for LoggingDatabaseOperations sanitizeValue and PII redaction
This patch introduces `tests/unit/loggingDatabaseOperations.test.ts` to comprehensively test the internal `sanitizeValue` method and the regex-based PII redaction logic within `LoggingDatabaseOperations`. It utilizes a mocked database layer and VS Code output channel to verify output formats and redaction accuracy (e.g., handling nulls, truncating strings, and masking emails, phones, API keys, etc.) indirectly through the public `executeQuery` interface, ensuring functionality without compromising encapsulation. Co-authored-by: zknpr <96851588+zknpr@users.noreply.github.com>
1 parent 0573573 commit d39ff57

1 file changed

Lines changed: 160 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import './vscode_mock_setup';
2+
import { describe, it, beforeEach } from 'node:test';
3+
import assert from 'node:assert';
4+
import * as vsc from 'vscode';
5+
import { LoggingDatabaseOperations } from '../../src/loggingDatabaseOperations';
6+
import type { DatabaseOperations, CellValue, QueryResultSet, ModificationEntry, CellUpdate, TableQueryOptions, TableCountOptions, SchemaSnapshot, ColumnMetadata, ColumnDefinition } from '../../src/core/types';
7+
8+
class MockDatabaseOperations implements DatabaseOperations {
9+
engineKind = 'sqlite' as const;
10+
async executeQuery(sql: string, params?: CellValue[]): Promise<QueryResultSet[]> { return []; }
11+
async serializeDatabase(name: string): Promise<Uint8Array> { return new Uint8Array(); }
12+
async applyModifications(mods: ModificationEntry[], signal?: AbortSignal): Promise<void> {}
13+
async undoModification(mod: ModificationEntry): Promise<void> {}
14+
async redoModification(mod: ModificationEntry): Promise<void> {}
15+
async flushChanges(signal?: AbortSignal): Promise<void> {}
16+
async discardModifications(mods: ModificationEntry[], signal?: AbortSignal): Promise<void> {}
17+
async updateCell(table: string, rowId: number, column: string, value: CellValue, patch?: string): Promise<void> {}
18+
async insertRow(table: string, data: Record<string, CellValue>): Promise<number> { return 1; }
19+
async deleteRows(table: string, rowIds: number[]): Promise<void> {}
20+
async deleteColumns(table: string, columns: string[], dropDependentIndexes?: string[]): Promise<void> {}
21+
async findDependentIndexes(table: string, columns: string[]): Promise<string[]> { return []; }
22+
async createTable(table: string, columns: ColumnDefinition[]): Promise<void> {}
23+
async updateCellBatch(table: string, updates: CellUpdate[]): Promise<void> {}
24+
async addColumn(table: string, column: string, type: string, defaultValue?: string): Promise<void> {}
25+
async fetchTableData(table: string, options: TableQueryOptions): Promise<QueryResultSet> { return { columns: [], rows: [], totalRows: 0 }; }
26+
async fetchTableCount(table: string, options: TableCountOptions): Promise<number> { return 0; }
27+
async fetchSchema(): Promise<SchemaSnapshot> { return { tables: [], views: [], indexes: [] }; }
28+
async getTableInfo(table: string): Promise<ColumnMetadata[]> { return []; }
29+
async getPragmas(): Promise<Record<string, CellValue>> { return {}; }
30+
async setPragma(pragma: string, value: CellValue): Promise<void> {}
31+
async ping(): Promise<boolean> { return true; }
32+
async writeToFile(path: string): Promise<void> {}
33+
}
34+
35+
class MockOutputChannel implements vsc.OutputChannel {
36+
name = 'Mock Output';
37+
lines: string[] = [];
38+
append(value: string): void {}
39+
appendLine(value: string): void {
40+
this.lines.push(value);
41+
}
42+
replace(value: string): void {}
43+
clear(): void { this.lines = []; }
44+
show(preserveFocus?: boolean): void {}
45+
hide(): void {}
46+
dispose(): void {}
47+
}
48+
49+
describe('LoggingDatabaseOperations', () => {
50+
let mockDb: MockDatabaseOperations;
51+
let mockChannel: MockOutputChannel;
52+
let logger: LoggingDatabaseOperations;
53+
54+
beforeEach(() => {
55+
mockDb = new MockDatabaseOperations();
56+
mockChannel = new MockOutputChannel();
57+
logger = new LoggingDatabaseOperations(mockDb, 'test.db', mockChannel);
58+
});
59+
60+
describe('sanitizeValue', () => {
61+
it('should sanitize null', async () => {
62+
await logger.executeQuery('SELECT *', [null]);
63+
assert.ok(mockChannel.lines[0].includes('params: [null]'));
64+
});
65+
66+
it('should sanitize undefined', async () => {
67+
await logger.executeQuery('SELECT *', [undefined as any]);
68+
assert.ok(mockChannel.lines[0].includes('params: [undefined]'));
69+
});
70+
71+
it('should sanitize normal string', async () => {
72+
await logger.executeQuery('SELECT *', ['hello']);
73+
assert.ok(mockChannel.lines[0].includes('params: ["hello"]'));
74+
});
75+
76+
it('should truncate long string (> 100 chars)', async () => {
77+
const longStr = 'a'.repeat(150);
78+
await logger.executeQuery('SELECT *', [longStr]);
79+
const expectedStr = 'a'.repeat(100);
80+
// Since it's > 32 chars of 'a', it will be replaced by [REDACTED_HEX]
81+
assert.ok(mockChannel.lines[0].includes(`params: ["[REDACTED_HEX]...[TRUNCATED]"]`));
82+
});
83+
84+
it('should truncate long non-hex string (> 100 chars)', async () => {
85+
const longStr = 'z'.repeat(150);
86+
await logger.executeQuery('SELECT *', [longStr]);
87+
const expectedStr = 'z'.repeat(100);
88+
assert.ok(mockChannel.lines[0].includes(`params: ["${expectedStr}...[TRUNCATED]"]`));
89+
});
90+
91+
it('should sanitize Uint8Array as BLOB', async () => {
92+
const blob = new Uint8Array([1, 2, 3]);
93+
await logger.executeQuery('SELECT *', [blob]);
94+
assert.ok(mockChannel.lines[0].includes('params: [[BLOB 3 bytes]]'));
95+
});
96+
97+
it('should sanitize object with buffer property as BLOB', async () => {
98+
const bufferObj = { buffer: new ArrayBuffer(4), byteLength: 4 };
99+
await logger.executeQuery('SELECT *', [bufferObj as any]);
100+
assert.ok(mockChannel.lines[0].includes('params: [[BLOB 4 bytes]]'));
101+
});
102+
103+
it('should sanitize standard objects to JSON', async () => {
104+
await logger.executeQuery('SELECT *', [{ foo: 'bar' } as any]);
105+
assert.ok(mockChannel.lines[0].includes('params: [{"foo":"bar"}...]'));
106+
});
107+
108+
it('should fallback to [Object] for circular objects', async () => {
109+
const circular: any = {};
110+
circular.self = circular;
111+
await logger.executeQuery('SELECT *', [circular]);
112+
assert.ok(mockChannel.lines[0].includes('params: [[Object]]'));
113+
});
114+
115+
it('should pass through numbers', async () => {
116+
await logger.executeQuery('SELECT *', [123.45]);
117+
assert.ok(mockChannel.lines[0].includes('params: [123.45]'));
118+
});
119+
120+
it('should pass through booleans', async () => {
121+
await logger.executeQuery('SELECT *', [true, false] as any);
122+
assert.ok(mockChannel.lines[0].includes('params: [true, false]'));
123+
});
124+
});
125+
126+
describe('PII/Secret Masking', () => {
127+
it('should mask email addresses', async () => {
128+
await logger.executeQuery("SELECT 'user@example.com'");
129+
assert.ok(mockChannel.lines[0].includes('***@***.***'));
130+
assert.ok(!mockChannel.lines[0].includes('user@example.com'));
131+
});
132+
133+
it('should mask phone numbers', async () => {
134+
await logger.executeQuery("SELECT '+1-234-567-8901'");
135+
assert.ok(mockChannel.lines[0].includes('***-***-****'));
136+
assert.ok(!mockChannel.lines[0].includes('+1-234-567-8901'));
137+
});
138+
139+
it('should mask API keys', async () => {
140+
await logger.executeQuery("SELECT 'sk_live_abcdefghijklmnopqr'");
141+
assert.ok(mockChannel.lines[0].includes('sk_live_[REDACTED]'));
142+
assert.ok(!mockChannel.lines[0].includes('abcdefghijklmnopqr'));
143+
});
144+
145+
it('should mask long hex strings', async () => {
146+
await logger.executeQuery("SELECT 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'");
147+
assert.ok(mockChannel.lines[0].includes('[REDACTED_HEX]'));
148+
});
149+
150+
it('should mask credit card numbers', async () => {
151+
await logger.executeQuery("SELECT '1234-5678-9012-3456'");
152+
assert.ok(mockChannel.lines[0].includes('****-****-****-****'));
153+
});
154+
155+
it('should mask SSN patterns', async () => {
156+
await logger.executeQuery("SELECT '123-45-6789'");
157+
assert.ok(mockChannel.lines[0].includes('***-**-****'));
158+
});
159+
});
160+
});

0 commit comments

Comments
 (0)