Add optional mailbox (UPN/SMTP) routing on email/folder/thread tools via
users/{upn}/... and X-AnchorMailbox. New list-mailboxes probes primary,
OUTLOOK_SHARED_MAILBOXES seeds, cache, and candidates. Send supports
mailbox-rooted sendMail and onBehalfOf. MSAL requests Mail.*.Shared;
check-auth-status reports token scp gaps. Docs, env example, tests.
129 lines
4.7 KiB
JavaScript
129 lines
4.7 KiB
JavaScript
/**
|
|
* Unit tests for utils/mailbox.js
|
|
* Run: node tests/mailbox-path.test.js
|
|
*/
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const {
|
|
normalizeMailbox,
|
|
buildPath,
|
|
withMailboxHeaders,
|
|
parseSharedMailboxEnv,
|
|
loadMailboxCache,
|
|
saveMailboxCache,
|
|
encodeGraphPath,
|
|
decodeJwtPayload,
|
|
analyzeTokenScopes
|
|
} = require('../utils/mailbox');
|
|
|
|
// --- normalizeMailbox ---
|
|
assert.deepStrictEqual(normalizeMailbox(undefined).kind, 'me');
|
|
assert.deepStrictEqual(normalizeMailbox(null).graphRoot, 'me');
|
|
assert.deepStrictEqual(normalizeMailbox('').graphRoot, 'me');
|
|
assert.deepStrictEqual(normalizeMailbox('me').graphRoot, 'me');
|
|
assert.deepStrictEqual(normalizeMailbox('PRIMARY').graphRoot, 'me');
|
|
|
|
const shared = normalizeMailbox('HelpDesk@Contoso.com');
|
|
assert.strictEqual(shared.kind, 'user');
|
|
assert.strictEqual(shared.smtpOrUpn, 'HelpDesk@Contoso.com');
|
|
assert.strictEqual(shared.key, 'helpdesk@contoso.com');
|
|
assert.strictEqual(shared.graphRoot, 'users/HelpDesk@Contoso.com');
|
|
|
|
const mailto = normalizeMailbox('mailto:it@springfieldy.org');
|
|
assert.strictEqual(mailto.smtpOrUpn, 'it@springfieldy.org');
|
|
assert.strictEqual(mailto.graphRoot, 'users/it@springfieldy.org');
|
|
|
|
const angled = normalizeMailbox('<desk@example.com>');
|
|
assert.strictEqual(angled.smtpOrUpn, 'desk@example.com');
|
|
console.log('✅ normalizeMailbox');
|
|
|
|
// --- buildPath ---
|
|
assert.strictEqual(buildPath(null, 'messages'), 'me/messages');
|
|
assert.strictEqual(buildPath('me', 'mailFolders/inbox/messages'), 'me/mailFolders/inbox/messages');
|
|
assert.strictEqual(
|
|
buildPath('helpdesk@contoso.com', 'messages'),
|
|
'users/helpdesk@contoso.com/messages'
|
|
);
|
|
assert.strictEqual(
|
|
buildPath('helpdesk@contoso.com', '/mailFolders/sentItems/messages'),
|
|
'users/helpdesk@contoso.com/mailFolders/sentItems/messages'
|
|
);
|
|
assert.strictEqual(buildPath(shared, 'sendMail'), 'users/HelpDesk@Contoso.com/sendMail');
|
|
console.log('✅ buildPath');
|
|
|
|
// --- encode once (same as callGraphAPI) ---
|
|
const encoded = encodeGraphPath('users/helpdesk@contoso.com/messages');
|
|
assert.strictEqual(encoded, 'users/helpdesk%40contoso.com/messages');
|
|
assert.ok(!encoded.includes('%2540'), 'must not double-encode @');
|
|
console.log('✅ encodeGraphPath single-encode');
|
|
|
|
// --- headers ---
|
|
assert.deepStrictEqual(withMailboxHeaders(null), {});
|
|
assert.deepStrictEqual(withMailboxHeaders('me'), {});
|
|
assert.deepStrictEqual(withMailboxHeaders('it@x.org'), { 'X-AnchorMailbox': 'it@x.org' });
|
|
console.log('✅ withMailboxHeaders');
|
|
|
|
// --- env parse ---
|
|
assert.deepStrictEqual(parseSharedMailboxEnv(''), []);
|
|
assert.deepStrictEqual(parseSharedMailboxEnv(undefined), []);
|
|
assert.deepStrictEqual(
|
|
parseSharedMailboxEnv('a@x.com, b@y.com;c@z.com d@w.com'),
|
|
['a@x.com', 'b@y.com', 'c@z.com', 'd@w.com']
|
|
);
|
|
console.log('✅ parseSharedMailboxEnv');
|
|
|
|
// --- cache round-trip ---
|
|
const tmp = path.join(os.tmpdir(), `outlook-mcp-mb-cache-${Date.now()}.json`);
|
|
try {
|
|
const empty = loadMailboxCache(tmp);
|
|
assert.strictEqual(empty.mailboxes.length, 0);
|
|
|
|
saveMailboxCache([
|
|
{
|
|
address: 'helpdesk@contoso.com',
|
|
displayName: 'Help Desk',
|
|
capabilities: { read: true, listFolders: true, sendAs: 'unverified' },
|
|
source: ['env', 'probe']
|
|
}
|
|
], tmp);
|
|
|
|
const loaded = loadMailboxCache(tmp);
|
|
assert.strictEqual(loaded.mailboxes.length, 1);
|
|
assert.strictEqual(loaded.mailboxes[0].address, 'helpdesk@contoso.com');
|
|
assert.ok(loaded.mailboxes[0].source.includes('env'));
|
|
assert.ok(loaded.mailboxes[0].lastProbedAt);
|
|
|
|
// upsert merges sources
|
|
saveMailboxCache([
|
|
{
|
|
address: 'helpdesk@contoso.com',
|
|
capabilities: { read: true, listFolders: true, sendAs: 'unverified' },
|
|
source: ['candidates']
|
|
}
|
|
], tmp);
|
|
const merged = loadMailboxCache(tmp);
|
|
assert.ok(merged.mailboxes[0].source.includes('candidates'));
|
|
assert.ok(merged.mailboxes[0].source.includes('env'));
|
|
console.log('✅ mailbox cache round-trip');
|
|
} finally {
|
|
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
}
|
|
|
|
// --- JWT decode / scope analysis ---
|
|
function makeJwt(payload) {
|
|
const h = Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url');
|
|
const p = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
return `${h}.${p}.sig`;
|
|
}
|
|
const tok = makeJwt({ scp: 'Mail.Read Mail.Read.Shared User.Read offline_access' });
|
|
const analyzed = analyzeTokenScopes(tok);
|
|
assert.ok(analyzed.present.includes('Mail.Read.Shared'));
|
|
assert.ok(analyzed.missing.includes('Mail.Send.Shared'));
|
|
assert.ok(analyzed.missing.includes('Mail.ReadWrite.Shared'));
|
|
assert.strictEqual(decodeJwtPayload('not-a-jwt'), null);
|
|
console.log('✅ analyzeTokenScopes / decodeJwtPayload');
|
|
|
|
console.log('\nAll mailbox-path tests passed.');
|