From 1831a6a339c555037b1079dacca3f3df3d67e407 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sun, 3 Jul 2022 20:54:54 +0900
Subject: [PATCH] =?UTF-8?q?fix:=20streaming=E3=83=86=E3=82=B9=E3=83=88?=
 =?UTF-8?q?=E3=81=8A=E3=81=9D=E3=81=84=20(#8912)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/test/streaming.ts | 1295 ++++++++++------------------
 packages/backend/test/utils.ts     |    6 +-
 2 files changed, 471 insertions(+), 830 deletions(-)

diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts
index f080b71dd..621d07f9c 100644
--- a/packages/backend/test/streaming.ts
+++ b/packages/backend/test/streaming.ts
@@ -3,22 +3,12 @@ process.env.NODE_ENV = 'test';
 import * as assert from 'assert';
 import * as childProcess from 'child_process';
 import { Following } from '../src/models/entities/following.js';
-import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js';
+import { connectStream, signup, api, post, startServer, shutdownServer, initTestDb, waitFire } from './utils.js';
 
 describe('Streaming', () => {
 	let p: childProcess.ChildProcess;
 	let Followings: any;
 
-	beforeEach(async () => {
-		p = await startServer();
-		const connection = await initTestDb(true);
-		Followings = connection.getRepository(Following);
-	});
-
-	afterEach(async () => {
-		await shutdownServer(p);
-	});
-
 	const follow = async (follower: any, followee: any) => {
 		await Followings.save({
 			id: 'a',
@@ -34,871 +24,522 @@ describe('Streaming', () => {
 		});
 	};
 
-	it('mention event', () => new Promise(async done => {
-		const alice = await signup({ username: 'alice' });
-		const bob = await signup({ username: 'bob' });
+	describe('Streaming', () => {
+		// Local users
+		let ayano: any;
+		let kyoko: any;
+		let chitose: any;
 
-		const ws = await connectStream(bob, 'main', ({ type, body }) => {
-			if (type == 'mention') {
-				assert.deepStrictEqual(body.userId, alice.id);
-				ws.close();
-				done();
-			}
-		});
+		// Remote users
+		let akari: any;
+		let chinatsu: any;
 
-		post(alice, {
-			text: 'foo @bob bar',
-		});
-	}));
+		let kyokoNote: any;
+		let list: any;
 
-	it('renote event', () => new Promise(async done => {
-		const alice = await signup({ username: 'alice' });
-		const bob = await signup({ username: 'bob' });
-		const bobNote = await post(bob, {
-			text: 'foo',
-		});
+		before(async () => {
+			p = await startServer();
+			const connection = await initTestDb(true);
+			Followings = connection.getRepository(Following);
 
-		const ws = await connectStream(bob, 'main', ({ type, body }) => {
-			if (type == 'renote') {
-				assert.deepStrictEqual(body.renoteId, bobNote.id);
-				ws.close();
-				done();
-			}
-		});
+			ayano = await signup({ username: 'ayano' });
+			kyoko = await signup({ username: 'kyoko' });
+			chitose = await signup({ username: 'chitose' });
 
-		post(alice, {
-			renoteId: bobNote.id,
-		});
-	}));
+			akari = await signup({ username: 'akari', host: 'example.com' });
+			chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
 
-	describe('Home Timeline', () => {
-		it('自分の投稿が流れる', () => new Promise(async done => {
-			const post = {
-				text: 'foo',
-			};
+			kyokoNote = await post(kyoko, { text: 'foo' });
 
-			const me = await signup();
+			// Follow: ayano => kyoko
+			await api('following/create', { userId: kyoko.id }, ayano);
 
-			const ws = await connectStream(me, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.text, post.text);
-					ws.close();
-					done();
-				}
-			});
+			// Follow: ayano => akari
+			await follow(ayano, akari);
 
-			request('/notes/create', post, me);
-		}));
-
-		it('フォローしているユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					assert.deepStrictEqual(body.text, 'foo');
-					ws.close();
-					done();
-				}
-			});
-
-			// Bob が Alice 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [alice.id],
-			});
-		}));
-
-		it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-			const carol = await signup({ username: 'carol' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// Bob が Carol 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [carol.id],
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
-
-	describe('Local Timeline', () => {
-		it('自分の投稿が流れる', () => new Promise(async done => {
-			const me = await signup();
-
-			const ws = await connectStream(me, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, me.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(me, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('リモートユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしてたとしてもリモートユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('ホーム指定の投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// ホーム指定
-			post(bob, {
-				text: 'foo',
-				visibility: 'home',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしているローカルユーザーのダイレクト投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// Bob が Alice 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [alice.id],
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// フォロワー宛て投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'followers',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
-
-	describe('Hybrid Timeline', () => {
-		it('自分の投稿が流れる', () => new Promise(async done => {
-			const me = await signup();
-
-			const ws = await connectStream(me, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, me.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(me, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしているリモートユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			// Alice が Bob をフォロー
-			await follow(alice, bob);
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないリモートユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					assert.deepStrictEqual(body.text, 'foo');
-					ws.close();
-					done();
-				}
-			});
-
-			// Bob が Alice 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [alice.id],
-			});
-		}));
-
-		it('フォローしているユーザーのホーム投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// Alice が Bob をフォロー
-			await request('/following/create', {
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					assert.deepStrictEqual(body.text, 'foo');
-					ws.close();
-					done();
-				}
-			});
-
-			// ホーム投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'home',
-			});
-		}));
-
-		it('フォローしていないローカルユーザーのホーム投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// ホーム投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'home',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// フォロワー宛て投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'followers',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
-
-	describe('Global Timeline', () => {
-		it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('フォローしていないリモートユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob', host: 'example.com' });
-
-			const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			});
-
-			post(bob, {
-				text: 'foo',
-			});
-		}));
-
-		it('ホーム投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			});
-
-			// ホーム投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'home',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
-
-	describe('UserList Timeline', () => {
-		it('リストに入れているユーザーの投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// リスト作成
-			const list = await request('/users/lists/create', {
+			// List: chitose => ayano, kyoko
+			list = await api('users/lists/create', {
 				name: 'my list',
-			}, alice).then(x => x.body);
+			}, chitose).then(x => x.body);
 
-			// Alice が Bob をリスイン
-			await request('/users/lists/push', {
+			await api('users/lists/push', {
 				listId: list.id,
-				userId: bob.id,
-			}, alice);
+				userId: ayano.id,
+			}, chitose);
 
-			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					ws.close();
-					done();
-				}
-			}, {
+			await api('users/lists/push', {
 				listId: list.id,
+				userId: kyoko.id,
+			}, chitose);
+		});
+
+		after(async () => {
+			await shutdownServer(p);
+		});
+
+		describe('Events', () => {
+			it('mention event', async () => {
+				const fired = await waitFire(
+					kyoko, 'main',	// kyoko:main
+					() => post(ayano, { text: 'foo @kyoko bar' }),	// ayano mention => kyoko
+					msg => msg.type === 'mention' && msg.body.userId === ayano.id	// wait ayano
+				);
+
+				assert.strictEqual(fired, true);
 			});
 
-			post(bob, {
-				text: 'foo',
+			it('renote event', async () => {
+				const fired = await waitFire(
+					kyoko, 'main',	// kyoko:main
+					() => post(ayano, { renoteId: kyokoNote.id }),	// ayano renote
+					msg => msg.type === 'renote' && msg.body.renoteId === kyokoNote.id	// wait renote
+				);
+
+				assert.strictEqual(fired, true);
 			});
-		}));
+		});
 
-		it('リストに入れていないユーザーの投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
+		describe('Home Timeline', () => {
+			it('自分の投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'homeTimeline',	// ayano:Home
+					() => api('notes/create', { text: 'foo' }, ayano),	// ayano posts
+					msg => msg.type === 'note' && msg.body.text === 'foo'
+				);
 
-			// リスト作成
-			const list = await request('/users/lists/create', {
-				name: 'my list',
-			}, alice).then(x => x.body);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			}, {
-				listId: list.id,
+				assert.strictEqual(fired, true);
 			});
 
-			post(bob, {
-				text: 'foo',
+			it('フォローしているユーザーの投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'homeTimeline',		// ayano:home
+					() => api('notes/create', { text: 'foo' }, kyoko),	// kyoko posts
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, true);
 			});
 
-			setTimeout(() => {
+			it('フォローしていないユーザーの投稿は流れない', async () => {
+				const fired = await waitFire(
+					kyoko, 'homeTimeline',	// kyoko:home
+					() => api('notes/create', { text: 'foo' }, ayano),	// ayano posts
+					msg => msg.type === 'note' && msg.body.userId === ayano.id	// wait ayano
+				);
+
 				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		// #4471
-		it('リストに入れているユーザーのダイレクト投稿が流れる', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// リスト作成
-			const list = await request('/users/lists/create', {
-				name: 'my list',
-			}, alice).then(x => x.body);
-
-			// Alice が Bob をリスイン
-			await request('/users/lists/push', {
-				listId: list.id,
-				userId: bob.id,
-			}, alice);
-
-			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.userId, bob.id);
-					assert.deepStrictEqual(body.text, 'foo');
-					ws.close();
-					done();
-				}
-			}, {
-				listId: list.id,
 			});
 
-			// Bob が Alice 宛てのダイレクト投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'specified',
-				visibleUserIds: [alice.id],
-			});
-		}));
+			it('フォローしているユーザーのダイレクト投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'homeTimeline',	// ayano:home
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id], }, kyoko),	// kyoko dm => ayano
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
 
