From f1f6fcbf76e03012451a3f0bf94c9b70d12e2779 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Wed, 18 Sep 2024 11:01:13 -0700 Subject: [PATCH] fix: app config generate and tests --- .../app-config/app-config.service.spec.ts | 48 +++++++++++++++++-- .../services/app-config/app-config.service.ts | 30 ++++++------ .../deployment/deployment.service.spec.ts | 11 +++++ .../shared/services/skin/skin.service.spec.ts | 9 ++++ src/app/shared/services/skin/skin.service.ts | 4 +- 5 files changed, 80 insertions(+), 22 deletions(-) diff --git a/src/app/shared/services/app-config/app-config.service.spec.ts b/src/app/shared/services/app-config/app-config.service.spec.ts index c87b16810..2b73be5c0 100644 --- a/src/app/shared/services/app-config/app-config.service.spec.ts +++ b/src/app/shared/services/app-config/app-config.service.spec.ts @@ -5,8 +5,14 @@ import { BehaviorSubject } from "rxjs/internal/BehaviorSubject"; import { IAppConfig } from "../../model"; import { signal } from "@angular/core"; import { DeploymentService } from "../deployment/deployment.service"; -import { IAppConfigOverride } from "packages/data-models"; +import { + getDefaultAppConfig, + IAppConfigOverride, + IDeploymentRuntimeConfig, +} from "packages/data-models"; import { deepMergeObjects } from "../../utils"; +import { firstValueFrom } from "rxjs/internal/firstValueFrom"; +import { MockDeploymentService } from "../deployment/deployment.service.spec"; /** Mock calls for field values from the template field service to return test data */ export class MockAppConfigService implements Partial { @@ -30,17 +36,51 @@ export class MockAppConfigService implements Partial { } } +const MOCK_DEPLOYMENT_CONFIG: Partial = { + app_config: { APP_FOOTER_DEFAULTS: { templateName: "mock_footer" } }, +}; + +/** + * Call standalone tests via: + * yarn ng test --include src/app/shared/services/app-config/app-config.service.spec.ts + */ describe("AppConfigService", () => { let service: AppConfigService; beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ provide: DeploymentService, useValue: { config: {} } }], + providers: [ + { provide: DeploymentService, useValue: new MockDeploymentService(MOCK_DEPLOYMENT_CONFIG) }, + ], }); service = TestBed.inject(AppConfigService); }); - it("should be created", () => { - expect(service).toBeTruthy(); + it("applies default config overrides on init", () => { + expect(service.appConfig().APP_HEADER_DEFAULTS.title).toEqual( + getDefaultAppConfig().APP_HEADER_DEFAULTS.title + ); + }); + + it("applies deployment-specific config overrides on init", () => { + expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("mock_footer"); + }); + + it("applies overrides to app config", () => { + service.setAppConfig({ APP_HEADER_DEFAULTS: { title: "updated" } }); + expect(service.appConfig().APP_HEADER_DEFAULTS).toEqual({ + ...getDefaultAppConfig().APP_HEADER_DEFAULTS, + title: "updated", + }); + // also ensure doesn't unset default deployment + expect(service.appConfig().APP_FOOTER_DEFAULTS.templateName).toEqual("mock_footer"); + }); + + it("emits partial changes on app config update", async () => { + firstValueFrom(service.changes$).then((v) => { + expect(v).toEqual({ APP_HEADER_DEFAULTS: { title: "partial changes" } }); + }); + + service.setAppConfig({ APP_HEADER_DEFAULTS: { title: "partial changes" } }); }); }); diff --git a/src/app/shared/services/app-config/app-config.service.ts b/src/app/shared/services/app-config/app-config.service.ts index ee1c23946..04c6bd3df 100644 --- a/src/app/shared/services/app-config/app-config.service.ts +++ b/src/app/shared/services/app-config/app-config.service.ts @@ -13,14 +13,23 @@ import { Router } from "@angular/router"; providedIn: "root", }) export class AppConfigService extends SyncServiceBase { + /** + * Initial config is generated by merging default app config with deployment-specific overrides + * It is accessed via a read-only getter to avoid update from methods + **/ + private readonly initialConfig: IAppConfig = deepMergeObjects( + getDefaultAppConfig(), + this.deploymentService.config.app_config + ); + /** Signal representation of current appConfig value */ - public appConfig = signal(getDefaultAppConfig()); + public appConfig = signal(this.initialConfig); /** * @deprecated - prefer use of config signal and computed/effect bindings * List of constants provided by data-models combined with deployment-specific overrides and skin-specific overrides **/ - public appConfig$ = new BehaviorSubject(getDefaultAppConfig()); + public appConfig$ = new BehaviorSubject(this.initialConfig); /** Tracking observable of deep changes to app config, exposed in `changes` public method */ private appConfigChanges$: Observable>; @@ -54,28 +63,19 @@ export class AppConfigService extends SyncServiceBase { this.initialise(); } - /** When service initialises load any deployment-specific config overrides */ + /** When service initialises load initial config to trigger any side-effects */ private initialise() { - // When first loading handle side-effects from default config (e.g. initial routing). - // Deployment-specific side-effects will be handled when setting the appConfig - const defaultConfig = getDefaultAppConfig(); - this.handleConfigSideEffects(defaultConfig, defaultConfig); - - // Set app config using deployment overrides - this.setAppConfig(this.deploymentService.config.app_config); + this.setAppConfig(this.initialConfig); } /** * Generate a complete app config by deep-merging app config overrides - * with the default config - * @param overrides - * @returns + * with the initial config */ public setAppConfig(overrides: IAppConfigOverride = {}) { // Ignore case where no overrides provides or overrides already applied if (Object.keys(overrides).length === 0) return; - - const mergedConfig = deepMergeObjects(getDefaultAppConfig(), overrides); + const mergedConfig = deepMergeObjects({} as IAppConfig, this.initialConfig, overrides); this.handleConfigSideEffects(overrides, mergedConfig); this.appConfig.set(mergedConfig); this.appConfig$.next(mergedConfig); diff --git a/src/app/shared/services/deployment/deployment.service.spec.ts b/src/app/shared/services/deployment/deployment.service.spec.ts index 956a9a482..d337640da 100644 --- a/src/app/shared/services/deployment/deployment.service.spec.ts +++ b/src/app/shared/services/deployment/deployment.service.spec.ts @@ -7,6 +7,17 @@ const mockConfig: IDeploymentRuntimeConfig = { name: "test", }; +export class MockDeploymentService implements Partial { + public readonly config: IDeploymentRuntimeConfig; + + constructor(config: Partial) { + this.config = { ...DEPLOYMENT_RUNTIME_CONFIG_DEFAULTS, ...config }; + } + public ready(): boolean { + return true; + } +} + /** * Call standalone tests via: * yarn ng test --include src/app/shared/services/deployment/deployment.service.spec.ts diff --git a/src/app/shared/services/skin/skin.service.spec.ts b/src/app/shared/services/skin/skin.service.spec.ts index 6d908902a..91163628c 100644 --- a/src/app/shared/services/skin/skin.service.spec.ts +++ b/src/app/shared/services/skin/skin.service.spec.ts @@ -48,6 +48,9 @@ const MOCK_APP_CONFIG: Partial = { available: ["MOCK_THEME_1", "MOCK_THEME_2"], defaultThemeName: "MOCK_THEME_1", }, + APP_FOOTER_DEFAULTS: { + templateName: "mock_footer", + }, }; /** @@ -82,6 +85,12 @@ describe("SkinService", () => { expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_1"); }); + it("does not change non-overridden values", () => { + expect(service["appConfigService"].appConfig().APP_FOOTER_DEFAULTS).toEqual({ + templateName: "mock_footer", + }); + }); + it("loads active skin from local storage on init if available", () => { service["localStorageService"].setProtected("APP_SKIN", "MOCK_SKIN_2"); expect(service.getActiveSkinName()).toEqual("MOCK_SKIN_2"); diff --git a/src/app/shared/services/skin/skin.service.ts b/src/app/shared/services/skin/skin.service.ts index df0c5c24b..3cea4f82d 100644 --- a/src/app/shared/services/skin/skin.service.ts +++ b/src/app/shared/services/skin/skin.service.ts @@ -7,7 +7,6 @@ import { AppConfigService } from "../app-config/app-config.service"; import { TemplateService } from "../../components/template/services/template.service"; import { ThemeService } from "src/app/feature/theme/services/theme.service"; import { SyncServiceBase } from "../syncService.base"; -import { DeploymentService } from "../deployment/deployment.service"; @Injectable({ providedIn: "root", @@ -21,7 +20,6 @@ export class SkinService extends SyncServiceBase { constructor( private appConfigService: AppConfigService, - private deploymentService: DeploymentService, private localStorageService: LocalStorageService, private templateService: TemplateService, private themeService: ThemeService @@ -83,7 +81,7 @@ export class SkinService extends SyncServiceBase { */ private generateOverrideConfig(skin: IAppSkin) { // Merge onto new object to avoid changing stored revertOverride - const base: RecursivePartial = this.deploymentService.config.app_config || {}; + const base: RecursivePartial = {}; return deepMergeObjects(base, this.revertOverride, skin.appConfig); }