Migrate project from typing_practiceweb

This commit is contained in:
2026-05-14 16:04:14 +08:00
parent dbc5425706
commit 0e87d5546d
225 changed files with 92811 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import csv
import re
def process_words_file(input_file, output_file):
# 读取文件内容
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
# 去除类别信息 (如 A. (103), B. (105), C. (144)...)
category_pattern = re.compile(r"^[A-Z]\.\s*\(\d+\)\s*$", re.MULTILINE)
content = category_pattern.sub('', content)
# 使用更精确的模式匹配单词条目
# 匹配格式: 序号. 单词 [发音]: 词义
word_pattern = re.compile(r"^\d+\.\s+([^[\n]+?)\s*\[([^\]]+)\]:\s*(.+?)(?=\n\d+\.|$)", re.DOTALL | re.MULTILINE)
matches = word_pattern.findall(content)
# 创建 CSV 文件
with open(output_file, 'w', encoding='utf-8', newline='') as csvfile:
csv_writer = csv.writer(csvfile)
# 写入表头
csv_writer.writerow(['word', 'pronunciation', 'translation'])
for match in matches:
word = match[0].strip()
pronunciation = match[1].strip()
# 处理多行翻译,替换换行符为空格
translation = re.sub(r'\s+', ' ', match[2].strip())
csv_writer.writerow([word, pronunciation, translation])
print(f"转换完成CSV 文件已保存为: {output_file}")
# 输入文件路径和输出文件路径
input_file = 'words.txt'
output_file = 'words.csv'
process_words_file(input_file, output_file)

230
scripts/migrate-complete.ts Normal file
View File

