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:
parent
997e9411b9
commit
51613ffe12
|
|
@ -58,7 +58,7 @@ function formatEventChoice(meeting) {
|
||||||
return `📅 ${meeting.date} ${meeting.start}-${meeting.end} ${meeting.title}`;
|
return `📅 ${meeting.date} ${meeting.start}-${meeting.end} ${meeting.title}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCalendarEvents(daysAhead = 7) {
|
async function buildAuthenticatedClient() {
|
||||||
const env = loadEnv();
|
const env = loadEnv();
|
||||||
|
|
||||||
const cca = new ConfidentialClientApplication({
|
const cca = new ConfidentialClientApplication({
|
||||||
|
|
@ -73,9 +73,14 @@ async function getCalendarEvents(daysAhead = 7) {
|
||||||
scopes: ['https://graph.microsoft.com/.default']
|
scopes: ['https://graph.microsoft.com/.default']
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = Client.init({
|
return Client.init({
|
||||||
authProvider: (done) => done(null, tokenResponse.accessToken)
|
authProvider: (done) => done(null, tokenResponse.accessToken)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCalendarEvents(daysAhead = 7) {
|
||||||
|
const env = loadEnv();
|
||||||
|
const client = await buildAuthenticatedClient();
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const future = new Date(now);
|
const future = new Date(now);
|
||||||
|
|
@ -102,26 +107,22 @@ function extractJoinUrlFromBody(body) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getEventById(eventId) {
|
async function getEventById(eventId) {
|
||||||
const env = loadEnv();
|
if (typeof eventId !== 'string' || !eventId.trim()) {
|
||||||
const cca = new ConfidentialClientApplication({
|
throw new TypeError('eventId must be a non-empty string');
|
||||||
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({
|
try {
|
||||||
scopes: ['https://graph.microsoft.com/.default']
|
const env = loadEnv();
|
||||||
});
|
const client = await buildAuthenticatedClient();
|
||||||
const client = Client.init({
|
|
||||||
authProvider: (done) => done(null, tokenResponse.accessToken)
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = await client
|
const event = await client
|
||||||
.api(`/users/${env.AZURE_USER_EMAIL}/events/${eventId}`)
|
.api(`/users/${env.AZURE_USER_EMAIL}/events/${eventId}`)
|
||||||
.select('id,subject,start,end,body,attendees,seriesMasterId,onlineMeeting')
|
.select('id,subject,start,end,body,attendees,seriesMasterId,onlineMeeting')
|
||||||
.get();
|
.get();
|
||||||
return event;
|
return event;
|
||||||
|
} catch (cause) {
|
||||||
|
throw new Error(`Failed to fetch event ${eventId}: ${cause.message}`, { cause });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ describe('isRecurring', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const { extractJoinUrlFromBody } = require('../lib/o365-calendar.js');
|
const { extractJoinUrlFromBody, getEventById } = require('../lib/o365-calendar.js');
|
||||||
|
|
||||||
describe('extractJoinUrlFromBody', () => {
|
describe('extractJoinUrlFromBody', () => {
|
||||||
it('extracts teams meeting join URL from body html', () => {
|
it('extracts teams meeting join URL from body html', () => {
|
||||||
|
|
@ -96,3 +96,26 @@ describe('extractJoinUrlFromBody', () => {
|
||||||
assert.equal(extractJoinUrlFromBody('<p>nothing</p>'), null);
|
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/ }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue