add tests

This commit is contained in:
Alex Holliday committed 2026-08-20 14:38:18 -07:00
1 parent 637bb5cca9
commit cd520dacc5
3 files changed
+374

No files matched your search

@@ -0,0 +1,208 @@
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(),
findById: 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("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();
});
});
});
@@ -470,3 +470,72 @@ describe("monitorValidation — customUpCodes", () => {
});
});
});
describe("monitorValidation — proxy fields", () => {
const baseHttpBody = {
name: "HTTP check",
type: "http" as const,
url: "https://example.com",
};
describe("createMonitorBodyValidation", () => {
it("defaults proxyMode to inherit", () => {
const parsed = createMonitorBodyValidation.parse(baseHttpBody);
expect(parsed.proxyMode).toBe("inherit");
expect(parsed.proxyId).toBeUndefined();
});
it("accepts custom mode with a proxyId", () => {
const parsed = createMonitorBodyValidation.parse({ ...baseHttpBody, proxyMode: "custom", proxyId: "proxy-1" });
expect(parsed.proxyMode).toBe("custom");
expect(parsed.proxyId).toBe("proxy-1");
});
it("rejects custom mode without a proxyId", () => {
expect(() => createMonitorBodyValidation.parse({ ...baseHttpBody, proxyMode: "custom" })).toThrow();
});
it("rejects an unknown proxyMode", () => {
expect(() => createMonitorBodyValidation.parse({ ...baseHttpBody, proxyMode: "global" })).toThrow();
});
it("allows a stray proxyId on non-custom modes", () => {
for (const proxyMode of ["inherit", "none"] as const) {
const parsed = createMonitorBodyValidation.parse({ ...baseHttpBody, proxyMode, proxyId: "proxy-1" });
expect(parsed.proxyMode).toBe(proxyMode);
}
});
});
describe("editMonitorBodyValidation", () => {
it("leaves proxyMode unset when omitted (no default injected on PATCH)", () => {
const parsed = editMonitorBodyValidation.parse({ name: "Renamed" });
expect(parsed.proxyMode).toBeUndefined();
});
it("rejects custom mode without a proxyId", () => {
expect(() => editMonitorBodyValidation.parse({ proxyMode: "custom" })).toThrow();
});
it("accepts custom mode with a proxyId", () => {
const parsed = editMonitorBodyValidation.parse({ proxyMode: "custom", proxyId: "proxy-1" });
expect(parsed.proxyId).toBe("proxy-1");
});
});
describe("importMonitorsBodyValidation", () => {
it("defaults proxyMode to inherit on import", () => {
const parsed = importMonitorsBodyValidation.parse({ monitors: [baseHttpBody] });
expect(parsed.monitors[0].proxyMode).toBe("inherit");
});
it("rejects an imported monitor with custom mode and no proxyId", () => {
expect(() => importMonitorsBodyValidation.parse({ monitors: [{ ...baseHttpBody, proxyMode: "custom" }] })).toThrow();
});
});
});
@@ -0,0 +1,97 @@
import { describe, expect, it } from "@jest/globals";
import { createProxyBodyValidation, editProxyBodyValidation, proxyResponseSchema } from "../../../src/api/validation/proxyValidation.ts";
const validBody = {
name: "Egress proxy",
protocol: "http" as const,
host: "proxy.internal",
port: 3128,
};
describe("proxyValidation", () => {
describe("createProxyBodyValidation", () => {
it("accepts a minimal valid body", () => {
const parsed = createProxyBodyValidation.parse(validBody);
expect(parsed).toEqual(validBody);
});
it("accepts optional credentials", () => {
const parsed = createProxyBodyValidation.parse({ ...validBody, username: "user", password: "secret" });
expect(parsed.username).toBe("user");
expect(parsed.password).toBe("secret");
});
it("rejects an empty name", () => {
expect(() => createProxyBodyValidation.parse({ ...validBody, name: "" })).toThrow();
});
it("rejects an empty host", () => {
expect(() => createProxyBodyValidation.parse({ ...validBody, host: "" })).toThrow();
});
it("rejects a protocol outside the tuple", () => {
expect(() => createProxyBodyValidation.parse({ ...validBody, protocol: "socks5" })).toThrow();
});
it("accepts the port boundaries", () => {
expect(createProxyBodyValidation.parse({ ...validBody, port: 1 }).port).toBe(1);
expect(createProxyBodyValidation.parse({ ...validBody, port: 65535 }).port).toBe(65535);
});
it("rejects out-of-range and non-integer ports", () => {
for (const port of [0, -1, 65536, 3128.5, "3128"]) {
expect(() => createProxyBodyValidation.parse({ ...validBody, port })).toThrow();
}
});
});
describe("editProxyBodyValidation", () => {
it("accepts the create shape without clear flags", () => {
const parsed = editProxyBodyValidation.parse(validBody);
expect(parsed.clearPassword).toBeUndefined();
expect(parsed.clearUsername).toBeUndefined();
});
it("accepts clearPassword and clearUsername", () => {
const parsed = editProxyBodyValidation.parse({ ...validBody, clearPassword: true, clearUsername: true });
expect(parsed.clearPassword).toBe(true);
expect(parsed.clearUsername).toBe(true);
});
it("rejects non-boolean clear flags", () => {
expect(() => editProxyBodyValidation.parse({ ...validBody, clearPassword: "yes" })).toThrow();
});
});
describe("proxyResponseSchema", () => {
const validResponse = {
id: "proxy-1",
teamId: "team-1",
name: "Egress proxy",
protocol: "http",
host: "proxy.internal",
port: 3128,
hasPassword: false,
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
};
it("accepts a masked proxy", () => {
expect(() => proxyResponseSchema.parse(validResponse)).not.toThrow();
});
it("requires hasPassword", () => {
const { hasPassword: _hasPassword, ...withoutFlag } = validResponse;
expect(() => proxyResponseSchema.parse(withoutFlag)).toThrow();
});
it("does not declare a password field", () => {
expect(Object.keys(proxyResponseSchema.shape)).not.toContain("password");
});
});
});