@@ -0,0 +1,230 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import path from 'path';
// 加载环境变量
dotenv.config({ path: path.resolve(__dirname, '../server/.env') });
// 定义模型类型接口
interface IModeStats {
streak?: number;
totalCorrect?: number;
totalWrong?: number;
mastered?: boolean;
inWrongBook?: boolean;
lastTestedAt?: Date;
lastMasteredAt?: Date;
}
interface IWordRecord {
_id: mongoose.Types.ObjectId;
user: mongoose.Types.ObjectId;
word: mongoose.Types.ObjectId;
multipleChoice?: IModeStats;
audioToEnglish?: IModeStats;
chineseToEnglish?: IModeStats;
isFullyMastered?: boolean;
lastFullyMasteredAt?: Date;
createdAt?: Date;
[key: string]: any;
}
// 定义模型结构
const ModeStatsSchema = new mongoose.Schema({
streak: { type: Number, default: 0 },
totalCorrect: { type: Number, default: 0 },
totalWrong: { type: Number, default: 0 },
mastered: { type: Boolean, default: false },
inWrongBook: { type: Boolean, default: false },
lastTestedAt: Date,
lastMasteredAt: Date
}, { _id: false });
const WordRecordSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
word: { type: mongoose.Schema.Types.ObjectId, ref: 'Word', required: true },
multipleChoice: { type: ModeStatsSchema, default: () => ({}) },
audioToEnglish: { type: ModeStatsSchema, default: () => ({}) },
chineseToEnglish: { type: ModeStatsSchema, default: () => ({}) },
isFullyMastered: { type: Boolean, default: false },
lastFullyMasteredAt: Date,
createdAt: { type: Date, default: Date.now }
}, { strict: false });
// 注册模型
const WordRecord = mongoose.model<IWordRecord>('WordRecord', WordRecordSchema);
// 获取数据库URI
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/typeskill';
console.log('使用数据库连接:', MONGODB_URI);
// 连接数据库
mongoose.connect(MONGODB_URI)
.then(() => {
console.log('数据库连接成功,开始检查数据...');
return migrateWordRecords();
})
.then(() => {
console.log('处理完成!');
mongoose.connection.close();
process.exit(0);
})
.catch(err => {
console.error('处理失败:', err);
mongoose.connection.close();
process.exit(1);
});
async function migrateWordRecords() {
try {
// 获取所有记录
const records = await WordRecord.find({}).lean();
console.log(`共找到 ${records.length} 条记录`);
// 准备批量更新
const updateOperations = [];
let needUpdateCount = 0;
// 检查每个记录
for (const record of records) {
const modes = ['chineseToEnglish', 'audioToEnglish', 'multipleChoice'];
// 检查是否所有模式都存在并计算掌握状态
let allModesExist = true;
let allMastered = true;
for (const mode of modes) {
if (!record[mode] || typeof record[mode] !== 'object') {
allModesExist = false;
allMastered = false;
break;
}
if (record[mode].mastered !== true) {
allMastered = false;
}
}
// 计算是否完全掌握
const shouldBeFullyMastered = allModesExist && allMastered;
// 计算最新掌握时间和最新测试时间
let latestMasteredDate = null;
let latestTestedDate = null;
if (shouldBeFullyMastered) {
for (const mode of modes) {
const modeMasteredDate = record[mode]?.lastMasteredAt;
if (modeMasteredDate && (!latestMasteredDate || new Date(modeMasteredDate) > new Date(latestMasteredDate))) {
latestMasteredDate = modeMasteredDate;
}
const modeTestedDate = record[mode]?.lastTestedAt;
if (modeTestedDate && (!latestTestedDate || new Date(modeTestedDate) > new Date(latestTestedDate))) {
latestTestedDate = modeTestedDate;
}
}
}
// 取最大值
let lastFullyMasteredAt = null;
if (shouldBeFullyMastered) {
if (latestMasteredDate && latestTestedDate) {
lastFullyMasteredAt = new Date(latestMasteredDate) > new Date(latestTestedDate)
? latestMasteredDate : latestTestedDate;
} else if (latestMasteredDate) {
lastFullyMasteredAt = latestMasteredDate;
} else if (latestTestedDate) {
lastFullyMasteredAt = latestTestedDate;
}
}
// 检查是否需要更新
let needsUpdate = false;
// 情况1: isFullyMastered字段与计算结果不一致
if (record.isFullyMastered !== shouldBeFullyMastered) {
needsUpdate = true;
}
// 情况2: 应该完全掌握但缺少lastFullyMasteredAt字段或值不正确
if (shouldBeFullyMastered &&
(!record.lastFullyMasteredAt ||
(lastFullyMasteredAt && new Date(record.lastFullyMasteredAt).getTime() !== new Date(lastFullyMasteredAt).getTime()))) {
needsUpdate = true;
}
// 如果需要更新,添加到批量操作
if (needsUpdate) {
needUpdateCount++;
const updateOperation = {
updateOne: {
filter: { _id: record._id },
update: {
$set: {
isFullyMastered: shouldBeFullyMastered,
...(lastFullyMasteredAt ? { lastFullyMasteredAt: new Date(lastFullyMasteredAt) } : {})
}
}
}
};
updateOperations.push(updateOperation);
}
}
console.log(`检查完成,发现 ${needUpdateCount} 条记录需要更新`);
// 执行批量更新
if (updateOperations.length > 0) {
const result = await WordRecord.bulkWrite(updateOperations);
console.log(`成功更新 ${result.modifiedCount} 条记录`);
} else {
console.log('没有记录需要更新');
}
// 验证更新后的状态
const missingFieldCount = await WordRecord.countDocuments({
isFullyMastered: { $exists: false }
});
if (missingFieldCount > 0) {
console.log(`警告:仍有 ${missingFieldCount} 条记录缺少isFullyMastered字段`);
} else {
console.log('所有记录现在都包含isFullyMastered字段');
}
// 检查掌握一致性
const inconsistentRecords = await WordRecord.find({
$or: [
// 情况1: 三种模式都掌握了但isFullyMastered为false
{
'chineseToEnglish.mastered': true,
'audioToEnglish.mastered': true,
'multipleChoice.mastered': true,
isFullyMastered: false
},
// 情况2: 至少一种模式未掌握但isFullyMastered为true
{
$or: [
{ 'chineseToEnglish.mastered': { $ne: true } },
{ 'audioToEnglish.mastered': { $ne: true } },
{ 'multipleChoice.mastered': { $ne: true } }
],
isFullyMastered: true
}
]
}).limit(5);
if (inconsistentRecords.length > 0) {
console.log(`警告:发现 ${inconsistentRecords.length} 条记录的掌握状态不一致显示前5条`);
inconsistentRecords.forEach(r => {
console.log(`ID: ${r._id}, Word: ${r.word}, Modes: ${r.chineseToEnglish?.mastered}, ${r.audioToEnglish?.mastered}, ${r.multipleChoice?.mastered}, isFullyMastered: ${r.isFullyMastered}`);
});
} else {
console.log('所有记录的掌握状态一致');
}
} catch (error) {
console.error('处理过程中发生错误:', error);
throw error;
}
}

