feat: add optimistic locking and improve type safety
- Implement optimistic locking for deposit balance updates - Prevent race conditions in concurrent deposit requests - Add automatic retry with exponential backoff (max 3 attempts) - Add version column to user_deposits table - Improve type safety across codebase - Add explicit types for Namecheap API responses - Add typed function arguments (ManageDepositArgs, etc.) - Remove `any` types from deposit-agent and tool files - Add reconciliation job for balance integrity verification - Compare user_deposits.balance vs SUM(confirmed transactions) - Alert admin on discrepancy detection - Set up test environment with Vitest + Miniflare - Add 50+ test cases for deposit system - Add helper functions for test data creation - Update documentation - Add migration guide for version columns - Document optimistic locking patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
109
tests/setup.ts
Normal file
109
tests/setup.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Vitest 테스트 환경 설정
|
||||
*
|
||||
* Miniflare를 사용하여 D1 Database를 in-memory SQLite로 시뮬레이션
|
||||
*/
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { beforeAll, afterEach } from 'vitest';
|
||||
|
||||
declare global {
|
||||
function getMiniflareBindings(): {
|
||||
DB: D1Database;
|
||||
RATE_LIMIT_KV: KVNamespace;
|
||||
};
|
||||
}
|
||||
|
||||
let db: D1Database;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Miniflare 바인딩 가져오기
|
||||
const bindings = getMiniflareBindings();
|
||||
db = bindings.DB;
|
||||
|
||||
// 스키마 초기화
|
||||
const schemaPath = join(__dirname, '../schema.sql');
|
||||
const schema = readFileSync(schemaPath, 'utf-8');
|
||||
|
||||
// 각 statement를 개별 실행
|
||||
const statements = schema
|
||||
.split(';')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0 && !s.startsWith('--'));
|
||||
|
||||
for (const statement of statements) {
|
||||
await db.exec(statement);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// 각 테스트 후 데이터 정리 (스키마는 유지)
|
||||
await db.exec('DELETE FROM deposit_transactions');
|
||||
await db.exec('DELETE FROM bank_notifications');
|
||||
await db.exec('DELETE FROM user_deposits');
|
||||
await db.exec('DELETE FROM user_domains');
|
||||
await db.exec('DELETE FROM summaries');
|
||||
await db.exec('DELETE FROM message_buffer');
|
||||
await db.exec('DELETE FROM users');
|
||||
});
|
||||
|
||||
/**
|
||||
* 테스트용 헬퍼 함수: 사용자 생성
|
||||
*/
|
||||
export async function createTestUser(
|
||||
telegramId: string,
|
||||
username?: string
|
||||
): Promise<number> {
|
||||
const bindings = getMiniflareBindings();
|
||||
const result = await bindings.DB.prepare(
|
||||
'INSERT INTO users (telegram_id, username) VALUES (?, ?)'
|
||||
).bind(telegramId, username || null).run();
|
||||
|
||||
return Number(result.meta?.last_row_id || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 헬퍼 함수: 은행 알림 생성
|
||||
*/
|
||||
export async function createBankNotification(
|
||||
depositorName: string,
|
||||
amount: number
|
||||
): Promise<number> {
|
||||
const bindings = getMiniflareBindings();
|
||||
const result = await bindings.DB.prepare(
|
||||
'INSERT INTO bank_notifications (depositor_name, depositor_name_prefix, amount) VALUES (?, ?, ?)'
|
||||
).bind(depositorName, depositorName.slice(0, 7), amount).run();
|
||||
|
||||
return Number(result.meta?.last_row_id || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 헬퍼 함수: 입금 거래 생성
|
||||
*/
|
||||
export async function createDepositTransaction(
|
||||
userId: number,
|
||||
amount: number,
|
||||
status: string,
|
||||
depositorName?: string
|
||||
): Promise<number> {
|
||||
const bindings = getMiniflareBindings();
|
||||
const result = await bindings.DB.prepare(
|
||||
`INSERT INTO deposit_transactions (user_id, type, amount, status, depositor_name, depositor_name_prefix)
|
||||
VALUES (?, 'deposit', ?, ?, ?, ?)`
|
||||
).bind(
|
||||
userId,
|
||||
amount,
|
||||
status,
|
||||
depositorName || null,
|
||||
depositorName ? depositorName.slice(0, 7) : null
|
||||
).run();
|
||||
|
||||
return Number(result.meta?.last_row_id || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 헬퍼 함수: DB 바인딩 가져오기
|
||||
*/
|
||||
export function getTestDB(): D1Database {
|
||||
return getMiniflareBindings().DB;
|
||||
}
|
||||
Reference in New Issue
Block a user