회원 탈퇴와 덱 일괄 삭제 추가
This commit is contained in:
@@ -8,10 +8,12 @@ import express from 'express';
|
||||
import { initDb } from '../src/db.js';
|
||||
import { createAdminRouter } from '../src/routes/admin.js';
|
||||
import { createAuthRouter } from '../src/routes/auth.js';
|
||||
import { deleteUserAccount } from '../src/services/accountDeletion.js';
|
||||
|
||||
test('삭제한 계정의 사용자명을 새 계정이 사용해도 기존 정보가 연결되지 않는다', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'uniare-account-delete-'));
|
||||
const dbPath = path.join(tempDir, 'test.db');
|
||||
process.env.AVATARS_PATH = path.join(tempDir, 'avatars');
|
||||
const db = initDb(dbPath);
|
||||
|
||||
try {
|
||||
@@ -34,6 +36,23 @@ test('삭제한 계정의 사용자명을 새 계정이 사용해도 기존 정
|
||||
INSERT INTO deck_favorites (user_id, share_code)
|
||||
VALUES (?, 'OLDDECK1')
|
||||
`).run(oldUserId);
|
||||
const referenceUserId = Number(
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
email, username, password_hash, display_name, email_verified_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
`).run(
|
||||
'reference@example.com',
|
||||
'reference',
|
||||
'reference-password-hash',
|
||||
'reference',
|
||||
).lastInsertRowid,
|
||||
);
|
||||
db.prepare(`
|
||||
INSERT INTO deck_favorites (user_id, share_code)
|
||||
VALUES (?, 'OLDDECK1')
|
||||
`).run(referenceUserId);
|
||||
db.prepare(`
|
||||
INSERT INTO auth_tokens (user_id, type, token_hash, expires_at)
|
||||
VALUES (?, 'verify_email', 'old-token', datetime('now', '+1 day'))
|
||||
@@ -43,7 +62,7 @@ test('삭제한 계정의 사용자명을 새 계정이 사용해도 기존 정
|
||||
VALUES (?, 'verify_email', '2026-06', 'sent')
|
||||
`).run(oldUserId);
|
||||
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(oldUserId);
|
||||
const deletionResult = deleteUserAccount(db, oldUserId);
|
||||
|
||||
const newUserId = Number(
|
||||
db.prepare(`
|
||||
@@ -58,12 +77,22 @@ test('삭제한 계정의 사용자명을 새 계정이 사용해도 기존 정
|
||||
SELECT user_id AS userId FROM decks WHERE share_code = 'OLDDECK1'
|
||||
`).get();
|
||||
|
||||
assert.equal(deletionResult.deleted, true);
|
||||
assert.equal(deletionResult.retainedDeckCount, 1);
|
||||
assert.ok(newUserId > oldUserId);
|
||||
assert.equal(oldDeck.userId, null);
|
||||
assert.equal(
|
||||
db.prepare('SELECT COUNT(*) AS count FROM deck_favorites WHERE user_id = ?').get(oldUserId).count,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
db.prepare(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM deck_favorites
|
||||
WHERE user_id = ? AND share_code = 'OLDDECK1'
|
||||
`).get(referenceUserId).count,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
db.prepare('SELECT COUNT(*) AS count FROM auth_tokens WHERE user_id = ?').get(oldUserId).count,
|
||||
0,
|
||||
@@ -85,23 +114,25 @@ test('삭제한 계정의 사용자명을 새 계정이 사용해도 기존 정
|
||||
}
|
||||
});
|
||||
|
||||
test('관리자는 미인증 회원의 이메일 인증을 수동 승인할 수 있다', async () => {
|
||||
test('이메일 승인·본인 탈퇴·관리자 탈퇴·덱 일괄 삭제 API가 동작한다', async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'uniare-admin-verify-'));
|
||||
const dbPath = path.join(tempDir, 'test.db');
|
||||
const db = initDb(dbPath);
|
||||
process.env.JWT_SECRET = 'test-secret-for-admin-email-verification';
|
||||
|
||||
const adminPassword = 'admin-password';
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
email, username, password_hash, display_name, role, email_verified_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 'admin', datetime('now'))
|
||||
`).run(
|
||||
'admin@example.com',
|
||||
'admin',
|
||||
bcrypt.hashSync(adminPassword, 4),
|
||||
'admin',
|
||||
const adminUserId = Number(
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
email, username, password_hash, display_name, role, email_verified_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 'admin', datetime('now'))
|
||||
`).run(
|
||||
'admin@example.com',
|
||||
'admin',
|
||||
bcrypt.hashSync(adminPassword, 4),
|
||||
'admin',
|
||||
).lastInsertRowid,
|
||||
);
|
||||
const targetUserId = Number(
|
||||
db.prepare(`
|
||||
@@ -120,6 +151,33 @@ test('관리자는 미인증 회원의 이메일 인증을 수동 승인할 수
|
||||
INSERT INTO auth_tokens (user_id, type, token_hash, expires_at)
|
||||
VALUES (?, 'verify_email', 'pending-token', datetime('now', '+1 day'))
|
||||
`).run(targetUserId);
|
||||
db.prepare(`
|
||||
INSERT INTO decks (
|
||||
share_code, share_type, user_id, series, series_name, name, cards_json, is_public
|
||||
)
|
||||
VALUES ('PENDING1', 'user', ?, 'ua01', '테스트', '탈퇴 후 보존 덱', '[]', 1)
|
||||
`).run(targetUserId);
|
||||
const forcedUserId = Number(
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
email, username, password_hash, display_name, email_verified_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
`).run(
|
||||
'forced@example.com',
|
||||
'forced',
|
||||
bcrypt.hashSync('forced-password', 4),
|
||||
'forced',
|
||||
).lastInsertRowid,
|
||||
);
|
||||
db.prepare(`
|
||||
INSERT INTO decks (
|
||||
share_code, share_type, user_id, series, series_name, name, cards_json, is_public
|
||||
)
|
||||
VALUES
|
||||
('BULKDEL1', 'user', ?, 'ua01', '테스트', '일괄 삭제 1', '[]', 1),
|
||||
('BULKDEL2', 'user', ?, 'ua01', '테스트', '일괄 삭제 2', '[]', 1)
|
||||
`).run(adminUserId, adminUserId);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -143,6 +201,16 @@ test('관리자는 미인증 회원의 이메일 인증을 수동 승인할 수
|
||||
assert.equal(loginResponse.status, 200);
|
||||
const { token } = await loginResponse.json();
|
||||
|
||||
const adminSelfDeleteResponse = await fetch(`${baseUrl}/admin/users/${adminUserId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ confirmation: 'DELETE' }),
|
||||
});
|
||||
assert.equal(adminSelfDeleteResponse.status, 400);
|
||||
|
||||
const approveResponse = await fetch(
|
||||
`${baseUrl}/admin/users/${targetUserId}/verify-email`,
|
||||
{
|
||||
@@ -162,6 +230,78 @@ test('관리자는 미인증 회원의 이메일 인증을 수동 승인할 수
|
||||
`).get(targetUserId);
|
||||
assert.ok(target.emailVerifiedAt);
|
||||
assert.ok(pendingToken.usedAt);
|
||||
|
||||
const pendingLoginResponse = await fetch(`${baseUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: 'pending@example.com',
|
||||
password: 'pending-password',
|
||||
}),
|
||||
});
|
||||
assert.equal(pendingLoginResponse.status, 200);
|
||||
const { token: pendingUserToken } = await pendingLoginResponse.json();
|
||||
const invalidSelfDeleteResponse = await fetch(`${baseUrl}/auth/me`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${pendingUserToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ confirmation: 'delete' }),
|
||||
});
|
||||
assert.equal(invalidSelfDeleteResponse.status, 400);
|
||||
const selfDeleteResponse = await fetch(`${baseUrl}/auth/me`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${pendingUserToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ confirmation: 'DELETE' }),
|
||||
});
|
||||
assert.equal(selfDeleteResponse.status, 200);
|
||||
assert.equal(
|
||||
db.prepare('SELECT COUNT(*) AS count FROM users WHERE id = ?').get(targetUserId).count,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
db.prepare(`
|
||||
SELECT user_id AS userId FROM decks WHERE share_code = 'PENDING1'
|
||||
`).get().userId,
|
||||
null,
|
||||
);
|
||||
|
||||
const adminDeleteResponse = await fetch(`${baseUrl}/admin/users/${forcedUserId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ confirmation: 'DELETE' }),
|
||||
});
|
||||
assert.equal(adminDeleteResponse.status, 200);
|
||||
assert.equal(
|
||||
db.prepare('SELECT COUNT(*) AS count FROM users WHERE id = ?').get(forcedUserId).count,
|
||||
0,
|
||||
);
|
||||
|
||||
const bulkDeleteResponse = await fetch(`${baseUrl}/admin/decks/bulk-delete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ shareCodes: ['BULKDEL1', 'BULKDEL2'] }),
|
||||
});
|
||||
assert.equal(bulkDeleteResponse.status, 200);
|
||||
const bulkDeleteResult = await bulkDeleteResponse.json();
|
||||
assert.equal(bulkDeleteResult.deletedCount, 2);
|
||||
assert.equal(
|
||||
db.prepare(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM decks WHERE share_code IN ('BULKDEL1', 'BULKDEL2')
|
||||
`).get().count,
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
|
||||
Reference in New Issue
Block a user