@smartsheet/smar-mcp
Version:
A Model Context Protocol (MCP) server for interacting with the Smartsheet API. This server provides tools for searching, retrieving, and updating Smartsheet sheets through the MCP protocol.
109 lines (108 loc) • 4.78 kB
JavaScript
import axios from 'axios';
jest.mock('../../../package.json', () => ({ version: '0.0.0-test' }), { virtual: true });
import { SmartsheetAPI } from '../smartsheet-api.js';
jest.mock('axios');
const mockedAxios = axios;
const TEST_TOKEN = 'test-secret-token-12345';
const TEST_BASE_URL = 'https://api.smartsheet.com/2.0';
function createAPI() {
return new SmartsheetAPI(TEST_TOKEN, TEST_BASE_URL);
}
describe('SmartsheetAPI - token leakage prevention', () => {
let errorSpy;
let infoSpy;
beforeEach(() => {
jest.clearAllMocks();
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
infoSpy = jest.spyOn(console, 'info').mockImplementation(() => { });
});
afterEach(() => {
errorSpy.mockRestore();
infoSpy.mockRestore();
});
describe('error logging sanitization', () => {
it('should not log Authorization header in error output', async () => {
const axiosError = {
message: 'Request failed with status code 500',
config: {
headers: {
Authorization: `Bearer ${TEST_TOKEN}`,
},
},
response: {
status: 500,
data: { message: 'Internal Server Error' },
headers: {},
},
};
mockedAxios.mockRejectedValueOnce(axiosError);
const api = createAPI();
await expect(api.request('GET', '/sheets')).rejects.toThrow();
expect(errorSpy).toHaveBeenCalled();
const loggedArgs = JSON.stringify(errorSpy.mock.calls);
expect(loggedArgs).not.toContain(TEST_TOKEN);
expect(loggedArgs).not.toContain('Authorization');
});
it('should log only message, status, and data in error output', async () => {
const axiosError = {
message: 'Not Found',
config: {
headers: { Authorization: `Bearer ${TEST_TOKEN}` },
url: `${TEST_BASE_URL}/sheets/123`,
},
response: {
status: 404,
data: { message: 'Sheet not found', errorCode: 1006 },
headers: {},
},
};
mockedAxios.mockRejectedValueOnce(axiosError);
const api = createAPI();
await expect(api.request('GET', '/sheets/123')).rejects.toThrow();
const sanitizedArg = errorSpy.mock.calls[0][1];
expect(Object.keys(sanitizedArg)).toEqual(['message', 'status', 'data']);
expect(sanitizedArg.message).toBe('Not Found');
expect(sanitizedArg.status).toBe(404);
expect(sanitizedArg.data).toEqual({ message: 'Sheet not found', errorCode: 1006 });
});
it('should not leak token when error has no response', async () => {
const networkError = {
message: 'Network Error',
config: {
headers: { Authorization: `Bearer ${TEST_TOKEN}` },
},
};
mockedAxios.mockRejectedValueOnce(networkError);
const api = createAPI();
await expect(api.request('GET', '/sheets')).rejects.toThrow();
const loggedArgs = JSON.stringify(errorSpy.mock.calls);
expect(loggedArgs).not.toContain(TEST_TOKEN);
});
});
describe('request logging sanitization', () => {
it('should log only the endpoint path, not the full URL', async () => {
mockedAxios.mockResolvedValueOnce({ data: { results: [] } });
const api = createAPI();
await api.request('GET', '/search', undefined, { query: 'sensitive-data' });
expect(infoSpy).toHaveBeenCalledWith('API Request: GET /search');
});
it('should not log query parameters', async () => {
mockedAxios.mockResolvedValueOnce({ data: {} });
const api = createAPI();
await api.request('GET', '/sheets', undefined, {
accessApiLevel: '2',
include: 'attachments'
});
const loggedArgs = JSON.stringify(infoSpy.mock.calls);
expect(loggedArgs).not.toContain('accessApiLevel');
expect(loggedArgs).not.toContain('attachments');
});
it('should not log the base URL', async () => {
mockedAxios.mockResolvedValueOnce({ data: {} });
const api = createAPI();
await api.request('GET', '/sheets');
const loggedArgs = JSON.stringify(infoSpy.mock.calls);
expect(loggedArgs).not.toContain(TEST_BASE_URL);
});
});
});