185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
import {
|
|
smsBlockedNumbersDb,
|
|
strongerBlockReason,
|
|
} from "../../src/db/smsBlockedNumbersSqlite.js";
|
|
import { closeDatabase } from "../../src/db/sqlite.js";
|
|
import { isPhoneNumberBlocked } from "../../src/services/smsService.js";
|
|
import { hashPhoneNumber } from "../../src/util/smsVerificationCode.js";
|
|
|
|
const SECRET = "block-test-secret";
|
|
const PHONE = "+15555550123";
|
|
const OTHER = "+15555550124";
|
|
const HASH = hashPhoneNumber(PHONE, SECRET);
|
|
|
|
let dir: string;
|
|
let savedDataDir: string | undefined;
|
|
let savedBlocked: string | undefined;
|
|
|
|
beforeEach(async () => {
|
|
savedDataDir = process.env.NOTIFY_DATA_DIR;
|
|
savedBlocked = process.env.SMS_BLOCKED_NUMBERS;
|
|
delete process.env.SMS_BLOCKED_NUMBERS;
|
|
dir = await mkdtemp(path.join(tmpdir(), "sms-block-"));
|
|
process.env.NOTIFY_DATA_DIR = dir;
|
|
closeDatabase();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
closeDatabase();
|
|
if (savedDataDir === undefined) delete process.env.NOTIFY_DATA_DIR;
|
|
else process.env.NOTIFY_DATA_DIR = savedDataDir;
|
|
if (savedBlocked === undefined) delete process.env.SMS_BLOCKED_NUMBERS;
|
|
else process.env.SMS_BLOCKED_NUMBERS = savedBlocked;
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("smsBlockedNumbersDb", () => {
|
|
it("blocks, reports, and unblocks by hash", async () => {
|
|
assert.equal(await smsBlockedNumbersDb.isBlocked(HASH), false);
|
|
|
|
const stored = await smsBlockedNumbersDb.block({
|
|
phoneHash: HASH,
|
|
phoneE164: PHONE,
|
|
reason: "opt-out",
|
|
detail: "keyword STOP",
|
|
});
|
|
assert.equal(stored.phoneE164, PHONE);
|
|
assert.equal(stored.reason, "opt-out");
|
|
assert.equal(await smsBlockedNumbersDb.isBlocked(HASH), true);
|
|
|
|
assert.equal(await smsBlockedNumbersDb.unblock(HASH), true);
|
|
assert.equal(await smsBlockedNumbersDb.isBlocked(HASH), false);
|
|
assert.equal(await smsBlockedNumbersDb.unblock(HASH), false);
|
|
});
|
|
|
|
it("is idempotent, so a second STOP refreshes rather than fails", async () => {
|
|
await smsBlockedNumbersDb.block({
|
|
phoneHash: HASH,
|
|
phoneE164: PHONE,
|
|
reason: "manual",
|
|
detail: "first",
|
|
});
|
|
await smsBlockedNumbersDb.block({
|
|
phoneHash: HASH,
|
|
reason: "manual",
|
|
detail: "second",
|
|
});
|
|
|
|
const rows = await smsBlockedNumbersDb.list();
|
|
assert.equal(rows.length, 1);
|
|
assert.equal(rows[0].detail, "second");
|
|
// The number is preserved when a later block omits it.
|
|
assert.equal(rows[0].phoneE164, PHONE);
|
|
});
|
|
|
|
it("never downgrades an opt-out to a manual block", async () => {
|
|
await smsBlockedNumbersDb.block({
|
|
phoneHash: HASH,
|
|
phoneE164: PHONE,
|
|
reason: "opt-out",
|
|
detail: "keyword STOP",
|
|
});
|
|
// An operator re-blocking must not erase the record saying the handset
|
|
// asked, because that record is what forbids simply lifting the block.
|
|
await smsBlockedNumbersDb.block({
|
|
phoneHash: HASH,
|
|
reason: "manual",
|
|
detail: "operator",
|
|
});
|
|
assert.equal((await smsBlockedNumbersDb.get(HASH))?.reason, "opt-out");
|
|
});
|
|
|
|
it("upgrades a manual block when the handset actually opts out", async () => {
|
|
await smsBlockedNumbersDb.block({ phoneHash: HASH, reason: "manual" });
|
|
await smsBlockedNumbersDb.block({
|
|
phoneHash: HASH,
|
|
reason: "provider-opt-out",
|
|
});
|
|
assert.equal(
|
|
(await smsBlockedNumbersDb.get(HASH))?.reason,
|
|
"provider-opt-out"
|
|
);
|
|
|
|
await smsBlockedNumbersDb.block({ phoneHash: HASH, reason: "opt-out" });
|
|
assert.equal((await smsBlockedNumbersDb.get(HASH))?.reason, "opt-out");
|
|
|
|
// ...and does not fall back down again.
|
|
await smsBlockedNumbersDb.block({
|
|
phoneHash: HASH,
|
|
reason: "provider-opt-out",
|
|
});
|
|
assert.equal((await smsBlockedNumbersDb.get(HASH))?.reason, "opt-out");
|
|
});
|
|
|
|
it("blocks only the hashed number, not its neighbours", async () => {
|
|
await smsBlockedNumbersDb.block({ phoneHash: HASH, reason: "manual" });
|
|
assert.equal(
|
|
await smsBlockedNumbersDb.isBlocked(hashPhoneNumber(OTHER, SECRET)),
|
|
false
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("isPhoneNumberBlocked", () => {
|
|
it("is false for a number on neither source", async () => {
|
|
assert.equal(
|
|
await isPhoneNumberBlocked({ phoneE164: PHONE, phoneHash: HASH }),
|
|
false
|
|
);
|
|
});
|
|
|
|
it("consults the table", async () => {
|
|
await smsBlockedNumbersDb.block({ phoneHash: HASH, reason: "opt-out" });
|
|
assert.equal(
|
|
await isPhoneNumberBlocked({ phoneE164: PHONE, phoneHash: HASH }),
|
|
true
|
|
);
|
|
});
|
|
|
|
it("consults SMS_BLOCKED_NUMBERS without needing a database row", async () => {
|
|
process.env.SMS_BLOCKED_NUMBERS = PHONE;
|
|
assert.equal(
|
|
await isPhoneNumberBlocked({ phoneE164: PHONE, phoneHash: HASH }),
|
|
true
|
|
);
|
|
assert.equal(
|
|
await isPhoneNumberBlocked({
|
|
phoneE164: OTHER,
|
|
phoneHash: hashPhoneNumber(OTHER, SECRET),
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
it("normalizes configured entries, so formatting does not defeat it", async () => {
|
|
process.env.SMS_BLOCKED_NUMBERS = " (555) 555-0123 , 5555550124 ";
|
|
assert.equal(
|
|
await isPhoneNumberBlocked({ phoneE164: PHONE, phoneHash: HASH }),
|
|
true
|
|
);
|
|
assert.equal(
|
|
await isPhoneNumberBlocked({
|
|
phoneE164: OTHER,
|
|
phoneHash: hashPhoneNumber(OTHER, SECRET),
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("strongerBlockReason", () => {
|
|
it("ranks an opt-out above a provider opt-out above a manual block", () => {
|
|
assert.equal(strongerBlockReason(undefined, "manual"), "manual");
|
|
assert.equal(strongerBlockReason("manual", "provider-opt-out"), "provider-opt-out");
|
|
assert.equal(strongerBlockReason("provider-opt-out", "opt-out"), "opt-out");
|
|
assert.equal(strongerBlockReason("opt-out", "manual"), "opt-out");
|
|
assert.equal(strongerBlockReason("opt-out", "provider-opt-out"), "opt-out");
|
|
assert.equal(strongerBlockReason("provider-opt-out", "manual"), "provider-opt-out");
|
|
});
|
|
});
|