const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const Sqlite = require('better-sqlite3'); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raw-account-line-')); const dbPath = path.join(tempDir, 'accounts.db'); // 模拟升级前的 accounts 表,验证 initDB 会为旧库补列。 const legacyDb = new Sqlite(dbPath); legacyDb.exec(` CREATE TABLE accounts ( email TEXT PRIMARY KEY, password TEXT, recovery_email TEXT, secret_key TEXT, browser_id TEXT, country TEXT, sheerid_link TEXT, status TEXT DEFAULT '未登录', message TEXT, status_reason TEXT, proxy_ip TEXT, is_exported INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); legacyDb.close(); const Config = require('../src/core/config'); Config.DATA_DIR = tempDir; Config.DB_PATH = dbPath; const database = require('../src/core/database'); try { database.initDB(); const columns = database.getDb().pragma('table_info(accounts)').map(row => row.name); assert(columns.includes('raw_account_line'), '旧数据库应自动增加 raw_account_line'); assert(columns.includes('password_changed_at'), '旧数据库应自动增加改密时间字段'); const firstRaw = ' test@example.com----OldPass#1----helper@example.com----ABCDEF234567 '; const result = database.importAccountsFromText(`# 注释\r\n${firstRaw}\r\n\r\n`); assert.strictEqual(result.successCount, 1); assert.strictEqual(result.errorCount, 0); let account = database.getAccount('test@example.com'); assert.strictEqual(account.raw_account_line, firstRaw, '应原样保留有效账号整行'); assert.strictEqual(account.secret_key, 'abcdef234567', '字段解析仍可执行既有的 2FA 小写转换'); database.updateAccountPassword(account.email, 'ChangedPassword'); database.updateAccount2faKey(account.email, 'changed2fa'); account = database.getAccount(account.email); assert.strictEqual(account.raw_account_line, firstRaw, '任务更新密码/2FA 不应改写导入原文'); assert.strictEqual(account.password_changed, 1, '成功保存新密码时应同步标记已改密'); assert.strictEqual(account.secret_key, 'changed2fa', '成功保存新2FA时应回填新密钥'); assert.strictEqual(account.fa_changed, 1, '新2FA密钥和已改2FA标记必须同时写入'); assert(account.password_changed_at, '成功保存新密码时应记录改密时间'); const securityDates = JSON.parse(account.security_dates); assert(securityDates.passwordMonth && securityDates.passwordDay, '成功改密后应立即写入今天的密码日期'); database.updateAccountStatusReason(account.email, ' totp_code_wrong '); account = database.getAccount(account.email); assert.strictEqual(account.status_reason, 'TOTP_CODE_WRONG', '失败原因码应标准化后持久化'); database.updateAccountStatusReason(account.email, null); account = database.getAccount(account.email); assert.strictEqual(account.status_reason, null, '登录成功后应能清除旧失败原因'); assert.strictEqual(account.raw_account_line, firstRaw, '原因码更新不应改写导入原文'); const latestRaw = 'test@example.com----LatestPass----LATEST234567'; database.importAccountsFromText(latestRaw); account = database.getAccount(account.email); assert.strictEqual(account.raw_account_line, latestRaw, '重复导入应保存最新一次原文'); const beforeSkippedImport = { ...account }; const skipped = database.importAccountsFromText( 'TEST@example.com----MustNotReplace----SKIPPED234567', '----', '待检查', null, '新客户', true, ); account = database.getAccount(account.email); assert.strictEqual(skipped.successCount, 0, '勾选跳过重复后不应把重复邮箱计为导入成功'); assert.strictEqual(skipped.skippedCount, 1, '邮箱大小写不同时也应识别为重复'); assert.deepStrictEqual(account, beforeSkippedImport, '跳过重复邮箱后账号全部字段都应保持不变'); assert.deepStrictEqual(skipped.importedEmails, [], '跳过的邮箱不应进入导入后的自动初始化名单'); const sameBatch = database.importAccountsFromText( 'new@example.com----FirstPassword\nNEW@example.com----SecondPassword', '----', '未登录', null, null, true, ); assert.strictEqual(sameBatch.successCount, 1, '同一批次首次出现的邮箱应正常导入'); assert.strictEqual(sameBatch.skippedCount, 1, '同一批次后续重复邮箱应跳过'); assert.strictEqual(database.getAccount('new@example.com').password, 'FirstPassword', '同批重复行不应覆盖首次导入内容'); database.updateAccountTag(account.email, '家庭组母号'); database.updateAccount2faKey(account.email, 'familychanged2fa'); account = database.getAccount(account.email); assert.strictEqual(account.secret_key, 'familychanged2fa', '家庭组母号的任务2FA回写也不得被导入保护跳过'); assert.strictEqual(account.fa_changed, 1, '家庭组母号应正常保留已改2FA标记'); const protectedRaw = 'test@example.com----MustNotReplace----PROTECTED234'; database.importAccountsFromText(protectedRaw); account = database.getAccount(account.email); assert.strictEqual(account.raw_account_line, latestRaw, '家庭组母号重复导入时仍应整体跳过'); database.updateAccountStatus(account.email, '登录成功'); database.updateAccountMessage(account.email, '✅ 登录成功'); account = database.getAccount(account.email); assert.strictEqual(account.status, '登录成功', '家庭组母号的任务状态不应被导入保护拦截'); assert.strictEqual(account.message, '✅ 登录成功', '家庭组母号的任务消息不应保留旧错误'); assert.strictEqual(account.raw_account_line, latestRaw, '任务状态/消息更新不应改写导入原文'); console.log('database_raw_account_line.test.js: PASS'); } finally { try { database.getDb().close(); } catch (_) {} fs.rmSync(tempDir, { recursive: true, force: true }); }