Skip to content

Commit

Permalink
fix: app config generate and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismclarke committed Sep 18, 2024
1 parent f72b682 commit f1f6fcb
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 22 deletions.
48 changes: 44 additions & 4 deletions src/app/shared/services/app-config/app-config.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppConfigService> {
Expand All @@ -30,17 +36,51 @@ export class MockAppConfigService implements Partial<AppConfigService> {
}
}

const MOCK_DEPLOYMENT_CONFIG: Partial<IDeploymentRuntimeConfig> = {
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" } });
});
});
30 changes: 15 additions & 15 deletions src/app/shared/services/app-config/app-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IAppConfig>(getDefaultAppConfig());
public appConfig$ = new BehaviorSubject(this.initialConfig);

/** Tracking observable of deep changes to app config, exposed in `changes` public method */
private appConfigChanges$: Observable<RecursivePartial<IAppConfig>>;
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions src/app/shared/services/deployment/deployment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ const mockConfig: IDeploymentRuntimeConfig = {
name: "test",
};

export class MockDeploymentService implements Partial<DeploymentService> {
public readonly config: IDeploymentRuntimeConfig;

constructor(config: Partial<IDeploymentRuntimeConfig>) {
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
Expand Down
9 changes: 9 additions & 0 deletions src/app/shared/services/skin/skin.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ const MOCK_APP_CONFIG: Partial<IAppConfig> = {
available: ["MOCK_THEME_1", "MOCK_THEME_2"],
defaultThemeName: "MOCK_THEME_1",
},
APP_FOOTER_DEFAULTS: {
templateName: "mock_footer",
},
};

/**
Expand Down Expand Up @@ -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");
Expand Down
4 changes: 1 addition & 3 deletions src/app/shared/services/skin/skin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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<IAppConfig> = this.deploymentService.config.app_config || {};
const base: RecursivePartial<IAppConfig> = {};
return deepMergeObjects(base, this.revertOverride, skin.appConfig);
}

Expand Down

0 comments on commit f1f6fcb

Please sign in to comment.