outlook-mcp/tests/mailbox-path.test.js
Seton Carmichael e70840552d feat(outlook-mcp): shared mailbox targeting, discovery, and scopes (v1.1.0)
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.
2026-08-24 08:41:05 -04:00

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