/ src / lib / server / storage-mcp.test.ts
storage-mcp.test.ts
  1  import { describe, it, before, after } from 'node:test'
  2  import assert from 'node:assert/strict'
  3  import Database from 'better-sqlite3'
  4  import os from 'node:os'
  5  import path from 'node:path'
  6  import fs from 'node:fs'
  7  
  8  /**
  9   * Tests for the MCP server storage operations (loadMcpServers, saveMcpServers, deleteMcpServer).
 10   *
 11   * Since storage.ts uses a hardcoded DB path with module-level initialization,
 12   * we replicate the same SQL pattern against a temporary SQLite database.
 13   */
 14  
 15  const TABLE = 'mcp_servers'
 16  
 17  let dbPath: string
 18  let db: InstanceType<typeof Database>
 19  
 20  // --- Replicated storage helpers (mirror storage.ts logic) ---
 21  
 22  function loadMcpServers(): Record<string, any> {
 23    const rows = db.prepare(`SELECT id, data FROM ${TABLE}`).all() as { id: string; data: string }[]
 24    const result: Record<string, any> = {}
 25    for (const row of rows) {
 26      try {
 27        result[row.id] = JSON.parse(row.data)
 28      } catch {
 29        // skip malformed
 30      }
 31    }
 32    return result
 33  }
 34  
 35  function saveMcpServers(m: Record<string, any>) {
 36    const existingRows = db.prepare(`SELECT id FROM ${TABLE}`).all() as { id: string }[]
 37    const nextIds = new Set(Object.keys(m))
 38    const toDelete = existingRows.map((r) => r.id).filter((id) => !nextIds.has(id))
 39    const upsert = db.prepare(`INSERT OR REPLACE INTO ${TABLE} (id, data) VALUES (?, ?)`)
 40    const del = db.prepare(`DELETE FROM ${TABLE} WHERE id = ?`)
 41    const transaction = db.transaction(() => {
 42      for (const id of toDelete) {
 43        del.run(id)
 44      }
 45      for (const [id, val] of Object.entries(m)) {
 46        upsert.run(id, JSON.stringify(val))
 47      }
 48    })
 49    transaction()
 50  }
 51  
 52  function deleteMcpServer(id: string) {
 53    db.prepare(`DELETE FROM ${TABLE} WHERE id = ?`).run(id)
 54  }
 55  
 56  // --- Test setup / teardown ---
 57  
 58  before(() => {
 59    dbPath = path.join(os.tmpdir(), `test-storage-mcp-${Date.now()}.db`)
 60    db = new Database(dbPath)
 61    db.pragma('journal_mode = WAL')
 62    db.exec(`CREATE TABLE IF NOT EXISTS ${TABLE} (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)
 63  })
 64  
 65  after(() => {
 66    db.close()
 67    try { fs.unlinkSync(dbPath) } catch { /* ignore */ }
 68    try { fs.unlinkSync(dbPath + '-wal') } catch { /* ignore */ }
 69    try { fs.unlinkSync(dbPath + '-shm') } catch { /* ignore */ }
 70  })
 71  
 72  // --- Tests ---
 73  
 74  describe('MCP server storage', () => {
 75    it('saveMcpServers saves a config and it is retrievable', () => {
 76      const config = { id: 'srv-1', name: 'Test MCP', url: 'http://localhost:9000' }
 77      saveMcpServers({ 'srv-1': config })
 78  
 79      const row = db.prepare(`SELECT data FROM ${TABLE} WHERE id = ?`).get('srv-1') as { data: string } | undefined
 80      assert.ok(row, 'row should exist after save')
 81      assert.deepStrictEqual(JSON.parse(row.data), config)
 82    })
 83  
 84    it('loadMcpServers returns all saved configs', () => {
 85      saveMcpServers({
 86        'srv-1': { id: 'srv-1', name: 'First' },
 87        'srv-2': { id: 'srv-2', name: 'Second' },
 88      })
 89  
 90      const all = loadMcpServers()
 91      assert.ok('srv-1' in all)
 92      assert.ok('srv-2' in all)
 93      assert.equal(all['srv-2'].name, 'Second')
 94    })
 95  
 96    it('loadMcpServers returns empty object when table is empty', () => {
 97      db.exec(`DELETE FROM ${TABLE}`)
 98      const all = loadMcpServers()
 99      assert.deepStrictEqual(all, {})
100    })
101  
102    it('saveMcpServers with same id updates the record', () => {
103      saveMcpServers({ 'srv-u': { id: 'srv-u', name: 'Original' } })
104      saveMcpServers({ 'srv-u': { id: 'srv-u', name: 'Updated' } })
105  
106      const all = loadMcpServers()
107      assert.equal(all['srv-u'].name, 'Updated')
108  
109      // only one row for that id
110      const count = (db.prepare(`SELECT COUNT(*) as c FROM ${TABLE} WHERE id = ?`).get('srv-u') as { c: number }).c
111      assert.equal(count, 1)
112    })
113  
114    it('saveMcpServers removes records omitted from the next save payload', () => {
115      saveMcpServers({
116        'srv-a': { id: 'srv-a', name: 'A' },
117        'srv-b': { id: 'srv-b', name: 'B' },
118      })
119      saveMcpServers({
120        'srv-b': { id: 'srv-b', name: 'B2' },
121      })
122  
123      const all = loadMcpServers()
124      assert.equal('srv-a' in all, false)
125      assert.equal(all['srv-b'].name, 'B2')
126    })
127  
128    it('deleteMcpServer removes the record', () => {
129      saveMcpServers({ 'srv-d': { id: 'srv-d', name: 'ToDelete' } })
130      deleteMcpServer('srv-d')
131  
132      const row = db.prepare(`SELECT data FROM ${TABLE} WHERE id = ?`).get('srv-d')
133      assert.equal(row, undefined)
134    })
135  
136    it('deleteMcpServer does not throw for nonexistent id', () => {
137      assert.doesNotThrow(() => {
138        deleteMcpServer('nonexistent-id-xyz')
139      })
140    })
141  
142    it('round-trip: save multiple, load all, verify count and data', () => {
143      db.exec(`DELETE FROM ${TABLE}`)
144  
145      const configs: Record<string, any> = {}
146      for (let i = 0; i < 5; i++) {
147        configs[`rt-${i}`] = { id: `rt-${i}`, name: `Server ${i}`, port: 3000 + i }
148      }
149      saveMcpServers(configs)
150  
151      const all = loadMcpServers()
152      assert.equal(Object.keys(all).length, 5)
153      for (let i = 0; i < 5; i++) {
154        assert.equal(all[`rt-${i}`].port, 3000 + i)
155      }
156    })
157  
158    it('data integrity: saved JSON is correctly parsed back with all fields', () => {
159      db.exec(`DELETE FROM ${TABLE}`)
160  
161      const config = {
162        id: 'integrity-1',
163        name: 'Full Config',
164        url: 'https://mcp.example.com',
165        transport: 'stdio',
166        command: 'npx',
167        args: ['-y', '@modelcontextprotocol/server-filesystem'],
168        env: { HOME: '/tmp' },
169        enabled: true,
170        createdAt: 1700000000000,
171        tags: ['production', 'filesystem'],
172        nested: { deep: { value: 42 } },
173      }
174  
175      saveMcpServers({ 'integrity-1': config })
176      const loaded = loadMcpServers()
177  
178      assert.deepStrictEqual(loaded['integrity-1'], config)
179      assert.ok(Array.isArray(loaded['integrity-1'].args))
180      assert.equal(loaded['integrity-1'].args.length, 2)
181      assert.equal(loaded['integrity-1'].nested.deep.value, 42)
182      assert.equal(typeof loaded['integrity-1'].enabled, 'boolean')
183    })
184  })