diff --git a/package-lock.json b/package-lock.json index 8d1623084e..d571725da2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10305,7 +10305,6 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz", "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==", "dev": true, - "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -10668,7 +10667,6 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==", "dev": true, - "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -21111,7 +21109,7 @@ }, "packages/config-service": { "name": "@hashgraph/json-rpc-config-service", - "version": "0.56.0-SNAPSHOT", + "version": "0.59.0-SNAPSHOT", "dependencies": { "dotenv": "^16.4.5", "find-config": "^1.0.0", diff --git a/packages/config-service/src/services/globalConfig.ts b/packages/config-service/src/services/globalConfig.ts index fd28cd287b..cd5dc1f4e4 100644 --- a/packages/config-service/src/services/globalConfig.ts +++ b/packages/config-service/src/services/globalConfig.ts @@ -325,7 +325,7 @@ export class GlobalConfig { envName: 'HBAR_SPENDING_PLANS_CONFIG_FILE', type: 'string', required: false, - defaultValue: null, + defaultValue: 'spendingPlansConfig.json', }, INITIAL_BALANCE: { envName: 'INITIAL_BALANCE', diff --git a/packages/relay/src/lib/clients/cache/localLRUCache.ts b/packages/relay/src/lib/clients/cache/localLRUCache.ts index f6c2918b9f..ab2250b53f 100644 --- a/packages/relay/src/lib/clients/cache/localLRUCache.ts +++ b/packages/relay/src/lib/clients/cache/localLRUCache.ts @@ -61,12 +61,24 @@ export class LocalLRUCache implements ICacheClient { private readonly logger: Logger; /** - * The metrics register used for metrics tracking. + * The gauge used for tracking the size of the cache. * @private */ - private readonly register: Registry; private readonly cacheKeyGauge: Gauge; + /** + * A set of keys that should never be evicted from the cache. + * @private + */ + private readonly reservedKeys: Set; + + /** + * The LRU cache used for caching items from requests that should never be evicted. + * + * @private + */ + private readonly reservedCache?: LRUCache; + /** * Represents a LocalLRUCache instance that uses an LRU (Least Recently Used) caching strategy * for caching items internally from requests. @@ -76,10 +88,13 @@ export class LocalLRUCache implements ICacheClient { * @param {Logger} logger - The logger instance to be used for logging. * @param {Registry} register - The registry instance used for metrics tracking. */ - public constructor(logger: Logger, register: Registry) { + public constructor(logger: Logger, register: Registry, reservedKeys: Set = new Set()) { this.cache = new LRUCache(this.options); this.logger = logger; - this.register = register; + this.reservedKeys = reservedKeys; + if (reservedKeys.size > 0) { + this.reservedCache = new LRUCache({ max: reservedKeys.size }); + } const cacheSizeCollect = (): void => { this.purgeStale(); @@ -107,7 +122,8 @@ export class LocalLRUCache implements ICacheClient { * @returns {*} The cached value if found, otherwise null. */ public async get(key: string, callingMethod: string, requestDetails: RequestDetails): Promise { - const value = this.cache.get(key); + const cache = this.getCacheInstance(key); + const value = cache.get(key); if (value !== undefined) { const censoredKey = key.replace(Utils.IP_ADDRESS_REGEX, ''); const censoredValue = JSON.stringify(value).replace(/"ipAddress":"[^"]+"/, '"ipAddress":""'); @@ -127,7 +143,8 @@ export class LocalLRUCache implements ICacheClient { * @returns {Promise} The remaining TTL in milliseconds. */ public async getRemainingTtl(key: string, callingMethod: string, requestDetails: RequestDetails): Promise { - const remainingTtl = this.cache.getRemainingTTL(key); // in milliseconds + const cache = this.getCacheInstance(key); + const remainingTtl = cache.getRemainingTTL(key); // in milliseconds this.logger.trace( `${requestDetails.formattedRequestId} returning remaining TTL ${key}:${remainingTtl} on ${callingMethod} call`, ); @@ -151,10 +168,11 @@ export class LocalLRUCache implements ICacheClient { ttl?: number, ): Promise { const resolvedTtl = ttl ?? this.options.ttl; + const cache = this.getCacheInstance(key); if (resolvedTtl > 0) { - this.cache.set(key, value, { ttl: resolvedTtl }); + cache.set(key, value, { ttl: resolvedTtl }); } else { - this.cache.set(key, value, { ttl: 0 }); // 0 means indefinite time + cache.set(key, value, { ttl: 0 }); // 0 means indefinite time } const censoredKey = key.replace(Utils.IP_ADDRESS_REGEX, ''); const censoredValue = JSON.stringify(value).replace(/"ipAddress":"[^"]+"/, '"ipAddress":""'); @@ -214,8 +232,9 @@ export class LocalLRUCache implements ICacheClient { * @param {RequestDetails} requestDetails - The request details for logging and tracking. */ public async delete(key: string, callingMethod: string, requestDetails: RequestDetails): Promise { - this.logger.trace(`${requestDetails.formattedRequestId} delete cache for ${key}`); - this.cache.delete(key); + this.logger.trace(`${requestDetails.formattedRequestId} delete cache for ${key} on ${callingMethod} call`); + const cache = this.getCacheInstance(key); + cache.delete(key); } /** @@ -242,7 +261,7 @@ export class LocalLRUCache implements ICacheClient { * @returns {Promise} An array of keys that match the pattern. */ public async keys(pattern: string, callingMethod: string, requestDetails: RequestDetails): Promise { - const keys = Array.from(this.cache.rkeys()); + const keys = [...this.cache.rkeys(), ...(this.reservedCache?.rkeys() ?? [])]; // Replace escaped special characters with placeholders let regexPattern = pattern @@ -275,4 +294,8 @@ export class LocalLRUCache implements ICacheClient { ); return matchingKeys; } + + private getCacheInstance(key: string): LRUCache { + return this.reservedCache && this.reservedKeys.has(key) ? this.reservedCache : this.cache; + } } diff --git a/packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts b/packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts index 778fb7d2b8..ba10371d97 100644 --- a/packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts +++ b/packages/relay/src/lib/config/hbarSpendingPlanConfigService.ts @@ -36,7 +36,6 @@ import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services' * It reads the pre-configured spending plans from a JSON file and populates the cache with them. * * @see SpendingPlanConfig - * @see SPENDING_PLANS_CONFIG_FILE */ export class HbarSpendingPlanConfigService { /** @@ -48,16 +47,6 @@ export class HbarSpendingPlanConfigService { */ private readonly TTL: number = -1; - /** - * The name of the spending plans configuration file. Defaults to `spendingPlansConfig.json`. - * - * @type {string} - * @private - */ - // @ts-ignore - private readonly SPENDING_PLANS_CONFIG_FILE: string = - ConfigService.get('HBAR_SPENDING_PLANS_CONFIG_FILE') || 'spendingPlansConfig.json'; - /** * Creates an instance of `HbarSpendingPlanConfigService`. * @@ -74,6 +63,36 @@ export class HbarSpendingPlanConfigService { private readonly ipAddressHbarSpendingPlanRepository: IPAddressHbarSpendingPlanRepository, ) {} + /** + * Returns the cache keys for the pre-configured spending plans. + * + * @param {Logger} logger - The logger instance. + * @returns {Set} - A set of cache keys for the pre-configured spending plans. + */ + public static getPreconfiguredSpendingPlanKeys(logger: Logger): Set { + try { + const { collectionKey: hbarSpendingPlanKey } = HbarSpendingPlanRepository; + const { collectionKey: ethAddressHbarSpendingPlanKey } = EthAddressHbarSpendingPlanRepository; + const { collectionKey: ipAddressHbarSpendingPlanKey } = IPAddressHbarSpendingPlanRepository; + + return new Set( + this.loadSpendingPlansConfig(logger).flatMap((plan) => { + const { id, ethAddresses = [], ipAddresses = [] } = plan; + return [ + `${hbarSpendingPlanKey}:${id}`, + `${hbarSpendingPlanKey}:${id}:amountSpent`, + `${hbarSpendingPlanKey}:${id}:spendingHistory`, + ...ethAddresses.map((ethAddress) => `${ethAddressHbarSpendingPlanKey}:${ethAddress.trim().toLowerCase()}`), + ...ipAddresses.map((ipAddress) => `${ipAddressHbarSpendingPlanKey}:${ipAddress}`), + ]; + }), + ); + } catch (error: any) { + logger.error(`Failed to get pre-configured spending plan keys: ${error.message}`); + return new Set(); + } + } + /** * Populates the database with pre-configured spending plans. * @@ -81,7 +100,7 @@ export class HbarSpendingPlanConfigService { * @throws {Error} - If the spending plans configuration file is not found or cannot be loaded. */ public async populatePreconfiguredSpendingPlans(): Promise { - const spendingPlanConfigs = this.loadSpendingPlansConfig(); + const spendingPlanConfigs = HbarSpendingPlanConfigService.loadSpendingPlansConfig(this.logger); if (!spendingPlanConfigs.length) { return 0; } @@ -107,10 +126,11 @@ export class HbarSpendingPlanConfigService { * @throws {Error} If the configuration file is not found or cannot be read or parsed. * @private */ - private loadSpendingPlansConfig(): SpendingPlanConfig[] { - const configPath = findConfig(this.SPENDING_PLANS_CONFIG_FILE); + private static loadSpendingPlansConfig(logger: Logger): SpendingPlanConfig[] { + const filename = String(ConfigService.get('HBAR_SPENDING_PLANS_CONFIG_FILE')); + const configPath = findConfig(filename); if (!configPath || !fs.existsSync(configPath)) { - this.logger.trace(`Configuration file not found at path "${configPath ?? this.SPENDING_PLANS_CONFIG_FILE}"`); + logger.trace(`Configuration file not found at path "${configPath ?? filename}"`); return []; } try { diff --git a/packages/relay/src/lib/db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.ts b/packages/relay/src/lib/db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.ts index 41076ca2b0..e06ba2aaac 100644 --- a/packages/relay/src/lib/db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.ts +++ b/packages/relay/src/lib/db/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.ts @@ -26,7 +26,7 @@ import { EthAddressHbarSpendingPlan } from '../../entities/hbarLimiter/ethAddres import { RequestDetails } from '../../../types'; export class EthAddressHbarSpendingPlanRepository { - private readonly collectionKey = 'ethAddressHbarSpendingPlan'; + public static readonly collectionKey = 'ethAddressHbarSpendingPlan'; /** * The cache service used for storing data. @@ -161,6 +161,6 @@ export class EthAddressHbarSpendingPlanRepository { * @private */ private getKey(ethAddress: string): string { - return `${this.collectionKey}:${ethAddress?.trim().toLowerCase()}`; + return `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress?.trim().toLowerCase()}`; } } diff --git a/packages/relay/src/lib/db/repositories/hbarLimiter/hbarSpendingPlanRepository.ts b/packages/relay/src/lib/db/repositories/hbarLimiter/hbarSpendingPlanRepository.ts index 71b7184afe..71eeaae644 100644 --- a/packages/relay/src/lib/db/repositories/hbarLimiter/hbarSpendingPlanRepository.ts +++ b/packages/relay/src/lib/db/repositories/hbarLimiter/hbarSpendingPlanRepository.ts @@ -30,7 +30,7 @@ import { HbarSpendingPlan } from '../../entities/hbarLimiter/hbarSpendingPlan'; import { RequestDetails } from '../../../types'; export class HbarSpendingPlanRepository { - private readonly collectionKey = 'hbarSpendingPlan'; + public static readonly collectionKey = 'hbarSpendingPlan'; /** * The cache service used for storing data. @@ -266,7 +266,7 @@ export class HbarSpendingPlanRepository { * @private */ private getKey(id: string): string { - return `${this.collectionKey}:${id}`; + return `${HbarSpendingPlanRepository.collectionKey}:${id}`; } /** @@ -275,7 +275,7 @@ export class HbarSpendingPlanRepository { * @private */ private getAmountSpentKey(id: string): string { - return `${this.collectionKey}:${id}:amountSpent`; + return `${HbarSpendingPlanRepository.collectionKey}:${id}:amountSpent`; } /** @@ -284,6 +284,6 @@ export class HbarSpendingPlanRepository { * @private */ private getSpendingHistoryKey(id: string): string { - return `${this.collectionKey}:${id}:spendingHistory`; + return `${HbarSpendingPlanRepository.collectionKey}:${id}:spendingHistory`; } } diff --git a/packages/relay/src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.ts b/packages/relay/src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.ts index 5e6892b1f5..6215248075 100644 --- a/packages/relay/src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.ts +++ b/packages/relay/src/lib/db/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.ts @@ -26,7 +26,7 @@ import { IPAddressHbarSpendingPlan } from '../../entities/hbarLimiter/ipAddressH import { RequestDetails } from '../../../types'; export class IPAddressHbarSpendingPlanRepository { - private readonly collectionKey = 'ipAddressHbarSpendingPlan'; + public static readonly collectionKey = 'ipAddressHbarSpendingPlan'; /** * The cache service used for storing data. @@ -161,6 +161,6 @@ export class IPAddressHbarSpendingPlanRepository { * @private */ private getKey(ipAddress: string): string { - return `${this.collectionKey}:${ipAddress}`; + return `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`; } } diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index 8b7a1c4248..6b2d794c3e 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -326,10 +326,9 @@ export class EthImpl implements Eth { requestDetails: RequestDetails, ): Promise { const requestIdPrefix = requestDetails.formattedRequestId; - const maxResults = - ConfigService.get('TEST') - ? constants.DEFAULT_FEE_HISTORY_MAX_RESULTS - : Number(ConfigService.get('FEE_HISTORY_MAX_RESULTS')); + const maxResults = ConfigService.get('TEST') + ? constants.DEFAULT_FEE_HISTORY_MAX_RESULTS + : Number(ConfigService.get('FEE_HISTORY_MAX_RESULTS')); this.logger.trace( `${requestIdPrefix} feeHistory(blockCount=${blockCount}, newestBlock=${newestBlock}, rewardPercentiles=${rewardPercentiles})`, diff --git a/packages/relay/src/lib/relay.ts b/packages/relay/src/lib/relay.ts index e256f3852b..a2b37d3e2e 100644 --- a/packages/relay/src/lib/relay.ts +++ b/packages/relay/src/lib/relay.ts @@ -138,7 +138,8 @@ export class RelayImpl implements Relay { const total = constants.HBAR_RATE_LIMIT_TOTAL; this.eventEmitter = new EventEmitter(); - this.cacheService = new CacheService(logger.child({ name: 'cache-service' }), register); + const reservedKeys = HbarSpendingPlanConfigService.getPreconfiguredSpendingPlanKeys(logger); + this.cacheService = new CacheService(logger.child({ name: 'cache-service' }), register, reservedKeys); const hbarSpendingPlanRepository = new HbarSpendingPlanRepository( this.cacheService, diff --git a/packages/relay/src/lib/services/cacheService/cacheService.ts b/packages/relay/src/lib/services/cacheService/cacheService.ts index 3516bac244..ee735aacb1 100644 --- a/packages/relay/src/lib/services/cacheService/cacheService.ts +++ b/packages/relay/src/lib/services/cacheService/cacheService.ts @@ -91,11 +91,11 @@ export class CacheService { private readonly cacheMethodsCounter: Counter; - public constructor(logger: Logger, register: Registry) { + public constructor(logger: Logger, register: Registry, reservedKeys: Set = new Set()) { this.logger = logger; this.register = register; - this.internalCache = new LocalLRUCache(logger.child({ name: 'localLRUCache' }), register); + this.internalCache = new LocalLRUCache(logger.child({ name: 'localLRUCache' }), register, reservedKeys); this.sharedCache = this.internalCache; // @ts-ignore this.isSharedCacheEnabled = !ConfigService.get('TEST') && this.isRedisEnabled(); diff --git a/packages/relay/tests/helpers.ts b/packages/relay/tests/helpers.ts index f88a9abb07..e0531f8155 100644 --- a/packages/relay/tests/helpers.ts +++ b/packages/relay/tests/helpers.ts @@ -1009,3 +1009,31 @@ export const estimateFileTransactionsFee = ( return estimatedTxFee; }; + +/** + * Verifies the result of a function call. + * @param {() => Promise} func - The function to call. + * @param {Partial | null} expected - The expected result. + * @param {string} [errorMessage] - The expected error message. + * @param {Function | Error} [errorType] - The expected error type. + * @returns {Promise} - A promise that resolves when the verification is complete. + * @template T + */ +export const verifyResult = async ( + func: () => Promise, + expected: Partial | null, + errorMessage?: string, + errorType?: Function | Error, +): Promise => { + if (expected) { + await expect(func()).to.eventually.deep.include(expected); + } else { + if (errorType) { + await expect(func()).to.eventually.be.rejectedWith(errorType, errorMessage); + } else if (errorMessage) { + await expect(func()).to.eventually.be.rejectedWith(errorMessage); + } else { + await expect(func()).to.eventually.be.rejected; + } + } +}; diff --git a/packages/relay/tests/lib/clients/localLRUCache.spec.ts b/packages/relay/tests/lib/clients/localLRUCache.spec.ts index efc50d1847..ebec0293d8 100644 --- a/packages/relay/tests/lib/clients/localLRUCache.spec.ts +++ b/packages/relay/tests/lib/clients/localLRUCache.spec.ts @@ -134,6 +134,37 @@ describe('LocalLRUCache Test Suite', async function () { expect(key3).to.be.equal(keyValuePairs.key3); }); + it('should not evict reserved keys from the cache on reaching the max cache size limit', async function () { + const reservedKeys = ['key1', 'key2']; + const customLocalLRUCache = new LocalLRUCache(logger.child({ name: `cache` }), registry, new Set(reservedKeys)); + const keyValuePairs = { + key1: 'value1', + key2: 'value2', + key3: 'value3', + key4: 'value4', + key5: 'value5', + }; + + for (const [key, value] of Object.entries(keyValuePairs)) { + await customLocalLRUCache.set(key, value, callingMethod, requestDetails); + } + + const key1 = await customLocalLRUCache.get('key1', callingMethod, requestDetails); + const key2 = await customLocalLRUCache.get('key2', callingMethod, requestDetails); + const key3 = await customLocalLRUCache.get('key3', callingMethod, requestDetails); + const key4 = await customLocalLRUCache.get('key4', callingMethod, requestDetails); + const key5 = await customLocalLRUCache.get('key5', callingMethod, requestDetails); + // expect cache to have capped at max size + expect(key1).to.be.equal(keyValuePairs.key1); // key1 should not have been evicted, as it is a reserved key + expect(key2).to.be.equal(keyValuePairs.key2); // key2 should not have been evicted, as it is a reserved key + expect(key3).to.be.null; // key3 should have been evicted + expect(key4).to.be.equal(keyValuePairs.key4); + expect(key5).to.be.equal(keyValuePairs.key5); + + const allKeys = await customLocalLRUCache.keys('*', callingMethod, requestDetails); + expect(allKeys).to.have.members(['key1', 'key2', 'key4', 'key5']); + }); + it('verify cache LRU nature', async function () { const customLocalLRUCache = new LocalLRUCache(logger.child({ name: `cache` }), registry); const key = 'key'; diff --git a/packages/relay/tests/lib/config/hbarSpendingPlanConfigService.spec.ts b/packages/relay/tests/lib/config/hbarSpendingPlanConfigService.spec.ts index cb1dbc996e..ad6da1645e 100644 --- a/packages/relay/tests/lib/config/hbarSpendingPlanConfigService.spec.ts +++ b/packages/relay/tests/lib/config/hbarSpendingPlanConfigService.spec.ts @@ -29,16 +29,32 @@ import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { SpendingPlanConfig } from '../../../src/lib/types/spendingPlanConfig'; import { RequestDetails } from '../../../src/lib/types'; -import { overrideEnvsInMochaDescribe, toHex, useInMemoryRedisServer } from '../../helpers'; +import { overrideEnvsInMochaDescribe, toHex, useInMemoryRedisServer, verifyResult } from '../../helpers'; import findConfig from 'find-config'; import { HbarSpendingPlanConfigService } from '../../../src/lib/config/hbarSpendingPlanConfigService'; import { CacheService } from '../../../src/lib/services/cacheService/cacheService'; import { Registry } from 'prom-client'; +import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'; +import { + EthAddressHbarSpendingPlanNotFoundError, + HbarSpendingPlanNotFoundError, + IPAddressHbarSpendingPlanNotFoundError, +} from '../../../src/lib/db/types/hbarLimiter/errors'; chai.use(chaiAsPromised); describe('HbarSpendingPlanConfigService', function () { - const logger = pino(); + const logger = pino({ + name: 'hbar-spending-plan-config-service', + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: true, + ignore: 'pid,hostname', + }, + }, + }); const registry = new Registry(); const neverExpireTtl = -1; const emptyRequestDetails = new RequestDetails({ requestId: '', ipAddress: '' }); @@ -59,13 +75,33 @@ describe('HbarSpendingPlanConfigService', function () { let ethAddressHbarSpendingPlanRepositorySpy: sinon.SinonSpiedInstance; let ipAddressHbarSpendingPlanRepositorySpy: sinon.SinonSpiedInstance; - overrideEnvsInMochaDescribe({ HBAR_SPENDING_PLANS_CONFIG_FILE: spendingPlansConfigFile }); + overrideEnvsInMochaDescribe({ + HBAR_SPENDING_PLANS_CONFIG_FILE: spendingPlansConfigFile, + CACHE_TTL: '100', + CACHE_MAX: spendingPlansConfig.length.toString(), + }); + + if (isSharedCacheEnabled) { + useInMemoryRedisServer(logger, 6384); + } else { + overrideEnvsInMochaDescribe({ REDIS_ENABLED: 'false' }); + } before(function () { - cacheService = new CacheService(logger, registry); - hbarSpendingPlanRepository = new HbarSpendingPlanRepository(cacheService, logger); - ethAddressHbarSpendingPlanRepository = new EthAddressHbarSpendingPlanRepository(cacheService, logger); - ipAddressHbarSpendingPlanRepository = new IPAddressHbarSpendingPlanRepository(cacheService, logger); + const reservedKeys = HbarSpendingPlanConfigService.getPreconfiguredSpendingPlanKeys(logger); + cacheService = new CacheService(logger.child({ name: 'cache-service' }), registry, reservedKeys); + hbarSpendingPlanRepository = new HbarSpendingPlanRepository( + cacheService, + logger.child({ name: 'hbar-spending-plan-repository' }), + ); + ethAddressHbarSpendingPlanRepository = new EthAddressHbarSpendingPlanRepository( + cacheService, + logger.child({ name: 'evm-address-spending-plan-repository' }), + ); + ipAddressHbarSpendingPlanRepository = new IPAddressHbarSpendingPlanRepository( + cacheService, + logger.child({ name: 'ip-address-spending-plan-repository' }), + ); hbarSpendingPlanConfigService = new HbarSpendingPlanConfigService( logger, hbarSpendingPlanRepository, @@ -74,10 +110,6 @@ describe('HbarSpendingPlanConfigService', function () { ); }); - if (isSharedCacheEnabled) { - useInMemoryRedisServer(logger, 6384); - } - beforeEach(function () { loggerSpy = sinon.spy(logger); cacheServiceSpy = sinon.spy(cacheService); @@ -112,7 +144,7 @@ describe('HbarSpendingPlanConfigService', function () { ethAddresses: ['0x123'], }; sinon - .stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) + .stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) .returns([...spendingPlansConfig, invalidPlan]); await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith( @@ -127,7 +159,7 @@ describe('HbarSpendingPlanConfigService', function () { ethAddresses: ['0x123'], }; sinon - .stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) + .stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) .returns([...spendingPlansConfig, invalidPlan]); await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith( @@ -142,7 +174,7 @@ describe('HbarSpendingPlanConfigService', function () { ethAddresses: ['0x123'], }; sinon - .stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) + .stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) .returns([...spendingPlansConfig, invalidPlan]); await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith( @@ -158,7 +190,7 @@ describe('HbarSpendingPlanConfigService', function () { ethAddresses: ['0x123'], }; sinon - .stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) + .stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) .returns([...spendingPlansConfig, invalidPlan]); await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith( @@ -173,7 +205,7 @@ describe('HbarSpendingPlanConfigService', function () { subscriptionTier: SubscriptionTier.EXTENDED, }; sinon - .stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) + .stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) .returns([...spendingPlansConfig, invalidPlan]); await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith( @@ -190,7 +222,7 @@ describe('HbarSpendingPlanConfigService', function () { ipAddresses: [], }; sinon - .stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) + .stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any) .returns([...spendingPlansConfig, invalidPlan]); await expect(hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans()).to.be.rejectedWith( @@ -200,6 +232,7 @@ describe('HbarSpendingPlanConfigService', function () { }); describe('positive scenarios', function () { + // Helper function to save spending plans and their associations from the configurations const saveSpendingPlans = async (spendingPlansConfig: SpendingPlanConfig[]) => { for (const plan of spendingPlansConfig) { await hbarSpendingPlanRepository.create( @@ -228,6 +261,100 @@ describe('HbarSpendingPlanConfigService', function () { ipAddressHbarSpendingPlanRepositorySpy.save.resetHistory(); }; + // Helper to find obsolete associations (deleted or associated with a different plan in the new config file) + const findObsoleteAssociations = ( + oldConfig: SpendingPlanConfig[], + newConfig: SpendingPlanConfig[], + fieldName: 'ethAddresses' | 'ipAddresses', + ) => { + const obsoleteAssociations: { address: string; oldPlanId: string; newPlanId?: string }[] = []; + + oldConfig.forEach((oldPlan) => { + oldPlan[fieldName]?.forEach((address) => { + const newPlan = newConfig.find((plan) => plan[fieldName]?.includes(address)); + if (!newPlan || newPlan.id !== oldPlan.id) { + obsoleteAssociations.push({ address, oldPlanId: oldPlan.id, newPlanId: newPlan?.id }); + } + }); + }); + + newConfig.forEach((newPlan) => { + newPlan[fieldName]?.forEach((address) => { + const oldPlan = oldConfig.find((oldPlan) => oldPlan[fieldName]?.includes(address)); + if (oldPlan) { + obsoleteAssociations.push({ address, oldPlanId: oldPlan.id, newPlanId: newPlan.id }); + } + }); + }); + + return obsoleteAssociations; + }; + + // Helper function to verify spending plans based on the changes in configuration file + const verifySpendingPlans = async (oldConfig: SpendingPlanConfig[], newConfig?: SpendingPlanConfig[]) => { + const spendingPlans = newConfig || oldConfig; + + // Validate existence of the configured spending plans and their associations to eth and ip addresses + for (const plan of spendingPlans) { + await verifyResult(() => hbarSpendingPlanRepository.findById(plan.id, emptyRequestDetails), { + id: plan.id, + subscriptionTier: plan.subscriptionTier, + active: true, + }); + + for (const ethAddress of plan.ethAddresses || []) { + await verifyResult( + () => ethAddressHbarSpendingPlanRepository.findByAddress(ethAddress, emptyRequestDetails), + { ethAddress, planId: plan.id }, + ); + } + + for (const ipAddress of plan.ipAddresses || []) { + await verifyResult( + () => ipAddressHbarSpendingPlanRepository.findByAddress(ipAddress, emptyRequestDetails), + { ipAddress, planId: plan.id }, + ); + } + } + + // If the config has been changed, check for deleted plans and associations which are no longer valid + if (newConfig) { + const obsoletePlans = oldConfig.filter((oldPlan) => !newConfig.some((plan) => plan.id === oldPlan.id)); + const obsoleteEthAssociations = findObsoleteAssociations(oldConfig, newConfig, 'ethAddresses'); + const obsoleteIpAssociations = findObsoleteAssociations(oldConfig, newConfig, 'ipAddresses'); + + // Validate non-existence of obsolete plans + for (const plan of obsoletePlans) { + await verifyResult( + () => hbarSpendingPlanRepository.findById(plan.id, emptyRequestDetails), + null, + `HbarSpendingPlan with ID ${plan.id} not found`, + HbarSpendingPlanNotFoundError, + ); + } + + // Validate non-existence of obsolete ETH address associations + for (const { address, newPlanId } of obsoleteEthAssociations) { + await verifyResult( + () => ethAddressHbarSpendingPlanRepository.findByAddress(address, emptyRequestDetails), + newPlanId ? { ethAddress: address, planId: newPlanId } : null, + `EthAddressHbarSpendingPlan with address ${address} not found`, + EthAddressHbarSpendingPlanNotFoundError, + ); + } + + // Validate non-existence of obsolete IP address associations + for (const { address, newPlanId } of obsoleteIpAssociations) { + await verifyResult( + () => ipAddressHbarSpendingPlanRepository.findByAddress(address, emptyRequestDetails), + newPlanId ? { ipAddress: address, planId: newPlanId } : null, + `IPAddressHbarSpendingPlan not found`, + IPAddressHbarSpendingPlanNotFoundError, + ); + } + } + }; + it('should populate the database with pre-configured spending plans', async function () { await hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(); @@ -244,6 +371,16 @@ describe('HbarSpendingPlanConfigService', function () { `Created HBAR spending plan "${name}" with ID "${id}" and subscriptionTier "${subscriptionTier}"`, ); }); + + await verifySpendingPlans(spendingPlansConfig); + }); + + it('should not delete pre-configured spending plans after default cache TTL expires', async function () { + await hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(); + + await new Promise((resolve) => setTimeout(resolve, Number(ConfigService.get('CACHE_TTL')))); + + await verifySpendingPlans(spendingPlansConfig); }); it('should remove associations of addresses which were previously linked to BASIC spending plans, but now appear in the configuration file', async function () { @@ -285,6 +422,8 @@ describe('HbarSpendingPlanConfigService', function () { }); } }); + + await verifySpendingPlans(spendingPlansConfig); }); it('should delete obsolete EXTENDED and PRIVILEGED plans from the database', async function () { @@ -337,6 +476,8 @@ describe('HbarSpendingPlanConfigService', function () { sinon.assert.calledWithMatch(cacheServiceSpy.delete, key); }); } + + await verifySpendingPlans(spendingPlansConfig); }); it('should not duplicate already existing spending plans and their associations', async function () { @@ -347,18 +488,19 @@ describe('HbarSpendingPlanConfigService', function () { sinon.assert.notCalled(hbarSpendingPlanRepositorySpy.create); sinon.assert.notCalled(ethAddressHbarSpendingPlanRepositorySpy.save); sinon.assert.notCalled(ipAddressHbarSpendingPlanRepositorySpy.save); + + await verifySpendingPlans(spendingPlansConfig); }); it('should save only new associations for ETH addresses', async function () { - sinon.stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any).returns( - spendingPlansConfig.map((plan, index) => ({ - id: plan.id, - name: plan.name, - subscriptionTier: plan.subscriptionTier, - ethAddresses: [toHex(index)].concat(plan.ethAddresses ? plan.ethAddresses : []), - ipAddresses: plan.ipAddresses, - })), - ); + const newSpendingPlansConfig = spendingPlansConfig.map((plan, index) => ({ + id: plan.id, + name: plan.name, + subscriptionTier: plan.subscriptionTier, + ethAddresses: [toHex(index)].concat(plan.ethAddresses ? plan.ethAddresses : []), + ipAddresses: plan.ipAddresses, + })); + sinon.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any).returns(newSpendingPlansConfig); await saveSpendingPlans(spendingPlansConfig); await hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(); @@ -386,18 +528,19 @@ describe('HbarSpendingPlanConfigService', function () { }); } }); + + await verifySpendingPlans(spendingPlansConfig, newSpendingPlansConfig); }); it('should save only new associations for IP addresses', async function () { - sinon.stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any).returns( - spendingPlansConfig.map((plan, index) => ({ - id: plan.id, - name: plan.name, - subscriptionTier: plan.subscriptionTier, - ethAddresses: plan.ethAddresses, - ipAddresses: [`255.0.0.${index}`].concat(plan.ipAddresses ? plan.ipAddresses : []), - })), - ); + const newSpendingPlansConfig = spendingPlansConfig.map((plan, index) => ({ + id: plan.id, + name: plan.name, + subscriptionTier: plan.subscriptionTier, + ethAddresses: plan.ethAddresses, + ipAddresses: [`255.0.0.${index}`].concat(plan.ipAddresses ? plan.ipAddresses : []), + })); + sinon.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any).returns(newSpendingPlansConfig); await saveSpendingPlans(spendingPlansConfig); await hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(); @@ -421,18 +564,19 @@ describe('HbarSpendingPlanConfigService', function () { }); } }); + + await verifySpendingPlans(spendingPlansConfig, newSpendingPlansConfig); }); it('should delete obsolete associations for ETH addresses', async function () { - sinon.stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any).returns( - spendingPlansConfig.map((plan) => ({ - id: plan.id, - name: plan.name, - subscriptionTier: plan.subscriptionTier, - ethAddresses: plan.ethAddresses ? [plan.ethAddresses[0]] : [], - ipAddresses: plan.ipAddresses, - })), - ); + const newSpendingPlansConfig = spendingPlansConfig.map((plan) => ({ + id: plan.id, + name: plan.name, + subscriptionTier: plan.subscriptionTier, + ethAddresses: plan.ethAddresses ? [plan.ethAddresses[0]] : [], + ipAddresses: plan.ipAddresses, + })); + sinon.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any).returns(newSpendingPlansConfig); await saveSpendingPlans(spendingPlansConfig); await hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(); @@ -460,18 +604,19 @@ describe('HbarSpendingPlanConfigService', function () { }); } }); + + await verifySpendingPlans(spendingPlansConfig, newSpendingPlansConfig); }); it('should delete obsolete associations for IP addresses', async function () { - sinon.stub(hbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any).returns( - spendingPlansConfig.map((plan) => ({ - id: plan.id, - name: plan.name, - subscriptionTier: plan.subscriptionTier, - ethAddresses: plan.ethAddresses, - ipAddresses: plan.ipAddresses ? [plan.ipAddresses[0]] : [], - })), - ); + const newSpendingPlansConfig = spendingPlansConfig.map((plan) => ({ + id: plan.id, + name: plan.name, + subscriptionTier: plan.subscriptionTier, + ethAddresses: plan.ethAddresses, + ipAddresses: plan.ipAddresses ? [plan.ipAddresses[0]] : [], + })); + sinon.stub(HbarSpendingPlanConfigService, 'loadSpendingPlansConfig' as any).returns(newSpendingPlansConfig); await saveSpendingPlans(spendingPlansConfig); await hbarSpendingPlanConfigService.populatePreconfiguredSpendingPlans(); @@ -499,6 +644,8 @@ describe('HbarSpendingPlanConfigService', function () { }); } }); + + await verifySpendingPlans(spendingPlansConfig, newSpendingPlansConfig); }); }); }); diff --git a/packages/relay/tests/lib/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.spec.ts b/packages/relay/tests/lib/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.spec.ts index e0bcd496f2..0a9a37d37b 100644 --- a/packages/relay/tests/lib/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.spec.ts +++ b/packages/relay/tests/lib/repositories/hbarLimiter/ethAddressHbarSpendingPlanRepository.spec.ts @@ -75,7 +75,12 @@ describe('EthAddressHbarSpendingPlanRepository', function () { it('returns true if address plan exists', async () => { const ethAddress = '0x123'; const addressPlan = new EthAddressHbarSpendingPlan({ ethAddress, planId: uuidV4(randomBytes(16)) }); - await cacheService.set(`${repository['collectionKey']}:${ethAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, + addressPlan, + 'test', + requestDetails, + ); await expect(repository.existsByAddress(ethAddress, requestDetails)).to.eventually.be.true; }); @@ -94,7 +99,12 @@ describe('EthAddressHbarSpendingPlanRepository', function () { new EthAddressHbarSpendingPlan({ ethAddress: '0x456', planId }), ]; for (const plan of ethAddressPlans) { - await cacheService.set(`${repository['collectionKey']}:${plan.ethAddress}`, plan, 'test', requestDetails); + await cacheService.set( + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${plan.ethAddress}`, + plan, + 'test', + requestDetails, + ); } const result = await repository.findAllByPlanId(planId, 'findAllByPlanId', requestDetails); @@ -114,14 +124,24 @@ describe('EthAddressHbarSpendingPlanRepository', function () { const ethAddresses = ['0x123', '0x456', '0x789']; for (const ethAddress of ethAddresses) { const addressPlan = new EthAddressHbarSpendingPlan({ ethAddress, planId }); - await cacheService.set(`${repository['collectionKey']}:${ethAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, + addressPlan, + 'test', + requestDetails, + ); } await repository.deleteAllByPlanId(planId, 'deleteAllByPlanId', requestDetails); for (const ethAddress of ethAddresses) { - await expect(cacheService.getAsync(`${repository['collectionKey']}:${ethAddress}`, 'test', requestDetails)).to - .eventually.be.null; + await expect( + cacheService.getAsync( + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, + 'test', + requestDetails, + ), + ).to.eventually.be.null; } }); @@ -135,7 +155,12 @@ describe('EthAddressHbarSpendingPlanRepository', function () { it('retrieves an address plan by address', async () => { const ethAddress = '0x123'; const addressPlan: IEthAddressHbarSpendingPlan = { ethAddress, planId: uuidV4(randomBytes(16)) }; - await cacheService.set(`${repository['collectionKey']}:${ethAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, + addressPlan, + 'test', + requestDetails, + ); const result = await repository.findByAddress(ethAddress, requestDetails); expect(result).to.deep.equal(addressPlan); @@ -157,14 +182,14 @@ describe('EthAddressHbarSpendingPlanRepository', function () { await repository.save(addressPlan, requestDetails, ttl); const result = await cacheService.getAsync( - `${repository['collectionKey']}:${ethAddress}`, + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, 'test', requestDetails, ); expect(result).to.deep.equal(addressPlan); sinon.assert.calledWith( cacheServiceSpy.set, - `${repository['collectionKey']}:${ethAddress}`, + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, addressPlan, 'save', requestDetails, @@ -175,20 +200,25 @@ describe('EthAddressHbarSpendingPlanRepository', function () { it('overwrites an existing address plan', async () => { const ethAddress = '0x123'; const addressPlan: IEthAddressHbarSpendingPlan = { ethAddress, planId: uuidV4(randomBytes(16)) }; - await cacheService.set(`${repository['collectionKey']}:${ethAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, + addressPlan, + 'test', + requestDetails, + ); const newPlanId = uuidV4(randomBytes(16)); const newAddressPlan: IEthAddressHbarSpendingPlan = { ethAddress, planId: newPlanId }; await repository.save(newAddressPlan, requestDetails, ttl); const result = await cacheService.getAsync( - `${repository['collectionKey']}:${ethAddress}`, + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, 'test', requestDetails, ); expect(result).to.deep.equal(newAddressPlan); sinon.assert.calledWith( cacheServiceSpy.set, - `${repository['collectionKey']}:${ethAddress}`, + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, newAddressPlan, 'save', requestDetails, @@ -201,11 +231,16 @@ describe('EthAddressHbarSpendingPlanRepository', function () { it('deletes an address plan successfully', async () => { const ethAddress = '0x123'; const addressPlan: IEthAddressHbarSpendingPlan = { ethAddress, planId: uuidV4(randomBytes(16)) }; - await cacheService.set(`${repository['collectionKey']}:${ethAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, + addressPlan, + 'test', + requestDetails, + ); await repository.delete(ethAddress, requestDetails); const result = await cacheService.getAsync( - `${repository['collectionKey']}:${ethAddress}`, + `${EthAddressHbarSpendingPlanRepository.collectionKey}:${ethAddress}`, 'test', requestDetails, ); diff --git a/packages/relay/tests/lib/repositories/hbarLimiter/hbarSpendingPlanRepository.spec.ts b/packages/relay/tests/lib/repositories/hbarLimiter/hbarSpendingPlanRepository.spec.ts index bd226c64a1..521ac8da4c 100644 --- a/packages/relay/tests/lib/repositories/hbarLimiter/hbarSpendingPlanRepository.spec.ts +++ b/packages/relay/tests/lib/repositories/hbarLimiter/hbarSpendingPlanRepository.spec.ts @@ -77,7 +77,7 @@ describe('HbarSpendingPlanRepository', function () { ); sinon.assert.calledWithMatch( cacheServiceSpy.set, - `${repository['collectionKey']}:${createdPlan.id}`, + `${HbarSpendingPlanRepository.collectionKey}:${createdPlan.id}`, createdPlan, 'create', requestDetails, @@ -137,7 +137,7 @@ describe('HbarSpendingPlanRepository', function () { const subscriptionTier = SubscriptionTier.BASIC; const createdPlan = await repository.create(subscriptionTier, requestDetails, ttl); - const key = `${repository['collectionKey']}:${createdPlan.id}:spendingHistory`; + const key = `${HbarSpendingPlanRepository.collectionKey}:${createdPlan.id}:spendingHistory`; const hbarSpending = { amount: 100, timestamp: new Date() } as IHbarSpendingRecord; await cacheService.rPush(key, hbarSpending, 'test', requestDetails); @@ -164,7 +164,7 @@ describe('HbarSpendingPlanRepository', function () { sinon.assert.calledWithMatch( cacheServiceSpy.rPush, - `${repository['collectionKey']}:${createdPlan.id}:spendingHistory`, + `${HbarSpendingPlanRepository.collectionKey}:${createdPlan.id}:spendingHistory`, { amount, timestamp: spendingHistory[0].timestamp }, 'addAmountToSpendingHistory', requestDetails, @@ -262,7 +262,7 @@ describe('HbarSpendingPlanRepository', function () { expect(plan!.amountSpent).to.equal(amount); sinon.assert.calledWithMatch( cacheServiceSpy.set, - `${repository['collectionKey']}:${createdPlan.id}:amountSpent`, + `${HbarSpendingPlanRepository.collectionKey}:${createdPlan.id}:amountSpent`, amount, 'addToAmountSpent', requestDetails, @@ -274,7 +274,7 @@ describe('HbarSpendingPlanRepository', function () { await repository.addToAmountSpent(createdPlan.id, newAmount, requestDetails, ttl); sinon.assert.calledWithMatch( cacheServiceSpy.incrBy, - `${repository['collectionKey']}:${createdPlan.id}:amountSpent`, + `${HbarSpendingPlanRepository.collectionKey}:${createdPlan.id}:amountSpent`, newAmount, 'addToAmountSpent', requestDetails, @@ -300,7 +300,7 @@ describe('HbarSpendingPlanRepository', function () { const createdPlan = await repository.create(subscriptionTier, requestDetails, ttl); // Manually set the plan to inactive - const key = `${repository['collectionKey']}:${createdPlan.id}`; + const key = `${HbarSpendingPlanRepository.collectionKey}:${createdPlan.id}`; await cacheService.set(key, { ...createdPlan, active: false }, 'test', requestDetails); const amount = 50; @@ -327,7 +327,7 @@ describe('HbarSpendingPlanRepository', function () { const createdPlan = await repository.create(subscriptionTier, requestDetails, ttl); // Manually set the plan to inactive - const key = `${repository['collectionKey']}:${createdPlan.id}`; + const key = `${HbarSpendingPlanRepository.collectionKey}:${createdPlan.id}`; await cacheService.set(key, { ...createdPlan, active: false }, 'test', requestDetails); await expect(repository.checkExistsAndActive(createdPlan.id, requestDetails)).to.be.eventually.rejectedWith( @@ -360,7 +360,7 @@ describe('HbarSpendingPlanRepository', function () { const inactivePlan = await repository.create(subscriptionTier, requestDetails, ttl); // Manually set the plan to inactive - const key = `${repository['collectionKey']}:${inactivePlan.id}`; + const key = `${HbarSpendingPlanRepository.collectionKey}:${inactivePlan.id}`; await cacheService.set(key, { ...inactivePlan, active: false }, 'test', requestDetails); const activePlans = await repository.findAllActiveBySubscriptionTier([subscriptionTier], requestDetails); diff --git a/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts b/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts index 3080c0221a..c17c4cea4d 100644 --- a/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts +++ b/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts @@ -76,7 +76,12 @@ describe('IPAddressHbarSpendingPlanRepository', function () { describe('existsByAddress', () => { it('returns true if address plan exists', async () => { const addressPlan = new IPAddressHbarSpendingPlan({ ipAddress, planId: uuidV4(randomBytes(16)) }); - await cacheService.set(`${repository['collectionKey']}:${ipAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, + addressPlan, + 'test', + requestDetails, + ); await expect(repository.existsByAddress(ipAddress, requestDetails)).to.eventually.be.true; }); @@ -94,7 +99,12 @@ describe('IPAddressHbarSpendingPlanRepository', function () { new IPAddressHbarSpendingPlan({ ipAddress: '666.666.666.666', planId }), ]; for (const plan of ipAddressPlans) { - await cacheService.set(`${repository['collectionKey']}:${plan.ipAddress}`, plan, 'test', requestDetails); + await cacheService.set( + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${plan.ipAddress}`, + plan, + 'test', + requestDetails, + ); } const result = await repository.findAllByPlanId(planId, 'findAllByPlanId', requestDetails); @@ -114,14 +124,24 @@ describe('IPAddressHbarSpendingPlanRepository', function () { const ipAddresses = ['555.555.555.555', '666.666.666.666']; for (const ipAddress of ipAddresses) { const addressPlan = new IPAddressHbarSpendingPlan({ ipAddress, planId }); - await cacheService.set(`${repository['collectionKey']}:${ipAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, + addressPlan, + 'test', + requestDetails, + ); } await repository.deleteAllByPlanId(planId, 'deleteAllByPlanId', requestDetails); for (const ipAddress of ipAddresses) { - await expect(cacheService.getAsync(`${repository['collectionKey']}:${ipAddress}`, 'test', requestDetails)).to - .eventually.be.null; + await expect( + cacheService.getAsync( + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, + 'test', + requestDetails, + ), + ).to.eventually.be.null; } }); @@ -134,7 +154,12 @@ describe('IPAddressHbarSpendingPlanRepository', function () { describe('findByAddress', () => { it('retrieves an address plan by ip', async () => { const addressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: uuidV4(randomBytes(16)) }; - await cacheService.set(`${repository['collectionKey']}:${ipAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, + addressPlan, + 'test', + requestDetails, + ); const result = await repository.findByAddress(ipAddress, requestDetails); expect(result).to.deep.equal(addressPlan); @@ -154,14 +179,14 @@ describe('IPAddressHbarSpendingPlanRepository', function () { await repository.save(addressPlan, requestDetails, ttl); const result = await cacheService.getAsync( - `${repository['collectionKey']}:${ipAddress}`, + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, 'test', requestDetails, ); expect(result).to.deep.equal(addressPlan); sinon.assert.calledWith( cacheServiceSpy.set, - `${repository['collectionKey']}:${ipAddress}`, + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, addressPlan, 'save', requestDetails, @@ -171,20 +196,25 @@ describe('IPAddressHbarSpendingPlanRepository', function () { it('overwrites an existing address plan', async () => { const addressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: uuidV4(randomBytes(16)) }; - await cacheService.set(`${repository['collectionKey']}:${ipAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, + addressPlan, + 'test', + requestDetails, + ); const newPlanId = uuidV4(randomBytes(16)); const newAddressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: newPlanId }; await repository.save(newAddressPlan, requestDetails, ttl); const result = await cacheService.getAsync( - `${repository['collectionKey']}:${ipAddress}`, + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, 'test', requestDetails, ); expect(result).to.deep.equal(newAddressPlan); sinon.assert.calledWith( cacheServiceSpy.set, - `${repository['collectionKey']}:${ipAddress}`, + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, newAddressPlan, 'save', requestDetails, @@ -196,11 +226,16 @@ describe('IPAddressHbarSpendingPlanRepository', function () { describe('delete', () => { it('deletes an address plan successfully', async () => { const addressPlan: IIPAddressHbarSpendingPlan = { ipAddress, planId: uuidV4(randomBytes(16)) }; - await cacheService.set(`${repository['collectionKey']}:${ipAddress}`, addressPlan, 'test', requestDetails); + await cacheService.set( + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, + addressPlan, + 'test', + requestDetails, + ); await repository.delete(ipAddress, requestDetails); const result = await cacheService.getAsync( - `${repository['collectionKey']}:${ipAddress}`, + `${IPAddressHbarSpendingPlanRepository.collectionKey}:${ipAddress}`, 'test', requestDetails, );