View File

@@ -0,0 +1,91 @@
import mongoose from 'mongoose';
import { config } from '../server/config';
// 连接数据库
mongoose.connect(config.MONGODB_URI)
.then(() => {
console.log('数据库连接成功,开始迁移...');
migrateWordRecords();
}).catch(err => {
console.error('数据库连接失败:', err);
process.exit(1);
});
// 导入 Vocabulary 模型定义
import '../server/models/Vocabulary'; // 确保模型已注册
async function migrateWordRecords() {
try {
// 获取WordRecord模型
const WordRecord = mongoose.model('WordRecord');
// 获取所有WordRecord记录
console.log('正在获取所有单词记录...');
const records = await WordRecord.find({});
console.log(`共找到 ${records.length} 条记录需要迁移`);
// 批量处理每批次1000条
const batchSize = 1000;
const totalBatches = Math.ceil(records.length / batchSize);
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
const start = batchIndex * batchSize;
const end = Math.min(start + batchSize, records.length);
const batch = records.slice(start, end);
console.log(`处理批次 ${batchIndex + 1}/${totalBatches}, 记录 ${start + 1}${end}`);
const updatePromises = batch.map(async (record) => {
try {
// 判断是否全部掌握
const modes = ['chineseToEnglish', 'audioToEnglish', 'multipleChoice'];
const isFullyMastered = modes.every(mode => record[mode]?.mastered === true);
// 如果全部掌握,找出最近的掌握时间
let lastFullyMasteredAt: Date | null = null;
if (isFullyMastered) {
let latestDate: Date | null = null;
modes.forEach(mode => {
const modeDate = record[mode]?.lastMasteredAt;
if (modeDate && (!latestDate || modeDate > latestDate)) {
latestDate = modeDate;
}
});
lastFullyMasteredAt = latestDate;
}
// 修复: 显式定义updateData类型
const updateData: {
isFullyMastered: boolean;
lastFullyMasteredAt?: Date
} = {
isFullyMastered: isFullyMastered
};
if (lastFullyMasteredAt) {
updateData.lastFullyMasteredAt = lastFullyMasteredAt;
}
// 执行更新
await WordRecord.updateOne({ _id: record._id }, { $set: updateData });
return true;
} catch (error) {
console.error(`更新记录 ${record._id} 失败:`, error);
return false;
}
});
// 等待批次完成
const results = await Promise.all(updatePromises);
const successCount = results.filter(Boolean).length;
console.log(`批次 ${batchIndex + 1} 完成: ${successCount}/${batch.length} 条记录成功更新`);
}
console.log('迁移完成!');
mongoose.connection.close();
process.exit(0);
} catch (error) {
console.error('迁移过程中发生错误:', error);
mongoose.connection.close();
process.exit(1);
}
}

105
scripts/migratePasswords.ts Normal file
View File

