From 54e0a7f8a8d977c7befc255cc4950a86ac2e72fb Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 19 Sep 2021 02:23:12 +0900
Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E5=87=8D=E7=B5=90=E3=81=95?=
 =?UTF-8?q?=E3=82=8C=E3=81=9F=E5=A0=B4=E5=90=88=E3=81=AE=E3=83=80=E3=82=A4?=
 =?UTF-8?q?=E3=82=A2=E3=83=AD=E3=82=B0=E3=82=92=E5=AE=9F=E8=A3=85=20(#7811?=
 =?UTF-8?q?)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: 凍結された場合のダイアログを実装

* Update CHANGELOG.md

* Update basic.js

* improve error handling

* cypressなんもわからん

* Update basic.js
---
 CHANGELOG.md                                |   2 +
 cypress/integration/basic.js                | 136 ++++++++++++++++----
 locales/ja-JP.yml                           |   2 +
 src/client/account.ts                       |  22 ++--
 src/client/components/signin.vue            |  41 ++++--
 src/client/scripts/show-suspended-dialog.ts |  10 ++
 src/server/api/endpoints/reset-db.ts        |   2 +
 src/server/api/private/signin.ts            |  37 +++---
 8 files changed, 186 insertions(+), 66 deletions(-)
 create mode 100644 src/client/scripts/show-suspended-dialog.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index dce25340f9..8a15faf6a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@
 ### Improvements
 - ActivityPub: リモートユーザーのDeleteアクティビティに対応
 - ActivityPub: add resolver check for blocked instance
+- アカウントが凍結された場合に、凍結された旨を表示してからログアウトするように
+- 凍結されたアカウントにログインしようとしたときに、凍結されている旨を表示するように
 - UIの改善
 
 ### Bugfixes
diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js
index 69d59bc2c6..52bcdb58d0 100644
--- a/cypress/integration/basic.js
+++ b/cypress/integration/basic.js
@@ -1,10 +1,16 @@
 describe('Basic', () => {
-	before(() => {
-		cy.request('POST', '/api/reset-db');
+	beforeEach(() => {
+		cy.request('POST', '/api/reset-db').as('reset');
+		cy.get('@reset').its('status').should('equal', 204);
+		cy.clearLocalStorage();
+		cy.clearCookies();
+		cy.reload(true);
 	});
 
-	beforeEach(() => {
-		cy.reload(true);
+	afterEach(() => {
+		// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
+		// waitを入れることでそれを防止できる
+		cy.wait(1000);
 	});
 
   it('successfully loads', () => {
@@ -14,56 +20,130 @@ describe('Basic', () => {
 	it('setup instance', () => {
     cy.visit('/');
 
+		cy.intercept('POST', '/api/admin/accounts/create').as('signup');
+	
 		cy.get('[data-cy-admin-username] input').type('admin');
-
 		cy.get('[data-cy-admin-password] input').type('admin1234');
-
 		cy.get('[data-cy-admin-ok]').click();
+
+		// なぜか動かない
+		//cy.wait('@signup').should('have.property', 'response.statusCode');
+		cy.wait('@signup');
   });
 
 	it('signup', () => {
-    cy.visit('/');
+		// インスタンス初期セットアップ
+		cy.request('POST', '/api/admin/accounts/create', {
+			username: 'admin',
+			password: 'pass',
+		}).as('setup');
 
-		cy.get('[data-cy-signup]').click();
+		cy.get('@setup').then(() => {
+			cy.visit('/');
 
-		cy.get('[data-cy-signup-username] input').type('alice');
+			cy.intercept('POST', '/api/signup').as('signup');
 
-		cy.get('[data-cy-signup-password] input').type('alice1234');
-	
-		cy.get('[data-cy-signup-password-retype] input').type('alice1234');
+			cy.get('[data-cy-signup]').click();
+			cy.get('[data-cy-signup-username] input').type('alice');
+			cy.get('[data-cy-signup-password] input').type('alice1234');
+			cy.get('[data-cy-signup-password-retype] input').type('alice1234');
+			cy.get('[data-cy-signup-submit]').click();
 
-		cy.get('[data-cy-signup-submit]').click();
+			cy.wait('@signup');
+		});
   });
 
 	it('signin', () => {
-    cy.visit('/');
+		// インスタンス初期セットアップ
+		cy.request('POST', '/api/admin/accounts/create', {
+			username: 'admin',
+			password: 'pass',
+		}).as('setup');
 
-		cy.get('[data-cy-signin]').click();
+		cy.get('@setup').then(() => {
+			// ユーザー作成
+			cy.request('POST', '/api/signup', {
+				username: 'alice',
+				password: 'alice1234',
+			}).as('signup');
+		});
 
-		cy.get('[data-cy-signin-username] input').type('alice');
+		cy.get('@signup').then(() => {
+			cy.visit('/');
 
-		// Enterキーでサインインできるかの確認も兼ねる
-		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+			cy.intercept('POST', '/api/signin').as('signin');
+
+			cy.get('[data-cy-signin]').click();
+			cy.get('[data-cy-signin-username] input').type('alice');
+			// Enterキーでサインインできるかの確認も兼ねる
+			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+
+			cy.wait('@signin');
+		});
   });
 
 	it('note', () => {
     cy.visit('/');
 
-		//#region TODO: この辺はUI操作ではなくAPI操作でログインする
-		cy.get('[data-cy-signin]').click();
+		// インスタンス初期セットアップ
+		cy.request('POST', '/api/admin/accounts/create', {
+			username: 'admin',
+			password: 'pass',
+		}).as('setup');
 
-		cy.get('[data-cy-signin-username] input').type('alice');
+		cy.get('@setup').then(() => {
+			// ユーザー作成
+			cy.request('POST', '/api/signup', {
+				username: 'alice',
+				password: 'alice1234',
+			}).as('signup');
+		});
 
-		// Enterキーでサインインできるかの確認も兼ねる
-		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
-		//#endregion
+		cy.get('@signup').then(() => {
+			cy.visit('/');
 
-		cy.get('[data-cy-open-post-form]').click();
+			cy.intercept('POST', '/api/signin').as('signin');
 
-		cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
+			cy.get('[data-cy-signin]').click();
+			cy.get('[data-cy-signin-username] input').type('alice');
+			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
 
-		cy.get('[data-cy-open-post-form-submit]').click();
+			cy.wait('@signin').as('signinEnd');
+		});
 
-		// TODO: 投稿した文字列が画面内にあるか(=タイムラインに流れてきたか)のテスト
+		cy.get('@signinEnd').then(() => {
+			cy.get('[data-cy-open-post-form]').click();
+			cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
+			cy.get('[data-cy-open-post-form-submit]').click();
+
+			cy.contains('Hello, Misskey!');
+		});
   });
+
+	it('suspend', function() {
+		cy.request('POST', '/api/admin/accounts/create', {
+			username: 'admin',
+			password: 'pass',
+		}).its('body').as('admin');
+
+		cy.request('POST', '/api/signup', {
+			username: 'alice',
+			password: 'pass',
+		}).its('body').as('alice');
+
+		cy.then(() => {
+			cy.request('POST', '/api/admin/suspend-user', {
+				i: this.admin.token,
+				userId: this.alice.id,
+			});
+	
+			cy.visit('/');
+	
+			cy.get('[data-cy-signin]').click();
+			cy.get('[data-cy-signin-username] input').type('alice');
+			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+	
+			cy.contains('アカウントが凍結されています');
+		});
+	});
 });
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index b9623ef0d0..2c0663cf87 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -529,6 +529,8 @@ removeAllFollowing: "フォローを全解除"
 removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。"
 userSuspended: "このユーザーは凍結されています。"
 userSilenced: "このユーザーはサイレンスされています。"
+yourAccountSuspendedTitle: "アカウントが凍結されています"
+yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。"
 menu: "メニュー"
 divider: "分割線"
 addItem: "項目を追加"
diff --git a/src/client/account.ts b/src/client/account.ts
index e469bae5a2..6e26ac1f7d 100644
--- a/src/client/account.ts
+++ b/src/client/account.ts
@@ -3,6 +3,7 @@ import { reactive } from 'vue';
 import { apiUrl } from '@client/config';
 import { waiting } from '@client/os';
 import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
+import { showSuspendedDialog } from './scripts/show-suspended-dialog';
 
 // TODO: 他のタブと永続化されたstateを同期
 
@@ -82,17 +83,20 @@ function fetchAccount(token): Promise<Account> {
 				i: token
 			})
 		})
+		.then(res => res.json())
 		.then(res => {
-			// When failed to authenticate user
-			if (res.status !== 200 && res.status < 500) {
-				return signout();
+			if (res.error) {
+				if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
+					showSuspendedDialog().then(() => {
+						signout();
+					});
+				} else {
+					signout();
+				}
+			} else {
+				res.token = token;
+				done(res);
 			}
-
-			// Parse response
-			res.json().then(i => {
-				i.token = token;
-				done(i);
-			});
 		})
 		.catch(fail);
 	});
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
index c051288d0a..69f527b7d6 100755
--- a/src/client/components/signin.vue
+++ b/src/client/components/signin.vue
@@ -54,6 +54,7 @@ import { apiUrl, host } from '@client/config';
 import { byteify, hexify } from '@client/scripts/2fa';
 import * as os from '@client/os';
 import { login } from '@client/account';
+import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
 
 export default defineComponent({
 	components: {
@@ -169,15 +170,7 @@ export default defineComponent({
 						this.signing = false;
 						this.challengeData = res;
 						return this.queryKey();
-					}).catch(() => {
-						os.dialog({
-							type: 'error',
-							text: this.$ts.signinFailed
-						});
-						this.challengeData = null;
-						this.totpLogin = false;
-						this.signing = false;
-					});
+					}).catch(this.loginFailed);
 				} else {
 					this.totpLogin = true;
 					this.signing = false;
@@ -190,14 +183,36 @@ export default defineComponent({
 				}).then(res => {
 					this.$emit('login', res);
 					this.onLogin(res);
-				}).catch(() => {
+				}).catch(this.loginFailed);
+			}
+		},
+
+		loginFailed(err) {
+			switch (err.id) {
+				case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
 					os.dialog({
 						type: 'error',
-						text: this.$ts.loginFailed
+						title: this.$ts.loginFailed,
+						text: this.$ts.noSuchUser
 					});
-					this.signing = false;
-				});
+					break;
+				}
+				case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
+					showSuspendedDialog();
+					break;
+				}
+				default: {
+					os.dialog({
+						type: 'error',
+						title: this.$ts.loginFailed,
+						text: JSON.stringify(err)
+					});
+				}
 			}
