Skip to main content
Glama
YUChoe

SQLite MCP Server

by YUChoe
deleteData.test.ts13.1 kB
/** * DELETE 데이터 도구 테스트 */ import { deleteDataTool } from './deleteData'; import { DatabaseManager } from '../database/DatabaseManager'; import fs from 'fs'; import path from 'path'; // 테스트용 데이터베이스 경로 const TEST_DB_DIR = 'test-dbs'; const getTestDbPath = () => path.join(TEST_DB_DIR, `test-delete-${Date.now()}.db`); // 테스트 전 설정 beforeAll(() => { if (!fs.existsSync(TEST_DB_DIR)) { fs.mkdirSync(TEST_DB_DIR, { recursive: true }); } }); describe('deleteDataTool', () => { let testDbPath: string; let dbManager: DatabaseManager; beforeEach(() => { testDbPath = getTestDbPath(); dbManager = new DatabaseManager(); // 테스트용 테이블과 데이터 생성 dbManager.executeQuery(testDbPath, ` CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE, age INTEGER ) `); // 테스트 데이터 삽입 dbManager.executeQuery(testDbPath, 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)', ['Alice', 'alice@example.com', 25] ); dbManager.executeQuery(testDbPath, 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)', ['Bob', 'bob@example.com', 30] ); dbManager.executeQuery(testDbPath, 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)', ['Charlie', 'charlie@example.com', 35] ); }); afterEach(() => { // 데이터베이스 연결 닫기 dbManager.closeAllDatabases(); // 테스트 데이터베이스 정리 if (fs.existsSync(testDbPath)) { try { fs.unlinkSync(testDbPath); } catch (error) { // Windows에서 파일이 잠겨있을 수 있으므로 무시 console.warn(`파일 삭제 실패: ${testDbPath}`, error); } } }); test('조건부 DELETE 쿼리 실행', async () => { const result = await deleteDataTool.handler({ dbPath: testDbPath, query: 'DELETE FROM users WHERE age > ?', params: [30] }); expect(result.structuredContent).toEqual({ success: true, rowsAffected: 1 }); expect(result.content[0].text).toContain('데이터가 성공적으로 삭제되었습니다'); expect(result.content[0].text).toContain('삭제된 행 수: 1'); // 삭제 후 데이터 확인 const selectResult = dbManager.executeQuery(testDbPath, 'SELECT COUNT(*) as count FROM users'); expect(selectResult.data?.[0].count).toBe(2); }); test('전체 테이블 DELETE 실행', async () => { const result = await deleteDataTool.handler({ dbPath: testDbPath, query: 'DELETE FROM users' }); expect(result.structuredContent).toEqual({ success: true, rowsAffected: 3 }); // 삭제 후 데이터 확인 const selectResult = dbManager.executeQuery(testDbPath, 'SELECT COUNT(*) as count FROM users'); expect(selectResult.data?.[0].count).toBe(0); }); test('조건에 맞는 행이 없는 경우', async () => { const result = await deleteDataTool.handler({ dbPath: testDbPath, query: 'DELETE FROM users WHERE age > ?', params: [100] }); expect(result.structuredContent).toEqual({ success: true, rowsAffected: 0 }); expect(result.content[0].text).toContain('삭제된 행 수: 0'); }); test('잘못된 DELETE 쿼리 - SELECT 쿼리 시도', async () => { const result = await deleteDataTool.handler({ dbPath: testDbPath, query: 'SELECT * FROM users' }); expect(result.structuredContent).toEqual({ success: false, error: 'DELETE 쿼리만 허용됩니다' }); }); test('잘못된 DELETE 쿼리 - FROM 절 누락', async () => { const result = await deleteDataTool.handler({ dbPath: testDbPath, query: 'DELETE users WHERE id = 1' }); expect(result.structuredContent).toEqual({ success: false, error: 'DELETE 쿼리에 FROM 절이 필요합니다' }); }); test('위험한 SQL 구문 차단 - DROP TABLE', async () => { const result = await deleteDataTool.handler({ dbPath: testDbPath, query: 'DELETE FROM users; DROP TABLE users;' }); expect(result.structuredContent).toEqual({ success: false, error: '허용되지 않는 SQL 구문이 포함되어 있습니다' }); }); test('존재하지 않는 테이블 삭제 시도', async () => { const result = await deleteDataTool.handler({ dbPath: testDbPath, query: 'DELETE FROM nonexistent_table' }); expect(result.structuredContent?.success).toBe(false); expect(result.structuredContent?.error).toContain('no such table'); }); test('잘못된 데이터베이스 경로', async () => { const result = await deleteDataTool.handler({ dbPath: '/invalid/path/database.db', query: 'DELETE FROM users WHERE id = 1' }); expect(result.structuredContent?.success).toBe(false); expect(result.structuredContent?.error).toBeDefined(); }); test('매개변수화된 DELETE 쿼리', async () => { const result = await deleteDataTool.handler({ dbPath: testDbPath, query: 'DELETE FROM users WHERE name = ? AND age = ?', params: ['Alice', 25] }); expect(result.structuredContent).toEqual({ success: true, rowsAffected: 1 }); // Alice가 삭제되었는지 확인 const selectResult = dbManager.executeQuery(testDbPath, 'SELECT * FROM users WHERE name = ?', ['Alice']); expect(selectResult.data?.length).toBe(0); }); }); /** * Property-Based Tests for DELETE operation accuracy * **Feature: sqlite-mcp-server, Property 7: DELETE 작업 정확성** * **Validates: Requirements 5.1, 5.2** */ import * as fc from 'fast-check'; describe('Property-Based Tests', () => { const testDbDir = 'test-dbs'; let propertyTestDbPath: string; beforeEach(() => { // 테스트 데이터베이스 디렉토리 생성 if (!fs.existsSync(testDbDir)) { fs.mkdirSync(testDbDir, { recursive: true }); } // 고유한 테스트 데이터베이스 파일 경로 생성 propertyTestDbPath = path.join(testDbDir, `test-delete-property-${Date.now()}.db`); }); afterEach(() => { // 데이터베이스 연결 닫기 const dbManager = new DatabaseManager(); dbManager.closeAllDatabases(); // 테스트 후 데이터베이스 파일 정리 if (fs.existsSync(propertyTestDbPath)) { try { fs.unlinkSync(propertyTestDbPath); } catch (error) { // Windows에서 파일이 잠겨있을 수 있으므로 무시 console.warn(`파일 삭제 실패: ${propertyTestDbPath}`, error); } } }); test('**Feature: sqlite-mcp-server, Property 7: DELETE 작업 정확성**', async () => { await fc.assert( fc.asyncProperty( // 테이블 이름 생성기 fc.string({ minLength: 1, maxLength: 20 }).filter(s => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s)), // 초기 데이터 생성기 fc.array( fc.record({ id: fc.integer({ min: 1, max: 1000 }), name: fc.string({ minLength: 1, maxLength: 50 }), category: fc.oneof( fc.constant('A'), fc.constant('B'), fc.constant('C') ) }), { minLength: 1, maxLength: 15 } ).map(arr => { // ID 중복 제거 const uniqueIds = new Set(); return arr.filter(item => { if (uniqueIds.has(item.id)) { return false; } uniqueIds.add(item.id); return true; }); }).filter(arr => arr.length > 0), // 삭제 조건 생성기 (카테고리 기반) fc.oneof( fc.constant('A'), fc.constant('B'), fc.constant('C'), fc.constant('D') // 존재하지 않는 카테고리 ), async (tableName, initialData, deleteCategory) => { // 고유한 데이터베이스 경로 생성 const dbPath = path.join(testDbDir, `test-property-${Date.now()}-${Math.random()}.db`); try { const dbManager = new DatabaseManager(); // 테이블 생성 dbManager.executeQuery(dbPath, ` CREATE TABLE ${tableName} ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL ) `); // 초기 데이터 삽입 for (const data of initialData) { dbManager.executeQuery(dbPath, `INSERT INTO ${tableName} (id, name, category) VALUES (?, ?, ?)`, [data.id, data.name, data.category] ); } // 삭제 전 상태 확인 const beforeAllResult = dbManager.executeQuery(dbPath, `SELECT * FROM ${tableName}`); const beforeTargetResult = dbManager.executeQuery(dbPath, `SELECT * FROM ${tableName} WHERE category = ?`, [deleteCategory] ); const totalRowsBefore = beforeAllResult.data?.length || 0; const targetRowsBefore = beforeTargetResult.data?.length || 0; // DELETE 실행 const deleteResult = await deleteDataTool.handler({ dbPath, query: `DELETE FROM ${tableName} WHERE category = ?`, params: [deleteCategory] }); // 삭제 후 상태 확인 const afterAllResult = dbManager.executeQuery(dbPath, `SELECT * FROM ${tableName}`); const afterTargetResult = dbManager.executeQuery(dbPath, `SELECT * FROM ${tableName} WHERE category = ?`, [deleteCategory] ); const totalRowsAfter = afterAllResult.data?.length || 0; const targetRowsAfter = afterTargetResult.data?.length || 0; // Property 검증: DELETE 작업 정확성 if (deleteResult.structuredContent?.success) { // 1. 삭제된 행 수가 정확해야 함 expect(deleteResult.structuredContent.rowsAffected).toBe(targetRowsBefore); // 2. 조건에 맞는 행들이 모두 삭제되어야 함 expect(targetRowsAfter).toBe(0); // 3. 전체 행 수가 올바르게 감소해야 함 expect(totalRowsAfter).toBe(totalRowsBefore - targetRowsBefore); // 4. 삭제되지 않은 행들은 그대로 남아있어야 함 if (afterAllResult.data) { for (const remainingRow of afterAllResult.data) { // 남은 행들은 삭제 조건에 맞지 않아야 함 expect(remainingRow.category).not.toBe(deleteCategory); // 남은 행들은 원본 데이터에 존재했던 것이어야 함 const originalRow = initialData.find(d => d.id === remainingRow.id); expect(originalRow).toBeDefined(); expect(originalRow?.name).toBe(remainingRow.name); expect(originalRow?.category).toBe(remainingRow.category); } } // 5. 삭제 조건에 맞지 않는 행들의 수는 변하지 않아야 함 const remainingCategories = new Set(initialData .filter(d => d.category !== deleteCategory) .map(d => d.category) ); for (const category of remainingCategories) { const beforeCategoryResult = dbManager.executeQuery(dbPath, `SELECT COUNT(*) as count FROM ${tableName} WHERE category = ?`, [category] ); const originalCategoryCount = initialData.filter(d => d.category === category).length; // 원본 데이터와 현재 데이터베이스의 해당 카테고리 행 수가 같아야 함 expect(beforeCategoryResult.data?.[0]?.count).toBe(originalCategoryCount); } } } finally { // 데이터베이스 연결 닫기 const dbManager = new DatabaseManager(); dbManager.closeDatabase(dbPath); // 테스트 후 정리 if (fs.existsSync(dbPath)) { try { fs.unlinkSync(dbPath); } catch (error) { // Windows에서 파일이 잠겨있을 수 있으므로 무시 console.warn(`파일 삭제 실패: ${dbPath}`, error); } } } } ), { numRuns: 100 } ); }, 30000); // 30초 타임아웃 });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/YUChoe/sqlite-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server