Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,300 @@
import { createHmac } from "node:crypto";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import {
jsonResponder,
type RecordingServer,
startRecordingServer,
} from "../../tests/setup/http-server.js";
import { SMS_PROVIDERS_CODES } from "../error-codes/error-codes.js";
import type { OutboundMessage } from "../types/provider.js";
import { PlivoProvider } from "./plivo.js";
let server: RecordingServer;
afterEach(async () => {
await server.close();
});
function makeProvider(config: { authId?: string; authToken?: string } = {}) {
return new PlivoProvider({
authId: config.authId ?? "A1",
authToken: config.authToken ?? "T1",
baseUrl: server.origin,
fetch: server.fetch,
});
}
const SEND_RESPONSE = {
api_id: "api-uuid",
message: "message(s) queued",
message_uuid: ["msg-uuid-1"],
};
const GET_RESPONSE = {
api_id: "api-uuid",
message_uuid: "msg-uuid-1",
to_number: "+15551234567",
message_state: "delivered",
message_time: "2026-04-24T10:00:00Z",
error_code: "000",
};
describe("PlivoProvider — construction + send", () => {
beforeEach(async () => {
server = await startRecordingServer(jsonResponder(SEND_RESPONSE, 202));
});
it("requires authId + authToken", { timeout: 5000 }, () => {
expect(() => makeProvider({ authId: "" })).toThrow();
expect(() => makeProvider({ authToken: "" })).toThrow();
});
it("POSTs Account/{id}/Message/ with Basic auth + JSON body", {
timeout: 5000,
}, async () => {
const p = makeProvider();
const r = await p.send({
to: "+15551234567",
from: "+15559999999",
body: "hi",
});
const req = server.requests[0]!;
expect(req.method).toBe("POST");
expect(req.path).toBe("/v1/Account/A1/Message/");
expect(req.headers.authorization).toBe(
`Basic ${Buffer.from("A1:T1").toString("base64")}`
);
expect(req.headers["content-type"]).toContain("application/json");
const parsed = JSON.parse(req.body) as Record<string, unknown>;
expect(parsed).toEqual({
src: "+15559999999",
dst: "+15551234567",
text: "hi",
});
expect(r.messageId).toBe("msg-uuid-1");
expect(r.status).toBe("queued");
});
it("forwards MMS media + flips type to mms", {
timeout: 5000,
}, async () => {
const p = makeProvider();
await p.send({
to: "+1",
from: "+1",
body: "hi",
mediaUrls: ["https://cdn/a.png"],
} as OutboundMessage);
const parsed = JSON.parse(server.requests[0]!.body) as Record<
string,
unknown
>;
expect(parsed["media_urls"]).toEqual(["https://cdn/a.png"]);
expect(parsed["type"]).toBe("mms");
});
it("defaults src to empty when from is undefined", {
timeout: 5000,
}, async () => {
const p = makeProvider();
await p.send({ to: "+1", body: "hi" });
const parsed = JSON.parse(server.requests[0]!.body) as Record<
string,
unknown
>;
expect(parsed["src"]).toBe("");
});
it("returns empty messageId when message_uuid[] is absent", {
timeout: 5000,
}, async () => {
await server.close();
server = await startRecordingServer(
jsonResponder({ ...SEND_RESPONSE, message_uuid: [] }, 202)
);
const p = makeProvider();
const r = await p.send({ to: "+1", from: "+1", body: "hi" });
expect(r.messageId).toBe("");
});
});
describe("PlivoProvider — getStatus", () => {
beforeEach(async () => {
server = await startRecordingServer(jsonResponder(GET_RESPONSE));
});
it("GETs the Message/{uuid}/ resource", { timeout: 5000 }, async () => {
const p = makeProvider();
const r = await p.getStatus("msg-uuid-1");
expect(server.requests[0]!.path).toBe(
"/v1/Account/A1/Message/msg-uuid-1/"
);
expect(r.status).toBe("delivered");
expect(r.errorCode).toBe("000");
});
it("defaults the timestamp when message_time is absent", {
timeout: 5000,
}, async () => {
await server.close();
server = await startRecordingServer(
jsonResponder({ ...GET_RESPONSE, message_time: undefined })
);
const p = makeProvider();
const r = await p.getStatus("msg");
expect(r.timestamp).toBeInstanceOf(Date);
});
});
describe("PlivoProvider — mapStatus", () => {
beforeEach(async () => {
server = await startRecordingServer(jsonResponder({}));
});
it.each([
["queued", "queued"],
["sent", "sent"],
["delivered", "delivered"],
["undelivered", "undelivered"],
["failed", "failed"],
["rejected", "failed"],
["wat", "failed"],
])("%s → %s", { timeout: 5000 }, (raw, mapped) => {
expect(makeProvider().mapStatus(raw)).toBe(mapped);
});
});
function plivoSign(
token: string,
url: string,
nonce: string,
params: Record<string, string>
): string {
const sortedKeys = Object.keys(params).sort();
const pairs = sortedKeys.map((k) => `${k}=${params[k]}`).join(",");
return createHmac("sha256", token)
.update(`${url}${nonce}${pairs}`)
.digest("base64");
}
describe("PlivoProvider — verifyWebhookSignature", () => {
beforeEach(async () => {
server = await startRecordingServer(jsonResponder({}));
});
it("accepts a valid signature", { timeout: 5000 }, async () => {
const url = "https://app/webhooks/plivo";
const nonce = "n123";
const params = { From: "+1", To: "+2", Text: "hi", MessageUUID: "u" };
const sig = plivoSign("T1", url, nonce, params);
const ok = await makeProvider().verifyWebhookSignature(
url,
nonce,
params,
sig
);
expect(ok).toBe(true);
});
it("rejects a tampered signature", { timeout: 5000 }, async () => {
const url = "https://app/webhooks/plivo";
const sig = plivoSign("T1", url, "n", { a: "1" });
await expect(
makeProvider().verifyWebhookSignature(
url,
"n",
{ a: "1" },
`${sig.slice(0, -1)}!`
)
).rejects.toMatchObject({
code: SMS_PROVIDERS_CODES.SIGNATURE_INVALID,
});
});
});
describe("PlivoProvider — parseInbound", () => {
beforeEach(async () => {
server = await startRecordingServer(jsonResponder({}));
});
it("identifies delivery receipts", { timeout: 5000 }, () => {
expect(
makeProvider().parseInbound({
MessageUUID: "u",
Status: "delivered",
ErrorCode: "000",
})
).toEqual({
type: "delivery_receipt",
messageId: "u",
status: "delivered",
errorCode: "000",
});
});
it("identifies DLR without errorCode when ErrorCode absent", {
timeout: 5000,
}, () => {
expect(
makeProvider().parseInbound({
MessageUUID: "u",
Status: "delivered",
})
).toEqual({
type: "delivery_receipt",
messageId: "u",
status: "delivered",
});
});
it("identifies STOP / YES / HELP", { timeout: 5000 }, () => {
const p = makeProvider();
expect(p.parseInbound({ From: "+1", Text: "stop" })).toMatchObject({
type: "opt_out",
});
expect(p.parseInbound({ From: "+1", Text: "YES" })).toMatchObject({
type: "opt_in",
});
expect(p.parseInbound({ From: "+1", Text: "HELP" })).toMatchObject({
type: "help",
});
});
it("returns message_received with providerId when MessageUUID present", {
timeout: 5000,
}, () => {
expect(
makeProvider().parseInbound({
From: "+1",
To: "+2",
Text: "free-form",
MessageUUID: "u",
})
).toEqual({
type: "message_received",
from: "+1",
to: "+2",
body: "free-form",
providerId: "u",
});
});
it("falls back to lowercase 'text' when 'Text' is absent", {
timeout: 5000,
}, () => {
expect(
makeProvider().parseInbound({ From: "+1", text: "lower" })
).toEqual({
type: "message_received",
from: "+1",
to: "",
body: "lower",
});
});
});