@@ -0,0 +1,105 @@
import mongoose, { Document } from 'mongoose';
import bcrypt from 'bcrypt';
import { config } from '../server/config';
// 定义用户接口
interface IUser extends Document {
username: string;
password: string;
email: string;
isAdmin: boolean;
created: Date;
lastLogin: Date;
}
// 创建 Schema
const userSchema = new mongoose.Schema({
username: String,
password: String,
email: String,
isAdmin: { type: Boolean, default: false },
created: { type: Date, default: Date.now },
lastLogin: { type: Date, default: Date.now }
});
// 使用相同的方式创建模型
const User = mongoose.model<IUser>('User', userSchema);
async function migratePasswords() {
try {
// 连接数据库并等待连接完成
await mongoose.connect(config.MONGODB_URI, {
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 10000,
socketTimeoutMS: 45000,
});
console.log('数据库连接成功: ' + config.MONGODB_URI);
// 使用 User 模型查询
const users = await User.find({}).exec();
console.log(`找到 ${users.length} 个用户需要迁移`);
// 记录成功和失败的数量
let successCount = 0;
let failCount = 0;
// 遍历所有用户
for (const user of users) {
try {
console.log('处理用户:', user.username);
// 检查密码是否已经是哈希形式
if (user.password && user.password.length === 60 && user.password.startsWith('$2b$')) {
console.log(`用户 ${user.username} 的密码已经是加密格式`);
continue;
}
// 获取原始密码
const originalPassword = user.password;
if (!originalPassword) {
console.log(`用户 ${user.username} 没有密码,跳过`);
continue;
}
// 生成加密密码
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(originalPassword, salt);
// 更新用户密码
await User.updateOne(
{ _id: user._id },
{ $set: { password: hashedPassword } }
);
console.log(`用户 ${user.username} 密码迁移成功`);
successCount++;
} catch (error) {
console.error(`用户 ${user.username} 密码迁移失败:`, error);
failCount++;
}
}
console.log('\n迁移完成统计:');
console.log(`总用户数: ${users.length}`);
console.log(`成功: ${successCount}`);
console.log(`失败: ${failCount}`);
} catch (error) {
console.error('迁移过程出错:', error);
} finally {
// 关闭数据库连接
await mongoose.disconnect();
console.log('数据库连接已关闭');
}
}
// 运行迁移
migratePasswords().then(() => {
console.log('迁移脚本执行完成');
process.exit(0);
}).catch((error) => {
console.error('迁移脚本执行失败:', error);
process.exit(1);
});

64
scripts/resetPassword.js Normal file
View File

@@ -0,0 +1,64 @@
const bcrypt = require('bcrypt');
const { MongoClient } = require('mongodb');
// 配置
const MONGODB_URI = 'mongodb://localhost:27017/typeskill';
const USERNAME = 'bobcoc';
const NEW_PASSWORD = 'Admin123'; // 新密码
const SALT_ROUNDS = 10;
async function resetPassword() {
let client;
try {
client = new MongoClient(MONGODB_URI);
await client.connect();
console.log('Connected to MongoDB');
const db = client.db();
const users = db.collection('users');
// 查找用户
const user = await users.findOne({ username: USERNAME });
if (!user) {
console.error(`用户 "${USERNAME}" 不存在`);
return;
}
// 生成新的密码哈希
const passwordHash = await bcrypt.hash(NEW_PASSWORD, SALT_ROUNDS);
// 更新用户密码
const result = await users.updateOne(
{ username: USERNAME },
{ $set: { password: passwordHash } }
);
if (result.modifiedCount === 1) {
console.log(`已成功重置 "${USERNAME}" 的密码为 "${NEW_PASSWORD}"`);
// 输出用户信息
const updatedUser = await users.findOne({ username: USERNAME });
console.log('用户信息:', {
_id: updatedUser._id,
username: updatedUser.username,
email: updatedUser.email,
isAdmin: updatedUser.isAdmin,
updatedAt: updatedUser.updatedAt
});
} else {
console.log('密码重置失败,请重试');
}
} catch (error) {
console.error('重置密码时出错:', error);
} finally {
if (client) {
await client.close();
console.log('MongoDB 连接已关闭');
}
}
}
// 执行密码重置
resetPassword();

