486 lines
14 KiB
JavaScript
486 lines
14 KiB
JavaScript
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
|
||
const {
|
||
buildHotmailMailApiLatestUrl,
|
||
extractVerificationCodeFromMessage,
|
||
filterHotmailAccountsByUsage,
|
||
extractVerificationCode,
|
||
getLatestHotmailMessage,
|
||
getHotmailBulkActionLabel,
|
||
getHotmailListToggleLabel,
|
||
getHotmailMailApiRequestConfig,
|
||
getHotmailVerificationPollConfig,
|
||
getHotmailVerificationRequestTimestamp,
|
||
normalizeHotmailMailApiMessages,
|
||
parseHotmailImportText,
|
||
pickHotmailAccountForRun,
|
||
pickVerificationMessage,
|
||
pickVerificationMessageWithFallback,
|
||
pickVerificationMessageWithTimeFallback,
|
||
shouldClearHotmailCurrentSelection,
|
||
upsertHotmailAccountInList,
|
||
} = require('../hotmail-utils.js');
|
||
|
||
test('pickHotmailAccountForRun prefers authorized account with oldest lastUsedAt', () => {
|
||
const now = Date.UTC(2026, 3, 10, 10, 0, 0);
|
||
const accounts = [
|
||
{
|
||
id: 'recent',
|
||
email: 'recent@hotmail.com',
|
||
status: 'authorized',
|
||
refreshToken: 'rt-recent',
|
||
lastUsedAt: now - 1_000,
|
||
},
|
||
{
|
||
id: 'oldest',
|
||
email: 'oldest@hotmail.com',
|
||
status: 'authorized',
|
||
refreshToken: 'rt-oldest',
|
||
lastUsedAt: now - 50_000,
|
||
},
|
||
{
|
||
id: 'pending',
|
||
email: 'pending@hotmail.com',
|
||
status: 'pending',
|
||
refreshToken: '',
|
||
lastUsedAt: 0,
|
||
},
|
||
];
|
||
|
||
const selected = pickHotmailAccountForRun(accounts, { now });
|
||
|
||
assert.equal(selected.id, 'oldest');
|
||
});
|
||
|
||
test('pickHotmailAccountForRun skips used accounts but ignores legacy enabled flag', () => {
|
||
const accounts = [
|
||
{
|
||
id: 'disabled',
|
||
email: 'disabled@hotmail.com',
|
||
status: 'authorized',
|
||
refreshToken: 'rt-disabled',
|
||
enabled: false,
|
||
used: false,
|
||
lastUsedAt: 1,
|
||
},
|
||
{
|
||
id: 'used',
|
||
email: 'used@hotmail.com',
|
||
status: 'authorized',
|
||
refreshToken: 'rt-used',
|
||
enabled: true,
|
||
used: true,
|
||
lastUsedAt: 0,
|
||
},
|
||
{
|
||
id: 'available',
|
||
email: 'available@hotmail.com',
|
||
status: 'authorized',
|
||
refreshToken: 'rt-available',
|
||
enabled: true,
|
||
used: false,
|
||
lastUsedAt: 2,
|
||
},
|
||
];
|
||
|
||
const selected = pickHotmailAccountForRun(accounts, {});
|
||
|
||
assert.equal(selected.id, 'disabled');
|
||
});
|
||
|
||
test('pickHotmailAccountForRun returns null for used-only pools', () => {
|
||
assert.equal(
|
||
pickHotmailAccountForRun([{
|
||
id: 'used-only',
|
||
email: 'used-only@hotmail.com',
|
||
status: 'authorized',
|
||
refreshToken: 'rt-used-only',
|
||
used: true,
|
||
lastUsedAt: 0,
|
||
}], {}),
|
||
null
|
||
);
|
||
});
|
||
|
||
test('pickHotmailAccountForRun falls back to never-used authorized account first', () => {
|
||
const accounts = [
|
||
{
|
||
id: 'used',
|
||
email: 'used@hotmail.com',
|
||
status: 'authorized',
|
||
refreshToken: 'rt-used',
|
||
lastUsedAt: Date.UTC(2026, 3, 10, 9, 0, 0),
|
||
},
|
||
{
|
||
id: 'fresh',
|
||
email: 'fresh@hotmail.com',
|
||
status: 'authorized',
|
||
refreshToken: 'rt-fresh',
|
||
lastUsedAt: 0,
|
||
},
|
||
];
|
||
|
||
const selected = pickHotmailAccountForRun(accounts, { now: Date.UTC(2026, 3, 10, 10, 0, 0) });
|
||
|
||
assert.equal(selected.id, 'fresh');
|
||
});
|
||
|
||
test('upsertHotmailAccountInList replaces matching account state by id', () => {
|
||
const accounts = [
|
||
{
|
||
id: 'active',
|
||
email: 'active@hotmail.com',
|
||
status: 'authorized',
|
||
used: false,
|
||
},
|
||
{
|
||
id: 'other',
|
||
email: 'other@hotmail.com',
|
||
status: 'authorized',
|
||
used: false,
|
||
},
|
||
];
|
||
|
||
const nextAccounts = upsertHotmailAccountInList(accounts, {
|
||
id: 'active',
|
||
email: 'active@hotmail.com',
|
||
status: 'authorized',
|
||
used: true,
|
||
});
|
||
|
||
assert.deepEqual(nextAccounts, [
|
||
{
|
||
id: 'active',
|
||
email: 'active@hotmail.com',
|
||
status: 'authorized',
|
||
used: true,
|
||
},
|
||
{
|
||
id: 'other',
|
||
email: 'other@hotmail.com',
|
||
status: 'authorized',
|
||
used: false,
|
||
},
|
||
]);
|
||
});
|
||
|
||
test('shouldClearHotmailCurrentSelection returns true only when account becomes used', () => {
|
||
assert.equal(shouldClearHotmailCurrentSelection({
|
||
id: 'used',
|
||
used: true,
|
||
}), true);
|
||
|
||
assert.equal(shouldClearHotmailCurrentSelection({
|
||
id: 'available',
|
||
used: false,
|
||
}), false);
|
||
});
|
||
|
||
test('extractVerificationCode returns first six-digit code from multilingual mail text', () => {
|
||
assert.equal(extractVerificationCode('你的 ChatGPT 验证码为 370794,请勿泄露。'), '370794');
|
||
assert.equal(extractVerificationCode('Your verification code is 654321.'), '654321');
|
||
assert.equal(extractVerificationCode('No code here'), null);
|
||
});
|
||
|
||
test('extractVerificationCodeFromMessage reads code from the latest message subject or preview', () => {
|
||
assert.equal(
|
||
extractVerificationCodeFromMessage({
|
||
subject: '你的 ChatGPT 代码为 192742',
|
||
bodyPreview: 'OpenAI 验证邮件',
|
||
from: { emailAddress: { address: 'noreply@openai.com' } },
|
||
}),
|
||
'192742'
|
||
);
|
||
|
||
assert.equal(
|
||
extractVerificationCodeFromMessage({
|
||
subject: 'OpenAI security message',
|
||
bodyPreview: 'Your verification code is 654321.',
|
||
from: { emailAddress: { address: 'noreply@openai.com' } },
|
||
}),
|
||
'654321'
|
||
);
|
||
});
|
||
|
||
test('getHotmailListToggleLabel reflects expanded state and account count', () => {
|
||
assert.equal(getHotmailListToggleLabel(false, 0), '展开列表');
|
||
assert.equal(getHotmailListToggleLabel(false, 7), '展开列表(7)');
|
||
assert.equal(getHotmailListToggleLabel(true, 7), '收起列表(7)');
|
||
});
|
||
|
||
test('filterHotmailAccountsByUsage can pick only used accounts or return all accounts', () => {
|
||
const accounts = [
|
||
{ id: 'used-1', email: 'used-1@hotmail.com', used: true },
|
||
{ id: 'fresh-1', email: 'fresh-1@hotmail.com', used: false },
|
||
{ id: 'used-2', email: 'used-2@hotmail.com', used: true },
|
||
];
|
||
|
||
assert.deepEqual(
|
||
filterHotmailAccountsByUsage(accounts, 'used').map((account) => account.id),
|
||
['used-1', 'used-2']
|
||
);
|
||
|
||
assert.deepEqual(
|
||
filterHotmailAccountsByUsage(accounts, 'all').map((account) => account.id),
|
||
['used-1', 'fresh-1', 'used-2']
|
||
);
|
||
});
|
||
|
||
test('getHotmailBulkActionLabel reflects action type and count', () => {
|
||
assert.equal(getHotmailBulkActionLabel('used', 0), '清空已用');
|
||
assert.equal(getHotmailBulkActionLabel('used', 3), '清空已用(3)');
|
||
assert.equal(getHotmailBulkActionLabel('all', 5), '全部删除(5)');
|
||
});
|
||
|
||
test('getLatestHotmailMessage picks the newest received mail', () => {
|
||
const latest = getLatestHotmailMessage([
|
||
{
|
||
id: 'older',
|
||
subject: 'older',
|
||
receivedDateTime: '2026-04-11T00:01:00.000Z',
|
||
},
|
||
{
|
||
id: 'newest',
|
||
subject: 'newest',
|
||
receivedDateTime: '2026-04-11T00:05:00.000Z',
|
||
},
|
||
{
|
||
id: 'middle',
|
||
subject: 'middle',
|
||
receivedDateTime: '2026-04-11T00:03:00.000Z',
|
||
},
|
||
]);
|
||
|
||
assert.equal(latest.id, 'newest');
|
||
});
|
||
|
||
test('pickVerificationMessage filters by time, sender, subject, and excluded codes', () => {
|
||
const messages = [
|
||
{
|
||
id: 'old-mail',
|
||
subject: 'Your code is 111111',
|
||
from: { emailAddress: { address: 'noreply@openai.com' } },
|
||
bodyPreview: '111111',
|
||
receivedDateTime: '2026-04-10T09:00:00.000Z',
|
||
},
|
||
{
|
||
id: 'wrong-sender',
|
||
subject: 'Your code is 222222',
|
||
from: { emailAddress: { address: 'noreply@example.com' } },
|
||
bodyPreview: '222222',
|
||
receivedDateTime: '2026-04-10T10:01:00.000Z',
|
||
},
|
||
{
|
||
id: 'good-mail',
|
||
subject: 'ChatGPT verification code 333333',
|
||
from: { emailAddress: { address: 'noreply@openai.com' } },
|
||
bodyPreview: 'Use 333333 to continue',
|
||
receivedDateTime: '2026-04-10T10:02:00.000Z',
|
||
},
|
||
{
|
||
id: 'excluded-mail',
|
||
subject: 'ChatGPT verification code 444444',
|
||
from: { emailAddress: { address: 'noreply@openai.com' } },
|
||
bodyPreview: 'Use 444444 to continue',
|
||
receivedDateTime: '2026-04-10T10:03:00.000Z',
|
||
},
|
||
];
|
||
|
||
const match = pickVerificationMessage(messages, {
|
||
afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 0),
|
||
senderFilters: ['openai', 'noreply'],
|
||
subjectFilters: ['verification', 'code', 'chatgpt'],
|
||
excludeCodes: ['444444'],
|
||
});
|
||
|
||
assert.equal(match.message.id, 'good-mail');
|
||
assert.equal(match.code, '333333');
|
||
});
|
||
|
||
test('pickVerificationMessageWithFallback no longer matches arbitrary recent mails when filters miss', () => {
|
||
const messages = [
|
||
{
|
||
id: 'login-mail',
|
||
subject: 'Use this security code to continue 555666',
|
||
from: { emailAddress: { address: 'account-security@openai.com' } },
|
||
bodyPreview: 'Your one-time security code is 555666',
|
||
receivedDateTime: '2026-04-10T10:05:00.000Z',
|
||
},
|
||
];
|
||
|
||
const result = pickVerificationMessageWithFallback(messages, {
|
||
afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 0),
|
||
senderFilters: ['noreply'],
|
||
subjectFilters: ['verification'],
|
||
excludeCodes: [],
|
||
});
|
||
|
||
assert.equal(result.match, null);
|
||
assert.equal(result.usedRelaxedFilters, false);
|
||
assert.equal(result.usedTimeFallback, false);
|
||
});
|
||
|
||
test('pickVerificationMessageWithTimeFallback can ignore afterTimestamp while keeping sender and subject filters', () => {
|
||
const messages = [
|
||
{
|
||
id: 'slightly-old-mail',
|
||
subject: '你的 ChatGPT 代码为 141735',
|
||
from: { emailAddress: { address: 'unknown' } },
|
||
bodyPreview: 'OpenAI logo ...',
|
||
receivedDateTime: '2026-04-10T10:00:02.000Z',
|
||
},
|
||
];
|
||
|
||
const result = pickVerificationMessageWithTimeFallback(messages, {
|
||
afterTimestamp: Date.UTC(2026, 3, 10, 10, 0, 10),
|
||
senderFilters: ['openai', 'noreply'],
|
||
subjectFilters: ['verify', 'verification', 'code'],
|
||
excludeCodes: [],
|
||
});
|
||
|
||
assert.equal(result.match.message.id, 'slightly-old-mail');
|
||
assert.equal(result.match.code, '141735');
|
||
assert.equal(result.usedRelaxedFilters, false);
|
||
assert.equal(result.usedTimeFallback, true);
|
||
});
|
||
|
||
test('buildHotmailMailApiLatestUrl includes email, client id, refresh token, and mailbox', () => {
|
||
const url = new URL(buildHotmailMailApiLatestUrl({
|
||
clientId: 'client-123',
|
||
email: 'user@hotmail.com',
|
||
refreshToken: 'refresh-token-xyz',
|
||
mailbox: 'Junk',
|
||
}));
|
||
|
||
assert.equal(url.origin + url.pathname, 'https://apple.882263.xyz/api/mail-new');
|
||
assert.equal(url.searchParams.get('client_id'), 'client-123');
|
||
assert.equal(url.searchParams.get('email'), 'user@hotmail.com');
|
||
assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-xyz');
|
||
assert.equal(url.searchParams.get('mailbox'), 'Junk');
|
||
assert.equal(url.searchParams.get('response_type'), 'json');
|
||
});
|
||
|
||
test('buildHotmailMailApiLatestUrl supports custom api url and can omit response_type', () => {
|
||
const url = new URL(buildHotmailMailApiLatestUrl({
|
||
apiUrl: 'https://example.com/custom-mail-api',
|
||
clientId: 'client-789',
|
||
email: 'custom@hotmail.com',
|
||
refreshToken: 'refresh-token-custom',
|
||
mailbox: 'Spam',
|
||
responseType: '',
|
||
}));
|
||
|
||
assert.equal(url.origin + url.pathname, 'https://example.com/custom-mail-api');
|
||
assert.equal(url.searchParams.get('client_id'), 'client-789');
|
||
assert.equal(url.searchParams.get('email'), 'custom@hotmail.com');
|
||
assert.equal(url.searchParams.get('refresh_token'), 'refresh-token-custom');
|
||
assert.equal(url.searchParams.get('mailbox'), 'Spam');
|
||
assert.equal(url.searchParams.has('response_type'), false);
|
||
});
|
||
|
||
test('normalizeHotmailMailApiMessages maps third-party payload fields into verification message shape', () => {
|
||
const messages = normalizeHotmailMailApiMessages([
|
||
{
|
||
id: 'mail-1',
|
||
from: 'noreply@openai.com',
|
||
subject: 'ChatGPT verification code',
|
||
text: 'Use 135790 to continue',
|
||
date: '2026-04-10T10:02:00.000Z',
|
||
},
|
||
{
|
||
message_id: 'mail-2',
|
||
sender_email: 'alerts@example.com',
|
||
title: 'Ignored',
|
||
body: 'No code here',
|
||
received_at: '2026-04-10T10:03:00.000Z',
|
||
},
|
||
]);
|
||
|
||
assert.deepEqual(messages, [
|
||
{
|
||
id: 'mail-1',
|
||
subject: 'ChatGPT verification code',
|
||
from: { emailAddress: { address: 'noreply@openai.com' } },
|
||
bodyPreview: 'Use 135790 to continue',
|
||
receivedDateTime: '2026-04-10T10:02:00.000Z',
|
||
},
|
||
{
|
||
id: 'mail-2',
|
||
subject: 'Ignored',
|
||
from: { emailAddress: { address: 'alerts@example.com' } },
|
||
bodyPreview: 'No code here',
|
||
receivedDateTime: '2026-04-10T10:03:00.000Z',
|
||
},
|
||
]);
|
||
});
|
||
|
||
test('getHotmailVerificationPollConfig gives Hotmail a slower initial wait and longer polling window', () => {
|
||
assert.deepEqual(getHotmailVerificationPollConfig(4), {
|
||
initialDelayMs: 5000,
|
||
maxAttempts: 12,
|
||
intervalMs: 5000,
|
||
requestFreshCodeFirst: false,
|
||
ignorePersistedLastCode: true,
|
||
});
|
||
|
||
assert.deepEqual(getHotmailVerificationPollConfig(7), {
|
||
initialDelayMs: 5000,
|
||
maxAttempts: 12,
|
||
intervalMs: 5000,
|
||
requestFreshCodeFirst: false,
|
||
ignorePersistedLastCode: true,
|
||
});
|
||
});
|
||
|
||
test('getHotmailVerificationRequestTimestamp prefers actual request timestamps with a safety buffer', () => {
|
||
const signupRequestedAt = Date.UTC(2026, 3, 10, 12, 0, 30);
|
||
const loginRequestedAt = Date.UTC(2026, 3, 10, 12, 5, 45);
|
||
|
||
assert.equal(
|
||
getHotmailVerificationRequestTimestamp(4, {
|
||
signupVerificationRequestedAt: signupRequestedAt,
|
||
flowStartTime: signupRequestedAt - 60_000,
|
||
}),
|
||
signupRequestedAt - 15_000
|
||
);
|
||
|
||
assert.equal(
|
||
getHotmailVerificationRequestTimestamp(7, {
|
||
loginVerificationRequestedAt: loginRequestedAt,
|
||
lastEmailTimestamp: loginRequestedAt - 120_000,
|
||
flowStartTime: loginRequestedAt - 300_000,
|
||
}),
|
||
loginRequestedAt - 15_000
|
||
);
|
||
});
|
||
|
||
test('getHotmailMailApiRequestConfig defines third-party mail API request defaults', () => {
|
||
assert.deepEqual(getHotmailMailApiRequestConfig(), {
|
||
timeoutMs: 15000,
|
||
});
|
||
});
|
||
|
||
test('parseHotmailImportText parses account lines in email----password----clientId----token format', () => {
|
||
const parsed = parseHotmailImportText(`
|
||
账号----密码----ID----Token
|
||
JohnRodriguez5425@hotmail.com----nb4ta1OK----9e5f94bc-e8a4-4e73-b8be-63364c29d753----refresh-token-1
|
||
alice@hotmail.com----pass-2----client-2----refresh-token-2
|
||
`.trim());
|
||
|
||
assert.deepEqual(parsed, [
|
||
{
|
||
email: 'JohnRodriguez5425@hotmail.com',
|
||
password: 'nb4ta1OK',
|
||
clientId: '9e5f94bc-e8a4-4e73-b8be-63364c29d753',
|
||
refreshToken: 'refresh-token-1',
|
||
},
|
||
{
|
||
email: 'alice@hotmail.com',
|
||
password: 'pass-2',
|
||
clientId: 'client-2',
|
||
refreshToken: 'refresh-token-2',
|
||
},
|
||
]);
|
||
});
|