-		// #4335
-		it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => {
-			const alice = await signup({ username: 'alice' });
-			const bob = await signup({ username: 'bob' });
-
-			// リスト作成
-			const list = await request('/users/lists/create', {
-				name: 'my list',
-			}, alice).then(x => x.body);
-
-			// Alice が Bob をリスイン
-			await request('/users/lists/push', {
-				listId: list.id,
-				userId: bob.id,
-			}, alice);
-
-			let fired = false;
-
-			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
-				if (type == 'note') {
-					fired = true;
-				}
-			}, {
-				listId: list.id,
+				assert.strictEqual(fired, true);
 			});
 
-			// フォロワー宛て投稿
-			post(bob, {
-				text: 'foo',
-				visibility: 'followers',
-			});
+			it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'homeTimeline',	// ayano:home
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id], }, kyoko),	// kyoko dm => chitose
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
 
-			setTimeout(() => {
 				assert.strictEqual(fired, false);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-	});
+			});
+		});	// Home
 
-	describe('Hashtag Timeline', () => {
-		it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => {
-			const me = await signup();
+		describe('Local Timeline', () => {
+			it('自分の投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo' }, ayano),	// ayano posts
+					msg => msg.type === 'note' && msg.body.text === 'foo'
+				);
 
-			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
-				if (type == 'note') {
-					assert.deepStrictEqual(body.text, '#foo');
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないローカルユーザーの投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo' }, chitose),	// chitose posts
+					msg => msg.type === 'note' && msg.body.userId === chitose.id	// wait chitose
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('リモートユーザーの投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo' }, chinatsu),	// chinatsu posts
+					msg => msg.type === 'note' && msg.body.userId === chinatsu.id	// wait chinatsu
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo' }, akari),	// akari posts
+					msg => msg.type === 'note' && msg.body.userId === akari.id	// wait akari
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('ホーム指定の投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),	// kyoko home posts
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko),	// kyoko DM => ayano
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'localTimeline',	// ayano:Local
+					() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
+					msg => msg.type === 'note' && msg.body.userId === chitose.id	// wait chitose
+				);
+
+				assert.strictEqual(fired, false);
+			});
+		});
+
+		describe('Hybrid Timeline', () => {
+			it('自分の投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo' }, ayano),	// ayano posts
+					msg => msg.type === 'note' && msg.body.text === 'foo'
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないローカルユーザーの投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo' }, chitose),	// chitose posts
+					msg => msg.type === 'note' && msg.body.userId === chitose.id	// wait chitose
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしているリモートユーザーの投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo' }, akari),	// akari posts
+					msg => msg.type === 'note' && msg.body.userId === akari.id	// wait akari
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないリモートユーザーの投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo' }, chinatsu),	// chinatsu posts
+					msg => msg.type === 'note' && msg.body.userId === chinatsu.id	// wait chinatsu
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしているユーザーのダイレクト投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko),
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしているユーザーのホーム投稿が流れる', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo', visibility: 'home' }, chitose),
+					msg => msg.type === 'note' && msg.body.userId === chitose.id
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => async () => {
+				const fired = await waitFire(
+					ayano, 'hybridTimeline',	// ayano:Hybrid
+					() => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
+					msg => msg.type === 'note' && msg.body.userId === chitose.id
+				);
+
+				assert.strictEqual(fired, false);
+			});
+		});
+
+		describe('Global Timeline', () => {
+			it('フォローしていないローカルユーザーの投稿が流れる', () => async () => {
+				const fired = await waitFire(
+					ayano, 'globalTimeline',	// ayano:Global
+					() => api('notes/create', { text: 'foo' }, chitose),	// chitose posts
+					msg => msg.type === 'note' && msg.body.userId === chitose.id	// wait chitose
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('フォローしていないリモートユーザーの投稿が流れる', () => async () => {
+				const fired = await waitFire(
+					ayano, 'globalTimeline',	// ayano:Global
+					() => api('notes/create', { text: 'foo' }, chinatsu),	// chinatsu posts
+					msg => msg.type === 'note' && msg.body.userId === chinatsu.id	// wait chinatsu
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('ホーム投稿は流れない', () => async () => {
+				const fired = await waitFire(
+					ayano, 'globalTimeline',	// ayano:Global
+					() => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),	// kyoko posts
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id	// wait kyoko
+				);
+
+				assert.strictEqual(fired, false);
+			});
+		});
+
+		describe('UserList Timeline', () => {
+			it('リストに入れているユーザーの投稿が流れる', () => async () => {
+				const fired = await waitFire(
+					chitose, 'userList',
+					() => api('notes/create', { text: 'foo' }, ayano),
+					msg => msg.type === 'note' && msg.body.userId === ayano.id,
+					{ listId: list.id, }
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			it('リストに入れていないユーザーの投稿は流れない', () => async () => {
+				const fired = await waitFire(
+					chitose, 'userList',
+					() => api('notes/create', { text: 'foo' }, chinatsu),
+					msg => msg.type === 'note' && msg.body.userId === chinatsu.id,
+					{ listId: list.id, }
+				);
+
+				assert.strictEqual(fired, false);
+			});
+
+			// #4471
+			it('リストに入れているユーザーのダイレクト投稿が流れる', () => async () => {
+				const fired = await waitFire(
+					chitose, 'userList',
+					() => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano),
+					msg => msg.type === 'note' && msg.body.userId === ayano.id,
+					{ listId: list.id, }
+				);
+
+				assert.strictEqual(fired, true);
+			});
+
+			// #4335
+			it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => async () => {
+				const fired = await waitFire(
+					chitose, 'userList',
+					() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko),
+					msg => msg.type === 'note' && msg.body.userId === kyoko.id,
+					{ listId: list.id, }
+				);
+
+				assert.strictEqual(fired, false);
+			});
+		});
+
+		describe('Hashtag Timeline', () => {
+			it('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => {
+				const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+					if (type == 'note') {
+						assert.deepStrictEqual(body.text, '#foo');
+						ws.close();
+						done();
+					}
+				}, {
+					q: [
+						['foo'],
+					],
+				});
+
+				post(chitose, {
+					text: '#foo',
+				});
+			}));
+
+			it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
+				let fooCount = 0;
+				let barCount = 0;
+				let fooBarCount = 0;
+	
+				const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+					if (type == 'note') {
+						if (body.text === '#foo') fooCount++;
+						if (body.text === '#bar') barCount++;
+						if (body.text === '#foo #bar') fooBarCount++;
+					}
+				}, {
+					q: [
+						['foo', 'bar'],
+					],
+				});
+	
+				post(chitose, {
+					text: '#foo',
+				});
+	
+				post(chitose, {
+					text: '#bar',
+				});
+	
+				post(chitose, {
+					text: '#foo #bar',
+				});
+	
+				setTimeout(() => {
+					assert.strictEqual(fooCount, 0);
+					assert.strictEqual(barCount, 0);
+					assert.strictEqual(fooBarCount, 1);
 					ws.close();
 					done();
-				}
-			}, {
-				q: [
-					['foo'],
-				],
-			});
+				}, 3000);
+			}));
 
