Merge branch 'develop' into feature/channel_muting
# Conflicts: # CHANGELOG.md # packages/backend/src/core/CoreModule.ts # packages/backend/src/server/api/endpoints/channels/timeline.ts # packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts # packages/backend/src/server/api/endpoints/notes/local-timeline.ts # packages/backend/src/server/api/endpoints/notes/timeline.ts # packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts # packages/backend/src/server/api/endpoints/users/notes.ts # packages/backend/src/server/api/stream/Connection.ts # packages/backend/src/server/api/stream/channel.ts # packages/backend/src/server/api/stream/channels/channel.ts # packages/backend/src/server/api/stream/channels/home-timeline.ts # packages/backend/src/server/api/stream/channels/hybrid-timeline.ts # packages/backend/test/e2e/timelines.ts
This commit is contained in:
commit
5595e8fbd1
847 changed files with 35217 additions and 13941 deletions
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { jest } from '@jest/globals';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { randomString } from '../utils.js';
|
||||
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
|
||||
import {
|
||||
AbuseReportNotificationRecipientRepository,
|
||||
|
|
@ -25,7 +26,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { randomString } from '../utils.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
|
|
@ -110,6 +111,9 @@ describe('AbuseReportNotificationService', () => {
|
|||
{
|
||||
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
|
||||
},
|
||||
{
|
||||
provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
|
||||
},
|
||||
{
|
||||
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ describe('ApMfmService', () => {
|
|||
|
||||
describe('getNoteHtml', () => {
|
||||
test('Do not provide _misskey_content for simple text', () => {
|
||||
const note: MiNote = {
|
||||
const note = {
|
||||
text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com',
|
||||
mentionedRemoteUsers: '[]',
|
||||
} as any;
|
||||
};
|
||||
|
||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||
|
||||
|
|
@ -35,10 +35,10 @@ describe('ApMfmService', () => {
|
|||
});
|
||||
|
||||
test('Provide _misskey_content for MFM', () => {
|
||||
const note: MiNote = {
|
||||
const note = {
|
||||
text: '$[tada foo]',
|
||||
mentionedRemoteUsers: '[]',
|
||||
} as any;
|
||||
};
|
||||
|
||||
const { content, noMisskeyContent } = apMfmService.getNoteHtml(note);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { ModuleMocker } from 'jest-mock';
|
|||
import { Test } from '@nestjs/testing';
|
||||
import { afterAll, beforeAll, describe, test } from '@jest/globals';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { FileInfoService } from '@/core/FileInfoService.js';
|
||||
import { FileInfo, FileInfoService } from '@/core/FileInfoService.js';
|
||||
//import { DI } from '@/di-symbols.js';
|
||||
import { AiService } from '@/core/AiService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
|
|
@ -28,6 +28,15 @@ const moduleMocker = new ModuleMocker(global);
|
|||
describe('FileInfoService', () => {
|
||||
let app: TestingModule;
|
||||
let fileInfoService: FileInfoService;
|
||||
const strip = (fileInfo: FileInfo): Omit<Partial<FileInfo>, 'warnings' | 'blurhash' | 'sensitive' | 'porn'> => {
|
||||
const fi: Partial<FileInfo> = fileInfo;
|
||||
delete fi.warnings;
|
||||
delete fi.sensitive;
|
||||
delete fi.blurhash;
|
||||
delete fi.porn;
|
||||
|
||||
return fi;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
|
|
@ -63,11 +72,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Empty file', async () => {
|
||||
const path = `${resources}/emptyfile`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 0,
|
||||
md5: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
|
|
@ -84,11 +89,7 @@ describe('FileInfoService', () => {
|
|||
describe('IMAGE', () => {
|
||||
test('Generic JPEG', async () => {
|
||||
const path = `${resources}/192.jpg`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 5131,
|
||||
md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
|
||||
|
|
@ -104,11 +105,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Generic APNG', async () => {
|
||||
const path = `${resources}/anime.png`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 1868,
|
||||
md5: '08189c607bea3b952704676bb3c979e0',
|
||||
|
|
@ -124,11 +121,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Generic AGIF', async () => {
|
||||
const path = `${resources}/anime.gif`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 2248,
|
||||
md5: '32c47a11555675d9267aee1a86571e7e',
|
||||
|
|
@ -144,11 +137,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('PNG with alpha', async () => {
|
||||
const path = `${resources}/with-alpha.png`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 3772,
|
||||
md5: 'f73535c3e1e27508885b69b10cf6e991',
|
||||
|
|
@ -164,11 +153,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Generic SVG', async () => {
|
||||
const path = `${resources}/image.svg`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 505,
|
||||
md5: 'b6f52b4b021e7b92cdd04509c7267965',
|
||||
|
|
@ -185,11 +170,7 @@ describe('FileInfoService', () => {
|
|||
test('SVG with XML definition', async () => {
|
||||
// https://github.com/misskey-dev/misskey/issues/4413
|
||||
const path = `${resources}/with-xml-def.svg`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 544,
|
||||
md5: '4b7a346cde9ccbeb267e812567e33397',
|
||||
|
|
@ -205,11 +186,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Dimension limit', async () => {
|
||||
const path = `${resources}/25000x25000.png`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 75933,
|
||||
md5: '268c5dde99e17cf8fe09f1ab3f97df56',
|
||||
|
|
@ -225,11 +202,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('Rotate JPEG', async () => {
|
||||
const path = `${resources}/rotate.jpg`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 12624,
|
||||
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
|
||||
|
|
@ -247,11 +220,7 @@ describe('FileInfoService', () => {
|
|||
describe('AUDIO', () => {
|
||||
test('MP3', async () => {
|
||||
const path = `${resources}/kick_gaba7.mp3`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
|
@ -267,11 +236,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('WAV', async () => {
|
||||
const path = `${resources}/kick_gaba7.wav`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
|
@ -287,11 +252,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('AAC', async () => {
|
||||
const path = `${resources}/kick_gaba7.aac`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
|
@ -307,11 +268,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('FLAC', async () => {
|
||||
const path = `${resources}/kick_gaba7.flac`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
|
@ -327,11 +284,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('MPEG-4 AUDIO (M4A)', async () => {
|
||||
const path = `${resources}/kick_gaba7.m4a`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
|
@ -347,11 +300,7 @@ describe('FileInfoService', () => {
|
|||
|
||||
test('WEBM AUDIO', async () => {
|
||||
const path = `${resources}/kick_gaba7.webm`;
|
||||
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
const info = strip(await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }));
|
||||
delete info.width;
|
||||
delete info.height;
|
||||
delete info.orientation;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import * as lolex from '@sinonjs/fake-timers';
|
|||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import {
|
||||
MiMeta,
|
||||
MiRole,
|
||||
MiRoleAssignment,
|
||||
MiUser,
|
||||
|
|
@ -41,7 +42,7 @@ describe('RoleService', () => {
|
|||
let usersRepository: UsersRepository;
|
||||
let rolesRepository: RolesRepository;
|
||||
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
||||
let metaService: jest.Mocked<MetaService>;
|
||||
let meta: jest.Mocked<MiMeta>;
|
||||
let notificationService: jest.Mocked<NotificationService>;
|
||||
let clock: lolex.InstalledClock;
|
||||
|
||||
|
|
@ -142,7 +143,7 @@ describe('RoleService', () => {
|
|||
rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
|
||||
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
|
||||
|
||||
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
|
||||
meta = app.get<MiMeta>(DI.meta) as jest.Mocked<MiMeta>;
|
||||
notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
|
||||
|
||||
await roleService.onModuleInit();
|
||||
|
|
@ -164,11 +165,9 @@ describe('RoleService', () => {
|
|||
describe('getUserPolicies', () => {
|
||||
test('instance default policies', async () => {
|
||||
const user = await createUser();
|
||||
metaService.fetch.mockResolvedValue({
|
||||
policies: {
|
||||
canManageCustomEmojis: false,
|
||||
},
|
||||
} as any);
|
||||
meta.policies = {
|
||||
canManageCustomEmojis: false,
|
||||
};
|
||||
|
||||
const result = await roleService.getUserPolicies(user.id);
|
||||
|
||||
|
|
@ -177,11 +176,9 @@ describe('RoleService', () => {
|
|||
|
||||
test('instance default policies 2', async () => {
|
||||
const user = await createUser();
|
||||
metaService.fetch.mockResolvedValue({
|
||||
policies: {
|
||||
canManageCustomEmojis: true,
|
||||
},
|
||||
} as any);
|
||||
meta.policies = {
|
||||
canManageCustomEmojis: true,
|
||||
};
|
||||
|
||||
const result = await roleService.getUserPolicies(user.id);
|
||||
|
||||
|
|
@ -201,11 +198,9 @@ describe('RoleService', () => {
|
|||
},
|
||||
});
|
||||
await roleService.assign(user.id, role.id);
|
||||
metaService.fetch.mockResolvedValue({
|
||||
policies: {
|
||||
canManageCustomEmojis: false,
|
||||
},
|
||||
} as any);
|
||||
meta.policies = {
|
||||
canManageCustomEmojis: false,
|
||||
};
|
||||
|
||||
const result = await roleService.getUserPolicies(user.id);
|
||||
|
||||
|
|
@ -236,11 +231,9 @@ describe('RoleService', () => {
|
|||
});
|
||||
await roleService.assign(user.id, role1.id);
|
||||
await roleService.assign(user.id, role2.id);
|
||||
metaService.fetch.mockResolvedValue({
|
||||
policies: {
|
||||
driveCapacityMb: 50,
|
||||
},
|
||||
} as any);
|
||||
meta.policies = {
|
||||
driveCapacityMb: 50,
|
||||
};
|
||||
|
||||
const result = await roleService.getUserPolicies(user.id);
|
||||
|
||||
|
|
@ -260,11 +253,9 @@ describe('RoleService', () => {
|
|||
},
|
||||
});
|
||||
await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24)));
|
||||
metaService.fetch.mockResolvedValue({
|
||||
policies: {
|
||||
canManageCustomEmojis: false,
|
||||
},
|
||||
} as any);
|
||||
meta.policies = {
|
||||
canManageCustomEmojis: false,
|
||||
};
|
||||
|
||||
const result = await roleService.getUserPolicies(user.id);
|
||||
expect(result.canManageCustomEmojis).toBe(true);
|
||||
|
|
|
|||
182
packages/backend/test/unit/SigninWithPasskeyApiService.ts
Normal file
182
packages/backend/test/unit/SigninWithPasskeyApiService.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import { HttpHeader } from 'fastify/types/utils.js';
|
||||
import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiService.js';
|
||||
import { RateLimiterService } from '@/server/api/RateLimiterService.js';
|
||||
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { SigninService } from '@/server/api/SigninService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
const moduleMocker = new ModuleMocker(global);
|
||||
|
||||
class FakeLimiter {
|
||||
public async limit() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeSigninService {
|
||||
public signin(..._args: any): any {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class DummyFastifyReply {
|
||||
public statusCode: number;
|
||||
code(num: number): void {
|
||||
this.statusCode = num;
|
||||
}
|
||||
header(_key: HttpHeader, _value: any): void {
|
||||
}
|
||||
}
|
||||
class DummyFastifyRequest {
|
||||
public ip: string;
|
||||
public body: {credential: any, context: string};
|
||||
public headers: IncomingHttpHeaders = { 'accept': 'application/json' };
|
||||
constructor(body?: any) {
|
||||
this.ip = '0.0.0.0';
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
type ApiFastifyRequestType = FastifyRequest<{
|
||||
Body: {
|
||||
credential?: AuthenticationResponseJSON;
|
||||
context?: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
describe('SigninWithPasskeyApiService', () => {
|
||||
let app: TestingModule;
|
||||
let passkeyApiService: SigninWithPasskeyApiService;
|
||||
let usersRepository: UsersRepository;
|
||||
let userProfilesRepository: UserProfilesRepository;
|
||||
let webAuthnService: WebAuthnService;
|
||||
let idService: IdService;
|
||||
let FakeWebauthnVerify: ()=>Promise<string>;
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}) {
|
||||
const user = await usersRepository
|
||||
.save({
|
||||
...data,
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
async function createUserProfile(data: Partial<MiUserProfile> = {}) {
|
||||
const userProfile = await userProfilesRepository
|
||||
.save({ ...data },
|
||||
);
|
||||
return userProfile;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule],
|
||||
providers: [
|
||||
SigninWithPasskeyApiService,
|
||||
{ provide: RateLimiterService, useClass: FakeLimiter },
|
||||
{ provide: SigninService, useClass: FakeSigninService },
|
||||
],
|
||||
}).useMocker((token) => {
|
||||
if (typeof token === 'function') {
|
||||
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
||||
return new Mock();
|
||||
}
|
||||
}).compile();
|
||||
passkeyApiService = app.get<SigninWithPasskeyApiService>(SigninWithPasskeyApiService);
|
||||
usersRepository = app.get<UsersRepository>(DI.usersRepository);
|
||||
userProfilesRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
|
||||
webAuthnService = app.get<WebAuthnService>(WebAuthnService);
|
||||
idService = app.get<IdService>(IdService);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const uid = idService.gen();
|
||||
FakeWebauthnVerify = async () => {
|
||||
return uid;
|
||||
};
|
||||
jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify);
|
||||
|
||||
const dummyUser = {
|
||||
id: uid, username: uid, usernameLower: uid.toLocaleLowerCase(), uri: null, host: null,
|
||||
};
|
||||
const dummyProfile = {
|
||||
userId: uid,
|
||||
password: 'qwerty',
|
||||
usePasswordLessLogin: true,
|
||||
};
|
||||
await createUser(dummyUser);
|
||||
await createUserProfile(dummyProfile);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('Get Passkey Options', () => {
|
||||
it('Should return passkey Auth Options', async () => {
|
||||
const req = new DummyFastifyRequest({}) as ApiFastifyRequestType;
|
||||
const res = new DummyFastifyReply() as unknown as FastifyReply;
|
||||
const res_body = await passkeyApiService.signin(req, res);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect((res_body as any).option).toBeDefined();
|
||||
expect(typeof (res_body as any).context).toBe('string');
|
||||
});
|
||||
});
|
||||
describe('Try Passkey Auth', () => {
|
||||
it('Should Success', async () => {
|
||||
const req = new DummyFastifyRequest({ context: 'auth-context', credential: { dummy: [] } }) as ApiFastifyRequestType;
|
||||
const res = new DummyFastifyReply() as FastifyReply;
|
||||
const res_body = await passkeyApiService.signin(req, res);
|
||||
expect((res_body as any).signinResponse).toBeDefined();
|
||||
});
|
||||
|
||||
it('Should return 400 Without Auth Context', async () => {
|
||||
const req = new DummyFastifyRequest({ credential: { dummy: [] } }) as ApiFastifyRequestType;
|
||||
const res = new DummyFastifyReply() as FastifyReply;
|
||||
const res_body = await passkeyApiService.signin(req, res);
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect((res_body as any).error?.id).toStrictEqual('1658cc2e-4495-461f-aee4-d403cdf073c1');
|
||||
});
|
||||
|
||||
it('Should return 403 When Challenge Verify fail', async () => {
|
||||
const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
|
||||
const res = new DummyFastifyReply() as FastifyReply;
|
||||
jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication')
|
||||
.mockImplementation(async () => {
|
||||
throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
|
||||
});
|
||||
const res_body = await passkeyApiService.signin(req, res);
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect((res_body as any).error?.id).toStrictEqual('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
|
||||
});
|
||||
|
||||
it('Should return 403 When The user not Enabled Passwordless login', async () => {
|
||||
const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
|
||||
const res = new DummyFastifyReply() as FastifyReply;
|
||||
const userId = await FakeWebauthnVerify();
|
||||
const data = { userId: userId, usePasswordLessLogin: false };
|
||||
await userProfilesRepository.update({ userId: userId }, data);
|
||||
const res_body = await passkeyApiService.signin(req, res);
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect((res_body as any).error?.id).toStrictEqual('2d84773e-f7b7-4d0b-8f72-bb69b584c912');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
|
@ -6,6 +7,7 @@
|
|||
import { setTimeout } from 'node:timers/promises';
|
||||
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { randomString } from '../utils.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
|
||||
|
|
@ -17,7 +19,6 @@ import { DI } from '@/di-symbols.js';
|
|||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { randomString } from '../utils.js';
|
||||
|
||||
describe('SystemWebhookService', () => {
|
||||
let app: TestingModule;
|
||||
|
|
@ -313,7 +314,7 @@ describe('SystemWebhookService', () => {
|
|||
isActive: true,
|
||||
on: ['abuseReport'],
|
||||
});
|
||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
|
||||
|
||||
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -323,7 +324,7 @@ describe('SystemWebhookService', () => {
|
|||
isActive: false,
|
||||
on: ['abuseReport'],
|
||||
});
|
||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
|
||||
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
|
||||
|
||||
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -337,8 +338,8 @@ describe('SystemWebhookService', () => {
|
|||
isActive: true,
|
||||
on: ['abuseReportResolved'],
|
||||
});
|
||||
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' });
|
||||
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
|
||||
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
|
||||
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
|
||||
|
||||
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
|||
265
packages/backend/test/unit/UserSearchService.ts
Normal file
265
packages/backend/test/unit/UserSearchService.ts
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { describe, jest, test } from '@jest/globals';
|
||||
import { In } from 'typeorm';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
describe('UserSearchService', () => {
|
||||
let app: TestingModule;
|
||||
let service: UserSearchService;
|
||||
|
||||
let usersRepository: UsersRepository;
|
||||
let followingsRepository: FollowingsRepository;
|
||||
let idService: IdService;
|
||||
let userProfilesRepository: UserProfilesRepository;
|
||||
|
||||
let root: MiUser;
|
||||
let alice: MiUser;
|
||||
let alyce: MiUser;
|
||||
let alycia: MiUser;
|
||||
let alysha: MiUser;
|
||||
let alyson: MiUser;
|
||||
let alyssa: MiUser;
|
||||
let bob: MiUser;
|
||||
let bobbi: MiUser;
|
||||
let bobbie: MiUser;
|
||||
let bobby: MiUser;
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}) {
|
||||
const user = await usersRepository
|
||||
.insert({
|
||||
id: idService.gen(),
|
||||
...data,
|
||||
})
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
await userProfilesRepository.insert({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function createFollowings(follower: MiUser, followees: MiUser[]) {
|
||||
for (const followee of followees) {
|
||||
await followingsRepository.insert({
|
||||
id: idService.gen(),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setActive(users: MiUser[]) {
|
||||
for (const user of users) {
|
||||
await usersRepository.update(user.id, {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setInactive(users: MiUser[]) {
|
||||
for (const user of users) {
|
||||
await usersRepository.update(user.id, {
|
||||
updatedAt: new Date(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setSuspended(users: MiUser[]) {
|
||||
for (const user of users) {
|
||||
await usersRepository.update(user.id, {
|
||||
isSuspended: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test
|
||||
.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
],
|
||||
providers: [
|
||||
UserSearchService,
|
||||
{
|
||||
provide: UserEntityService, useFactory: jest.fn(() => ({
|
||||
// とりあえずIDが返れば確認が出来るので
|
||||
packMany: (value: any) => value,
|
||||
})),
|
||||
},
|
||||
IdService,
|
||||
],
|
||||
})
|
||||
.compile();
|
||||
|
||||
await app.init();
|
||||
|
||||
usersRepository = app.get(DI.usersRepository);
|
||||
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||
followingsRepository = app.get(DI.followingsRepository);
|
||||
|
||||
service = app.get(UserSearchService);
|
||||
idService = app.get(IdService);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||
alice = await createUser({ username: 'Alice', usernameLower: 'alice' });
|
||||
alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' });
|
||||
alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' });
|
||||
alysha = await createUser({ username: 'Alysha', usernameLower: 'alysha' });
|
||||
alyson = await createUser({ username: 'Alyson', usernameLower: 'alyson', host: 'example.com' });
|
||||
alyssa = await createUser({ username: 'Alyssa', usernameLower: 'alyssa', host: 'example.com' });
|
||||
bob = await createUser({ username: 'Bob', usernameLower: 'bob' });
|
||||
bobbi = await createUser({ username: 'Bobbi', usernameLower: 'bobbi' });
|
||||
bobbie = await createUser({ username: 'Bobbie', usernameLower: 'bobbie', host: 'example.com' });
|
||||
bobby = await createUser({ username: 'Bobby', usernameLower: 'bobby', host: 'example.com' });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await usersRepository.delete({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
test('フォロー中のアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alycia, alysha, alyson]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
// alycia, alysha, alysonは非アクティブなので後ろに行く
|
||||
expect(result).toEqual([alice, alyce, alyssa, alycia, alysha, alyson].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォロー中の非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
// alice, alyceはフォローしていないので後ろに行く
|
||||
expect(result).toEqual([alycia, alysha, alyson, alyssa, alice, alyce].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォローしていないアクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
// alice, alyce, alyciaは非アクティブなので後ろに行く
|
||||
expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォローしていない非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォロー(アクティブ)、フォロー(非アクティブ)、非フォロー(アクティブ)、非フォロー(非アクティブ)混在時の優先順位度確認', async () => {
|
||||
await createFollowings(root, [alyson, alyssa, bob, bobbi, bobbie]);
|
||||
await setActive([root, alyssa, bob, bobbi, alyce, alycia]);
|
||||
await setInactive([alyson, alice, alysha, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
// 見る用
|
||||
// const users = await usersRepository.findBy({ id: In(result) }).then(it => new Map(it.map(x => [x.id, x])));
|
||||
// console.log(result.map(x => users.get(x as any)).map(it => it?.username));
|
||||
|
||||
// フォローしててアクティブなので先頭: alyssa, bob, bobbi
|
||||
// フォローしてて非アクティブなので次: alyson, bobbie
|
||||
// フォローしてないけどアクティブなので次: alyce, alycia, root(アルファベット順的にここになる)
|
||||
// フォローしてないし非アクティブなので最後: alice, alysha, bobby
|
||||
expect(result).toEqual([alyssa, bob, bobbi, alyson, bobbie, alyce, alycia, root, alice, alysha, bobby].map(x => x.id));
|
||||
});
|
||||
|
||||
test('[非ログイン] アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setInactive([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
);
|
||||
|
||||
// alice, alyce, alyciaは非アクティブなので後ろに行く
|
||||
expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id));
|
||||
});
|
||||
|
||||
test('[非ログイン] 非アクティブユーザのうち、"al"から始まる人が全員ヒットする', async () => {
|
||||
await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
);
|
||||
|
||||
expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id));
|
||||
});
|
||||
|
||||
test('フォロー中のアクティブユーザのうち、"al"から始まり"example.com"にいる人が全員ヒットする', async () => {
|
||||
await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al', host: 'exam' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
expect(result).toEqual([alyson, alyssa].map(x => x.id));
|
||||
});
|
||||
|
||||
test('サスペンド済みユーザは出ない', async () => {
|
||||
await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]);
|
||||
await setSuspended([alice, alyce, alycia]);
|
||||
|
||||
const result = await service.search(
|
||||
{ username: 'al' },
|
||||
{ limit: 100 },
|
||||
root,
|
||||
);
|
||||
|
||||
expect(result).toEqual([alysha, alyson, alyssa].map(x => x.id));
|
||||
});
|
||||
});
|
||||
});
|
||||
245
packages/backend/test/unit/UserWebhookService.ts
Normal file
245
packages/backend/test/unit/UserWebhookService.ts
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { randomString } from '../utils.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiWebhook, UsersRepository, WebhooksRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
|
||||
describe('UserWebhookService', () => {
|
||||
let app: TestingModule;
|
||||
let service: UserWebhookService;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
let usersRepository: UsersRepository;
|
||||
let userWebhooksRepository: WebhooksRepository;
|
||||
let idService: IdService;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
let root: MiUser;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}) {
|
||||
return await usersRepository
|
||||
.insert({
|
||||
id: idService.gen(),
|
||||
...data,
|
||||
})
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
async function createWebhook(data: Partial<MiWebhook> = {}) {
|
||||
return userWebhooksRepository
|
||||
.insert({
|
||||
id: idService.gen(),
|
||||
name: randomString(),
|
||||
on: ['mention'],
|
||||
url: 'https://example.com',
|
||||
secret: randomString(),
|
||||
userId: root.id,
|
||||
...data,
|
||||
})
|
||||
.then(x => userWebhooksRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
async function beforeAllImpl() {
|
||||
app = await Test
|
||||
.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
],
|
||||
providers: [
|
||||
UserWebhookService,
|
||||
IdService,
|
||||
LoggerService,
|
||||
GlobalEventService,
|
||||
{
|
||||
provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
|
||||
},
|
||||
],
|
||||
})
|
||||
.compile();
|
||||
|
||||
usersRepository = app.get(DI.usersRepository);
|
||||
userWebhooksRepository = app.get(DI.webhooksRepository);
|
||||
|
||||
service = app.get(UserWebhookService);
|
||||
idService = app.get(IdService);
|
||||
queueService = app.get(QueueService) as jest.Mocked<QueueService>;
|
||||
|
||||
app.enableShutdownHooks();
|
||||
}
|
||||
|
||||
async function afterAllImpl() {
|
||||
await app.close();
|
||||
}
|
||||
|
||||
async function beforeEachImpl() {
|
||||
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
|
||||
}
|
||||
|
||||
async function afterEachImpl() {
|
||||
await usersRepository.delete({});
|
||||
await userWebhooksRepository.delete({});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
describe('アプリを毎回作り直す必要のないグループ', () => {
|
||||
beforeAll(beforeAllImpl);
|
||||
afterAll(afterAllImpl);
|
||||
beforeEach(beforeEachImpl);
|
||||
afterEach(afterEachImpl);
|
||||
|
||||
describe('fetchSystemWebhooks', () => {
|
||||
test('フィルタなし', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks();
|
||||
expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]);
|
||||
});
|
||||
|
||||
test('activeのみ', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ isActive: true });
|
||||
expect(fetchedWebhooks).toEqual([webhook1, webhook3]);
|
||||
});
|
||||
|
||||
test('特定のイベントのみ', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'] });
|
||||
expect(fetchedWebhooks).toEqual([webhook1, webhook2]);
|
||||
});
|
||||
|
||||
test('activeな特定のイベントのみ', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'], isActive: true });
|
||||
expect(fetchedWebhooks).toEqual([webhook1]);
|
||||
});
|
||||
|
||||
test('ID指定', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id] });
|
||||
expect(fetchedWebhooks).toEqual([webhook1, webhook4]);
|
||||
});
|
||||
|
||||
test('ID指定(他条件とANDになるか見たい)', async () => {
|
||||
const webhook1 = await createWebhook({
|
||||
active: true,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook2 = await createWebhook({
|
||||
active: false,
|
||||
on: ['mention'],
|
||||
});
|
||||
const webhook3 = await createWebhook({
|
||||
active: true,
|
||||
on: ['reply'],
|
||||
});
|
||||
const webhook4 = await createWebhook({
|
||||
active: false,
|
||||
on: [],
|
||||
});
|
||||
|
||||
const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false });
|
||||
expect(fetchedWebhooks).toEqual([webhook4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
225
packages/backend/test/unit/WebhookTestService.ts
Normal file
225
packages/backend/test/unit/WebhookTestService.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { beforeAll, describe, jest } from '@jest/globals';
|
||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
describe('WebhookTestService', () => {
|
||||
let app: TestingModule;
|
||||
let service: WebhookTestService;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
let usersRepository: UsersRepository;
|
||||
let userProfilesRepository: UserProfilesRepository;
|
||||
let queueService: jest.Mocked<QueueService>;
|
||||
let userWebhookService: jest.Mocked<UserWebhookService>;
|
||||
let systemWebhookService: jest.Mocked<SystemWebhookService>;
|
||||
let idService: IdService;
|
||||
|
||||
let root: MiUser;
|
||||
let alice: MiUser;
|
||||
|
||||
async function createUser(data: Partial<MiUser> = {}) {
|
||||
const user = await usersRepository
|
||||
.insert({
|
||||
id: idService.gen(),
|
||||
...data,
|
||||
})
|
||||
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
await userProfilesRepository.insert({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
],
|
||||
providers: [
|
||||
WebhookTestService,
|
||||
IdService,
|
||||
{
|
||||
provide: QueueService, useFactory: () => ({
|
||||
systemWebhookDeliver: jest.fn(),
|
||||
userWebhookDeliver: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: UserWebhookService, useFactory: () => ({
|
||||
fetchWebhooks: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: SystemWebhookService, useFactory: () => ({
|
||||
fetchSystemWebhooks: jest.fn(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
usersRepository = app.get(DI.usersRepository);
|
||||
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||
|
||||
service = app.get(WebhookTestService);
|
||||
idService = app.get(IdService);
|
||||
queueService = app.get(QueueService) as jest.Mocked<QueueService>;
|
||||
userWebhookService = app.get(UserWebhookService) as jest.Mocked<UserWebhookService>;
|
||||
systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
|
||||
|
||||
app.enableShutdownHooks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
|
||||
alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
|
||||
|
||||
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
|
||||
{ id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook,
|
||||
]));
|
||||
systemWebhookService.fetchSystemWebhooks.mockReturnValue(Promise.resolve([
|
||||
{ id: 'dummy-webhook', isActive: true } as MiSystemWebhook,
|
||||
]));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
queueService.systemWebhookDeliver.mockClear();
|
||||
queueService.userWebhookDeliver.mockClear();
|
||||
userWebhookService.fetchWebhooks.mockClear();
|
||||
systemWebhookService.fetchSystemWebhooks.mockClear();
|
||||
|
||||
await usersRepository.delete({});
|
||||
await userProfilesRepository.delete({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
describe('testUserWebhook', () => {
|
||||
test('note', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('note');
|
||||
expect((calls[2] as any).id).toBe('dummy-note-1');
|
||||
});
|
||||
|
||||
test('reply', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reply' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('reply');
|
||||
expect((calls[2] as any).id).toBe('dummy-reply-1');
|
||||
});
|
||||
|
||||
test('renote', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'renote' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('renote');
|
||||
expect((calls[2] as any).id).toBe('dummy-renote-1');
|
||||
});
|
||||
|
||||
test('mention', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'mention' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('mention');
|
||||
expect((calls[2] as any).id).toBe('dummy-mention-1');
|
||||
});
|
||||
|
||||
test('follow', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'follow' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('follow');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-1');
|
||||
});
|
||||
|
||||
test('followed', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'followed' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('followed');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-2');
|
||||
});
|
||||
|
||||
test('unfollow', async () => {
|
||||
await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'unfollow' }, alice);
|
||||
|
||||
const calls = queueService.userWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('unfollow');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-3');
|
||||
});
|
||||
|
||||
describe('NoSuchWebhookError', () => {
|
||||
test('user not match', async () => {
|
||||
userWebhookService.fetchWebhooks.mockClear();
|
||||
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
|
||||
{ id: 'dummy-webhook', active: true } as MiWebhook,
|
||||
]));
|
||||
|
||||
await expect(service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, root))
|
||||
.rejects.toThrow(WebhookTestService.NoSuchWebhookError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('testSystemWebhook', () => {
|
||||
test('abuseReport', async () => {
|
||||
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReport' });
|
||||
|
||||
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('abuseReport');
|
||||
expect((calls[2] as any).id).toBe('dummy-abuse-report1');
|
||||
expect((calls[2] as any).resolved).toBe(false);
|
||||
});
|
||||
|
||||
test('abuseReportResolved', async () => {
|
||||
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReportResolved' });
|
||||
|
||||
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('abuseReportResolved');
|
||||
expect((calls[2] as any).id).toBe('dummy-abuse-report1');
|
||||
expect((calls[2] as any).resolved).toBe(true);
|
||||
});
|
||||
|
||||
test('userCreated', async () => {
|
||||
await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'userCreated' });
|
||||
|
||||
const calls = queueService.systemWebhookDeliver.mock.calls[0];
|
||||
expect((calls[0] as any).id).toBe('dummy-webhook');
|
||||
expect(calls[1]).toBe('userCreated');
|
||||
expect((calls[2] as any).id).toBe('dummy-user-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -20,10 +20,10 @@ import { CoreModule } from '@/core/CoreModule.js';
|
|||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
|
||||
import { MiMeta, MiNote } from '@/models/_.js';
|
||||
import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { DownloadService } from '@/core/DownloadService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import { MockResolver } from '../misc/mock-resolver.js';
|
||||
|
|
@ -86,6 +86,7 @@ async function createRandomRemoteUser(
|
|||
}
|
||||
|
||||
describe('ActivityPub', () => {
|
||||
let userProfilesRepository: UserProfilesRepository;
|
||||
let imageService: ApImageService;
|
||||
let noteService: ApNoteService;
|
||||
let personService: ApPersonService;
|
||||
|
|
@ -105,7 +106,14 @@ describe('ActivityPub', () => {
|
|||
sensitiveWords: [] as string[],
|
||||
prohibitedWords: [] as string[],
|
||||
} as MiMeta;
|
||||
let meta = metaInitial;
|
||||
const meta = { ...metaInitial };
|
||||
|
||||
function updateMeta(newMeta: Partial<MiMeta>): void {
|
||||
for (const key in meta) {
|
||||
delete (meta as any)[key];
|
||||
}
|
||||
Object.assign(meta, newMeta);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = await Test.createTestingModule({
|
||||
|
|
@ -118,15 +126,14 @@ describe('ActivityPub', () => {
|
|||
};
|
||||
},
|
||||
})
|
||||
.overrideProvider(MetaService).useValue({
|
||||
async fetch(): Promise<MiMeta> {
|
||||
return meta;
|
||||
},
|
||||
}).compile();
|
||||
.overrideProvider(DI.meta).useFactory({ factory: () => meta })
|
||||
.compile();
|
||||
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||
|
||||
noteService = app.get<ApNoteService>(ApNoteService);
|
||||
personService = app.get<ApPersonService>(ApPersonService);
|
||||
rendererService = app.get<ApRendererService>(ApRendererService);
|
||||
|
|
@ -205,6 +212,53 @@ describe('ActivityPub', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Collection visibility', () => {
|
||||
test('Public following/followers', async () => {
|
||||
const actor = createRandomActor();
|
||||
actor.following = {
|
||||
id: `${actor.id}/following`,
|
||||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
first: `${actor.id}/following?page=1`,
|
||||
};
|
||||
actor.followers = `${actor.id}/followers`;
|
||||
|
||||
resolver.register(actor.id, actor);
|
||||
resolver.register(actor.followers, {
|
||||
id: actor.followers,
|
||||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
first: `${actor.followers}?page=1`,
|
||||
});
|
||||
|
||||
const user = await personService.createPerson(actor.id, resolver);
|
||||
const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
assert.deepStrictEqual(userProfile.followingVisibility, 'public');
|
||||
assert.deepStrictEqual(userProfile.followersVisibility, 'public');
|
||||
});
|
||||
|
||||
test('Private following/followers', async () => {
|
||||
const actor = createRandomActor();
|
||||
actor.following = {
|
||||
id: `${actor.id}/following`,
|
||||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
// first: …
|
||||
};
|
||||
actor.followers = `${actor.id}/followers`;
|
||||
|
||||
resolver.register(actor.id, actor);
|
||||
//resolver.register(actor.followers, { … });
|
||||
|
||||
const user = await personService.createPerson(actor.id, resolver);
|
||||
const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
assert.deepStrictEqual(userProfile.followingVisibility, 'private');
|
||||
assert.deepStrictEqual(userProfile.followersVisibility, 'private');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Renderer', () => {
|
||||
test('Render an announce with visibility: followers', () => {
|
||||
rendererService.renderAnnounce('https://example.com/notes/00example', {
|
||||
|
|
@ -316,7 +370,7 @@ describe('ActivityPub', () => {
|
|||
});
|
||||
|
||||
test('cacheRemoteFiles=false disables caching', async () => {
|
||||
meta = { ...metaInitial, cacheRemoteFiles: false };
|
||||
updateMeta({ ...metaInitial, cacheRemoteFiles: false });
|
||||
|
||||
const imageObject: IApDocument = {
|
||||
type: 'Document',
|
||||
|
|
@ -345,7 +399,7 @@ describe('ActivityPub', () => {
|
|||
});
|
||||
|
||||
test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => {
|
||||
meta = { ...metaInitial, cacheRemoteSensitiveFiles: false };
|
||||
updateMeta({ ...metaInitial, cacheRemoteSensitiveFiles: false });
|
||||
|
||||
const imageObject: IApDocument = {
|
||||
type: 'Document',
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import {
|
||||
|
|
@ -49,6 +49,7 @@ import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
|
|||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { ReactionService } from '@/core/ReactionService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
|
|
@ -169,6 +170,7 @@ describe('UserEntityService', () => {
|
|||
ApLoggerService,
|
||||
AccountMoveService,
|
||||
ReactionService,
|
||||
ReactionsBufferingService,
|
||||
NotificationService,
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue