227 lines
7.8 KiB
JavaScript
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;
|
|
});
|