-			post(me, {
-				text: '#foo',
-			});
-		}));
+			it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => {
+				let fooCount = 0;
+				let barCount = 0;
+				let fooBarCount = 0;
+				let piyoCount = 0;
 
-		it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => {
-			const me = await signup();
+				const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+					if (type == 'note') {
+						if (body.text === '#foo') fooCount++;
+						if (body.text === '#bar') barCount++;
+						if (body.text === '#foo #bar') fooBarCount++;
+						if (body.text === '#piyo') piyoCount++;
+					}
+				}, {
+					q: [
+						['foo'],
+						['bar'],
+					],
+				});
 
-			let fooCount = 0;
-			let barCount = 0;
-			let fooBarCount = 0;
+				post(chitose, {
+					text: '#foo',
+				});
 
-			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
-				if (type == 'note') {
-					if (body.text === '#foo') fooCount++;
-					if (body.text === '#bar') barCount++;
-					if (body.text === '#foo #bar') fooBarCount++;
-				}
-			}, {
-				q: [
-					['foo', 'bar'],
-				],
-			});
+				post(chitose, {
+					text: '#bar',
+				});
 
-			post(me, {
-				text: '#foo',
-			});
+				post(chitose, {
+					text: '#foo #bar',
+				});
 
-			post(me, {
-				text: '#bar',
-			});
+				post(chitose, {
+					text: '#piyo',
+				});
 
-			post(me, {
-				text: '#foo #bar',
-			});
+				setTimeout(() => {
+					assert.strictEqual(fooCount, 1);
+					assert.strictEqual(barCount, 1);
+					assert.strictEqual(fooBarCount, 1);
+					assert.strictEqual(piyoCount, 0);
+					ws.close();
+					done();
+				}, 3000);
+			}));
 
-			setTimeout(() => {
-				assert.strictEqual(fooCount, 0);
-				assert.strictEqual(barCount, 0);
-				assert.strictEqual(fooBarCount, 1);
-				ws.close();
-				done();
-			}, 3000);
-		}));
+			it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => {
+				let fooCount = 0;
+				let barCount = 0;
+				let fooBarCount = 0;
+				let piyoCount = 0;
+				let waaaCount = 0;
 
-		it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => {
-			const me = await signup();
+				const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+					if (type == 'note') {
+						if (body.text === '#foo') fooCount++;
+						if (body.text === '#bar') barCount++;
+						if (body.text === '#foo #bar') fooBarCount++;
+						if (body.text === '#piyo') piyoCount++;
+						if (body.text === '#waaa') waaaCount++;
+					}
+				}, {
+					q: [
+						['foo', 'bar'],
+						['piyo'],
+					],
+				});
 
-			let fooCount = 0;
-			let barCount = 0;
-			let fooBarCount = 0;
-			let piyoCount = 0;
+				post(chitose, {
+					text: '#foo',
+				});
 
