|
| 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