refactor(o365): extract authenticated graph client helper, add getEventById validation

Extract buildAuthenticatedClient() helper to eliminate duplicate MSAL token
acquisition logic in getCalendarEvents and getEventById. Add eventId validation
and error wrapping with cause chain in getEventById.

New tests verify eventId type and empty-string checks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
beo3000 2026-05-06 21:03:30 +02:00
parent 997e9411b9
commit 51613ffe12
2 changed files with 46 additions and 22 deletions

View File

@ -58,7 +58,7 @@ function formatEventChoice(meeting) {
return `📅 ${meeting.date} ${meeting.start}-${meeting.end} ${meeting.title}`;
}
async function getCalendarEvents(daysAhead = 7) {
async function buildAuthenticatedClient() {
const env = loadEnv();
const cca = new ConfidentialClientApplication({
@ -73,9 +73,14 @@ async function getCalendarEvents(daysAhead = 7) {
scopes: ['https://graph.microsoft.com/.default']
});
const client = Client.init({
return Client.init({
authProvider: (done) => done(null, tokenResponse.accessToken)
});
}
async function getCalendarEvents(daysAhead = 7) {
const env = loadEnv();
const client = await buildAuthenticatedClient();
const now = new Date();
const future = new Date(now);
@ -102,26 +107,22 @@ function extractJoinUrlFromBody(body) {
}
async function getEventById(eventId) {
const env = loadEnv();
const cca = new ConfidentialClientApplication({
auth: {
clientId: env.AZURE_CLIENT_ID,
clientSecret: env.AZURE_CLIENT_SECRET,
authority: `https://login.microsoftonline.com/${env.AZURE_TENANT_ID}`
}
});
const tokenResponse = await cca.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default']
});
const client = Client.init({
authProvider: (done) => done(null, tokenResponse.accessToken)
});
if (typeof eventId !== 'string' || !eventId.trim()) {
throw new TypeError('eventId must be a non-empty string');
}
const event = await client
.api(`/users/${env.AZURE_USER_EMAIL}/events/${eventId}`)
.select('id,subject,start,end,body,attendees,seriesMasterId,onlineMeeting')
.get();
return event;
try {
const env = loadEnv();
const client = await buildAuthenticatedClient();
const event = await client
.api(`/users/${env.AZURE_USER_EMAIL}/events/${eventId}`)
.select('id,subject,start,end,body,attendees,seriesMasterId,onlineMeeting')
.get();
return event;
} catch (cause) {
throw new Error(`Failed to fetch event ${eventId}: ${cause.message}`, { cause });
}
}
module.exports = {

View File

@ -81,7 +81,7 @@ describe('isRecurring', () => {
});
});
const { extractJoinUrlFromBody } = require('../lib/o365-calendar.js');
const { extractJoinUrlFromBody, getEventById } = require('../lib/o365-calendar.js');
describe('extractJoinUrlFromBody', () => {
it('extracts teams meeting join URL from body html', () => {
@ -96,3 +96,26 @@ describe('extractJoinUrlFromBody', () => {
assert.equal(extractJoinUrlFromBody('<p>nothing</p>'), null);
});
});
describe('getEventById', () => {
it('throws TypeError when eventId is empty string', async () => {
await assert.rejects(
() => getEventById(''),
{ name: 'TypeError', message: /eventId must be a non-empty string/ }
);
});
it('throws TypeError when eventId is not a string', async () => {
await assert.rejects(
() => getEventById(null),
{ name: 'TypeError', message: /eventId must be a non-empty string/ }
);
});
it('throws TypeError when eventId is whitespace only', async () => {
await assert.rejects(
() => getEventById(' '),
{ name: 'TypeError', message: /eventId must be a non-empty string/ }
);
});
});