Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 79 additions & 13 deletions packages/app-store/office365video/lib/VideoApiAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import { internalServerErrorResponse, successResponse } from "../../_utils/testU
import config from "../config.json";
import VideoApiAdapter from "./VideoApiAdapter";

const FAKE_MEETING_ID = "FAKE_MEETING_ID";
const FAKE_BOOKING_REF = { type: "office365_video", uid: "FAKE_UID", meetingId: FAKE_MEETING_ID };

const URLS = {
CREATE_MEETING: {
url: "https://graph.microsoft.com/v1.0/me/onlineMeetings",
method: "POST",
},
UPDATE_MEETING: {
url: "https://graph.microsoft.com/v1.0/me/onlineMeetings",
method: "POST",
url: `https://graph.microsoft.com/v1.0/me/onlineMeetings/${FAKE_MEETING_ID}`,
method: "PATCH",
},
DELETE_MEETING: {
url: `https://graph.microsoft.com/v1.0/me/onlineMeetings/${FAKE_MEETING_ID}`,
method: "DELETE",
},
};

Expand Down Expand Up @@ -199,11 +206,11 @@ describe("updateMeeting", () => {
const videoApi = VideoApiAdapter(testCredential);

mockRequestRaw.mockImplementation(({ url }) => {
if (url === URLS.CREATE_MEETING.url) {
if (url === URLS.UPDATE_MEETING.url) {
return Promise.resolve(
successResponse({
json: {
id: 1,
id: FAKE_MEETING_ID,
joinWebUrl: "https://join_web_url.example.com",
joinUrl: "https://join_url.example.com",
},
Expand All @@ -220,12 +227,12 @@ describe("updateMeeting", () => {
endTime: new Date(),
};

const updatedMeeting = await videoApi?.updateMeeting(null, event);
const updatedMeeting = await videoApi?.updateMeeting(FAKE_BOOKING_REF, event);
expect(OAuthManager).toHaveBeenCalled();
expect(mockRequestRaw).toHaveBeenCalledWith({
url: URLS.CREATE_MEETING.url,
url: URLS.UPDATE_MEETING.url,
options: {
method: "POST",
method: "PATCH",
body: JSON.stringify({
startDateTime: event.startTime,
endDateTime: event.endTime,
Expand All @@ -234,7 +241,7 @@ describe("updateMeeting", () => {
},
});
expect(updatedMeeting).toEqual({
id: 1,
id: FAKE_MEETING_ID,
password: "",
type: config.type,
url: "https://join_web_url.example.com",
Expand All @@ -245,11 +252,11 @@ describe("updateMeeting", () => {
const videoApi = VideoApiAdapter(testCredential);

mockRequestRaw.mockImplementation(({ url }) => {
if (url === URLS.CREATE_MEETING.url) {
if (url === URLS.UPDATE_MEETING.url) {
return Promise.resolve(
internalServerErrorResponse({
json: {
id: 1,
id: FAKE_MEETING_ID,
joinWebUrl: "https://join_web_url.example.com",
joinUrl: "https://join_url.example.com",
},
Expand All @@ -266,12 +273,14 @@ describe("updateMeeting", () => {
endTime: new Date(),
};

await expect(() => videoApi?.updateMeeting(null, event)).rejects.toThrowError("Internal Server Error");
await expect(() => videoApi?.updateMeeting(FAKE_BOOKING_REF, event)).rejects.toThrowError(
"Internal Server Error"
);
expect(OAuthManager).toHaveBeenCalled();
expect(mockRequestRaw).toHaveBeenCalledWith({
url: URLS.CREATE_MEETING.url,
url: URLS.UPDATE_MEETING.url,
options: {
method: "POST",
method: "PATCH",
body: JSON.stringify({
startDateTime: event.startTime,
endDateTime: event.endTime,
Expand All @@ -280,4 +289,61 @@ describe("updateMeeting", () => {
},
});
});

test("`updateMeeting` throws when meetingId is missing", async () => {
const videoApi = VideoApiAdapter(testCredential);
const event = {
title: "Test Meeting",
description: "Test Description",
startTime: new Date(),
endTime: new Date(),
};
await expect(() =>
videoApi?.updateMeeting({ type: "office365_video", uid: "uid", meetingId: null }, event)
).rejects.toThrowError("Meeting ID is required");
});
});

describe("deleteMeeting", () => {
test("Successful `deleteMeeting` call", async () => {
const videoApi = VideoApiAdapter(testCredential);

mockRequestRaw.mockImplementation(({ url }) => {
if (url === URLS.DELETE_MEETING.url) {
return Promise.resolve(successResponse({ json: {} }));
}
throw new Error("Unexpected URL");
});

await expect(videoApi?.deleteMeeting(FAKE_MEETING_ID)).resolves.toBeDefined();
expect(mockRequestRaw).toHaveBeenCalledWith({
url: URLS.DELETE_MEETING.url,
options: { method: "DELETE" },
});
});

test("`deleteMeeting` is idempotent on 404", async () => {
const videoApi = VideoApiAdapter(testCredential);

mockRequestRaw.mockImplementation(() =>
Promise.resolve({ ok: false, status: 404, statusText: "Not Found", text: async () => "" })
);

await expect(videoApi?.deleteMeeting(FAKE_MEETING_ID)).resolves.toBeDefined();
});

test("Failing `deleteMeeting` call", async () => {
const videoApi = VideoApiAdapter(testCredential);

mockRequestRaw.mockImplementation(({ url }) => {
if (url === URLS.DELETE_MEETING.url) {
return Promise.resolve(internalServerErrorResponse({ json: {} }));
}
throw new Error("Unexpected URL");
});

await expect(() => videoApi?.deleteMeeting(FAKE_MEETING_ID)).rejects.toThrowError(
"Internal Server Error"
);
});
});
41 changes: 34 additions & 7 deletions packages/app-store/office365video/lib/VideoApiAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,18 @@ const TeamsVideoApiAdapter = (credential: CredentialForCalendarServiceWithTenant
return Promise.resolve([]);
},
updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent) => {
const meetingId = bookingRef?.meetingId;
if (!meetingId) {
throw new HttpError({
statusCode: 400,
message: "Meeting ID is required to update a Teams meeting",
});
Comment on lines +233 to +236
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how ErrorWithCode is imported/constructed across the repo,
# then migrate this file's newly added throws consistently.

rg -n --type=ts -C2 '\bErrorWithCode\b'
rg -n --type=ts -C2 'new HttpError\(' packages/app-store/office365video/lib/VideoApiAdapter.ts

Repository: calcom/cal.diy

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/app-store/office365video/lib/VideoApiAdapter.ts"

echo "== Imports in $FILE (first 120 lines) =="
sed -n '1,120p' "$FILE"

echo
echo "== Error usage in $FILE (ErrorWithCode / HttpError) =="
rg -n --type=ts '\bErrorWithCode\b' "$FILE" || true
rg -n --type=ts '\bHttpError\b' "$FILE" || true
rg -n --type=ts 'new HttpError\(' "$FILE" || true

echo
echo "== Relevant sections (around 220-310) =="
sed -n '210,310p' "$FILE"

Repository: calcom/cal.diy

Length of output: 8861


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/app-store/office365video/lib/VideoApiAdapter.ts"

echo "== ErrorWithCode usage in $FILE =="
rg -n --type=ts '\bErrorWithCode\b' "$FILE" || true

echo
echo "== ErrorCode values used in typical ErrorWithCode throws (spot check) =="
ls -1 packages/lib | head -n 50 || true
# Try common errorCodes locations
rg -n --type=ts "enum ErrorCode|export (const )?ErrorCode|ErrorCode\.(BadRequest|InternalServerError|NotFound)" packages/lib/errorCodes* packages/lib/*errorCodes* 2>/dev/null || true
rg -n --type=ts "BadRequest|InternalServerError|NotFound" packages/lib/errorCodes* 2>/dev/null || true

echo
echo "== HttpError definition/contract =="
rg -n --type=ts "export (class|function) HttpError|class HttpError" packages -S -g'*.ts' -g'*.tsx' | head -n 50
if [ -f "packages/lib/http-error.ts" ]; then
  sed -n '1,200p' packages/lib/http-error.ts
elif [ -f "packages/lib/http-error/index.ts" ]; then
  sed -n '1,200p' packages/lib/http-error/index.ts
fi

echo
echo "== How updateMeeting/deleteMeeting errors are consumed (search for office365_video usage) =="
rg -n --type=ts "office365_video" packages | head -n 50

echo
echo "== Call sites for updateMeeting/deleteMeeting =="
rg -n --type=ts "updateMeeting\\s*:\\s*async|updateMeeting\\s*\\(" packages | head -n 50
rg -n --type=ts "deleteMeeting\\s*:\\s*async|deleteMeeting\\s*\\(" packages | head -n 50

echo
echo "== Search for handling of HttpError vs ErrorWithCode near video adapters =="
rg -n --type=ts "instanceof HttpError|instanceof ErrorWithCode" packages/features packages/app-store packages/lib | head -n 80

Repository: calcom/cal.diy

Length of output: 11879


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how conferencing layer handles errors thrown by video adapters
sed -n '1,220p' packages/features/conferencing/lib/videoClient.ts

echo
echo "== Other VideoApiAdapter implementations using HttpError? =="
rg -n --type=ts "import \\{ HttpError \\} from \"`@calcom/lib/http-error`\"" packages/app-store/*/lib/VideoApiAdapter.ts || true
rg -n --type=ts "\bnew HttpError\(" packages/app-store/*/lib/VideoApiAdapter.ts || true

echo
echo "== Error mapping helpers (client/server) =="
sed -n '1,220p' packages/lib/getClientErrorFromUnknown.ts
sed -n '1,220p' packages/lib/server/getServerErrorFromUnknown.ts

Repository: calcom/cal.diy

Length of output: 16378


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' packages/lib/errors.ts

Repository: calcom/cal.diy

Length of output: 3191


Use ErrorWithCode (not HttpError) in the new non‑tRPC error branches of VideoApiAdapter.ts.

packages/app-store/office365video/lib/VideoApiAdapter.ts currently throws new HttpError({ statusCode: 400, ... }) (and other HttpError instances) for the new meeting update/delete error paths (lines 233-236, 248-251, 280-283, 291-294). Switch these to ErrorWithCode using import { ErrorCode } from "@calcom/lib/errorCodes"; import { ErrorWithCode } from "@calcom/lib/errors"; and throw new ErrorWithCode(ErrorCode.BadRequest, "…") (or ErrorWithCode.Factory.*).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app-store/office365video/lib/VideoApiAdapter.ts` around lines 233 -
236, Replace uses of HttpError in the new non‑tRPC error branches with
ErrorWithCode: add imports "ErrorCode" and "ErrorWithCode" from
"`@calcom/lib/errorCodes`" and "`@calcom/lib/errors`" respectively, then change
throws like the ones in VideoApiAdapter (e.g. the meeting update/delete branches
that currently throw new HttpError(...)) to throw new
ErrorWithCode(ErrorCode.BadRequest, "<message>") or use
ErrorWithCode.Factory.BadRequest("<message>") so all new error paths use
ErrorWithCode instead of HttpError.

}
try {
const response = await auth.requestRaw({
url: `${await getUserEndpoint()}/onlineMeetings`,
url: `${await getUserEndpoint()}/onlineMeetings/${meetingId}`,
options: {
method: "POST",
method: "PATCH",
body: JSON.stringify(translateEvent(event)),
},
});
Expand All @@ -244,12 +251,11 @@ const TeamsVideoApiAdapter = (credential: CredentialForCalendarServiceWithTenant
});
}

const resultString = await response.text();
const resultObject = JSON.parse(resultString);
const resultObject = JSON.parse(await response.text());

return Promise.resolve({
type: "office365_video",
id: resultObject.id,
id: resultObject.id ?? meetingId,
password: "",
url: resultObject.joinWebUrl || resultObject.joinUrl,
});
Expand All @@ -264,8 +270,29 @@ const TeamsVideoApiAdapter = (credential: CredentialForCalendarServiceWithTenant
});
}
},
deleteMeeting:() => {
return Promise.resolve([]);
deleteMeeting: async (uid: string) => {
try {
const response = await auth.requestRaw({
url: `${await getUserEndpoint()}/onlineMeetings/${uid}`,
options: { method: "DELETE" },
});
if (!response.ok && response.status !== 404) {
throw new HttpError({
statusCode: response.status,
message: response.statusText,
});
}
return Promise.resolve([]);
} catch (error) {
log.error(`Error deleting MS Teams meeting ${uid}`, error);
if (error instanceof HttpError) {
throw error;
}
throw new HttpError({
statusCode: 500,
message: `Error deleting MS Teams meeting ${uid}`,
});
}
},
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> => {
const url = `${await getUserEndpoint()}/onlineMeetings`;
Expand Down
Loading