From 61215e50ff9e4c84787c8d99c75fd36dafbd8815 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Fri, 3 Mar 2023 03:13:12 +0100 Subject: test(backend): APIテストの復活 (#10163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- cypress/e2e/api.cy.js | 11 - packages/backend/jest.config.cjs | 13 +- packages/backend/package.json | 5 +- packages/backend/src/GlobalModule.ts | 9 + packages/backend/src/boot/common.ts | 4 +- .../backend/src/core/CreateNotificationService.ts | 37 +- packages/backend/src/core/NoteCreateService.ts | 16 +- packages/backend/src/core/NoteReadService.ts | 47 +- .../src/core/chart/ChartManagementService.ts | 8 +- .../src/core/chart/charts/per-user-notes.ts | 4 +- packages/backend/src/server/ServerService.ts | 16 +- packages/backend/src/server/api/ApiCallService.ts | 5 +- .../backend/src/server/api/ApiServerService.ts | 12 +- packages/backend/src/server/api/endpoints.ts | 4 +- packages/backend/test/_e2e/api-visibility.ts | 477 ------------ packages/backend/test/_e2e/api.ts | 83 --- packages/backend/test/_e2e/block.ts | 85 --- packages/backend/test/_e2e/endpoints.ts | 824 --------------------- packages/backend/test/_e2e/fetch-resource.ts | 205 ----- packages/backend/test/_e2e/ff-visibility.ts | 167 ----- packages/backend/test/_e2e/mute.ts | 123 --- packages/backend/test/_e2e/note.ts | 370 --------- packages/backend/test/_e2e/streaming.ts | 545 -------------- packages/backend/test/_e2e/thread-mute.ts | 103 --- packages/backend/test/_e2e/user-notes.ts | 61 -- packages/backend/test/e2e/api-visibility.ts | 477 ++++++++++++ packages/backend/test/e2e/api.ts | 83 +++ packages/backend/test/e2e/block.ts | 85 +++ packages/backend/test/e2e/endpoints.ts | 797 ++++++++++++++++++++ packages/backend/test/e2e/fetch-resource.ts | 193 +++++ packages/backend/test/e2e/ff-visibility.ts | 165 +++++ packages/backend/test/e2e/mute.ts | 123 +++ packages/backend/test/e2e/note.ts | 370 +++++++++ packages/backend/test/e2e/streaming.ts | 547 ++++++++++++++ packages/backend/test/e2e/thread-mute.ts | 103 +++ packages/backend/test/e2e/user-notes.ts | 61 ++ packages/backend/test/utils.ts | 240 ++++++ pnpm-lock.yaml | 58 +- 38 files changed, 3371 insertions(+), 3165 deletions(-) delete mode 100644 cypress/e2e/api.cy.js delete mode 100644 packages/backend/test/_e2e/api-visibility.ts delete mode 100644 packages/backend/test/_e2e/api.ts delete mode 100644 packages/backend/test/_e2e/block.ts delete mode 100644 packages/backend/test/_e2e/endpoints.ts delete mode 100644 packages/backend/test/_e2e/fetch-resource.ts delete mode 100644 packages/backend/test/_e2e/ff-visibility.ts delete mode 100644 packages/backend/test/_e2e/mute.ts delete mode 100644 packages/backend/test/_e2e/note.ts delete mode 100644 packages/backend/test/_e2e/streaming.ts delete mode 100644 packages/backend/test/_e2e/thread-mute.ts delete mode 100644 packages/backend/test/_e2e/user-notes.ts create mode 100644 packages/backend/test/e2e/api-visibility.ts create mode 100644 packages/backend/test/e2e/api.ts create mode 100644 packages/backend/test/e2e/block.ts create mode 100644 packages/backend/test/e2e/endpoints.ts create mode 100644 packages/backend/test/e2e/fetch-resource.ts create mode 100644 packages/backend/test/e2e/ff-visibility.ts create mode 100644 packages/backend/test/e2e/mute.ts create mode 100644 packages/backend/test/e2e/note.ts create mode 100644 packages/backend/test/e2e/streaming.ts create mode 100644 packages/backend/test/e2e/thread-mute.ts create mode 100644 packages/backend/test/e2e/user-notes.ts diff --git a/cypress/e2e/api.cy.js b/cypress/e2e/api.cy.js deleted file mode 100644 index 00df987bfc..0000000000 --- a/cypress/e2e/api.cy.js +++ /dev/null @@ -1,11 +0,0 @@ -describe('API', () => { - it('returns HTTP 404 to unknown API endpoint paths', () => { - cy.request({ - url: '/api/foo', - failOnStatusCode: false, - }).then((response) => { - expect(response.status).to.eq(404); - expect(response.body.error.code).to.eq('UNKNOWN_API_ENDPOINT'); - }); - }); -}); diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 8a11ad848c..6b1afec734 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -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 @@ -160,7 +160,7 @@ module.exports = { testMatch: [ "/test/unit/**/*.ts", "/src/**/*.test.ts", - //"/test/e2e/**/*.ts" + "/test/e2e/**/*.ts", ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped @@ -207,4 +207,13 @@ module.exports = { // watchman: true, extensionsToTreatAsEsm: ['.ts'], + + testTimeout: 60000, + + // Let Jest kill the test worker whenever it grows too much + // (It seems there's a known memory leak issue in Node.js' vm.Script used by Jest) + // https://github.com/facebook/jest/issues/11956 + maxWorkers: 1, // Make it use worker (that can be killed and restarted) + logHeapUsage: true, // To debug when out-of-memory happens on CI + workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) }; diff --git a/packages/backend/package.json b/packages/backend/package.json index 9fa1e68a46..42efb881e2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -15,8 +15,8 @@ "typecheck": "tsc --noEmit", "eslint": "eslint --quiet \"src/**/*.ts\"", "lint": "pnpm typecheck && pnpm eslint", - "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand --detectOpenHandles", - "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand --detectOpenHandles", + "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit", + "jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit", "jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache", "test": "pnpm jest", "test-and-coverage": "pnpm jest-and-coverage" @@ -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", 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 { + 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) .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 { 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 { } @bindThis - public async update(user: { id: User['id'] }, note: Note, isAdditional: boolean): Promise { - 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 { + 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, Querystring: Record }>, 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, Querystring: Record, - }>('/' + 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, Querystring: Record, - }>('/' + 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; }, }; }); diff --git a/packages/backend/test/_e2e/api-visibility.ts b/packages/backend/test/_e2e/api-visibility.ts deleted file mode 100644 index d29b9acb3d..0000000000 --- a/packages/backend/test/_e2e/api-visibility.ts +++ /dev/null @@ -1,477 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, startServer, shutdownServer } from '../utils.js'; - -describe('API visibility', () => { - let p: childProcess.ChildProcess; - - beforeAll(async () => { - p = await startServer(); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - describe('Note visibility', () => { - //#region vars - /** ヒロイン */ - let alice: any; - /** フォロワー */ - let follower: any; - /** 非フォロワー */ - let other: any; - /** 非フォロワーでもリプライやメンションをされた人 */ - let target: any; - /** specified mentionでmentionを飛ばされる人 */ - let target2: any; - - /** public-post */ - let pub: any; - /** home-post */ - let home: any; - /** followers-post */ - let fol: any; - /** specified-post */ - let spe: any; - - /** public-reply to target's post */ - let pubR: any; - /** home-reply to target's post */ - let homeR: any; - /** followers-reply to target's post */ - let folR: any; - /** specified-reply to target's post */ - let speR: any; - - /** public-mention to target */ - let pubM: any; - /** home-mention to target */ - let homeM: any; - /** followers-mention to target */ - let folM: any; - /** specified-mention to target */ - let speM: any; - - /** reply target post */ - let tgt: any; - //#endregion - - const show = async (noteId: any, by: any) => { - return await request('/notes/show', { - noteId, - }, by); - }; - - beforeAll(async () => { - //#region prepare - // signup - alice = await signup({ username: 'alice' }); - follower = await signup({ username: 'follower' }); - other = await signup({ username: 'other' }); - target = await signup({ username: 'target' }); - target2 = await signup({ username: 'target2' }); - - // follow alice <= follower - await request('/following/create', { userId: alice.id }, follower); - - // normal posts - pub = await post(alice, { text: 'x', visibility: 'public' }); - home = await post(alice, { text: 'x', visibility: 'home' }); - fol = await post(alice, { text: 'x', visibility: 'followers' }); - spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); - - // replies - tgt = await post(target, { text: 'y', visibility: 'public' }); - pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); - homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); - folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); - speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); - - // mentions - pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); - homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); - folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); - speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); - //#endregion - }); - - //#region show post - // public - test('[show] public-postを自分が見れる', async () => { - const res = await show(pub.id, alice); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] public-postをフォロワーが見れる', async () => { - const res = await show(pub.id, follower); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] public-postを非フォロワーが見れる', async () => { - const res = await show(pub.id, other); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] public-postを未認証が見れる', async () => { - const res = await show(pub.id, null); - assert.strictEqual(res.body.text, 'x'); - }); - - // home - test('[show] home-postを自分が見れる', async () => { - const res = await show(home.id, alice); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] home-postをフォロワーが見れる', async () => { - const res = await show(home.id, follower); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] home-postを非フォロワーが見れる', async () => { - const res = await show(home.id, other); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] home-postを未認証が見れる', async () => { - const res = await show(home.id, null); - assert.strictEqual(res.body.text, 'x'); - }); - - // followers - test('[show] followers-postを自分が見れる', async () => { - const res = await show(fol.id, alice); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] followers-postをフォロワーが見れる', async () => { - const res = await show(fol.id, follower); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] followers-postを非フォロワーが見れない', async () => { - const res = await show(fol.id, other); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] followers-postを未認証が見れない', async () => { - const res = await show(fol.id, null); - assert.strictEqual(res.body.isHidden, true); - }); - - // specified - test('[show] specified-postを自分が見れる', async () => { - const res = await show(spe.id, alice); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] specified-postを指定ユーザーが見れる', async () => { - const res = await show(spe.id, target); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] specified-postをフォロワーが見れない', async () => { - const res = await show(spe.id, follower); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] specified-postを非フォロワーが見れない', async () => { - const res = await show(spe.id, other); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] specified-postを未認証が見れない', async () => { - const res = await show(spe.id, null); - assert.strictEqual(res.body.isHidden, true); - }); - //#endregion - - //#region show reply - // public - test('[show] public-replyを自分が見れる', async () => { - const res = await show(pubR.id, alice); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] public-replyをされた人が見れる', async () => { - const res = await show(pubR.id, target); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] public-replyをフォロワーが見れる', async () => { - const res = await show(pubR.id, follower); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] public-replyを非フォロワーが見れる', async () => { - const res = await show(pubR.id, other); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] public-replyを未認証が見れる', async () => { - const res = await show(pubR.id, null); - assert.strictEqual(res.body.text, 'x'); - }); - - // home - test('[show] home-replyを自分が見れる', async () => { - const res = await show(homeR.id, alice); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] home-replyをされた人が見れる', async () => { - const res = await show(homeR.id, target); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] home-replyをフォロワーが見れる', async () => { - const res = await show(homeR.id, follower); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] home-replyを非フォロワーが見れる', async () => { - const res = await show(homeR.id, other); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] home-replyを未認証が見れる', async () => { - const res = await show(homeR.id, null); - assert.strictEqual(res.body.text, 'x'); - }); - - // followers - test('[show] followers-replyを自分が見れる', async () => { - const res = await show(folR.id, alice); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async () => { - const res = await show(folR.id, target); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] followers-replyをフォロワーが見れる', async () => { - const res = await show(folR.id, follower); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] followers-replyを非フォロワーが見れない', async () => { - const res = await show(folR.id, other); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] followers-replyを未認証が見れない', async () => { - const res = await show(folR.id, null); - assert.strictEqual(res.body.isHidden, true); - }); - - // specified - test('[show] specified-replyを自分が見れる', async () => { - const res = await show(speR.id, alice); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] specified-replyを指定ユーザーが見れる', async () => { - const res = await show(speR.id, target); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] specified-replyをされた人が指定されてなくても見れる', async () => { - const res = await show(speR.id, target); - assert.strictEqual(res.body.text, 'x'); - }); - - test('[show] specified-replyをフォロワーが見れない', async () => { - const res = await show(speR.id, follower); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] specified-replyを非フォロワーが見れない', async () => { - const res = await show(speR.id, other); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] specified-replyを未認証が見れない', async () => { - const res = await show(speR.id, null); - assert.strictEqual(res.body.isHidden, true); - }); - //#endregion - - //#region show mention - // public - test('[show] public-mentionを自分が見れる', async () => { - const res = await show(pubM.id, alice); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] public-mentionをされた人が見れる', async () => { - const res = await show(pubM.id, target); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] public-mentionをフォロワーが見れる', async () => { - const res = await show(pubM.id, follower); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] public-mentionを非フォロワーが見れる', async () => { - const res = await show(pubM.id, other); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] public-mentionを未認証が見れる', async () => { - const res = await show(pubM.id, null); - assert.strictEqual(res.body.text, '@target x'); - }); - - // home - test('[show] home-mentionを自分が見れる', async () => { - const res = await show(homeM.id, alice); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] home-mentionをされた人が見れる', async () => { - const res = await show(homeM.id, target); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] home-mentionをフォロワーが見れる', async () => { - const res = await show(homeM.id, follower); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] home-mentionを非フォロワーが見れる', async () => { - const res = await show(homeM.id, other); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] home-mentionを未認証が見れる', async () => { - const res = await show(homeM.id, null); - assert.strictEqual(res.body.text, '@target x'); - }); - - // followers - test('[show] followers-mentionを自分が見れる', async () => { - const res = await show(folM.id, alice); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async () => { - const res = await show(folM.id, target); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] followers-mentionをフォロワーが見れる', async () => { - const res = await show(folM.id, follower); - assert.strictEqual(res.body.text, '@target x'); - }); - - test('[show] followers-mentionを非フォロワーが見れない', async () => { - const res = await show(folM.id, other); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] followers-mentionを未認証が見れない', async () => { - const res = await show(folM.id, null); - assert.strictEqual(res.body.isHidden, true); - }); - - // specified - test('[show] specified-mentionを自分が見れる', async () => { - const res = await show(speM.id, alice); - assert.strictEqual(res.body.text, '@target2 x'); - }); - - test('[show] specified-mentionを指定ユーザーが見れる', async () => { - const res = await show(speM.id, target); - assert.strictEqual(res.body.text, '@target2 x'); - }); - - test('[show] specified-mentionをされた人が指定されてなかったら見れない', async () => { - const res = await show(speM.id, target2); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] specified-mentionをフォロワーが見れない', async () => { - const res = await show(speM.id, follower); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] specified-mentionを非フォロワーが見れない', async () => { - const res = await show(speM.id, other); - assert.strictEqual(res.body.isHidden, true); - }); - - test('[show] specified-mentionを未認証が見れない', async () => { - const res = await show(speM.id, null); - assert.strictEqual(res.body.isHidden, true); - }); - //#endregion - - //#region HTL - test('[HTL] public-post が 自分が見れる', async () => { - const res = await request('/notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === pub.id); - assert.strictEqual(notes[0].text, 'x'); - }); - - test('[HTL] public-post が 非フォロワーから見れない', async () => { - const res = await request('/notes/timeline', { limit: 100 }, other); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === pub.id); - assert.strictEqual(notes.length, 0); - }); - - test('[HTL] followers-post が フォロワーから見れる', async () => { - const res = await request('/notes/timeline', { limit: 100 }, follower); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === fol.id); - assert.strictEqual(notes[0].text, 'x'); - }); - //#endregion - - //#region RTL - test('[replies] followers-reply が フォロワーから見れる', async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folR.id); - assert.strictEqual(notes[0].text, 'x'); - }); - - test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folR.id); - assert.strictEqual(notes.length, 0); - }); - - test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { - const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folR.id); - assert.strictEqual(notes[0].text, 'x'); - }); - //#endregion - - //#region MTL - test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { - const res = await request('/notes/mentions', { limit: 100 }, target); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folR.id); - assert.strictEqual(notes[0].text, 'x'); - }); - - test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { - const res = await request('/notes/mentions', { limit: 100 }, target); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folM.id); - assert.strictEqual(notes[0].text, '@target x'); - }); - //#endregion - }); -}); -*/ diff --git a/packages/backend/test/_e2e/api.ts b/packages/backend/test/_e2e/api.ts deleted file mode 100644 index 7542c34db0..0000000000 --- a/packages/backend/test/_e2e/api.ts +++ /dev/null @@ -1,83 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from '../utils.js'; - -describe('API', () => { - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; - - beforeAll(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - describe('General validation', () => { - test('wrong type', async(async () => { - const res = await request('/test', { - required: true, - string: 42, - }); - assert.strictEqual(res.status, 400); - })); - - test('missing require param', async(async () => { - const res = await request('/test', { - string: 'a', - }); - assert.strictEqual(res.status, 400); - })); - - test('invalid misskey:id (empty string)', async(async () => { - const res = await request('/test', { - required: true, - id: '', - }); - assert.strictEqual(res.status, 400); - })); - - test('valid misskey:id', async(async () => { - const res = await request('/test', { - required: true, - id: '8wvhjghbxu', - }); - assert.strictEqual(res.status, 200); - })); - - test('default value', async(async () => { - const res = await request('/test', { - required: true, - string: 'a', - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.default, 'hello'); - })); - - test('can set null even if it has default value', async(async () => { - const res = await request('/test', { - required: true, - nullableDefault: null, - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.nullableDefault, null); - })); - - test('cannot set undefined if it has default value', async(async () => { - const res = await request('/test', { - required: true, - nullableDefault: undefined, - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.nullableDefault, 'hello'); - })); - }); -}); diff --git a/packages/backend/test/_e2e/block.ts b/packages/backend/test/_e2e/block.ts deleted file mode 100644 index c5f43e153c..0000000000 --- a/packages/backend/test/_e2e/block.ts +++ /dev/null @@ -1,85 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, startServer, shutdownServer } from '../utils.js'; - -describe('Block', () => { - let p: childProcess.ChildProcess; - - // alice blocks bob - let alice: any; - let bob: any; - let carol: any; - - beforeAll(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - test('Block作成', async () => { - const res = await request('/blocking/create', { - userId: bob.id, - }, alice); - - assert.strictEqual(res.status, 200); - }); - - test('ブロックされているユーザーをフォローできない', async () => { - const res = await request('/following/create', { userId: alice.id }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); - }); - - test('ブロックされているユーザーにリアクションできない', async () => { - const note = await post(alice, { text: 'hello' }); - - const res = await request('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); - }); - - test('ブロックされているユーザーに返信できない', async () => { - const note = await post(alice, { text: 'hello' }); - - const res = await request('/notes/create', { replyId: note.id, text: 'yo' }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); - }); - - test('ブロックされているユーザーのノートをRenoteできない', async () => { - const note = await post(alice, { text: 'hello' }); - - const res = await request('/notes/create', { renoteId: note.id, text: 'yo' }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); - }); - - // TODO: ユーザーリストに入れられないテスト - - // TODO: ユーザーリストから除外されるテスト - - test('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => { - const aliceNote = await post(alice); - const bobNote = await post(bob); - const carolNote = await post(carol); - - const res = await request('/notes/local-timeline', {}, bob); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); - }); -}); diff --git a/packages/backend/test/_e2e/endpoints.ts b/packages/backend/test/_e2e/endpoints.ts deleted file mode 100644 index aed980d6c8..0000000000 --- a/packages/backend/test/_e2e/endpoints.ts +++ /dev/null @@ -1,824 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import * as openapi from '@redocly/openapi-core'; -import { startServer, signup, post, request, simpleGet, port, shutdownServer, api } from '../utils.js'; - -describe('Endpoints', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let bob: any; - - beforeAll(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - describe('signup', () => { - test('不正なユーザー名でアカウントが作成できない', async () => { - const res = await request('api/signup', { - username: 'test.', - password: 'test', - }); - assert.strictEqual(res.status, 400); - }); - - test('空のパスワードでアカウントが作成できない', async () => { - const res = await request('api/signup', { - username: 'test', - password: '', - }); - assert.strictEqual(res.status, 400); - }); - - test('正しくアカウントが作成できる', async () => { - const me = { - username: 'test1', - password: 'test1', - }; - - const res = await request('api/signup', me); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.username, me.username); - }); - - test('同じユーザー名のアカウントは作成できない', async () => { - const res = await request('api/signup', { - username: 'test1', - password: 'test1', - }); - - assert.strictEqual(res.status, 400); - }); - }); - - describe('signin', () => { - test('間違ったパスワードでサインインできない', async () => { - const res = await request('api/signin', { - username: 'test1', - password: 'bar', - }); - - assert.strictEqual(res.status, 403); - }); - - test('クエリをインジェクションできない', async () => { - const res = await request('api/signin', { - username: 'test1', - password: { - $gt: '', - }, - }); - - assert.strictEqual(res.status, 400); - }); - - test('正しい情報でサインインできる', async () => { - const res = await request('api/signin', { - username: 'test1', - password: 'test1', - }); - - assert.strictEqual(res.status, 200); - }); - }); - - describe('i/update', () => { - test('アカウント設定を更新できる', async () => { - const myName = '大室櫻子'; - const myLocation = '七森中'; - const myBirthday = '2000-09-07'; - - const res = await api('/i/update', { - name: myName, - location: myLocation, - birthday: myBirthday, - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, myName); - assert.strictEqual(res.body.location, myLocation); - assert.strictEqual(res.body.birthday, myBirthday); - }); - - test('名前を空白にできない', async () => { - const res = await api('/i/update', { - name: ' ', - }, alice); - assert.strictEqual(res.status, 400); - }); - - test('誕生日の設定を削除できる', async () => { - await api('/i/update', { - birthday: '2000-09-07', - }, alice); - - const res = await api('/i/update', { - birthday: null, - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.birthday, null); - }); - - test('不正な誕生日の形式で怒られる', async () => { - const res = await api('/i/update', { - birthday: '2000/09/07', - }, alice); - assert.strictEqual(res.status, 400); - }); - }); - - describe('users/show', () => { - test('ユーザーが取得できる', async () => { - const res = await api('/users/show', { - userId: alice.id, - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.id, alice.id); - }); - - test('ユーザーが存在しなかったら怒る', async () => { - const res = await api('/users/show', { - userId: '000000000000000000000000', - }); - assert.strictEqual(res.status, 400); - }); - - test('間違ったIDで怒られる', async () => { - const res = await api('/users/show', { - userId: 'kyoppie', - }); - assert.strictEqual(res.status, 400); - }); - }); - - describe('notes/show', () => { - test('投稿が取得できる', async () => { - const myPost = await post(alice, { - text: 'test', - }); - - const res = await api('/notes/show', { - noteId: myPost.id, - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.id, myPost.id); - assert.strictEqual(res.body.text, myPost.text); - }); - - test('投稿が存在しなかったら怒る', async () => { - const res = await api('/notes/show', { - noteId: '000000000000000000000000', - }); - assert.strictEqual(res.status, 400); - }); - - test('間違ったIDで怒られる', async () => { - const res = await api('/notes/show', { - noteId: 'kyoppie', - }); - assert.strictEqual(res.status, 400); - }); - }); - - describe('notes/reactions/create', () => { - test('リアクションできる', async () => { - const bobPost = await post(bob); - - const alice = await signup({ username: 'alice' }); - const res = await api('/notes/reactions/create', { - noteId: bobPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 204); - - const resNote = await api('/notes/show', { - noteId: bobPost.id, - }, alice); - - assert.strictEqual(resNote.status, 200); - assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]); - }); - - test('自分の投稿にもリアクションできる', async () => { - const myPost = await post(alice); - - const res = await api('/notes/reactions/create', { - noteId: myPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 204); - }); - - test('二重にリアクションできない', async () => { - const bobPost = await post(bob); - - await api('/notes/reactions/create', { - noteId: bobPost.id, - reaction: '🥰', - }, alice); - - const res = await api('/notes/reactions/create', { - noteId: bobPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - }); - - test('存在しない投稿にはリアクションできない', async () => { - const res = await api('/notes/reactions/create', { - noteId: '000000000000000000000000', - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - }); - - test('空のパラメータで怒られる', async () => { - const res = await api('/notes/reactions/create', {}, alice); - - assert.strictEqual(res.status, 400); - }); - - test('間違ったIDで怒られる', async () => { - const res = await api('/notes/reactions/create', { - noteId: 'kyoppie', - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - }); - }); - - describe('following/create', () => { - test('フォローできる', async () => { - const res = await api('/following/create', { - userId: alice.id, - }, bob); - - assert.strictEqual(res.status, 200); - }); - - test('既にフォローしている場合は怒る', async () => { - const res = await api('/following/create', { - userId: alice.id, - }, bob); - - assert.strictEqual(res.status, 400); - }); - - test('存在しないユーザーはフォローできない', async () => { - const res = await api('/following/create', { - userId: '000000000000000000000000', - }, alice); - - assert.strictEqual(res.status, 400); - }); - - test('自分自身はフォローできない', async () => { - const res = await api('/following/create', { - userId: alice.id, - }, alice); - - assert.strictEqual(res.status, 400); - }); - - test('空のパラメータで怒られる', async () => { - const res = await api('/following/create', {}, alice); - - assert.strictEqual(res.status, 400); - }); - - test('間違ったIDで怒られる', async () => { - const res = await api('/following/create', { - userId: 'foo', - }, alice); - - assert.strictEqual(res.status, 400); - }); - }); - - describe('following/delete', () => { - test('フォロー解除できる', async () => { - await api('/following/create', { - userId: alice.id, - }, bob); - - const res = await api('/following/delete', { - userId: alice.id, - }, bob); - - assert.strictEqual(res.status, 200); - }); - - test('フォローしていない場合は怒る', async () => { - const res = await api('/following/delete', { - userId: alice.id, - }, bob); - - assert.strictEqual(res.status, 400); - }); - - test('存在しないユーザーはフォロー解除できない', async () => { - const res = await api('/following/delete', { - userId: '000000000000000000000000', - }, alice); - - assert.strictEqual(res.status, 400); - }); - - test('自分自身はフォロー解除できない', async () => { - const res = await api('/following/delete', { - userId: alice.id, - }, alice); - - assert.strictEqual(res.status, 400); - }); - - test('空のパラメータで怒られる', async () => { - const res = await api('/following/delete', {}, alice); - - assert.strictEqual(res.status, 400); - }); - - test('間違ったIDで怒られる', async () => { - const res = await api('/following/delete', { - userId: 'kyoppie', - }, alice); - - assert.strictEqual(res.status, 400); - }); - }); - - /* - describe('/i', () => { - test('', async () => { - }); - }); - */ -}); - -/* -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js'; - -describe('API: Endpoints', () => { - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe('drive', () => { - test('ドライブ情報を取得できる', async () => { - await uploadFile({ - userId: alice.id, - size: 256 - }); - await uploadFile({ - userId: alice.id, - size: 512 - }); - await uploadFile({ - userId: alice.id, - size: 1024 - }); - const res = await api('/drive', {}, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - expect(res.body).have.property('usage').eql(1792); - })); - }); - - describe('drive/files/create', () => { - test('ファイルを作成できる', async () => { - const res = await uploadFile(alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'Lenna.png'); - })); - - test('ファイルに名前を付けられる', async () => { - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', alice.token) - .field('name', 'Belmond.png') - .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('Belmond.png'); - })); - - test('ファイル無しで怒られる', async () => { - const res = await api('/drive/files/create', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - test('SVGファイルを作成できる', async () => { - const res = await uploadFile(alice, __dirname + '/resources/image.svg'); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'image.svg'); - assert.strictEqual(res.body.type, 'image/svg+xml'); - })); - }); - - describe('drive/files/update', () => { - test('名前を更新できる', async () => { - const file = await uploadFile(alice); - const newName = 'いちごパスタ.png'; - - const res = await api('/drive/files/update', { - fileId: file.id, - name: newName - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, newName); - })); - - test('他人のファイルは更新できない', async () => { - const file = await uploadFile(bob); - - const res = await api('/drive/files/update', { - fileId: file.id, - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('親フォルダを更新できる', async () => { - const file = await uploadFile(alice); - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await api('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.folderId, folder.id); - })); - - test('親フォルダを無しにできる', async () => { - const file = await uploadFile(alice); - - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - - await api('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - const res = await api('/drive/files/update', { - fileId: file.id, - folderId: null - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.folderId, null); - })); - - test('他人のフォルダには入れられない', async () => { - const file = await uploadFile(alice); - const folder = (await api('/drive/folders/create', { - name: 'test' - }, bob)).body; - - const res = await api('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('存在しないフォルダで怒られる', async () => { - const file = await uploadFile(alice); - - const res = await api('/drive/files/update', { - fileId: file.id, - folderId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('不正なフォルダIDで怒られる', async () => { - const file = await uploadFile(alice); - - const res = await api('/drive/files/update', { - fileId: file.id, - folderId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('ファイルが存在しなかったら怒る', async () => { - const res = await api('/drive/files/update', { - fileId: '000000000000000000000000', - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('間違ったIDで怒られる', async () => { - const res = await api('/drive/files/update', { - fileId: 'kyoppie', - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('drive/folders/create', () => { - test('フォルダを作成できる', async () => { - const res = await api('/drive/folders/create', { - name: 'test' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'test'); - })); - }); - - describe('drive/folders/update', () => { - test('名前を更新できる', async () => { - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await api('/drive/folders/update', { - folderId: folder.id, - name: 'new name' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'new name'); - })); - - test('他人のフォルダを更新できない', async () => { - const folder = (await api('/drive/folders/create', { - name: 'test' - }, bob)).body; - - const res = await api('/drive/folders/update', { - folderId: folder.id, - name: 'new name' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('親フォルダを更新できる', async () => { - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await api('/drive/folders/create', { - name: 'parent' - }, alice)).body; - - const res = await api('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.parentId, parentFolder.id); - })); - - test('親フォルダを無しに更新できる', async () => { - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await api('/drive/folders/create', { - name: 'parent' - }, alice)).body; - await api('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - const res = await api('/drive/folders/update', { - folderId: folder.id, - parentId: null - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.parentId, null); - })); - - test('他人のフォルダを親フォルダに設定できない', async () => { - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await api('/drive/folders/create', { - name: 'parent' - }, bob)).body; - - const res = await api('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('フォルダが循環するような構造にできない', async () => { - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await api('/drive/folders/create', { - name: 'parent' - }, alice)).body; - await api('/drive/folders/update', { - folderId: parentFolder.id, - parentId: folder.id - }, alice); - - const res = await api('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('フォルダが循環するような構造にできない(再帰的)', async () => { - const folderA = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - const folderB = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - const folderC = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - await api('/drive/folders/update', { - folderId: folderB.id, - parentId: folderA.id - }, alice); - await api('/drive/folders/update', { - folderId: folderC.id, - parentId: folderB.id - }, alice); - - const res = await api('/drive/folders/update', { - folderId: folderA.id, - parentId: folderC.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('フォルダが循環するような構造にできない(自身)', async () => { - const folderA = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await api('/drive/folders/update', { - folderId: folderA.id, - parentId: folderA.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('存在しない親フォルダを設定できない', async () => { - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await api('/drive/folders/update', { - folderId: folder.id, - parentId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('不正な親フォルダIDで怒られる', async () => { - const folder = (await api('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await api('/drive/folders/update', { - folderId: folder.id, - parentId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('存在しないフォルダを更新できない', async () => { - const res = await api('/drive/folders/update', { - folderId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - test('不正なフォルダIDで怒られる', async () => { - const res = await api('/drive/folders/update', { - folderId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('notes/replies', () => { - test('自分に閲覧権限のない投稿は含まれない', async () => { - const alicePost = await post(alice, { - text: 'foo' - }); - - await post(bob, { - replyId: alicePost.id, - text: 'bar', - visibility: 'specified', - visibleUserIds: [alice.id] - }); - - const res = await api('/notes/replies', { - noteId: alicePost.id - }, carol); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 0); - })); - }); - - describe('notes/timeline', () => { - test('フォロワー限定投稿が含まれる', async () => { - await api('/following/create', { - userId: alice.id - }, bob); - - const alicePost = await post(alice, { - text: 'foo', - visibility: 'followers' - }); - - const res = await api('/notes/timeline', {}, bob); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 1); - assert.strictEqual(res.body[0].id, alicePost.id); - })); - }); -}); -*/ diff --git a/packages/backend/test/_e2e/fetch-resource.ts b/packages/backend/test/_e2e/fetch-resource.ts deleted file mode 100644 index b8ba3f2477..0000000000 --- a/packages/backend/test/_e2e/fetch-resource.ts +++ /dev/null @@ -1,205 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import * as openapi from '@redocly/openapi-core'; -import { startServer, signup, post, request, simpleGet, port, shutdownServer } from '../utils.js'; - -// Request Accept -const ONLY_AP = 'application/activity+json'; -const PREFER_AP = 'application/activity+json, */*'; -const PREFER_HTML = 'text/html, */*'; -const UNSPECIFIED = '*/*'; - -// Response Content-Type -const AP = 'application/activity+json; charset=utf-8'; -const JSON = 'application/json; charset=utf-8'; -const HTML = 'text/html; charset=utf-8'; - -describe('Fetch resource', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let alicesPost: any; - - beforeAll(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - alicesPost = await post(alice, { - text: 'test', - }); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - describe('Common', () => { - test('meta', async () => { - const res = await request('/meta', { - }); - - assert.strictEqual(res.status, 200); - }); - - test('GET root', async () => { - const res = await simpleGet('/'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); - - test('GET docs', async () => { - const res = await simpleGet('/docs/ja-JP/about'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); - - test('GET api-doc', async () => { - const res = await simpleGet('/api-doc'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); - - test('GET api.json', async () => { - const res = await simpleGet('/api.json'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, JSON); - }); - - test('Validate api.json', async () => { - const config = await openapi.loadConfig(); - const result = await openapi.bundle({ - config, - ref: `http://localhost:${port}/api.json`, - }); - - for (const problem of result.problems) { - console.log(`${problem.message} - ${problem.location[0]?.pointer}`); - } - - assert.strictEqual(result.problems.length, 0); - }); - - test('GET favicon.ico', async () => { - const res = await simpleGet('/favicon.ico'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/x-icon'); - }); - - test('GET apple-touch-icon.png', async () => { - const res = await simpleGet('/apple-touch-icon.png'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/png'); - }); - - test('GET twemoji svg', async () => { - const res = await simpleGet('/twemoji/2764.svg'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/svg+xml'); - }); - - test('GET twemoji svg with hyphen', async () => { - const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg'); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'image/svg+xml'); - }); - }); - - describe('/@:username', () => { - test('Only AP => AP', async () => { - const res = await simpleGet(`/@${alice.username}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - }); - - test('Prefer AP => AP', async () => { - const res = await simpleGet(`/@${alice.username}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - }); - - test('Prefer HTML => HTML', async () => { - const res = await simpleGet(`/@${alice.username}`, PREFER_HTML); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); - - test('Unspecified => HTML', async () => { - const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); - }); - - describe('/users/:id', () => { - test('Only AP => AP', async () => { - const res = await simpleGet(`/users/${alice.id}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - }); - - test('Prefer AP => AP', async () => { - const res = await simpleGet(`/users/${alice.id}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - }); - - test('Prefer HTML => Redirect to /@:username', async () => { - const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML); - assert.strictEqual(res.status, 302); - assert.strictEqual(res.location, `/@${alice.username}`); - }); - - test('Undecided => HTML', async () => { - const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED); - assert.strictEqual(res.status, 302); - assert.strictEqual(res.location, `/@${alice.username}`); - }); - }); - - describe('/notes/:id', () => { - test('Only AP => AP', async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - }); - - test('Prefer AP => AP', async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - }); - - test('Prefer HTML => HTML', async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); - - test('Unspecified => HTML', async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - }); - }); - - describe('Feeds', () => { - test('RSS', async () => { - const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8'); - }); - - test('ATOM', async () => { - const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8'); - }); - - test('JSON', async () => { - const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, 'application/json; charset=utf-8'); - }); - }); -}); diff --git a/packages/backend/test/_e2e/ff-visibility.ts b/packages/backend/test/_e2e/ff-visibility.ts deleted file mode 100644 index 84a5b5ef28..0000000000 --- a/packages/backend/test/_e2e/ff-visibility.ts +++ /dev/null @@ -1,167 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, react, connectStream, startServer, shutdownServer, simpleGet } from '../utils.js'; - -describe('FF visibility', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let bob: any; - let carol: any; - - beforeAll(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { - await request('/i/update', { - ffVisibility: 'public', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - }); - - test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, alice); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, alice); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - }); - - test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 400); - assert.strictEqual(followersRes.status, 400); - }); - - test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - await request('/following/create', { - userId: alice.id, - }, bob); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - }); - - test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, alice); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, alice); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - }); - - test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await request('/users/following', { - userId: alice.id, - }, bob); - const followersRes = await request('/users/followers', { - userId: alice.id, - }, bob); - - assert.strictEqual(followingRes.status, 400); - assert.strictEqual(followersRes.status, 400); - }); - - describe('AP', () => { - test('ffVisibility が public 以外ならばAPからは取得できない', async () => { - { - await request('/i/update', { - ffVisibility: 'public', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(followersRes.status, 200); - } - { - await request('/i/update', { - ffVisibility: 'followers', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); - } - { - await request('/i/update', { - ffVisibility: 'private', - }, alice); - - const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json').catch(res => ({ status: res.statusCode })); - assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); - } - }); - }); -}); diff --git a/packages/backend/test/_e2e/mute.ts b/packages/backend/test/_e2e/mute.ts deleted file mode 100644 index 8f7f72bb97..0000000000 --- a/packages/backend/test/_e2e/mute.ts +++ /dev/null @@ -1,123 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, react, startServer, shutdownServer, waitFire } from '../utils.js'; - -describe('Mute', () => { - let p: childProcess.ChildProcess; - - // alice mutes carol - let alice: any; - let bob: any; - let carol: any; - - beforeAll(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - test('ミュート作成', async () => { - const res = await request('/mute/create', { - userId: carol.id, - }, alice); - - assert.strictEqual(res.status, 204); - }); - - test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => { - const bobNote = await post(bob, { text: '@alice hi' }); - const carolNote = await post(carol, { text: '@alice hi' }); - - const res = await request('/notes/mentions', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - }); - - test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - - await post(carol, { text: '@alice hi' }); - - const res = await request('/i', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.hasUnreadMentions, false); - }); - - test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - - const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); - - assert.strictEqual(fired, false); - }); - - test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - await request('/notifications/mark-all-as-read', {}, alice); - - const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); - - assert.strictEqual(fired, false); - }); - - describe('Timeline', () => { - test('タイムラインにミュートしているユーザーの投稿が含まれない', async () => { - const aliceNote = await post(alice); - const bobNote = await post(bob); - const carolNote = await post(carol); - - const res = await request('/notes/local-timeline', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - }); - - test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => { - const aliceNote = await post(alice); - const carolNote = await post(carol); - const bobNote = await post(bob, { - renoteId: carolNote.id, - }); - - const res = await request('/notes/local-timeline', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - }); - }); - - describe('Notification', () => { - test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => { - const aliceNote = await post(alice); - await react(bob, aliceNote, 'like'); - await react(carol, aliceNote, 'like'); - - const res = await request('/i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); - }); - }); -}); diff --git a/packages/backend/test/_e2e/note.ts b/packages/backend/test/_e2e/note.ts deleted file mode 100644 index 47af6808f6..0000000000 --- a/packages/backend/test/_e2e/note.ts +++ /dev/null @@ -1,370 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { Note } from '../../src/models/entities/note.js'; -import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from '../utils.js'; - -describe('Note', () => { - let p: childProcess.ChildProcess; - let Notes: any; - - let alice: any; - let bob: any; - - beforeAll(async () => { - p = await startServer(); - const connection = await initTestDb(true); - Notes = connection.getRepository(Note); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - test('投稿できる', async () => { - const post = { - text: 'test', - }; - - const res = await request('/notes/create', post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, post.text); - }); - - test('ファイルを添付できる', async () => { - const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - - const res = await request('/notes/create', { - fileIds: [file.id], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); - }, 1000 * 10); - - test('他人のファイルは無視', async () => { - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - - const res = await request('/notes/create', { - text: 'test', - fileIds: [file.id], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - }, 1000 * 10); - - test('存在しないファイルは無視', async () => { - const res = await request('/notes/create', { - text: 'test', - fileIds: ['000000000000000000000000'], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - }); - - test('不正なファイルIDは無視', async () => { - const res = await request('/notes/create', { - fileIds: ['kyoppie'], - }, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - }); - - test('返信できる', async () => { - const bobPost = await post(bob, { - text: 'foo', - }); - - const alicePost = { - text: 'bar', - replyId: bobPost.id, - }; - - const res = await request('/notes/create', alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, alicePost.text); - assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); - assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); - }); - - test('renoteできる', async () => { - const bobPost = await post(bob, { - text: 'test', - }); - - const alicePost = { - renoteId: bobPost.id, - }; - - const res = await request('/notes/create', alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); - }); - - test('引用renoteできる', async () => { - const bobPost = await post(bob, { - text: 'test', - }); - - const alicePost = { - text: 'test', - renoteId: bobPost.id, - }; - - const res = await request('/notes/create', alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, alicePost.text); - assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); - }); - - test('文字数ぎりぎりで怒られない', async () => { - const post = { - text: '!'.repeat(3000), - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 200); - }); - - test('文字数オーバーで怒られる', async () => { - const post = { - text: '!'.repeat(3001), - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - }); - - test('存在しないリプライ先で怒られる', async () => { - const post = { - text: 'test', - replyId: '000000000000000000000000', - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - }); - - test('存在しないrenote対象で怒られる', async () => { - const post = { - renoteId: '000000000000000000000000', - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - }); - - test('不正なリプライ先IDで怒られる', async () => { - const post = { - text: 'test', - replyId: 'foo', - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - }); - - test('不正なrenote対象IDで怒られる', async () => { - const post = { - renoteId: 'foo', - }; - const res = await request('/notes/create', post, alice); - assert.strictEqual(res.status, 400); - }); - - test('存在しないユーザーにメンションできる', async () => { - const post = { - text: '@ghost yo', - }; - - const res = await request('/notes/create', post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, post.text); - }); - - test('同じユーザーに複数メンションしても内部的にまとめられる', async () => { - const post = { - text: '@bob @bob @bob yo', - }; - - const res = await request('/notes/create', post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.text, post.text); - - const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); - assert.deepStrictEqual(noteDoc.mentions, [bob.id]); - }); - - describe('notes/create', () => { - test('投票を添付できる', async () => { - const res = await request('/notes/create', { - text: 'test', - poll: { - choices: ['foo', 'bar'], - }, - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.createdNote.poll != null, true); - }); - - test('投票の選択肢が無くて怒られる', async () => { - const res = await request('/notes/create', { - poll: {}, - }, alice); - assert.strictEqual(res.status, 400); - }); - - test('投票の選択肢が無くて怒られる (空の配列)', async () => { - const res = await request('/notes/create', { - poll: { - choices: [], - }, - }, alice); - assert.strictEqual(res.status, 400); - }); - - test('投票の選択肢が1つで怒られる', async () => { - const res = await request('/notes/create', { - poll: { - choices: ['Strawberry Pasta'], - }, - }, alice); - assert.strictEqual(res.status, 400); - }); - - test('投票できる', async () => { - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - }, - }, alice); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1, - }, alice); - - assert.strictEqual(res.status, 204); - }); - - test('複数投票できない', async () => { - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - }, - }, alice); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 0, - }, alice); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 2, - }, alice); - - assert.strictEqual(res.status, 400); - }); - - test('許可されている場合は複数投票できる', async () => { - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - multiple: true, - }, - }, alice); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 0, - }, alice); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1, - }, alice); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 2, - }, alice); - - assert.strictEqual(res.status, 204); - }); - - test('締め切られている場合は投票できない', async () => { - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - expiredAfter: 1, - }, - }, alice); - - await new Promise(x => setTimeout(x, 2)); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1, - }, alice); - - assert.strictEqual(res.status, 400); - }); - }); - - describe('notes/delete', () => { - test('delete a reply', async () => { - const mainNoteRes = await api('notes/create', { - text: 'main post', - }, alice); - const replyOneRes = await api('notes/create', { - text: 'reply one', - replyId: mainNoteRes.body.createdNote.id, - }, alice); - const replyTwoRes = await api('notes/create', { - text: 'reply two', - replyId: mainNoteRes.body.createdNote.id, - }, alice); - - const deleteOneRes = await api('notes/delete', { - noteId: replyOneRes.body.createdNote.id, - }, alice); - - assert.strictEqual(deleteOneRes.status, 204); - let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.strictEqual(mainNote.repliesCount, 1); - - const deleteTwoRes = await api('notes/delete', { - noteId: replyTwoRes.body.createdNote.id, - }, alice); - - assert.strictEqual(deleteTwoRes.status, 204); - mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.strictEqual(mainNote.repliesCount, 0); - }); - }); -}); diff --git a/packages/backend/test/_e2e/streaming.ts b/packages/backend/test/_e2e/streaming.ts deleted file mode 100644 index 790451d9b4..0000000000 --- a/packages/backend/test/_e2e/streaming.ts +++ /dev/null @@ -1,545 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { Following } from '../../src/models/entities/following.js'; -import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from '../utils.js'; - -describe('Streaming', () => { - let p: childProcess.ChildProcess; - let Followings: any; - - const follow = async (follower: any, followee: any) => { - await Followings.save({ - id: 'a', - createdAt: new Date(), - followerId: follower.id, - followeeId: followee.id, - followerHost: follower.host, - followerInbox: null, - followerSharedInbox: null, - followeeHost: followee.host, - followeeInbox: null, - followeeSharedInbox: null, - }); - }; - - describe('Streaming', () => { - // Local users - let ayano: any; - let kyoko: any; - let chitose: any; - - // Remote users - let akari: any; - let chinatsu: any; - - let kyokoNote: any; - let list: any; - - beforeAll(async () => { - p = await startServer(); - const connection = await initTestDb(true); - Followings = connection.getRepository(Following); - - ayano = await signup({ username: 'ayano' }); - kyoko = await signup({ username: 'kyoko' }); - chitose = await signup({ username: 'chitose' }); - - akari = await signup({ username: 'akari', host: 'example.com' }); - chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); - - kyokoNote = await post(kyoko, { text: 'foo' }); - - // Follow: ayano => kyoko - await api('following/create', { userId: kyoko.id }, ayano); - - // Follow: ayano => akari - await follow(ayano, akari); - - // List: chitose => ayano, kyoko - list = await api('users/lists/create', { - name: 'my list', - }, chitose).then(x => x.body); - - await api('users/lists/push', { - listId: list.id, - userId: ayano.id, - }, chitose); - - await api('users/lists/push', { - listId: list.id, - userId: kyoko.id, - }, chitose); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - describe('Events', () => { - test('mention event', async () => { - const fired = await waitFire( - kyoko, 'main', // kyoko:main - () => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko - msg => msg.type === 'mention' && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, true); - }); - - test('renote event', async () => { - const fired = await waitFire( - kyoko, 'main', // kyoko:main - () => post(ayano, { renoteId: kyokoNote.id }), // ayano renote - msg => msg.type === 'renote' && msg.body.renoteId === kyokoNote.id, // wait renote - ); - - assert.strictEqual(fired, true); - }); - }); - - describe('Home Timeline', () => { - test('自分の投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:Home - () => api('notes/create', { text: 'foo' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.text === 'foo', - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしているユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしていないユーザーの投稿は流れない', async () => { - const fired = await waitFire( - kyoko, 'homeTimeline', // kyoko:home - () => api('notes/create', { text: 'foo' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, false); - }); - - test('フォローしているユーザーのダイレクト投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko dm => ayano - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'homeTimeline', // ayano:home - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), // kyoko dm => chitose - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - }); // Home - - describe('Local Timeline', () => { - test('自分の投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.text === 'foo', - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしていないローカルユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo' }, chitose), // chitose posts - msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - test('リモートユーザーの投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts - msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu - ); - - assert.strictEqual(fired, false); - }); - - test('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo' }, akari), // akari posts - msg => msg.type === 'note' && msg.body.userId === akari.id, // wait akari - ); - - assert.strictEqual(fired, false); - }); - - test('ホーム指定の投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - test('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'localTimeline', // ayano:Local - () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), - msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, false); - }); - }); - - describe('Hybrid Timeline', () => { - test('自分の投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo' }, ayano), // ayano posts - msg => msg.type === 'note' && msg.body.text === 'foo', - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしていないローカルユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo' }, chitose), // chitose posts - msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしているリモートユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo' }, akari), // akari posts - msg => msg.type === 'note' && msg.body.userId === akari.id, // wait akari - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしていないリモートユーザーの投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts - msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu - ); - - assert.strictEqual(fired, false); - }); - - test('フォローしているユーザーのダイレクト投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしているユーザーのホーム投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしていないローカルユーザーのホーム投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'home' }, chitose), - msg => msg.type === 'note' && msg.body.userId === chitose.id, - ); - - assert.strictEqual(fired, false); - }); - - test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'hybridTimeline', // ayano:Hybrid - () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), - msg => msg.type === 'note' && msg.body.userId === chitose.id, - ); - - assert.strictEqual(fired, false); - }); - }); - - describe('Global Timeline', () => { - test('フォローしていないローカルユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'globalTimeline', // ayano:Global - () => api('notes/create', { text: 'foo' }, chitose), // chitose posts - msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - test('フォローしていないリモートユーザーの投稿が流れる', async () => { - const fired = await waitFire( - ayano, 'globalTimeline', // ayano:Global - () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts - msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu - ); - - assert.strictEqual(fired, true); - }); - - test('ホーム投稿は流れない', async () => { - const fired = await waitFire( - ayano, 'globalTimeline', // ayano:Global - () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - }); - - describe('UserList Timeline', () => { - test('リストに入れているユーザーの投稿が流れる', async () => { - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo' }, ayano), - msg => msg.type === 'note' && msg.body.userId === ayano.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, true); - }); - - test('リストに入れていないユーザーの投稿は流れない', async () => { - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo' }, chinatsu), - msg => msg.type === 'note' && msg.body.userId === chinatsu.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - - // #4471 - test('リストに入れているユーザーのダイレクト投稿が流れる', async () => { - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano), - msg => msg.type === 'note' && msg.body.userId === ayano.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, true); - }); - - // #4335 - test('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', async () => { - const fired = await waitFire( - chitose, 'userList', - () => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), - msg => msg.type === 'note' && msg.body.userId === kyoko.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - }); - - describe('Hashtag Timeline', () => { - test('指定したハッシュタグの投稿が流れる', () => new Promise(async done => { - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - assert.deepStrictEqual(body.text, '#foo'); - ws.close(); - done(); - } - }, { - q: [ - ['foo'], - ], - }); - - post(chitose, { - text: '#foo', - }); - })); - - test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - } - }, { - q: [ - ['foo', 'bar'], - ], - }); - - post(chitose, { - text: '#foo', - }); - - post(chitose, { - text: '#bar', - }); - - post(chitose, { - text: '#foo #bar', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - ws.close(); - done(); - }, 3000); - })); - - test('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - let piyoCount = 0; - - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - if (body.text === '#piyo') piyoCount++; - } - }, { - q: [ - ['foo'], - ['bar'], - ], - }); - - post(chitose, { - text: '#foo', - }); - - post(chitose, { - text: '#bar', - }); - - post(chitose, { - text: '#foo #bar', - }); - - post(chitose, { - text: '#piyo', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 1); - assert.strictEqual(barCount, 1); - assert.strictEqual(fooBarCount, 1); - assert.strictEqual(piyoCount, 0); - ws.close(); - done(); - }, 3000); - })); - - test('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - let piyoCount = 0; - let waaaCount = 0; - - const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { - if (type === 'note') { - if (body.text === '#foo') fooCount++; - if (body.text === '#bar') barCount++; - if (body.text === '#foo #bar') fooBarCount++; - if (body.text === '#piyo') piyoCount++; - if (body.text === '#waaa') waaaCount++; - } - }, { - q: [ - ['foo', 'bar'], - ['piyo'], - ], - }); - - post(chitose, { - text: '#foo', - }); - - post(chitose, { - text: '#bar', - }); - - post(chitose, { - text: '#foo #bar', - }); - - post(chitose, { - text: '#piyo', - }); - - post(chitose, { - text: '#waaa', - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - assert.strictEqual(piyoCount, 1); - assert.strictEqual(waaaCount, 0); - ws.close(); - done(); - }, 3000); - })); - }); - }); -}); diff --git a/packages/backend/test/_e2e/thread-mute.ts b/packages/backend/test/_e2e/thread-mute.ts deleted file mode 100644 index 890b52a8c1..0000000000 --- a/packages/backend/test/_e2e/thread-mute.ts +++ /dev/null @@ -1,103 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, react, connectStream, startServer, shutdownServer } from '../utils.js'; - -describe('Note thread mute', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let bob: any; - let carol: any; - - beforeAll(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }, 1000 * 30); - - afterAll(async () => { - await shutdownServer(p); - }); - - test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => { - const bobNote = await post(bob, { text: '@alice @carol root note' }); - const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); - - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - - const res = await request('/notes/mentions', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); - }); - - test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - - const bobNote = await post(bob, { text: '@alice @carol root note' }); - - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - - const res = await request('/i', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.hasUnreadMentions, false); - }); - - test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { - // 状態リセット - await request('/i/read-all-unread-notes', {}, alice); - - const bobNote = await post(bob, { text: '@alice @carol root note' }); - - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); - - let fired = false; - - const ws = await connectStream(alice, 'main', async ({ type, body }) => { - if (type === 'unreadMention') { - if (body === bobNote.id) return; - fired = true; - } - }); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - - setTimeout(() => { - assert.strictEqual(fired, false); - ws.close(); - done(); - }, 5000); - })); - - test('i/notifications にミュートしているスレッドの通知が含まれない', async () => { - const bobNote = await post(bob, { text: '@alice @carol root note' }); - const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); - - await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); - - const res = await request('/i/notifications', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); - assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); - - // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい - }); -}); diff --git a/packages/backend/test/_e2e/user-notes.ts b/packages/backend/test/_e2e/user-notes.ts deleted file mode 100644 index a6cc1057f9..0000000000 --- a/packages/backend/test/_e2e/user-notes.ts +++ /dev/null @@ -1,61 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { signup, request, post, uploadUrl, startServer, shutdownServer } from '../utils.js'; - -describe('users/notes', () => { - let p: childProcess.ChildProcess; - - let alice: any; - let jpgNote: any; - let pngNote: any; - let jpgPngNote: any; - - beforeAll(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); - jpgNote = await post(alice, { - fileIds: [jpg.id], - }); - pngNote = await post(alice, { - fileIds: [png.id], - }); - jpgPngNote = await post(alice, { - fileIds: [jpg.id, png.id], - }); - }, 1000 * 30); - - afterAll(async() => { - await shutdownServer(p); - }); - - test('ファイルタイプ指定 (jpg)', async () => { - const res = await request('/users/notes', { - userId: alice.id, - fileType: ['image/jpeg'], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 2); - assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); - }); - - test('ファイルタイプ指定 (jpg or png)', async () => { - const res = await request('/users/notes', { - userId: alice.id, - fileType: ['image/jpeg', 'image/png'], - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 3); - assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); - }); -}); diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts new file mode 100644 index 0000000000..4e162f42d0 --- /dev/null +++ b/packages/backend/test/e2e/api-visibility.ts @@ -0,0 +1,477 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, post, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('API visibility', () => { + let p: INestApplicationContext; + + beforeAll(async () => { + p = await startServer(); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + describe('Note visibility', () => { + //#region vars + /** ヒロイン */ + let alice: any; + /** フォロワー */ + let follower: any; + /** 非フォロワー */ + let other: any; + /** 非フォロワーでもリプライやメンションをされた人 */ + let target: any; + /** specified mentionでmentionを飛ばされる人 */ + let target2: any; + + /** public-post */ + let pub: any; + /** home-post */ + let home: any; + /** followers-post */ + let fol: any; + /** specified-post */ + let spe: any; + + /** public-reply to target's post */ + let pubR: any; + /** home-reply to target's post */ + let homeR: any; + /** followers-reply to target's post */ + let folR: any; + /** specified-reply to target's post */ + let speR: any; + + /** public-mention to target */ + let pubM: any; + /** home-mention to target */ + let homeM: any; + /** followers-mention to target */ + let folM: any; + /** specified-mention to target */ + let speM: any; + + /** reply target post */ + let tgt: any; + //#endregion + + const show = async (noteId: any, by: any) => { + return await api('/notes/show', { + noteId, + }, by); + }; + + beforeAll(async () => { + //#region prepare + // signup + alice = await signup({ username: 'alice' }); + follower = await signup({ username: 'follower' }); + other = await signup({ username: 'other' }); + target = await signup({ username: 'target' }); + target2 = await signup({ username: 'target2' }); + + // follow alice <= follower + await api('/following/create', { userId: alice.id }, follower); + + // normal posts + pub = await post(alice, { text: 'x', visibility: 'public' }); + home = await post(alice, { text: 'x', visibility: 'home' }); + fol = await post(alice, { text: 'x', visibility: 'followers' }); + spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); + + // replies + tgt = await post(target, { text: 'y', visibility: 'public' }); + pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' }); + homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); + folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); + speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); + + // mentions + pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); + homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); + folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); + speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' }); + //#endregion + }); + + //#region show post + // public + test('[show] public-postを自分が見れる', async () => { + const res = await show(pub.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] public-postをフォロワーが見れる', async () => { + const res = await show(pub.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] public-postを非フォロワーが見れる', async () => { + const res = await show(pub.id, other); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] public-postを未認証が見れる', async () => { + const res = await show(pub.id, null); + assert.strictEqual(res.body.text, 'x'); + }); + + // home + test('[show] home-postを自分が見れる', async () => { + const res = await show(home.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] home-postをフォロワーが見れる', async () => { + const res = await show(home.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] home-postを非フォロワーが見れる', async () => { + const res = await show(home.id, other); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] home-postを未認証が見れる', async () => { + const res = await show(home.id, null); + assert.strictEqual(res.body.text, 'x'); + }); + + // followers + test('[show] followers-postを自分が見れる', async () => { + const res = await show(fol.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] followers-postをフォロワーが見れる', async () => { + const res = await show(fol.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] followers-postを非フォロワーが見れない', async () => { + const res = await show(fol.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] followers-postを未認証が見れない', async () => { + const res = await show(fol.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + + // specified + test('[show] specified-postを自分が見れる', async () => { + const res = await show(spe.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] specified-postを指定ユーザーが見れる', async () => { + const res = await show(spe.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] specified-postをフォロワーが見れない', async () => { + const res = await show(spe.id, follower); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] specified-postを非フォロワーが見れない', async () => { + const res = await show(spe.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] specified-postを未認証が見れない', async () => { + const res = await show(spe.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + //#endregion + + //#region show reply + // public + test('[show] public-replyを自分が見れる', async () => { + const res = await show(pubR.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] public-replyをされた人が見れる', async () => { + const res = await show(pubR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] public-replyをフォロワーが見れる', async () => { + const res = await show(pubR.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] public-replyを非フォロワーが見れる', async () => { + const res = await show(pubR.id, other); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] public-replyを未認証が見れる', async () => { + const res = await show(pubR.id, null); + assert.strictEqual(res.body.text, 'x'); + }); + + // home + test('[show] home-replyを自分が見れる', async () => { + const res = await show(homeR.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] home-replyをされた人が見れる', async () => { + const res = await show(homeR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] home-replyをフォロワーが見れる', async () => { + const res = await show(homeR.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] home-replyを非フォロワーが見れる', async () => { + const res = await show(homeR.id, other); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] home-replyを未認証が見れる', async () => { + const res = await show(homeR.id, null); + assert.strictEqual(res.body.text, 'x'); + }); + + // followers + test('[show] followers-replyを自分が見れる', async () => { + const res = await show(folR.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async () => { + const res = await show(folR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] followers-replyをフォロワーが見れる', async () => { + const res = await show(folR.id, follower); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] followers-replyを非フォロワーが見れない', async () => { + const res = await show(folR.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] followers-replyを未認証が見れない', async () => { + const res = await show(folR.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + + // specified + test('[show] specified-replyを自分が見れる', async () => { + const res = await show(speR.id, alice); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] specified-replyを指定ユーザーが見れる', async () => { + const res = await show(speR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] specified-replyをされた人が指定されてなくても見れる', async () => { + const res = await show(speR.id, target); + assert.strictEqual(res.body.text, 'x'); + }); + + test('[show] specified-replyをフォロワーが見れない', async () => { + const res = await show(speR.id, follower); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] specified-replyを非フォロワーが見れない', async () => { + const res = await show(speR.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] specified-replyを未認証が見れない', async () => { + const res = await show(speR.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + //#endregion + + //#region show mention + // public + test('[show] public-mentionを自分が見れる', async () => { + const res = await show(pubM.id, alice); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] public-mentionをされた人が見れる', async () => { + const res = await show(pubM.id, target); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] public-mentionをフォロワーが見れる', async () => { + const res = await show(pubM.id, follower); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] public-mentionを非フォロワーが見れる', async () => { + const res = await show(pubM.id, other); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] public-mentionを未認証が見れる', async () => { + const res = await show(pubM.id, null); + assert.strictEqual(res.body.text, '@target x'); + }); + + // home + test('[show] home-mentionを自分が見れる', async () => { + const res = await show(homeM.id, alice); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] home-mentionをされた人が見れる', async () => { + const res = await show(homeM.id, target); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] home-mentionをフォロワーが見れる', async () => { + const res = await show(homeM.id, follower); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] home-mentionを非フォロワーが見れる', async () => { + const res = await show(homeM.id, other); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] home-mentionを未認証が見れる', async () => { + const res = await show(homeM.id, null); + assert.strictEqual(res.body.text, '@target x'); + }); + + // followers + test('[show] followers-mentionを自分が見れる', async () => { + const res = await show(folM.id, alice); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async () => { + const res = await show(folM.id, target); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] followers-mentionをフォロワーが見れる', async () => { + const res = await show(folM.id, follower); + assert.strictEqual(res.body.text, '@target x'); + }); + + test('[show] followers-mentionを非フォロワーが見れない', async () => { + const res = await show(folM.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] followers-mentionを未認証が見れない', async () => { + const res = await show(folM.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + + // specified + test('[show] specified-mentionを自分が見れる', async () => { + const res = await show(speM.id, alice); + assert.strictEqual(res.body.text, '@target2 x'); + }); + + test('[show] specified-mentionを指定ユーザーが見れる', async () => { + const res = await show(speM.id, target); + assert.strictEqual(res.body.text, '@target2 x'); + }); + + test('[show] specified-mentionをされた人が指定されてなかったら見れない', async () => { + const res = await show(speM.id, target2); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] specified-mentionをフォロワーが見れない', async () => { + const res = await show(speM.id, follower); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] specified-mentionを非フォロワーが見れない', async () => { + const res = await show(speM.id, other); + assert.strictEqual(res.body.isHidden, true); + }); + + test('[show] specified-mentionを未認証が見れない', async () => { + const res = await show(speM.id, null); + assert.strictEqual(res.body.isHidden, true); + }); + //#endregion + + //#region HTL + test('[HTL] public-post が 自分が見れる', async () => { + const res = await api('/notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === pub.id); + assert.strictEqual(notes[0].text, 'x'); + }); + + test('[HTL] public-post が 非フォロワーから見れない', async () => { + const res = await api('/notes/timeline', { limit: 100 }, other); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === pub.id); + assert.strictEqual(notes.length, 0); + }); + + test('[HTL] followers-post が フォロワーから見れる', async () => { + const res = await api('/notes/timeline', { limit: 100 }, follower); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === fol.id); + assert.strictEqual(notes[0].text, 'x'); + }); + //#endregion + + //#region RTL + test('[replies] followers-reply が フォロワーから見れる', async () => { + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folR.id); + assert.strictEqual(notes[0].text, 'x'); + }); + + test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => { + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folR.id); + assert.strictEqual(notes.length, 0); + }); + + test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { + const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folR.id); + assert.strictEqual(notes[0].text, 'x'); + }); + //#endregion + + //#region MTL + test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => { + const res = await api('/notes/mentions', { limit: 100 }, target); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folR.id); + assert.strictEqual(notes[0].text, 'x'); + }); + + test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => { + const res = await api('/notes/mentions', { limit: 100 }, target); + assert.strictEqual(res.status, 200); + const notes = res.body.filter((n: any) => n.id === folM.id); + assert.strictEqual(notes[0].text, '@target x'); + }); + //#endregion + }); +}); + diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts new file mode 100644 index 0000000000..6ceccf66eb --- /dev/null +++ b/packages/backend/test/e2e/api.ts @@ -0,0 +1,83 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('API', () => { + let p: INestApplicationContext; + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + describe('General validation', () => { + test('wrong type', async () => { + const res = await api('/test', { + required: true, + string: 42, + }); + assert.strictEqual(res.status, 400); + }); + + test('missing require param', async () => { + const res = await api('/test', { + string: 'a', + }); + assert.strictEqual(res.status, 400); + }); + + test('invalid misskey:id (empty string)', async () => { + const res = await api('/test', { + required: true, + id: '', + }); + assert.strictEqual(res.status, 400); + }); + + test('valid misskey:id', async () => { + const res = await api('/test', { + required: true, + id: '8wvhjghbxu', + }); + assert.strictEqual(res.status, 200); + }); + + test('default value', async () => { + const res = await api('/test', { + required: true, + string: 'a', + }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.default, 'hello'); + }); + + test('can set null even if it has default value', async () => { + const res = await api('/test', { + required: true, + nullableDefault: null, + }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.nullableDefault, null); + }); + + test('cannot set undefined if it has default value', async () => { + const res = await api('/test', { + required: true, + nullableDefault: undefined, + }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.nullableDefault, 'hello'); + }); + }); +}); diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts new file mode 100644 index 0000000000..4e9030f85d --- /dev/null +++ b/packages/backend/test/e2e/block.ts @@ -0,0 +1,85 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, post, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('Block', () => { + let p: INestApplicationContext; + + // alice blocks bob + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + test('Block作成', async () => { + const res = await api('/blocking/create', { + userId: bob.id, + }, alice); + + assert.strictEqual(res.status, 200); + }); + + test('ブロックされているユーザーをフォローできない', async () => { + const res = await api('/following/create', { userId: alice.id }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); + }); + + test('ブロックされているユーザーにリアクションできない', async () => { + const note = await post(alice, { text: 'hello' }); + + const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); + }); + + test('ブロックされているユーザーに返信できない', async () => { + const note = await post(alice, { text: 'hello' }); + + const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + }); + + test('ブロックされているユーザーのノートをRenoteできない', async () => { + const note = await post(alice, { text: 'hello' }); + + const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + }); + + // TODO: ユーザーリストに入れられないテスト + + // TODO: ユーザーリストから除外されるテスト + + test('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => { + const aliceNote = await post(alice); + const bobNote = await post(bob); + const carolNote = await post(carol); + + const res = await api('/notes/local-timeline', {}, bob); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + }); +}); diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts new file mode 100644 index 0000000000..e864eab6cb --- /dev/null +++ b/packages/backend/test/e2e/endpoints.ts @@ -0,0 +1,797 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +// node-fetch only supports it's own Blob yet +// https://github.com/node-fetch/node-fetch/pull/1664 +import { Blob } from 'node-fetch'; +import { startServer, signup, post, api, uploadFile } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('Endpoints', () => { + let p: INestApplicationContext; + + let alice: any; + let bob: any; + let carol: any; + let dave: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + dave = await signup({ username: 'dave' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + describe('signup', () => { + test('不正なユーザー名でアカウントが作成できない', async () => { + const res = await api('signup', { + username: 'test.', + password: 'test', + }); + assert.strictEqual(res.status, 400); + }); + + test('空のパスワードでアカウントが作成できない', async () => { + const res = await api('signup', { + username: 'test', + password: '', + }); + assert.strictEqual(res.status, 400); + }); + + test('正しくアカウントが作成できる', async () => { + const me = { + username: 'test1', + password: 'test1', + }; + + const res = await api('signup', me); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.username, me.username); + }); + + test('同じユーザー名のアカウントは作成できない', async () => { + const res = await api('signup', { + username: 'test1', + password: 'test1', + }); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('signin', () => { + test('間違ったパスワードでサインインできない', async () => { + const res = await api('signin', { + username: 'test1', + password: 'bar', + }); + + assert.strictEqual(res.status, 403); + }); + + test('クエリをインジェクションできない', async () => { + const res = await api('signin', { + username: 'test1', + password: { + $gt: '', + }, + }); + + assert.strictEqual(res.status, 400); + }); + + test('正しい情報でサインインできる', async () => { + const res = await api('signin', { + username: 'test1', + password: 'test1', + }); + + assert.strictEqual(res.status, 200); + }); + }); + + describe('i/update', () => { + test('アカウント設定を更新できる', async () => { + const myName = '大室櫻子'; + const myLocation = '七森中'; + const myBirthday = '2000-09-07'; + + const res = await api('/i/update', { + name: myName, + location: myLocation, + birthday: myBirthday, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, myName); + assert.strictEqual(res.body.location, myLocation); + assert.strictEqual(res.body.birthday, myBirthday); + }); + + test('名前を空白にできる', async () => { + const res = await api('/i/update', { + name: ' ', + }, alice); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.name, ' '); + }); + + test('誕生日の設定を削除できる', async () => { + await api('/i/update', { + birthday: '2000-09-07', + }, alice); + + const res = await api('/i/update', { + birthday: null, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.birthday, null); + }); + + test('不正な誕生日の形式で怒られる', async () => { + const res = await api('/i/update', { + birthday: '2000/09/07', + }, alice); + assert.strictEqual(res.status, 400); + }); + }); + + describe('users/show', () => { + test('ユーザーが取得できる', async () => { + const res = await api('/users/show', { + userId: alice.id, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.id, alice.id); + }); + + test('ユーザーが存在しなかったら怒る', async () => { + const res = await api('/users/show', { + userId: '000000000000000000000000', + }); + assert.strictEqual(res.status, 400); + }); + + test('間違ったIDで怒られる', async () => { + const res = await api('/users/show', { + userId: 'kyoppie', + }); + assert.strictEqual(res.status, 400); + }); + }); + + describe('notes/show', () => { + test('投稿が取得できる', async () => { + const myPost = await post(alice, { + text: 'test', + }); + + const res = await api('/notes/show', { + noteId: myPost.id, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.id, myPost.id); + assert.strictEqual(res.body.text, myPost.text); + }); + + test('投稿が存在しなかったら怒る', async () => { + const res = await api('/notes/show', { + noteId: '000000000000000000000000', + }); + assert.strictEqual(res.status, 400); + }); + + test('間違ったIDで怒られる', async () => { + const res = await api('/notes/show', { + noteId: 'kyoppie', + }); + assert.strictEqual(res.status, 400); + }); + }); + + describe('notes/reactions/create', () => { + test('リアクションできる', async () => { + const bobPost = await post(bob); + + const res = await api('/notes/reactions/create', { + noteId: bobPost.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 204); + + const resNote = await api('/notes/show', { + noteId: bobPost.id, + }, alice); + + assert.strictEqual(resNote.status, 200); + assert.strictEqual(resNote.body.reactions['🚀'], 1); + }); + + test('自分の投稿にもリアクションできる', async () => { + const myPost = await post(alice); + + const res = await api('/notes/reactions/create', { + noteId: myPost.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 204); + }); + + test('二重にリアクションすると上書きされる', async () => { + const bobPost = await post(bob); + + await api('/notes/reactions/create', { + noteId: bobPost.id, + reaction: '🥰', + }, alice); + + const res = await api('/notes/reactions/create', { + noteId: bobPost.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 204); + + const resNote = await api('/notes/show', { + noteId: bobPost.id, + }, alice); + + assert.strictEqual(resNote.status, 200); + assert.deepStrictEqual(resNote.body.reactions, { '🚀': 1 }); + }); + + test('存在しない投稿にはリアクションできない', async () => { + const res = await api('/notes/reactions/create', { + noteId: '000000000000000000000000', + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('空のパラメータで怒られる', async () => { + const res = await api('/notes/reactions/create', {}, alice); + + assert.strictEqual(res.status, 400); + }); + + test('間違ったIDで怒られる', async () => { + const res = await api('/notes/reactions/create', { + noteId: 'kyoppie', + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('following/create', () => { + test('フォローできる', async () => { + const res = await api('/following/create', { + userId: alice.id, + }, bob); + + assert.strictEqual(res.status, 200); + }); + + test('既にフォローしている場合は怒る', async () => { + const res = await api('/following/create', { + userId: alice.id, + }, bob); + + assert.strictEqual(res.status, 400); + }); + + test('存在しないユーザーはフォローできない', async () => { + const res = await api('/following/create', { + userId: '000000000000000000000000', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('自分自身はフォローできない', async () => { + const res = await api('/following/create', { + userId: alice.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('空のパラメータで怒られる', async () => { + const res = await api('/following/create', {}, alice); + + assert.strictEqual(res.status, 400); + }); + + test('間違ったIDで怒られる', async () => { + const res = await api('/following/create', { + userId: 'foo', + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('following/delete', () => { + test('フォロー解除できる', async () => { + await api('/following/create', { + userId: alice.id, + }, bob); + + const res = await api('/following/delete', { + userId: alice.id, + }, bob); + + assert.strictEqual(res.status, 200); + }); + + test('フォローしていない場合は怒る', async () => { + const res = await api('/following/delete', { + userId: alice.id, + }, bob); + + assert.strictEqual(res.status, 400); + }); + + test('存在しないユーザーはフォロー解除できない', async () => { + const res = await api('/following/delete', { + userId: '000000000000000000000000', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('自分自身はフォロー解除できない', async () => { + const res = await api('/following/delete', { + userId: alice.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('空のパラメータで怒られる', async () => { + const res = await api('/following/delete', {}, alice); + + assert.strictEqual(res.status, 400); + }); + + test('間違ったIDで怒られる', async () => { + const res = await api('/following/delete', { + userId: 'kyoppie', + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('drive', () => { + test('ドライブ情報を取得できる', async () => { + await uploadFile(alice, { + blob: new Blob([new Uint8Array(256)]), + }); + await uploadFile(alice, { + blob: new Blob([new Uint8Array(512)]), + }); + await uploadFile(alice, { + blob: new Blob([new Uint8Array(1024)]), + }); + const res = await api('/drive', {}, alice); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + expect(res.body).toHaveProperty('usage', 1792); + }); + }); + + describe('drive/files/create', () => { + test('ファイルを作成できる', async () => { + const res = await uploadFile(alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'Lenna.jpg'); + }); + + test('ファイルに名前を付けられる', async () => { + const res = await uploadFile(alice, { name: 'Belmond.png' }); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'Belmond.png'); + }); + + test('ファイル無しで怒られる', async () => { + const res = await api('/drive/files/create', {}, alice); + + assert.strictEqual(res.status, 400); + }); + + test('SVGファイルを作成できる', async () => { + const res = await uploadFile(alice, { path: 'image.svg' }); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'image.svg'); + assert.strictEqual(res.body.type, 'image/svg+xml'); + }); + }); + + describe('drive/files/update', () => { + test('名前を更新できる', async () => { + const file = (await uploadFile(alice)).body; + const newName = 'いちごパスタ.png'; + + const res = await api('/drive/files/update', { + fileId: file.id, + name: newName, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, newName); + }); + + test('他人のファイルは更新できない', async () => { + const file = (await uploadFile(alice)).body; + + const res = await api('/drive/files/update', { + fileId: file.id, + name: 'いちごパスタ.png', + }, bob); + + assert.strictEqual(res.status, 400); + }); + + test('親フォルダを更新できる', async () => { + const file = (await uploadFile(alice)).body; + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: folder.id, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.folderId, folder.id); + }); + + test('親フォルダを無しにできる', async () => { + const file = (await uploadFile(alice)).body; + + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + + await api('/drive/files/update', { + fileId: file.id, + folderId: folder.id, + }, alice); + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: null, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.folderId, null); + }); + + test('他人のフォルダには入れられない', async () => { + const file = (await uploadFile(alice)).body; + const folder = (await api('/drive/folders/create', { + name: 'test', + }, bob)).body; + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: folder.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('存在しないフォルダで怒られる', async () => { + const file = (await uploadFile(alice)).body; + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: '000000000000000000000000', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('不正なフォルダIDで怒られる', async () => { + const file = (await uploadFile(alice)).body; + + const res = await api('/drive/files/update', { + fileId: file.id, + folderId: 'foo', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('ファイルが存在しなかったら怒る', async () => { + const res = await api('/drive/files/update', { + fileId: '000000000000000000000000', + name: 'いちごパスタ.png', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('間違ったIDで怒られる', async () => { + const res = await api('/drive/files/update', { + fileId: 'kyoppie', + name: 'いちごパスタ.png', + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('drive/folders/create', () => { + test('フォルダを作成できる', async () => { + const res = await api('/drive/folders/create', { + name: 'test', + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'test'); + }); + }); + + describe('drive/folders/update', () => { + test('名前を更新できる', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + name: 'new name', + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'new name'); + }); + + test('他人のフォルダを更新できない', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test', + }, bob)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + name: 'new name', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('親フォルダを更新できる', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + const parentFolder = (await api('/drive/folders/create', { + name: 'parent', + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: parentFolder.id, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.parentId, parentFolder.id); + }); + + test('親フォルダを無しに更新できる', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + const parentFolder = (await api('/drive/folders/create', { + name: 'parent', + }, alice)).body; + await api('/drive/folders/update', { + folderId: folder.id, + parentId: parentFolder.id, + }, alice); + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: null, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.parentId, null); + }); + + test('他人のフォルダを親フォルダに設定できない', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + const parentFolder = (await api('/drive/folders/create', { + name: 'parent', + }, bob)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: parentFolder.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('フォルダが循環するような構造にできない', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + const parentFolder = (await api('/drive/folders/create', { + name: 'parent', + }, alice)).body; + await api('/drive/folders/update', { + folderId: parentFolder.id, + parentId: folder.id, + }, alice); + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: parentFolder.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('フォルダが循環するような構造にできない(再帰的)', async () => { + const folderA = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + const folderB = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + const folderC = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + await api('/drive/folders/update', { + folderId: folderB.id, + parentId: folderA.id, + }, alice); + await api('/drive/folders/update', { + folderId: folderC.id, + parentId: folderB.id, + }, alice); + + const res = await api('/drive/folders/update', { + folderId: folderA.id, + parentId: folderC.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('フォルダが循環するような構造にできない(自身)', async () => { + const folderA = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folderA.id, + parentId: folderA.id, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('存在しない親フォルダを設定できない', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: '000000000000000000000000', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('不正な親フォルダIDで怒られる', async () => { + const folder = (await api('/drive/folders/create', { + name: 'test', + }, alice)).body; + + const res = await api('/drive/folders/update', { + folderId: folder.id, + parentId: 'foo', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('存在しないフォルダを更新できない', async () => { + const res = await api('/drive/folders/update', { + folderId: '000000000000000000000000', + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('不正なフォルダIDで怒られる', async () => { + const res = await api('/drive/folders/update', { + folderId: 'foo', + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('notes/replies', () => { + test('自分に閲覧権限のない投稿は含まれない', async () => { + const alicePost = await post(alice, { + text: 'foo', + }); + + await post(bob, { + replyId: alicePost.id, + text: 'bar', + visibility: 'specified', + visibleUserIds: [alice.id], + }); + + const res = await api('/notes/replies', { + noteId: alicePost.id, + }, carol); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 0); + }); + }); + + describe('notes/timeline', () => { + test('フォロワー限定投稿が含まれる', async () => { + await api('/following/create', { + userId: carol.id, + }, dave); + + const carolPost = await post(carol, { + text: 'foo', + visibility: 'followers', + }); + + const res = await api('/notes/timeline', {}, dave); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 1); + assert.strictEqual(res.body[0].id, carolPost.id); + }); + }); +}); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts new file mode 100644 index 0000000000..6b3c795235 --- /dev/null +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -0,0 +1,193 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { startServer, signup, post, api, simpleGet } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +// Request Accept +const ONLY_AP = 'application/activity+json'; +const PREFER_AP = 'application/activity+json, */*'; +const PREFER_HTML = 'text/html, */*'; +const UNSPECIFIED = '*/*'; + +// Response Content-Type +const AP = 'application/activity+json; charset=utf-8'; +const HTML = 'text/html; charset=utf-8'; + +describe('Fetch resource', () => { + let p: INestApplicationContext; + + let alice: any; + let alicesPost: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + alicesPost = await post(alice, { + text: 'test', + }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + describe('Common', () => { + test('meta', async () => { + const res = await api('/meta', { + }); + + assert.strictEqual(res.status, 200); + }); + + test('GET root', async () => { + const res = await simpleGet('/'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + test('GET docs', async () => { + const res = await simpleGet('/docs/ja-JP/about'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + test('GET api-doc (廃止)', async () => { + const res = await simpleGet('/api-doc'); + assert.strictEqual(res.status, 404); + }); + + test('GET api.json (廃止)', async () => { + const res = await simpleGet('/api.json'); + assert.strictEqual(res.status, 404); + }); + + test('GET api/foo (存在しない)', async () => { + const res = await simpleGet('/api/foo'); + assert.strictEqual(res.status, 404); + assert.strictEqual(res.body.error.code, 'UNKNOWN_API_ENDPOINT'); + }); + + test('GET favicon.ico', async () => { + const res = await simpleGet('/favicon.ico'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'image/vnd.microsoft.icon'); + }); + + test('GET apple-touch-icon.png', async () => { + const res = await simpleGet('/apple-touch-icon.png'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'image/png'); + }); + + test('GET twemoji svg', async () => { + const res = await simpleGet('/twemoji/2764.svg'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'image/svg+xml'); + }); + + test('GET twemoji svg with hyphen', async () => { + const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'image/svg+xml'); + }); + }); + + describe('/@:username', () => { + test('Only AP => AP', async () => { + const res = await simpleGet(`/@${alice.username}`, ONLY_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + test('Prefer AP => AP', async () => { + const res = await simpleGet(`/@${alice.username}`, PREFER_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + test('Prefer HTML => HTML', async () => { + const res = await simpleGet(`/@${alice.username}`, PREFER_HTML); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + test('Unspecified => HTML', async () => { + const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + }); + + describe('/users/:id', () => { + test('Only AP => AP', async () => { + const res = await simpleGet(`/users/${alice.id}`, ONLY_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + test('Prefer AP => AP', async () => { + const res = await simpleGet(`/users/${alice.id}`, PREFER_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + test('Prefer HTML => Redirect to /@:username', async () => { + const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML); + assert.strictEqual(res.status, 302); + assert.strictEqual(res.location, `/@${alice.username}`); + }); + + test('Undecided => HTML', async () => { + const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED); + assert.strictEqual(res.status, 302); + assert.strictEqual(res.location, `/@${alice.username}`); + }); + }); + + describe('/notes/:id', () => { + test('Only AP => AP', async () => { + const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + test('Prefer AP => AP', async () => { + const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, AP); + }); + + test('Prefer HTML => HTML', async () => { + const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + + test('Unspecified => HTML', async () => { + const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, HTML); + }); + }); + + describe('Feeds', () => { + test('RSS', async () => { + const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8'); + }); + + test('ATOM', async () => { + const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8'); + }); + + test('JSON', async () => { + const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.type, 'application/json; charset=utf-8'); + }); + }); +}); diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts new file mode 100644 index 0000000000..d53919ca1e --- /dev/null +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -0,0 +1,165 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, startServer, simpleGet } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('FF visibility', () => { + let p: INestApplicationContext; + + let alice: any; + let bob: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { + await api('/i/update', { + ffVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + }); + + test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { + await api('/i/update', { + ffVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + }); + + test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { + await api('/i/update', { + ffVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + + assert.strictEqual(followingRes.status, 400); + assert.strictEqual(followersRes.status, 400); + }); + + test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { + await api('/i/update', { + ffVisibility: 'followers', + }, alice); + + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + }); + + test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { + await api('/i/update', { + ffVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + }); + + test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { + await api('/i/update', { + ffVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + + assert.strictEqual(followingRes.status, 400); + assert.strictEqual(followersRes.status, 400); + }); + + describe('AP', () => { + test('ffVisibility が public 以外ならばAPからは取得できない', async () => { + { + await api('/i/update', { + ffVisibility: 'public', + }, alice); + + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(followersRes.status, 200); + } + { + await api('/i/update', { + ffVisibility: 'followers', + }, alice); + + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 403); + assert.strictEqual(followersRes.status, 403); + } + { + await api('/i/update', { + ffVisibility: 'private', + }, alice); + + const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followingRes.status, 403); + assert.strictEqual(followersRes.status, 403); + } + }); + }); +}); diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts new file mode 100644 index 0000000000..6654a290be --- /dev/null +++ b/packages/backend/test/e2e/mute.ts @@ -0,0 +1,123 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, post, react, startServer, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('Mute', () => { + let p: INestApplicationContext; + + // alice mutes carol + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + test('ミュート作成', async () => { + const res = await api('/mute/create', { + userId: carol.id, + }, alice); + + assert.strictEqual(res.status, 204); + }); + + test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => { + const bobNote = await post(bob, { text: '@alice hi' }); + const carolNote = await post(carol, { text: '@alice hi' }); + + const res = await api('/notes/mentions', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + + await post(carol, { text: '@alice hi' }); + + const res = await api('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + }); + + test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + + const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); + + assert.strictEqual(fired, false); + }); + + test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + await api('/notifications/mark-all-as-read', {}, alice); + + const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); + + assert.strictEqual(fired, false); + }); + + describe('Timeline', () => { + test('タイムラインにミュートしているユーザーの投稿が含まれない', async () => { + const aliceNote = await post(alice); + const bobNote = await post(bob); + const carolNote = await post(carol); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => { + const aliceNote = await post(alice); + const carolNote = await post(carol); + const bobNote = await post(bob, { + renoteId: carolNote.id, + }); + + const res = await api('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + }); + + describe('Notification', () => { + test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => { + const aliceNote = await post(alice); + await react(bob, aliceNote, 'like'); + await react(carol, aliceNote, 'like'); + + const res = await api('/i/notifications', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); + assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + }); + }); +}); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts new file mode 100644 index 0000000000..98ee34d8d1 --- /dev/null +++ b/packages/backend/test/e2e/note.ts @@ -0,0 +1,370 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { Note } from '@/models/entities/Note.js'; +import { signup, post, uploadUrl, startServer, initTestDb, api } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('Note', () => { + let p: INestApplicationContext; + let Notes: any; + + let alice: any; + let bob: any; + + beforeAll(async () => { + p = await startServer(); + const connection = await initTestDb(true); + Notes = connection.getRepository(Note); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + test('投稿できる', async () => { + const post = { + text: 'test', + }; + + const res = await api('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + }); + + test('ファイルを添付できる', async () => { + const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + + const res = await api('/notes/create', { + fileIds: [file.id], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); + }, 1000 * 10); + + test('他人のファイルで怒られる', async () => { + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + + const res = await api('/notes/create', { + text: 'test', + fileIds: [file.id], + }, alice); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + }, 1000 * 10); + + test('存在しないファイルで怒られる', async () => { + const res = await api('/notes/create', { + text: 'test', + fileIds: ['000000000000000000000000'], + }, alice); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + }); + + test('不正なファイルIDで怒られる', async () => { + const res = await api('/notes/create', { + fileIds: ['kyoppie'], + }, alice); + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); + assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + }); + + test('返信できる', async () => { + const bobPost = await post(bob, { + text: 'foo', + }); + + const alicePost = { + text: 'bar', + replyId: bobPost.id, + }; + + const res = await api('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, alicePost.text); + assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); + assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); + }); + + test('renoteできる', async () => { + const bobPost = await post(bob, { + text: 'test', + }); + + const alicePost = { + renoteId: bobPost.id, + }; + + const res = await api('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); + }); + + test('引用renoteできる', async () => { + const bobPost = await post(bob, { + text: 'test', + }); + + const alicePost = { + text: 'test', + renoteId: bobPost.id, + }; + + const res = await api('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, alicePost.text); + assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); + }); + + test('文字数ぎりぎりで怒られない', async () => { + const post = { + text: '!'.repeat(3000), + }; + const res = await api('/notes/create', post, alice); + assert.strictEqual(res.status, 200); + }); + + test('文字数オーバーで怒られる', async () => { + const post = { + text: '!'.repeat(3001), + }; + const res = await api('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + test('存在しないリプライ先で怒られる', async () => { + const post = { + text: 'test', + replyId: '000000000000000000000000', + }; + const res = await api('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + test('存在しないrenote対象で怒られる', async () => { + const post = { + renoteId: '000000000000000000000000', + }; + const res = await api('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + test('不正なリプライ先IDで怒られる', async () => { + const post = { + text: 'test', + replyId: 'foo', + }; + const res = await api('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + test('不正なrenote対象IDで怒られる', async () => { + const post = { + renoteId: 'foo', + }; + const res = await api('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + }); + + test('存在しないユーザーにメンションできる', async () => { + const post = { + text: '@ghost yo', + }; + + const res = await api('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + }); + + test('同じユーザーに複数メンションしても内部的にまとめられる', async () => { + const post = { + text: '@bob @bob @bob yo', + }; + + const res = await api('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + + const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); + assert.deepStrictEqual(noteDoc.mentions, [bob.id]); + }); + + describe('notes/create', () => { + test('投票を添付できる', async () => { + const res = await api('/notes/create', { + text: 'test', + poll: { + choices: ['foo', 'bar'], + }, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.poll != null, true); + }); + + test('投票の選択肢が無くて怒られる', async () => { + const res = await api('/notes/create', { + poll: {}, + }, alice); + assert.strictEqual(res.status, 400); + }); + + test('投票の選択肢が無くて怒られる (空の配列)', async () => { + const res = await api('/notes/create', { + poll: { + choices: [], + }, + }, alice); + assert.strictEqual(res.status, 400); + }); + + test('投票の選択肢が1つで怒られる', async () => { + const res = await api('/notes/create', { + poll: { + choices: ['Strawberry Pasta'], + }, + }, alice); + assert.strictEqual(res.status, 400); + }); + + test('投票できる', async () => { + const { body } = await api('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + }, + }, alice); + + const res = await api('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1, + }, alice); + + assert.strictEqual(res.status, 204); + }); + + test('複数投票できない', async () => { + const { body } = await api('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + }, + }, alice); + + await api('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 0, + }, alice); + + const res = await api('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 2, + }, alice); + + assert.strictEqual(res.status, 400); + }); + + test('許可されている場合は複数投票できる', async () => { + const { body } = await api('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + multiple: true, + }, + }, alice); + + await api('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 0, + }, alice); + + await api('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1, + }, alice); + + const res = await api('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 2, + }, alice); + + assert.strictEqual(res.status, 204); + }); + + test('締め切られている場合は投票できない', async () => { + const { body } = await api('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + expiredAfter: 1, + }, + }, alice); + + await new Promise(x => setTimeout(x, 2)); + + const res = await api('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1, + }, alice); + + assert.strictEqual(res.status, 400); + }); + }); + + describe('notes/delete', () => { + test('delete a reply', async () => { + const mainNoteRes = await api('notes/create', { + text: 'main post', + }, alice); + const replyOneRes = await api('notes/create', { + text: 'reply one', + replyId: mainNoteRes.body.createdNote.id, + }, alice); + const replyTwoRes = await api('notes/create', { + text: 'reply two', + replyId: mainNoteRes.body.createdNote.id, + }, alice); + + const deleteOneRes = await api('notes/delete', { + noteId: replyOneRes.body.createdNote.id, + }, alice); + + assert.strictEqual(deleteOneRes.status, 204); + let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); + assert.strictEqual(mainNote.repliesCount, 1); + + const deleteTwoRes = await api('notes/delete', { + noteId: replyTwoRes.body.createdNote.id, + }, alice); + + assert.strictEqual(deleteTwoRes.status, 204); + mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); + assert.strictEqual(mainNote.repliesCount, 0); + }); + }); +}); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts new file mode 100644 index 0000000000..23c431f2e7 --- /dev/null +++ b/packages/backend/test/e2e/streaming.ts @@ -0,0 +1,547 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { Following } from '@/models/entities/Following.js'; +import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('Streaming', () => { + let p: INestApplicationContext; + let Followings: any; + + const follow = async (follower: any, followee: any) => { + await Followings.save({ + id: 'a', + createdAt: new Date(), + followerId: follower.id, + followeeId: followee.id, + followerHost: follower.host, + followerInbox: null, + followerSharedInbox: null, + followeeHost: followee.host, + followeeInbox: null, + followeeSharedInbox: null, + }); + }; + + describe('Streaming', () => { + // Local users + let ayano: any; + let kyoko: any; + let chitose: any; + + // Remote users + let akari: any; + let chinatsu: any; + + let kyokoNote: any; + let list: any; + + beforeAll(async () => { + p = await startServer(); + const connection = await initTestDb(true); + Followings = connection.getRepository(Following); + + ayano = await signup({ username: 'ayano' }); + kyoko = await signup({ username: 'kyoko' }); + chitose = await signup({ username: 'chitose' }); + + akari = await signup({ username: 'akari', host: 'example.com' }); + chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); + + kyokoNote = await post(kyoko, { text: 'foo' }); + + // Follow: ayano => kyoko + await api('following/create', { userId: kyoko.id }, ayano); + + // Follow: ayano => akari + await follow(ayano, akari); + + // List: chitose => ayano, kyoko + list = await api('users/lists/create', { + name: 'my list', + }, chitose).then(x => x.body); + + await api('users/lists/push', { + listId: list.id, + userId: ayano.id, + }, chitose); + + await api('users/lists/push', { + listId: list.id, + userId: kyoko.id, + }, chitose); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + describe('Events', () => { + test('mention event', async () => { + const fired = await waitFire( + kyoko, 'main', // kyoko:main + () => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko + msg => msg.type === 'mention' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, true); + }); + + test('renote event', async () => { + const fired = await waitFire( + kyoko, 'main', // kyoko:main + () => post(ayano, { renoteId: kyokoNote.id }), // ayano renote + msg => msg.type === 'renote' && msg.body.renoteId === kyokoNote.id, // wait renote + ); + + assert.strictEqual(fired, true); + }); + }); + + describe('Home Timeline', () => { + test('自分の投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:Home + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしているユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしていないユーザーの投稿は流れない', async () => { + const fired = await waitFire( + kyoko, 'homeTimeline', // kyoko:home + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, false); + }); + + test('フォローしているユーザーのダイレクト投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko dm => ayano + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), // kyoko dm => chitose + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + }); // Home + + describe('Local Timeline', () => { + test('自分の投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしていないローカルユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, chitose), // chitose posts + msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose + ); + + assert.strictEqual(fired, true); + }); + + test('リモートユーザーの投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts + msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu + ); + + assert.strictEqual(fired, false); + }); + + test('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, akari), // akari posts + msg => msg.type === 'note' && msg.body.userId === akari.id, // wait akari + ); + + assert.strictEqual(fired, false); + }); + + test('ホーム指定の投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + + test('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + + test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'localTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), + msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose + ); + + assert.strictEqual(fired, false); + }); + }); + + describe('Hybrid Timeline', () => { + test('自分の投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしていないローカルユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo' }, chitose), // chitose posts + msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしているリモートユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo' }, akari), // akari posts + msg => msg.type === 'note' && msg.body.userId === akari.id, // wait akari + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしていないリモートユーザーの投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts + msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu + ); + + assert.strictEqual(fired, false); + }); + + test('フォローしているユーザーのダイレクト投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしているユーザーのホーム投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしていないローカルユーザーのホーム投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo', visibility: 'home' }, chitose), + msg => msg.type === 'note' && msg.body.userId === chitose.id, + ); + + assert.strictEqual(fired, false); + }); + + test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'hybridTimeline', // ayano:Hybrid + () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), + msg => msg.type === 'note' && msg.body.userId === chitose.id, + ); + + assert.strictEqual(fired, false); + }); + }); + + describe('Global Timeline', () => { + test('フォローしていないローカルユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'globalTimeline', // ayano:Global + () => api('notes/create', { text: 'foo' }, chitose), // chitose posts + msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose + ); + + assert.strictEqual(fired, true); + }); + + test('フォローしていないリモートユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'globalTimeline', // ayano:Global + () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts + msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu + ); + + assert.strictEqual(fired, true); + }); + + test('ホーム投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'globalTimeline', // ayano:Global + () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + }); + + describe('UserList Timeline', () => { + test('リストに入れているユーザーの投稿が流れる', async () => { + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo' }, ayano), + msg => msg.type === 'note' && msg.body.userId === ayano.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, true); + }); + + test('リストに入れていないユーザーの投稿は流れない', async () => { + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo' }, chinatsu), + msg => msg.type === 'note' && msg.body.userId === chinatsu.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + + // #4471 + test('リストに入れているユーザーのダイレクト投稿が流れる', async () => { + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano), + msg => msg.type === 'note' && msg.body.userId === ayano.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, true); + }); + + // #4335 + test('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', async () => { + const fired = await waitFire( + chitose, 'userList', + () => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), + msg => msg.type === 'note' && msg.body.userId === kyoko.id, + { listId: list.id }, + ); + + assert.strictEqual(fired, false); + }); + }); + + describe('Hashtag Timeline', () => { + test('指定したハッシュタグの投稿が流れる', () => new Promise(async done => { + const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + if (type === 'note') { + assert.deepStrictEqual(body.text, '#foo'); + ws.close(); + done(); + } + }, { + q: [ + ['foo'], + ], + }); + + post(chitose, { + text: '#foo', + }); + })); + + // XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac" + + // test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { + // let fooCount = 0; + // let barCount = 0; + // let fooBarCount = 0; + + // const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + // if (type === 'note') { + // if (body.text === '#foo') fooCount++; + // if (body.text === '#bar') barCount++; + // if (body.text === '#foo #bar') fooBarCount++; + // } + // }, { + // q: [ + // ['foo', 'bar'], + // ], + // }); + + // post(chitose, { + // text: '#foo', + // }); + + // post(chitose, { + // text: '#bar', + // }); + + // post(chitose, { + // text: '#foo #bar', + // }); + + // setTimeout(() => { + // assert.strictEqual(fooCount, 0); + // assert.strictEqual(barCount, 0); + // assert.strictEqual(fooBarCount, 1); + // ws.close(); + // done(); + // }, 3000); + // })); + + test('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + let piyoCount = 0; + + const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + if (type === 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + if (body.text === '#piyo') piyoCount++; + } + }, { + q: [ + ['foo'], + ['bar'], + ], + }); + + post(chitose, { + text: '#foo', + }); + + post(chitose, { + text: '#bar', + }); + + post(chitose, { + text: '#foo #bar', + }); + + post(chitose, { + text: '#piyo', + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 1); + assert.strictEqual(barCount, 1); + assert.strictEqual(fooBarCount, 1); + assert.strictEqual(piyoCount, 0); + ws.close(); + done(); + }, 3000); + })); + + test('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => { + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + let piyoCount = 0; + let waaaCount = 0; + + const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => { + if (type === 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + if (body.text === '#piyo') piyoCount++; + if (body.text === '#waaa') waaaCount++; + } + }, { + q: [ + ['foo', 'bar'], + ['piyo'], + ], + }); + + post(chitose, { + text: '#foo', + }); + + post(chitose, { + text: '#bar', + }); + + post(chitose, { + text: '#foo #bar', + }); + + post(chitose, { + text: '#piyo', + }); + + post(chitose, { + text: '#waaa', + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 0); + assert.strictEqual(barCount, 0); + assert.strictEqual(fooBarCount, 1); + assert.strictEqual(piyoCount, 1); + assert.strictEqual(waaaCount, 0); + ws.close(); + done(); + }, 3000); + })); + }); + }); +}); diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts new file mode 100644 index 0000000000..792436d88f --- /dev/null +++ b/packages/backend/test/e2e/thread-mute.ts @@ -0,0 +1,103 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, post, connectStream, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('Note thread mute', () => { + let p: INestApplicationContext; + + let alice: any; + let bob: any; + let carol: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await p.close(); + }); + + test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => { + const bobNote = await post(bob, { text: '@alice @carol root note' }); + const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); + + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); + + const res = await api('/notes/mentions', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); + }); + + test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + + const bobNote = await post(bob, { text: '@alice @carol root note' }); + + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + + const res = await api('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + }); + + test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { + // 状態リセット + await api('/i/read-all-unread-notes', {}, alice); + + const bobNote = await post(bob, { text: '@alice @carol root note' }); + + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'main', async ({ type, body }) => { + if (type === 'unreadMention') { + if (body === bobNote.id) return; + fired = true; + } + }); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 5000); + })); + + test('i/notifications にミュートしているスレッドの通知が含まれない', async () => { + const bobNote = await post(bob, { text: '@alice @carol root note' }); + const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); + + await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); + + const res = await api('/i/notifications', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); + + // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい + }); +}); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts new file mode 100644 index 0000000000..690cba1746 --- /dev/null +++ b/packages/backend/test/e2e/user-notes.ts @@ -0,0 +1,61 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { signup, api, post, uploadUrl, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('users/notes', () => { + let p: INestApplicationContext; + + let alice: any; + let jpgNote: any; + let pngNote: any; + let jpgPngNote: any; + + beforeAll(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); + jpgNote = await post(alice, { + fileIds: [jpg.id], + }); + pngNote = await post(alice, { + fileIds: [png.id], + }); + jpgPngNote = await post(alice, { + fileIds: [jpg.id, png.id], + }); + }, 1000 * 60 * 2); + + afterAll(async() => { + await p.close(); + }); + + test('ファイルタイプ指定 (jpg)', async () => { + const res = await api('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg'], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 2); + assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); + }); + + test('ファイルタイプ指定 (jpg or png)', async () => { + const res = await api('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg', 'image/png'], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 3); + assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index b813362893..8203e49359 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,3 +1,243 @@ +import { readFile } from 'node:fs/promises'; +import { isAbsolute, basename } from 'node:path'; +import WebSocket from 'ws'; +import fetch, { Blob, File, RequestInit } from 'node-fetch'; +import { DataSource } from 'typeorm'; +import { entities } from '../src/postgres.js'; +import { loadConfig } from '../src/config.js'; +import type * as misskey from 'misskey-js'; + +export { server as startServer } from '@/boot/common.js'; + +const config = loadConfig(); +export const port = config.port; + +export const api = async (endpoint: string, params: any, me?: any) => { + const normalized = endpoint.replace(/^\//, ''); + return await request(`api/${normalized}`, params, me); +}; + +const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { + const auth = me ? { + i: me.token, + } : {}; + + const res = await relativeFetch(path, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(Object.assign(auth, params)), + redirect: 'manual', + }); + + const status = res.status; + const body = res.headers.get('content-type') === 'application/json; charset=utf-8' + ? await res.json() + : null; + + return { + body, status, + }; +}; + +const relativeFetch = async (path: string, init?: RequestInit | undefined) => { + return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init); +}; + +export const signup = async (params?: any): Promise => { + const q = Object.assign({ + username: 'test', + password: 'test', + }, params); + + const res = await api('signup', q); + + return res.body; +}; + +export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise => { + const q = Object.assign({ + text: 'test', + }, params); + + const res = await api('notes/create', q, user); + + return res.body ? res.body.createdNote : null; +}; + +export const react = async (user: any, note: any, reaction: string): Promise => { + await api('notes/reactions/create', { + noteId: note.id, + reaction: reaction, + }, user); +}; + +interface UploadOptions { + /** Optional, absolute path or relative from ./resources/ */ + path?: string | URL; + /** The name to be used for the file upload */ + name?: string; + /** A Blob can be provided instead of path */ + blob?: Blob; +} + +/** + * Upload file + * @param user User + */ +export const uploadFile = async (user: any, { path, name, blob }: UploadOptions = {}): Promise => { + const absPath = path == null + ? new URL('resources/Lenna.jpg', import.meta.url) + : isAbsolute(path.toString()) + ? new URL(path) + : new URL(path, new URL('resources/', import.meta.url)); + + const formData = new FormData(); + formData.append('i', user.token); + formData.append('file', blob ?? + new File([await readFile(absPath)], basename(absPath.toString()))); + formData.append('force', 'true'); + if (name) { + formData.append('name', name); + } + + const res = await relativeFetch('api/drive/files/create', { + method: 'POST', + body: formData, + }); + + const body = res.status !== 204 ? await res.json() : null; + + return { + status: res.status, + body, + }; +}; + +export const uploadUrl = async (user: any, url: string) => { + let file: any; + const marker = Math.random().toString(); + + const ws = await connectStream(user, 'main', (msg) => { + if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) { + file = msg.body.file; + } + }); + + await api('drive/files/upload-from-url', { + url, + marker, + force: true, + }, user); + + await sleep(7000); + ws.close(); + + return file; +}; + +export function connectStream(user: any, channel: string, listener: (message: Record) => any, params?: any): Promise { + return new Promise((res, rej) => { + const ws = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${user.token}`); + + ws.on('open', () => { + ws.on('message', data => { + const msg = JSON.parse(data.toString()); + if (msg.type === 'channel' && msg.body.id === 'a') { + listener(msg.body); + } else if (msg.type === 'connected' && msg.body.id === 'a') { + res(ws); + } + }); + + ws.send(JSON.stringify({ + type: 'connect', + body: { + channel: channel, + id: 'a', + pong: true, + params: params, + }, + })); + }); + }); +} + +export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { + return new Promise(async (res, rej) => { + let timer: NodeJS.Timeout | null = null; + + let ws: WebSocket; + try { + ws = await connectStream(user, channel, msg => { + if (cond(msg)) { + ws.close(); + if (timer) clearTimeout(timer); + res(true); + } + }, params); + } catch (e) { + rej(e); + } + + if (!ws!) return; + + timer = setTimeout(() => { + ws.close(); + res(false); + }, 3000); + + try { + await trgr(); + } catch (e) { + ws.close(); + if (timer) clearTimeout(timer); + rej(e); + } + }); +}; + +export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status: number, body: any, type: string | null, location: string | null }> => { + const res = await relativeFetch(path, { + headers: { + Accept: accept, + }, + redirect: 'manual', + }); + + const body = res.headers.get('content-type') === 'application/json; charset=utf-8' + ? await res.json() + : null; + + return { + status: res.status, + body, + type: res.headers.get('content-type'), + location: res.headers.get('location'), + }; +}; + +export async function initTestDb(justBorrow = false, initEntities?: any[]) { + if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; + + const db = new DataSource({ + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + synchronize: true && !justBorrow, + dropSchema: true && !justBorrow, + entities: initEntities ?? entities, + }); + + await db.initialize(); + + return db; +} + export function sleep(msec: number) { return new Promise(res => { setTimeout(() => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6d65b2967..1b9947a080 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,6 @@ importers: '@nestjs/core': 9.3.9 '@nestjs/testing': 9.3.9 '@peertube/http-signature': 1.7.0 - '@redocly/openapi-core': 1.0.0-beta.123 '@sinonjs/fake-timers': 10.0.2 '@swc/cli': 0.1.62 '@swc/core': 1.3.36 @@ -341,7 +340,6 @@ importers: '@tensorflow/tfjs-node': 4.2.0_seedrandom@3.0.5 devDependencies: '@jest/globals': 29.4.3 - '@redocly/openapi-core': 1.0.0-beta.123 '@swc/jest': 0.2.24_@swc+core@1.3.36 '@types/accepts': 1.3.5 '@types/archiver': 5.3.1 @@ -2077,33 +2075,6 @@ packages: '@redis/client': 1.4.2 dev: true - /@redocly/ajv/8.11.0: - resolution: {integrity: sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==} - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - dev: true - - /@redocly/openapi-core/1.0.0-beta.123: - resolution: {integrity: sha512-W6MbUWpb/VaV+Kf0c3jmMIJw3WwwF7iK5nAfcOS+ZwrlbxtIl37+1hEydFlJ209vCR9HL12PaMwdh2Vpihj6Jw==} - engines: {node: '>=12.0.0'} - dependencies: - '@redocly/ajv': 8.11.0 - '@types/node': 14.18.36 - colorette: 1.4.0 - js-levenshtein: 1.1.6 - js-yaml: 4.1.0 - lodash.isequal: 4.5.0 - minimatch: 5.1.2 - node-fetch: 2.6.7 - pluralize: 8.0.0 - yaml-ast-parser: 0.0.43 - transitivePeerDependencies: - - encoding - dev: true - /@rollup/plugin-alias/4.0.3_rollup@3.17.3: resolution: {integrity: sha512-ZuDWE1q4PQDhvm/zc5Prun8sBpLJy41DMptYrS6MhAy9s9kL/doN1613BWfEchGVfKxzliJ3BjbOPizXX38DbQ==} engines: {node: '>=14.0.0'} @@ -4777,10 +4748,6 @@ packages: color-string: 1.9.1 dev: false - /colorette/1.4.0: - resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} - dev: true - /colorette/2.0.19: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: true @@ -8793,11 +8760,6 @@ packages: resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} dev: false - /js-levenshtein/1.1.6: - resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} - engines: {node: '>=0.10.0'} - dev: true - /js-sdsl/4.2.0: resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} dev: true @@ -8902,6 +8864,7 @@ packages: /json-schema-traverse/1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: false /json-schema/0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -9218,10 +9181,6 @@ packages: /lodash.isarguments/3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - /lodash.isequal/4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: true - /lodash.isplainobject/4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} dev: false @@ -9491,6 +9450,7 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.1 + dev: false /minimatch/6.2.0: resolution: {integrity: sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==} @@ -9801,6 +9761,7 @@ packages: optional: true dependencies: whatwg-url: 5.0.0 + dev: false /node-fetch/3.3.0: resolution: {integrity: sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==} @@ -10558,11 +10519,6 @@ packages: extend-shallow: 3.0.2 dev: false - /pluralize/8.0.0: - resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} - engines: {node: '>=4'} - dev: true - /pngjs-nozlib/1.0.0: resolution: {integrity: sha512-N1PggqLp9xDqwAoKvGohmZ3m4/N9xpY0nDZivFqQLcpLHmliHnCp9BuNCsOeqHWMuEEgFjpEaq9dZq6RZyy0fA==} engines: {iojs: '>= 1.0.0', node: '>=0.10.0'} @@ -11519,6 +11475,7 @@ packages: /require-from-string/2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + dev: false /require-main-filename/1.0.1: resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==} @@ -12663,6 +12620,7 @@ packages: /tr46/0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false /tr46/3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} @@ -13371,6 +13329,7 @@ packages: /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false /webidl-conversions/7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} @@ -13416,6 +13375,7 @@ packages: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 + dev: false /whet.extend/0.9.9: resolution: {integrity: sha512-mmIPAft2vTgEILgPeZFqE/wWh24SEsR/k+N9fJ3Jxrz44iDFy9aemCxdksfURSHYFCLmvs/d/7Iso5XjPpNfrA==} @@ -13606,10 +13566,6 @@ packages: /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - /yaml-ast-parser/0.0.43: - resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} - dev: true - /yargs-parser/18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} -- cgit v1.2.3-freya