-			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
-				if (type == 'note') {
-					if (body.text === '#foo') fooCount++;
-					if (body.text === '#bar') barCount++;
-					if (body.text === '#foo #bar') fooBarCount++;
-					if (body.text === '#piyo') piyoCount++;
-				}
-			}, {
-				q: [
-					['foo'],
-					['bar'],
-				],
-			});
+				post(chitose, {
+					text: '#bar',
+				});
 
-			post(me, {
-				text: '#foo',
-			});
+				post(chitose, {
+					text: '#foo #bar',
+				});
 
-			post(me, {
-				text: '#bar',
-			});
+				post(chitose, {
+					text: '#piyo',
+				});
 
-			post(me, {
-				text: '#foo #bar',
-			});
+				post(chitose, {
+					text: '#waaa',
+				});
 
-			post(me, {
-				text: '#piyo',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fooCount, 1);
-				assert.strictEqual(barCount, 1);
-				assert.strictEqual(fooBarCount, 1);
-				assert.strictEqual(piyoCount, 0);
-				ws.close();
-				done();
-			}, 3000);
-		}));
-
-		it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => {
-			const me = await signup();
-
-			let fooCount = 0;
-			let barCount = 0;
-			let fooBarCount = 0;
-			let piyoCount = 0;
-			let waaaCount = 0;
-
-			const ws = await connectStream(me, 'hashtag', ({ type, body }) => {
-				if (type == 'note') {
-					if (body.text === '#foo') fooCount++;
-					if (body.text === '#bar') barCount++;
-					if (body.text === '#foo #bar') fooBarCount++;
-					if (body.text === '#piyo') piyoCount++;
-					if (body.text === '#waaa') waaaCount++;
-				}
-			}, {
-				q: [
-					['foo', 'bar'],
-					['piyo'],
-				],
-			});
-
-			post(me, {
-				text: '#foo',
-			});
-
-			post(me, {
-				text: '#bar',
-			});
-
-			post(me, {
-				text: '#foo #bar',
-			});
-
-			post(me, {
-				text: '#piyo',
-			});
-
-			post(me, {
-				text: '#waaa',
-			});
-
-			setTimeout(() => {
-				assert.strictEqual(fooCount, 0);
-				assert.strictEqual(barCount, 0);
-				assert.strictEqual(fooBarCount, 1);
-				assert.strictEqual(piyoCount, 1);
-				assert.strictEqual(waaaCount, 0);
-				ws.close();
-				done();
-			}, 3000);
-		}));
+				setTimeout(() => {
+					assert.strictEqual(fooCount, 0);
+					assert.strictEqual(barCount, 0);
+					assert.strictEqual(fooBarCount, 1);
+					assert.strictEqual(piyoCount, 1);
+					assert.strictEqual(waaaCount, 0);
+					ws.close();
+					done();
+				}, 3000);
+			}));
+		});
 	});
 });
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 0ee15067d..245cf858d 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -186,7 +186,7 @@ export function connectStream(user: any, channel: string, listener: (message: Re
 	});
 }
 
-export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean) => {
+export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => {
 	return new Promise<boolean>(async (res, rej) => {
 		let timer: NodeJS.Timeout;
 
@@ -198,7 +198,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond
 					if (timer) clearTimeout(timer);
 					res(true);
 				}
-			});
+			}, params);
 		} catch (e) {
 			rej(e);
 		}
@@ -208,7 +208,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond
 		timer = setTimeout(() => {
 			ws.close();
 			res(false);
-		}, 5000);
+		}, 3000);
 
 		try {
 			await trgr();