Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(backend): APIテストの復活 #10163

Merged
merged 21 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions cypress/e2e/api.cy.js

This file was deleted.

6 changes: 4 additions & 2 deletions packages/backend/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ module.exports = {
// See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
// TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
// directly import `.ts` files without this hack.
'^(\\.{1,2}/.*)\\.js$': '$1',
'^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1',
},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
Expand Down Expand Up @@ -160,7 +160,7 @@ module.exports = {
testMatch: [
"<rootDir>/test/unit/**/*.ts",
"<rootDir>/src/**/*.test.ts",
//"<rootDir>/test/e2e/**/*.ts"
"<rootDir>/test/e2e/**/*.ts",
],

// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
Expand Down Expand Up @@ -207,4 +207,6 @@ module.exports = {
// watchman: true,

extensionsToTreatAsEsm: ['.ts'],

testTimeout: 60000,
};
1 change: 0 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@
},
"devDependencies": {
"@jest/globals": "29.4.3",
"@redocly/openapi-core": "1.0.0-beta.123",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.1",
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/GlobalModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ export class GlobalModule implements OnApplicationShutdown {
) {}

async onApplicationShutdown(signal: string): Promise<void> {
if (process.env.NODE_ENV === 'test') {
// XXX:
// Misskey has asynchronous postgres/redis connections that are not awaited.
// Shutting down the existing connections causes errors on Jest.
// This can cause intermittent test failures as there's no promise that
// those connections will finish before the next test starts.
// See also the comment for `cache` in packages/backend/src/postgres.ts
return;
}
await Promise.all([
this.db.destroy(),
this.redisClient.disconnect(),
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/boot/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
37 changes: 22 additions & 15 deletions packages/backend/src/core/CreateNotificationService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand All @@ -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;
}

Expand All @@ -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) {
/*
Expand All @@ -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();
}
}
16 changes: 13 additions & 3 deletions packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -756,4 +762,8 @@ export class NoteCreateService {

return mentionedUsers;
}

onApplicationShutdown(signal?: string | undefined) {
this.#shutdownController.abort();
}
}
47 changes: 27 additions & 20 deletions packages/backend/src/core/NoteReadService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
Expand All @@ -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(
Expand All @@ -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)) {
Expand All @@ -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({
Expand Down Expand Up @@ -183,22 +186,22 @@ 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)),
noteId: In(readAntennaNotes.map(n => n.id)),
}, {
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 });
Expand All @@ -213,4 +216,8 @@ export class NoteReadService {
});
}
}

onApplicationShutdown(signal?: string | undefined): void {
this.#shutdownController.abort();
}
}
4 changes: 2 additions & 2 deletions packages/backend/src/core/chart/charts/per-user-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading