From a0a697fa9cfe9cedc77e8f51059197e76ba54925 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:00:56 +0000 Subject: [PATCH] feat(test): add unit tests for SQLiteFileSystemProvider.writeFile Introduces unit tests for `SQLiteFileSystemProvider.writeFile` to improve test coverage for core file system operations. - Created `tests/mocks/vscode.ts` to mock `vscode` module dependencies (`Uri`, `FileSystemError`, etc.). - Created `tsconfig.test.json` to map `vscode` imports to the mock implementation during tests. - Added `tests/unit/virtualFileSystem.test.ts` covering: - Happy path for text content updates. - Handling of binary content (invalid UTF-8). - Error handling for invalid Row IDs and permission checks (`__create__.sql`). - Updated `package.json` test script to use `tsconfig.test.json` for all unit tests. This enables testing of `src/virtualFileSystem.ts` which was previously untestable due to direct `vscode` imports. Co-authored-by: zknpr <96851588+zknpr@users.noreply.github.com> --- package.json | 2 +- tests/mocks/vscode.ts | 77 +++++++++++++++ tests/unit/virtualFileSystem.test.ts | 139 +++++++++++++++++++++++++++ tsconfig.test.json | 9 ++ 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 tests/mocks/vscode.ts create mode 100644 tests/unit/virtualFileSystem.test.ts create mode 100644 tsconfig.test.json diff --git a/package.json b/package.json index b5cb5b9..bfe2554 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "build": "npm run compile", "package": "npm run build && npx vsce package", "package-all": "npm run build && npx vsce package", - "test": "npx tsx --test tests/unit/*.test.ts" + "test": "npx tsx --tsconfig tsconfig.test.json --test tests/unit/*.test.ts" }, "devDependencies": { "@types/node": "^25.2.0", diff --git a/tests/mocks/vscode.ts b/tests/mocks/vscode.ts new file mode 100644 index 0000000..a528738 --- /dev/null +++ b/tests/mocks/vscode.ts @@ -0,0 +1,77 @@ + +export class Uri { + scheme: string; + authority: string; + path: string; + query: string; + fragment: string; + + constructor(scheme: string, authority: string, path: string, query: string, fragment: string) { + this.scheme = scheme; + this.authority = authority; + this.path = path; + this.query = query; + this.fragment = fragment; + } + + static parse(str: string) { + // simple parser + // We assume file URI or similar + // For tests, we can just return a dummy + const url = new URL(str); + return new Uri(url.protocol.replace(":", ""), url.host, url.pathname, url.search, url.hash); + } + + static file(path: string) { + return new Uri("file", "", path, "", ""); + } + + with(change: any) { + return new Uri( + change.scheme || this.scheme, + change.authority || this.authority, + change.path || this.path, + change.query || this.query, + change.fragment || this.fragment + ); + } + + toString() { + return `${this.scheme}://${this.authority}${this.path}`; + } + + fsPath() { + return this.path; + } +} + +export class FileSystemError extends Error { + code: string; + constructor(message: string, code: string) { + super(message); + this.code = code; + } + static FileNotFound(uri?: Uri) { return new FileSystemError("File not found", "FileNotFound"); } + static NoPermissions(message?: string) { return new FileSystemError(message || "No permissions", "NoPermissions"); } + static Unavailable(message?: string) { return new FileSystemError(message || "Unavailable", "Unavailable"); } +} + +export class EventEmitter { + listeners: Array<(e: T) => any> = []; + event = (listener: (e: T) => any) => { + this.listeners.push(listener); + return { dispose: () => {} }; + } + fire(data: T) { this.listeners.forEach(l => l(data)); } + dispose() { this.listeners = []; } +} + +export class Disposable { + callOnDispose: () => void; + constructor(callOnDispose: () => void) { this.callOnDispose = callOnDispose; } + dispose() { if (this.callOnDispose) this.callOnDispose(); } +} + +export enum FileType { File = 1, Directory = 2 } +export enum FilePermission { Readonly = 1 } +export enum FileChangeType { Changed = 1, Created = 2, Deleted = 3 } diff --git a/tests/unit/virtualFileSystem.test.ts b/tests/unit/virtualFileSystem.test.ts new file mode 100644 index 0000000..42fb9bc --- /dev/null +++ b/tests/unit/virtualFileSystem.test.ts @@ -0,0 +1,139 @@ +import { describe, it, beforeEach, afterEach, mock } from 'node:test'; +import assert from 'node:assert'; +import * as vscode from 'vscode'; +import { SQLiteFileSystemProvider } from '../../src/virtualFileSystem'; +import { DocumentRegistry } from '../../src/documentRegistry'; + +// Mock Document Interface matching DatabaseDocument behavior used by FileSystemProvider +interface MockDocument { + databaseOperations: { + updateCell: (table: string, rowId: number, column: string, value: any) => Promise; + }; + recordExternalModification: (mod: any) => void; +} + +describe('SQLiteFileSystemProvider', () => { + let provider: SQLiteFileSystemProvider; + let mockDocument: MockDocument; + const documentKey = 'mock-doc-key'; + + // Helper to create URI. + // Format: ///// + const createUri = (path: string) => vscode.Uri.file(path).with({ scheme: 'sqlite-explorer' }); + + beforeEach(() => { + provider = new SQLiteFileSystemProvider(); + + mockDocument = { + databaseOperations: { + updateCell: async () => {} // Default mock + }, + recordExternalModification: () => {} + }; + + // Populate Registry + DocumentRegistry.set(documentKey, mockDocument as any); + }); + + afterEach(() => { + DocumentRegistry.clear(); + mock.restoreAll(); + }); + + it('writeFile should update cell with valid text content', async (t) => { + const table = 'users'; + const rowId = '1'; + const column = 'name'; + const filename = `${column}.txt`; + const path = `/${documentKey}/${table}/group/${rowId}/${filename}`; + const uri = createUri(path); + + const contentStr = 'Alice'; + const content = new TextEncoder().encode(contentStr); + + // Mock updateCell to verify arguments + const updateCellMock = t.mock.fn(async (tbl: string, rid: number, col: string, val: any) => { + assert.strictEqual(tbl, table); + assert.strictEqual(rid, 1); + assert.strictEqual(col, column); + assert.strictEqual(val, contentStr); + }); + mockDocument.databaseOperations.updateCell = updateCellMock; + + // Mock recordExternalModification + const recordMock = t.mock.fn(); + mockDocument.recordExternalModification = recordMock; + + await provider.writeFile(uri, content, { create: false, overwrite: true }); + + assert.strictEqual(updateCellMock.mock.callCount(), 1); + assert.strictEqual(recordMock.mock.callCount(), 1); + + // Verify modification record + const callArgs = recordMock.mock.calls[0].arguments; + assert.strictEqual(callArgs[0].newValue, contentStr); + assert.strictEqual(callArgs[0].modificationType, 'cell_update'); + }); + + it('writeFile should handle binary content (invalid utf-8) by saving as Uint8Array', async (t) => { + const table = 'data'; + const rowId = '2'; + const column = 'blob'; + const path = `/${documentKey}/${table}/group/${rowId}/${column}.bin`; + const uri = createUri(path); + + // Invalid UTF-8 sequence (0xFF cannot appear in valid UTF-8) + const content = new Uint8Array([0xFF, 0xFF]); + + const updateCellMock = t.mock.fn(async (tbl: string, rid: number, col: string, val: any) => { + assert.ok(val instanceof Uint8Array); + assert.deepStrictEqual(val, content); + }); + mockDocument.databaseOperations.updateCell = updateCellMock; + mockDocument.recordExternalModification = t.mock.fn(); + + await provider.writeFile(uri, content, { create: false, overwrite: true }); + + assert.strictEqual(updateCellMock.mock.callCount(), 1); + }); + + it('writeFile should throw NoPermissions for __create__.sql', async () => { + const path = `/${documentKey}/table/group/__create__.sql/foo.sql`; + const uri = createUri(path); + + await assert.rejects( + async () => await provider.writeFile(uri, new Uint8Array(), { create: false, overwrite: true }), + (err: any) => { + assert.strictEqual(err.code, 'NoPermissions'); + return true; + } + ); + }); + + it('writeFile should throw Unavailable for invalid Row ID', async () => { + const path = `/${documentKey}/table/group/invalid-id/col.txt`; + const uri = createUri(path); + + await assert.rejects( + async () => await provider.writeFile(uri, new Uint8Array(), { create: false, overwrite: true }), + (err: any) => { + assert.strictEqual(err.code, 'Unavailable'); + assert.match(err.message, /Invalid Row ID/); + return true; + } + ); + }); + + it('writeFile should throw FileNotFound if document not found', async () => { + const path = `/non-existent-key/table/group/1/col.txt`; + const uri = createUri(path); + + await assert.rejects( + async () => await provider.writeFile(uri, new Uint8Array(), { create: false, overwrite: true }), + (err: any) => { + assert.strictEqual(err.code, 'FileNotFound'); + return true; + } + ); + }); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..4dc3837 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "vscode": ["./tests/mocks/vscode.ts"] + } + } +}