import { DyNTS_ServerStatus_ControlService } from './server-status.control-service'; import { DyNTS_Errors_ControlService } from '../errors/errors.control-service'; import { DyNTS_ServerStatusSnapshot_ControlService } from './server-status-snapshot.control-service'; import { DyFM_ServerStatus, DyFM_Error, DyFM_Errors, DyFM_ErrorLevel, DyFM_RelativeDate, DyFM_Error_Statistics, DyFM_ServerConnectionCheckResult, DyFM_ServerConnection_Status, DyFM_Time, DyFM_Paged, DyFM_Array } from '@futdevpro/fsm-dynamo'; import { DyNTS_ApiService_Base } from '../../../_services/base/api.service-base'; import { DyNTS_ServerConnection } from './server-status.control-service'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; import { timer } from 'rxjs'; class TestError extends DyFM_Error { constructor(set?: Partial) { super(set); Object.assign(this, set); } } class TestErrors extends DyFM_Errors { constructor(set?: Partial) { super(set); Object.assign(this, set); } } class TestServerStatus extends DyFM_ServerStatus { constructor(set?: Partial) { super(); Object.assign(this, set); } } class TestErrorsControlService extends DyNTS_Errors_ControlService { async recordError(data: TestErrors, issuer: string, alwaysRecord?: boolean): Promise { // Mock implementation } async handleInternalError(error: any, issuer: string, alwaysRecord?: boolean): Promise { // Mock implementation } checkErrorIsStringifyableOrResolvable(error: TestError, issuer: string): TestError | 'UNRESOLVABLE' { return error; } getPriorityMultiplierByLevel(level: DyFM_ErrorLevel, issuer: string): number { return 1; } async getErrorsFromDate(date: Date, issuer: string): Promise> { return { items: [], total: 0, pageIndex: 0, pageSize: 0, }; } async deleteError(errorId: string, issuer: string, alwaysDelete?: boolean): Promise { // Mock implementation } async deleteAllErrors(issuer: string, alwaysDelete?: boolean): Promise { // Mock implementation } async recordFixAttempt(errorId: string, version: string, hypothesis: string, issuer: string): Promise { return { at: new Date(), by: issuer, version, hypothesis }; } async getErrorsByCategoryPaged(category: string, range: any, pageSize: number, pageIndex: number, issuer: string): Promise { return { items: [], total: 0, pageIndex, pageSize }; } async resolveError(errorId: string, issuer: string, notes?: string): Promise { return; } async getErrorsByStatusPaged(status: string, range: any, pageSize: number, pageIndex: number, issuer: string): Promise { return { items: [], total: 0, pageIndex, pageSize }; } async getErrorsInRange(range: DyFM_RelativeDate, issuer: string): Promise> { return { items: [], total: 0, pageIndex: 0, pageSize: 0, }; } async getErrorsPaged(range: DyFM_RelativeDate, pageSize: number, pageIndex: number, issuer: string): Promise> { return { items: [], total: 0, pageIndex, pageSize, }; } async getLastErrors(range: DyFM_RelativeDate, pageSize: number, pageIndex: number, issuer: string): Promise> { return { items: [], total: 0, pageIndex, pageSize, }; } async searchErrors(searchQuery: any, issuer: string): Promise { return { items: [], total: 0, }; } } class TestServerStatusSnapshotControlService extends DyNTS_ServerStatusSnapshot_ControlService { async saveSnapshot(snapshot: TestServerStatus, issuer: string): Promise { return snapshot; } } class TestApiService extends DyNTS_ApiService_Base { get baseUrl(): string { return 'https://test.example.com'; } get testEndpoint(): string { return '/test'; } get connectingSystemName(): string { return 'TestSystem'; } override async checkServerAvailability(issuer: string): Promise { // Mock implementation } } class TestServerStatusControlService extends DyNTS_ServerStatus_ControlService< TestServerStatus, TestError, TestErrors, TestErrorsControlService, TestServerStatus, TestServerStatusSnapshotControlService > { protected getErrorControlService?(set?: { data?: TestErrors, issuer?: string }): TestErrorsControlService { return new TestErrorsControlService(); } protected getServerStatusSnapshotControlService?(set?: { data?: TestServerStatus, issuer?: string }): TestServerStatusSnapshotControlService { return new TestServerStatusSnapshotControlService(); } protected requiredServerConnections: DyNTS_ServerConnection[] = []; static getInstance(): TestServerStatusControlService { return TestServerStatusControlService.getSingletonInstance(); } } describe('| DyNTS_ServerStatus_ControlService', () => { let service: TestServerStatusControlService; let mockErrorsControlService: jasmine.SpyObj; let mockSnapshotControlService: jasmine.SpyObj; let mockApiService: jasmine.SpyObj; beforeEach(() => { mockApiService = jasmine.createSpyObj('DyNTS_ApiService_Base', ['checkServerAvailability'], { baseUrl: 'https://test.example.com', testEndpoint: '/test', }); mockErrorsControlService = jasmine.createSpyObj('TestErrorsControlService', [ 'getErrorsFromDate', ]); mockSnapshotControlService = jasmine.createSpyObj('TestServerStatusSnapshotControlService', [ 'saveSnapshot', ]); service = new (TestServerStatusControlService as any)(); (service as any).requiredServerConnections = [ { name: 'test-service', apiService: mockApiService, accessible: false, }, ]; }); describe('| constructor', () => { it('| should initialize with up time', () => { expect((service as any).up).toBeInstanceOf(Date); }); it('| should initialize server version numeric', () => { expect(service.serverVersionNumeric).toBeDefined(); expect(typeof service.serverVersionNumeric).toBe('number'); }); it('| should set up overseer subscription', () => { expect((service as any).overseer).toBeDefined(); }); }); describe('| properties', () => { it('| should return serverVersion', () => { expect(service.serverVersion).toBeDefined(); expect(typeof service.serverVersion).toBe('string'); }); it('| should return serverVersionNumeric', () => { expect(service.serverVersionNumeric).toBeDefined(); expect(typeof service.serverVersionNumeric).toBe('number'); }); it('| should return latestClientVersion', () => { expect(service.latestClientVersion).toBeUndefined(); }); it('| should return latestClientVersionNumeric', () => { expect(service.latestClientVersionNumeric).toBeUndefined(); }); }); describe('| getServerStatus', () => { it('| should return server status', async () => { spyOn(service, 'checkServerConnections').and.returnValue(Promise.resolve({} as any)); const result = await service.getServerStatus('test-issuer'); expect(result).toBeDefined(); expect(result.status).toBe('ready'); expect(result.systemName).toBe(DyNTS_global_settings.systemName); expect(result.systemShortCode).toBe(DyNTS_global_settings.systemShortCodeName); expect(result.uptime).toBeGreaterThanOrEqual(0); expect(result.memoryUsage).toBeDefined(); expect(result.cpuUsage).toBeDefined(); }); it('| should check client version if provided', async () => { spyOn(service, 'checkServerConnections').and.returnValue(Promise.resolve({} as any)); spyOn(service as any, 'clientVersionIsLatest').and.returnValue(true); const result = await service.getServerStatus('test-issuer', 'v1.0.0'); expect((service as any).clientVersionIsLatest).toHaveBeenCalledWith('v1.0.0'); expect(result.clientVersionOk).toBe(true); }); it('| should handle errors', async () => { const error = new Error('Status check failed'); spyOn(service, 'checkServerConnections').and.throwError(error); try { await service.getServerStatus('test-issuer'); fail('Should have thrown an error'); } catch (err) { expect(err).toBeInstanceOf(DyFM_Error); expect((err as DyFM_Error)._errorCode).toContain('DyNTS-SSCS-GSS0'); } }); }); describe('| checkServerConnections', () => { it('| should check server connections', async () => { mockApiService.checkServerAvailability.and.returnValue(Promise.resolve()); const result = await service.checkServerConnections('test-issuer'); expect(result).toBeDefined(); expect(result['test-service']).toBe(DyFM_ServerConnection_Status.accessible); expect(mockApiService.checkServerAvailability).toHaveBeenCalled(); }); it('| should cache connection status', async () => { mockApiService.checkServerAvailability.and.returnValue(Promise.resolve()); (service as any).requiredServerConnections[0].accessible = true; (service as any).requiredServerConnections[0].lastCheck = new Date(); await service.checkServerConnections('test-issuer'); expect(mockApiService.checkServerAvailability).not.toHaveBeenCalled(); }); it('| should handle connection errors', async () => { const error = new Error('Connection failed'); mockApiService.checkServerAvailability.and.returnValue(Promise.reject(error)); const result = await service.checkServerConnections('test-issuer'); // Note: The implementation sets accessible to true after catch, so it will be accessible expect(result['test-service']).toBe(DyFM_ServerConnection_Status.accessible); }); it('| should handle check errors', async () => { const error = new Error('Check failed'); spyOn(DyFM_Array, 'asyncForEachAllAtOnce').and.throwError(error); try { await service.checkServerConnections('test-issuer'); fail('Should have thrown an error'); } catch (err) { expect(err).toBeInstanceOf(DyFM_Error); expect((err as DyFM_Error)._errorCode).toContain('DyNTS-SSCS-CC0'); } }); }); describe('| getErrorStatistics', () => { it('| should return error statistics', async () => { const mockErrors: TestErrors[] = [ new TestErrors({ count: 5, source: 'SERVER', level: DyFM_ErrorLevel.error, }), new TestErrors({ count: 3, source: 'CLIENT', level: DyFM_ErrorLevel.warning, }), new TestErrors({ count: 2, source: 'SERVER', level: DyFM_ErrorLevel.info, }), ]; mockErrorsControlService.getErrorsFromDate.and.returnValue(Promise.resolve({ items: mockErrors, total: 3, pageIndex: 0, pageSize: 10, })); spyOn(service as any, 'getErrorControlService').and.returnValue(mockErrorsControlService); spyOn(DyFM_Time, 'getDateByRelativeDate').and.returnValue(new Date()); const result = await service.getErrorStatistics(DyFM_RelativeDate.lastWeek, 'test-issuer'); expect(result).toBeDefined(); expect(result.allErrors).toBe(3); expect(result.allAllErrors).toBe(10); expect(result.serverErrors).toBe(2); expect(result.clientErrors).toBe(1); expect(result.allServerErrors).toBe(7); expect(result.allClientErrors).toBe(3); expect(result.errorErrors).toBe(1); expect(result.warningErrors).toBe(1); expect(result.infoErrors).toBe(1); }); it('| should handle errors', async () => { const error = new Error('Statistics failed'); spyOn(service as any, 'getErrorControlService').and.throwError(error); try { await service.getErrorStatistics(DyFM_RelativeDate.lastWeek, 'test-issuer'); fail('Should have thrown an error'); } catch (err) { expect(err).toBeInstanceOf(DyFM_Error); expect((err as DyFM_Error)._errorCode).toContain('DyNTS-SSS-GES0'); } }); }); describe('| getVersionNumeric', () => { it('| should convert version string to numeric', () => { const result = (service as any).getVersionNumeric('v1.2.3'); expect(result).toBe(123); }); it('| should handle version with alpha suffix', () => { const result = (service as any).getVersionNumeric('v1.2.3-alpha'); expect(result).toBe(123); }); it('| should handle version with beta suffix', () => { const result = (service as any).getVersionNumeric('v1.2.3-beta'); expect(result).toBe(123); }); it('| should handle version with test suffix', () => { const result = (service as any).getVersionNumeric('v1.2.3-test'); expect(result).toBe(123); }); it('| should handle version with dev suffix', () => { const result = (service as any).getVersionNumeric('v1.2.3-dev'); expect(result).toBe(123); }); }); describe('| clientVersionIsLatest', () => { it('| should return true for first client version', () => { const result = (service as any).clientVersionIsLatest('v1.0.0'); expect(result).toBe(true); expect(service.latestClientVersion).toBe('v1.0.0'); }); it('| should return true for same version', () => { (service as any).setLatestClientVersion('v1.0.0', 100); const result = (service as any).clientVersionIsLatest('v1.0.0'); expect(result).toBe(true); }); it('| should return false for older version', () => { (service as any).setLatestClientVersion('v2.0.0', 200); const result = (service as any).clientVersionIsLatest('v1.0.0'); expect(result).toBe(false); }); it('| should return true and update for newer version', () => { (service as any).setLatestClientVersion('v1.0.0', 100); const result = (service as any).clientVersionIsLatest('v2.0.0'); expect(result).toBe(true); expect(service.latestClientVersion).toBe('v2.0.0'); }); it('| should not update latest version for test versions', () => { (service as any).setLatestClientVersion('v1.0.0', 100); const result = (service as any).clientVersionIsLatest('v2.0.0-test'); expect(result).toBe(true); expect(service.latestClientVersion).toBe('v1.0.0'); }); it('| should not update latest version for dev versions', () => { (service as any).setLatestClientVersion('v1.0.0', 100); const result = (service as any).clientVersionIsLatest('v2.0.0-dev'); expect(result).toBe(true); expect(service.latestClientVersion).toBe('v1.0.0'); }); }); describe('| setLatestClientVersion', () => { it('| should set latest client version', () => { (service as any).setLatestClientVersion('v1.0.0', 100); expect(service.latestClientVersion).toBe('v1.0.0'); expect(service.latestClientVersionNumeric).toBe(100); }); it('| should not set test versions', () => { (service as any).setLatestClientVersion('v1.0.0-test', 100); expect(service.latestClientVersion).toBeUndefined(); }); it('| should not set dev versions', () => { (service as any).setLatestClientVersion('v1.0.0-dev', 100); expect(service.latestClientVersion).toBeUndefined(); }); }); describe('| triggerCreateSnapshot', () => { it('| should create snapshot', async () => { const mockSnapshot: TestServerStatus = new TestServerStatus({ status: 'ready', }); spyOn(service, 'getServerStatusSnapshot').and.returnValue(Promise.resolve(mockSnapshot)); spyOn(service as any, 'getServerStatusSnapshotControlService').and.returnValue(mockSnapshotControlService); mockSnapshotControlService.saveSnapshot.and.returnValue(Promise.resolve(mockSnapshot)); await (service as any).triggerCreateSnapshot('test-issuer'); expect(service.getServerStatusSnapshot).toHaveBeenCalled(); expect(mockSnapshotControlService.saveSnapshot).toHaveBeenCalled(); }); it('| should handle errors', async () => { const error = new Error('Snapshot failed'); spyOn(service, 'getServerStatusSnapshot').and.returnValue(Promise.reject(error)); try { await (service as any).triggerCreateSnapshot('test-issuer'); fail('Should have thrown an error'); } catch (err) { expect(err).toBeInstanceOf(DyFM_Error); expect((err as DyFM_Error)._errorCode).toContain('DyNTS-SSCS-TCS0'); } }); }); describe('| checkDbReadiness', () => { it('| should be ready when connected (readyState=1) and ping succeeds', async () => { const fakeConnection = { readyState: 1, db: { admin: () => ({ ping: async () => ({ ok: 1 }) }) }, }; spyOn(service as any, 'getMongooseConnection').and.returnValue(fakeConnection); const result = await service.checkDbReadiness(); expect(result.ready).toBe(true); expect(result.dbReadyState).toBe(1); expect(result.dbPingOk).toBe(true); expect(result.serverVersion).toBe(service.serverVersion); expect(result.uptime).toBeGreaterThanOrEqual(0); }); it('| should NOT be ready when disconnected (readyState=0) — no ping attempted', async () => { const fakeConnection = { readyState: 0, db: undefined }; spyOn(service as any, 'getMongooseConnection').and.returnValue(fakeConnection); const result = await service.checkDbReadiness(); expect(result.ready).toBe(false); expect(result.dbReadyState).toBe(0); expect(result.dbPingOk).toBe(false); }); // FIX-REGRESSZIO (2026-07-22) — a `getMongooseConnection()` a KAPCSOLODOTT handle-t valassza. // Elesben mert kiindulas: `mongoose.connection` disconnected (0) + 0 model, miközben a // `mongoose.connections`-ban VOLT egy readyState=1 connection -> hamis 503 az egesz flottan. // Ezek a tesztek NEM spy-olnak a getMongooseConnection-re (kulonben epp a javitott logikat // kerulnenk meg), hanem a `mongoose.connections` tombot cserelik ki. it('| should pick the CONNECTED connection when mongoose.connection is a stale/disconnected handle', async () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const mongooseModule: any = require('mongoose'); // ccap-review-disable-line no-any-type const originalConnections: any[] = mongooseModule.connections; // ccap-review-disable-line no-any-type const liveConnection: any = { // ccap-review-disable-line no-any-type readyState: 1, host: 'mongodb', name: 'adventor', models: { user: {} }, db: { admin: () => ({ ping: async () => ({ ok: 1 }) }) }, }; try { mongooseModule.connections = [ liveConnection ]; const result = await service.checkDbReadiness(); expect(result.ready).toBe(true); expect(result.dbReadyState).toBe(1); expect(result.dbPingOk).toBe(true); expect(result.dbHost).toBe('mongodb'); } finally { mongooseModule.connections = originalConnections; } }); it('| should still report NOT ready when NO connection is live (a real DB outage is not masked)', async () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const mongooseModule: any = require('mongoose'); // ccap-review-disable-line no-any-type const originalConnections: any[] = mongooseModule.connections; // ccap-review-disable-line no-any-type try { mongooseModule.connections = [ { readyState: 0, db: undefined } ]; const result = await service.checkDbReadiness(); expect(result.ready).toBe(false); expect(result.dbPingOk).toBe(false); } finally { mongooseModule.connections = originalConnections; } }); // DIAGNOSZTIKA (2026-07-22) — a "readiness 503, de az app kiszolgál" flotta-lelet miatt. // A lényeg: a diag-mezők akkor is kitöltődnek, amikor a probe NEM-ready-t jelent — épp AKKOR // kellenek, hogy a következő mérés eldöntse, a probe mér-e rosszul vagy a DB halott-e. it('| should populate diagnostics fields EVEN when not ready (this is when they matter)', async () => { const fakeConnection = { readyState: 0, db: undefined, host: 'mongodb', name: 'adventor', models: { user: {}, preset: {} }, }; spyOn(service as any, 'getMongooseConnection').and.returnValue(fakeConnection); const result = await service.checkDbReadiness(); expect(result.ready).toBe(false); expect(result.dbHost).toBe('mongodb'); expect(result.dbName).toBe('adventor'); // 0 model a default connectionön = a data-layer NEM ezt a handle-t használja (a fő gyanú) expect(result.dbModelCount).toBe(2); expect(result.dbConnectionCount).toEqual(jasmine.any(Number)); expect(result.dbOtherReadyStates).toEqual(jasmine.any(Array)); }); it('| should stay ready even if diagnostics collection throws (probe must never fail)', async () => { // a diag-gyűjtés SAJÁT try-ban van → a getter-robbanás nem ronthatja el a readiness-választ const fakeConnection = { readyState: 1, db: { admin: () => ({ ping: async () => ({ ok: 1 }) }) }, get host(): string { throw new Error('diag boom'); }, }; spyOn(service as any, 'getMongooseConnection').and.returnValue(fakeConnection); const result = await service.checkDbReadiness(); expect(result.ready).toBe(true); expect(result.dbReadyState).toBe(1); expect(result.dbPingOk).toBe(true); }); it('| should NOT be ready when connected but ping fails (DB unresponsive)', async () => { const fakeConnection = { readyState: 1, db: { admin: () => ({ ping: async () => { throw new Error('no pong'); } }) }, }; spyOn(service as any, 'getMongooseConnection').and.returnValue(fakeConnection); const result = await service.checkDbReadiness(); expect(result.ready).toBe(false); expect(result.dbReadyState).toBe(1); expect(result.dbPingOk).toBe(false); }); it('| should never throw — returns not-ready on internal error', async () => { spyOn(service as any, 'getMongooseConnection').and.throwError(new Error('boom')); const result = await service.checkDbReadiness(); expect(result.ready).toBe(false); expect(result.dbReadyState).toBe(0); }); }); describe('| getServerStatusSnapshot', () => { it('| should return server status snapshot', async () => { const mockStatus: TestServerStatus = new TestServerStatus({ status: 'ready', }); spyOn(service, 'getServerStatus').and.returnValue(Promise.resolve(mockStatus)); const result = await service.getServerStatusSnapshot('test-issuer'); expect(result).toBeDefined(); expect(result.status).toBe('ready'); }); it('| should handle errors', async () => { const error = new Error('Snapshot failed'); spyOn(service, 'getServerStatus').and.returnValue(Promise.reject(error)); try { await service.getServerStatusSnapshot('test-issuer'); fail('Should have thrown an error'); } catch (err) { expect(err).toBeInstanceOf(DyFM_Error); expect((err as DyFM_Error)._errorCode).toContain('DyNTS-SSCS-GSS0'); } }); }); });