settingsService tests

This commit is contained in:
Alex Holliday committed 2026-04-10 10:50:37 -07:00
1 parent 6c8adf8594
commit a12154523f
8 files changed
+184 -279

No files matched your search

@@ -51,7 +51,6 @@ export interface ISuperSimpleQueue {
getMetrics(): Promise<QueueMetrics>;
getJobs(): Promise<QueueJobSummary[]>;
flushQueues(): Promise<{ success: boolean }>;
obliterate(): Promise<void>;
}
export class SuperSimpleQueue implements ISuperSimpleQueue {
@@ -363,12 +362,4 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
success: Boolean(stopRes && flushRes && initRes),
};
};
obliterate = async () => {
this.logger.warn({
message: "obliterate method not implemented",
service: SERVICE_NAME,
method: "obliterate",
});
};
}
@@ -9,7 +9,6 @@ const SERVICE_NAME = "BufferService";
export interface IBufferService {
addToBuffer(check: Check): void;
addGeoCheckToBuffer(geoCheck: GeoCheck): void;
removeCheckFromBuffer(check: Check): boolean;
scheduleNextFlush(): void;
flushBuffer(): Promise<void>;
flushGeoBuffer(): Promise<void>;
@@ -72,44 +71,6 @@ export class BufferService implements IBufferService {
}
}
removeCheckFromBuffer(checkToRemove: Check) {
try {
if (!checkToRemove) {
return false;
}
const index = this.buffer.findIndex((check) => {
if (checkToRemove.id && check.id) {
return check.id.toString() === checkToRemove.id.toString();
}
return (
check.metadata.monitorId.toString() === checkToRemove.metadata.monitorId &&
check.metadata.teamId.toString() === checkToRemove.metadata.teamId &&
check.metadata.type === checkToRemove.metadata.type &&
check.status === checkToRemove.status &&
check.statusCode === checkToRemove.statusCode &&
check.responseTime === checkToRemove.responseTime &&
check.message === checkToRemove.message
);
});
if (index !== -1) {
this.buffer.splice(index, 1);
return true;
}
return false;
} catch (error: unknown) {
this.logger.error({
message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME,
method: "removeCheckFromBuffer",
stack: error instanceof Error ? error.stack : undefined,
});
return false;
}
}
scheduleNextFlush() {
if (this.bufferTimer) {
clearTimeout(this.bufferTimer);
@@ -362,27 +362,4 @@ export class StatusService implements IStatusService {
});
}
};
insertCheck = async (check: Check) => {
try {
if (typeof check === "undefined") {
this.logger.warn({
message: "Failed to build check",
service: SERVICE_NAME,
method: "insertCheck",
});
return false;
}
this.buffer.addToBuffer(check);
return true;
} catch (error: unknown) {
this.logger.error({
message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME,
method: "insertCheck",
details: { msg: `Error inserting check for monitor: ${check?.metadata.monitorId}` },
stack: error instanceof Error ? error.stack : undefined,
});
}
};
}
@@ -53,9 +53,6 @@ export class SettingsService implements ISettingsService {
}
getSettings() {
if (!this.settings) {
throw new Error("Settings have not been loaded");
}
return this.settings;
}
@@ -212,125 +212,6 @@ describe("BufferService", () => {
});
});
// ── removeCheckFromBuffer ────────────────────────────────────────────
describe("removeCheckFromBuffer", () => {
it("removes a check by id and returns true", () => {
const { service } = createService();
const check = makeCheck({ id: "c1" });
service.addToBuffer(check);
const result = service.removeCheckFromBuffer(check);
expect(result).toBe(true);
});
it("returns false when check is not found", () => {
const { service } = createService();
service.addToBuffer(makeCheck({ id: "c1" }));
const result = service.removeCheckFromBuffer(makeCheck({ id: "c999" }));
expect(result).toBe(false);
});
it("returns false when passed a falsy value", () => {
const { service } = createService();
const result = service.removeCheckFromBuffer(null as any);
expect(result).toBe(false);
});
it("matches by metadata fields when ids are missing", async () => {
const { service, checkService } = createService();
const check = makeCheck({
id: undefined as any,
metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" } as any,
status: true,
statusCode: 200,
responseTime: 50,
message: "OK",
});
service.addToBuffer(check);
const result = service.removeCheckFromBuffer(check);
expect(result).toBe(true);
await service.flushBuffer();
expect(checkService.createChecks).not.toHaveBeenCalled();
});
it("does not match when metadata fields differ", () => {
const { service } = createService();
const check = makeCheck({
id: undefined as any,
metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" } as any,
status: true,
statusCode: 200,
responseTime: 50,
message: "OK",
});
service.addToBuffer(check);
const different = makeCheck({
id: undefined as any,
metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" } as any,
status: false,
statusCode: 500,
responseTime: 50,
message: "Error",
});
const result = service.removeCheckFromBuffer(different);
expect(result).toBe(false);
});
it("logs error and returns false on exception", () => {
const { service, logger } = createService();
// Force findIndex to throw
Object.defineProperty(service, "buffer", {
value: {
findIndex: () => {
throw new Error("findIndex failed");
},
},
});
const result = service.removeCheckFromBuffer(makeCheck());
expect(result).toBe(false);
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({
message: "findIndex failed",
service: "BufferService",
method: "removeCheckFromBuffer",
})
);
});
it("logs 'Unknown error' for non-Error thrown values in removeCheckFromBuffer", () => {
const { service, logger } = createService();
Object.defineProperty(service, "buffer", {
value: {
findIndex: () => {
throw null;
},
},
});
const result = service.removeCheckFromBuffer(makeCheck());
expect(result).toBe(false);
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({
message: "Unknown error",
stack: undefined,
})
);
});
});
// ── scheduleNextFlush ────────────────────────────────────────────────
describe("scheduleNextFlush", () => {
@@ -0,0 +1,184 @@
import { describe, expect, it, jest } from "@jest/globals";
import { SettingsService } from "../../../src/service/system/settingsService.ts";
import type { ISettingsRepository } from "../../../src/repositories/settings/ISettingsRepository.ts";
import type { ValidatedEnv } from "../../../src/validation/envValidation.ts";
import type { Settings } from "../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const makeEnv = (overrides?: Partial<ValidatedEnv>): ValidatedEnv =>
({
JWT_SECRET: "test-secret",
TOKEN_TTL: "99d",
NODE_ENV: "development",
LOG_LEVEL: "debug",
CLIENT_HOST: "http://localhost:5173",
DB_CONNECTION_STRING: "mongodb://localhost:27017/test_db",
DB_TYPE: "mongodb",
...overrides,
}) as ValidatedEnv;
const makeSettings = (overrides?: Partial<Settings>): Settings =>
({
id: "settings-1",
checkTTL: 30,
language: "en",
systemEmailSecure: false,
systemEmailPool: false,
systemEmailIgnoreTLS: false,
systemEmailRequireTLS: false,
systemEmailRejectUnauthorized: false,
showURL: true,
singleton: true,
version: 1,
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
...overrides,
}) as Settings;
const createSettingsRepo = () =>
({
create: jest.fn().mockResolvedValue(makeSettings()),
findSingleton: jest.fn().mockResolvedValue(makeSettings()),
update: jest.fn().mockResolvedValue(makeSettings()),
deleteLegacy: jest.fn().mockResolvedValue(true),
}) as unknown as jest.Mocked<ISettingsRepository>;
const createService = (envOverrides?: Partial<ValidatedEnv>) => {
const env = makeEnv(envOverrides);
const service = new SettingsService(env);
const settingsRepository = createSettingsRepo();
service.setRepository(settingsRepository);
return { service, settingsRepository, env };
};
// ── Tests ────────────────────────────────────────────────────────────────────
describe("SettingsService", () => {
describe("serviceName", () => {
it("returns SettingsService from static property", () => {
expect(SettingsService.SERVICE_NAME).toBe("SettingsService");
});
it("returns SettingsService from instance getter", () => {
const { service } = createService();
expect(service.serviceName).toBe("SettingsService");
});
});
// ── loadSettings ────────────────────────────────────────────────────────
describe("loadSettings", () => {
it("returns env config mapped from constructor", () => {
const { service } = createService();
const settings = service.loadSettings();
expect(settings).toEqual({
jwtSecret: "test-secret",
jwtTTL: "99d",
nodeEnv: "development",
logLevel: "debug",
clientHost: "http://localhost:5173",
dbConnectionString: "mongodb://localhost:27017/test_db",
dbType: "mongodb",
});
});
it("reflects custom env values", () => {
const { service } = createService({ NODE_ENV: "production", LOG_LEVEL: "error" });
const settings = service.loadSettings();
expect(settings.nodeEnv).toBe("production");
expect(settings.logLevel).toBe("error");
});
});
// ── getSettings ─────────────────────────────────────────────────────────
describe("getSettings", () => {
it("returns the same config as loadSettings", () => {
const { service } = createService();
expect(service.getSettings()).toEqual(service.loadSettings());
});
});
// ── setRepository ───────────────────────────────────────────────────────
describe("setRepository", () => {
it("allows DB methods to work after being called", async () => {
const env = makeEnv();
const service = new SettingsService(env);
const repo = createSettingsRepo();
service.setRepository(repo);
const settings = await service.getDBSettings();
expect(settings).toEqual(makeSettings());
});
});
// ── getDBSettings ───────────────────────────────────────────────────────
describe("getDBSettings", () => {
it("deletes legacy settings and returns singleton", async () => {
const { service, settingsRepository } = createService();
const expected = makeSettings();
(settingsRepository.findSingleton as jest.Mock).mockResolvedValue(expected);
const result = await service.getDBSettings();
expect(settingsRepository.deleteLegacy).toHaveBeenCalled();
expect(settingsRepository.findSingleton).toHaveBeenCalled();
expect(result).toBe(expected);
});
it("creates default settings when singleton is null, then returns them", async () => {
const { service, settingsRepository } = createService();
const created = makeSettings();
(settingsRepository.findSingleton as jest.Mock).mockResolvedValueOnce(null).mockResolvedValueOnce(created);
const result = await service.getDBSettings();
expect(settingsRepository.create).toHaveBeenCalledWith({});
expect(settingsRepository.findSingleton).toHaveBeenCalledTimes(2);
expect(result).toBe(created);
});
it("throws when settings are still null after creation attempt", async () => {
const { service, settingsRepository } = createService();
(settingsRepository.findSingleton as jest.Mock).mockResolvedValue(null);
await expect(service.getDBSettings()).rejects.toThrow("Settings not found");
});
it("throws when repository is not set", async () => {
const env = makeEnv();
const service = new SettingsService(env);
await expect(service.getDBSettings()).rejects.toThrow("Settings repository not initialized");
});
});
// ── updateDbSettings ────────────────────────────────────────────────────
describe("updateDbSettings", () => {
it("delegates to repository and returns updated settings", async () => {
const updated = makeSettings({ checkTTL: 60 });
const { service, settingsRepository } = createService();
(settingsRepository.update as jest.Mock).mockResolvedValue(updated);
const result = await service.updateDbSettings({ checkTTL: 60 });
expect(result).toBe(updated);
expect(settingsRepository.update).toHaveBeenCalledWith({ checkTTL: 60 });
});
it("throws when repository is not set", async () => {
const env = makeEnv();
const service = new SettingsService(env);
await expect(service.updateDbSettings({ checkTTL: 60 })).rejects.toThrow("Settings repository not initialized");
});
});
});
@@ -810,80 +810,4 @@ describe("StatusService", () => {
});
});
});
// ── insertCheck ──────────────────────────────────────────────────────────
describe("insertCheck", () => {
it("adds check to buffer and returns true", async () => {
const { service, buffer } = createService();
const check = makeCheck();
const result = await service.insertCheck(check);
expect(result).toBe(true);
expect(buffer.addToBuffer).toHaveBeenCalledWith(check);
});
it("returns false and logs warning when check is undefined", async () => {
const { service, logger } = createService();
const result = await service.insertCheck(undefined as any);
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: "Failed to build check",
service: "StatusService",
method: "insertCheck",
})
);
});
it("logs error when buffer throws", async () => {
const buffer = createBuffer();
(buffer.addToBuffer as jest.Mock).mockImplementation(() => {
throw new Error("buffer full");
});
const { service, logger } = createService({ buffer });
const result = await service.insertCheck(makeCheck());
expect(result).toBeUndefined();
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({
message: "buffer full",
service: "StatusService",
method: "insertCheck",
})
);
});
it("handles nullish check in error path", async () => {
const buffer = createBuffer();
(buffer.addToBuffer as jest.Mock).mockImplementation(() => {
throw new Error("boom");
});
const { service, logger } = createService({ buffer });
await service.insertCheck(null as any);
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({
details: { msg: "Error inserting check for monitor: undefined" },
})
);
});
it("logs 'Unknown error' for non-Error exceptions in insertCheck", async () => {
const buffer = createBuffer();
(buffer.addToBuffer as jest.Mock).mockImplementation(() => {
throw "string error";
});
const { service, logger } = createService({ buffer });
await service.insertCheck(makeCheck());
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
});
});
});
@@ -535,16 +535,6 @@ describe("SuperSimpleQueue", () => {
});
});
// ── obliterate ───────────────────────────────────────────────────────────
describe("obliterate", () => {
it("logs not-implemented warning", async () => {
const { queue, logger } = createQueue();
await queue.obliterate();
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "obliterate method not implemented" }));
});
});
// ── static create ────────────────────────────────────────────────────────
describe("static create", () => {