summaryrefslogtreecommitdiff
path: root/packages/backend/test
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-08-31 08:42:43 +0000
committerGitHub <noreply@github.com>2025-08-31 08:42:43 +0000
commitec21336d45e6e3bb26a0225fc0aa57ac98d5be4b (patch)
tree2c7aef5ba1626009377faf96941a57411dd619e5 /packages/backend/test
parentMerge pull request #16244 from misskey-dev/develop (diff)
parentRelease: 2025.8.0 (diff)
downloadmisskey-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.ts1
-rw-r--r--packages/backend/test/e2e/clips.ts9
-rw-r--r--packages/backend/test/e2e/endpoints.ts1
-rw-r--r--packages/backend/test/e2e/fetch-resource.ts3
-rw-r--r--packages/backend/test/e2e/ff-visibility.ts1
-rw-r--r--packages/backend/test/e2e/timelines.ts2478
-rw-r--r--packages/backend/test/e2e/well-known.ts3
-rw-r--r--packages/backend/test/unit/NoteCreateService.ts1
-rw-r--r--packages/backend/test/unit/misc/is-renote.ts1
-rw-r--r--packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts652
-rw-r--r--packages/backend/test/utils.ts10
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');