/**
* @jest-environment node
*/
import axios from "axios";
import { makePlaneRequest } from "../../src/common/request-helper";
// Мокаем axios
jest.mock("axios");
const mockedAxios = jest.mocked(axios);
describe("Request Helper", () => {
const originalEnv = process.env;
beforeEach(() => {
jest.resetAllMocks();
// Сбрасываем переменные окружения
process.env = { ...originalEnv };
process.env.PLANE_API_HOST_URL = "https://api.plane.so";
process.env.PLANE_API_KEY = "test-api-key";
// Мокаем console.log и console.error для чистоты тестов
jest.spyOn(console, "log").mockImplementation();
jest.spyOn(console, "error").mockImplementation();
});
afterEach(() => {
process.env = originalEnv;
jest.restoreAllMocks();
});
describe("makePlaneRequest", () => {
describe("URL Construction", () => {
it("should construct URL with default host", async () => {
delete process.env.PLANE_API_HOST_URL;
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users");
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://api.plane.so/api/v1/users",
method: "GET",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
},
});
});
it("should construct URL with custom host", async () => {
process.env.PLANE_API_HOST_URL = "https://custom.plane.so";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users");
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://custom.plane.so/api/v1/users",
method: "GET",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
},
});
});
it("should add trailing slash to host URL if missing", async () => {
process.env.PLANE_API_HOST_URL = "https://api.plane.so";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users");
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://api.plane.so/api/v1/users",
method: "GET",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
},
});
});
it("should add /api/v1 to host URL if missing", async () => {
process.env.PLANE_API_HOST_URL = "https://api.plane.so/";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users");
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://api.plane.so/api/v1/users",
method: "GET",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
},
});
});
it("should not duplicate /api/v1 if already present", async () => {
process.env.PLANE_API_HOST_URL = "https://api.plane.so/api/v1/";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users");
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://api.plane.so/api/v1/users",
method: "GET",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
},
});
});
it("should handle double slashes in URL", async () => {
process.env.PLANE_API_HOST_URL = "https://api.plane.so//";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users//profile");
const expectedUrl = "https://api.plane.so/api/v1/users//profile";
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: expectedUrl,
})
);
});
});
describe("Headers Configuration", () => {
it("should set default API key from environment", async () => {
process.env.PLANE_API_KEY = "my-secret-key";
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users");
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
"X-API-Key": "my-secret-key",
Accept: "application/json",
},
})
);
});
it("should use empty string if API key is not set", async () => {
delete process.env.PLANE_API_KEY;
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users");
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
"X-API-Key": "",
Accept: "application/json",
},
})
);
});
it("should not include Content-Type for GET requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users");
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
},
})
);
});
it("should include Content-Type for POST requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("POST", "users", { name: "Test" });
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
})
);
});
it("should include Content-Type for PUT requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("PUT", "users/1", { name: "Updated" });
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
})
);
});
it("should include Content-Type for DELETE requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("DELETE", "users/1", { reason: "cleanup" });
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
})
);
});
});
describe("Request Body Handling", () => {
it("should not include body for GET requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users", { query: "test" });
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://api.plane.so/api/v1/users",
method: "GET",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
},
});
});
it("should include body for POST requests", async () => {
const body = { name: "Test User", email: "test@example.com" };
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("POST", "users", body);
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://api.plane.so/api/v1/users",
method: "POST",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
data: body,
});
});
it("should not include data when body is null for POST", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("POST", "users", null);
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://api.plane.so/api/v1/users",
method: "POST",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
});
});
it("should handle empty object as body", async () => {
const body = {};
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("POST", "users", body);
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://api.plane.so/api/v1/users",
method: "POST",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
data: body,
});
});
});
describe("HTTP Methods", () => {
it("should handle GET requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { users: [] } });
const result = await makePlaneRequest("GET", "users");
expect(result).toEqual({ users: [] });
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
method: "GET",
})
);
});
it("should handle POST requests", async () => {
const responseData = { id: 1, name: "Test User" };
mockedAxios.mockResolvedValueOnce({ data: responseData });
const result = await makePlaneRequest("POST", "users", { name: "Test User" });
expect(result).toEqual(responseData);
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
method: "POST",
})
);
});
it("should handle PUT requests", async () => {
const responseData = { id: 1, name: "Updated User" };
mockedAxios.mockResolvedValueOnce({ data: responseData });
const result = await makePlaneRequest("PUT", "users/1", { name: "Updated User" });
expect(result).toEqual(responseData);
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
method: "PUT",
})
);
});
it("should handle DELETE requests", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
const result = await makePlaneRequest("DELETE", "users/1");
expect(result).toEqual({ success: true });
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
method: "DELETE",
})
);
});
it("should handle case-insensitive method names", async () => {
// Мокаем каждый запрос отдельно
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("get", "users");
await makePlaneRequest("POST", "users", {});
await makePlaneRequest("put", "users/1", {});
await makePlaneRequest("DELETE", "users/1");
expect(mockedAxios).toHaveBeenCalledTimes(4);
});
});
describe("Error Handling", () => {
it("should handle Axios errors with response", async () => {
const axiosError = {
isAxiosError: true,
message: "Request failed with status code 404",
response: {
status: 404,
data: { error: "User not found" },
},
};
mockedAxios.isAxiosError.mockReturnValue(true);
mockedAxios.mockRejectedValueOnce(axiosError);
await expect(makePlaneRequest("GET", "users/999")).rejects.toThrow(
"Request failed: Request failed with status code 404"
);
expect(console.error).toHaveBeenCalledWith("[DEBUG] Plane API Error Response", {
status: 404,
data: { error: "User not found" },
});
});
it("should handle Axios errors without response", async () => {
const axiosError = {
isAxiosError: true,
message: "Network Error",
};
mockedAxios.isAxiosError.mockReturnValue(true);
mockedAxios.mockRejectedValueOnce(axiosError);
await expect(makePlaneRequest("GET", "users")).rejects.toThrow("Request failed: Network Error");
expect(console.error).not.toHaveBeenCalled();
});
it("should handle non-Axios errors", async () => {
const genericError = new Error("Something went wrong");
mockedAxios.isAxiosError.mockReturnValue(false);
mockedAxios.mockRejectedValueOnce(genericError);
await expect(makePlaneRequest("GET", "users")).rejects.toThrow("Something went wrong");
});
it("should handle different HTTP error codes", async () => {
const testCases = [
{ status: 400, message: "Bad Request" },
{ status: 401, message: "Unauthorized" },
{ status: 403, message: "Forbidden" },
{ status: 500, message: "Internal Server Error" },
];
for (const testCase of testCases) {
const axiosError = {
isAxiosError: true,
message: testCase.message,
response: {
status: testCase.status,
data: { error: testCase.message },
},
};
mockedAxios.isAxiosError.mockReturnValue(true);
mockedAxios.mockRejectedValueOnce(axiosError);
await expect(makePlaneRequest("GET", "users")).rejects.toThrow(`Request failed: ${testCase.message}`);
}
});
});
describe("Debugging and Logging", () => {
it("should log request details", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("POST", "users", { name: "Test" });
expect(console.log).toHaveBeenCalledWith("[DEBUG] Plane API Request", {
method: "POST",
url: "https://api.plane.so/api/v1/users",
headers: {
"X-API-Key": "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
body: { name: "Test" },
});
});
it("should log error response details", async () => {
const axiosError = {
isAxiosError: true,
message: "Request failed",
response: {
status: 422,
data: { error: "Validation failed", details: ["Name is required"] },
},
};
mockedAxios.isAxiosError.mockReturnValue(true);
mockedAxios.mockRejectedValueOnce(axiosError);
await expect(makePlaneRequest("POST", "users", {})).rejects.toThrow();
expect(console.error).toHaveBeenCalledWith("[DEBUG] Plane API Error Response", {
status: 422,
data: { error: "Validation failed", details: ["Name is required"] },
});
});
});
describe("Return Type Handling", () => {
it("should return typed response data", async () => {
interface User {
id: number;
name: string;
email: string;
}
const userData: User = {
id: 1,
name: "Test User",
email: "test@example.com",
};
mockedAxios.mockResolvedValueOnce({ data: userData });
const result = await makePlaneRequest<User>("GET", "users/1");
expect(result).toEqual(userData);
expect(result.id).toBe(1);
expect(result.name).toBe("Test User");
expect(result.email).toBe("test@example.com");
});
it("should handle array responses", async () => {
const usersData = [
{ id: 1, name: "User 1" },
{ id: 2, name: "User 2" },
];
mockedAxios.mockResolvedValueOnce({ data: usersData });
const result = await makePlaneRequest<typeof usersData>("GET", "users");
expect(result).toEqual(usersData);
expect(Array.isArray(result)).toBe(true);
expect(result).toHaveLength(2);
});
it("should handle null/undefined responses", async () => {
mockedAxios.mockResolvedValueOnce({ data: null });
const result = await makePlaneRequest("DELETE", "users/1");
expect(result).toBeNull();
});
});
describe("Edge Cases", () => {
it("should handle empty path", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "");
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.plane.so/api/v1/",
})
);
});
it("should handle path with leading slash", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "/users");
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.plane.so/api/v1//users",
})
);
});
it("should handle complex query paths", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users?page=1&limit=10&sort=name");
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.plane.so/api/v1/users?page=1&limit=10&sort=name",
})
);
});
it("should handle special characters in path", async () => {
mockedAxios.mockResolvedValueOnce({ data: { success: true } });
await makePlaneRequest("GET", "users/test@example.com");
expect(mockedAxios).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.plane.so/api/v1/users/test@example.com",
})
);
});
});
});
});