diff --git a/Server/index.js b/Server/index.js index 71dc42467..68a6c354a 100644 --- a/Server/index.js +++ b/Server/index.js @@ -34,9 +34,11 @@ import pkg from "handlebars"; const { compile } = pkg; import mjml2html from "mjml"; +// Settings Service and dependencies import SettingsService from "./service/settingsService.js"; +import AppSettings from "../db/models/AppSettings.js"; + import db from "./db/mongo/MongoDB.js"; -import { fetchMonitorCertificate } from "./controllers/controllerUtils.js"; const SERVICE_NAME = "Server"; let cleaningUp = false; @@ -142,7 +144,7 @@ const startApp = async () => { // Create services await connectDbAndRunServer(app, db); - const settingsService = new SettingsService(); + const settingsService = new SettingsService(AppSettings); await settingsService.loadSettings(); const emailService = new EmailService( diff --git a/Server/service/settingsService.js b/Server/service/settingsService.js index 130de2934..9f44f7d07 100644 --- a/Server/service/settingsService.js +++ b/Server/service/settingsService.js @@ -1,4 +1,3 @@ -import AppSettings from "../db/models/AppSettings.js"; const SERVICE_NAME = "SettingsService"; const envConfig = { logLevel: undefined, @@ -30,7 +29,8 @@ class SettingsService { * Constructs a new SettingsService * @constructor * @throws {Error} - */ constructor() { + */ constructor(appSettings) { + this.appSettings = appSettings; this.settings = { ...envConfig }; } /** @@ -40,7 +40,7 @@ class SettingsService { * @throws Will throw an error if settings are not found in the database or if settings have not been loaded. */ async loadSettings() { try { - const dbSettings = await AppSettings.findOne(); + const dbSettings = await this.appSettings.findOne(); if (!this.settings) { throw new Error("Settings not found"); } @@ -51,10 +51,6 @@ class SettingsService { this.settings[key] = dbSettings[key]; } } - - if (!this.settings) { - throw new Error("Settings not found"); - } return this.settings; } catch (error) { error.service === undefined ? (error.service = SERVICE_NAME) : null; diff --git a/Server/tests/services/settingsService.test.js b/Server/tests/services/settingsService.test.js new file mode 100644 index 000000000..3060758f7 --- /dev/null +++ b/Server/tests/services/settingsService.test.js @@ -0,0 +1,134 @@ +import sinon from "sinon"; +import SettingsService from "../../service/settingsService.js"; +import { expect } from "chai"; +import NetworkService from "../../service/networkService.js"; +const SERVICE_NAME = "SettingsService"; + +describe("SettingsService", () => { + let sandbox, mockAppSettings; + beforeEach(function () { + sandbox = sinon.createSandbox(); + sandbox.stub(process.env, "CLIENT_HOST").value("http://localhost"); + sandbox.stub(process.env, "JWT_SECRET").value("secret"); + sandbox.stub(process.env, "REFRESH_TOKEN_SECRET").value("refreshSecret"); + sandbox.stub(process.env, "DB_TYPE").value("postgres"); + sandbox + .stub(process.env, "DB_CONNECTION_STRING") + .value("postgres://user:pass@localhost/db"); + sandbox.stub(process.env, "REDIS_HOST").value("localhost"); + sandbox.stub(process.env, "REDIS_PORT").value("6379"); + sandbox.stub(process.env, "TOKEN_TTL").value("3600"); + sandbox.stub(process.env, "REFRESH_TOKEN_TTL").value("86400"); + sandbox.stub(process.env, "PAGESPEED_API_KEY").value("apiKey"); + sandbox.stub(process.env, "SYSTEM_EMAIL_HOST").value("smtp.mailtrap.io"); + sandbox.stub(process.env, "SYSTEM_EMAIL_PORT").value("2525"); + sandbox.stub(process.env, "SYSTEM_EMAIL_ADDRESS").value("test@example.com"); + sandbox.stub(process.env, "SYSTEM_EMAIL_PASSWORD").value("password"); + }); + + mockAppSettings = { + settingOne: 123, + settingTwo: 456, + }; + + afterEach(function () { + sandbox.restore(); + sinon.restore(); + }); + describe("constructor", () => { + it("should construct a new SettingsService", () => { + const settingsService = new SettingsService(mockAppSettings); + expect(settingsService.appSettings).to.equal(mockAppSettings); + }); + }); + describe("loadSettings", () => { + it("should load settings from DB when environment variables are not set", async () => { + const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; + const appSettings = { findOne: sinon.stub().returns(dbSettings) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = {}; + const result = await settingsService.loadSettings(); + expect(result).to.deep.equal(dbSettings); + }); + it("should throw an error if settings are not found", async function () { + const appSettings = { findOne: sinon.stub().returns(null) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = null; + + try { + await settingsService.loadSettings(); + } catch (error) { + expect(error.message).to.equal("Settings not found"); + expect(error.service).to.equal(SERVICE_NAME); + expect(error.method).to.equal("loadSettings"); + } + }); + + it("should add its method and service name to error if not present", async () => { + const appSettings = { findOne: sinon.stub().throws(new Error("Test error")) }; + const settingsService = new SettingsService(appSettings); + try { + await settingsService.loadSettings(); + } catch (error) { + expect(error.message).to.equal("Test error"); + expect(error.service).to.equal(SERVICE_NAME); + expect(error.method).to.equal("loadSettings"); + } + }); + it("should not add its method and service name to error if present", async () => { + const error = new Error("Test error"); + error.method = "otherMethod"; + error.service = "OTHER_SERVICE"; + const appSettings = { findOne: sinon.stub().throws(error) }; + const settingsService = new SettingsService(appSettings); + try { + await settingsService.loadSettings(); + } catch (error) { + console.log(error); + expect(error.message).to.equal("Test error"); + expect(error.service).to.equal("OTHER_SERVICE"); + expect(error.method).to.equal("otherMethod"); + } + }); + it("should merge DB settings with environment variables", async function () { + const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; + const appSettings = { findOne: sinon.stub().returns(dbSettings) }; + const settingsService = new SettingsService(appSettings); + const result = await settingsService.loadSettings(); + expect(result).to.deep.equal(settingsService.settings); + expect(settingsService.settings.logLevel).to.equal("debug"); + expect(settingsService.settings.apiBaseUrl).to.equal("http://localhost"); + }); + }); + describe("reloadSettings", () => { + it("should call loadSettings", async () => { + const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; + const appSettings = { findOne: sinon.stub().returns(dbSettings) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = {}; + const result = await settingsService.reloadSettings(); + expect(result).to.deep.equal(dbSettings); + }); + }); + describe("getSettings", () => { + it("should return the current settings", () => { + const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; + const appSettings = { findOne: sinon.stub().returns(dbSettings) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = dbSettings; + const result = settingsService.getSettings(); + expect(result).to.deep.equal(dbSettings); + }); + it("should throw an error if settings have not been loaded", () => { + const appSettings = { findOne: sinon.stub().returns(null) }; + const settingsService = new SettingsService(appSettings); + settingsService.settings = null; + + try { + settingsService.getSettings(); + } catch (error) { + expect(error.message).to.equal("Settings have not been loaded"); + } + }); + }); +});