refactor(system-time): integrate ConfigService for state management

- Replaced direct state access with ConfigService in SystemTimeService to improve dependency management and testability.
- Updated unit tests to mock ConfigService for retrieving system time settings, enhancing test isolation.
- Removed unnecessary getters and store dispatch calls, streamlining the service logic.

This update enhances the maintainability and clarity of the SystemTimeService by leveraging NestJS's configuration management capabilities.
This commit is contained in:
Eli Bosley
2025-10-14 08:25:24 -04:00
parent e7828c316f
commit ff6c4af8ff
4 changed files with 97 additions and 82 deletions

View File

@@ -1,11 +1,11 @@
import { BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { emcmd } from '@app/core/utils/clients/emcmd.js';
import * as PhpLoaderModule from '@app/core/utils/plugins/php-loader.js';
import { getters, store } from '@app/store/index.js';
import {
MANUAL_TIME_REGEX,
UpdateSystemTimeInput,
@@ -20,43 +20,48 @@ const phpLoaderSpy = vi.spyOn(PhpLoaderModule, 'phpLoader');
describe('SystemTimeService', () => {
let service: SystemTimeService;
const emhttpSpy = vi.spyOn(getters, 'emhttp');
const pathsSpy = vi.spyOn(getters, 'paths');
const dispatchSpy = vi.spyOn(store, 'dispatch');
let configService: ConfigService;
beforeEach(async () => {
vi.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [SystemTimeService],
providers: [
SystemTimeService,
{
provide: ConfigService,
useValue: {
get: vi.fn(),
},
},
],
}).compile();
service = module.get<SystemTimeService>(SystemTimeService);
configService = module.get<ConfigService>(ConfigService);
emhttpSpy.mockReturnValue({
var: {
timeZone: 'UTC',
useNtp: true,
ntpServer1: 'time1.google.com',
ntpServer2: 'time2.google.com',
ntpServer3: '',
ntpServer4: '',
},
} as any);
vi.mocked(configService.get).mockImplementation((key: string, defaultValue?: any) => {
if (key === 'store.emhttp.var') {
return {
timeZone: 'UTC',
useNtp: true,
ntpServer1: 'time1.google.com',
ntpServer2: 'time2.google.com',
ntpServer3: '',
ntpServer4: '',
};
}
if (key === 'store.paths.webGuiBase') {
return '/usr/local/emhttp/webGui';
}
return defaultValue;
});
pathsSpy.mockReturnValue({
webGuiBase: '/usr/local/emhttp/webGui',
} as any);
dispatchSpy.mockResolvedValue({} as any);
vi.mocked(emcmd).mockResolvedValue({ ok: true } as any);
phpLoaderSpy.mockResolvedValue('');
});
afterEach(() => {
emhttpSpy.mockReset();
pathsSpy.mockReset();
dispatchSpy.mockReset();
phpLoaderSpy.mockReset();
});
@@ -70,27 +75,33 @@ describe('SystemTimeService', () => {
it('updates time settings, disables NTP, and triggers timezone reset', async () => {
const oldState = {
var: {
timeZone: 'UTC',
useNtp: true,
ntpServer1: 'pool.ntp.org',
ntpServer2: '',
ntpServer3: '',
ntpServer4: '',
},
} as any;
timeZone: 'UTC',
useNtp: true,
ntpServer1: 'pool.ntp.org',
ntpServer2: '',
ntpServer3: '',
ntpServer4: '',
};
const newState = {
var: {
timeZone: 'America/Los_Angeles',
useNtp: false,
ntpServer1: 'time.google.com',
ntpServer2: '',
ntpServer3: '',
ntpServer4: '',
},
} as any;
timeZone: 'America/Los_Angeles',
useNtp: false,
ntpServer1: 'time.google.com',
ntpServer2: '',
ntpServer3: '',
ntpServer4: '',
};
emhttpSpy.mockImplementationOnce(() => oldState).mockReturnValue(newState);
let callCount = 0;
vi.mocked(configService.get).mockImplementation((key: string, defaultValue?: any) => {
if (key === 'store.emhttp.var') {
callCount++;
return callCount === 1 ? oldState : newState;
}
if (key === 'store.paths.webGuiBase') {
return '/usr/local/emhttp/webGui';
}
return defaultValue;
});
const input: UpdateSystemTimeInput = {
timeZone: 'America/Los_Angeles',
@@ -119,8 +130,6 @@ describe('SystemTimeService', () => {
file: '/usr/local/emhttp/webGui/include/ResetTZ.php',
method: 'GET',
});
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expect(typeof dispatchSpy.mock.calls[0][0]).toBe('function');
expect(result.timeZone).toBe('America/Los_Angeles');
expect(result.useNtp).toBe(false);
@@ -143,24 +152,33 @@ describe('SystemTimeService', () => {
it('retains manual mode and generates timestamp when not supplied', async () => {
const manualState = {
var: {
timeZone: 'UTC',
useNtp: false,
ntpServer1: '',
ntpServer2: '',
ntpServer3: '',
ntpServer4: '',
},
} as any;
timeZone: 'UTC',
useNtp: false,
ntpServer1: '',
ntpServer2: '',
ntpServer3: '',
ntpServer4: '',
};
const updatedState = {
timeZone: 'UTC',
useNtp: false,
ntpServer1: 'time.cloudflare.com',
ntpServer2: '',
ntpServer3: '',
ntpServer4: '',
};
const manualStateAfter = {
var: {
...manualState.var,
ntpServer1: 'time.cloudflare.com',
},
} as any;
emhttpSpy.mockImplementationOnce(() => manualState).mockReturnValue(manualStateAfter);
let callCount = 0;
vi.mocked(configService.get).mockImplementation((key: string, defaultValue?: any) => {
if (key === 'store.emhttp.var') {
callCount++;
return callCount === 1 ? manualState : updatedState;
}
if (key === 'store.paths.webGuiBase') {
return '/usr/local/emhttp/webGui';
}
return defaultValue;
});
const result = await service.updateSystemTime({ ntpServers: ['time.cloudflare.com'] });

View File

@@ -1,11 +1,10 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { join } from 'node:path';
import type { Var } from '@app/core/types/states/var.js';
import { emcmd } from '@app/core/utils/clients/emcmd.js';
import { phpLoader } from '@app/core/utils/plugins/php-loader.js';
import { getters, store } from '@app/store/index.js';
import { loadStateFiles } from '@app/store/modules/emhttp.js';
import {
SystemTime,
UpdateSystemTimeInput,
@@ -17,8 +16,10 @@ const MAX_NTP_SERVERS = 4;
export class SystemTimeService {
private readonly logger = new Logger(SystemTimeService.name);
constructor(private readonly configService: ConfigService) {}
public async getSystemTime(): Promise<SystemTime> {
const varState = this.getVarState();
const varState = this.configService.get<Partial<Var>>('store.emhttp.var', {});
const ntpServers = this.extractNtpServers(varState);
return {
@@ -30,7 +31,7 @@ export class SystemTimeService {
}
public async updateSystemTime(input: UpdateSystemTimeInput): Promise<SystemTime> {
const current = this.getVarState();
const current = this.configService.get<Partial<Var>>('store.emhttp.var', {});
const desiredTimeZone = (input.timeZone ?? current.timeZone)?.trim();
if (!desiredTimeZone) {
@@ -83,20 +84,9 @@ export class SystemTimeService {
await this.resetTimezoneWatcher();
}
try {
await store.dispatch(loadStateFiles());
} catch (error) {
this.logger.warn('Failed to reload emhttp state after updating system time', error as Error);
}
return this.getSystemTime();
}
private getVarState(): Partial<Var> {
const state = getters.emhttp();
return (state?.var ?? {}) as Partial<Var>;
}
private extractNtpServers(varState: Partial<Var>): string[] {
const servers = [
varState.ntpServer1 ?? '',
@@ -151,7 +141,10 @@ export class SystemTimeService {
}
private async resetTimezoneWatcher() {
const webGuiBase = getters.paths().webGuiBase ?? '/usr/local/emhttp/webGui';
const webGuiBase = this.configService.get<string>(
'store.paths.webGuiBase',
'/usr/local/emhttp/webGui'
);
const scriptPath = join(webGuiBase, 'include', 'ResetTZ.php');
try {

View File

@@ -95,16 +95,22 @@ const currentStepIndex = computed(() => {
const isMobile = ref(false);
const checkScreenSize = () => {
isMobile.value = window.innerWidth < 768;
if (typeof window !== 'undefined') {
isMobile.value = window.innerWidth < 768;
}
};
onMounted(() => {
checkScreenSize();
window.addEventListener('resize', checkScreenSize);
if (typeof window !== 'undefined') {
window.addEventListener('resize', checkScreenSize);
}
});
onUnmounted(() => {
window.removeEventListener('resize', checkScreenSize);
if (typeof window !== 'undefined') {
window.removeEventListener('resize', checkScreenSize);
}
});
const orientation = computed(() => (isMobile.value ? 'vertical' : 'horizontal'));

View File

@@ -87,9 +87,7 @@ defineExpose({
</div>
<ActivationWelcomeStep
:t="t"
:partner-name="partnerInfo?.partnerName || undefined"
:is-initial-setup="isInitialSetup"
:on-complete="dropdownHide"
:redirect-to-login="true"
/>