'use strict'; const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { GOOGLE_APP_PASSWORDS_URL, DEFAULT_APP_NAME, createGmailAppPassword, _private, } = require('../src/services/gmail_app_password_service'); class FakeLocator { constructor(page, kind, visible) { this.page = page; this.kind = kind; this.visible = visible; } first() { return this; } nth() { return this; } async count() { return 1; } async isVisible() { return this.visible; } async fill(value) { assert.strictEqual(this.kind, 'appName'); this.page.appName = value; } async click() { if (this.kind === 'create') this.page.created = true; if (this.kind === 'done') this.page.doneClicked = true; } } class FakePage { constructor({ unavailable = false, reauth = false, language = 'en' } = {}) { this.currentUrl = 'about:blank'; this.unavailable = unavailable; this.reauth = reauth; this.language = language; this.created = false; this.doneClicked = false; this.appName = ''; } url() { return this.currentUrl; } async goto(url) { this.currentUrl = this.reauth ? 'https://accounts.google.com/signin/v2/challenge/pwd' : url; this.reauth = false; } async waitForLoadState() {} async waitForTimeout() {} async evaluate() { if (this.unavailable) { return { dialogText: '', bodyText: 'App passwords are not available for your account.', readonlyValues: [], }; } if (this.created) { return { dialogText: 'Your app password\nabcd efgh ijkl mnop\nUse it in your app', bodyText: 'Your app password', readonlyValues: [], }; } return { dialogText: '', bodyText: 'App passwords', readonlyValues: [] }; } locator(selector) { const onForm = this.currentUrl === GOOGLE_APP_PASSWORDS_URL && !this.unavailable; if (!selector.includes('following::button') && selector.includes('input') && (selector.includes('App name') || selector.includes('type="text"'))) { return new FakeLocator(this, 'appName', onForm && !this.created); } if ((this.language === 'en' && selector.includes('Create')) || (this.language === 'vi' && selector.includes('Tạo')) || (this.language === 'other' && selector.includes('following::button[1]'))) { return new FakeLocator(this, 'create', onForm && !!this.appName && !this.created); } if ((this.language === 'en' && selector.includes('Done')) || (this.language === 'vi' && selector.includes('Xong'))) { return new FakeLocator(this, 'done', this.created); } return new FakeLocator(this, 'missing', false); } } (async () => { assert.strictEqual(DEFAULT_APP_NAME, 'zhaoqiaoqing2001'); assert.strictEqual(_private.normalizeAppPassword('abcd efgh ijkl mnop'), 'abcdefghijklmnop'); assert.strictEqual(_private.normalizeAppPassword('too-short'), null); assert.strictEqual(_private.extractAppPasswordFromSnapshot({ dialogText: 'Your app password: abcd efgh ijkl mnop', readonlyValues: [], }), 'abcdefghijklmnop'); assert.strictEqual(_private.extractAppPasswordFromSnapshot({ dialogText: 'Application name: zhaoqiaoqing2001', readonlyValues: [], }), null, '不得把16位应用名称误识别成密码'); { const page = new FakePage(); const result = await createGmailAppPassword(page, { email: 'mail@example.com', password: 'not-used', }); assert.strictEqual(result.ok, true); assert.strictEqual(result.appPassword, 'abcdefghijklmnop'); assert.strictEqual(page.appName, DEFAULT_APP_NAME); assert.strictEqual(page.doneClicked, true); } { const page = new FakePage({ reauth: true }); let verificationCalled = false; const result = await createGmailAppPassword(page, { email: 'mail@example.com', password: 'not-logged', }, () => {}, { continueGoogleVerificationFn: async currentPage => { verificationCalled = true; currentPage.currentUrl = GOOGLE_APP_PASSWORDS_URL; return [true, '登录成功']; }, }); assert.strictEqual(verificationCalled, true); assert.strictEqual(result.ok, true); } { // Google 会根据账号语言显示越南语按钮:Tạo(创建)/ Xong(完成)。 const page = new FakePage({ language: 'vi' }); const result = await createGmailAppPassword(page, { email: 'mail@example.com', password: 'not-used', }); assert.strictEqual(result.ok, true); assert.strictEqual(page.doneClicked, true); } { // 未知语言不依赖按钮文字,使用应用名称输入框后的首个按钮。 const page = new FakePage({ language: 'other' }); const result = await createGmailAppPassword(page, { email: 'mail@example.com', password: 'not-used', }); assert.strictEqual(result.ok, true); } { // 回归真实故障:第一次 TOTP 验证后回到账户首页,再次打开应用密码页时 // Google 又要求 TOTP。流程必须继续原地验证,不能等待应用名称框超时。 const page = new FakePage(); let appPasswordNavigationCount = 0; page.goto = async url => { if (url === GOOGLE_APP_PASSWORDS_URL) { appPasswordNavigationCount++; page.currentUrl = 'https://accounts.google.com/v3/signin/challenge/totp?continue=apppasswords'; return; } page.currentUrl = url; }; let verificationCount = 0; const result = await createGmailAppPassword(page, { email: 'mail@example.com', password: 'not-used', secret: 'JBSWY3DPEHPK3PXP', }, () => {}, { ensureLoggedInFn: async () => { throw new Error('TOTP challenge 不应跳回普通登录入口'); }, continueGoogleVerificationFn: async currentPage => { verificationCount++; currentPage.currentUrl = verificationCount === 1 ? 'https://myaccount.google.com/' : GOOGLE_APP_PASSWORDS_URL; return [true, '验证成功']; }, }); assert.strictEqual(result.ok, true); assert.strictEqual(verificationCount, 2); assert.strictEqual(appPasswordNavigationCount, 2); } { const page = new FakePage({ unavailable: true }); const result = await createGmailAppPassword(page, { email: 'mail@example.com', password: 'x' }); assert.strictEqual(result.ok, false); assert.match(result.msg, /无法使用应用专用密码/); } const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gmail-app-password-')); let database = null; try { const Config = require('../src/core/config'); Config.DATA_DIR = tempDir; Config.DB_PATH = path.join(tempDir, 'accounts.db'); const db = require('../src/core/database'); database = db; db.initDB(); db.upsertAccount({ email: 'mail@example.com', password: 'password' }); assert.strictEqual(db.hasGmailAppPassword('mail@example.com'), false); assert.strictEqual(db.upsertGmailAppPassword('mail@example.com', 'abcd efgh ijkl mnop'), true); assert.strictEqual(db.hasGmailAppPassword('MAIL@example.com'), true); const saved = db.getGmailAppPassword('mail@example.com'); assert.strictEqual(saved.email, 'mail@example.com'); assert.strictEqual(saved.app_password, 'abcdefghijklmnop'); assert.strictEqual(db.getAllAccounts()[0].gmail_app_password, 'abcdefghijklmnop'); db.upsertGmailAppPassword('mail@example.com', 'qrst uvwx yzab cdef'); assert.strictEqual(db.getGmailAppPassword('mail@example.com').app_password, 'qrstuvwxyzabcdef'); assert.throws(() => db.upsertGmailAppPassword('mail@example.com', 'invalid'), /16 位/); } finally { if (database) database.getDb().close(); fs.rmSync(tempDir, { recursive: true, force: true }); } console.log('gmail_app_password_service.test.js: PASS'); })().catch(error => { console.error(error); process.exitCode = 1; });