summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-03-03 03:13:12 +0100
committerGitHub <noreply@github.com>2023-03-03 11:13:12 +0900
commit61215e50ff9e4c84787c8d99c75fd36dafbd8815 (patch)
tree36419e8a3ec97afa0a3a0011d523d80addf8e724 /packages/backend/src
parentfix(server): チャンネルでミュートが正しく機能していない... (diff)
downloadsharkey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.tar.gz
sharkey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.tar.bz2
sharkey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.zip
test(backend): APIテストの復活 (#10163)
* Revert 1c5291f8185651c231903129ee7c1cee263f9f03 * APIテストの復活 * apiテストの移行 * moduleNameMapper修正 * simpleGetでthrowしないように status確認しているので要らない * longer timeout * ローカルでは問題ないのになんで * case sensitive * Run Nest instance within the current process * Skip some setIntervals * wait for 5 seconds * kill them all!! * logHeapUsage: true * detectOpenHandlesがじゃましているらしい * maxWorkers=1? * restore drive api tests * workerIdleMemoryLimit: 500MB * 1024MiB * Wait what
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/GlobalModule.ts9
-rw-r--r--packages/backend/src/boot/common.ts4
-rw-r--r--packages/backend/src/core/CreateNotificationService.ts37
-rw-r--r--packages/backend/src/core/NoteCreateService.ts16
-rw-r--r--packages/backend/src/core/NoteReadService.ts47
-rw-r--r--packages/backend/src/core/chart/ChartManagementService.ts8
-rw-r--r--packages/backend/src/core/chart/charts/per-user-notes.ts4
-rw-r--r--packages/backend/src/server/ServerService.ts16
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts5
-rw-r--r--packages/backend/src/server/api/ApiServerService.ts12
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
11 files changed, 107 insertions, 55 deletions
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 35416209a0..801f1db741 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -1,3 +1,4 @@
+import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common';
import Redis from 'ioredis';
import { DataSource } from 'typeorm';
@@ -57,6 +58,14 @@ export class GlobalModule implements OnApplicationShutdown {
) {}
async onApplicationShutdown(signal: string): Promise<void> {
+ if (process.env.NODE_ENV === 'test') {
+ // XXX:
+ // Shutting down the existing connections causes errors on Jest as
+ // Misskey has asynchronous postgres/redis connections that are not
+ // awaited.
+ // Let's wait for some random time for them to finish.
+ await setTimeout(5000);
+ }
await Promise.all([
this.db.destroy(),
this.redisClient.disconnect(),
diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts
index 04aa26e652..279a1fe59d 100644
--- a/packages/backend/src/boot/common.ts
+++ b/packages/backend/src/boot/common.ts
@@ -16,12 +16,14 @@ export async function server() {
app.enableShutdownHooks();
const serverService = app.get(ServerService);
- serverService.launch();
+ await serverService.launch();
app.get(ChartManagementService).start();
app.get(JanitorService).start();
app.get(QueueStatsService).start();
app.get(ServerStatsService).start();
+
+ return app;
}
export async function jobQueue() {
diff --git a/packages/backend/src/core/CreateNotificationService.ts b/packages/backend/src/core/CreateNotificationService.ts
index cd47844a75..eba7171fb6 100644
--- a/packages/backend/src/core/CreateNotificationService.ts
+++ b/packages/backend/src/core/CreateNotificationService.ts
@@ -1,4 +1,5 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { setTimeout } from 'node:timers/promises';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js';
@@ -10,7 +11,9 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
-export class CreateNotificationService {
+export class CreateNotificationService implements OnApplicationShutdown {
+ #shutdownController = new AbortController();
+
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -40,11 +43,11 @@ export class CreateNotificationService {
if (data.notifierId && (notifieeId === data.notifierId)) {
return null;
}
-
+
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
-
+
const isMuted = profile?.mutingNotificationTypes.includes(type);
-
+
// Create notification
const notification = await this.notificationsRepository.insert({
id: this.idService.genId(),
@@ -56,18 +59,18 @@ export class CreateNotificationService {
...data,
} as Partial<Notification>)
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
-
+
const packed = await this.notificationEntityService.pack(notification, {});
-
+
// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
-
+
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
- setTimeout(async () => {
+ setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
if (fresh == null) return; // 既に削除されているかもしれない
if (fresh.isRead) return;
-
+
//#region ただしミュートしているユーザーからの通知なら無視
const mutings = await this.mutingsRepository.findBy({
muterId: notifieeId,
@@ -76,14 +79,14 @@ export class CreateNotificationService {
return;
}
//#endregion
-
+
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
-
+
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
- }, 2000);
-
+ }, () => { /* aborted, ignore it */ });
+
return notification;
}
@@ -103,7 +106,7 @@ export class CreateNotificationService {
sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
-
+
@bindThis
private async emailNotificationReceiveFollowRequest(userId: User['id'], follower: User) {
/*
@@ -115,4 +118,8 @@ export class CreateNotificationService {
sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
+
+ onApplicationShutdown(signal?: string | undefined): void {
+ this.#shutdownController.abort();
+ }
}
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 54c135a7c5..4c4261ba79 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -1,6 +1,7 @@
+import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm';
-import { Inject, Injectable } from '@nestjs/common';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
@@ -137,7 +138,9 @@ type Option = {
};
@Injectable()
-export class NoteCreateService {
+export class NoteCreateService implements OnApplicationShutdown {
+ #shutdownController = new AbortController();
+
constructor(
@Inject(DI.config)
private config: Config,
@@ -313,7 +316,10 @@ export class NoteCreateService {
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
- setImmediate(() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!));
+ setImmediate('post created', { signal: this.#shutdownController.signal }).then(
+ () => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
+ () => { /* aborted, ignore this */ },
+ );
return note;
}
@@ -756,4 +762,8 @@ export class NoteCreateService {
return mentionedUsers;
}
+
+ onApplicationShutdown(signal?: string | undefined) {
+ this.#shutdownController.abort();
+ }
}
diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts
index 84983d600e..d23fb8238b 100644
--- a/packages/backend/src/core/NoteReadService.ts
+++ b/packages/backend/src/core/NoteReadService.ts
@@ -1,4 +1,5 @@
-import { Inject, Injectable } from '@nestjs/common';
+import { setTimeout } from 'node:timers/promises';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In, IsNull, Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js';
@@ -15,7 +16,9 @@ import { AntennaService } from './AntennaService.js';
import { PushNotificationService } from './PushNotificationService.js';
@Injectable()
-export class NoteReadService {
+export class NoteReadService implements OnApplicationShutdown {
+ #shutdownController = new AbortController();
+
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -60,14 +63,14 @@ export class NoteReadService {
});
if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion
-
+
// スレッドミュート
const threadMute = await this.noteThreadMutingsRepository.findOneBy({
userId: userId,
threadId: note.threadId ?? note.id,
});
if (threadMute) return;
-
+
const unread = {
id: this.idService.genId(),
noteId: note.id,
@@ -77,15 +80,15 @@ export class NoteReadService {
noteChannelId: note.channelId,
noteUserId: note.userId,
};
-
+
await this.noteUnreadsRepository.insert(unread);
-
+
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
- setTimeout(async () => {
+ setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id });
-
+
if (exist == null) return;
-
+
if (params.isMentioned) {
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
}
@@ -95,8 +98,8 @@ export class NoteReadService {
if (note.channelId) {
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
}
- }, 2000);
- }
+ }, () => { /* aborted, ignore it */ });
+ }
@bindThis
public async read(
@@ -113,24 +116,24 @@ export class NoteReadService {
},
select: ['followeeId'],
})).map(x => x.followeeId));
-
+
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
-
+
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
readMentions.push(note);
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note);
}
-
+
if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
-
+
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
@@ -139,14 +142,14 @@ export class NoteReadService {
}
}
}
-
+
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
});
-
+
// TODO: ↓まとめてクエリしたい
this.noteUnreadsRepository.countBy({
@@ -183,7 +186,7 @@ export class NoteReadService {
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
}
-
+
if (readAntennaNotes.length > 0) {
await this.antennaNotesRepository.update({
antennaId: In(myAntennas.map(a => a.id)),
@@ -191,14 +194,14 @@ export class NoteReadService {
}, {
read: true,
});
-
+
// TODO: まとめてクエリしたい
for (const antenna of myAntennas) {
const count = await this.antennaNotesRepository.countBy({
antennaId: antenna.id,
read: false,
});
-
+
if (count === 0) {
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
@@ -213,4 +216,8 @@ export class NoteReadService {
});
}
}
+
+ onApplicationShutdown(signal?: string | undefined): void {
+ this.#shutdownController.abort();
+ }
}
diff --git a/packages/backend/src/core/chart/ChartManagementService.ts b/packages/backend/src/core/chart/ChartManagementService.ts
index dbde757676..03e3612658 100644
--- a/packages/backend/src/core/chart/ChartManagementService.ts
+++ b/packages/backend/src/core/chart/ChartManagementService.ts
@@ -62,8 +62,10 @@ export class ChartManagementService implements OnApplicationShutdown {
async onApplicationShutdown(signal: string): Promise<void> {
clearInterval(this.saveIntervalId);
- await Promise.all(
- this.charts.map(chart => chart.save()),
- );
+ if (process.env.NODE_ENV !== 'test') {
+ await Promise.all(
+ this.charts.map(chart => chart.save()),
+ );
+ }
}
}
diff --git a/packages/backend/src/core/chart/charts/per-user-notes.ts b/packages/backend/src/core/chart/charts/per-user-notes.ts
index 1e2a579dfa..d8966f34c1 100644
--- a/packages/backend/src/core/chart/charts/per-user-notes.ts
+++ b/packages/backend/src/core/chart/charts/per-user-notes.ts
@@ -45,8 +45,8 @@ export default class PerUserNotesChart extends Chart<typeof schema> {
}
@bindThis
- public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise<void> {
- await this.commit({
+ public update(user: { id: User['id'] }, note: Note, isAdditional: boolean): void {
+ this.commit({
'total': isAdditional ? 1 : -1,
'inc': isAdditional ? 1 : 0,
'dec': isAdditional ? 0 : 1,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 417f50f95d..e61383468c 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -1,7 +1,7 @@
import cluster from 'node:cluster';
import * as fs from 'node:fs';
-import { Inject, Injectable } from '@nestjs/common';
-import Fastify from 'fastify';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
+import Fastify, { FastifyInstance } from 'fastify';
import { IsNull } from 'typeorm';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Config } from '@/config.js';
@@ -23,8 +23,9 @@ import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
@Injectable()
-export class ServerService {
+export class ServerService implements OnApplicationShutdown {
private logger: Logger;
+ #fastify: FastifyInstance;
constructor(
@Inject(DI.config)
@@ -54,11 +55,12 @@ export class ServerService {
}
@bindThis
- public launch() {
+ public async launch() {
const fastify = Fastify({
trustProxy: true,
logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''),
});
+ this.#fastify = fastify;
// HSTS
// 6months (15552000sec)
@@ -203,5 +205,11 @@ export class ServerService {
});
fastify.listen({ port: this.config.port, host: '0.0.0.0' });
+
+ await fastify.ready();
+ }
+
+ async onApplicationShutdown(signal: string): Promise<void> {
+ await this.#fastify.close();
}
}
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 6d8540dd4f..f84a3aa59b 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -100,9 +100,12 @@ export class ApiCallService implements OnApplicationShutdown {
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
reply: FastifyReply,
) {
- const multipartData = await request.file();
+ const multipartData = await request.file().catch(() => {
+ /* Fastify throws if the remote didn't send multipart data. Return 400 below. */
+ });
if (multipartData == null) {
reply.code(400);
+ reply.send();
return;
}
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 501ce63877..115d60986c 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -73,28 +73,32 @@ export class ApiServerService {
Params: { endpoint: string; },
Body: Record<string, unknown>,
Querystring: Record<string, unknown>,
- }>('/' + endpoint.name, (request, reply) => {
+ }>('/' + endpoint.name, async (request, reply) => {
if (request.method === 'GET' && !endpoint.meta.allowGet) {
reply.code(405);
reply.send();
return;
}
- this.apiCallService.handleMultipartRequest(ep, request, reply);
+ // Await so that any error can automatically be translated to HTTP 500
+ await this.apiCallService.handleMultipartRequest(ep, request, reply);
+ return reply;
});
} else {
fastify.all<{
Params: { endpoint: string; },
Body: Record<string, unknown>,
Querystring: Record<string, unknown>,
- }>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, (request, reply) => {
+ }>('/' + endpoint.name, { bodyLimit: 1024 * 32 }, async (request, reply) => {
if (request.method === 'GET' && !endpoint.meta.allowGet) {
reply.code(405);
reply.send();
return;
}
- this.apiCallService.handleRequest(ep, request, reply);
+ // Await so that any error can automatically be translated to HTTP 500
+ await this.apiCallService.handleRequest(ep, request, reply);
+ return reply;
});
}
}
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 4d5ed9fb62..4f521148e0 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -741,8 +741,8 @@ export interface IEndpoint {
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
return {
name: name,
- meta: ep.meta ?? {},
- params: ep.paramDef,
+ get meta() { return ep.meta ?? {}; },
+ get params() { return ep.paramDef; },
};
});