From a2d966220349c04848013b85a5d2114cd9b3272a Mon Sep 17 00:00:00 2001 From: Mavrik Date: Tue, 28 May 2024 11:40:16 +0200 Subject: [PATCH] feat: added backend unit tests to husky precommit --- .husky/pre-commit | 14 ++ app/Main.tsx | 2 +- .../src/logs/tests/logs.controller.spec.ts | 78 ++++++++- backend/src/logs/tests/logs.service.spec.ts | 18 -- backend/src/utils/utils.service.spec.ts | 18 -- .../tests/validator.controller.spec.ts | 165 +++++++++++++++++- .../validator/tests/validator.service.spec.ts | 18 -- backend/src/validator/validator.service.ts | 1 + src/mocks/beacon.ts | 23 ++- src/mocks/logs.ts | 7 + src/mocks/validatorResults.ts | 31 ++++ 11 files changed, 315 insertions(+), 60 deletions(-) delete mode 100644 backend/src/logs/tests/logs.service.spec.ts delete mode 100644 backend/src/utils/utils.service.spec.ts delete mode 100644 backend/src/validator/tests/validator.service.spec.ts create mode 100644 src/mocks/logs.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 666051fa..07279c5c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,4 +2,18 @@ . "$(dirname "$0")/_/husky.sh" export PATH="/Users/rickimoore/lighthouse-ui:$PATH" + +# Run the Next.js linter with auto-fixing npx next lint --fix + +# Navigate to the backend directory +cd backend + +# Run Jest tests using yarn +yarn test + +# Check if the Jest tests passed +if [ $? -ne 0 ]; then + echo "Jest tests failed. Aborting commit." + exit 1 +fi diff --git a/app/Main.tsx b/app/Main.tsx index 7c9cde62..72d6ff7e 100644 --- a/app/Main.tsx +++ b/app/Main.tsx @@ -26,7 +26,7 @@ const Main = () => { const searchParams = useSearchParams() const redirect = searchParams.get('redirect') const [isLoading, setLoading] = useState(false) - const [step, setStep] = useState(1) + const [step] = useState(1) const [isReady, setReady] = useState(false) const [isVersionError, setVersionError] = useState(false) const [sessionToken, setToken] = useState(Cookies.get('session-token')) diff --git a/backend/src/logs/tests/logs.controller.spec.ts b/backend/src/logs/tests/logs.controller.spec.ts index f45c2b47..bb67ea15 100644 --- a/backend/src/logs/tests/logs.controller.spec.ts +++ b/backend/src/logs/tests/logs.controller.spec.ts @@ -1,18 +1,90 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LogsController } from '../logs.controller'; +import { LogsService } from '../logs.service'; +import { UtilsModule } from '../../utils/utils.module'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { JwtModule } from '@nestjs/jwt'; +import { HttpService } from '@nestjs/axios'; +import { Cache } from 'cache-manager'; +import { SequelizeModule } from '@nestjs/sequelize'; +import { Log } from '../entities/log.entity'; +import { Sequelize } from 'sequelize-typescript'; +import { mockCritLog, mockErrorLog, mockWarningLog } from '../../../../src/mocks/logs'; describe('LogsController', () => { + let logsService: LogsService; let controller: LogsController; + let cacheManager: Cache; + let httpService: HttpService; + let sequelize: Sequelize; + + const mockCacheManager = { + get: jest.fn(), + set: jest.fn(), + }; + + const mockHttpService = { + request: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [ + UtilsModule, + SequelizeModule.forFeature([Log]), + SequelizeModule.forRoot({ + dialect: 'sqlite', + storage: ':memory:', + autoLoadModels: true, + synchronize: true, + }), + JwtModule.register({ + global: true, + secret: 'fake-value', + signOptions: { expiresIn: '7200s' }, // set to 2 hours + }), + ], + providers: [LogsService], controllers: [LogsController], - }).compile(); + }) + .overrideProvider(CACHE_MANAGER) + .useValue(mockCacheManager) + .overrideProvider(HttpService) + .useValue(mockHttpService) + .compile(); controller = module.get(LogsController); + cacheManager = module.get(CACHE_MANAGER); + httpService = module.get(HttpService); + logsService = module.get(LogsService); + sequelize = module.get(Sequelize); + + mockCacheManager.get.mockResolvedValue(null); + + await Log.bulkCreate([ + mockWarningLog, + mockErrorLog, + mockCritLog, + ]); + }); + + afterEach(async () => { + await sequelize.close(); + }); + + it('should return log metrics', async () => { + const data = { + warningLogs: [mockWarningLog], + errorLogs: [mockErrorLog], + criticalLogs: [mockCritLog], + }; + + const result = await controller.getLogMetrics(); + expect(result).toEqual(data); }); - it('should be defined', () => { - expect(controller).toBeDefined(); + it('should update log metrics', async () => { + const result = await controller.dismissLogAlert('1'); + expect(result).toEqual([1]); }); }); diff --git a/backend/src/logs/tests/logs.service.spec.ts b/backend/src/logs/tests/logs.service.spec.ts deleted file mode 100644 index 9287879b..00000000 --- a/backend/src/logs/tests/logs.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { LogsService } from '../logs.service'; - -describe('LogsService', () => { - let service: LogsService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [LogsService], - }).compile(); - - service = module.get(LogsService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/backend/src/utils/utils.service.spec.ts b/backend/src/utils/utils.service.spec.ts deleted file mode 100644 index 9d1b05a2..00000000 --- a/backend/src/utils/utils.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { UtilsService } from './utils.service'; - -describe('UtilsService', () => { - let service: UtilsService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [UtilsService], - }).compile(); - - service = module.get(UtilsService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/backend/src/validator/tests/validator.controller.spec.ts b/backend/src/validator/tests/validator.controller.spec.ts index db096e66..a169cdf9 100644 --- a/backend/src/validator/tests/validator.controller.spec.ts +++ b/backend/src/validator/tests/validator.controller.spec.ts @@ -1,18 +1,181 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ValidatorController } from '../validator.controller'; +import { Cache } from 'cache-manager'; +import { HttpService } from '@nestjs/axios'; +import { UtilsModule } from '../../utils/utils.module'; +import { CACHE_MANAGER, CacheModule } from '@nestjs/cache-manager'; +import { JwtModule } from '@nestjs/jwt'; +import { ValidatorService } from '../validator.service'; +import { AxiosResponse } from 'axios'; +import { of } from 'rxjs'; +import { mockFormattedStates, mockStateResults, mockValCacheValues } from '../../../../src/mocks/beacon'; +import { mockValCacheResults, mockValInfoResult } from '../../../../src/mocks/validatorResults'; +import { SequelizeModule } from '@nestjs/sequelize'; +import { Metric } from '../entities/metric.entity'; +import { Sequelize } from 'sequelize-typescript'; describe('ValidatorController', () => { let controller: ValidatorController; + let cacheManager: Cache; + let httpService: HttpService; + let sequelize: Sequelize; + + const mockCacheManager = { + get: jest.fn(), + set: jest.fn(), + }; + + const mockHttpService = { + request: jest.fn(), + }; + beforeEach(async () => { + process.env.VALIDATOR_URL = 'mock-url' + process.env.API_TOKEN = 'mock-api-token' const module: TestingModule = await Test.createTestingModule({ + imports: [ + UtilsModule, + CacheModule.register(), + SequelizeModule.forFeature([Metric]), + SequelizeModule.forRoot({ + dialect: 'sqlite', + storage: ':memory:', + autoLoadModels: true, + synchronize: true, + }), + JwtModule.register({ + global: true, + secret: 'fake-value', + signOptions: { expiresIn: '7200s' }, // set to 2 hours + }) + ], + providers: [ + ValidatorService + ], controllers: [ValidatorController], - }).compile(); + }).overrideProvider(CACHE_MANAGER).useValue(mockCacheManager) + .overrideProvider(HttpService).useValue(mockHttpService) + .compile(); controller = module.get(ValidatorController); + cacheManager = module.get(CACHE_MANAGER); + httpService = module.get(HttpService); + sequelize = module.get(Sequelize); + + mockCacheManager.get.mockResolvedValue(null); + }); + + afterEach(async () => { + await sequelize.close(); }); it('should be defined', () => { expect(controller).toBeDefined(); }); + + it('getValidatorAuth should return correct auth path', async () => { + const httpVcResponse: AxiosResponse = { data: { data: 'mock-auth-key' } } as AxiosResponse; + mockHttpService.request.mockReturnValueOnce(of(httpVcResponse)); + + const results = await controller.getValidatorAuth() + + console.log(process.env.VALIDATOR_URL) + + expect(results).toEqual({data: 'mock-auth-key'}) + expect(mockHttpService.request).toBeCalledWith({method: "GET", url: "mock-url/lighthouse/auth"}) + }); + + it('should call getValidatorVersion and return correct version', async () => { + const httpVcResponse: AxiosResponse = { data: { data: 'mock-version' } } as AxiosResponse; + mockHttpService.request.mockReturnValueOnce(of(httpVcResponse)); + + const results = await controller.getValidatorVersion() + + expect(results).toEqual('mock-version') + expect(mockHttpService.request).toBeCalledWith({headers: {Authorization: "Bearer mock-api-token"}, method: "GET", url: "mock-url/lighthouse/version"}) + }); + + describe('getValidatorStates', () => { + it('should fetch data from cache', async ()=> { + mockCacheManager.get.mockResolvedValueOnce({ SECONDS_PER_SLOT: '12' }); + const mockCacheValue = { data: 'mock-state' }; + mockCacheManager.get.mockResolvedValue(mockCacheValue); + + const result = await controller.getValidatorStates(); + expect(result).toEqual(mockCacheValue); + expect(mockCacheManager.get).toHaveBeenCalledWith('valStates'); + }); + + it('should return correct data from node', async () => { + mockCacheManager.get.mockResolvedValueOnce({ SECONDS_PER_SLOT: '12' }); + mockCacheManager.get.mockResolvedValueOnce(null); + mockCacheManager.get.mockResolvedValueOnce(mockValCacheValues); + + const httpBeaconResponse: AxiosResponse = { data: { data: mockStateResults } } as AxiosResponse; + mockHttpService.request.mockReturnValueOnce(of(httpBeaconResponse)); + + const result = await controller.getValidatorStates(); + expect(result).toEqual(mockFormattedStates); + }); + }) + + describe('getValidatorCaches', () => { + it('should fetch data from cache', async () => { + mockCacheManager.get.mockResolvedValueOnce({ SECONDS_PER_SLOT: '12' }); + const mockCacheValue = { data: 'mock-cache' }; + mockCacheManager.get.mockResolvedValue(mockCacheValue); + + const result = await controller.getValidatorCaches(); + expect(result).toEqual(mockCacheValue); + expect(mockCacheManager.get).toHaveBeenCalledWith('valCache'); + }); + + it('should return correct data from node', async () => { + mockCacheManager.get.mockResolvedValueOnce({ SECONDS_PER_SLOT: '12' }); + mockCacheManager.get.mockResolvedValueOnce(null); + mockCacheManager.get.mockResolvedValueOnce(mockValCacheValues); + + const httpBeaconResponse: AxiosResponse = { data: { data: mockValInfoResult } } as AxiosResponse; + mockHttpService.request.mockReturnValueOnce(of(httpBeaconResponse)); + + const result = await controller.getValidatorCaches(); + expect(result).toEqual(mockValCacheResults); + }); + }) + + describe('getValidatorMetrics', () => { + it('should fetch all metrics from db', async () => { + await Metric.bulkCreate([ + {id: 1, index: '1', data: JSON.stringify({attestation_target_hit_percentage: 90, attestation_hit_percentage: 95})}, + {id: 2, index: '2', data: JSON.stringify({attestation_target_hit_percentage: 90, attestation_hit_percentage: 95})}, + {id: 3, index: '3', data: JSON.stringify({attestation_target_hit_percentage: 90, attestation_hit_percentage: 95})} + ]) + + const results = await controller.getValidatorMetrics() + expect(results).toEqual({hitEffectiveness: 95, targetEffectiveness: 90, totalEffectiveness: 92.5}) + }); + it('should fetch metrics by id', async () => { + await Metric.bulkCreate([ + {id: 1, index: '1', data: JSON.stringify({attestation_target_hit_percentage: 90, attestation_hit_percentage: 95})}, + {id: 2, index: '1', data: JSON.stringify({attestation_target_hit_percentage: 100, attestation_hit_percentage: 100})}, + {id: 3, index: '2', data: JSON.stringify({attestation_target_hit_percentage: 90, attestation_hit_percentage: 95})}, + {id: 4, index: '3', data: JSON.stringify({attestation_target_hit_percentage: 90, attestation_hit_percentage: 95})} + ]) + + const results = await controller.getValidatorMetricsById(1) + expect(results).toEqual({hitEffectiveness: 97.5, targetEffectiveness: 95, totalEffectiveness: 96.25}) + }); + }) + + it('should fetch val graffiti', async () => { + mockCacheManager.get.mockResolvedValueOnce(mockValCacheValues); + + const httpBeaconResponse: AxiosResponse = { data: { data: {'fake-pub': 'mavrik'} } } as AxiosResponse; + mockHttpService.request.mockReturnValueOnce(of(httpBeaconResponse)); + + const result = await controller.fetchValidatorGraffiti('1') + expect(result).toEqual({data: 'mavrik'}) + expect(mockHttpService.request).toBeCalledWith({headers: {Authorization: "Bearer mock-api-token"}, method: "GET", url: "mock-url/lighthouse/ui/graffiti"}) + }) }); diff --git a/backend/src/validator/tests/validator.service.spec.ts b/backend/src/validator/tests/validator.service.spec.ts deleted file mode 100644 index 917fd55c..00000000 --- a/backend/src/validator/tests/validator.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ValidatorService } from '../validator.service'; - -describe('ValidatorService', () => { - let service: ValidatorService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ValidatorService], - }).compile(); - - service = module.get(ValidatorService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/backend/src/validator/validator.service.ts b/backend/src/validator/validator.service.ts index 8da535b9..3cc4877f 100644 --- a/backend/src/validator/validator.service.ts +++ b/backend/src/validator/validator.service.ts @@ -132,6 +132,7 @@ export class ValidatorService { try { const options = index ? {where: {index}} : undefined const metrics = await this.utilsService.fetchAll(Metric, options) + console.log(metrics) const metricsData = metrics.map(metric => JSON.parse(metric.data)) const targetEffectiveness = getAverageKeyValue(metricsData, 'attestation_target_hit_percentage') diff --git a/src/mocks/beacon.ts b/src/mocks/beacon.ts index cb03ce77..ef42cfcd 100644 --- a/src/mocks/beacon.ts +++ b/src/mocks/beacon.ts @@ -1,3 +1,5 @@ +import { ValidatorStatus } from '../types/validator'; + export const mockEpochCacheValue = { SLOTS_PER_EPOCH: '32', SECONDS_PER_SLOT: '12' }; export const mockedSyncNodeResults = { @@ -39,4 +41,23 @@ export const mockValCacheValues = [ status: 'active_ongoing', withdrawal_credentials: 'fake-creds' } -]; \ No newline at end of file +]; + +export const mockStateResults = [{ + index: '1', + balance: '32000000000', + status: 'active_ongoing', + validator: { + activation_eligibility_epoch: '123', + activation_epoch: '1', + effective_balance: '320000000000', + exit_epoch: '1234', + pubkey: 'mock-pubkey', + slashed: false, + withdrawal_epoch: '12345', + withdrawal_credentials: 'mock-creds' + } +}] + +export const mockFormattedStates = [ + {"aggregated": 0, "attested": 0, "balance": 32, "index": 1, "missed": 0, "name": "VAL-1", "processed": 0, "pubKey": "mock-pubkey", "rewards": 0, "slashed": false, "status": "active_ongoing", "withdrawalAddress": "mock-creds"}] \ No newline at end of file diff --git a/src/mocks/logs.ts b/src/mocks/logs.ts new file mode 100644 index 00000000..c6494877 --- /dev/null +++ b/src/mocks/logs.ts @@ -0,0 +1,7 @@ +import { LogLevels, LogType } from '../types'; + +const fixedDate = new Date('1991-02-21T00:00:00Z'); + +export const mockWarningLog = { id: 1, level: LogLevels.WARN, data: 'fake-warning', type: LogType.BEACON, isHidden: false, createdAt: fixedDate, updatedAt: fixedDate } +export const mockErrorLog = { id: 2, level: LogLevels.ERRO, data: 'fake-error', type: LogType.BEACON, isHidden: false, createdAt: fixedDate, updatedAt: fixedDate } +export const mockCritLog = { id: 3, level: LogLevels.CRIT, data: 'fake-crit', type: LogType.VALIDATOR, isHidden: false, createdAt: fixedDate, updatedAt: fixedDate } \ No newline at end of file diff --git a/src/mocks/validatorResults.ts b/src/mocks/validatorResults.ts index b0680bef..0aaf6a90 100644 --- a/src/mocks/validatorResults.ts +++ b/src/mocks/validatorResults.ts @@ -127,3 +127,34 @@ export const mockValidatorInfo = { attested: 0, aggregated: 0, } + +export const mockValInfoResult = { + "validators": { + "1": { + "info": { + "epoch": "100", + "total_balance": "5000" + } + }, + "2": { + "info": { + "epoch": "101", + "total_balance": "10000" + } + }, + "3": { + "info": { + "epoch": "102", + "total_balance": "7500" + } + } + } +} + +export const mockValCacheResults = { + 1: { epoch: "100", total_balance: "5000" }, + 2: { epoch: "101", total_balance: "10000" }, + 3: { epoch: "102", total_balance: "7500" } +} + +