Files
go_bt/tests/gmail_app_password_service.test.js

227 lines
7.8 KiB
JavaScript

'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;
});