|
|
|
@@ -1,14 +1,12 @@
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
|
import { detectKeyword } from "../consent.js";
|
|
|
|
|
|
|
|
|
|
const mockDb = {
|
|
|
|
|
insert: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
select: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
vi.mock("@groombook/db", () => ({
|
|
|
|
|
getDb: () => mockDb,
|
|
|
|
|
db: {
|
|
|
|
|
insert: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
select: vi.fn(),
|
|
|
|
|
},
|
|
|
|
|
clients: {},
|
|
|
|
|
messageConsentEvents: {},
|
|
|
|
|
businessSettings: {},
|
|
|
|
@@ -16,6 +14,7 @@ vi.mock("@groombook/db", () => ({
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const { handleConsentKeyword } = await import("../consent.js");
|
|
|
|
|
const { db } = await import("@groombook/db");
|
|
|
|
|
|
|
|
|
|
describe("detectKeyword", () => {
|
|
|
|
|
it.each([
|
|
|
|
@@ -66,10 +65,10 @@ describe("detectKeyword", () => {
|
|
|
|
|
describe("handleConsentKeyword", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
mockDb.insert.mockReturnValue({
|
|
|
|
|
db.insert.mockReturnValue({
|
|
|
|
|
values: vi.fn().mockResolvedValue([{ id: "event-1" }]),
|
|
|
|
|
} as any);
|
|
|
|
|
mockDb.update.mockReturnValue({
|
|
|
|
|
db.update.mockReturnValue({
|
|
|
|
|
set: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockResolvedValue([]),
|
|
|
|
|
}),
|
|
|
|
@@ -79,12 +78,12 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
const baseOpts = {
|
|
|
|
|
clientId: "client-1",
|
|
|
|
|
businessId: "biz-1",
|
|
|
|
|
db: mockDb as unknown as ReturnType<typeof import("@groombook/db").getDb>,
|
|
|
|
|
db: db as unknown as typeof import("@groombook/db").db,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe("opt_out", () => {
|
|
|
|
|
it("inserts consent event with sms_keyword source", async () => {
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: true }]),
|
|
|
|
@@ -94,11 +93,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_out" });
|
|
|
|
|
|
|
|
|
|
expect(mockDb.insert).toHaveBeenCalledOnce();
|
|
|
|
|
expect(db.insert).toHaveBeenCalledOnce();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("sets smsOptIn=false and smsOptOutDate when currently opted in", async () => {
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: true }]),
|
|
|
|
@@ -108,11 +107,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_out" });
|
|
|
|
|
|
|
|
|
|
expect(mockDb.update).toHaveBeenCalled();
|
|
|
|
|
expect(db.update).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is idempotent — second opt-out logs event but skips client update", async () => {
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: false }]),
|
|
|
|
@@ -122,11 +121,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_out" });
|
|
|
|
|
|
|
|
|
|
expect(mockDb.update).not.toHaveBeenCalled();
|
|
|
|
|
expect(db.update).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns unsubscribe reply text", async () => {
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: true }]),
|
|
|
|
@@ -143,7 +142,7 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
describe("opt_in", () => {
|
|
|
|
|
it("sets smsOptIn=true and smsConsentDate when currently opted out", async () => {
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: false, smsConsentDate: null }]),
|
|
|
|
@@ -153,11 +152,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_in" });
|
|
|
|
|
|
|
|
|
|
expect(mockDb.update).toHaveBeenCalled();
|
|
|
|
|
expect(db.update).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("clears smsOptOutDate on opt-in after opt-out", async () => {
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: false }]),
|
|
|
|
@@ -167,11 +166,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_in" });
|
|
|
|
|
|
|
|
|
|
expect(mockDb.update).toHaveBeenCalled();
|
|
|
|
|
expect(db.update).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("is idempotent — second opt-in skips client update", async () => {
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: true }]),
|
|
|
|
@@ -181,11 +180,11 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
|
|
|
|
|
await handleConsentKeyword({ ...baseOpts, kind: "opt_in" });
|
|
|
|
|
|
|
|
|
|
expect(mockDb.update).not.toHaveBeenCalled();
|
|
|
|
|
expect(db.update).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("returns resubscribe reply text", async () => {
|
|
|
|
|
mockDb.select.mockReturnValue({
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ smsOptIn: false }]),
|
|
|
|
@@ -201,14 +200,34 @@ describe("handleConsentKeyword", () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("help", () => {
|
|
|
|
|
it("returns default help reply without querying businessSettings", async () => {
|
|
|
|
|
it("does not call update — opt-in state unchanged", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ messagingHelpReply: null }]),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
const result = await handleConsentKeyword({ ...baseOpts, kind: "help" });
|
|
|
|
|
|
|
|
|
|
expect(mockDb.update).not.toHaveBeenCalled();
|
|
|
|
|
expect(mockDb.select).not.toHaveBeenCalled();
|
|
|
|
|
expect(db.update).not.toHaveBeenCalled();
|
|
|
|
|
expect(result.replyText).toBe(
|
|
|
|
|
"Reply STOP to unsubscribe or START to resubscribe. For help, contact your groomer directly."
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("uses business messagingHelpReply when configured", async () => {
|
|
|
|
|
db.select.mockReturnValue({
|
|
|
|
|
from: vi.fn().mockReturnValue({
|
|
|
|
|
where: vi.fn().mockReturnValue({
|
|
|
|
|
limit: vi.fn().mockResolvedValue([{ messagingHelpReply: "Custom help text." }]),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
const result = await handleConsentKeyword({ ...baseOpts, kind: "help" });
|
|
|
|
|
expect(result.replyText).toBe("Custom help text.");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|