View File

@@ -0,0 +1,66 @@
const puppeteer = require('puppeteer');
(async () => {
const url = process.argv[2] || 'http://localhost:5001/tower-defense';
console.log('Opening', url);
const browser = await puppeteer.launch({headless: true, args: ['--no-sandbox','--disable-setuid-sandbox']});
const page = await browser.newPage();
// Collect console messages from all frames
page.on('console', msg => {
try {
const type = msg.type();
const text = msg.text();
console.log(`[PAGE][${type}] ${text}`);
} catch (e) {
console.log('[PAGE][console] (error reading message)');
}
});
// Also listen to page errors
page.on('pageerror', err => {
console.log('[PAGE][error]', err.toString());
});
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
// Try to focus the iframe and click center
try {
const frames = page.frames();
const tdFrame = frames.find(f => f.url().includes('/tower-defense/td.html')) || frames[0];
console.log('Found frames:', frames.map(f => f.url()));
if (tdFrame) {
// Evaluate in iframe: print some markers and simulate a click on canvas
await tdFrame.evaluate(() => {
try {
console.log('Inside iframe: document.readyState=' + document.readyState);
const canvas = document.querySelector('canvas');
if (canvas) {
const rect = canvas.getBoundingClientRect();
console.log('Inside iframe: canvas rect', rect.width, rect.height);
// dispatch mousemove and click events at center
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const move = new MouseEvent('mousemove', {clientX: cx, clientY: cy, bubbles: true});
const click = new MouseEvent('click', {clientX: cx, clientY: cy, bubbles: true});
canvas.dispatchEvent(move);
canvas.dispatchEvent(click);
console.log('Inside iframe: dispatched synthetic events');
} else {
console.log('Inside iframe: canvas not found');
}
} catch (e) {
console.log('Inside iframe: eval error', e.toString());
}
});
}
} catch (e) {
console.log('Error interacting with iframe:', e.toString());
}
// Wait a bit to collect logs
await new Promise(r => setTimeout(r, 4000));
await browser.close();
console.log('Done.');
})();

134
scripts/test_tower_e2e.js Normal file
View File

