Files
Checkmate/server/test/unit/services/proxiesService.test.ts
2026-08-25 11:19:33 -07:00

249 lines
9.9 KiB
TypeScript

import { describe, expect, it, jest } from "@jest/globals";
import { ProxiesService } from "../../../src/domain/proxies/proxy.service.ts";
import type { Proxy } from "../../../src/domain/proxies/proxy.type.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const createProxiesRepo = () => ({
create: jest.fn(),
findAll: jest.fn(),
findById: jest.fn(),
findByIdOrNull: jest.fn(),
findByTeamId: jest.fn(),
updateById: jest.fn(),
deleteById: jest.fn(),
});
const createMonitorsRepo = () => ({
findMonitorCountByProxyId: jest.fn(),
});
const createSettingsService = () => ({
getDBSettings: jest.fn(),
});
const createService = () => {
const proxiesRepository = createProxiesRepo();
const monitorsRepository = createMonitorsRepo();
const settingsService = createSettingsService();
(monitorsRepository.findMonitorCountByProxyId as jest.Mock).mockResolvedValue(0);
(settingsService.getDBSettings as jest.Mock).mockResolvedValue({ globalProxyId: null });
const service = new ProxiesService(proxiesRepository as any, monitorsRepository as any, settingsService as any);
return { service, proxiesRepository, monitorsRepository, settingsService };
};
const makeProxy = (overrides?: Partial<Proxy>): Proxy =>
({
id: "proxy-1",
teamId: "team-1",
name: "Egress proxy",
protocol: "http",
host: "proxy.internal",
port: 3128,
username: "user",
password: "secret",
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
...overrides,
}) as Proxy;
// ── Tests ────────────────────────────────────────────────────────────────────
describe("ProxiesService", () => {
describe("createProxy", () => {
it("sets teamId and delegates to repository", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.create as jest.Mock).mockResolvedValue(makeProxy());
await service.createProxy({ name: "Egress proxy", protocol: "http", host: "proxy.internal", port: 3128 }, "team-1");
expect(proxiesRepository.create).toHaveBeenCalledWith(expect.objectContaining({ teamId: "team-1", name: "Egress proxy" }));
});
it("omits password from the response and reports hasPassword", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.create as jest.Mock).mockResolvedValue(makeProxy());
const result = await service.createProxy({ name: "Egress proxy" }, "team-1");
expect(result).not.toHaveProperty("password");
expect(result.hasPassword).toBe(true);
});
it("strips blank credentials before creating", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.create as jest.Mock).mockResolvedValue(makeProxy({ username: undefined, password: undefined }));
await service.createProxy({ name: "Egress proxy", username: "", password: "" }, "team-1");
const created = (proxiesRepository.create as jest.Mock).mock.calls[0][0] as Partial<Proxy>;
expect(created).not.toHaveProperty("username");
expect(created).not.toHaveProperty("password");
});
});
describe("getProxy", () => {
it("delegates to repository and masks the password", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.findById as jest.Mock).mockResolvedValue(makeProxy());
const result = await service.getProxy("proxy-1", "team-1");
expect(proxiesRepository.findById).toHaveBeenCalledWith("proxy-1", "team-1");
expect(result).not.toHaveProperty("password");
expect(result.hasPassword).toBe(true);
});
it("reports hasPassword false when none is stored", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.findById as jest.Mock).mockResolvedValue(makeProxy({ password: undefined }));
const result = await service.getProxy("proxy-1", "team-1");
expect(result.hasPassword).toBe(false);
});
});
describe("getProxies", () => {
it("lists every proxy on the instance and masks passwords", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.findAll as jest.Mock).mockResolvedValue([makeProxy(), makeProxy({ id: "proxy-2", teamId: "team-2", password: undefined })]);
const result = await service.getProxies();
expect(proxiesRepository.findAll).toHaveBeenCalledWith();
expect(result).toHaveLength(2);
for (const proxy of result) {
expect(proxy).not.toHaveProperty("password");
}
expect(result[0].hasPassword).toBe(true);
expect(result[1].hasPassword).toBe(false);
});
});
describe("getProxySummary", () => {
it("looks up without a team filter and returns only display fields", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.findByIdOrNull as jest.Mock).mockResolvedValue(makeProxy());
const result = await service.getProxySummary("proxy-1");
expect(proxiesRepository.findByIdOrNull).toHaveBeenCalledWith("proxy-1");
expect(result).toEqual({ id: "proxy-1", name: "Egress proxy", host: "proxy.internal", port: 3128 });
});
it("returns null when the proxy does not exist", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.findByIdOrNull as jest.Mock).mockResolvedValue(null);
const result = await service.getProxySummary("missing");
expect(result).toBeNull();
});
});
describe("getProxiesByTeamId", () => {
it("masks every proxy in the list", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.findByTeamId as jest.Mock).mockResolvedValue([makeProxy(), makeProxy({ id: "proxy-2", password: undefined })]);
const result = await service.getProxiesByTeamId("team-1");
expect(proxiesRepository.findByTeamId).toHaveBeenCalledWith("team-1");
expect(result).toHaveLength(2);
for (const proxy of result) {
expect(proxy).not.toHaveProperty("password");
}
expect(result[0].hasPassword).toBe(true);
expect(result[1].hasPassword).toBe(false);
});
});
describe("updateProxy", () => {
it("keeps the stored password when the patch has a blank one", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.updateById as jest.Mock).mockResolvedValue(makeProxy());
await service.updateProxy("proxy-1", "team-1", { name: "Renamed", password: "", username: "" });
const patch = (proxiesRepository.updateById as jest.Mock).mock.calls[0][2] as Partial<Proxy>;
expect(patch).not.toHaveProperty("password");
expect(patch).not.toHaveProperty("username");
expect(patch.name).toBe("Renamed");
});
it("passes a new password through", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.updateById as jest.Mock).mockResolvedValue(makeProxy({ password: "next" }));
await service.updateProxy("proxy-1", "team-1", { password: "next" });
const patch = (proxiesRepository.updateById as jest.Mock).mock.calls[0][2] as Partial<Proxy>;
expect(patch.password).toBe("next");
});
it("maps clear flags to unset options and keeps them out of the patch", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.updateById as jest.Mock).mockResolvedValue(makeProxy({ username: undefined, password: undefined }));
await service.updateProxy("proxy-1", "team-1", { name: "Renamed", clearPassword: true, clearUsername: true });
const [, , patch, options] = (proxiesRepository.updateById as jest.Mock).mock.calls[0];
expect(patch).not.toHaveProperty("clearPassword");
expect(patch).not.toHaveProperty("clearUsername");
expect(options).toEqual({ unsetPassword: true, unsetUsername: true });
});
it("does not unset when clear flags are absent", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.updateById as jest.Mock).mockResolvedValue(makeProxy());
await service.updateProxy("proxy-1", "team-1", { name: "Renamed" });
const options = (proxiesRepository.updateById as jest.Mock).mock.calls[0][3];
expect(options).toEqual({ unsetPassword: false, unsetUsername: false });
});
it("masks the updated proxy in the response", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.updateById as jest.Mock).mockResolvedValue(makeProxy());
const result = await service.updateProxy("proxy-1", "team-1", { name: "Renamed" });
expect(result).not.toHaveProperty("password");
expect(result.hasPassword).toBe(true);
});
});
describe("deleteProxy", () => {
it("deletes when unreferenced and masks the response", async () => {
const { service, proxiesRepository } = createService();
(proxiesRepository.deleteById as jest.Mock).mockResolvedValue(makeProxy());
const result = await service.deleteProxy("proxy-1", "team-1");
expect(proxiesRepository.deleteById).toHaveBeenCalledWith("proxy-1", "team-1");
expect(result).not.toHaveProperty("password");
});
it("rejects with 409 when monitors reference the proxy", async () => {
const { service, proxiesRepository, monitorsRepository } = createService();
(monitorsRepository.findMonitorCountByProxyId as jest.Mock).mockResolvedValue(3);
await expect(service.deleteProxy("proxy-1", "team-1")).rejects.toMatchObject({ status: 409 });
expect(proxiesRepository.deleteById).not.toHaveBeenCalled();
});
it("rejects with 409 when the proxy is the global proxy", async () => {
const { service, proxiesRepository, settingsService } = createService();
(settingsService.getDBSettings as jest.Mock).mockResolvedValue({ globalProxyId: "proxy-1" });
await expect(service.deleteProxy("proxy-1", "team-1")).rejects.toMatchObject({ status: 409 });
expect(proxiesRepository.deleteById).not.toHaveBeenCalled();
});
});
});