fix(backend): Webhook Test一致性 (#14863)

* fix(backend): Webhook Test一致性

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* UserWebhookPayload<'followed'> 修正

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
饺子w (Yumechi) 2024-11-11 18:51:18 -06:00 committed by GitHub
parent 1bc4f400c0
commit a11b77a415
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 48 additions and 27 deletions

View file

@ -45,6 +45,7 @@
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正 - Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
- Fix: User Webhookテスト機能のMock Payloadを修正
### Misskey.js ### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 - Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正

View file

@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; import type { MiWebhook, WebhookEventTypes, webhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -35,6 +35,7 @@ import type {
} from './QueueModule.js'; } from './QueueModule.js';
import type httpSignature from '@peertube/http-signature'; import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import { type UserWebhookPayload } from './UserWebhookService.js';
@Injectable() @Injectable()
export class QueueService { export class QueueService {
@ -468,10 +469,10 @@ export class QueueService {
* @see UserWebhookDeliverProcessorService * @see UserWebhookDeliverProcessorService
*/ */
@bindThis @bindThis
public userWebhookDeliver( public userWebhookDeliver<T extends WebhookEventTypes>(
webhook: MiWebhook, webhook: MiWebhook,
type: typeof webhookEventTypes[number], type: T,
content: unknown, content: UserWebhookPayload<T>,
opts?: { attempts?: number }, opts?: { attempts?: number },
) { ) {
const data: UserWebhookDeliverJobData = { const data: UserWebhookDeliverJobData = {

View file

@ -6,11 +6,23 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { type WebhooksRepository } from '@/models/_.js'; import { type WebhooksRepository } from '@/models/_.js';
import { MiWebhook } from '@/models/Webhook.js'; import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
export type UserWebhookPayload<T extends WebhookEventTypes> =
T extends 'note' | 'reply' | 'renote' |'mention' ? {
note: Packed<'Note'>,
} :
T extends 'follow' | 'unfollow' ? {
user: Packed<'UserDetailedNotMe'>,
} :
T extends 'followed' ? {
user: Packed<'UserLite'>,
} : never;
@Injectable() @Injectable()
export class UserWebhookService implements OnApplicationShutdown { export class UserWebhookService implements OnApplicationShutdown {

View file

@ -10,7 +10,7 @@ import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWeb
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js'; import { type WebhookEventTypes } from '@/models/Webhook.js';
import { UserWebhookService } from '@/core/UserWebhookService.js'; import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
@ -306,10 +306,10 @@ export class WebhookTestService {
* - on * - on
*/ */
@bindThis @bindThis
public async testUserWebhook( public async testUserWebhook<T extends WebhookEventTypes>(
params: { params: {
webhookId: MiWebhook['id'], webhookId: MiWebhook['id'],
type: WebhookEventTypes, type: T,
override?: Partial<Omit<MiWebhook, 'id'>>, override?: Partial<Omit<MiWebhook, 'id'>>,
}, },
sender: MiUser | null, sender: MiUser | null,
@ -321,7 +321,7 @@ export class WebhookTestService {
} }
const webhook = webhooks[0]; const webhook = webhooks[0];
const send = (contents: unknown) => { const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
const merged = { const merged = {
...webhook, ...webhook,
...params.override, ...params.override,
@ -329,7 +329,7 @@ export class WebhookTestService {
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図. // テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図.
// また、Jobの試行回数も1回だけ. // また、Jobの試行回数も1回だけ.
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 }); this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
}; };
const dummyNote1 = generateDummyNote({ const dummyNote1 = generateDummyNote({
@ -361,33 +361,40 @@ export class WebhookTestService {
switch (params.type) { switch (params.type) {
case 'note': { case 'note': {
send(toPackedNote(dummyNote1)); send('note', { note: toPackedNote(dummyNote1) });
break; break;
} }
case 'reply': { case 'reply': {
send(toPackedNote(dummyReply1)); send('reply', { note: toPackedNote(dummyReply1) });
break; break;
} }
case 'renote': { case 'renote': {
send(toPackedNote(dummyRenote1)); send('renote', { note: toPackedNote(dummyRenote1) });
break; break;
} }
case 'mention': { case 'mention': {
send(toPackedNote(dummyMention1)); send('mention', { note: toPackedNote(dummyMention1) });
break; break;
} }
case 'follow': { case 'follow': {
send(toPackedUserDetailedNotMe(dummyUser1)); send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
break; break;
} }
case 'followed': { case 'followed': {
send(toPackedUserLite(dummyUser2)); send('followed', { user: toPackedUserLite(dummyUser2) });
break; break;
} }
case 'unfollow': { case 'unfollow': {
send(toPackedUserDetailedNotMe(dummyUser3)); send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
break; break;
} }
// まだ実装されていない (#9485)
case 'reaction': return;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
} }
} }

View file

@ -7,7 +7,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { beforeAll, describe, jest } from '@jest/globals'; import { beforeAll, describe, jest } from '@jest/globals';
import { WebhookTestService } from '@/core/WebhookTestService.js'; import { WebhookTestService } from '@/core/WebhookTestService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js'; import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@ -122,7 +122,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('note'); expect(calls[1]).toBe('note');
expect((calls[2] as any).id).toBe('dummy-note-1'); expect((calls[2] as UserWebhookPayload<'note'>).note.id).toBe('dummy-note-1');
}); });
test('reply', async () => { test('reply', async () => {
@ -131,7 +131,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('reply'); expect(calls[1]).toBe('reply');
expect((calls[2] as any).id).toBe('dummy-reply-1'); expect((calls[2] as UserWebhookPayload<'reply'>).note.id).toBe('dummy-reply-1');
}); });
test('renote', async () => { test('renote', async () => {
@ -140,7 +140,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('renote'); expect(calls[1]).toBe('renote');
expect((calls[2] as any).id).toBe('dummy-renote-1'); expect((calls[2] as UserWebhookPayload<'renote'>).note.id).toBe('dummy-renote-1');
}); });
test('mention', async () => { test('mention', async () => {
@ -149,7 +149,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('mention'); expect(calls[1]).toBe('mention');
expect((calls[2] as any).id).toBe('dummy-mention-1'); expect((calls[2] as UserWebhookPayload<'mention'>).note.id).toBe('dummy-mention-1');
}); });
test('follow', async () => { test('follow', async () => {
@ -158,7 +158,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('follow'); expect(calls[1]).toBe('follow');
expect((calls[2] as any).id).toBe('dummy-user-1'); expect((calls[2] as UserWebhookPayload<'follow'>).user.id).toBe('dummy-user-1');
}); });
test('followed', async () => { test('followed', async () => {
@ -167,7 +167,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('followed'); expect(calls[1]).toBe('followed');
expect((calls[2] as any).id).toBe('dummy-user-2'); expect((calls[2] as UserWebhookPayload<'followed'>).user.id).toBe('dummy-user-2');
}); });
test('unfollow', async () => { test('unfollow', async () => {
@ -176,7 +176,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('unfollow'); expect(calls[1]).toBe('unfollow');
expect((calls[2] as any).id).toBe('dummy-user-3'); expect((calls[2] as UserWebhookPayload<'unfollow'>).user.id).toBe('dummy-user-3');
}); });
describe('NoSuchWebhookError', () => { describe('NoSuchWebhookError', () => {

View file

@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton> <MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
</div> </div>
<div :class="$style.switchBox"> <div :class="$style.switchBox">
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton> <MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
</div> </div>
<div :class="$style.switchBox"> <div :class="$style.switchBox">

View file

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
</div> </div>
</FormSection> </FormSection>