implement MockEnvService and fix tests

This commit is contained in:
Hazelnoot 2025-10-07 23:14:03 -04:00
parent f83a403106
commit 8e000ae313
3 changed files with 96 additions and 14 deletions

View file

@ -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<Record<string, string>>;
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<Record<string, string>> {
return this._env;
}
/**
* Replaces the entire mocked environment.
* Pass undefined to restore the original un-mocked values.
*/
set env(value: Partial<Record<string, string>> | 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();
}
}

View file

@ -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);
});

View file

@ -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<string, string | undefined>;
let mockEnvService: MockEnvService;
let serviceUnderTest: () => SkRateLimiterService;
let mockDefaultUserPolicies: Partial<RolePolicies>;
let mockUserPolicies: Record<string, Partial<RolePolicies>>;
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);