Skip to main content
Glama

Karakeep MCP server

by karakeep-app
interface-compliance.test.ts17.9 kB
import * as fs from "fs"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { ASSET_TYPES, AssetStore } from "@karakeep/shared/assetdb"; import { assertAssetExists, assertAssetNotExists, cleanupTempDirectory, cleanupTestBucket, createLocalFileSystemStore, createS3Store, createTempDirectory, createTempFile, createTestAssetData, createTestBucket, createTestImageData, createTestPdfData, generateLargeBuffer, getAllAssetsArray, streamToBuffer, } from "./assetdb-utils"; interface TestContext { store: AssetStore; cleanup: () => Promise<void>; } async function createLocalContext(): Promise<TestContext> { const tempDir = await createTempDirectory(); const store = createLocalFileSystemStore(tempDir); return { store, cleanup: async () => { await cleanupTempDirectory(tempDir); }, }; } async function createS3Context(): Promise<TestContext> { const bucketName = `test-bucket-${Math.random().toString(36).substring(7)}`; const s3Client = await createTestBucket(bucketName); const store = createS3Store(bucketName); return { store, cleanup: async () => { await cleanupTestBucket(s3Client, bucketName); }, }; } describe.each([ { name: "LocalFileSystemAssetStore", createContext: createLocalContext }, { name: "S3AssetStore", createContext: createS3Context }, ])("AssetStore Interface Compliance - $name", ({ createContext }) => { let context: TestContext; beforeEach(async () => { context = await createContext(); }); afterEach(async () => { await context.cleanup(); }); describe("Basic CRUD Operations", () => { it("should save and retrieve an asset", async () => { const testData = createTestAssetData(); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const { asset, metadata } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(asset).toEqual(testData.content); expect(metadata).toEqual(testData.metadata); }); it("should delete an asset", async () => { const testData = createTestAssetData(); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); await assertAssetExists(context.store, testData.userId, testData.assetId); await context.store.deleteAsset({ userId: testData.userId, assetId: testData.assetId, }); await assertAssetNotExists( context.store, testData.userId, testData.assetId, ); }); it("should get asset size", async () => { const testData = createTestAssetData(); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const size = await context.store.getAssetSize({ userId: testData.userId, assetId: testData.assetId, }); expect(size).toBe(testData.content.length); }); it("should read asset metadata", async () => { const testData = createTestAssetData(); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const metadata = await context.store.readAssetMetadata({ userId: testData.userId, assetId: testData.assetId, }); expect(metadata).toEqual(testData.metadata); }); }); describe("Streaming Operations", () => { it("should create readable stream", async () => { const testData = createTestAssetData(); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const stream = await context.store.createAssetReadStream({ userId: testData.userId, assetId: testData.assetId, }); const streamedContent = await streamToBuffer(stream); expect(streamedContent).toEqual(testData.content); }); it("should support range requests in streams", async () => { const content = Buffer.from("0123456789abcdef"); const testData = createTestAssetData({ content }); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const stream = await context.store.createAssetReadStream({ userId: testData.userId, assetId: testData.assetId, start: 5, end: 10, }); const streamedContent = await streamToBuffer(stream); expect(streamedContent.toString()).toBe("56789a"); }); }); describe("Asset Types Support", () => { it("should support all required asset types", async () => { const testCases = [ { contentType: ASSET_TYPES.IMAGE_JPEG, fileName: "test.jpg" }, { contentType: ASSET_TYPES.IMAGE_PNG, fileName: "test.png" }, { contentType: ASSET_TYPES.IMAGE_WEBP, fileName: "test.webp" }, { contentType: ASSET_TYPES.APPLICATION_PDF, fileName: "test.pdf" }, { contentType: ASSET_TYPES.TEXT_HTML, fileName: "test.html" }, { contentType: ASSET_TYPES.VIDEO_MP4, fileName: "test.mp4" }, ]; for (const { contentType, fileName } of testCases) { const testData = createTestAssetData({ metadata: { contentType, fileName }, }); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const { metadata } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(metadata.contentType).toBe(contentType); expect(metadata.fileName).toBe(fileName); } }); it("should handle large assets", async () => { const largeContent = generateLargeBuffer(5); // 5MB const testData = createTestAssetData({ content: largeContent }); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const { asset } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(asset.length).toBe(largeContent.length); expect(asset).toEqual(largeContent); }); it("should reject unsupported asset types", async () => { const testData = createTestAssetData({ metadata: { contentType: "unsupported/type", fileName: "test.unsupported", }, }); await expect( context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }), ).rejects.toThrow("Unsupported asset type"); }); }); describe("Bulk Operations", () => { it("should delete all user assets", async () => { const userId = "bulk-test-user"; const testAssets = [ createTestAssetData({ userId }), createTestAssetData({ userId }), createTestAssetData({ userId }), ]; const otherUserAsset = createTestAssetData(); // Different user // Save all assets await Promise.all([ ...testAssets.map((testData) => context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }), ), context.store.saveAsset({ userId: otherUserAsset.userId, assetId: otherUserAsset.assetId, asset: otherUserAsset.content, metadata: otherUserAsset.metadata, }), ]); // Delete user assets await context.store.deleteUserAssets({ userId }); // Verify user assets are deleted for (const testData of testAssets) { await assertAssetNotExists( context.store, testData.userId, testData.assetId, ); } // Verify other user's asset still exists await assertAssetExists( context.store, otherUserAsset.userId, otherUserAsset.assetId, ); }); it("should list all assets", async () => { const testAssets = [ createTestAssetData(), createTestImageData(), createTestPdfData(), ]; // Save all assets await Promise.all( testAssets.map((testData) => context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }), ), ); const assets = await getAllAssetsArray(context.store); expect(assets).toHaveLength(3); // Verify all assets are present const assetIds = assets.map((a) => a.assetId); for (const testData of testAssets) { expect(assetIds).toContain(testData.assetId); } // Verify asset structure for (const asset of assets) { expect(asset).toHaveProperty("userId"); expect(asset).toHaveProperty("assetId"); expect(asset).toHaveProperty("contentType"); expect(asset).toHaveProperty("size"); expect(typeof asset.size).toBe("number"); expect(asset.size).toBeGreaterThan(0); } }); }); describe("File Operations", () => { it("should save asset from file and delete original file", async () => { const testData = createTestAssetData(); const tempFile = await createTempFile(testData.content, "temp-asset.bin"); // Verify temp file exists before operation expect( await fs.promises .access(tempFile) .then(() => true) .catch(() => false), ).toBe(true); await context.store.saveAssetFromFile({ userId: testData.userId, assetId: testData.assetId, assetPath: tempFile, metadata: testData.metadata, }); // Verify temp file was deleted expect( await fs.promises .access(tempFile) .then(() => true) .catch(() => false), ).toBe(false); // Verify asset was saved correctly const { asset, metadata } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(asset).toEqual(testData.content); expect(metadata).toEqual(testData.metadata); }); }); describe("Error Handling", () => { it("should throw error for non-existent asset read", async () => { await expect( context.store.readAsset({ userId: "non-existent-user", assetId: "non-existent-asset", }), ).rejects.toThrow(); }); it("should throw error for non-existent asset metadata", async () => { await expect( context.store.readAssetMetadata({ userId: "non-existent-user", assetId: "non-existent-asset", }), ).rejects.toThrow(); }); it("should throw error for non-existent asset size", async () => { await expect( context.store.getAssetSize({ userId: "non-existent-user", assetId: "non-existent-asset", }), ).rejects.toThrow(); }); it("should handle deleting non-existent asset gracefully", async () => { // Filesystem implementation throws errors for non-existent files await expect( context.store.deleteAsset({ userId: "non-existent-user", assetId: "non-existent-asset", }), ).resolves.not.toThrow(); }); it("should handle deletion of non-existent user directory gracefully", async () => { // Should not throw error when user directory doesn't exist await expect( context.store.deleteUserAssets({ userId: "non-existent-user" }), ).resolves.not.toThrow(); }); it("should handle non-existent asset stream appropriately", async () => { const streamResult = context.store.createAssetReadStream({ userId: "non-existent-user", assetId: "non-existent-asset", }); await expect(streamResult).rejects.toThrow(); }); }); describe("Data Integrity", () => { it("should preserve binary data integrity", async () => { const testData = createTestImageData(); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const { asset } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); // Verify exact binary match expect(asset).toEqual(testData.content); // Verify PNG header is intact expect(asset[0]).toBe(0x89); expect(asset[1]).toBe(0x50); expect(asset[2]).toBe(0x4e); expect(asset[3]).toBe(0x47); }); it("should preserve metadata exactly", async () => { const testData = createTestAssetData({ metadata: { contentType: ASSET_TYPES.APPLICATION_PDF, fileName: "test-document.pdf", }, }); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const { metadata } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(metadata).toEqual(testData.metadata); expect(metadata.contentType).toBe(ASSET_TYPES.APPLICATION_PDF); expect(metadata.fileName).toBe("test-document.pdf"); }); it("should handle null fileName correctly", async () => { const testData = createTestAssetData({ metadata: { contentType: ASSET_TYPES.TEXT_HTML, fileName: null, }, }); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const { metadata } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(metadata.fileName).toBeNull(); }); }); describe("Concurrent Operations", () => { it("should handle concurrent saves safely", async () => { const testAssets = Array.from({ length: 5 }, () => createTestAssetData()); await Promise.all( testAssets.map((testData) => context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }), ), ); // Verify all assets were saved correctly for (const testData of testAssets) { const { asset, metadata } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(asset).toEqual(testData.content); expect(metadata).toEqual(testData.metadata); } }); it("should handle concurrent reads safely", async () => { const testData = createTestAssetData(); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); // Perform multiple concurrent reads const readPromises = Array.from({ length: 10 }, () => context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }), ); const results = await Promise.all(readPromises); // Verify all reads returned the same data for (const { asset, metadata } of results) { expect(asset).toEqual(testData.content); expect(metadata).toEqual(testData.metadata); } }); }); describe("Edge Cases", () => { it("should handle empty assets", async () => { const testData = createTestAssetData({ content: Buffer.alloc(0), }); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const { asset } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(asset.length).toBe(0); const size = await context.store.getAssetSize({ userId: testData.userId, assetId: testData.assetId, }); expect(size).toBe(0); }); it("should handle special characters in user and asset IDs", async () => { const testData = createTestAssetData({ userId: "user-with-special_chars.123", assetId: "asset_with-special.chars_456", }); await context.store.saveAsset({ userId: testData.userId, assetId: testData.assetId, asset: testData.content, metadata: testData.metadata, }); const { asset, metadata } = await context.store.readAsset({ userId: testData.userId, assetId: testData.assetId, }); expect(asset).toEqual(testData.content); expect(metadata).toEqual(testData.metadata); }); }); });

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/karakeep-app/karakeep'

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