Merge branch 'develop' into upstream/2025.5.0
This commit is contained in:
commit
33aee38a59
125 changed files with 3926 additions and 2148 deletions
|
|
@ -19,7 +19,7 @@ import {
|
|||
ResourceOwnerPassword,
|
||||
} from 'simple-oauth2';
|
||||
import pkceChallenge from 'pkce-challenge';
|
||||
import { load as cheerio } from 'cheerio';
|
||||
import { load as cheerio } from 'cheerio/slim';
|
||||
import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify';
|
||||
import { api, port, sendEnvUpdateRequest, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
|
|
|||
92
packages/backend/test/misc/FakeInternalEventService.ts
Normal file
92
packages/backend/test/misc/FakeInternalEventService.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Listener, ListenerProps } from '@/core/InternalEventService.js';
|
||||
import type Redis from 'ioredis';
|
||||
import type { GlobalEventService, InternalEventTypes } from '@/core/GlobalEventService.js';
|
||||
import { InternalEventService } from '@/core/InternalEventService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
type FakeCall<K extends keyof InternalEventService> = [K, Parameters<InternalEventService[K]>];
|
||||
type FakeListener<K extends keyof InternalEventTypes> = [K, Listener<K>, ListenerProps];
|
||||
|
||||
/**
|
||||
* Minimal implementation of InternalEventService meant for use in unit tests.
|
||||
* There is no redis connection, and metadata is tracked in the public _calls and _listeners arrays.
|
||||
* The on/off/emit methods are fully functional and can be called in tests to invoke any registered listeners.
|
||||
*/
|
||||
export class FakeInternalEventService extends InternalEventService {
|
||||
/**
|
||||
* List of calls to public methods, in chronological order.
|
||||
*/
|
||||
public _calls: FakeCall<keyof InternalEventService>[] = [];
|
||||
|
||||
/**
|
||||
* List of currently registered listeners.
|
||||
*/
|
||||
public _listeners: FakeListener<keyof InternalEventTypes>[] = [];
|
||||
|
||||
/**
|
||||
* Resets the mock.
|
||||
* Clears all listeners and tracked calls.
|
||||
*/
|
||||
public _reset() {
|
||||
this._calls = [];
|
||||
this._listeners = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates a remote event sent from another process in the cluster via redis.
|
||||
*/
|
||||
@bindThis
|
||||
public async _emitRedis<K extends keyof InternalEventTypes>(type: K, value: InternalEventTypes[K]): Promise<void> {
|
||||
await this.emit(type, value, false);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
{ on: () => {} } as unknown as Redis.Redis,
|
||||
{} as unknown as GlobalEventService,
|
||||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public on<K extends keyof InternalEventTypes>(type: K, listener: Listener<K>, props?: ListenerProps): void {
|
||||
if (!this._listeners.some(l => l[0] === type && l[1] === listener)) {
|
||||
this._listeners.push([type, listener as Listener<keyof InternalEventTypes>, props ?? {}]);
|
||||
}
|
||||
this._calls.push(['on', [type, listener as Listener<keyof InternalEventTypes>, props]]);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public off<K extends keyof InternalEventTypes>(type: K, listener: Listener<K>): void {
|
||||
this._listeners = this._listeners.filter(l => l[0] !== type || l[1] !== listener);
|
||||
this._calls.push(['off', [type, listener as Listener<keyof InternalEventTypes>]]);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async emit<K extends keyof InternalEventTypes>(type: K, value: InternalEventTypes[K], isLocal = true): Promise<void> {
|
||||
for (const listener of this._listeners) {
|
||||
if (listener[0] === type) {
|
||||
if ((isLocal && !listener[2].ignoreLocal) || (!isLocal && !listener[2].ignoreRemote)) {
|
||||
await listener[1](value, type, isLocal);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._calls.push(['emit', [type, value]]);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this._listeners = [];
|
||||
this._calls.push(['dispose', []]);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(): void {
|
||||
this._calls.push(['onApplicationShutdown', []]);
|
||||
}
|
||||
}
|
||||
|
||||
187
packages/backend/test/misc/noOpCaches.ts
Normal file
187
packages/backend/test/misc/noOpCaches.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Redis from 'ioredis';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { FakeInternalEventService } from './FakeInternalEventService.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, MiUser, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiLocalUser } from '@/models/User.js';
|
||||
import { MemoryKVCache, MemorySingleCache, RedisKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { QuantumKVCache, QuantumKVOpts } from '@/misc/QuantumKVCache.js';
|
||||
import { CacheService, FollowStats } from '@/core/CacheService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { InternalEventService } from '@/core/InternalEventService.js';
|
||||
|
||||
export function noOpRedis() {
|
||||
return {
|
||||
set: () => Promise.resolve(),
|
||||
get: () => Promise.resolve(null),
|
||||
del: () => Promise.resolve(),
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
} as unknown as Redis.Redis;
|
||||
}
|
||||
|
||||
export class NoOpCacheService extends CacheService {
|
||||
public readonly fakeRedis: {
|
||||
[K in keyof Redis.Redis]: Redis.Redis[K];
|
||||
};
|
||||
public readonly fakeInternalEventService: FakeInternalEventService;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(UserEntityService)
|
||||
userEntityService: UserEntityService,
|
||||
) {
|
||||
const fakeRedis = noOpRedis();
|
||||
const fakeInternalEventService = new FakeInternalEventService();
|
||||
|
||||
super(
|
||||
fakeRedis,
|
||||
fakeRedis,
|
||||
usersRepository,
|
||||
userProfilesRepository,
|
||||
mutingsRepository,
|
||||
blockingsRepository,
|
||||
renoteMutingsRepository,
|
||||
followingsRepository,
|
||||
userEntityService,
|
||||
fakeInternalEventService,
|
||||
);
|
||||
|
||||
this.fakeRedis = fakeRedis;
|
||||
this.fakeInternalEventService = fakeInternalEventService;
|
||||
|
||||
// Override caches
|
||||
this.userByIdCache = new NoOpMemoryKVCache<MiUser>();
|
||||
this.localUserByNativeTokenCache = new NoOpMemoryKVCache<MiLocalUser | null>();
|
||||
this.localUserByIdCache = new NoOpMemoryKVCache<MiLocalUser>();
|
||||
this.uriPersonCache = new NoOpMemoryKVCache<MiUser | null>();
|
||||
this.userProfileCache = NoOpQuantumKVCache.copy(this.userProfileCache, fakeInternalEventService);
|
||||
this.userMutingsCache = NoOpQuantumKVCache.copy(this.userMutingsCache, fakeInternalEventService);
|
||||
this.userBlockingCache = NoOpQuantumKVCache.copy(this.userBlockingCache, fakeInternalEventService);
|
||||
this.userBlockedCache = NoOpQuantumKVCache.copy(this.userBlockedCache, fakeInternalEventService);
|
||||
this.renoteMutingsCache = NoOpQuantumKVCache.copy(this.renoteMutingsCache, fakeInternalEventService);
|
||||
this.userFollowingsCache = NoOpQuantumKVCache.copy(this.userFollowingsCache, fakeInternalEventService);
|
||||
this.userFollowersCache = NoOpQuantumKVCache.copy(this.userFollowersCache, fakeInternalEventService);
|
||||
this.hibernatedUserCache = NoOpQuantumKVCache.copy(this.hibernatedUserCache, fakeInternalEventService);
|
||||
this.userFollowStatsCache = new NoOpMemoryKVCache<FollowStats>();
|
||||
this.translationsCache = NoOpRedisKVCache.copy(this.translationsCache, fakeRedis);
|
||||
}
|
||||
}
|
||||
|
||||
export class NoOpMemoryKVCache<T> extends MemoryKVCache<T> {
|
||||
constructor() {
|
||||
super(-1);
|
||||
}
|
||||
}
|
||||
|
||||
export class NoOpMemorySingleCache<T> extends MemorySingleCache<T> {
|
||||
constructor() {
|
||||
super(-1);
|
||||
}
|
||||
}
|
||||
|
||||
export class NoOpRedisKVCache<T> extends RedisKVCache<T> {
|
||||
constructor(opts?: {
|
||||
redis?: Redis.Redis;
|
||||
fetcher?: RedisKVCache<T>['fetcher'];
|
||||
toRedisConverter?: RedisKVCache<T>['toRedisConverter'];
|
||||
fromRedisConverter?: RedisKVCache<T>['fromRedisConverter'];
|
||||
}) {
|
||||
super(
|
||||
opts?.redis ?? noOpRedis(),
|
||||
'no-op',
|
||||
{
|
||||
lifetime: -1,
|
||||
memoryCacheLifetime: -1,
|
||||
fetcher: opts?.fetcher,
|
||||
toRedisConverter: opts?.toRedisConverter,
|
||||
fromRedisConverter: opts?.fromRedisConverter,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public static copy<T>(cache: RedisKVCache<T>, redis?: Redis.Redis): NoOpRedisKVCache<T> {
|
||||
return new NoOpRedisKVCache<T>({
|
||||
redis,
|
||||
fetcher: cache.fetcher,
|
||||
toRedisConverter: cache.toRedisConverter,
|
||||
fromRedisConverter: cache.fromRedisConverter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class NoOpRedisSingleCache<T> extends RedisSingleCache<T> {
|
||||
constructor(opts?: {
|
||||
redis?: Redis.Redis;
|
||||
fetcher?: RedisSingleCache<T>['fetcher'];
|
||||
toRedisConverter?: RedisSingleCache<T>['toRedisConverter'];
|
||||
fromRedisConverter?: RedisSingleCache<T>['fromRedisConverter'];
|
||||
}) {
|
||||
super(
|
||||
opts?.redis ?? noOpRedis(),
|
||||
'no-op',
|
||||
{
|
||||
lifetime: -1,
|
||||
memoryCacheLifetime: -1,
|
||||
fetcher: opts?.fetcher,
|
||||
toRedisConverter: opts?.toRedisConverter,
|
||||
fromRedisConverter: opts?.fromRedisConverter,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public static copy<T>(cache: RedisSingleCache<T>, redis?: Redis.Redis): NoOpRedisSingleCache<T> {
|
||||
return new NoOpRedisSingleCache<T>({
|
||||
redis,
|
||||
fetcher: cache.fetcher,
|
||||
toRedisConverter: cache.toRedisConverter,
|
||||
fromRedisConverter: cache.fromRedisConverter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class NoOpQuantumKVCache<T> extends QuantumKVCache<T> {
|
||||
constructor(opts: Omit<QuantumKVOpts<T>, 'lifetime'> & {
|
||||
internalEventService?: InternalEventService,
|
||||
}) {
|
||||
super(
|
||||
opts.internalEventService ?? new FakeInternalEventService(),
|
||||
'no-op',
|
||||
{
|
||||
...opts,
|
||||
lifetime: -1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public static copy<T>(cache: QuantumKVCache<T>, internalEventService?: InternalEventService): NoOpQuantumKVCache<T> {
|
||||
return new NoOpQuantumKVCache<T>({
|
||||
internalEventService,
|
||||
fetcher: cache.fetcher,
|
||||
bulkFetcher: cache.bulkFetcher,
|
||||
onChanged: cache.onChanged,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({}),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
799
packages/backend/test/unit/misc/QuantumKVCache.ts
Normal file
799
packages/backend/test/unit/misc/QuantumKVCache.ts
Normal 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']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,12 +11,12 @@ import { inspect } from 'node:util';
|
|||
import WebSocket, { ClientOptions } from 'ws';
|
||||
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { load as cheerio } from 'cheerio';
|
||||
import { load as cheerio } from 'cheerio/slim';
|
||||
import { type Response } from 'node-fetch';
|
||||
import Fastify from 'fastify';
|
||||
import { entities } from '../src/postgres.js';
|
||||
import { loadConfig } from '../src/config.js';
|
||||
import type { CheerioAPI } from 'cheerio';
|
||||
import type { CheerioAPI } from 'cheerio/slim';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue