diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2025-08-31 08:42:43 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-31 08:42:43 +0000 |
| commit | ec21336d45e6e3bb26a0225fc0aa57ac98d5be4b (patch) | |
| tree | 2c7aef5ba1626009377faf96941a57411dd619e5 /packages/backend/test | |
| parent | Merge pull request #16244 from misskey-dev/develop (diff) | |
| parent | Release: 2025.8.0 (diff) | |
| download | misskey-ec21336d45e6e3bb26a0225fc0aa57ac98d5be4b.tar.gz misskey-ec21336d45e6e3bb26a0225fc0aa57ac98d5be4b.tar.bz2 misskey-ec21336d45e6e3bb26a0225fc0aa57ac98d5be4b.zip | |
Merge pull request #16335 from misskey-dev/develop
Release: 2025.8.0
Diffstat (limited to 'packages/backend/test')
| -rw-r--r-- | packages/backend/test/e2e/antennas.ts | 1 | ||||
| -rw-r--r-- | packages/backend/test/e2e/clips.ts | 9 | ||||
| -rw-r--r-- | packages/backend/test/e2e/endpoints.ts | 1 | ||||
| -rw-r--r-- | packages/backend/test/e2e/fetch-resource.ts | 3 | ||||
| -rw-r--r-- | packages/backend/test/e2e/ff-visibility.ts | 1 | ||||
| -rw-r--r-- | packages/backend/test/e2e/timelines.ts | 2478 | ||||
| -rw-r--r-- | packages/backend/test/e2e/well-known.ts | 3 | ||||
| -rw-r--r-- | packages/backend/test/unit/NoteCreateService.ts | 1 | ||||
| -rw-r--r-- | packages/backend/test/unit/misc/is-renote.ts | 1 | ||||
| -rw-r--r-- | packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts | 652 | ||||
| -rw-r--r-- | packages/backend/test/utils.ts | 10 |
11 files changed, 2096 insertions, 1064 deletions
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 4dbeacf925..1bbacd065b 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -673,7 +673,6 @@ describe('アンテナ', () => { assert.deepStrictEqual(response, expected); }); - test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { }); test.each([ { label: 'ID指定', offsetBy: 'id' }, diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index 570cc61c4b..fe9a217ee8 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -363,14 +363,11 @@ describe('クリップ', () => { const clipLimit = DEFAULT_POLICIES.clipLimit; const clips = await createMany({}, clipLimit); const res = await list({ - parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる + parameters: { limit: clips.length }, }); - // 返ってくる配列には順序保障がないのでidでソートして厳密比較 - assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), - clips.sort(compareBy(s => s.id)), - ); + // 作成responseの配列には順序保障がないのでidでソートして厳密比較 + assert.deepStrictEqual(res.toReversed(), clips.sort(compareBy(s => s.id))); }); test('の一覧が取得できる(空)', async () => { diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index b52162a687..469f19e2b9 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -24,6 +24,7 @@ describe('Endpoints', () => { bob = await signup({ username: 'bob' }); carol = await signup({ username: 'carol' }); dave = await signup({ username: 'dave' }); + await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse); }, 1000 * 60 * 2); describe('signup', () => { diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 740295bda8..bef98893c6 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -6,7 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; +import { api, channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js'; import type * as misskey from 'misskey-js'; @@ -78,6 +78,7 @@ describe('Webリソース', () => { beforeAll(async () => { alice = await signup({ username: 'alice' }); + await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse); aliceUploadedFile = (await uploadFile(alice)).body; alicesPost = await post(alice, { text: 'test', diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 5d0c70a3c2..02582ae815 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -16,6 +16,7 @@ describe('FF visibility', () => { beforeAll(async () => { alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); + await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse); }, 1000 * 60 * 2); test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index e53c3d8f34..4f7d1a4d69 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -9,6 +9,7 @@ import * as assert from 'assert'; import { setTimeout } from 'node:timers/promises'; import { Redis } from 'ioredis'; +import { SignupResponse, Note, UserList } from 'misskey-js/entities.js'; import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js'; import { loadConfig } from '@/config.js'; @@ -16,1554 +17,1929 @@ function genHost() { return randomString() + '.example.com'; } -function waitForPushToTl() { - return setTimeout(500); -} - let redisForTimelines: Redis; +let root: SignupResponse; describe('Timelines', () => { - beforeAll(() => { + beforeAll(async () => { redisForTimelines = new Redis(loadConfig().redisForTimelines); - }); + root = await signup({ username: 'root' }); + }, 1000 * 60 * 2); - describe('Home TL', () => { - test.concurrent('自分の visibility: followers なノートが含まれる', async () => { - const [alice] = await Promise.all([signup()]); + describe.each([ + { enableFanoutTimeline: true }, + { enableFanoutTimeline: false }, + ])('Timelines (enableFanoutTimeline: $enableFanoutTimeline)', ({ enableFanoutTimeline }) => { + function waitForPushToTl() { + return setTimeout(250); + } - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + beforeAll(async () => { + await api('admin/update-meta', { enableFanoutTimeline }, root); + }, 1000 * 60 * 2); - await waitForPushToTl(); + describe('Home TL', () => { + test('自分の visibility: followers なノートが含まれる', async () => { + const [alice] = await Promise.all([signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); - }); + await waitForPushToTl(); - test.concurrent('フォローしているユーザーのノートが含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi' }); - const carolNote = await post(carol, { text: 'hi' }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + }); - await waitForPushToTl(); + test('フォローしているユーザーのノートが含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi' }); + const carolNote = await post(carol, { text: 'hi' }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - const carolNote = await post(carol, { text: 'hi' }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + const carolNote = await post(carol, { text: 'hi' }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('withReplies: false でフォローしているユーザーの他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('withReplies: true でフォローしているユーザーの他人への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - const res = await api('notes/timeline', { limit: 100 }, alice); + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await api('following/create', { userId: bob.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - test.concurrent('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await waitForPushToTl(); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); + const res = await api('notes/timeline', { limit: 100 }, alice); - await waitForPushToTl(); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - const res = await api('notes/timeline', { limit: 100 }, alice); + test('withReplies: true でフォローしているユーザーの他人へのDM返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await api('following/create', { userId: bob.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); - test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await waitForPushToTl(); - await api('following/create', { userId: carol.id }, bob); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + const res = await api('notes/timeline', { limit: 100 }, alice); - await waitForPushToTl(); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - const res = await api('notes/timeline', { limit: 100 }, alice); + test('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await api('following/create', { userId: carol.id }, bob); + await api('following/create', { userId: bob.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await waitForPushToTl(); - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/create', { userId: carol.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + const res = await api('notes/timeline', { limit: 100 }, alice); - await waitForPushToTl(); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - const res = await api('notes/timeline', { limit: 100 }, alice); + test('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); - }); + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, alice); + await api('following/create', { userId: carol.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + await waitForPushToTl(); - await waitForPushToTl(); + const res = await api('notes/timeline', { limit: 100 }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); + }); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - }); + test('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: alice.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - }); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }); - test.concurrent('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }); - test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('withReplies: false でフォローしているユーザーのそのユーザー自身への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }); - test.concurrent('自分の他人への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - const bobNote = await post(bob, { text: 'hi' }); - const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id }); + const [alice, bob] = await Promise.all([signup(), signup()]); - await waitForPushToTl(); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - const res = await api('notes/timeline', { limit: 100 }, alice); + await waitForPushToTl(); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - }); + const res = await api('notes/timeline', { limit: 100 }, alice); - test.concurrent('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { renoteId: carolNote.id }); + test('自分の他人への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - await waitForPushToTl(); + const [alice, bob] = await Promise.all([signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + const bobNote = await post(bob, { text: 'hi' }); + const aliceNote = await post(alice, { text: 'hi', replyId: bobNote.id }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { renoteId: carolNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + }); - await waitForPushToTl(); + test('フォローしているユーザーの他人の投稿のリノートが含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { - withRenotes: false, - }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { renoteId: carolNote.id }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('[withRenotes: false] フォローしているユーザーの投稿が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { - withRenotes: false, - }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi' }); + const carolNote = await post(carol, { text: 'hi' }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { + limit: 100, + withRenotes: false, + }, alice); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('[withRenotes: false] フォローしているユーザーのファイルのみの投稿が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const [bobFile, carolFile] = await Promise.all([ + uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), + uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), + ]); + const bobNote = await post(bob, { fileIds: [bobFile.id] }); + const carolNote = await post(carol, { fileIds: [carolFile.id] }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { + limit: 100, + withRenotes: false, + }, alice); - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('[withRenotes: false] フォローしているユーザーの他人の投稿のリノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { renoteId: carolNote.id }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { + withRenotes: false, + }, alice); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('[withRenotes: false] フォローしているユーザーの他人の投稿の引用が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、フォローしているユーザーによるリノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); + const res = await api('notes/timeline', { + withRenotes: false, + }, alice); - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('フォローしているユーザーの他人への visibility: specified なノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、フォローしているユーザーによるリノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - await waitForPushToTl(); + test('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - const bobNote = await post(bob, { text: 'hi' }); + test('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await waitForPushToTl(); + await api('following/create', { userId: bob.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - const res = await api('notes/timeline', { limit: 100 }, alice); + await waitForPushToTl(); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + const res = await api('notes/timeline', { limit: 100 }, alice); - test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); + test('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、フォローしているユーザーによるリノートが含まれない', async () => { + const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + await api('following/create', { userId: bob.id }, alice); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); + const bobNote = await post(bob, { renoteId: daveNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100 }, alice); + const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + test('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、フォローしているユーザーによるリノートが含まれない', async () => { + const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const [bobFile, carolFile] = await Promise.all([ - uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), - uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), - ]); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [bobFile.id] }); - const carolNote1 = await post(carol, { text: 'hi' }); - const carolNote2 = await post(carol, { fileIds: [carolFile.id] }); + await api('following/create', { userId: bob.id }, alice); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); + const bobNote = await post(bob, { renoteId: daveNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice); + const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); - }, 1000 * 30); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('フォローしているリモートユーザーのノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); + await api('following/create', { userId: bob.id }, alice); - await waitForPushToTl(); + const bobNote = await post(bob, { text: 'hi' }); - const res = await api('notes/timeline', { limit: 100 }, alice); + await waitForPushToTl(); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + const res = await api('notes/timeline', { limit: 100 }, alice); - test.concurrent('自分の visibility: specified なノートが含まれる', async () => { - const [alice] = await Promise.all([signup()]); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); + test('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - await waitForPushToTl(); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); + await api('following/create', { userId: bob.id }, alice); - const res = await api('notes/timeline', { limit: 100 }, alice); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); - }); + await waitForPushToTl(); - test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - await waitForPushToTl(); + test('[withFiles: true] フォローしているユーザーのファイル付きノートのみ含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const [bobFile, carolFile] = await Promise.all([ + uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), + uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), + ]); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [bobFile.id] }); + const carolNote1 = await post(carol, { text: 'hi' }); + const carolNote2 = await post(carol, { fileIds: [carolFile.id] }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - }); + await waitForPushToTl(); - test.concurrent('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); + }, 1000 * 30); - await waitForPushToTl(); + test('フォローしているユーザーのチャンネル投稿が含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - await waitForPushToTl(); + test('自分の visibility: specified なノートが含まれる', async () => { + const [alice] = await Promise.all([signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - const aliceNote = await post(alice, { text: 'ok', visibility: 'specified', visibleUserIds: [bob.id], replyId: bobNote.id }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + }); - await waitForPushToTl(); + test('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok'); - }); + await waitForPushToTl(); - /* TODO - test.concurrent('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); - const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + }); - await waitForPushToTl(); + test('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok'); - }); - */ + await waitForPushToTl(); - // ↑の挙動が理想だけど実装が面倒かも - test.concurrent('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); - const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - await waitForPushToTl(); + test('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/timeline', { limit: 100 }, alice); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('FTT: ローカルユーザーの HTL にはプッシュされる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { - userId: alice.id, - }, bob); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - const aliceNote = await post(alice, { text: 'I\'m Alice.' }); - const bobNote = await post(bob, { text: 'I\'m Bob.' }); - const carolNote = await post(carol, { text: 'I\'m Carol.' }); + test('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - await waitForPushToTl(); + const [alice, bob] = await Promise.all([signup(), signup()]); - // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); + const aliceNote = await post(alice, { text: 'ok', visibility: 'specified', visibleUserIds: [bob.id], replyId: bobNote.id }); - const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1); - assert.strictEqual(bobHTL.includes(aliceNote.id), true); - assert.strictEqual(bobHTL.includes(bobNote.id), true); - assert.strictEqual(bobHTL.includes(carolNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('FTT: リモートユーザーの HTL にはプッシュされない', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('following/create', { - userId: alice.id, - }, bob); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok'); + }); - await post(alice, { text: 'I\'m Alice.' }); - await post(bob, { text: 'I\'m Bob.' }); + /* TODO + test('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); + const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); + await waitForPushToTl(); + const res = await api('notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok'); + }); + */ - await waitForPushToTl(); + // ↑の挙動が理想だけど実装が面倒かも + test('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる - assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); - }); - }); + const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] }); + const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id }); - describe('Local TL', () => { - test.concurrent('visibility: home なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await waitForPushToTl(); - const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); - const bobNote = await post(bob, { text: 'hi' }); + const res = await api('notes/timeline', { limit: 100 }, alice); - await waitForPushToTl(); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + test('FTT: ローカルユーザーの HTL にはプッシュされる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await api('following/create', { + userId: alice.id, + }, bob); - test.concurrent('他人の他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const aliceNote = await post(alice, { text: 'I\'m Alice.' }); + const bobNote = await post(bob, { text: 'I\'m Bob.' }); + const carolNote = await post(carol, { text: 'I\'m Carol.' }); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + await waitForPushToTl(); - await waitForPushToTl(); + if (enableFanoutTimeline) { + // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる + assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1); + assert.strictEqual(bobHTL.includes(aliceNote.id), true); + assert.strictEqual(bobHTL.includes(bobNote.id), true); + assert.strictEqual(bobHTL.includes(carolNote.id), false); + } else { + assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); + } + }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - }); + test('FTT: リモートユーザーの HTL にはプッシュされない', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - test.concurrent('他人のその人自身への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + await api('following/create', { + userId: alice.id, + }, bob); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + await post(alice, { text: 'I\'m Alice.' }); + await post(bob, { text: 'I\'m Bob.' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + // NOTE: notes/timeline だと DB へのフォールバックが効くので Redis を直接見て確かめる + assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); + }); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }); + describe('凍結', () => { + let alice: SignupResponse, bob: SignupResponse, carol: SignupResponse; + let aliceNote: Note, bobNote: Note, carolNote: Note; - test.concurrent('チャンネル投稿が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + beforeAll(async () => { + [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, alice); + aliceNote = await post(alice, { text: 'hi' }); + bobNote = await post(bob, { text: 'yo' }); + carolNote = await post(carol, { text: 'kon\'nichiwa' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + await api('admin/suspend-user', { userId: carol.id }, root); + await setTimeout(100); + }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + test('凍結後に凍結されたユーザーのノートは見えなくなる', async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - test.concurrent('リモートユーザーのノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + test('凍結解除後に凍結されていたユーザーのノートは見えるようになる', async () => { + await api('admin/unsuspend-user', { userId: carol.id }, root); + await setTimeout(100); - const bobNote = await post(bob, { text: 'hi' }); + const res = await api('notes/timeline', { limit: 100 }, alice); - await waitForPushToTl(); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'kon\'nichiwa'); + }); + }); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + describe('凍結 (Renote)', () => { + let alice: SignupResponse, bob: SignupResponse, carol: SignupResponse; + let aliceNote: Note, bobNote: Note, carolNote: Note, bobRenote: Note, carolRenote: Note; - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + beforeAll(async () => { + [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - // 含まれても良いと思うけど実装が面倒なので含まれない - test.concurrent('フォローしているユーザーの visibility: home なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, alice); + aliceNote = await post(alice, { text: 'hi' }); + bobNote = await post(bob, { text: 'yo' }); + carolNote = await post(carol, { text: 'kon\'nichiwa' }); + bobRenote = await post(bob, { renoteId: carolNote.id }); + carolRenote = await post(carol, { renoteId: bobNote.id }); - await api('following/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); - const bobNote = await post(bob, { text: 'hi' }); + await waitForPushToTl(); - await waitForPushToTl(); + await api('admin/suspend-user', { userId: carol.id }, root); + await setTimeout(100); + }); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + test('凍結後に凍結されたユーザーに対するRenoteや凍結されたユーザーのRenoteが見えなくなる', async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobRenote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false); + }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + test('凍結解除後に凍結されていたユーザーに対するRenoteや凍結されたユーザーのRenoteが見えるようになる', async () => { + await api('admin/unsuspend-user', { userId: carol.id }, root); + await setTimeout(100); - test.concurrent('ミュートしているユーザーのノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/timeline', { limit: 100 }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi' }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true); + }); + }); - await waitForPushToTl(); + describe('凍結(リモート)', () => { + let alice: SignupResponse, bob: SignupResponse, carol: SignupResponse; + let aliceNote: Note, bobNote: Note, carolNote: Note; - const res = await api('notes/local-timeline', { limit: 100 }, alice); + beforeAll(async () => { + [alice, bob, carol] = await Promise.all([signup(), signup({ host: genHost() }), signup({ host: genHost() })]); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, alice); + aliceNote = await post(alice, { text: 'hi' }); + bobNote = await post(bob, { text: 'yo' }); + carolNote = await post(carol, { text: 'kon\'nichiwa' }); + + await waitForPushToTl(); - test.concurrent('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await api('admin/suspend-user', { userId: carol.id }, root); + await setTimeout(100); + }); - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + test('凍結後に凍結されたユーザーのノートは見えなくなる', async () => { + const res = await api('notes/timeline', { limit: 100 }, alice); - await waitForPushToTl(); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + test('凍結解除後に凍結されていたユーザーのノートは見えるようになる', async () => { + await api('admin/unsuspend-user', { userId: carol.id }, root); + await setTimeout(100); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + const res = await api('notes/timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }); + }); }); - test.concurrent('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + describe('Local TL', () => { + test('visibility: home なノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); + const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); + test('他人の他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }); - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); + test('他人のその人自身への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); - assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }); - test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('チャンネル投稿が含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('リモートユーザーのノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + // 含まれても良いと思うけど実装が面倒なので含まれない + test('フォローしているユーザーの visibility: home なノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + await api('following/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); + const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('ミュートしているユーザーのノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [file.id] }); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - describe('Social TL', () => { - test.concurrent('ローカルユーザーのノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('フォローしているユーザーが行ったミュートしているユーザーのリノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const bobNote = await post(bob, { text: 'hi' }); + await api('following/create', { userId: bob.id }, alice); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - test.concurrent('ローカルユーザーの visibility: home なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('withReplies: true でフォローしているユーザーが行ったミュートしているユーザーの投稿への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + await api('following/create', { userId: bob.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - test.concurrent('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => { + const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); + const bobNote = await post(bob, { renoteId: daveNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => { + const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); + const bobNote = await post(bob, { renoteId: daveNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + test('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - await api('following/create', { userId: carol.id }, bob); - await api('following/create', { userId: bob.id }, alice); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + const [alice, bob] = await Promise.all([signup(), signup()]); - await waitForPushToTl(); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + await waitForPushToTl(); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); - }); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: carol.id }, alice); - await api('following/create', { userId: carol.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + test('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - await waitForPushToTl(); + const [alice, bob] = await Promise.all([signup(), signup()]); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); - }); + await waitForPushToTl(); - test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - await api('following/create', { userId: bob.id }, alice); - await api('following/create', { userId: alice.id }, bob); - await api('following/update', { userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - await waitForPushToTl(); + test('[withReplies: true] 他人の他人への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - }); + await waitForPushToTl(); - test.concurrent('他人の他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - await waitForPushToTl(); + test('[withFiles: true] ファイル付きノートのみ含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); - }); + await waitForPushToTl(); - test.concurrent('リモートユーザーのノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice); - const bobNote = await post(bob, { text: 'hi' }); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, 1000 * 10); - await waitForPushToTl(); + describe('凍結', () => { + let alice: SignupResponse, bob: SignupResponse, carol: SignupResponse; + let aliceNote: Note, bobNote: Note, carolNote: Note; - const res = await api('notes/local-timeline', { limit: 100 }, alice); + beforeAll(async () => { + [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + aliceNote = await post(alice, { text: 'hi' }); + bobNote = await post(bob, { text: 'yo' }); + carolNote = await post(carol, { text: 'kon\'nichiwa' }); - test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + await waitForPushToTl(); + }); - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); + test('凍結後に凍結されたユーザーのノートは見えなくなる', async () => { + await api('admin/suspend-user', { userId: carol.id }, root); + await setTimeout(100); - const bobNote = await post(bob, { text: 'hi' }); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - await waitForPushToTl(); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + test('凍結解除後に凍結されていたユーザーのノートは見えるようになる', async () => { + await api('admin/unsuspend-user', { userId: carol.id }, root); + await setTimeout(100); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + const res = await api('notes/local-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'kon\'nichiwa'); + }); + }); }); - test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + describe('Social TL', () => { + test('ローカルユーザーのノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); - await api('following/create', { userId: bob.id }, alice); + const bobNote = await post(bob, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + await waitForPushToTl(); - await waitForPushToTl(); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + test('ローカルユーザーの visibility: home なノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + await waitForPushToTl(); - await waitForPushToTl(); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/local-timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + test('フォローしているローカルユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + await waitForPushToTl(); - await waitForPushToTl(); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + test('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const [alice, bob] = await Promise.all([signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [file.id] }); + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); - }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - describe('User List TL', () => { - test.concurrent('リスインしているフォローしていないユーザーのノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi' }); + await api('following/create', { userId: carol.id }, bob); + await api('following/create', { userId: bob.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.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); + }); - test.concurrent('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await waitForPushToTl(); + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, alice); + await api('following/create', { userId: carol.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + await waitForPushToTl(); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); + }); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + test('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - await waitForPushToTl(); + const [alice, bob] = await Promise.all([signup(), signup()]); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: alice.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + }); - await waitForPushToTl(); + test('他人の他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await waitForPushToTl(); - test.concurrent('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }); - await waitForPushToTl(); + test('リモートユーザーのノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const bobNote = await post(bob, { text: 'hi' }); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }); + await waitForPushToTl(); - test.concurrent('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - await waitForPushToTl(); + test('フォローしているリモートユーザーのノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); + await api('following/create', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + const bobNote = await post(bob, { text: 'hi' }); - test.concurrent('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await waitForPushToTl(); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - await waitForPushToTl(); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + test('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); + await api('following/create', { userId: bob.id }, alice); - test.concurrent('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + await waitForPushToTl(); - await waitForPushToTl(); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + test('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; - test.concurrent('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - test.concurrent('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('[withReplies: true] 他人の他人への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - test.concurrent('リスインしている自分の visibility: followers なノートが含まれる', async () => { - const [alice] = await Promise.all([signup(), signup()]); + test('[withFiles: true] ファイル付きノートのみ含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: alice.id }, alice); - await setTimeout(1000); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, 1000 * 10); - test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + describe('凍結', () => { + /* + * bob = 未フォローのローカルユーザー (凍結対象でない) + * carol = 未フォローのローカルユーザー (凍結対象) + * dave = フォローしているローカルユーザー (凍結対象) + */ + let alice: SignupResponse, bob: SignupResponse, carol: SignupResponse, dave: SignupResponse; + let aliceNote: Note, bobNote: Note, carolNote: Note, daveNote: Note; - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + beforeAll(async () => { + [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); - await waitForPushToTl(); + await api('following/create', { userId: dave.id }, alice); + aliceNote = await post(alice, { text: 'hi' }); + bobNote = await post(bob, { text: 'yo' }); + carolNote = await post(carol, { text: 'kon\'nichiwa' }); + daveNote = await post(dave, { text: 'hello' }); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + await waitForPushToTl(); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + await api('admin/suspend-user', { userId: carol.id }, root); + await api('admin/suspend-user', { userId: dave.id }, root); + await setTimeout(250); + }); - test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('凍結後に凍結されたユーザーのノートは見えなくなる', async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [file.id] }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === daveNote.id), false); + }); - await waitForPushToTl(); + test('凍結解除後に凍結されていたユーザーのノートは見えるようになる', async () => { + await api('admin/unsuspend-user', { userId: carol.id }, root); + await api('admin/unsuspend-user', { userId: dave.id }, root); + await setTimeout(250); - const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === daveNote.id), true); + }); + }); - test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + describe('凍結 (リモート)', () => { + /* + * carol = 未フォローのリモートユーザー (凍結対象) + * elle = フォローしているリモートユーザー (凍結対象) + */ + let alice: SignupResponse, carol: SignupResponse, elle: SignupResponse; + let aliceNote: Note, carolNote: Note, elleNote: Note; - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); + beforeAll(async () => { + [alice, carol, elle] = await Promise.all([signup(), signup({ host: genHost() }), signup({ host: genHost() })]); - await waitForPushToTl(); + await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' }); + await api('following/create', { userId: elle.id }, alice); + aliceNote = await post(alice, { text: 'hi' }); + carolNote = await post(carol, { text: 'kon\'nichiwa' }); + elleNote = await post(elle, { text: 'hi there' }); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + await waitForPushToTl(); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - }); + await api('admin/suspend-user', { userId: carol.id }, root); + await api('admin/suspend-user', { userId: elle.id }, root); + await setTimeout(250); + }); - test.concurrent('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + test('凍結後に凍結されたユーザーのノートは見えなくなる', async () => { + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === elleNote.id), false); + }); - await waitForPushToTl(); + test('凍結解除後に凍結されていたユーザーのノートは見えるようになる', async () => { + await api('admin/unsuspend-user', { userId: carol.id }, root); + await api('admin/unsuspend-user', { userId: elle.id }, root); + await setTimeout(250); - const res = await api('notes/user-list-timeline', { listId: list.id }, alice); + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === elleNote.id), true); + }); + }); }); - }); - describe('User TL', () => { - test.concurrent('ノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + describe('User List TL', () => { + test('リスインしているフォローしていないユーザーのノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const bobNote = await post(bob, { text: 'hi' }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('リスインしているフォローしていないユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('自身の visibility: followers なノートが含まれる', async () => { - const [alice] = await Promise.all([signup()]); + test('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: alice.id }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('チャンネル投稿が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('リスインしているフォローしていないユーザーのユーザー自身への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }); - test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + test('withReplies: false でリスインしているフォローしていないユーザーからの自分への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + test('withReplies: false でリスインしているフォローしていないユーザーの他人への返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + test('withReplies: true でリスインしているフォローしていないユーザーの他人への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified' }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('リスインしているフォローしているユーザーの visibility: home なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { fileIds: [file.id] }); + await api('following/create', { userId: bob.id }, alice); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('リスインしているフォローしているユーザーの visibility: followers なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + await api('following/create', { userId: bob.id }, alice); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + }); - test.concurrent('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + test('リスインしている自分の visibility: followers なノートが含まれる', async () => { + const [alice] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: alice.id }, alice); + await setTimeout(250); + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + }); - test.concurrent('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => { - const [bob] = await Promise.all([signup()]); + test('リスインしているユーザーのチャンネルノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); - const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => { - const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + test('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, 1000 * 10); - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); + test('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + }); - test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => { - const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); + test('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await api('following/create', { userId: bob.id }, alice); - await api('mute/create', { userId: carol.id }, alice); - await setTimeout(1000); - const carolNote = await post(carol, { text: 'hi' }); - const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); - const bobNote = await post(bob, { renoteId: daveNote.id }); + const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); - test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + describe('凍結', () => { + let alice: SignupResponse, bob: SignupResponse, carol: SignupResponse; + let aliceNote: Note, bobNote: Note, carolNote: Note; + let list: UserList; - await api('mute/create', { userId: bob.id }, alice); - await setTimeout(1000); - const bobNote1 = await post(bob, { text: 'hi' }); - const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); - const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); - const bobNote4 = await post(bob, { renoteId: bobNote2.id }); - const bobNote5 = await post(bob, { renoteId: bobNote3.id }); + beforeAll(async () => { + [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); - await waitForPushToTl(); + list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); - const res = await api('users/notes', { userId: bob.id }, alice); + await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); + await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); + aliceNote = await post(alice, { text: 'hi' }); + bobNote = await post(bob, { text: 'yo' }); + carolNote = await post(carol, { text: 'kon\'nichiwa' }); - assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote4.id), true); - assert.strictEqual(res.body.some(note => note.id === bobNote5.id), true); - }); + await waitForPushToTl(); + + await api('admin/suspend-user', { userId: carol.id }, root); + await setTimeout(100); + }); - test.concurrent('自身の visibility: specified なノートが含まれる', async () => { - const [alice] = await Promise.all([signup()]); + test('凍結後に凍結されたユーザーのノートは見えなくなる', async () => { + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + }); - await waitForPushToTl(); + test('凍結解除後に凍結されていたユーザーのノートは見えるようになる', async () => { + await api('admin/unsuspend-user', { userId: carol.id }, root); + await setTimeout(100); - const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice); + const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + }); + }); }); - test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => { - const [alice, bob] = await Promise.all([signup(), signup()]); + describe('User TL', () => { + test('ノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const bobNote = await post(bob, { text: 'hi', visibility: 'specified' }); + const bobNote = await post(bob, { text: 'hi' }); - await waitForPushToTl(); + await waitForPushToTl(); - const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); + const res = await api('users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); - }); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); - /** @see https://github.com/misskey-dev/misskey/issues/14000 */ - test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId による絞り込みが正しく動作する', async () => { - const alice = await signup(); - const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); - const note1 = await post(alice, { text: '1' }); - const note2 = await post(alice, { text: '2' }); - await redisForTimelines.del('list:userTimeline:' + alice.id); - const note3 = await post(alice, { text: '3' }); + test('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); - const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id }); - assert.deepStrictEqual(res.body, [note1, note2, note3]); - }); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + test('フォローしているユーザーの visibility: followers なノートが含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('following/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + }); + + test('自身の visibility: followers なノートが含まれる', async () => { + const [alice] = await Promise.all([signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: alice.id }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); + }); + + test('チャンネル投稿が含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + test('[withReplies: false] 他人への返信が含まれない', async () => { + /* FIXME: https://github.com/misskey-dev/misskey/issues/12065 */ if (!enableFanoutTimeline) return; + + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); + }); + + test('[withReplies: true] 他人への返信が含まれる', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }); + + test('[withReplies: true] 他人への visibility: specified な返信が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + const carolNote = await post(carol, { text: 'hi' }); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified' }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); + }); + + test('[withFiles: true] ファイル付きノートのみ含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { fileIds: [file.id] }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + }, 1000 * 10); + + test('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); - test.concurrent('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => { - const alice = await signup(); - const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); - const note1 = await post(alice, { text: '1' }); - const note2 = await post(alice, { text: '2' }); - await redisForTimelines.del('list:userTimeline:' + alice.id); - const note3 = await post(alice, { text: '3' }); - const noteUntil = await post(alice, { text: 'Note where id will be `untilId`.' }); - await post(alice, { text: '4' }); + const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id }); - assert.deepStrictEqual(res.body, [note3, note2, note1]); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); + + test('[withChannelNotes: true] 他人が取得した場合センシティブチャンネル投稿が含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + test('[withChannelNotes: true] 自分が取得した場合センシティブチャンネル投稿が含まれる', async () => { + const [bob] = await Promise.all([signup()]); + + const channel = await api('channels/create', { name: 'channel', isSensitive: true }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); + + test('ミュートしているユーザーに関連する投稿が含まれない', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + test('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => { + const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); + + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id }); + const bobNote = await post(bob, { renoteId: daveNote.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + test('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => { + const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]); + + await api('following/create', { userId: bob.id }, alice); + await api('mute/create', { userId: carol.id }, alice); + await setTimeout(250); + const carolNote = await post(carol, { text: 'hi' }); + const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id }); + const bobNote = await post(bob, { renoteId: daveNote.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + test('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('mute/create', { userId: bob.id }, alice); + await setTimeout(250); + const bobNote1 = await post(bob, { text: 'hi' }); + const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); + const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); + const bobNote4 = await post(bob, { renoteId: bobNote2.id }); + const bobNote5 = await post(bob, { renoteId: bobNote3.id }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote4.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote5.id), true); + }); + + test('自身の visibility: specified なノートが含まれる', async () => { + const [alice] = await Promise.all([signup()]); + + const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + }); + + test('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const bobNote = await post(bob, { text: 'hi', visibility: 'specified' }); + + await waitForPushToTl(); + + const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); + + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + /** @see https://github.com/misskey-dev/misskey/issues/14000 */ + test('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId による絞り込みが正しく動作する', async () => { + const alice = await signup(); + const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); + const note1 = await post(alice, { text: '1' }); + const note2 = await post(alice, { text: '2' }); + await redisForTimelines.del('list:userTimeline:' + alice.id); + const note3 = await post(alice, { text: '3' }); + + const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id }); + assert.deepStrictEqual(res.body, [note1, note2, note3]); + }); + + test('FTT: sinceId にキャッシュより古いノートを指定しても、sinceId と untilId による絞り込みが正しく動作する', async () => { + const alice = await signup(); + const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); + const note1 = await post(alice, { text: '1' }); + const note2 = await post(alice, { text: '2' }); + await redisForTimelines.del('list:userTimeline:' + alice.id); + const note3 = await post(alice, { text: '3' }); + const noteUntil = await post(alice, { text: 'Note where id will be `untilId`.' }); + await post(alice, { text: '4' }); + + const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id }); + assert.deepStrictEqual(res.body, [note3, note2, note1]); + }); }); - }); - // TODO: リノートミュート済みユーザーのテスト - // TODO: ページネーションのテスト + // TODO: リノートミュート済みユーザーのテスト + // TODO: ページネーションのテスト + }); }); diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts index bdb298dfe4..538a990a4e 100644 --- a/packages/backend/test/e2e/well-known.ts +++ b/packages/backend/test/e2e/well-known.ts @@ -6,7 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { host, origin, relativeFetch, signup } from '../utils.js'; +import { api, host, origin, relativeFetch, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('.well-known', () => { @@ -14,6 +14,7 @@ describe('.well-known', () => { beforeAll(async () => { alice = await signup({ username: 'alice' }); + await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse); }, 1000 * 60 * 2); test('nodeinfo', async () => { diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts index f2d4c8ffbb..23f409420e 100644 --- a/packages/backend/test/unit/NoteCreateService.ts +++ b/packages/backend/test/unit/NoteCreateService.ts @@ -40,6 +40,7 @@ describe('NoteCreateService', () => { renoteCount: 0, repliesCount: 0, clippedCount: 0, + pageCount: 0, reactions: {}, visibility: 'public', uri: null, diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts index 0b713e8bf6..74d17abcb6 100644 --- a/packages/backend/test/unit/misc/is-renote.ts +++ b/packages/backend/test/unit/misc/is-renote.ts @@ -23,6 +23,7 @@ const base: MiNote = { renoteCount: 0, repliesCount: 0, clippedCount: 0, + pageCount: 0, reactions: {}, visibility: 'public', uri: null, diff --git a/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts new file mode 100644 index 0000000000..631e160afc --- /dev/null +++ b/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts @@ -0,0 +1,652 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { jest } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import ms from 'ms'; +import { + type MiNote, + type MiUser, + type NotesRepository, + type NoteFavoritesRepository, + type UserNotePiningsRepository, + type UsersRepository, + type UserProfilesRepository, + MiMeta, +} from '@/models/_.js'; +import { CleanRemoteNotesProcessorService } from '@/queue/processors/CleanRemoteNotesProcessorService.js'; +import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueLoggerService } from '@/queue/QueueLoggerService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; + +describe('CleanRemoteNotesProcessorService', () => { + let app: TestingModule; + let service: CleanRemoteNotesProcessorService; + let idService: IdService; + let notesRepository: NotesRepository; + let noteFavoritesRepository: NoteFavoritesRepository; + let userNotePiningsRepository: UserNotePiningsRepository; + let usersRepository: UsersRepository; + let userProfilesRepository: UserProfilesRepository; + + // Local user + let alice: MiUser; + // Remote user 1 + let bob: MiUser; + // Remote user 2 + let carol: MiUser; + + const meta = new MiMeta(); + + // Mock job object + const createMockJob = () => ({ + log: jest.fn(), + updateProgress: jest.fn(), + }); + + async function createUser(data: Partial<MiUser> = {}) { + const id = idService.gen(); + const un = data.username || secureRndstr(16); + const user = await usersRepository + .insert({ + id, + username: un, + usernameLower: un.toLowerCase(), + ...data, + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); + + await userProfilesRepository.save({ + userId: id, + }); + + return user; + } + + async function createNote(data: Partial<MiNote>, user: MiUser, time?: number): Promise<MiNote> { + const id = idService.gen(time); + const note = await notesRepository + .insert({ + id: id, + text: `note_${id}`, + userId: user.id, + userHost: user.host, + visibility: 'public', + ...data, + }) + .then(x => notesRepository.findOneByOrFail(x.identifiers[0])); + return note; + } + + beforeAll(async () => { + app = await Test + .createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + CleanRemoteNotesProcessorService, + IdService, + { + provide: QueueLoggerService, + useFactory: () => ({ + logger: { + createSubLogger: () => ({ + info: jest.fn(), + warn: jest.fn(), + succ: jest.fn(), + }), + }, + }), + }, + ], + }) + .overrideProvider(DI.meta).useFactory({ factory: () => meta }) + .compile(); + + service = app.get(CleanRemoteNotesProcessorService); + idService = app.get(IdService); + notesRepository = app.get(DI.notesRepository); + noteFavoritesRepository = app.get(DI.noteFavoritesRepository); + userNotePiningsRepository = app.get(DI.userNotePiningsRepository); + usersRepository = app.get(DI.usersRepository); + userProfilesRepository = app.get(DI.userProfilesRepository); + + alice = await createUser({ username: 'alice', host: null }); + bob = await createUser({ username: 'bob', host: 'remote1.example.com' }); + carol = await createUser({ username: 'carol', host: 'remote2.example.com' }); + + app.enableShutdownHooks(); + }); + + beforeEach(() => { + // Reset mocks + jest.clearAllMocks(); + + // Set default meta values + meta.enableRemoteNotesCleaning = true; + meta.remoteNotesCleaningMaxProcessingDurationInMinutes = 0.3; + meta.remoteNotesCleaningExpiryDaysForEachNotes = 30; + }, 60 * 1000); + + afterEach(async () => { + // Clean up test data + await Promise.all([ + notesRepository.createQueryBuilder().delete().execute(), + userNotePiningsRepository.createQueryBuilder().delete().execute(), + noteFavoritesRepository.createQueryBuilder().delete().execute(), + ]); + }, 60 * 1000); + + afterAll(async () => { + await app.close(); + }); + + describe('basic', () => { + test('should skip cleaning when enableRemoteNotesCleaning is false', async () => { + meta.enableRemoteNotesCleaning = false; + const job = createMockJob(); + + const result = await service.process(job as any); + + expect(result).toEqual({ + deletedCount: 0, + oldest: null, + newest: null, + skipped: true, + transientErrors: 0, + }); + }); + + test('should return success result when enableRemoteNotesCleaning is true and no notes to clean', async () => { + const job = createMockJob(); + + await createNote({}, alice); + const result = await service.process(job as any); + + expect(result).toEqual({ + deletedCount: 0, + oldest: null, + newest: null, + skipped: false, + transientErrors: 0, + }); + }, 3000); + + test('should clean remote notes and return stats', async () => { + // Remote notes + const remoteNotes = await Promise.all([ + createNote({}, bob), + createNote({}, carol), + createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000), + createNote({}, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000), // Note older than expiry + ]); + + // Local notes + const localNotes = await Promise.all([ + createNote({}, alice), + createNote({}, alice, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000), + ]); + + const job = createMockJob(); + + const result = await service.process(job as any); + + expect(result).toEqual({ + deletedCount: 2, + oldest: expect.any(Number), + newest: expect.any(Number), + skipped: false, + transientErrors: 0, + }); + + // Check side-by-side from all notes + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.length).toBe(4); + expect(remainingNotes.some(n => n.id === remoteNotes[0].id)).toBe(true); + expect(remainingNotes.some(n => n.id === remoteNotes[1].id)).toBe(true); + expect(remainingNotes.some(n => n.id === remoteNotes[2].id)).toBe(false); + expect(remainingNotes.some(n => n.id === remoteNotes[3].id)).toBe(false); + expect(remainingNotes.some(n => n.id === localNotes[0].id)).toBe(true); + expect(remainingNotes.some(n => n.id === localNotes[1].id)).toBe(true); + }); + }); + + describe('advanced', () => { + // お気に入り + test('should not delete note that is favorited by any user', async () => { + const job = createMockJob(); + + // Create old remote note that should be deleted + const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + // Favorite the note + await noteFavoritesRepository.save({ + id: idService.gen(), + userId: alice.id, + noteId: olderRemoteNote.id, + }); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); + expect(result.skipped).toBe(false); + + const remainingNote = await notesRepository.findOneBy({ id: olderRemoteNote.id }); + expect(remainingNote).not.toBeNull(); + }); + + // ピン留め + test('should not delete note that is pinned by the user', async () => { + const job = createMockJob(); + + // Create old remote note that should be deleted + const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + // Pin the note by the user who created it + await userNotePiningsRepository.save({ + id: idService.gen(), + userId: bob.id, // Same user as the note creator + noteId: olderRemoteNote.id, + }); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); + expect(result.skipped).toBe(false); + + const remainingNote = await notesRepository.findOneBy({ id: olderRemoteNote.id }); + expect(remainingNote).not.toBeNull(); + }); + + // クリップ + test('should not delete note that is clipped', async () => { + const job = createMockJob(); + + // Create old remote note that is clipped + const clippedNote = await createNote({ + clippedCount: 1, // Clipped + }, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); + expect(result.skipped).toBe(false); + + const remainingNote = await notesRepository.findOneBy({ id: clippedNote.id }); + expect(remainingNote).not.toBeNull(); + }); + + // ページ + test('should not delete note that is embedded in a page', async () => { + const job = createMockJob(); + + // Create old remote note that is embedded in a page + const clippedNote = await createNote({ + pageCount: 1, // Embedded in a page + }, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); + expect(result.skipped).toBe(false); + + const remainingNote = await notesRepository.findOneBy({ id: clippedNote.id }); + expect(remainingNote).not.toBeNull(); + }); + + // 古いreply, renoteが含まれている時の挙動 + test('should handle reply/renote relationships correctly', async () => { + const job = createMockJob(); + + // Create old remote notes with reply/renote relationships + const originalNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + const replyNote = await createNote({ + replyId: originalNote.id, + }, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000); + const renoteNote = await createNote({ + renoteId: originalNote.id, + }, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 3000); + + const result = await service.process(job as any); + + // Should delete all three notes as they are all old and remote + expect(result.deletedCount).toBe(3); + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.some(n => n.id === originalNote.id)).toBe(false); + expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(false); + expect(remainingNotes.some(n => n.id === renoteNote.id)).toBe(false); + }); + + // 古いリモートノートに新しいリプライがある時、どちらも削除されない + test('should not delete both old remote note with new reply', async () => { + const job = createMockJob(); + + // Create old remote note that should be deleted + const oldNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + // Create a reply note that is newer than the expiry period + const recentReplyNote = await createNote({ + replyId: oldNote.id, + }, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) + 1000); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); // Only the old note should be deleted + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.some(n => n.id === oldNote.id)).toBe(true); + expect(remainingNotes.some(n => n.id === recentReplyNote.id)).toBe(true); // Recent reply note should remain + }); + + // 古いリモートノートに新しいリプライと古いリプライがある時、全て残る + test('should not delete old remote note with new reply and old reply', async () => { + const job = createMockJob(); + + // Create old remote note that should be deleted + const oldNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + // Create a reply note that is newer than the expiry period + const recentReplyNote = await createNote({ + replyId: oldNote.id, + }, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) + 1000); + + // Create an old reply note that should be deleted + const oldReplyNote = await createNote({ + replyId: oldNote.id, + }, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.some(n => n.id === oldNote.id)).toBe(true); + expect(remainingNotes.some(n => n.id === recentReplyNote.id)).toBe(true); // Recent reply note should remain + expect(remainingNotes.some(n => n.id === oldReplyNote.id)).toBe(true); // Old reply note should be deleted + }); + + // リプライがお気に入りされているとき、どちらも削除されない + test('should not delete reply note that is favorited', async () => { + const job = createMockJob(); + + // Create old remote note that should be deleted + const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + // Create a reply note that is newer than the expiry period + const replyNote = await createNote({ + replyId: olderRemoteNote.id, + }, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000); + + // Favorite the reply note + await noteFavoritesRepository.save({ + id: idService.gen(), + userId: alice.id, + noteId: replyNote.id, + }); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); // Only the old note should be deleted + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true); + expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true); // Recent reply note should remain + }); + + // リプライがピン留めされているとき、どちらも削除されない + test('should not delete reply note that is pinned', async () => { + const job = createMockJob(); + + // Create old remote note that should be deleted + const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + // Create a reply note that is newer than the expiry period + const replyNote = await createNote({ + replyId: olderRemoteNote.id, + }, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000); + + // Pin the reply note + await userNotePiningsRepository.save({ + id: idService.gen(), + userId: carol.id, + noteId: replyNote.id, + }); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); // Only the old note should be deleted + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true); + expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true); // Reply note should remain + }); + + // リプライがクリップされているとき、どちらも削除されない + test('should not delete reply note that is clipped', async () => { + const job = createMockJob(); + + // Create old remote note that should be deleted + const olderRemoteNote = await createNote({}, bob, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000); + + // Create a reply note that is old but clipped + const replyNote = await createNote({ + replyId: olderRemoteNote.id, + clippedCount: 1, // Clipped + }, carol, Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 2000); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); // Both notes should be kept because reply is clipped + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.some(n => n.id === olderRemoteNote.id)).toBe(true); + expect(remainingNotes.some(n => n.id === replyNote.id)).toBe(true); + }); + + test('should handle mixed scenarios with multiple conditions', async () => { + const job = createMockJob(); + + // Create various types of notes + const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000; + + // Should be deleted: old remote note with no special conditions + const deletableNote = await createNote({}, bob, oldTime); + + // Should NOT be deleted: old remote note but favorited + const favoritedNote = await createNote({}, carol, oldTime); + await noteFavoritesRepository.save({ + id: idService.gen(), + userId: alice.id, + noteId: favoritedNote.id, + }); + + // Should NOT be deleted: old remote note but pinned + const pinnedNote = await createNote({}, bob, oldTime); + await userNotePiningsRepository.save({ + id: idService.gen(), + userId: bob.id, + noteId: pinnedNote.id, + }); + + // Should NOT be deleted: old remote note but clipped + const clippedNote = await createNote({ + clippedCount: 2, + }, carol, oldTime); + + // Should NOT be deleted: old local note + const localNote = await createNote({}, alice, oldTime); + + // Should NOT be deleted: new remote note + const newerRemoteNote = await createNote({}, bob); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(1); // Only deletableNote should be deleted + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.length).toBe(5); + expect(remainingNotes.some(n => n.id === deletableNote.id)).toBe(false); // Deleted + expect(remainingNotes.some(n => n.id === favoritedNote.id)).toBe(true); // Kept + expect(remainingNotes.some(n => n.id === pinnedNote.id)).toBe(true); // Kept + expect(remainingNotes.some(n => n.id === clippedNote.id)).toBe(true); // Kept + expect(remainingNotes.some(n => n.id === localNote.id)).toBe(true); // Kept + expect(remainingNotes.some(n => n.id === newerRemoteNote.id)).toBe(true); // Kept + }); + + // 大量のノート + test('should handle large number of notes correctly', async () => { + const AMOUNT = 130; + const job = createMockJob(); + + const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000; + const noteIds = []; + for (let i = 0; i < AMOUNT; i++) { + const note = await createNote({}, bob, oldTime - i); + noteIds.push(note.id); + } + + const result = await service.process(job as any); + + // Should delete all notes, but may require multiple batches + expect(result.deletedCount).toBe(AMOUNT); + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.length).toBe(0); + }); + + // 大量のノート + リプライ or リノート + test('should handle large number of notes with replies correctly', async () => { + const AMOUNT = 130; + const job = createMockJob(); + + const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000; + const noteIds = []; + for (let i = 0; i < AMOUNT; i++) { + const note = await createNote({}, bob, oldTime - i - AMOUNT); + noteIds.push(note.id); + if (i % 2 === 0) { + // Create a reply for every second note + await createNote({ replyId: note.id }, carol, oldTime - i); + } else { + // Create a renote for every second note + await createNote({ renoteId: note.id }, bob, oldTime - i); + } + } + + const result = await service.process(job as any); + // Should delete all notes, but may require multiple batches + expect(result.deletedCount).toBe(AMOUNT * 2); + expect(result.skipped).toBe(false); + }); + + // 大量の古いノート + 新しいリプライ or リノート + test('should handle large number of old notes with new replies correctly', async () => { + const AMOUNT = 130; + const job = createMockJob(); + + const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000; + const newTime = Date.now(); + const noteIds = []; + for (let i = 0; i < AMOUNT; i++) { + const note = await createNote({}, bob, oldTime - i); + noteIds.push(note.id); + if (i % 2 === 0) { + // Create a reply for every second note + await createNote({ replyId: note.id }, carol, newTime + i); + } else { + // Create a renote for every second note + await createNote({ renoteId: note.id }, bob, newTime + i); + } + } + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(0); + expect(result.skipped).toBe(false); + }); + + // 大量の残す対象(clippedCount: 1)と大量の削除対象 + test('should handle large number of notes, mixed conditions with clippedCount', async () => { + const AMOUNT_BASE = 70; + const job = createMockJob(); + + const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000; + const noteIds = []; + for (let i = 0; i < AMOUNT_BASE; i++) { + const note = await createNote({ clippedCount: 1 }, bob, oldTime - i - AMOUNT_BASE); + noteIds.push(note.id); + } + for (let i = 0; i < AMOUNT_BASE; i++) { + const note = await createNote({}, carol, oldTime - i); + noteIds.push(note.id); + } + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(AMOUNT_BASE); // Assuming half are deletable + expect(result.skipped).toBe(false); + }); + + // 大量の残す対象(リプライ)と大量の削除対象 + test('should handle large number of notes, mixed conditions with replies', async () => { + const AMOUNT_BASE = 70; + const job = createMockJob(); + const oldTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 1000; + const newTime = Date.now(); + for (let i = 0; i < AMOUNT_BASE; i++) { + // should remain + const note = await createNote({}, carol, oldTime - AMOUNT_BASE - i); + // should remain + await createNote({ replyId: note.id }, bob, newTime + i); + } + + const noteIdsExpectedToBeDeleted = []; + for (let i = 0; i < AMOUNT_BASE; i++) { + // should be deleted + const note = await createNote({}, bob, oldTime - i); + noteIdsExpectedToBeDeleted.push(note.id); + } + + const result = await service.process(job as any); + expect(result.deletedCount).toBe(AMOUNT_BASE); // Assuming all replies are deletable + expect(result.skipped).toBe(false); + + const remainingNotes = await notesRepository.find(); + expect(remainingNotes.length).toBe(AMOUNT_BASE * 2); // Only replies should remain + noteIdsExpectedToBeDeleted.forEach(id => { + expect(remainingNotes.some(n => n.id === id)).toBe(false); // All original notes should be deleted + }); + }); + + test('should update cursor correctly during batch processing', async () => { + const job = createMockJob(); + + // Create notes with specific timing to test cursor behavior + const baseTime = Date.now() - ms(`${meta.remoteNotesCleaningExpiryDaysForEachNotes} days`) - 10000; + + const note1 = await createNote({}, bob, baseTime); + const note2 = await createNote({}, carol, baseTime - 1000); + const note3 = await createNote({}, bob, baseTime - 2000); + + const result = await service.process(job as any); + + expect(result.deletedCount).toBe(3); + expect(result.newest).toBe(idService.parse(note1.id).date.getTime()); + expect(result.oldest).toBe(idService.parse(note3.id).date.getTime()); + expect(result.skipped).toBe(false); + }); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 7eecf8bb0d..ace614115c 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -317,7 +317,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO const formData = new FormData(); formData.append('file', blob ?? - new File([await readFile(absPath)], basename(absPath.toString()))); + new File([new Uint8Array(await readFile(absPath))], basename(absPath.toString()))); formData.append('force', 'true'); if (name) { formData.append('name', name); @@ -608,8 +608,8 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { username: config.db.user, password: config.db.pass, database: config.db.db, - synchronize: true && !justBorrow, - dropSchema: true && !justBorrow, + synchronize: !justBorrow, + dropSchema: !justBorrow, entities: initEntities ?? entities, }); @@ -661,7 +661,9 @@ export async function captureWebhook<T = SystemWebhookPayload>(postAction: () => let timeoutHandle: NodeJS.Timeout | null = null; const result = await new Promise<string>(async (resolve, reject) => { fastify.all('/', async (req, res) => { - timeoutHandle && clearTimeout(timeoutHandle); + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } const body = JSON.stringify(req.body); res.status(200).send('ok'); |