+
+			this.challengeData = null;
+			this.totpLogin = false;
+			this.signing = false;
 		},
 
 		resetPassword() {
diff --git a/src/client/scripts/show-suspended-dialog.ts b/src/client/scripts/show-suspended-dialog.ts
new file mode 100644
index 0000000000..dde829cdae
--- /dev/null
+++ b/src/client/scripts/show-suspended-dialog.ts
@@ -0,0 +1,10 @@
+import * as os from '@client/os';
+import { i18n } from '@client/i18n';
+
+export function showSuspendedDialog() {
+	return os.dialog({
+		type: 'error',
+		title: i18n.locale.yourAccountSuspendedTitle,
+		text: i18n.locale.yourAccountSuspendedDescription
+	});
+}
diff --git a/src/server/api/endpoints/reset-db.ts b/src/server/api/endpoints/reset-db.ts
index f430869302..f0a9dae4ff 100644
--- a/src/server/api/endpoints/reset-db.ts
+++ b/src/server/api/endpoints/reset-db.ts
@@ -18,4 +18,6 @@ export default define(meta, async (ps, user) => {
 	if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
 
 	await resetDb();
+
+	await new Promise(resolve => setTimeout(resolve, 1000));
 });
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index fff1037ff9..83c3dfee94 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -18,6 +18,11 @@ export default async (ctx: Koa.Context) => {
 	const password = body['password'];
 	const token = body['token'];
 
+	function error(status: number, error: { id: string }) {
+		ctx.status = status;
+		ctx.body = { error };
+	}
+
 	if (typeof username != 'string') {
 		ctx.status = 400;
 		return;
@@ -40,15 +45,15 @@ export default async (ctx: Koa.Context) => {
 	}) as ILocalUser;
 
 	if (user == null) {
-		ctx.throw(404, {
-			error: 'user not found'
+		error(404, {
+			id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
 		});
 		return;
 	}
 
 	if (user.isSuspended) {
-		ctx.throw(403, {
-			error: 'user is suspended'
+		error(403, {
+			id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
 		});
 		return;
 	}
@@ -58,7 +63,7 @@ export default async (ctx: Koa.Context) => {
 	// Compare password
 	const same = await bcrypt.compare(password, profile.password!);
 
-	async function fail(status?: number, failure?: { error: string }) {
+	async function fail(status?: number, failure?: { id: string }) {
 		// Append signin history
 		await Signins.insert({
 			id: genId(),
@@ -69,7 +74,7 @@ export default async (ctx: Koa.Context) => {
 			success: false
 		});
 
-		ctx.throw(status || 500, failure || { error: 'someting happened' });
+		error(status || 500, failure || { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
 	}
 
 	if (!profile.twoFactorEnabled) {
@@ -78,7 +83,7 @@ export default async (ctx: Koa.Context) => {
 			return;
 		} else {
 			await fail(403, {
-				error: 'incorrect password'
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
 			});
 			return;
 		}
@@ -87,7 +92,7 @@ export default async (ctx: Koa.Context) => {
 	if (token) {
 		if (!same) {
 			await fail(403, {
-				error: 'incorrect password'
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
 			});
 			return;
 		}
@@ -104,14 +109,14 @@ export default async (ctx: Koa.Context) => {
 			return;
 		} else {
 			await fail(403, {
-				error: 'invalid token'
+				id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f'
 			});
 			return;
 		}
 	} else if (body.credentialId) {
 		if (!same && !profile.usePasswordLessLogin) {
 			await fail(403, {
-				error: 'incorrect password'
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
 			});
 			return;
 		}
@@ -127,7 +132,7 @@ export default async (ctx: Koa.Context) => {
 
 		if (!challenge) {
 			await fail(403, {
-				error: 'non-existent challenge'
+				id: '2715a88a-2125-4013-932f-aa6fe72792da'
 			});
 			return;
 		}
@@ -139,7 +144,7 @@ export default async (ctx: Koa.Context) => {
 
 		if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * 60 * 1000) {
 			await fail(403, {
-				error: 'non-existent challenge'
+				id: '2715a88a-2125-4013-932f-aa6fe72792da'
 			});
 			return;
 		}
@@ -155,7 +160,7 @@ export default async (ctx: Koa.Context) => {
 
 		if (!securityKey) {
 			await fail(403, {
-				error: 'invalid credentialId'
+				id: '66269679-aeaf-4474-862b-eb761197e046'
 			});
 			return;
 		}
@@ -174,14 +179,14 @@ export default async (ctx: Koa.Context) => {
 			return;
 		} else {
 			await fail(403, {
-				error: 'invalid challenge data'
+				id: '93b86c4b-72f9-40eb-9815-798928603d1e'
 			});
 			return;
 		}
 	} else {
 		if (!same && !profile.usePasswordLessLogin) {
 			await fail(403, {
-				error: 'incorrect password'
+				id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c'
 			});
 			return;
 		}
@@ -192,7 +197,7 @@ export default async (ctx: Koa.Context) => {
 
 		if (keys.length === 0) {
 			await fail(403, {
-				error: 'no keys found'
+				id: 'f27fd449-9af4-4841-9249-1f989b9fa4a4'
 			});
 			return;
 		}

From 36b483d04d21176d7955c2c67becc0a080f5e528 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 19 Sep 2021 02:43:39 +0900
Subject: [PATCH 2/5] Update .gitignore

---
 .gitignore | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.gitignore b/.gitignore
index 0786295cf5..fe303ec526 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,9 @@
 /node_modules
 report.*.json
 
+# Cypress
+cypress/screenshots
+
 # config
 /.config/*
 !/.config/example.yml

From 91171c559a6d655719160393b9c9ba135a56c290 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 19 Sep 2021 02:58:17 +0900
Subject: [PATCH 3/5] Update .gitignore

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index fe303ec526..f8baa43848 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ report.*.json
 
 # Cypress
 cypress/screenshots
+cypress/videos
 
 # config
 /.config/*

From 186163ec3f905ca03e4143e7ee01fab422c12b9a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 19 Sep 2021 02:58:25 +0900
Subject: [PATCH 4/5] refactor

---
 cypress/integration/basic.js | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js
index 52bcdb58d0..fefc64baa0 100644
--- a/cypress/integration/basic.js
+++ b/cypress/integration/basic.js
@@ -2,8 +2,6 @@ describe('Basic', () => {
 	beforeEach(() => {
 		cy.request('POST', '/api/reset-db').as('reset');
 		cy.get('@reset').its('status').should('equal', 204);
-		cy.clearLocalStorage();
-		cy.clearCookies();
 		cy.reload(true);
 	});
 
@@ -121,17 +119,21 @@ describe('Basic', () => {
   });
 
 	it('suspend', function() {
+		// インスタンス初期セットアップ
 		cy.request('POST', '/api/admin/accounts/create', {
 			username: 'admin',
 			password: 'pass',
 		}).its('body').as('admin');
 
-		cy.request('POST', '/api/signup', {
-			username: 'alice',
-			password: 'pass',
-		}).its('body').as('alice');
+		cy.get('@admin').then(() => {
+			// ユーザー作成
+			cy.request('POST', '/api/signup', {
+				username: 'alice',
+				password: 'alice1234',
+			}).its('body').as('alice');
+		});
 
-		cy.then(() => {
+		cy.get('@alice').then(() => {
 			cy.request('POST', '/api/admin/suspend-user', {
 				i: this.admin.token,
 				userId: this.alice.id,

From 8a0a46b1c98de86cdae4b50cad4c9b36b21b0027 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 19 Sep 2021 14:27:16 +0900
Subject: [PATCH 5/5] test: improve e2e test

---
 cypress/integration/basic.js | 200 +++++++++++++++++++++--------------
 1 file changed, 119 insertions(+), 81 deletions(-)

diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js
index fefc64baa0..182f70ff68 100644
--- a/cypress/integration/basic.js
+++ b/cypress/integration/basic.js
@@ -1,4 +1,4 @@
-describe('Basic', () => {
+describe('Before setup instance', () => {
 	beforeEach(() => {
 		cy.request('POST', '/api/reset-db').as('reset');
 		cy.get('@reset').its('status').should('equal', 204);
@@ -28,97 +28,116 @@ describe('Basic', () => {
 		//cy.wait('@signup').should('have.property', 'response.statusCode');
 		cy.wait('@signup');
   });
+});
+
+describe('After setup instance', () => {
+	beforeEach(() => {
+		cy.request('POST', '/api/reset-db').as('reset');
+		cy.get('@reset').its('status').should('equal', 204);
+		cy.reload(true);
 
-	it('signup', () => {
 		// インスタンス初期セットアップ
 		cy.request('POST', '/api/admin/accounts/create', {
 			username: 'admin',
 			password: 'pass',
-		}).as('setup');
+		}).its('body').as('admin');
 
-		cy.get('@setup').then(() => {
-			cy.visit('/');
+		cy.get('@admin');
+	});
 
-			cy.intercept('POST', '/api/signup').as('signup');
+	afterEach(() => {
+		// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
+		// waitを入れることでそれを防止できる
+		cy.wait(1000);
+	});
 
-			cy.get('[data-cy-signup]').click();
-			cy.get('[data-cy-signup-username] input').type('alice');
-			cy.get('[data-cy-signup-password] input').type('alice1234');
-			cy.get('[data-cy-signup-password-retype] input').type('alice1234');
-			cy.get('[data-cy-signup-submit]').click();
+  it('successfully loads', () => {
+    cy.visit('/');
+  });
 
-			cy.wait('@signup');
+	it('signup', () => {
+		cy.visit('/');
+
+		cy.intercept('POST', '/api/signup').as('signup');
+
+		cy.get('[data-cy-signup]').click();
+		cy.get('[data-cy-signup-username] input').type('alice');
+		cy.get('[data-cy-signup-password] input').type('alice1234');
+		cy.get('[data-cy-signup-password-retype] input').type('alice1234');
+		cy.get('[data-cy-signup-submit]').click();
+
+		cy.wait('@signup');
+  });
+});
+
+describe('After user signup', () => {
+	beforeEach(() => {
+		cy.request('POST', '/api/reset-db').as('reset');
+		cy.get('@reset').its('status').should('equal', 204);
+		cy.reload(true);
+
+		// インスタンス初期セットアップ
+		cy.request('POST', '/api/admin/accounts/create', {
+			username: 'admin',
+			password: 'pass',
+		}).its('body').as('admin');
+
+		cy.get('@admin').then(() => {
+			// ユーザー作成
+			cy.request('POST', '/api/signup', {
+				username: 'alice',
+				password: 'alice1234',
+			}).its('body').as('alice');
 		});
+
+		cy.get('@alice');
+	});
+
+	afterEach(() => {
+		// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
+		// waitを入れることでそれを防止できる
+		cy.wait(1000);
+	});
+
+  it('successfully loads', () => {
+    cy.visit('/');
   });
 
 	it('signin', () => {
-		// インスタンス初期セットアップ
-		cy.request('POST', '/api/admin/accounts/create', {
-			username: 'admin',
-			password: 'pass',
-		}).as('setup');
+		cy.visit('/');
 
-		cy.get('@setup').then(() => {
-			// ユーザー作成
-			cy.request('POST', '/api/signup', {
-				username: 'alice',
-				password: 'alice1234',
-			}).as('signup');
-		});
+		cy.intercept('POST', '/api/signin').as('signin');
 
-		cy.get('@signup').then(() => {
-			cy.visit('/');
+		cy.get('[data-cy-signin]').click();
+		cy.get('[data-cy-signin-username] input').type('alice');
+		// Enterキーでサインインできるかの確認も兼ねる
+		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
 
-			cy.intercept('POST', '/api/signin').as('signin');
-
-			cy.get('[data-cy-signin]').click();
-			cy.get('[data-cy-signin-username] input').type('alice');
-			// Enterキーでサインインできるかの確認も兼ねる
-			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
-
-			cy.wait('@signin');
-		});
-  });
-
-	it('note', () => {
-    cy.visit('/');
-
-		// インスタンス初期セットアップ
-		cy.request('POST', '/api/admin/accounts/create', {
-			username: 'admin',
-			password: 'pass',
-		}).as('setup');
-
-		cy.get('@setup').then(() => {
-			// ユーザー作成
-			cy.request('POST', '/api/signup', {
-				username: 'alice',
-				password: 'alice1234',
-			}).as('signup');
-		});
-
-		cy.get('@signup').then(() => {
-			cy.visit('/');
-
-			cy.intercept('POST', '/api/signin').as('signin');
-
-			cy.get('[data-cy-signin]').click();
-			cy.get('[data-cy-signin-username] input').type('alice');
-			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
-
-			cy.wait('@signin').as('signinEnd');
-		});
-
-		cy.get('@signinEnd').then(() => {
-			cy.get('[data-cy-open-post-form]').click();
-			cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
-			cy.get('[data-cy-open-post-form-submit]').click();
-
-			cy.contains('Hello, Misskey!');
-		});
+		cy.wait('@signin');
   });
 
 	it('suspend', function() {
+		cy.request('POST', '/api/admin/suspend-user', {
+			i: this.admin.token,
+			userId: this.alice.id,
+		});
+
+		cy.visit('/');
+
+		cy.get('[data-cy-signin]').click();
+		cy.get('[data-cy-signin-username] input').type('alice');
+		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+
+		cy.contains('アカウントが凍結されています');
+	});
+});
+
+describe('After user singed in', () => {
+	beforeEach(() => {
+		cy.request('POST', '/api/reset-db').as('reset');
+		cy.get('@reset').its('status').should('equal', 204);
+		cy.reload(true);
+
 		// インスタンス初期セットアップ
 		cy.request('POST', '/api/admin/accounts/create', {
 			username: 'admin',
@@ -134,18 +153,37 @@ describe('Basic', () => {
 		});
 
 		cy.get('@alice').then(() => {
-			cy.request('POST', '/api/admin/suspend-user', {
-				i: this.admin.token,
-				userId: this.alice.id,
-			});
-	
 			cy.visit('/');
-	
+
+			cy.intercept('POST', '/api/signin').as('signin');
+
 			cy.get('[data-cy-signin]').click();
 			cy.get('[data-cy-signin-username] input').type('alice');
 			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
-	
-			cy.contains('アカウントが凍結されています');
+
+			cy.wait('@signin').as('signedIn');
 		});
+
+		cy.get('@signedIn');
 	});
+
+	afterEach(() => {
+		// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
+		// waitを入れることでそれを防止できる
+		cy.wait(1000);
+	});
+
+  it('successfully loads', () => {
+    cy.visit('/');
+  });
+
+	it('note', () => {
+    cy.visit('/');
+
+		cy.get('[data-cy-open-post-form]').click();
+		cy.get('[data-cy-post-form-text]').type('Hello, Misskey!');
+		cy.get('[data-cy-open-post-form-submit]').click();
+
+		cy.contains('Hello, Misskey!');
+  });
 });