Merge branch 'develop' into upstream/2025.5.0

This commit is contained in:
dakkar 2025-06-13 08:43:17 +01:00
commit 33aee38a59
125 changed files with 3926 additions and 2148 deletions

View file

@ -8,9 +8,12 @@ process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import { NoOpCacheService } from '../misc/noOpCaches.js';
import { FakeInternalEventService } from '../misc/FakeInternalEventService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { InternalEventService } from '@/core/InternalEventService.js';
import type {
AnnouncementReadsRepository,
AnnouncementsRepository,
@ -71,24 +74,27 @@ describe('AnnouncementService', () => {
AnnouncementEntityService,
CacheService,
IdService,
InternalEventService,
GlobalEventService,
ModerationLogService,
],
})
.useMocker((token) => {
if (token === GlobalEventService) {
return {
publishMainStream: jest.fn(),
publishBroadcastStream: jest.fn(),
};
} else if (token === ModerationLogService) {
return {
log: jest.fn(),
};
} else if (typeof token === 'function') {
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.overrideProvider(GlobalEventService).useValue({
publishMainStream: jest.fn(),
publishBroadcastStream: jest.fn(),
} as unknown as GlobalEventService)
.overrideProvider(ModerationLogService).useValue({
log: jest.fn(),
})
.overrideProvider(InternalEventService).useClass(FakeInternalEventService)
.overrideProvider(CacheService).useClass(NoOpCacheService)
.compile();
app.enableShutdownHooks();

View file

@ -4,7 +4,7 @@
*/
import * as assert from 'assert';
import * as mfm from '@transfem-org/sfm-js';
import * as mfm from 'mfm-js';
import { Test } from '@nestjs/testing';
import { CoreModule } from '@/core/CoreModule.js';
@ -86,7 +86,7 @@ describe('MfmService', () => {
test('ruby', async () => {
const input = '$[ruby $[group *some* text] ignore me]';
const output = '<p><ruby><span><span>*some*</span><span> text</span></span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
const output = '<p><ruby><span><span>*some*</span> text</span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>';
assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output);
});
});

View file

@ -10,12 +10,15 @@ import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import { NoOpCacheService } from '../misc/noOpCaches.js';
import { FakeInternalEventService } from '../misc/FakeInternalEventService.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import {
InstancesRepository,
MetasRepository,
MiMeta,
MiRole,
MiRoleAssignment,
@ -34,6 +37,7 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InternalEventService } from '@/core/InternalEventService.js';
const moduleMocker = new ModuleMocker(global);
@ -45,6 +49,7 @@ describe('RoleService', () => {
let rolesRepository: RolesRepository;
let roleAssignmentsRepository: RoleAssignmentsRepository;
let meta: jest.Mocked<MiMeta>;
let metasRepository: MetasRepository;
let notificationService: jest.Mocked<NotificationService>;
let clock: lolex.InstalledClock;
@ -143,18 +148,20 @@ describe('RoleService', () => {
provide: NotificationService.name,
useExisting: NotificationService,
},
MetaService,
InternalEventService,
],
})
.useMocker((token) => {
if (token === MetaService) {
return { fetch: jest.fn() };
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
return new Mock();
}
})
.overrideProvider(MetaService).useValue({ fetch: jest.fn() })
.overrideProvider(InternalEventService).useClass(FakeInternalEventService)
.overrideProvider(CacheService).useClass(NoOpCacheService)
.compile();
app.enableShutdownHooks();
@ -164,6 +171,7 @@ describe('RoleService', () => {
usersRepository = app.get<UsersRepository>(DI.usersRepository);
rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
metasRepository = app.get<MetasRepository>(DI.metasRepository);
meta = app.get<MiMeta>(DI.meta) as jest.Mocked<MiMeta>;
notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
@ -175,7 +183,7 @@ describe('RoleService', () => {
clock.uninstall();
await Promise.all([
app.get(DI.metasRepository).delete({}),
metasRepository.delete({}),
usersRepository.delete({}),
rolesRepository.delete({}),
roleAssignmentsRepository.delete({}),

View file

@ -9,8 +9,12 @@ import { generateKeyPair } from 'crypto';
import { Test } from '@nestjs/testing';
import { jest } from '@jest/globals';
import { NoOpCacheService } from '../misc/noOpCaches.js';
import { FakeInternalEventService } from '../misc/FakeInternalEventService.js';
import type { Config } from '@/config.js';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { InternalEventService } from '@/core/InternalEventService.js';
import { CacheService } from '@/core/CacheService.js';
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@ -30,7 +34,7 @@ import { genAidx } from '@/misc/id/aidx.js';
import { IdService } from '@/core/IdService.js';
import { MockResolver } from '../misc/mock-resolver.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import { MemoryKVCache } from '@/misc/cache.js';
const host = 'https://host1.test';
@ -154,6 +158,8 @@ describe('ActivityPub', () => {
},
})
.overrideProvider(DI.meta).useFactory({ factory: () => meta })
.overrideProvider(CacheService).useClass(NoOpCacheService)
.overrideProvider(InternalEventService).useClass(FakeInternalEventService)
.compile();
await app.init();
@ -473,8 +479,6 @@ describe('ActivityPub', () => {
describe('JSON-LD', () => {
test('Compaction', async () => {
const jsonLd = jsonLdService.use();
const object = {
'@context': [
'https://www.w3.org/ns/activitystreams',
@ -493,7 +497,7 @@ describe('ActivityPub', () => {
unknown: 'test test bar',
undefined: 'test test baz',
};
const compacted = await jsonLd.compact(object);
const compacted = await jsonLdService.compact(object);
assert.deepStrictEqual(compacted, {
'@context': CONTEXT,
@ -556,7 +560,7 @@ describe('ActivityPub', () => {
publicKey,
privateKey,
});
((userKeypairService as unknown as { cache: RedisKVCache<MiUserKeypair> }).cache as unknown as { memoryCache: MemoryKVCache<MiUserKeypair> }).memoryCache.set(author.id, keypair);
(userKeypairService as unknown as { cache: MemoryKVCache<MiUserKeypair> }).cache.set(author.id, keypair);
note = new MiNote({
id: idService.gen(),

View file

@ -4,6 +4,8 @@
*/
import { Test, TestingModule } from '@nestjs/testing';
import { FakeInternalEventService } from '../../misc/FakeInternalEventService.js';
import { NoOpCacheService } from '../../misc/noOpCaches.js';
import type { MiUser } from '@/models/User.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalModule } from '@/GlobalModule.js';
@ -51,6 +53,7 @@ import { ReactionService } from '@/core/ReactionService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { ChatService } from '@/core/ChatService.js';
import { InternalEventService } from '@/core/InternalEventService.js';
process.env.NODE_ENV = 'test';
@ -174,6 +177,7 @@ describe('UserEntityService', () => {
ReactionsBufferingService,
NotificationService,
ChatService,
InternalEventService,
];
app = await Test.createTestingModule({
@ -182,7 +186,10 @@ describe('UserEntityService', () => {
...services,
...services.map(x => ({ provide: x.name, useExisting: x })),
],
}).compile();
})
.overrideProvider(InternalEventService).useClass(FakeInternalEventService)
.overrideProvider(CacheService).useClass(NoOpCacheService)
.compile();
await app.init();
app.enableShutdownHooks();

View file

@ -5,7 +5,7 @@
import * as assert from 'assert';
import { parse } from '@transfem-org/sfm-js';
import { parse } from 'mfm-js';
import { extractMentions } from '@/misc/extract-mentions.js';
describe('Extract mentions', () => {

View file

@ -0,0 +1,799 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { jest } from '@jest/globals';
import { FakeInternalEventService } from '../../misc/FakeInternalEventService.js';
import { QuantumKVCache, QuantumKVOpts } from '@/misc/QuantumKVCache.js';
describe(QuantumKVCache, () => {
let fakeInternalEventService: FakeInternalEventService;
let madeCaches: { dispose: () => void }[];
function makeCache<T>(opts?: Partial<QuantumKVOpts<T>> & { name?: string }): QuantumKVCache<T> {
const _opts = {
name: 'test',
lifetime: Infinity,
fetcher: () => { throw new Error('not implemented'); },
} satisfies QuantumKVOpts<T> & { name: string };
if (opts) {
Object.assign(_opts, opts);
}
const cache = new QuantumKVCache<T>(fakeInternalEventService, _opts.name, _opts);
madeCaches.push(cache);
return cache;
}
beforeEach(() => {
madeCaches = [];
fakeInternalEventService = new FakeInternalEventService();
});
afterEach(() => {
madeCaches.forEach(cache => {
cache.dispose();
});
});
it('should connect on construct', () => {
makeCache();
expect(fakeInternalEventService._calls).toContainEqual(['on', ['quantumCacheUpdated', expect.anything(), { ignoreLocal: true }]]);
});
it('should disconnect on dispose', () => {
const cache = makeCache();
cache.dispose();
const callback = fakeInternalEventService._calls
.find(c => c[0] === 'on' && c[1][0] === 'quantumCacheUpdated')
?.[1][1];
expect(fakeInternalEventService._calls).toContainEqual(['off', ['quantumCacheUpdated', callback]]);
});
it('should store in memory cache', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
await cache.set('alpha', 'omega');
const result1 = await cache.get('foo');
const result2 = await cache.get('alpha');
expect(result1).toBe('bar');
expect(result2).toBe('omega');
});
it('should emit event when storing', async () => {
const cache = makeCache<string>({ name: 'fake' });
await cache.set('foo', 'bar');
expect(fakeInternalEventService._calls).toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['foo'] }]]);
});
it('should call onChanged when storing', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await cache.set('foo', 'bar');
expect(fakeOnChanged).toHaveBeenCalledWith(['foo'], cache);
});
it('should not emit event when storing unchanged value', async () => {
const cache = makeCache<string>({ name: 'fake' });
await cache.set('foo', 'bar');
await cache.set('foo', 'bar');
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(1);
});
it('should not call onChanged when storing unchanged value', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await cache.set('foo', 'bar');
await cache.set('foo', 'bar');
expect(fakeOnChanged).toHaveBeenCalledTimes(1);
});
it('should fetch an unknown value', async () => {
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
});
const result = await cache.fetch('foo');
expect(result).toBe('value#foo');
});
it('should store fetched value in memory cache', async () => {
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
});
await cache.fetch('foo');
const result = cache.has('foo');
expect(result).toBe(true);
});
it('should call onChanged when fetching', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
onChanged: fakeOnChanged,
});
await cache.fetch('foo');
expect(fakeOnChanged).toHaveBeenCalledWith(['foo'], cache);
});
it('should not emit event when fetching', async () => {
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
});
await cache.fetch('foo');
expect(fakeInternalEventService._calls).not.toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['foo'] }]]);
});
it('should delete from memory cache', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
await cache.delete('foo');
const result = cache.has('foo');
expect(result).toBe(false);
});
it('should call onChanged when deleting', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await cache.set('foo', 'bar');
await cache.delete('foo');
expect(fakeOnChanged).toHaveBeenCalledWith(['foo'], cache);
});
it('should emit event when deleting', async () => {
const cache = makeCache<string>({ name: 'fake' });
await cache.set('foo', 'bar');
await cache.delete('foo');
expect(fakeInternalEventService._calls).toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['foo'] }]]);
});
it('should delete when receiving set event', async () => {
const cache = makeCache<string>({ name: 'fake' });
await cache.set('foo', 'bar');
await fakeInternalEventService._emitRedis('quantumCacheUpdated', { name: 'fake', keys: ['foo'] });
const result = cache.has('foo');
expect(result).toBe(false);
});
it('should call onChanged when receiving set event', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await fakeInternalEventService._emitRedis('quantumCacheUpdated', { name: 'fake', keys: ['foo'] });
expect(fakeOnChanged).toHaveBeenCalledWith(['foo'], cache);
});
it('should delete when receiving delete event', async () => {
const cache = makeCache<string>({ name: 'fake' });
await cache.set('foo', 'bar');
await fakeInternalEventService._emitRedis('quantumCacheUpdated', { name: 'fake', keys: ['foo'] });
const result = cache.has('foo');
expect(result).toBe(false);
});
it('should call onChanged when receiving delete event', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await cache.set('foo', 'bar');
await fakeInternalEventService._emitRedis('quantumCacheUpdated', { name: 'fake', keys: ['foo'] });
expect(fakeOnChanged).toHaveBeenCalledWith(['foo'], cache);
});
describe('get', () => {
it('should return value if present', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
const result = cache.get('foo');
expect(result).toBe('bar');
});
it('should return undefined if missing', () => {
const cache = makeCache<string>();
const result = cache.get('foo');
expect(result).toBe(undefined);
});
});
describe('setMany', () => {
it('should populate all values', async () => {
const cache = makeCache<string>();
await cache.setMany([['foo', 'bar'], ['alpha', 'omega']]);
expect(cache.has('foo')).toBe(true);
expect(cache.has('alpha')).toBe(true);
});
it('should emit one event', async () => {
const cache = makeCache<string>({
name: 'fake',
});
await cache.setMany([['foo', 'bar'], ['alpha', 'omega']]);
expect(fakeInternalEventService._calls).toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['foo', 'alpha'] }]]);
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(1);
});
it('should call onChanged once with all items', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await cache.setMany([['foo', 'bar'], ['alpha', 'omega']]);
expect(fakeOnChanged).toHaveBeenCalledWith(['foo', 'alpha'], cache);
expect(fakeOnChanged).toHaveBeenCalledTimes(1);
});
it('should emit events only for changed items', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await cache.set('foo', 'bar');
fakeOnChanged.mockClear();
fakeInternalEventService._reset();
await cache.setMany([['foo', 'bar'], ['alpha', 'omega']]);
expect(fakeInternalEventService._calls).toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['alpha'] }]]);
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(1);
expect(fakeOnChanged).toHaveBeenCalledWith(['alpha'], cache);
expect(fakeOnChanged).toHaveBeenCalledTimes(1);
});
});
describe('getMany', () => {
it('should return empty for empty input', () => {
const cache = makeCache();
const result = cache.getMany([]);
expect(result).toEqual([]);
});
it('should return the value for all keys', () => {
const cache = makeCache();
cache.add('foo', 'bar');
cache.add('alpha', 'omega');
const result = cache.getMany(['foo', 'alpha']);
expect(result).toEqual([['foo', 'bar'], ['alpha', 'omega']]);
});
it('should return undefined for missing keys', () => {
const cache = makeCache();
cache.add('foo', 'bar');
const result = cache.getMany(['foo', 'alpha']);
expect(result).toEqual([['foo', 'bar'], ['alpha', undefined]]);
});
});
describe('fetchMany', () => {
it('should do nothing for empty input', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache({
onChanged: fakeOnChanged,
});
await cache.fetchMany([]);
expect(fakeOnChanged).not.toHaveBeenCalled();
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(0);
});
it('should return existing items', async () => {
const cache = makeCache();
cache.add('foo', 'bar');
cache.add('alpha', 'omega');
const result = await cache.fetchMany(['foo', 'alpha']);
expect(result).toEqual([['foo', 'bar'], ['alpha', 'omega']]);
});
it('should return existing items without events', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache({
onChanged: fakeOnChanged,
});
cache.add('foo', 'bar');
cache.add('alpha', 'omega');
await cache.fetchMany(['foo', 'alpha']);
expect(fakeOnChanged).not.toHaveBeenCalled();
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(0);
});
it('should call bulkFetcher for missing items', async () => {
const cache = makeCache({
bulkFetcher: keys => keys.map(k => [k, `${k}#many`]),
fetcher: key => `${key}#single`,
});
const results = await cache.fetchMany(['foo', 'alpha']);
expect(results).toEqual([['foo', 'foo#many'], ['alpha', 'alpha#many']]);
});
it('should call bulkFetcher only once', async () => {
const mockBulkFetcher = jest.fn((keys: string[]) => keys.map(k => [k, `${k}#value`] as [string, string]));
const cache = makeCache({
bulkFetcher: mockBulkFetcher,
});
await cache.fetchMany(['foo', 'bar']);
expect(mockBulkFetcher).toHaveBeenCalledTimes(1);
});
it('should call fetcher when fetchMany is undefined', async () => {
const cache = makeCache({
fetcher: key => `${key}#single`,
});
const results = await cache.fetchMany(['foo', 'alpha']);
expect(results).toEqual([['foo', 'foo#single'], ['alpha', 'alpha#single']]);
});
it('should call onChanged', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache({
onChanged: fakeOnChanged,
fetcher: k => k,
});
await cache.fetchMany(['foo', 'alpha']);
expect(fakeOnChanged).toHaveBeenCalledWith(['foo', 'alpha'], cache);
expect(fakeOnChanged).toHaveBeenCalledTimes(1);
});
it('should call onChanged only for changed', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache({
onChanged: fakeOnChanged,
fetcher: k => k,
});
cache.add('foo', 'bar');
await cache.fetchMany(['foo', 'alpha']);
expect(fakeOnChanged).toHaveBeenCalledWith(['alpha'], cache);
expect(fakeOnChanged).toHaveBeenCalledTimes(1);
});
it('should not emit event', async () => {
const cache = makeCache({
fetcher: k => k,
});
await cache.fetchMany(['foo', 'alpha']);
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(0);
});
});
describe('refreshMany', () => {
it('should do nothing for empty input', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache({
onChanged: fakeOnChanged,
});
const result = await cache.refreshMany([]);
expect(result).toEqual([]);
expect(fakeOnChanged).not.toHaveBeenCalled();
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(0);
});
it('should call bulkFetcher for all keys', async () => {
const mockBulkFetcher = jest.fn((keys: string[]) => keys.map(k => [k, `${k}#value`] as [string, string]));
const cache = makeCache({
bulkFetcher: mockBulkFetcher,
});
const result = await cache.refreshMany(['foo', 'alpha']);
expect(result).toEqual([['foo', 'foo#value'], ['alpha', 'alpha#value']]);
expect(mockBulkFetcher).toHaveBeenCalledWith(['foo', 'alpha'], cache);
expect(mockBulkFetcher).toHaveBeenCalledTimes(1);
});
it('should replace any existing keys', async () => {
const mockBulkFetcher = jest.fn((keys: string[]) => keys.map(k => [k, `${k}#value`] as [string, string]));
const cache = makeCache({
bulkFetcher: mockBulkFetcher,
});
cache.add('foo', 'bar');
const result = await cache.refreshMany(['foo', 'alpha']);
expect(result).toEqual([['foo', 'foo#value'], ['alpha', 'alpha#value']]);
expect(mockBulkFetcher).toHaveBeenCalledWith(['foo', 'alpha'], cache);
expect(mockBulkFetcher).toHaveBeenCalledTimes(1);
});
it('should call onChanged for all keys', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache({
bulkFetcher: keys => keys.map(k => [k, `${k}#value`]),
onChanged: fakeOnChanged,
});
cache.add('foo', 'bar');
await cache.refreshMany(['foo', 'alpha']);
expect(fakeOnChanged).toHaveBeenCalledWith(['foo', 'alpha'], cache);
expect(fakeOnChanged).toHaveBeenCalledTimes(1);
});
it('should emit event for all keys', async () => {
const cache = makeCache({
name: 'fake',
bulkFetcher: keys => keys.map(k => [k, `${k}#value`]),
});
cache.add('foo', 'bar');
await cache.refreshMany(['foo', 'alpha']);
expect(fakeInternalEventService._calls).toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['foo', 'alpha'] }]]);
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(1);
});
});
describe('deleteMany', () => {
it('should remove keys from memory cache', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
await cache.set('alpha', 'omega');
await cache.deleteMany(['foo', 'alpha']);
expect(cache.has('foo')).toBe(false);
expect(cache.has('alpha')).toBe(false);
});
it('should emit only one event', async () => {
const cache = makeCache<string>({
name: 'fake',
});
await cache.deleteMany(['foo', 'alpha']);
expect(fakeInternalEventService._calls).toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['foo', 'alpha'] }]]);
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(1);
});
it('should call onChanged once with all items', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await cache.deleteMany(['foo', 'alpha']);
expect(fakeOnChanged).toHaveBeenCalledWith(['foo', 'alpha'], cache);
expect(fakeOnChanged).toHaveBeenCalledTimes(1);
});
it('should do nothing if no keys are provided', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
onChanged: fakeOnChanged,
});
await cache.deleteMany([]);
expect(fakeOnChanged).not.toHaveBeenCalled();
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(0);
});
});
describe('refresh', () => {
it('should populate the value', async () => {
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
});
await cache.refresh('foo');
const result = cache.has('foo');
expect(result).toBe(true);
});
it('should return the value', async () => {
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
});
const result = await cache.refresh('foo');
expect(result).toBe('value#foo');
});
it('should replace the value if it exists', async () => {
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
});
await cache.set('foo', 'bar');
const result = await cache.refresh('foo');
expect(result).toBe('value#foo');
});
it('should call onChanged', async () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
onChanged: fakeOnChanged,
});
await cache.refresh('foo');
expect(fakeOnChanged).toHaveBeenCalledWith(['foo'], cache);
});
it('should emit event', async () => {
const cache = makeCache<string>({
name: 'fake',
fetcher: key => `value#${key}`,
});
await cache.refresh('foo');
expect(fakeInternalEventService._calls).toContainEqual(['emit', ['quantumCacheUpdated', { name: 'fake', keys: ['foo'] }]]);
});
});
describe('add', () => {
it('should add the item', () => {
const cache = makeCache();
cache.add('foo', 'bar');
expect(cache.has('foo')).toBe(true);
});
it('should not emit event', () => {
const cache = makeCache({
name: 'fake',
});
cache.add('foo', 'bar');
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(0);
});
it('should not call onChanged', () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache({
onChanged: fakeOnChanged,
});
cache.add('foo', 'bar');
expect(fakeOnChanged).not.toHaveBeenCalled();
});
});
describe('addMany', () => {
it('should add all items', () => {
const cache = makeCache();
cache.addMany([['foo', 'bar'], ['alpha', 'omega']]);
expect(cache.has('foo')).toBe(true);
expect(cache.has('alpha')).toBe(true);
});
it('should not emit event', () => {
const cache = makeCache({
name: 'fake',
});
cache.addMany([['foo', 'bar'], ['alpha', 'omega']]);
expect(fakeInternalEventService._calls.filter(c => c[0] === 'emit')).toHaveLength(0);
});
it('should not call onChanged', () => {
const fakeOnChanged = jest.fn(() => Promise.resolve());
const cache = makeCache({
onChanged: fakeOnChanged,
});
cache.addMany([['foo', 'bar'], ['alpha', 'omega']]);
expect(fakeOnChanged).not.toHaveBeenCalled();
});
});
describe('has', () => {
it('should return false when empty', () => {
const cache = makeCache();
const result = cache.has('foo');
expect(result).toBe(false);
});
it('should return false when value is not in memory', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
const result = cache.has('alpha');
expect(result).toBe(false);
});
it('should return true when value is in memory', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
const result = cache.has('foo');
expect(result).toBe(true);
});
});
describe('size', () => {
it('should return 0 when empty', () => {
const cache = makeCache();
expect(cache.size).toBe(0);
});
it('should return correct size when populated', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
expect(cache.size).toBe(1);
});
});
describe('entries', () => {
it('should return empty when empty', () => {
const cache = makeCache();
const result = Array.from(cache.entries());
expect(result).toHaveLength(0);
});
it('should return all entries when populated', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
const result = Array.from(cache.entries());
expect(result).toEqual([['foo', 'bar']]);
});
});
describe('keys', () => {
it('should return empty when empty', () => {
const cache = makeCache();
const result = Array.from(cache.keys());
expect(result).toHaveLength(0);
});
it('should return all keys when populated', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
const result = Array.from(cache.keys());
expect(result).toEqual(['foo']);
});
});
describe('values', () => {
it('should return empty when empty', () => {
const cache = makeCache();
const result = Array.from(cache.values());
expect(result).toHaveLength(0);
});
it('should return all values when populated', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
const result = Array.from(cache.values());
expect(result).toEqual(['bar']);
});
});
describe('[Symbol.iterator]', () => {
it('should return empty when empty', () => {
const cache = makeCache();
const result = Array.from(cache);
expect(result).toHaveLength(0);
});
it('should return all entries when populated', async () => {
const cache = makeCache<string>();
await cache.set('foo', 'bar');
const result = Array.from(cache);
expect(result).toEqual([['foo', 'bar']]);
});
});
});