Skip to main content
Glama

Git MCP Server

kvProvider.test.ts7.26 kB
/** * @fileoverview Unit tests for the KvProvider. * @module tests/storage/providers/cloudflare/kvProvider.test */ import { McpError } from '../../../../src/types-global/errors.js'; import { KvProvider } from '../../../../src/storage/providers/cloudflare/kvProvider.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { RequestContext } from '../../../../src/utils/index.js'; import { requestContextService } from '../../../../src/utils/index.js'; // Mock KVNamespace const createMockKvNamespace = () => ({ get: vi.fn(), put: vi.fn(), delete: vi.fn(), list: vi.fn(), }); describe('KvProvider', () => { let kvProvider: KvProvider; let mockKv: ReturnType<typeof createMockKvNamespace>; let context: RequestContext; beforeEach(() => { mockKv = createMockKvNamespace(); kvProvider = new KvProvider(mockKv as any); context = requestContextService.createRequestContext({ operation: 'test-kv-provider', }); }); describe('get', () => { it('should return null if key not found', async () => { mockKv.get.mockResolvedValue(null); const result = await kvProvider.get('tenant-1', 'key-1', context); expect(result).toBeNull(); expect(mockKv.get).toHaveBeenCalledWith('tenant-1:key-1', 'json'); }); it('should return parsed JSON object if found', async () => { const storedObject = { data: 'test-data' }; mockKv.get.mockResolvedValue(storedObject); const result = await kvProvider.get<{ data: string }>( 'tenant-1', 'key-1', context, ); expect(result).toEqual(storedObject); }); it('should throw McpError on JSON parsing error', async () => { const parsingError = new Error('Invalid JSON'); mockKv.get.mockRejectedValue(parsingError); await expect( kvProvider.get('tenant-1', 'key-1', context), ).rejects.toThrow(McpError); }); }); describe('set', () => { it('should call put with correct key and value', async () => { const value = { data: 'test-data' }; await kvProvider.set('tenant-1', 'key-1', value, context); expect(mockKv.put).toHaveBeenCalledWith( 'tenant-1:key-1', JSON.stringify(value), { expirationTtl: undefined }, ); }); it('should include expirationTtl if ttl is provided', async () => { const value = { data: 'test' }; await kvProvider.set('tenant-1', 'key-1', value, context, { ttl: 3600 }); expect(mockKv.put).toHaveBeenCalledWith( 'tenant-1:key-1', JSON.stringify(value), { expirationTtl: 3600 }, ); }); }); describe('delete', () => { it('should return false if key does not exist', async () => { mockKv.get.mockResolvedValue(null); const result = await kvProvider.delete('tenant-1', 'key-1', context); expect(result).toBe(false); expect(mockKv.delete).not.toHaveBeenCalled(); }); it('should return true and call delete if key exists', async () => { mockKv.get.mockResolvedValue('some value'); const result = await kvProvider.delete('tenant-1', 'key-1', context); expect(result).toBe(true); expect(mockKv.delete).toHaveBeenCalledWith('tenant-1:key-1'); }); }); describe('list', () => { it('should return a list of keys with tenant prefix stripped', async () => { mockKv.list.mockResolvedValue({ keys: [ { name: 'tenant-1:key-1' }, { name: 'tenant-1:key-2' }, { name: 'unrelated-key' }, ], list_complete: true, }); const result = await kvProvider.list('tenant-1', 'key', context); expect(result.keys).toEqual(['key-1', 'key-2', 'unrelated-key']); expect(result.nextCursor).toBeUndefined(); expect(mockKv.list).toHaveBeenCalledWith( expect.objectContaining({ prefix: 'tenant-1:key', limit: 1000, }), ); }); it('should forward cursors when Cloudflare pagination continues', async () => { mockKv.list.mockResolvedValue({ keys: [{ name: 'tenant-1:page-1' }], list_complete: false, cursor: 'next-cursor', }); const result = await kvProvider.list('tenant-1', 'page', context, { limit: 5, cursor: 'prev-cursor', }); expect(result.keys).toEqual(['page-1']); expect(result.nextCursor).toBe('next-cursor'); expect(mockKv.list).toHaveBeenCalledWith( expect.objectContaining({ prefix: 'tenant-1:page', limit: 5, cursor: 'prev-cursor', }), ); }); }); describe('batch operations', () => { it('getMany should aggregate non-null values', async () => { const getSpy = vi .spyOn(kvProvider, 'get') .mockResolvedValueOnce('value-1' as never) .mockResolvedValueOnce(null as never) .mockResolvedValueOnce('value-3' as never); const result = await kvProvider.getMany<string>( 'tenant-1', ['a', 'b', 'c'], context, ); expect(result).toBeInstanceOf(Map); expect(Array.from(result.entries())).toEqual([ ['a', 'value-1'], ['c', 'value-3'], ]); expect(getSpy).toHaveBeenCalledTimes(3); getSpy.mockRestore(); }); it('setMany should delegate writes with provided options', async () => { const entries = new Map<string, unknown>([ ['k1', { foo: 'bar' }], ['k2', { baz: 2 }], ]); await kvProvider.setMany('tenant-1', entries, context, { ttl: 120 }); expect(mockKv.put).toHaveBeenCalledTimes(2); expect(mockKv.put).toHaveBeenCalledWith( 'tenant-1:k1', JSON.stringify({ foo: 'bar' }), { expirationTtl: 120 }, ); expect(mockKv.put).toHaveBeenCalledWith( 'tenant-1:k2', JSON.stringify({ baz: 2 }), { expirationTtl: 120 }, ); }); it('deleteMany should return count of deleted keys', async () => { const deleteSpy = vi .spyOn(kvProvider, 'delete') .mockResolvedValueOnce(true) .mockResolvedValueOnce(false) .mockResolvedValueOnce(true); const deleted = await kvProvider.deleteMany( 'tenant-1', ['a', 'b', 'c'], context, ); expect(deleted).toBe(2); deleteSpy.mockRestore(); }); it('clear should iterate through pages and delete all keys', async () => { mockKv.list .mockResolvedValueOnce({ keys: [{ name: 'tenant-1:k1' }, { name: 'tenant-1:k2' }], list_complete: false, cursor: 'cursor-1', }) .mockResolvedValueOnce({ keys: [{ name: 'tenant-1:k3' }], list_complete: true, }); mockKv.delete.mockResolvedValue(undefined); const cleared = await kvProvider.clear('tenant-1', context); expect(cleared).toBe(3); expect(mockKv.delete).toHaveBeenCalledTimes(3); expect(mockKv.list).toHaveBeenNthCalledWith(1, { prefix: 'tenant-1:', limit: 1000, }); expect(mockKv.list).toHaveBeenNthCalledWith(2, { prefix: 'tenant-1:', limit: 1000, cursor: 'cursor-1', }); }); }); });

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/cyanheads/git-mcp-server'

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