From 8e000ae313694be9cc8aebf186ddd05b6b2d52e2 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Tue, 7 Oct 2025 23:14:03 -0400 Subject: [PATCH] implement MockEnvService and fix tests --- packages/backend/test/misc/MockEnvService.ts | 66 +++++++++++++++++++ packages/backend/test/unit/UtilityService.ts | 3 +- .../server/api/SkRateLimiterServiceTests.ts | 41 ++++++++---- 3 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 packages/backend/test/misc/MockEnvService.ts diff --git a/packages/backend/test/misc/MockEnvService.ts b/packages/backend/test/misc/MockEnvService.ts new file mode 100644 index 0000000000..a947c70d44 --- /dev/null +++ b/packages/backend/test/misc/MockEnvService.ts @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import process from 'node:process'; +import { Injectable } from '@nestjs/common'; +import { EnvService } from '@/core/EnvService.js'; +import { CacheManagementService } from '@/core/CacheManagementService.js'; +import { bindThis } from '@/decorators.js'; + +/** + * Implementation of EnvService with support for mocking values. + * Environment and package versions are loaded from their original sources, but can be overridden as-needed. + */ +@Injectable() +export class MockEnvService extends EnvService { + private _env: Partial>; + + constructor(cacheManagementService: CacheManagementService) { + super(cacheManagementService); + this._env = process.env; + } + + /** + * Gets the mocked environment. + * The returned object is "live" and can be modified without polluting the actual application environment. + */ + get env(): Partial> { + return this._env; + } + + /** + * Replaces the entire mocked environment. + * Pass undefined to restore the original un-mocked values. + */ + set env(value: Partial> | undefined) { + if (value !== undefined) { + this._env = value; + } else { + this._env = process.env; + } + } + + /** + * Overrides the version for a dependency. + * Pass a string or null to override the version, or pass undefined to clear the override and restore the original value. + */ + @bindThis + public setDependencyVersion(dependency: string, version: string | null | undefined) { + if (version !== undefined) { + this.dependencyVersionCache.set(dependency, version); + } else { + this.dependencyVersionCache.delete(dependency); + } + } + + /** + * Resets the mock to initial values. + */ + @bindThis + public mockReset(): void { + this._env = process.env; + this.dependencyVersionCache.clear(); + } +} diff --git a/packages/backend/test/unit/UtilityService.ts b/packages/backend/test/unit/UtilityService.ts index 111a35c6b1..5d08829033 100644 --- a/packages/backend/test/unit/UtilityService.ts +++ b/packages/backend/test/unit/UtilityService.ts @@ -4,6 +4,7 @@ */ import * as assert from 'assert'; +import { FakeCacheManagementService } from '../misc/FakeCacheManagementService.js'; import type { MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { SoftwareSuspension } from '@/models/Meta.js'; @@ -31,7 +32,7 @@ describe('UtilityService', () => { federation: 'all', } as unknown as MiMeta; - const envService = new EnvService(); + const envService = new EnvService(new FakeCacheManagementService()); utilityService = new UtilityService(config, meta, envService); }); diff --git a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts index 9bc1caf58a..13b19ae386 100644 --- a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts +++ b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts @@ -4,6 +4,8 @@ */ import { GodOfTimeService } from '../../../misc/GodOfTimeService.js'; +import { MockEnvService } from '../../../misc/MockEnvService.js'; +import { MockInternalEventService } from '../../../misc/MockInternalEventService.js'; import type Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; import type { RolePolicies, RoleService } from '@/core/RoleService.js'; @@ -11,18 +13,19 @@ import type { Config } from '@/config.js'; import { SkRateLimiterService } from '@/server/SkRateLimiterService.js'; import { BucketRateLimit, Keyed, LegacyRateLimit } from '@/misc/rate-limit-utils.js'; import { CacheManagementService } from '@/core/CacheManagementService.js'; -import { InternalEventService } from '@/core/InternalEventService.js'; describe(SkRateLimiterService, () => { + let cacheManagementService: CacheManagementService; + let mockInternalEventService: MockInternalEventService; let mockTimeService: GodOfTimeService; let mockRedis: Array<(command: [string, ...unknown[]]) => [Error | null, unknown] | null>; let mockRedisExec: (batch: [string, ...unknown[]][]) => Promise<[Error | null, unknown][] | null>; - let mockEnvironment: Record; + let mockEnvService: MockEnvService; let serviceUnderTest: () => SkRateLimiterService; let mockDefaultUserPolicies: Partial; let mockUserPolicies: Record>; - beforeEach(() => { + beforeAll(() => { mockTimeService = new GodOfTimeService(); function callMockRedis(command: [string, ...unknown[]]) { @@ -65,11 +68,12 @@ describe(SkRateLimiterService, () => { off() {}, } as unknown as Redis.Redis; - mockEnvironment = Object.create(process.env); - mockEnvironment.NODE_ENV = 'production'; - const mockEnvService = { - env: mockEnvironment, - }; + const fakeConfig = { host: 'example.com' } as unknown as Config; + mockInternalEventService = new MockInternalEventService(fakeConfig); + cacheManagementService = new CacheManagementService(mockRedisClient, mockTimeService, mockInternalEventService); + + mockEnvService = new MockEnvService(cacheManagementService); + mockEnvService.env.NODE_ENV = 'production'; mockDefaultUserPolicies = { rateLimitFactor: 1 }; mockUserPolicies = {}; @@ -80,16 +84,27 @@ describe(SkRateLimiterService, () => { }, } as unknown as RoleService; - const fakeConfig = { host: 'example.com' } as unknown as Config; - const internalEventService = new InternalEventService(mockRedisClient, mockRedisClient, fakeConfig); - const cacheManagementService = new CacheManagementService(mockRedisClient, mockTimeService, internalEventService); - let service: SkRateLimiterService | undefined = undefined; serviceUnderTest = () => { return service ??= new SkRateLimiterService(mockRedisClient, mockRoleService, mockTimeService, mockEnvService, cacheManagementService); }; }); + afterAll(() => { + cacheManagementService.dispose(); + mockInternalEventService.dispose(); + }); + + beforeEach(() => { + mockTimeService.reset(); + }); + + afterEach(() => { + cacheManagementService.clear(); + mockInternalEventService.mockReset(); + mockEnvService.mockReset(); + }); + describe('limit', () => { const actor = 'actor'; const key = 'test'; @@ -173,7 +188,7 @@ describe(SkRateLimiterService, () => { }); it('should bypass in test environment', async () => { - mockEnvironment.NODE_ENV = 'test'; + mockEnvService.env.NODE_ENV = 'test'; const info = await serviceUnderTest().limit({ key: 'l', type: undefined, max: 0 }, actor);