/**
* @jest-environment node
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import axios from "axios";
import { SessionManager } from "../../src/tools/auth-tools";
import { registerCalendarTools } from "../../src/tools/calendar-tools";
// Мокаем axios
jest.mock("axios");
const mockedAxios = jest.mocked(axios);
// Мокаем axios.isAxiosError
(axios.isAxiosError as jest.Mock) = jest.fn();
// Мокаем SessionManager
jest.mock("../../src/tools/auth-tools");
const mockedSessionManager = jest.mocked(SessionManager);
// Мокаем console.log и console.error
const mockConsoleLog = jest.spyOn(console, "log").mockImplementation();
const mockConsoleError = jest.spyOn(console, "error").mockImplementation();
describe("Calendar Tools", () => {
let mockServer: jest.Mocked<McpServer>;
let mockSessionManagerInstance: jest.Mocked<SessionManager>;
beforeEach(() => {
jest.clearAllMocks();
mockConsoleLog.mockClear();
mockConsoleError.mockClear();
// Создаем мок сервера
mockServer = {
tool: jest.fn(),
} as any;
// Создаем мок SessionManager
mockSessionManagerInstance = {
getApiKey: jest.fn(),
getCurrentApiKey: jest.fn(),
} as any;
mockedSessionManager.getInstance = jest.fn().mockReturnValue(mockSessionManagerInstance);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe("registerCalendarTools", () => {
it("should register all calendar tools", () => {
registerCalendarTools(mockServer);
expect(mockServer.tool).toHaveBeenCalledTimes(6);
expect(mockServer.tool).toHaveBeenCalledWith(
"calendar_get_my",
expect.any(Object),
expect.any(Object),
expect.any(Function)
);
expect(mockServer.tool).toHaveBeenCalledWith(
"calendar_get_by_user",
expect.any(Object),
expect.any(Object),
expect.any(Function)
);
expect(mockServer.tool).toHaveBeenCalledWith(
"meeting_create",
expect.any(Object),
expect.any(Object),
expect.any(Function)
);
expect(mockServer.tool).toHaveBeenCalledWith(
"meeting_get",
expect.any(Object),
expect.any(Object),
expect.any(Function)
);
expect(mockServer.tool).toHaveBeenCalledWith(
"meeting_get_user_availability",
expect.any(Object),
expect.any(Object),
expect.any(Function)
);
expect(mockServer.tool).toHaveBeenCalledWith(
"meeting_get_requiring_attention",
expect.any(Object),
expect.any(Object),
expect.any(Function)
);
});
});
describe("calendar_get_my tool", () => {
let toolHandler: Function;
beforeEach(() => {
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
toolHandler = toolCall![3] as Function;
});
it("should get current user calendar with default parameters", async () => {
const mockResponse = { events: [{ id: 1, title: "Meeting" }] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({});
expect(result).toEqual({
content: [
{
type: "text",
text: JSON.stringify(mockResponse, null, 2),
},
],
});
});
it("should get current user calendar with date range", async () => {
const mockResponse = { events: [{ id: 1, title: "Meeting" }] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({
start: "2024-01-01",
end: "2024-01-31",
});
expect(result).toEqual({
content: [
{
type: "text",
text: JSON.stringify(mockResponse, null, 2),
},
],
});
});
it("should use provided API key", async () => {
const mockResponse = { events: [] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
mockSessionManagerInstance.getApiKey.mockReturnValue("session-key");
mockSessionManagerInstance.getCurrentApiKey.mockReturnValue("current-key");
await toolHandler({
api_key: "provided-key",
});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
authorization: "provided-key",
}),
})
);
});
it("should use session API key when no key provided", async () => {
const mockResponse = { events: [] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
mockSessionManagerInstance.getApiKey.mockReturnValue("session-key");
mockSessionManagerInstance.getCurrentApiKey.mockReturnValue("current-key");
await toolHandler({
session_id: "test-session",
});
expect(mockSessionManagerInstance.getApiKey).toHaveBeenCalledWith("test-session");
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
authorization: "session-key",
}),
})
);
});
it("should handle API errors", async () => {
const error = {
isAxiosError: true,
message: "API Error",
};
mockedAxios.mockRejectedValueOnce(error);
(axios.isAxiosError as jest.Mock).mockReturnValue(true);
await expect(toolHandler({})).rejects.toThrow("Calendar API request failed: API Error");
});
});
describe("calendar_get_by_user tool", () => {
let toolHandler: Function;
beforeEach(() => {
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_by_user");
toolHandler = toolCall![3] as Function;
});
it("should get calendar by user ID", async () => {
const mockResponse = { events: [{ id: 1, title: "User Meeting" }] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({
user_id: 123,
});
expect(result).toEqual({
content: [
{
type: "text",
text: JSON.stringify(mockResponse, null, 2),
},
],
});
});
it("should get calendar by user ID with date range", async () => {
const mockResponse = { events: [{ id: 1, title: "User Meeting" }] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({
user_id: 123,
start: "2024-01-01",
end: "2024-01-31",
});
expect(result).toEqual({
content: [
{
type: "text",
text: JSON.stringify(mockResponse, null, 2),
},
],
});
});
it("should validate required user_id", async () => {
await expect(toolHandler({})).rejects.toThrow();
});
});
describe("meeting_create tool", () => {
let toolHandler: Function;
beforeEach(() => {
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "meeting_create");
toolHandler = toolCall![3] as Function;
});
it("should create meeting with required fields", async () => {
const mockResponse = { id: "meeting-123", title: "New Meeting" };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({
title: "New Meeting",
start: "2024-01-01T10:00:00Z",
end: "2024-01-01T11:00:00Z",
});
expect(result).toEqual({
content: [
{
type: "text",
text: `Meeting created successfully: ${JSON.stringify(mockResponse, null, 2)}`,
},
],
});
});
it("should create meeting with all optional fields", async () => {
const mockResponse = { id: "meeting-123", title: "Full Meeting" };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({
title: "Full Meeting",
description: "Meeting description",
place: "Conference Room",
start: "2024-01-01T10:00:00Z",
end: "2024-01-01T11:00:00Z",
invited: [1, 2, 3],
notify: 15,
});
expect(result).toEqual({
content: [
{
type: "text",
text: `Meeting created successfully: ${JSON.stringify(mockResponse, null, 2)}`,
},
],
});
});
it("should validate required fields", async () => {
await expect(
toolHandler({
title: "Meeting",
// Missing start and end
})
).rejects.toThrow();
});
});
describe("meeting_get tool", () => {
let toolHandler: Function;
beforeEach(() => {
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "meeting_get");
toolHandler = toolCall![3] as Function;
});
it("should get meeting by ID", async () => {
const mockResponse = { id: "meeting-123", title: "Existing Meeting" };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({
meeting_id: "meeting-123",
});
expect(result).toEqual({
content: [
{
type: "text",
text: JSON.stringify(mockResponse, null, 2),
},
],
});
});
it("should validate required meeting_id", async () => {
await expect(toolHandler({})).rejects.toThrow();
});
});
describe("meeting_get_user_availability tool", () => {
let toolHandler: Function;
beforeEach(() => {
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "meeting_get_user_availability");
toolHandler = toolCall![3] as Function;
});
it("should check user availability", async () => {
const mockResponse = { availability: [{ user_id: 1, busy_times: [] }] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({
start: "2024-01-01T10:00:00Z",
end: "2024-01-01T11:00:00Z",
user_ids: [1, 2, 3],
});
expect(result).toEqual({
content: [
{
type: "text",
text: JSON.stringify(mockResponse, null, 2),
},
],
});
});
it("should check user availability with meeting exclusion", async () => {
const mockResponse = { availability: [{ user_id: 1, busy_times: [] }] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({
start: "2024-01-01T10:00:00Z",
end: "2024-01-01T11:00:00Z",
user_ids: [1, 2, 3],
meeting_id: "exclude-meeting-123",
});
expect(result).toEqual({
content: [
{
type: "text",
text: JSON.stringify(mockResponse, null, 2),
},
],
});
});
it("should validate required fields", async () => {
await expect(
toolHandler({
start: "2024-01-01T10:00:00Z",
end: "2024-01-01T11:00:00Z",
// Missing user_ids
})
).rejects.toThrow();
});
});
describe("meeting_get_requiring_attention tool", () => {
let toolHandler: Function;
beforeEach(() => {
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "meeting_get_requiring_attention");
toolHandler = toolCall![3] as Function;
});
it("should get meetings requiring attention", async () => {
const mockResponse = { meetings: [{ id: "meeting-123", requires_attention: true }] };
mockedAxios.mockResolvedValueOnce({ data: mockResponse });
const result = await toolHandler({});
expect(result).toEqual({
content: [
{
type: "text",
text: JSON.stringify(mockResponse, null, 2),
},
],
});
});
});
describe("makeCalendarRequest function", () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
originalEnv = process.env;
process.env = { ...originalEnv };
});
afterEach(() => {
process.env = originalEnv;
});
it("should use default host URL", async () => {
process.env.CALENDAR_API_HOST_URL = undefined;
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
// We need to test the makeCalendarRequest function indirectly through a tool
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await toolHandler({});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: "http://localhost:8000/platform-calendar/api/v1/calendar/my",
})
);
});
it("should use custom host URL", async () => {
process.env.CALENDAR_API_HOST_URL = "https://api.example.com";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await toolHandler({});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/platform-calendar/api/v1/calendar/my",
})
);
});
it("should handle host URL with trailing slash", async () => {
process.env.CALENDAR_API_HOST_URL = "https://api.example.com/";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await toolHandler({});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/platform-calendar/api/v1/calendar/my",
})
);
});
it("should handle host URL with existing platform-calendar path", async () => {
process.env.CALENDAR_API_HOST_URL = "https://api.example.com/platform-calendar";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await toolHandler({});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.example.com/platform-calendar/api/v1/calendar/my",
})
);
});
it("should use environment API key when no session key available", async () => {
process.env.CALENDAR_API_KEY = "env-api-key";
mockSessionManagerInstance.getApiKey.mockReturnValue(undefined);
mockSessionManagerInstance.getCurrentApiKey.mockReturnValue(undefined);
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await toolHandler({});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
authorization: "env-api-key",
}),
})
);
});
it("should handle empty API key", async () => {
process.env.CALENDAR_API_KEY = undefined;
mockSessionManagerInstance.getApiKey.mockReturnValue(undefined);
mockSessionManagerInstance.getCurrentApiKey.mockReturnValue(undefined);
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await toolHandler({});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
authorization: "",
}),
})
);
});
it("should add Content-Type header for non-GET requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "meeting_create");
const toolHandler = toolCall![3] as Function;
await toolHandler({
title: "Test Meeting",
start: "2024-01-01T10:00:00Z",
end: "2024-01-01T11:00:00Z",
});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
"Content-Type": "application/json",
}),
})
);
});
it("should not add Content-Type header for GET requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await toolHandler({});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
Accept: "application/json",
}),
})
);
expect(mockedAxios).toHaveBeenCalledWith(
expect.not.objectContaining({
headers: expect.objectContaining({
"Content-Type": "application/json",
}),
})
);
});
it("should make request with correct parameters", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await toolHandler({});
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
method: "GET",
url: expect.stringContaining("/api/v1/calendar/my"),
headers: expect.objectContaining({
authorization: "",
Accept: "application/json",
}),
})
);
});
it("should handle Axios error responses", async () => {
const error = {
isAxiosError: true,
response: {
status: 404,
data: { error: "Not found" },
},
message: "Request failed with status code 404",
};
mockedAxios.mockRejectedValueOnce(error);
(axios.isAxiosError as jest.Mock).mockReturnValue(true);
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await expect(toolHandler({})).rejects.toThrow("Calendar API request failed: Request failed with status code 404");
});
it("should handle non-Axios errors", async () => {
const error = new Error("Network error");
mockedAxios.mockRejectedValueOnce(error);
(axios.isAxiosError as jest.Mock).mockReturnValue(false);
registerCalendarTools(mockServer);
const toolCall = mockServer.tool.mock.calls.find((call) => call[0] === "calendar_get_my");
const toolHandler = toolCall![3] as Function;
await expect(toolHandler({})).rejects.toThrow("Network error");
});
});
});