rest-mcp-server.test.ts•11.2 kB
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { getTestEnv, TEST_FILEPATH } from './test-config.js';
import fetch from 'node-fetch';
import { spawn, ChildProcess } from 'child_process';
// Skip tests if running in CI environment
// These tests require specific environment configuration and external services
// that might not be available in CI environments
const isCI = process.env.CI === 'true';
(isCI ? describe.skip : describe)('MCP Server REST Integration Tests', () => {
  let testFileContent: string;
  let testFileBase64: string = '';
  console.log('testFileBase64', testFileBase64);
  let serverProcess: ChildProcess;
  let baseUrl: string;
  const PORT = 3001; // Specific port for REST tests
  // Helper function to wait for a specified time
  const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
  // Helper function to send POST requests to the server
  const post = async (url: string, body: any) => {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id: body.id,
        method: body.method,
        params: body.params,
      }),
    });
    return response;
  };
  // Helper function to check if server is ready using POST
  const isServerReady = async (url: string) => {
    try {
      // Use a JSON-RPC initialize to check if the server is alive
      const requestBody = {
        id: 'test',
        method: 'initialize',
        params: {
          protocolVersion: '2024-11-05',
          capabilities: {},
          clientInfo: {
            name: 'test',
            version: '1.0.0',
          },
        },
      };
      const response = await post(url, requestBody);
      let responseText;
      try {
        responseText = await response.text();
        console.log(`Raw response: ${responseText}`);
      } catch (err) {
        console.log(`Error reading response text: ${err}`);
      }
      const data = responseText ? JSON.parse(responseText) : null;
      console.log('Server response:', JSON.stringify(data));
      return data?.result && data.result.serverInfo;
    } catch (error: any) {
      console.log(`Server check failed: ${error.message}`);
      if (error.cause) {
        console.log(`Cause: ${error.cause}`);
      }
      return false;
    }
  };
  // Create a test file in memory and start server
  beforeAll(async () => {
    console.log('Starting REST integration test setup');
    // Create an in-memory test file
    testFileContent = 'This is a test file for upload';
    testFileBase64 = Buffer.from(testFileContent).toString('base64');
    // Set up environment variables for the server
    const env = {
      ...process.env,
      ...getTestEnv(),
      NODE_ENV: 'development',
      MCP_TRANSPORT_MODE: 'rest', // Use REST mode
      MCP_SERVER_PORT: PORT.toString(),
    };
    console.log(
      'Starting server with env:',
      JSON.stringify({
        NODE_ENV: env.NODE_ENV,
        MCP_TRANSPORT_MODE: env.MCP_TRANSPORT_MODE,
        MCP_SERVER_PORT: env.MCP_SERVER_PORT,
        PRIVATE_KEY: env.PRIVATE_KEY ? 'set' : 'not set',
        DELEGATION: env.DELEGATION ? 'set' : 'not set',
      })
    );
    // Start the server as a separate process
    serverProcess = spawn('node', ['dist/index.js'], {
      env,
      stdio: 'pipe',
    });
    // Add listeners for server process output
    serverProcess.stdout?.on('data', data => {
      console.log(`Server stdout: ${data}`);
    });
    serverProcess.stderr?.on('data', data => {
      console.log(`Server stderr: ${data}`);
    });
    serverProcess.on('error', error => {
      console.error(`Server process error: ${error.message}`);
    });
    // Wait for the server to start with retries
    baseUrl = `http://localhost:${PORT}/rest`;
    const maxRetries = 5;
    let retries = 0;
    let ready = false;
    console.log(`Waiting for server to start at ${baseUrl}...`);
    await wait(2000);
    // Then check with retries
    while (!ready && retries < maxRetries) {
      console.log(`Checking if server is ready (attempt ${retries + 1}/${maxRetries})...`);
      ready = await isServerReady(baseUrl);
      if (!ready) {
        console.log(`Server not ready yet, waiting...`);
        await wait(1000);
        retries++;
      }
    }
    if (!ready) {
      throw new Error(`Server failed to start after ${maxRetries} attempts`);
    }
    console.log('REST Server is ready for tests');
  }, 60000);
  afterAll(async () => {
    console.log('Cleaning up after tests');
    if (serverProcess) {
      console.log('Terminating server process');
      serverProcess.kill();
    }
  });
  it('should list available tools', async () => {
    console.log('Testing: list available tools');
    const response = await post(baseUrl, {
      id: 'test',
      method: 'tools/list',
      params: {},
    });
    expect(response.status).toBe(200);
    // Get the response body
    const responseText = await response.text();
    console.log('Tools response text:', responseText);
    const data = JSON.parse(responseText);
    // Verify the response has the expected structure
    expect(data).toHaveProperty('result');
    expect(data.result).toHaveProperty('tools');
    expect(Array.isArray(data.result.tools)).toBe(true);
    // Check for the expected tools
    const tools = data.result.tools || [];
    expect(tools.some((tool: { name: string }) => tool.name === 'identity')).toBe(true);
    expect(tools.some((tool: { name: string }) => tool.name === 'upload')).toBe(true);
    expect(tools.some((tool: { name: string }) => tool.name === 'retrieve')).toBe(true);
  });
  it('should get identity information', async () => {
    console.log('Testing: get identity information');
    const response = await post(baseUrl, {
      id: 'test',
      method: 'tools/call',
      params: {
        name: 'identity',
        arguments: {
          random_string: 'random', // Dummy parameter
        },
      },
    });
    expect(response.status).toBe(200);
    // Get the response body
    const responseText = await response.text();
    console.log('Identity response text:', responseText);
    const data = JSON.parse(responseText);
    // Verify the response has the expected structure
    expect(data).toHaveProperty('result');
    expect(data.result).toHaveProperty('content');
    expect(Array.isArray(data.result.content)).toBe(true);
    expect(data.result.content.length).toBeGreaterThan(0);
    // Get the tool result content
    const content = data.result.content[0];
    expect(content).toHaveProperty('type');
    expect(content).toHaveProperty('text');
    // Parse the text content and verify it's the identity info
    const identityInfo = JSON.parse(content.text);
    expect(identityInfo).toHaveProperty('id');
    expect(identityInfo.id).toContain('did:key:');
  });
  it('should upload a file', async () => {
    console.log('Testing: upload a file');
    const response = await post(baseUrl, {
      id: 'test',
      method: 'tools/call',
      params: {
        name: 'upload',
        arguments: {
          file: testFileBase64, // Send as base64 string
          name: 'test-file.txt',
        },
      },
    });
    expect(response.status).toBe(200);
    // Get the response body
    const responseText = await response.text();
    console.log('Upload response text:', responseText);
    const data = JSON.parse(responseText);
    // Verify the response has the expected structure
    expect(data).toHaveProperty('result');
    expect(data.result).toHaveProperty('content');
    expect(Array.isArray(data.result.content)).toBe(true);
    expect(data.result.content.length).toBeGreaterThan(0);
    // Get the content
    const content = data.result.content[0];
    expect(content).toHaveProperty('type');
    expect(content).toHaveProperty('text');
    // Parse the text content and verify it's the upload result
    const uploadResult = JSON.parse(content.text);
    expect(uploadResult).toHaveProperty('root');
    expect(uploadResult.root).toBeDefined();
    expect(uploadResult).toHaveProperty('url');
    expect(uploadResult).toHaveProperty('files');
    // Test that files is an object with at least one entry
    expect(typeof uploadResult.files).toBe('object');
    expect(uploadResult.files).not.toBeNull();
    // Get the first file entry
    const fileEntries = Object.entries(uploadResult.files);
    expect(fileEntries.length).toBeGreaterThan(0);
    expect(fileEntries[0]).toBeDefined();
  }, 30000); // Increase the timeout for upload test
  it('should retrieve a file', async () => {
    console.log('Testing: retrieve a file');
    const response = await post(baseUrl, {
      id: 'test',
      method: 'tools/call',
      params: {
        name: 'retrieve',
        arguments: {
          filepath: TEST_FILEPATH,
        },
      },
    });
    expect(response.status).toBe(200);
    // Get the response body
    const responseText = await response.text();
    console.log('Retrieve response text:', responseText);
    const data = JSON.parse(responseText);
    // Verify the response has the expected structure
    expect(data).toHaveProperty('result');
    expect(data.result).toHaveProperty('content');
    expect(Array.isArray(data.result.content)).toBe(true);
    expect(data.result.content.length).toBeGreaterThan(0);
    // Get the content
    const content = data.result.content[0];
    expect(content).toHaveProperty('type');
    expect(content).toHaveProperty('text');
    // Just verify we have content
    expect(content.text).toBeTruthy();
  }, 30000);
  it('should handle invalid upload parameters', async () => {
    console.log('Testing: invalid upload parameters');
    const response = await post(baseUrl, {
      id: 'test',
      method: 'tools/call',
      params: {
        name: 'upload',
        arguments: {
          // Missing required parameters
        },
      },
    });
    // For invalid parameters, we expect an error response with error details
    const responseText = await response.text();
    console.log('Invalid upload response text:', responseText);
    const data = JSON.parse(responseText);
    expect(data).toHaveProperty('error');
  });
  it('should handle invalid retrieve parameters', async () => {
    console.log('Testing: invalid retrieve parameters');
    const response = await post(baseUrl, {
      id: 'test',
      method: 'tools/call',
      params: {
        name: 'retrieve',
        arguments: {
          filepath: 'invalid-cid', // Missing the required filename structure
        },
      },
    });
    // For invalid parameters, we expect a response with an error content
    const responseText = await response.text();
    console.log('Invalid retrieve response text:', responseText);
    const data = JSON.parse(responseText);
    // Retrieve tool returns error as part of content
    expect(data).toHaveProperty('result');
    expect(data.result).toHaveProperty('content');
    expect(data.result.content[0]).toHaveProperty('error', true);
  });
});