Skip to content
This repository was archived by the owner on Dec 5, 2025. It is now read-only.
Closed
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
4 changes: 2 additions & 2 deletions infrastructure/terraform/jumphost.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ data "aws_ami" "ubuntu" {

filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"]
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}

filter {
Expand Down Expand Up @@ -38,7 +38,7 @@ resource "aws_instance" "jumphost" {

# Public IP
resource "aws_eip" "jumphost" {
vpc = true
domain = "vpc"
instance = aws_instance.jumphost.id
depends_on = [aws_internet_gateway.gw]
}
Expand Down
55 changes: 40 additions & 15 deletions tests/general/twilio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ jest.mock("../../twilio/client", () => ({
twilioClient: jest.fn(),
}));

// Mock Prisma to prevent database initialization issues
jest.mock("../../services/prisma", () => ({
__esModule: true,
default: {},
}));

import notifs from "../../twilio/notifs";
import express from "express";
import macros from "../../utils/macros";
Expand Down Expand Up @@ -132,17 +138,17 @@ describe("TwilioNotifyer", () => {
create: jest.fn(async (args) => {
const err = new Error();
switch (args.to) {
case "1":
case "+1 (1)":
return 200;
case "2":
case "+1 (2)":
// @ts-expect-error -- wrong error type
err.code = notifs.TWILIO_ERRORS.SMS_NOT_FOR_LANDLINE;
throw err;
case "3":
case "+1 (3)":
// @ts-expect-error -- wrong error type
err.code = notifs.TWILIO_ERRORS.INVALID_PHONE_NUMBER;
throw err;
case "4":
case "+1 (4)":
// @ts-expect-error -- wrong error type
err.code = notifs.TWILIO_ERRORS.MAX_SEND_ATTEMPTS_REACHED;
throw err;
Expand All @@ -158,31 +164,41 @@ describe("TwilioNotifyer", () => {
});

it("non-error", async () => {
const resp = await notifs.sendVerificationCode("1");
const resp = await notifs.sendVerificationCode("+1 (1)");
expect(resp.statusCode).toBe(200);
expect(resp.message).toMatch(/code sent/i);
});

it("landline error", async () => {
const resp = await notifs.sendVerificationCode("2");
const resp = await notifs.sendVerificationCode("+1 (2)");
expect(resp.statusCode).toBe(400);
expect(resp.message).toMatch(/not supported by landline/i);
});

it("blocks international numbers", async () => {
const resp = await notifs.sendVerificationCode("+441234567890");
expect(resp.statusCode).toBe(400);
expect(resp.message).toBe(
"Invalid phone number format. Please use a US or Canadian number.",
);
});

it("invalid number error", async () => {
const resp = await notifs.sendVerificationCode("3");
const resp = await notifs.sendVerificationCode("+1 (3)");
expect(resp.statusCode).toBe(400);
expect(resp.message).toMatch(/invalid phone number/i);
});

it("max send attempts error", async () => {
const resp = await notifs.sendVerificationCode("4");
const resp = await notifs.sendVerificationCode("+1 (4)");
expect(resp.statusCode).toBe(400);
expect(resp.message).toMatch(/attempted to send.*too many times/i);
});

it("default error", async () => {
await expect(notifs.sendVerificationCode("123123142")).rejects.toThrow();
await expect(
notifs.sendVerificationCode("+1 (123)-123-1420"),
).rejects.toThrow();
});
});

Expand All @@ -193,9 +209,9 @@ describe("TwilioNotifyer", () => {
create: jest.fn(async (args) => {
const err = new Error();
switch (args.to) {
case "1":
case "+1 (1)":
return;
case "2":
case "+1 (2)":
// @ts-expect-error -- wrong error type
err.code = notifs.TWILIO_ERRORS.USER_UNSUBSCRIBED;
throw err;
Expand All @@ -208,7 +224,7 @@ describe("TwilioNotifyer", () => {

it("Successfully sends a message", async () => {
jest.spyOn(macros, "log");
await notifs.sendNotificationText("1", "message");
await notifs.sendNotificationText("+1 (1)", "message");
expect(macros.log).toHaveBeenCalledWith(
expect.stringMatching(/sent.*text/i),
);
Expand All @@ -222,21 +238,30 @@ describe("TwilioNotifyer", () => {
// don't do anytthing
});

await notifs.sendNotificationText("2", "message");
await notifs.sendNotificationText("+1 (2)", "message");
expect(macros.warn).toHaveBeenCalledWith(
expect.stringMatching(/has unsubscribed/i),
);
expect(
notificationsManager.deleteAllUserSubscriptions,
).toHaveBeenCalledWith("2");
).toHaveBeenCalledWith("+1 (2)");
});

it("blocks international numbers", async () => {
jest.spyOn(macros, "warn");

await notifs.sendNotificationText("+441234567890", "test message");
expect(macros.warn).toHaveBeenCalledWith(
"Invalid phone number format for +441234567890. Please use a US or Canadian number.",
);
});

it("Default error", async () => {
jest.spyOn(macros, "error").mockImplementationOnce(() => {
// don't do anytthing
});

await notifs.sendNotificationText("3", "message");
await notifs.sendNotificationText("+15555555555", "message");
expect(macros.error).toHaveBeenCalledWith(
expect.stringMatching(/error trying to send/i),
expect.any(Error),
Expand Down
26 changes: 26 additions & 0 deletions twilio/notifs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,25 @@ class TwilioNotifyer {
this.twilioClient = twilioClient;
}

/**
* Validates that the phone number is US or Canadian (+1 country code)
*/
private isValidDomesticNumber(phoneNumber: string): boolean {
// Check if number starts with +1 (US/Canada country code)
return phoneNumber.startsWith("+1");
}

async sendNotificationText(
recipientNumber: string,
message: string,
): Promise<void> {
if (!this.isValidDomesticNumber(recipientNumber)) {
macros.warn(
`Invalid phone number format for ${recipientNumber}. Please use a US or Canadian number.`,
);
return;
}

return this.twilioClient.messages
.create({ body: message, from: this.TWILIO_NUMBER, to: recipientNumber })
.then(() => {
Expand All @@ -67,6 +82,17 @@ class TwilioNotifyer {
}

async sendVerificationCode(recipientNumber: string): Promise<TwilioResponse> {
if (!this.isValidDomesticNumber(recipientNumber)) {
macros.warn(
`Invalid phone number format for ${recipientNumber}. Please use a US or Canadian number.`,
);
return {
statusCode: 400,
message:
"Invalid phone number format. Please use a US or Canadian number.",
};
}

return this.twilioClient.verify.v2
.services(this.TWILIO_VERIFY_SERVICE_SID)
.verifications.create({ to: recipientNumber, channel: "sms" })
Expand Down
Loading