@@ -0,0 +1,134 @@
/**
* 塔防游戏成绩提交端到端测试
* 测试流程:登录 -> 模拟成绩提交 -> 验证数据库存储 -> 检查排行榜
*/
const fetch = globalThis.fetch || require('node-fetch');
const BASE_URL = 'http://localhost:5001';
const TEST_USERNAME = 'testuser';
const TEST_PASSWORD = 'testpass123';
let token = null;
let userId = null;
let sessionCookie = null;
async function request(method, path, body = null, headers = {}) {
const url = `${BASE_URL}${path}`;
const options = {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
};
if (body) {
options.body = JSON.stringify(body);
}
try {
const res = await fetch(url, options);
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch (e) {
data = text;
}
return { status: res.status, data };
} catch (e) {
console.error(`Request error (${method} ${path}):`, e.message);
return { status: 0, data: null };
}
}
async function test() {
console.log('🎮 Tower Defense E2E Test Started\n');
// 1. 尝试注册或登录
console.log('1⃣ Registering/logging in...');
const registerRes = await request('POST', '/api/auth/register', {
username: TEST_USERNAME,
password: TEST_PASSWORD,
email: 'testuser@example.com',
fullname: 'Test User',
});
if (registerRes.status !== 201 && registerRes.status !== 409) {
console.error('❌ Register failed:', registerRes);
return;
}
// 2. 登录
const loginRes = await request('POST', '/api/auth/login', {
username: TEST_USERNAME,
password: TEST_PASSWORD,
});
if (loginRes.status !== 200) {
console.error('❌ Login failed:', loginRes);
return;
}
token = loginRes.data?.token;
userId = loginRes.data?.user?._id;
console.log(`✅ Logged in. Token: ${token?.substring(0, 20)}... UserId: ${userId}`);
// 3. 提交成绩
console.log('\n2⃣ Submitting tower defense score...');
const score = Math.floor(Math.random() * 5000) + 1000;
const wave = Math.floor(Math.random() * 10) + 1;
const timeSeconds = Math.floor(Math.random() * 300) + 60;
const submitRes = await request('POST', '/api/tower-defense/record',
{ wave, score, timeSeconds },
{ Authorization: `Bearer ${token}` }
);
if (submitRes.status !== 201) {
console.error('❌ Score submission failed:', submitRes);
return;
}
console.log(`✅ Score submitted: wave=${wave}, score=${score}, time=${timeSeconds}s`);
console.log(` Response:`, submitRes.data);
// 4. 获取个人最佳
console.log('\n3⃣ Fetching personal best...');
const bestRes = await request('GET', '/api/tower-defense/personal-best', null, {
Authorization: `Bearer ${token}`,
});
if (bestRes.status === 200 && bestRes.data?.hasBest) {
console.log(`✅ Personal best found: score=${bestRes.data.bestScore}, wave=${bestRes.data.bestWave}`);
} else {
console.error('⚠️ No personal best found:', bestRes);
}
// 5. 获取排行榜
console.log('\n4⃣ Fetching leaderboard...');
const leaderRes = await request('GET', '/api/tower-defense/leaderboard?page=1&limit=10');
if (leaderRes.status === 200 && leaderRes.data?.records?.length > 0) {
console.log(`✅ Leaderboard retrieved (${leaderRes.data.records.length} records)`);
console.log(' Top 3:');
leaderRes.data.records.slice(0, 3).forEach((rec, i) => {
console.log(` ${i + 1}. ${rec.fullname}: score=${rec.bestScore}, wave=${rec.bestWave}, games=${rec.totalGames}`);
});
// 检查我们的用户是否在榜单中
const found = leaderRes.data.records.some(r => String(r.userId) === String(userId));
if (found) {
console.log(`✅ Current user found in leaderboard!`);
} else {
console.log(`⚠️ Current user NOT found in leaderboard (may be filtered or paginated)`);
}
} else {
console.error('❌ Leaderboard fetch failed:', leaderRes);
}
console.log('\n✅ E2E Test Complete!\n');
}
// 等待服务器启动并运行测试
setTimeout(test, 3000);

156
scripts/test_tower_e2e.ts Normal file
View File

@@ -0,0 +1,156 @@
/**
* Tower Defense E2E Test
* 测试完整链路:游戏发送 postMessage -> 前端接收 -> API 提交 -> 数据库存储
*/
import axios from 'axios';
import { MongoClient } from 'mongodb';
const API_BASE = 'http://localhost:5001';
const MONGODB_URI = 'mongodb://localhost:27017/typeskill';
// 模拟用户登录信息(可根据实际调整)
const TEST_USER = {
_id: 'test-user-e2e-' + Date.now(),
username: 'testuser_e2e',
fullname: 'Test User E2E'
};
const TEST_RECORD = {
wave: 5,
score: 1250,
timeSeconds: 180
};
async function runE2ETest() {
console.log('🧪 Tower Defense E2E Test Started');
console.log('=====================================\n');
let client: MongoClient | null = null;
try {
// Step 1: Connect to MongoDB and setup test data
console.log('📊 Step 1: Connecting to MongoDB...');
client = new MongoClient(MONGODB_URI);
await client.connect();
const db = client.db();
const usersCollection = db.collection('users');
const recordsCollection = db.collection('towerdefenserecords');
// Clean up old test records
await recordsCollection.deleteMany({ username: TEST_USER.username });
console.log(' ✓ MongoDB connected, test records cleaned\n');
// Step 2: Insert test user (simulate logged-in user)
console.log('👤 Step 2: Creating test user...');
const userResult = await usersCollection.updateOne(
{ username: TEST_USER.username },
{
$set: {
...TEST_USER,
email: `${TEST_USER.username}@test.local`,
password: 'hashed-password',
isAdmin: false,
createdAt: new Date(),
updatedAt: new Date()
}
},
{ upsert: true }
);
const userId = userResult.upsertedId || (await usersCollection.findOne({ username: TEST_USER.username }))?._id;
console.log(` ✓ Test user created/found: ${userId}\n`);
// Step 3: Simulate game completing and calling API with user auth
console.log('🎮 Step 3: Simulating game completion & API submission...');
console.log(` Submitting: wave=${TEST_RECORD.wave}, score=${TEST_RECORD.score}, timeSeconds=${TEST_RECORD.timeSeconds}`);
// In a real scenario, the session cookie would be set; here we'll use basic auth or mock
const submitResponse = await axios.post(
`${API_BASE}/api/tower-defense/record`,
TEST_RECORD,
{
headers: {
'Content-Type': 'application/json',
// In real test, you'd have session/auth headers set
},
// For this mock test, axios will send with any existing cookies/auth
withCredentials: true
}
);
if (submitResponse.status === 201) {
console.log(' ✓ API submission successful (201)\n');
} else {
console.log(` ⚠ API response: ${submitResponse.status}\n`);
}
// Step 4: Verify record was saved to database
console.log('🔍 Step 4: Verifying record in database...');
const savedRecord = await recordsCollection.findOne(
{ username: TEST_USER.username, score: TEST_RECORD.score }
);
if (savedRecord) {
console.log(' ✓ Record found in database:');
console.log(` - username: ${savedRecord.username}`);
console.log(` - wave: ${savedRecord.wave}`);
console.log(` - score: ${savedRecord.score}`);
console.log(` - timeSeconds: ${savedRecord.timeSeconds}`);
console.log(` - createdAt: ${savedRecord.createdAt}\n`);
} else {
console.log(' ✗ Record NOT found in database (check auth/session)\n');
throw new Error('Record insertion failed');
}
// Step 5: Test leaderboard endpoint
console.log('🏆 Step 5: Testing leaderboard endpoint...');
const leaderboardResponse = await axios.get(
`${API_BASE}/api/tower-defense/leaderboard?page=1&limit=20`
);
if (leaderboardResponse.status === 200) {
const records = leaderboardResponse.data.records || [];
console.log(` ✓ Leaderboard retrieved: ${records.length} record(s)`);
if (records.length > 0) {
console.log(` - Top: ${records[0].fullname} (score: ${records[0].bestScore})\n`);
}
}
// Step 6: Test duplicate submission protection
console.log('🛡 Step 6: Testing duplicate submission protection...');
const dupeResponse = await axios.post(
`${API_BASE}/api/tower-defense/record`,
TEST_RECORD,
{ withCredentials: true }
);
if (dupeResponse.status === 200 && dupeResponse.data.message?.includes('重复')) {
console.log(' ✓ Duplicate protection working (returned 200 with ignore message)\n');
} else if (dupeResponse.status === 201) {
console.log(' Duplicate was accepted (debounce window may have expired)\n');
}
console.log('✅ E2E Test PASSED\n');
console.log('Summary:');
console.log(' - postMessage simulation: ✓');
console.log(' - API submission: ✓');
console.log(' - Database storage: ✓');
console.log(' - Leaderboard retrieval: ✓');
console.log(' - Duplicate protection: ✓');
} catch (error: any) {
console.error('\n❌ E2E Test FAILED');
console.error('Error:', error.message);
if (error.response?.data) {
console.error('Response data:', error.response.data);
}
process.exit(1);
} finally {
if (client) {
await client.close();
console.log('\nMongoDB connection closed.');
}
}
}
runE2ETest();

1544
scripts/words.csv Normal file

File diff suppressed because it is too large Load Diff

1825
scripts/words.txt Normal file

File diff suppressed because it is too large Load Diff