import axios from "axios";
import { EmployeeApiClient } from "../../src/api/employee-api";
import {
CreateEmployeeDTO,
EmployeeSearch,
SendInviteDTO,
UpdateEmployeeDTO,
} from "../../src/schemas/employee-schemas";
import { ErrorHandler } from "../../src/utils/error-handler";
// Мокаем axios
jest.mock("axios");
const mockedAxios = jest.mocked(axios);
// Мокаем ErrorHandler
jest.mock("../../src/utils/error-handler");
const mockedErrorHandler = jest.mocked(ErrorHandler);
// Мокаем сгенерированный API
jest.mock("../../src/api/generated/index", () => ({
EmployeeApi: jest.fn().mockImplementation(() => ({
createEmployee: jest.fn(),
searchEmployees: jest.fn(),
getEmployee: jest.fn(),
deleteEmployee: jest.fn(),
})),
}));
describe("EmployeeApiClient", () => {
let client: EmployeeApiClient;
let mockGeneratedApi: any;
beforeEach(() => {
// Сбрасываем все моки
jest.clearAllMocks();
// Мокаем axios.isAxiosError
(axios.isAxiosError as jest.Mock) = jest.fn();
// Создаем мок для сгенерированного API
mockGeneratedApi = {
createEmployee: jest.fn(),
searchEmployees: jest.fn(),
getEmployee: jest.fn(),
deleteEmployee: jest.fn(),
};
// Мокаем конструктор EmployeeApi
const { EmployeeApi } = require("../../src/api/generated/index");
EmployeeApi.mockImplementation(() => mockGeneratedApi);
// Создаем клиент
client = new EmployeeApiClient("https://test-api.com", "test-api-key");
});
describe("constructor", () => {
it("should initialize with provided baseUrl and apiKey", () => {
const client = new EmployeeApiClient("https://custom-api.com", "custom-key");
expect(client).toBeDefined();
});
it("should use environment variables when no parameters provided", () => {
const originalEnv = process.env.EMPLOYEE_API_HOST_URL;
const originalKey = process.env.EMPLOYEE_API_KEY;
process.env.EMPLOYEE_API_HOST_URL = "https://env-api.com";
process.env.EMPLOYEE_API_KEY = "env-key";
const client = new EmployeeApiClient();
expect(client).toBeDefined();
// Восстанавливаем переменные окружения
process.env.EMPLOYEE_API_HOST_URL = originalEnv;
process.env.EMPLOYEE_API_KEY = originalKey;
});
it("should add /employees/ suffix to baseUrl if not present", () => {
const client = new EmployeeApiClient("https://test-api.com", "test-key");
expect(client).toBeDefined();
});
it("should not add /employees/ suffix if already present", () => {
const client = new EmployeeApiClient("https://test-api.com/employees/", "test-key");
expect(client).toBeDefined();
});
it("should handle baseUrl with trailing slash", () => {
const client = new EmployeeApiClient("https://test-api.com/", "test-key");
expect(client).toBeDefined();
});
});
describe("makeRequest", () => {
it("should make successful GET request", async () => {
const mockResponse = { data: { id: "123", name: "Test Employee" } };
mockedAxios.mockResolvedValueOnce(mockResponse);
// Используем рефлексию для доступа к приватному методу
const result = await (client as any).makeRequest("GET", "/test", null, { param: "value" });
expect(result).toEqual(mockResponse.data);
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://test-api.com/employees//test",
method: "GET",
headers: {
Authorization: "test-api-key",
Accept: "application/json",
},
params: { param: "value" },
});
});
it("should make successful POST request with body", async () => {
const mockResponse = { data: { id: "123", name: "Test Employee" } };
mockedAxios.mockResolvedValueOnce(mockResponse);
const body = { name: "Test Employee" };
const result = await (client as any).makeRequest("POST", "/test", body);
expect(result).toEqual(mockResponse.data);
expect(mockedAxios).toHaveBeenCalledWith({
url: "https://test-api.com/employees//test",
method: "POST",
headers: {
Authorization: "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
params: null,
data: body,
});
});
it("should handle axios errors", async () => {
const mockError = {
response: {
status: 400,
data: { error: "Bad Request" },
},
message: "Request failed",
};
(axios.isAxiosError as jest.Mock).mockReturnValue(true);
mockedAxios.mockRejectedValueOnce(mockError);
await expect((client as any).makeRequest("GET", "/test")).rejects.toThrow(
"Employee API request failed: Request failed"
);
});
it("should handle non-axios errors", async () => {
const mockError = new Error("Network error");
(axios.isAxiosError as jest.Mock).mockReturnValue(false);
mockedAxios.mockRejectedValueOnce(mockError);
await expect((client as any).makeRequest("GET", "/test")).rejects.toThrow("Network error");
});
it("should handle axios error without response", async () => {
const mockError = {
message: "Network Error",
// Нет response
};
(axios.isAxiosError as jest.Mock).mockReturnValue(true);
mockedAxios.mockRejectedValueOnce(mockError);
await expect((client as any).makeRequest("GET", "/test")).rejects.toThrow(
"Employee API request failed: Network Error"
);
});
});
describe("createEmployee", () => {
it("should create employee successfully", async () => {
const employeeData: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const mockResult = { id: "emp123", name: "Иванов Иван" };
mockGeneratedApi.createEmployee.mockResolvedValueOnce(mockResult);
// Мокаем ErrorHandler.executeWithResilience
mockedErrorHandler.executeWithResilience.mockImplementation(async (fn) => {
return await fn();
});
const result = await client.createEmployee(employeeData);
expect(result).toEqual(mockResult);
expect(mockGeneratedApi.createEmployee).toHaveBeenCalledWith(employeeData);
expect(mockedErrorHandler.executeWithResilience).toHaveBeenCalled();
});
it("should handle create employee error", async () => {
const employeeData: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const mockError = new Error("API Error");
mockGeneratedApi.createEmployee.mockRejectedValueOnce(mockError);
// Мокаем ErrorHandler.executeWithResilience для выброса ошибки
mockedErrorHandler.executeWithResilience.mockRejectedValueOnce(mockError);
await expect(client.createEmployee(employeeData)).rejects.toThrow("API Error");
});
});
describe("searchEmployees", () => {
it("should search employees successfully", async () => {
const searchParams: EmployeeSearch = {
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const mockResult = { employees: [{ id: "emp123", name: "Иванов Иван" }] };
mockGeneratedApi.searchEmployees.mockResolvedValueOnce(mockResult);
// Мокаем ErrorHandler.executeWithResilience
mockedErrorHandler.executeWithResilience.mockImplementation(async (fn) => {
return await fn();
});
const result = await client.searchEmployees(searchParams);
expect(result).toEqual(mockResult);
expect(mockGeneratedApi.searchEmployees).toHaveBeenCalledWith({
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
});
});
it("should handle search employees error", async () => {
const searchParams: EmployeeSearch = {
lastName: "Иванов",
};
const mockError = new Error("Search failed");
mockGeneratedApi.searchEmployees.mockRejectedValueOnce(mockError);
// Мокаем ErrorHandler.executeWithResilience для выброса ошибки
mockedErrorHandler.executeWithResilience.mockRejectedValueOnce(mockError);
await expect(client.searchEmployees(searchParams)).rejects.toThrow("Search failed");
});
});
describe("getEmployee", () => {
it("should get employee by id successfully", async () => {
const employeeId = "emp123";
const mockResult = { id: "emp123", name: "Иванов Иван" };
mockGeneratedApi.getEmployee.mockResolvedValueOnce(mockResult);
const result = await client.getEmployee(employeeId);
expect(result).toEqual(mockResult);
expect(mockGeneratedApi.getEmployee).toHaveBeenCalledWith(employeeId);
});
it("should handle get employee error", async () => {
const employeeId = "emp123";
const mockError = new Error("Employee not found");
mockGeneratedApi.getEmployee.mockRejectedValueOnce(mockError);
await expect(client.getEmployee(employeeId)).rejects.toThrow("Employee not found");
});
});
describe("deleteEmployee", () => {
it("should delete employee successfully", async () => {
const employeeId = "emp123";
const mockResult = { success: true };
mockGeneratedApi.deleteEmployee.mockResolvedValueOnce(mockResult);
const result = await client.deleteEmployee(employeeId);
expect(result).toEqual(mockResult);
expect(mockGeneratedApi.deleteEmployee).toHaveBeenCalledWith(employeeId);
});
it("should handle delete employee error", async () => {
const employeeId = "emp123";
const mockError = new Error("Delete failed");
mockGeneratedApi.deleteEmployee.mockRejectedValueOnce(mockError);
await expect(client.deleteEmployee(employeeId)).rejects.toThrow("Delete failed");
});
});
describe("sendInvite", () => {
it("should send invite successfully", async () => {
const inviteData: SendInviteDTO = {
email: "test@example.com",
companyId: "company123",
};
const mockResponse = { data: { success: true, inviteId: "inv123" } };
mockedAxios.post.mockResolvedValueOnce(mockResponse);
const result = await client.sendInvite(inviteData);
expect(result).toEqual(mockResponse.data);
expect(mockedAxios.post).toHaveBeenCalledWith("https://test-api.com/invites/send", inviteData, {
headers: {
Authorization: "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
});
});
it("should handle send invite error", async () => {
const inviteData: SendInviteDTO = {
email: "test@example.com",
companyId: "company123",
};
const mockError = {
response: {
status: 400,
data: { error: "Invalid email" },
},
message: "Bad Request",
};
(axios.isAxiosError as jest.Mock).mockReturnValue(true);
mockedAxios.post.mockRejectedValueOnce(mockError);
await expect(client.sendInvite(inviteData)).rejects.toThrow("Invite API request failed: Bad Request");
});
it("should handle non-axios error in send invite", async () => {
const inviteData: SendInviteDTO = {
email: "test@example.com",
companyId: "company123",
};
const mockError = new Error("Network error");
(axios.isAxiosError as jest.Mock).mockReturnValue(false);
mockedAxios.post.mockRejectedValueOnce(mockError);
await expect(client.sendInvite(inviteData)).rejects.toThrow("Network error");
});
it("should handle axios error without response in send invite", async () => {
const inviteData: SendInviteDTO = {
email: "test@example.com",
companyId: "company123",
};
const mockError = {
message: "Network Error",
// Нет response
};
(axios.isAxiosError as jest.Mock).mockReturnValue(true);
mockedAxios.post.mockRejectedValueOnce(mockError);
await expect(client.sendInvite(inviteData)).rejects.toThrow("Invite API request failed: Network Error");
});
});
describe("checkEmployeeUniqueness", () => {
beforeEach(() => {
// Мокаем searchEmployees для checkEmployeeUniqueness
jest.spyOn(client, "searchEmployees").mockImplementation(async (params) => {
if (params.lastName === "Иванов" && params.firstName === "Иван") {
return [{ id: "emp123", name: "Иванов Иван" }];
}
if (params.email === "ivan@example.com") {
return [{ id: "emp456", name: "Петров Петр", email: "ivan@example.com" }];
}
if (params.lastName === "Иванов" && !params.firstName) {
return [{ id: "emp789", name: "Иванов Петр" }];
}
return [];
});
});
it("should find exact match", async () => {
const result = await client.checkEmployeeUniqueness("Иванов", "Иван", "ivan@example.com");
expect(result).toEqual({
found: true,
employees: [{ id: "emp123", name: "Иванов Иван" }],
exactMatch: true,
conflictType: "exact",
message: "Найдено точное совпадение: 1 сотрудник(ов) с такими ФИО и email",
});
});
it("should find email conflict", async () => {
// Мокаем searchEmployees для случая без точного совпадения ФИО
jest.spyOn(client, "searchEmployees").mockImplementation(async (params) => {
if (params.lastName === "Петров" && params.firstName === "Петр") {
return []; // Нет точного совпадения ФИО
}
if (params.email === "ivan@example.com") {
return [{ id: "emp456", name: "Петров Петр", email: "ivan@example.com" }];
}
return [];
});
const result = await client.checkEmployeeUniqueness("Петров", "Петр", "ivan@example.com");
expect(result).toEqual({
found: true,
employees: [{ id: "emp456", name: "Петров Петр", email: "ivan@example.com" }],
exactMatch: false,
similarMatches: [{ id: "emp456", name: "Петров Петр", email: "ivan@example.com" }],
conflictType: "email",
message: "Найден сотрудник с таким же email: ivan@example.com",
});
});
it("should find similar matches", async () => {
// Мокаем searchEmployees для случая без точного совпадения и email
jest.spyOn(client, "searchEmployees").mockImplementation(async (params) => {
if (params.lastName === "Иванов" && params.firstName === "Петр") {
return []; // Нет точного совпадения ФИО
}
if (params.email === "petr@example.com") {
return []; // Нет совпадения email
}
if (params.lastName === "Иванов" && !params.firstName) {
return [{ id: "emp789", name: "Иванов Петр" }];
}
return [];
});
const result = await client.checkEmployeeUniqueness("Иванов", "Петр", "petr@example.com");
expect(result).toEqual({
found: false,
employees: [],
exactMatch: false,
similarMatches: [{ id: "emp789", name: "Иванов Петр" }],
conflictType: "similar",
message: "Найдено 1 сотрудник(ов) с такой же фамилией. Проверьте, не является ли это дубликатом.",
});
});
it("should find no conflicts", async () => {
// Мокаем searchEmployees для случая без совпадений
jest.spyOn(client, "searchEmployees").mockResolvedValue([]);
const result = await client.checkEmployeeUniqueness("Сидоров", "Сидор", "sidor@example.com");
expect(result).toEqual({
found: false,
employees: [],
exactMatch: false,
conflictType: "none",
message: "Сотрудник не найден, можно создавать",
});
});
it("should handle search error", async () => {
jest.spyOn(client, "searchEmployees").mockRejectedValue(new Error("Search failed"));
const result = await client.checkEmployeeUniqueness("Иванов", "Иван");
expect(result).toEqual({
found: false,
employees: [],
message: "Ошибка при проверке уникальности",
conflictType: "none",
});
});
it("should handle search result as object with employees array", async () => {
// Мокаем searchEmployees для возврата объекта с employees
jest.spyOn(client, "searchEmployees").mockImplementation(async (params) => {
if (params.lastName === "Иванов" && params.firstName === "Иван") {
return { employees: [{ id: "emp123", name: "Иванов Иван" }] };
}
return { employees: [] };
});
const result = await client.checkEmployeeUniqueness("Иванов", "Иван", "ivan@example.com");
expect(result).toEqual({
found: true,
employees: [{ id: "emp123", name: "Иванов Иван" }],
exactMatch: true,
conflictType: "exact",
message: "Найдено точное совпадение: 1 сотрудник(ов) с такими ФИО и email",
});
});
it("should handle email search with object result", async () => {
// Мокаем searchEmployees для случая без точного совпадения ФИО, но с email конфликтом
jest.spyOn(client, "searchEmployees").mockImplementation(async (params) => {
if (params.lastName === "Петров" && params.firstName === "Петр") {
return { employees: [] }; // Нет точного совпадения ФИО
}
if (params.email === "ivan@example.com") {
return { employees: [{ id: "emp456", name: "Петров Петр", email: "ivan@example.com" }] };
}
return { employees: [] };
});
const result = await client.checkEmployeeUniqueness("Петров", "Петр", "ivan@example.com");
expect(result).toEqual({
found: true,
employees: [{ id: "emp456", name: "Петров Петр", email: "ivan@example.com" }],
exactMatch: false,
similarMatches: [{ id: "emp456", name: "Петров Петр", email: "ivan@example.com" }],
conflictType: "email",
message: "Найден сотрудник с таким же email: ivan@example.com",
});
});
it("should handle similar search with object result", async () => {
// Мокаем searchEmployees для случая без точного совпадения и email, но с похожими
jest.spyOn(client, "searchEmployees").mockImplementation(async (params) => {
if (params.lastName === "Иванов" && params.firstName === "Петр") {
return { employees: [] }; // Нет точного совпадения ФИО
}
if (params.email === "petr@example.com") {
return { employees: [] }; // Нет совпадения email
}
if (params.lastName === "Иванов" && !params.firstName) {
return { employees: [{ id: "emp789", name: "Иванов Петр" }] };
}
return { employees: [] };
});
const result = await client.checkEmployeeUniqueness("Иванов", "Петр", "petr@example.com");
expect(result).toEqual({
found: false,
employees: [],
exactMatch: false,
similarMatches: [{ id: "emp789", name: "Иванов Петр" }],
conflictType: "similar",
message: "Найдено 1 сотрудник(ов) с такой же фамилией. Проверьте, не является ли это дубликатом.",
});
});
});
describe("handleEmployeeConflict", () => {
it("should return create action when no conflicts", async () => {
const createDto: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const conflictInfo = {
found: false,
employees: [],
conflictType: "none" as const,
};
const result = await client.handleEmployeeConflict(createDto, conflictInfo);
expect(result).toEqual({
action: "create",
message: "Конфликтов не найдено, создаём нового сотрудника",
});
});
it("should return manual action for exact conflict", async () => {
const createDto: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const conflictInfo = {
found: true,
employees: [{ id: "emp123", name: "Иванов Иван" }],
conflictType: "exact" as const,
message: "Найден точный дубликат",
};
const result = await client.handleEmployeeConflict(createDto, conflictInfo);
expect(result).toEqual({
action: "manual",
message: "⚠️ Найден точный дубликат: Найден точный дубликат",
suggestedActions: [
"1. Обновить существующую запись",
"2. Создать новую запись (возможно, это другой человек)",
"3. Пропустить добавление",
],
});
});
it("should return manual action for email conflict", async () => {
const createDto: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const conflictInfo = {
found: true,
employees: [{ id: "emp456", name: "Петров Петр", email: "ivan@example.com" }],
conflictType: "email" as const,
message: "Найден сотрудник с таким же email",
};
const result = await client.handleEmployeeConflict(createDto, conflictInfo);
expect(result).toEqual({
action: "manual",
message: "⚠️ Конфликт email: Найден сотрудник с таким же email",
suggestedActions: [
"1. Обновить существующую запись с новым email",
"2. Создать новую запись с другим email",
"3. Пропустить добавление",
],
});
});
it("should return manual action for similar conflict", async () => {
const createDto: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const conflictInfo = {
found: true,
employees: [],
conflictType: "similar" as const,
message: "Найдены похожие сотрудники",
};
const result = await client.handleEmployeeConflict(createDto, conflictInfo);
expect(result).toEqual({
action: "manual",
message: "⚠️ Возможный дубликат: Найдены похожие сотрудники",
suggestedActions: [
"1. Проверить, не является ли это дубликатом",
"2. Создать новую запись (разные люди)",
"3. Пропустить добавление",
],
});
});
it("should return create action for unknown conflict type", async () => {
const createDto: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const conflictInfo = {
found: true,
employees: [],
conflictType: "unknown" as any,
message: "Unknown conflict",
};
const result = await client.handleEmployeeConflict(createDto, conflictInfo);
expect(result).toEqual({
action: "create",
message: "Создаём нового сотрудника",
});
});
});
describe("createEmployeeForce", () => {
it("should create employee forcefully", async () => {
const employeeData: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const mockResponse = { data: { id: "emp123", name: "Иванов Иван" } };
mockedAxios.post.mockResolvedValueOnce(mockResponse);
// Мокаем ErrorHandler.executeWithResilience
mockedErrorHandler.executeWithResilience.mockImplementation(async (fn) => {
return await fn();
});
const result = await client.createEmployeeForce(employeeData);
expect(result).toEqual(mockResponse.data);
expect(mockedAxios.post).toHaveBeenCalledWith("https://test-api.com/employees//employees", employeeData, {
headers: {
Authorization: "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
});
});
it("should handle create employee force error", async () => {
const employeeData: CreateEmployeeDTO = {
companyId: "company123",
lastName: "Иванов",
firstName: "Иван",
email: "ivan@example.com",
};
const mockError = new Error("Force create failed");
mockedAxios.post.mockRejectedValueOnce(mockError);
// Мокаем ErrorHandler.executeWithResilience для выброса ошибки
mockedErrorHandler.executeWithResilience.mockRejectedValueOnce(mockError);
await expect(client.createEmployeeForce(employeeData)).rejects.toThrow("Force create failed");
});
});
describe("updateEmployee", () => {
it("should update employee successfully", async () => {
const employeeId = "emp123";
const updateData: UpdateEmployeeDTO = {
lastName: "Иванов",
firstName: "Иван",
email: "ivan.updated@example.com",
};
const mockResponse = { data: { id: "emp123", name: "Иванов Иван" } };
mockedAxios.put.mockResolvedValueOnce(mockResponse);
// Мокаем ErrorHandler.executeWithResilience
mockedErrorHandler.executeWithResilience.mockImplementation(async (fn) => {
return await fn();
});
const result = await client.updateEmployee(employeeId, updateData);
expect(result).toEqual(mockResponse.data);
expect(mockedAxios.put).toHaveBeenCalledWith("https://test-api.com/employees//employees/emp123", updateData, {
headers: {
Authorization: "test-api-key",
Accept: "application/json",
"Content-Type": "application/json",
},
});
});
it("should handle update employee error", async () => {
const employeeId = "emp123";
const updateData: UpdateEmployeeDTO = {
lastName: "Иванов",
firstName: "Иван",
email: "ivan.updated@example.com",
};
const mockError = new Error("Update failed");
mockedAxios.put.mockRejectedValueOnce(mockError);
// Мокаем ErrorHandler.executeWithResilience для выброса ошибки
mockedErrorHandler.executeWithResilience.mockRejectedValueOnce(mockError);
await expect(client.updateEmployee(employeeId, updateData)).rejects.toThrow("Update failed");
});
});
});