summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNanashia <nanashia.128@gmail.com>2023-05-19 20:53:20 +0900
committerGitHub <noreply@github.com>2023-05-19 20:53:20 +0900
commit15db0b8812f269d71e232f37cb7f066ad4a9b6db (patch)
treeeed5d0e3f9be9366acf61d812a9230637fe48475
parentrefactor (diff)
downloadmisskey-15db0b8812f269d71e232f37cb7f066ad4a9b6db.tar.gz
misskey-15db0b8812f269d71e232f37cb7f066ad4a9b6db.tar.bz2
misskey-15db0b8812f269d71e232f37cb7f066ad4a9b6db.zip
test(backend): Add tests for antennas (#10868)
-rw-r--r--packages/backend/test/e2e/antennas.ts653
-rw-r--r--packages/backend/test/utils.ts97
2 files changed, 750 insertions, 0 deletions
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
new file mode 100644
index 0000000000..dd3b09f85a
--- /dev/null
+++ b/packages/backend/test/e2e/antennas.ts
@@ -0,0 +1,653 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { inspect } from 'node:util';
+import { DEFAULT_POLICIES } from '@/core/RoleService.js';
+import type { Packed } from '@/misc/json-schema.js';
+import {
+ signup,
+ post,
+ userList,
+ page,
+ role,
+ startServer,
+ api,
+ successfulApiCall,
+ failedApiCall,
+ uploadFile,
+ testPaginationConsistency,
+} from '../utils.js';
+import type * as misskey from 'misskey-js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
+ return selector(a).localeCompare(selector(b));
+};
+
+describe('アンテナ', () => {
+ // エンティティとしてのアンテナを主眼においたテストを記述する
+ // (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからノートを取得するエンドポイントをテストする)
+
+ // BUG misskey-jsとjson-schemaが一致していない。
+ // - srcのenumにgroupが残っている
+ // - userGroupIdが残っている, isActiveがない
+ type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
+ type User = misskey.entities.MeDetailed & { token: string };
+ type Note = misskey.entities.Note;
+
+ // アンテナを作成できる最小のパラメタ
+ const defaultParam = {
+ caseSensitive: false,
+ excludeKeywords: [['']],
+ keywords: [['keyword']],
+ name: 'test',
+ notify: false,
+ src: 'all' as const,
+ userListId: null,
+ users: [''],
+ withFile: false,
+ withReplies: false,
+ };
+
+ let app: INestApplicationContext;
+
+ let root: User;
+ let alice: User;
+ let bob: User;
+ let carol: User;
+
+ let alicePost: Note;
+ let aliceList: misskey.entities.UserList;
+ let bobFile: misskey.entities.DriveFile;
+ let bobList: misskey.entities.UserList;
+
+ let userNotExplorable: User;
+ let userLocking: User;
+ let userSilenced: User;
+ let userSuspended: User;
+ let userDeletedBySelf: User;
+ let userDeletedByAdmin: User;
+ let userFollowingAlice: User;
+ let userFollowedByAlice: User;
+ let userBlockingAlice: User;
+ let userBlockedByAlice: User;
+ let userMutingAlice: User;
+ let userMutedByAlice: User;
+
+ beforeAll(async () => {
+ app = await startServer();
+ }, 1000 * 60 * 2);
+
+ beforeAll(async () => {
+ root = await signup({ username: 'root' });
+ alice = await signup({ username: 'alice' });
+ alicePost = await post(alice, { text: 'test' });
+ aliceList = await userList(alice, {});
+ bob = await signup({ username: 'bob' });
+ aliceList = await userList(alice, {});
+ bobFile = (await uploadFile(bob)).body;
+ bobList = await userList(bob);
+ carol = await signup({ username: 'carol' });
+ await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
+ await api('users/lists/push', { listId: aliceList.id, userId: carol.id }, alice);
+
+ userNotExplorable = await signup({ username: 'userNotExplorable' });
+ await post(userNotExplorable, { text: 'test' });
+ await api('i/update', { isExplorable: false }, userNotExplorable);
+ userLocking = await signup({ username: 'userLocking' });
+ await post(userLocking, { text: 'test' });
+ await api('i/update', { isLocked: true }, userLocking);
+ userSilenced = await signup({ username: 'userSilenced' });
+ await post(userSilenced, { text: 'test' });
+ const roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } });
+ await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
+ userSuspended = await signup({ username: 'userSuspended' });
+ await post(userSuspended, { text: 'test' });
+ await successfulApiCall({ endpoint: 'i/update', parameters: { description: '#user_testuserSuspended' }, user: userSuspended });
+ await api('admin/suspend-user', { userId: userSuspended.id }, root);
+ userDeletedBySelf = await signup({ username: 'userDeletedBySelf', password: 'userDeletedBySelf' });
+ await post(userDeletedBySelf, { text: 'test' });
+ await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf);
+ userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' });
+ await post(userDeletedByAdmin, { text: 'test' });
+ await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root);
+ userFollowedByAlice = await signup({ username: 'userFollowedByAlice' });
+ await post(userFollowedByAlice, { text: 'test' });
+ await api('following/create', { userId: userFollowedByAlice.id }, alice);
+ userFollowingAlice = await signup({ username: 'userFollowingAlice' });
+ await post(userFollowingAlice, { text: 'test' });
+ await api('following/create', { userId: alice.id }, userFollowingAlice);
+ userBlockingAlice = await signup({ username: 'userBlockingAlice' });
+ await post(userBlockingAlice, { text: 'test' });
+ await api('blocking/create', { userId: alice.id }, userBlockingAlice);
+ userBlockedByAlice = await signup({ username: 'userBlockedByAlice' });
+ await post(userBlockedByAlice, { text: 'test' });
+ await api('blocking/create', { userId: userBlockedByAlice.id }, alice);
+ userMutingAlice = await signup({ username: 'userMutingAlice' });
+ await post(userMutingAlice, { text: 'test' });
+ await api('mute/create', { userId: alice.id }, userMutingAlice);
+ userMutedByAlice = await signup({ username: 'userMutedByAlice' });
+ await post(userMutedByAlice, { text: 'test' });
+ await api('mute/create', { userId: userMutedByAlice.id }, alice);
+ }, 1000 * 60 * 10);
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ beforeEach(async () => {
+ // テスト間で影響し合わないように毎回全部消す。
+ for (const user of [alice, bob]) {
+ const list = await api('/antennas/list', {}, user);
+ for (const antenna of list.body) {
+ await api('/antennas/delete', { antennaId: antenna.id }, user);
+ }
+ }
+ });
+
+ //#region 作成(antennas/create)
+
+ test('が作成できること、キーが過不足なく入っていること。', async () => {
+ const response = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ });
+ assert.match(response.id, /[0-9a-z]{10}/);
+ const expected = {
+ id: response.id,
+ caseSensitive: false,
+ createdAt: new Date(response.createdAt).toISOString(),
+ excludeKeywords: [['']],
+ hasUnreadNote: false,
+ isActive: true,
+ keywords: [['keyword']],
+ name: 'test',
+ notify: false,
+ src: 'all',
+ userListId: null,
+ users: [''],
+ withFile: false,
+ withReplies: false,
+ } as Antenna;
+ assert.deepStrictEqual(response, expected);
+ });
+
+ test('が上限いっぱいまで作成できること', async () => {
+ // antennaLimit + 1まで作れるのがキモ
+ const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ })));
+
+ const expected = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice });
+ assert.deepStrictEqual(
+ response.sort(compareBy(s => s.id)),
+ expected.sort(compareBy(s => s.id)));
+
+ failedApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'TOO_MANY_ANTENNAS',
+ id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
+ });
+ });
+
+ test('を作成するとき他人のリストを指定したらエラーになる', async () => {
+ failedApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, src: 'list', userListId: bobList.id },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'NO_SUCH_USER_LIST',
+ id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f',
+ });
+ });
+
+ const antennaParamPattern = [
+ { parameters: (): object => ({ name: 'x'.repeat(100) }) },
+ { parameters: (): object => ({ name: 'x' }) },
+ { parameters: (): object => ({ src: 'home' }) },
+ { parameters: (): object => ({ src: 'all' }) },
+ { parameters: (): object => ({ src: 'users' }) },
+ { parameters: (): object => ({ src: 'list' }) },
+ { parameters: (): object => ({ userListId: null }) },
+ { parameters: (): object => ({ src: 'list', userListId: aliceList.id }) },
+ { parameters: (): object => ({ keywords: [['x']] }) },
+ { parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
+ { parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
+ { parameters: (): object => ({ users: [alice.username] }) },
+ { parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) },
+ { parameters: (): object => ({ caseSensitive: false }) },
+ { parameters: (): object => ({ caseSensitive: true }) },
+ { parameters: (): object => ({ withReplies: false }) },
+ { parameters: (): object => ({ withReplies: true }) },
+ { parameters: (): object => ({ withFile: false }) },
+ { parameters: (): object => ({ withFile: true }) },
+ { parameters: (): object => ({ notify: false }) },
+ { parameters: (): object => ({ notify: true }) },
+ ];
+ test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
+ const response = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, ...parameters() },
+ user: alice,
+ });
+ const expected = { ...response, ...parameters() };
+ assert.deepStrictEqual(response, expected);
+ });
+
+ //#endregion
+ //#region 更新(antennas/update)
+
+ test.each(antennaParamPattern)('を変更できること($#)', async ({ parameters }) => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/update',
+ parameters: { antennaId: antenna.id, ...defaultParam, ...parameters() },
+ user: alice,
+ });
+ const expected = { ...response, ...parameters() };
+ assert.deepStrictEqual(response, expected);
+ });
+ test.todo('は他人のものは変更できない');
+
+ test('を変更するとき他人のリストを指定したらエラーになる', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ failedApiCall({
+ endpoint: 'antennas/update',
+ parameters: { antennaId: antenna.id, ...defaultParam, src: 'list', userListId: bobList.id },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'NO_SUCH_USER_LIST',
+ id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
+ });
+ });
+
+ //#endregion
+ //#region 表示(antennas/show)
+
+ test('をID指定で表示できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/show',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ const expected = { ...antenna };
+ assert.deepStrictEqual(response, expected);
+ });
+ test.todo('は他人のものをID指定で表示できない');
+
+ //#endregion
+ //#region 一覧(antennas/list)
+
+ test('をリスト形式で取得できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: bob });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/list',
+ parameters: {},
+ user: alice,
+ });
+ const expected = [{ ...antenna }];
+ assert.deepStrictEqual(response, expected);
+ });
+
+ //#endregion
+ //#region 削除(antennas/delete)
+
+ test('を削除できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/delete',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ assert.deepStrictEqual(response, null);
+ const list = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice });
+ assert.deepStrictEqual(list, []);
+ });
+ test.todo('は他人のものを削除できない');
+
+ //#endregion
+
+ describe('のノート', () => {
+ //#region アンテナのノート取得(antennas/notes)
+
+ test('を取得できること。', async () => {
+ const keyword = 'キーワード';
+ await post(bob, { text: `test ${keyword} beforehand` });
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]] },
+ user: alice,
+ });
+ const note = await post(bob, { text: `test ${keyword}` });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ const expected = [note];
+ assert.deepStrictEqual(response, expected);
+ });
+
+ const keyword = 'キーワード';
+ test.each([
+ {
+ label: '全体から',
+ parameters: (): object => ({ src: 'all' }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ // BUG e4144a1 以降home指定は壊れている(allと同じ)
+ label: 'ホーム指定はallと同じ',
+ parameters: (): object => ({ src: 'home' }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ // https://github.com/misskey-dev/misskey/issues/9025
+ label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }) },
+ { note: (): Promise<Note> => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }) },
+ ],
+ },
+ {
+ label: 'ブロックしているユーザーのノートは含む',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'ブロックされているユーザーのノートは含まない',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userBlockingAlice, { text: `${keyword}` }) },
+ ],
+ },
+ {
+ label: 'ミュートしているユーザーのノートは含まない',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userMutedByAlice, { text: `${keyword}` }) },
+ ],
+ },
+ {
+ label: 'ミュートされているユーザーのノートは含む',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userMutingAlice, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '「見つけやすくする」がOFFのユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userNotExplorable, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '鍵付きユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userLocking, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'サイレンスのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userSilenced, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '削除ユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise<Note> => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'ユーザー指定で',
+ parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'リスト指定で',
+ parameters: (): object => ({ src: 'list', userListId: aliceList.id }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: `test ${keyword}` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'CWにもマッチする',
+ parameters: (): object => ({ keywords: [[keyword]] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード1つ',
+ parameters: (): object => ({ keywords: [[keyword]] }),
+ posts: [
+ { note: (): Promise<Note> => post(alice, { text: 'test' }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(carol, { text: 'test' }) },
+ ],
+ },
+ {
+ label: 'キーワード3つ(AND)',
+ parameters: (): object => ({ keywords: [['A', 'B', 'C']] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'test A' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'test A B' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'test B C' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'test A B C' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test C B A A B C' }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード3つ(OR)',
+ parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'test' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'test A' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test A B' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test B C' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test B C A' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test C B' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'test C' }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード3つ(AND)',
+ parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A B` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C A` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C B` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C` }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード3つ(OR)',
+ parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} A B` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} B C A` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C B` }) },
+ { note: (): Promise<Note> => post(bob, { text: `test ${keyword} C` }) },
+ ],
+ },
+ {
+ label: 'キーワード1つ(大文字小文字区別する)',
+ parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'keyword' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }) },
+ { note: (): Promise<Note> => post(bob, { text: 'KEYWORD' }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード1つ(大文字小文字区別しない)',
+ parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: 'keyword' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'kEyWoRd' }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: 'KEYWORD' }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード1つ(大文字小文字区別する)',
+ parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} kEyWoRd` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} KEYWORD` }) },
+ ],
+ },
+ {
+ label: '除外ワード1つ(大文字小文字区別しない)',
+ parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: false }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} keyword` }) },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} kEyWoRd` }) },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword} KEYWORD` }) },
+ ],
+ },
+ {
+ label: '添付ファイルを問わない',
+ parameters: (): object => ({ withFile: false }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '添付ファイル付きのみ',
+ parameters: (): object => ({ withFile: true }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}`, fileIds: [bobFile.id] }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }) },
+ ],
+ },
+ {
+ label: 'リプライ以外',
+ parameters: (): object => ({ withReplies: false }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }) },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'リプライも含む',
+ parameters: (): object => ({ withReplies: true }),
+ posts: [
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}`, replyId: alicePost.id }), included: true },
+ { note: (): Promise<Note> => post(bob, { text: `${keyword}` }), included: true },
+ ],
+ },
+ ])('が取得できること($label)', async ({ parameters, posts }) => {
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]], ...parameters() },
+ user: alice,
+ });
+
+ const notes = await posts.reduce(async (prev, current) => {
+ // includedに関わらずnote()は評価して投稿する。
+ const p = await prev;
+ const n = await current.note();
+ if (current.included) return p.concat(n);
+ return p;
+ }, Promise.resolve([] as Note[]));
+
+ // alice視点でNoteを取り直す
+ const expected = await Promise.all(notes.reverse().map(s => successfulApiCall({
+ endpoint: 'notes/show',
+ parameters: { noteId: s.id },
+ user: alice,
+ })));
+
+ const response = await successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ assert.deepStrictEqual(
+ response.map(({ userId, id, text }) => ({ userId, id, text })),
+ expected.map(({ userId, id, text }) => ({ userId, id, text })));
+ assert.deepStrictEqual(response, expected);
+ });
+
+ test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
+ test.each([
+ { label: 'ID指定', offsetBy: 'id' },
+
+ // BUG sinceDate, untilDateはsinceIdや他のエンドポイントとは異なり、その時刻に一致するレコードを含んでしまう。
+ // { label: '日付指定', offsetBy: 'createdAt' },
+ ] as const)('が取得でき、$labelのPaginationに一貫性があること', async ({ offsetBy }) => {
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]] },
+ user: alice,
+ });
+ const notes = await [...Array(30)].reduce(async (prev, current, index) => {
+ const p = await prev;
+ const n = await post(alice, { text: `${keyword} (${index})` });
+ return [n].concat(p);
+ }, Promise.resolve([] as Note[]));
+
+ // antennas/notesは降順のみで、昇順をサポートしない。
+ await testPaginationConsistency(notes, async (paginationParam) => {
+ return successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id, ...paginationParam },
+ user: alice,
+ }) as any as Note[];
+ }, offsetBy, 'desc');
+ });
+
+ // BUG 7日過ぎると作り直すしかない。 https://github.com/misskey-dev/misskey/issues/10476
+ test.todo('を取得したときActiveに戻る');
+
+ //#endregion
+ });
+});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 809ed2c66c..1a4a0b3545 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -124,6 +124,13 @@ export const react = async (user: any, note: any, reaction: string): Promise<any
}, user);
};
+export const userList = async (user: any, userList: any = {}): Promise<any> => {
+ const res = await api('users/lists/create', {
+ name: 'test',
+ }, user);
+ return res.body;
+};
+
export const page = async (user: any, page: any = {}): Promise<any> => {
const res = await api('pages/create', {
alignCenter: false,
@@ -380,6 +387,96 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
};
};
+/**
+ * あるAPIエンドポイントのPaginationが複数の条件で一貫した挙動であることをテストします。
+ * (sinceId, untilId, sinceDate, untilDate, offset, limit)
+ * @param expected 期待値となるEntityの並び(例:Note[])昇順降順が一致している必要がある
+ * @param fetchEntities Entity[]を返却するテスト対象のAPIを呼び出す関数
+ * @param offsetBy 何をキーとしてPaginationするか。
+ * @param ordering 昇順・降順
+ */
+export async function testPaginationConsistency<Entity extends { id: string, createdAt?: string }>(
+ expected: Entity[],
+ fetchEntities: (paginationParam: {
+ limit?: number,
+ offset?: number,
+ sinceId?: string,
+ untilId?: string,
+ sinceDate?: number,
+ untilDate?: number,
+ }) => Promise<Entity[]>,
+ offsetBy: 'offset' | 'id' | 'createdAt' = 'id',
+ ordering: 'desc' | 'asc' = 'desc'): Promise<void> {
+ const rangeToParam = (p: { limit?: number, until?: Entity, since?: Entity }): object => {
+ if (offsetBy === 'id') {
+ return { limit: p.limit, sinceId: p.since?.id, untilId: p.until?.id };
+ } else {
+ const sinceDate = p.since?.createdAt !== undefined ? new Date(p.since.createdAt).getTime() : undefined;
+ const untilDate = p.until?.createdAt !== undefined ? new Date(p.until.createdAt).getTime() : undefined;
+ return { limit: p.limit, sinceDate, untilDate };
+ }
+ };
+
+ for (const limit of [1, 5, 10, 100, undefined]) {
+ // 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること
+ if (ordering === 'desc') {
+ const end = expected[expected.length - 1];
+ let last = await fetchEntities(rangeToParam({ limit, since: end }));
+ const actual: Entity[] = [];
+ while (last.length !== 0) {
+ actual.push(...last);
+ last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1], since: end }));
+ }
+ actual.push(end);
+ assert.deepStrictEqual(
+ actual.map(({ id, createdAt }) => id + ':' + createdAt),
+ expected.map(({ id, createdAt }) => id + ':' + createdAt));
+ }
+
+ // 2. sinceId/Date指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
+ if (ordering === 'asc') {
+ // 昇順にしたときの先頭(一番古いもの)をもってくる(expected[1]を基準に降順にして0番目)
+ let last = await fetchEntities({ limit: 1, untilId: expected[1].id });
+ const actual: Entity[] = [];
+ while (last.length !== 0) {
+ actual.push(...last);
+ last = await fetchEntities(rangeToParam({ limit, since: last[last.length - 1] }));
+ }
+ assert.deepStrictEqual(
+ actual.map(({ id, createdAt }) => id + ':' + createdAt),
+ expected.map(({ id, createdAt }) => id + ':' + createdAt));
+ }
+
+ // 3. untilId指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
+ if (ordering === 'desc') {
+ let last = await fetchEntities({ limit });
+ const actual: Entity[] = [];
+ while (last.length !== 0) {
+ actual.push(...last);
+ last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1] }));
+ }
+ assert.deepStrictEqual(
+ actual.map(({ id, createdAt }) => id + ':' + createdAt),
+ expected.map(({ id, createdAt }) => id + ':' + createdAt));
+ }
+
+ // 4. offset指定+limitで取得してつなぎ合わせた結果が期待通りになっていること
+ if (offsetBy === 'offset') {
+ let last = await fetchEntities({ limit, offset: 0 });
+ let offset = limit ?? 10;
+ const actual: Entity[] = [];
+ while (last.length !== 0) {
+ actual.push(...last);
+ last = await fetchEntities({ limit, offset });
+ offset += limit ?? 10;
+ }
+ assert.deepStrictEqual(
+ actual.map(({ id, createdAt }) => id + ':' + createdAt),
+ expected.map(({ id, createdAt }) => id + ':' + createdAt));
+ }
+ }
+}
+
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';