diff --git a/client/src/Hooks/useNotificationForm.ts b/client/src/Hooks/useNotificationForm.ts index 4a1e05f2c..d08f17548 100644 --- a/client/src/Hooks/useNotificationForm.ts +++ b/client/src/Hooks/useNotificationForm.ts @@ -64,10 +64,10 @@ function buildDefaults(data: Notification | null): NotificationFormData { return { type: "twilio", notificationName: data.notificationName || "", - address: data.address || "", + accountSid: data.accountSid || "", accessToken: data.accessToken || "", phone: data.phone || "", - homeserverUrl: data.homeserverUrl || "", + twilioPhoneNumber: data.twilioPhoneNumber || "", }; } if (data?.type === "pushover") { diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx index 3d7f85acd..0f6d9b652 100644 --- a/client/src/Pages/Notifications/create/index.tsx +++ b/client/src/Pages/Notifications/create/index.tsx @@ -270,9 +270,9 @@ const NotificationsCreatePage = () => { rightContent={ ( { )} /> ( ( homeserverUrl: { type: String }, roomId: { type: String }, accessToken: { type: String }, + accountSid: { type: String }, + twilioPhoneNumber: { type: String }, }, { timestamps: true, diff --git a/server/src/service/infrastructure/notificationProviders/twilio.ts b/server/src/service/infrastructure/notificationProviders/twilio.ts index 6d84f80f3..a871fe5cb 100644 --- a/server/src/service/infrastructure/notificationProviders/twilio.ts +++ b/server/src/service/infrastructure/notificationProviders/twilio.ts @@ -7,18 +7,18 @@ import got from "got"; export class TwilioProvider extends NotificationProvider { async sendTestAlert(notification: Partial): Promise { - if (!notification.address || !notification.accessToken || !notification.phone || !notification.homeserverUrl) { + if (!notification.accountSid || !notification.accessToken || !notification.phone || !notification.twilioPhoneNumber) { return false; } try { - await got.post(`https://api.twilio.com/2010-04-01/Accounts/${notification.address}/Messages.json`, { + await got.post(`https://api.twilio.com/2010-04-01/Accounts/${notification.accountSid}/Messages.json`, { form: { To: notification.phone, - From: notification.homeserverUrl, + From: notification.twilioPhoneNumber, Body: getTestMessage(), }, - username: notification.address, + username: notification.accountSid, password: notification.accessToken, ...this.gotRequestOptions(), }); @@ -38,20 +38,20 @@ export class TwilioProvider extends NotificationProvider { } async sendMessage(notification: Notification, message: NotificationMessage): Promise { - if (!notification.address || !notification.accessToken || !notification.phone || !notification.homeserverUrl) { + if (!notification.accountSid || !notification.accessToken || !notification.phone || !notification.twilioPhoneNumber) { return false; } const text = this.buildSmsText(message); try { - await got.post(`https://api.twilio.com/2010-04-01/Accounts/${notification.address}/Messages.json`, { + await got.post(`https://api.twilio.com/2010-04-01/Accounts/${notification.accountSid}/Messages.json`, { form: { To: notification.phone, - From: notification.homeserverUrl, + From: notification.twilioPhoneNumber, Body: text, }, - username: notification.address, + username: notification.accountSid, password: notification.accessToken, ...this.gotRequestOptions(), }); diff --git a/server/src/types/notification.ts b/server/src/types/notification.ts index f306e2592..9d5b914c0 100644 --- a/server/src/types/notification.ts +++ b/server/src/types/notification.ts @@ -23,6 +23,8 @@ export interface Notification { homeserverUrl?: string; roomId?: string; accessToken?: string; + accountSid?: string; + twilioPhoneNumber?: string; createdAt: string; updatedAt: string; } diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index 545335dad..cf7478610 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -83,10 +83,10 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("twilio"), - address: z.string().min(1, "Account SID is required"), + accountSid: z.string().min(1, "Account SID is required"), accessToken: z.string().min(1, "Auth token is required"), phone: z.string().min(1, "Recipient phone number is required"), - homeserverUrl: z.string().min(1, "Twilio phone number is required"), + twilioPhoneNumber: z.string().min(1, "Twilio phone number is required"), }), ]); diff --git a/server/test/unit/providers/notifications/twilioProvider.test.ts b/server/test/unit/providers/notifications/twilioProvider.test.ts index 6156b648f..88bfcdf7e 100644 --- a/server/test/unit/providers/notifications/twilioProvider.test.ts +++ b/server/test/unit/providers/notifications/twilioProvider.test.ts @@ -16,7 +16,8 @@ const createProvider = () => { const makeTwilioNotification = (overrides?: Record) => makeNotification({ phone: "+15559876543", - homeserverUrl: "+15551234567", + twilioPhoneNumber: "+15551234567", + accountSid: "ACtest123", ...overrides, }); @@ -44,8 +45,8 @@ describe("TwilioProvider", () => { ); }); - it("returns false when address is missing", async () => { - expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ address: "" }))).toBe(false); + it("returns false when accountSid is missing", async () => { + expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ accountSid: "" }))).toBe(false); }); it("returns false when accessToken is missing", async () => { @@ -56,8 +57,8 @@ describe("TwilioProvider", () => { expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ phone: "" }))).toBe(false); }); - it("returns false when homeserverUrl (from number) is missing", async () => { - expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ homeserverUrl: "" }))).toBe(false); + it("returns false when twilioPhoneNumber (from number) is missing", async () => { + expect(await createProvider().provider.sendTestAlert(makeTwilioNotification({ twilioPhoneNumber: "" }))).toBe(false); }); it("returns false and logs on error", async () => { @@ -85,8 +86,8 @@ describe("TwilioProvider", () => { expect(form.From).toBe("+15551234567"); }); - it("returns false when address is missing", async () => { - expect(await createProvider().provider.sendMessage(makeTwilioNotification({ address: "" }) as any, makeMessage())).toBe(false); + it("returns false when accountSid is missing", async () => { + expect(await createProvider().provider.sendMessage(makeTwilioNotification({ accountSid: "" }) as any, makeMessage())).toBe(false); }); it("returns false when accessToken is missing", async () => { diff --git a/server/test/unit/services/notificationsService.test.ts b/server/test/unit/services/notificationsService.test.ts index d8ee4b05c..cdc1ad320 100644 --- a/server/test/unit/services/notificationsService.test.ts +++ b/server/test/unit/services/notificationsService.test.ts @@ -147,7 +147,7 @@ describe("NotificationsService", () => { }); it("routes to correct provider for each notification type", async () => { - const types = ["webhook", "slack", "matrix", "pager_duty", "discord", "email", "teams", "telegram"] as const; + const types = ["webhook", "slack", "matrix", "pager_duty", "discord", "email", "teams", "telegram", "pushover", "twilio"] as const; for (const type of types) { const deps = createService(); (deps.notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification({ type })]); @@ -163,6 +163,8 @@ describe("NotificationsService", () => { email: deps.emailProvider, teams: deps.teamsProvider, telegram: deps.telegramProvider, + pushover: deps.pushoverProvider, + twilio: deps.twilioProvider, }; expect(providerMap[type].sendMessage).toHaveBeenCalledTimes(1); } @@ -236,7 +238,7 @@ describe("NotificationsService", () => { // ── sendTestNotification ───────────────────────────────────────────────── describe("sendTestNotification", () => { - it.each([["email"], ["slack"], ["discord"], ["pager_duty"], ["matrix"], ["webhook"], ["teams"], ["telegram"]] as const)( + it.each([["email"], ["slack"], ["discord"], ["pager_duty"], ["matrix"], ["webhook"], ["teams"], ["telegram"], ["pushover"], ["twilio"]] as const)( "routes %s to the correct provider", async (type) => { const deps = createService(); @@ -254,6 +256,8 @@ describe("NotificationsService", () => { email: deps.emailProvider, teams: deps.teamsProvider, telegram: deps.telegramProvider, + pushover: deps.pushoverProvider, + twilio: deps.twilioProvider, }; expect(providerMap[type].sendTestAlert).toHaveBeenCalledWith(notification); }