初始化提交:go比特版本精简
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const {
|
||||
runFeishuSyncTask,
|
||||
syncFeishuRefundProcessStatuses,
|
||||
_private,
|
||||
} = require('../src/services/feishu_sync_service');
|
||||
|
||||
(async () => {
|
||||
assert.strictEqual(_private.normalizeEmail('Abeja.rArthuro@GoogleMail.com'), 'abejararthuro@gmail.com');
|
||||
assert.strictEqual(_private.normalizeEmail('Name.Tag+work@gmail.com'), 'nametag@gmail.com');
|
||||
assert.strictEqual(_private.columnName(14), 'O');
|
||||
assert.strictEqual(_private.formatRechargeTimeForFeishu('2026-08-22 05:51:48'), '2026-08-22 13:51:48');
|
||||
assert.strictEqual(_private.formatRechargeTimeForFeishu(''), '');
|
||||
assert.strictEqual(_private.formatAccountRefundStatusForFeishu('无状态'), '未操作');
|
||||
assert.strictEqual(_private.formatAccountRefundStatusForFeishu(''), '未操作');
|
||||
assert.strictEqual(_private.formatAccountRefundStatusForFeishu('已退款'), '已退款');
|
||||
assert.strictEqual(_private.normalizeSession(''), '', '空 Session 应允许继续同步其他字段');
|
||||
assert.throws(() => _private.normalizeSession('not-json'), /不是有效 JSON/, '非空无效 Session 仍应阻止覆盖');
|
||||
assert.strictEqual(
|
||||
_private.parseSpreadsheetUrl('https://tenant.feishu.cn/wiki/wik_test?sheet=sheet1').kind,
|
||||
'wiki',
|
||||
);
|
||||
|
||||
const sessionMarker = 'SESSION_MUST_NEVER_APPEAR_IN_LOGS';
|
||||
const accounts = [
|
||||
{
|
||||
email: 'abejararthuro@gmail.com',
|
||||
password: 'password-one',
|
||||
recovery_email: 'recovery-one@example.com',
|
||||
secret_key: 'TWOFAONE',
|
||||
refund_process_status: '已发起',
|
||||
account_refund_status: 'gpt警告',
|
||||
recharged_at: '2026-08-20 01:02:03',
|
||||
chatgpt_session: { accessToken: sessionMarker, user: { email: 'abejararthuro@gmail.com' } },
|
||||
},
|
||||
{
|
||||
email: 'new.user@example.com',
|
||||
password: 'password-two',
|
||||
recovery_email: '',
|
||||
secret_key: 'TWOFATWO',
|
||||
refund_process_status: '未发起',
|
||||
recharged_at: '2026-08-22 05:51:48',
|
||||
// 本地无 Session,但飞书已有 AC:同步其他字段时必须保留飞书 AC。
|
||||
chatgpt_session: '',
|
||||
},
|
||||
{
|
||||
email: 'without.recharge@example.com',
|
||||
password: 'password-three',
|
||||
recovery_email: 'recovery-three@example.com',
|
||||
secret_key: 'TWOFATHREE',
|
||||
refund_process_status: '发起失败',
|
||||
chatgpt_session: JSON.stringify({ accessToken: 'third-token' }),
|
||||
},
|
||||
];
|
||||
const headers = [
|
||||
'邮件', '密码', '辅助邮件', '2FA密钥', '是否已申请退款', '是否是速刷', '去处', '周限是否已耗尽', '是否已退款', 'AC',
|
||||
'账号状态', '套餐', '所属账号池', '5h剩余', '7d剩余', '限流重置', '最后同步', '开卡卡号',
|
||||
'充值时间',
|
||||
];
|
||||
const requests = [];
|
||||
const httpClient = {
|
||||
async request(config) {
|
||||
requests.push(config);
|
||||
if (config.url.endsWith('/auth/v3/tenant_access_token/internal')) {
|
||||
return { status: 200, data: { code: 0, tenant_access_token: 'tenant-token', expire: 7200 } };
|
||||
}
|
||||
if (config.url.includes('/sheets/query')) {
|
||||
return {
|
||||
status: 200,
|
||||
data: { code: 0, data: { sheets: [{ sheet_id: 'sheet1', grid_properties: { row_count: 100 } }] } },
|
||||
};
|
||||
}
|
||||
if (config.url.includes('/values/') && !config.url.includes('values_batch_update')) {
|
||||
const decoded = decodeURIComponent(config.url);
|
||||
if (decoded.includes('A1:ZZ1')) {
|
||||
return { status: 200, data: { code: 0, data: { valueRange: { values: [headers] } } } };
|
||||
}
|
||||
if (decoded.includes('S2:S100')) {
|
||||
return { status: 200, data: { code: 0, data: { valueRange: { values: [['飞书已有时间'], ['']] } } } };
|
||||
}
|
||||
if (decoded.includes('I2:I100')) {
|
||||
return { status: 200, data: { code: 0, data: { valueRange: { values: [['已退款'], ['旧状态']] } } } };
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
data: { code: 0, data: { valueRange: { values: [['Abeja.rArthuro@gmail.com'], ['new.user@example.com']] } } },
|
||||
};
|
||||
}
|
||||
if (config.url.includes('/insert_dimension_range')) {
|
||||
return { status: 200, data: { code: 0, msg: 'success', data: {} } };
|
||||
}
|
||||
if (config.url.includes('/values_batch_update')) {
|
||||
return { status: 200, data: { code: 0, msg: 'success', data: {} } };
|
||||
}
|
||||
throw new Error(`unexpected request: ${config.method} ${config.url}`);
|
||||
},
|
||||
};
|
||||
const logs = [];
|
||||
const progress = [];
|
||||
const result = await runFeishuSyncTask({
|
||||
emails: ['abejararthuro@gmail.com', 'new.user@example.com', 'without.recharge@example.com'],
|
||||
database: {
|
||||
getAllAccounts: () => accounts,
|
||||
getSetting: (key, fallback) => fallback,
|
||||
},
|
||||
credentialStore: {
|
||||
loadFeishuCredentials: async () => ({ appId: 'cli_test', appSecret: 'app-secret' }),
|
||||
},
|
||||
settings: {
|
||||
feishu_document_url: 'https://tenant.feishu.cn/sheets/sht_test?sheet=sheet1',
|
||||
feishu_notify_mode: 'none',
|
||||
},
|
||||
httpClient,
|
||||
onLog: message => logs.push(message),
|
||||
onProgress: (current, total) => progress.push([current, total]),
|
||||
});
|
||||
|
||||
assert.strictEqual(result.success, true);
|
||||
assert.deepStrictEqual(result.successEmails, ['abejararthuro@gmail.com', 'new.user@example.com', 'without.recharge@example.com']);
|
||||
assert.deepStrictEqual(progress.at(-1), [3, 3]);
|
||||
assert.strictEqual(logs.join('\n').includes(sessionMarker), false, '日志不得包含 Session 内容');
|
||||
const update = requests.find(request => request.url.includes('/values_batch_update'));
|
||||
assert(update, '应调用飞书批量写入接口');
|
||||
const insert = requests.find(request => request.url.includes('/insert_dimension_range'));
|
||||
assert(insert, '新增账号前应先插入带样式的数据行');
|
||||
assert.deepStrictEqual(insert.data.dimension, {
|
||||
sheetId: 'sheet1', majorDimension: 'ROWS', startIndex: 3, endIndex: 4,
|
||||
});
|
||||
assert.strictEqual(insert.data.inheritStyle, 'BEFORE', '新增行应继承上一数据行样式');
|
||||
const ranges = update.data.valueRanges.map(item => item.range);
|
||||
assert(ranges.includes('sheet1!A2:A2'), 'Gmail 点号等价邮箱应更新原有第2行');
|
||||
assert(ranges.includes('sheet1!A3:A3'), '本地无 Session 的已有账号也应更新其他字段');
|
||||
assert(ranges.includes('sheet1!A4:A4'), '未匹配邮箱应新增到下一行');
|
||||
assert(ranges.includes('sheet1!E2:E2') && ranges.includes('sheet1!E3:E3') && ranges.includes('sheet1!E4:E4'),
|
||||
'是否已申请退款列应同步所有账号的独立退款流程状态');
|
||||
assert(!ranges.includes('sheet1!I2:I2'), '飞书是否已退款已有“已退款”时不得被本地状态覆盖');
|
||||
assert(ranges.includes('sheet1!I3:I3') && ranges.includes('sheet1!I4:I4'), '非最终状态账号仍应写入本地退款状态');
|
||||
assert(ranges.includes('sheet1!J2:J2'), '完整 ChatGPT Session 应写入 AC 列');
|
||||
assert(!ranges.includes('sheet1!J3:J3'), '本地 ChatGPT Session 为空时不得覆盖飞书已有 AC');
|
||||
assert(ranges.includes('sheet1!J4:J4'), '本地 ChatGPT Session 有值时应写入并覆盖飞书 AC');
|
||||
assert(!ranges.includes('sheet1!S2:S2'), '飞书已有充值时间时不得覆盖');
|
||||
assert(ranges.includes('sheet1!S3:S3'), '本地有充值时间且飞书为空时应写入');
|
||||
assert(!ranges.includes('sheet1!S4:S4'), '本地没有充值时间时不得写入');
|
||||
assert(!ranges.some(range => /!Q\d+:Q\d+$/.test(range)), '最后同步列不得写入');
|
||||
assert(ranges.every(range => /![ABCDEIJS]\d+:[ABCDEIJS]\d+$/.test(range)), '只允许写入固定映射列和符合条件的充值时间');
|
||||
const processWrites = update.data.valueRanges.filter(item => /!E\d+:E\d+$/.test(item.range));
|
||||
assert.deepStrictEqual(processWrites.map(item => item.values[0][0]), ['已发起', '未发起', '发起失败']);
|
||||
const refundWrites = update.data.valueRanges.filter(item => /!I\d+:I\d+$/.test(item.range));
|
||||
assert.deepStrictEqual(refundWrites.map(item => item.values[0][0]), ['未操作', '未操作']);
|
||||
const rechargeTimeWrite = update.data.valueRanges.find(item => item.range === 'sheet1!S3:S3');
|
||||
assert.strictEqual(rechargeTimeWrite.values[0][0], '2026-08-22 13:51:48', '充值时间应按北京时间写入');
|
||||
const writtenText = JSON.stringify(update.data);
|
||||
assert(writtenText.includes(sessionMarker), 'AC 列应包含完整结构化 Session JSON');
|
||||
assert(!writtenText.includes('最后同步'), '写入请求不应引用最后同步列');
|
||||
|
||||
const statusOnlyRequests = [];
|
||||
const statusOnlyHttpClient = {
|
||||
async request(config) {
|
||||
statusOnlyRequests.push(config);
|
||||
if (config.url.endsWith('/auth/v3/tenant_access_token/internal')) {
|
||||
return { status: 200, data: { code: 0, tenant_access_token: 'tenant-token', expire: 7200 } };
|
||||
}
|
||||
if (config.url.includes('/sheets/query')) {
|
||||
return {
|
||||
status: 200,
|
||||
data: { code: 0, data: { sheets: [{ sheet_id: 'sheet1', grid_properties: { row_count: 20 } }] } },
|
||||
};
|
||||
}
|
||||
if (config.url.includes('/values_batch_update')) {
|
||||
return { status: 200, data: { code: 0, msg: 'success', data: {} } };
|
||||
}
|
||||
if (config.url.includes('/values/')) {
|
||||
const decoded = decodeURIComponent(config.url);
|
||||
if (decoded.includes('A1:ZZ1')) {
|
||||
return {
|
||||
status: 200,
|
||||
data: { code: 0, data: { valueRange: { values: [['邮件', '密码', '是否已授权退款', 'AC']] } } },
|
||||
};
|
||||
}
|
||||
if (decoded.includes('A2:A20')) {
|
||||
return {
|
||||
status: 200,
|
||||
data: { code: 0, data: { valueRange: { values: [['a@example.com'], ['B.Test@gmail.com']] } } },
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error(`unexpected status-only request: ${config.method} ${config.url}`);
|
||||
},
|
||||
};
|
||||
const statusOnly = await syncFeishuRefundProcessStatuses({
|
||||
entries: [
|
||||
{ email: 'a@example.com', refundProcessStatus: '已发起' },
|
||||
{ email: 'btest@gmail.com', refundProcessStatus: '发起失败' },
|
||||
{ email: 'missing@example.com', refundProcessStatus: '已发起' },
|
||||
],
|
||||
database: {
|
||||
getAccount: () => null,
|
||||
getSetting: (key, fallback) => fallback,
|
||||
},
|
||||
credentialStore: {
|
||||
loadFeishuCredentials: async () => ({ appId: 'cli_test', appSecret: 'app-secret' }),
|
||||
},
|
||||
settings: { feishu_document_url: 'https://tenant.feishu.cn/sheets/sht_test?sheet=sheet1' },
|
||||
httpClient: statusOnlyHttpClient,
|
||||
});
|
||||
assert.strictEqual(statusOnly.updatedCount, 2);
|
||||
assert.strictEqual(statusOnly.skippedCount, 1);
|
||||
assert.strictEqual(statusOnly.column, '是否已授权退款');
|
||||
const statusOnlyUpdate = statusOnlyRequests.find(request => request.url.includes('/values_batch_update'));
|
||||
assert(statusOnlyUpdate, '定时退款后应调用飞书批量更新接口');
|
||||
assert.deepStrictEqual(statusOnlyUpdate.data.valueRanges, [
|
||||
{ range: 'sheet1!C2:C2', values: [['已发起']] },
|
||||
{ range: 'sheet1!C3:C3', values: [['发起失败']] },
|
||||
]);
|
||||
assert(statusOnlyUpdate.data.valueRanges.every(item => /!C\d+:C\d+$/.test(item.range)),
|
||||
'定时退款回写只能修改退款流程状态列');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const appSource = fs.readFileSync(path.join(root, 'public', 'app.js'), 'utf8');
|
||||
const html = fs.readFileSync(path.join(root, 'public', 'index.html'), 'utf8');
|
||||
assert.match(html, /@click="runFeishuSync\(\)"/);
|
||||
assert.match(html, /class="btn btn-green"[^>]*@click="runFeishuSync\(\)"/);
|
||||
assert.match(html, /“最后同步”不会读取或写入/);
|
||||
assert.match(html, /x-model="feishuCredentialForm\.appSecret"/);
|
||||
|
||||
let submittedBody = null;
|
||||
class FakeEventSource {
|
||||
constructor(url) { this.url = url; }
|
||||
close() {}
|
||||
}
|
||||
const context = {
|
||||
console,
|
||||
URL,
|
||||
EventSource: FakeEventSource,
|
||||
fetch: async (url, init) => {
|
||||
assert.strictEqual(url, '/api/feishu/sync');
|
||||
submittedBody = JSON.parse(init.body);
|
||||
return { ok: true, json: async () => ({ success: true, taskId: 'feishu-test' }) };
|
||||
},
|
||||
localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} },
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval: () => 0,
|
||||
clearInterval,
|
||||
confirm: () => true,
|
||||
prompt: () => null,
|
||||
document: { addEventListener: () => {}, body: { classList: { add: () => {}, remove: () => {} } } },
|
||||
navigator: { clipboard: { writeText: async () => {} } },
|
||||
Blob,
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(`${appSource}\nthis.__geminiApp = geminiApp;`, context, { filename: 'public/app.js' });
|
||||
const app = context.__geminiApp();
|
||||
app.selectedEmails = new Set(['selected@example.com']);
|
||||
app.feishuCredentialMeta.configured = true;
|
||||
app.settings.feishu_document_url = 'https://tenant.feishu.cn/sheets/sht_test?sheet=sheet1';
|
||||
app.clearLogs = () => {};
|
||||
app.addLog = () => {};
|
||||
await app.runFeishuSync();
|
||||
assert.deepStrictEqual(Array.from(submittedBody.emails), ['selected@example.com']);
|
||||
assert.deepStrictEqual(Object.keys(submittedBody), ['emails'], '前端同步请求只能提交邮箱列表');
|
||||
|
||||
console.log('feishu_sync.test.js passed');
|
||||
})().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user