settingsService tests
This commit is contained in:
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", () => {
|
||||
|
||||
Reference in new issue
Block a user