Files
notification-wakeup-service/test/db/smsBlockedNumbers.test.ts
T

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");
});
});