feat: add extensive test suite
This commit is contained in:
@@ -7,85 +7,81 @@ import { donations, settings } from "@/lib/db/schema";
|
||||
import { sse } from "@/lib/db/sse-endpoints";
|
||||
import { getPostHogClient } from "@/lib/posthog-server";
|
||||
import { fileToDataUrl } from "@/lib/utils";
|
||||
import {
|
||||
getDonationImage,
|
||||
publishTestPopup,
|
||||
reloadDonationWidget,
|
||||
resendDonationPopup,
|
||||
resetDonationGoal,
|
||||
} from "./service";
|
||||
|
||||
function commonDependencies() {
|
||||
return {
|
||||
isAdmin: adminCheck,
|
||||
capture: (
|
||||
event: Parameters<ReturnType<typeof getPostHogClient>["capture"]>[0],
|
||||
) => getPostHogClient().capture(event),
|
||||
publish: (event: "update" | "ping" | "refresh", data: unknown) => {
|
||||
if (event === "update") return sse.donate.pub(event, data as null);
|
||||
if (event === "refresh") return sse.donate.pub(event, data as null);
|
||||
return sse.donate.pub(
|
||||
event,
|
||||
data as Parameters<typeof sse.donate.pub<"ping">>[1],
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function resetGoal() {
|
||||
if (!(await adminCheck())) throw new Error("Unauthorized");
|
||||
|
||||
getPostHogClient().capture({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_goal_reset",
|
||||
return resetDonationGoal({
|
||||
...commonDependencies(),
|
||||
now: () => new Date(),
|
||||
async resetGoal(at) {
|
||||
await db
|
||||
.insert(settings)
|
||||
.values({ donateGoalStarting: at })
|
||||
.onConflictDoUpdate({
|
||||
target: settings.id,
|
||||
set: { donateGoalStarting: at },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await db
|
||||
.insert(settings)
|
||||
.values({ donateGoalStarting: new Date() })
|
||||
.onConflictDoUpdate({
|
||||
target: settings.id,
|
||||
set: { donateGoalStarting: new Date() },
|
||||
});
|
||||
|
||||
sse.donate.pub("update", null);
|
||||
}
|
||||
|
||||
export async function testPopup() {
|
||||
if (!(await adminCheck())) throw new Error("Unauthorized");
|
||||
|
||||
getPostHogClient().capture({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_test_popup",
|
||||
});
|
||||
|
||||
sse.donate.pub("ping", {
|
||||
id: "test",
|
||||
name: "Mr. Buzz",
|
||||
message: "นี่คือข้อความทดสอบโดเนท",
|
||||
amount: 67,
|
||||
});
|
||||
return publishTestPopup(commonDependencies());
|
||||
}
|
||||
|
||||
export async function reloadWidget() {
|
||||
if (!(await adminCheck())) throw new Error("Unauthorized");
|
||||
|
||||
getPostHogClient().capture({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_widget_reload",
|
||||
});
|
||||
|
||||
sse.donate.pub("refresh", null);
|
||||
return reloadDonationWidget(commonDependencies());
|
||||
}
|
||||
|
||||
export async function resendPopup(id: string) {
|
||||
if (!(await adminCheck())) throw new Error("Unauthorized");
|
||||
const [sub] = await db
|
||||
.update(donations)
|
||||
.set({ sent: false })
|
||||
.where(eq(donations.id, id))
|
||||
.returning();
|
||||
if (!sub) throw new Error("not found");
|
||||
|
||||
getPostHogClient().capture({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_resend",
|
||||
properties: { id },
|
||||
});
|
||||
|
||||
sse.donate.pub("ping", {
|
||||
...sub,
|
||||
message: sub.message ?? "",
|
||||
image: sub.image
|
||||
? await fileToDataUrl(new File([Buffer.from(sub.image)], "abc.jpeg"))
|
||||
: undefined,
|
||||
return resendDonationPopup(id, {
|
||||
...commonDependencies(),
|
||||
async resetSent(donationId) {
|
||||
const [donation] = await db
|
||||
.update(donations)
|
||||
.set({ sent: false })
|
||||
.where(eq(donations.id, donationId))
|
||||
.returning();
|
||||
return donation;
|
||||
},
|
||||
imageToDataUrl: (image) =>
|
||||
fileToDataUrl(new File([Buffer.from(image)], "abc.jpeg")),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getImage(id: string) {
|
||||
if (!(await adminCheck())) throw new Error("Unauthorized");
|
||||
const [sub] = await db
|
||||
.select({ image: donations.image })
|
||||
.from(donations)
|
||||
.where(eq(donations.id, id));
|
||||
if (!sub) throw new Error("not found");
|
||||
if (!sub.image) throw new Error("no image");
|
||||
|
||||
return new Blob([Buffer.from(sub.image)], { type: "image/jpeg" });
|
||||
const image = await getDonationImage(id, {
|
||||
isAdmin: adminCheck,
|
||||
async findImage(donationId) {
|
||||
const [donation] = await db
|
||||
.select({ image: donations.image })
|
||||
.from(donations)
|
||||
.where(eq(donations.id, donationId));
|
||||
return donation?.image;
|
||||
},
|
||||
});
|
||||
return new Blob([Buffer.from(image)], { type: "image/jpeg" });
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { DonateWatcher } from "../client";
|
||||
import { db } from "@/lib/db";
|
||||
import { donations } from "@/lib/db/schema";
|
||||
import { desc } from "drizzle-orm";
|
||||
import { Gavel, ImageOff, RefreshCw } from "lucide-react";
|
||||
import { fileToDataUrl } from "@/lib/utils";
|
||||
import { redirect } from "next/navigation";
|
||||
import { markDone } from "@/app/widget/donate/api";
|
||||
import { ActionButton } from "@/components/action-button";
|
||||
import Image from "@/components/image";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { ActionButton } from "@/components/action-button";
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { donations } from "@/lib/db/schema";
|
||||
import { fileToDataUrl } from "@/lib/utils";
|
||||
import { reloadWidget } from "../api";
|
||||
import { markDone } from "@/app/widget/donate/api";
|
||||
import { DonateWatcher } from "../client";
|
||||
import { ClientTracker, PIP, VolumeChanger } from "./client";
|
||||
|
||||
export default async function DonateModeratorPage() {
|
||||
@@ -26,8 +26,9 @@ export default async function DonateModeratorPage() {
|
||||
|
||||
async function reject() {
|
||||
"use server";
|
||||
markDone(sub.id);
|
||||
reloadWidget();
|
||||
if (!(await adminCheck())) throw new Error("Unauthorized");
|
||||
await markDone(sub.id, process.env.DONATE_WIDGET_KEY ?? null);
|
||||
await reloadWidget();
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -37,9 +38,11 @@ export default async function DonateModeratorPage() {
|
||||
{sub.image ? (
|
||||
<div className="relative flex aspect-square w-full max-w-100 items-center justify-center overflow-hidden rounded-lg border bg-black/50">
|
||||
<Image
|
||||
src={await fileToDataUrl(
|
||||
new File([Buffer.from(sub.image)], "abc.jpeg"),
|
||||
)}
|
||||
src={
|
||||
await fileToDataUrl(
|
||||
new File([Buffer.from(sub.image)], "abc.jpeg"),
|
||||
)
|
||||
}
|
||||
alt="donation image"
|
||||
fill
|
||||
className="z-10"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, expect, mock, test } from "bun:test";
|
||||
import {
|
||||
getDonationImage,
|
||||
publishTestPopup,
|
||||
reloadDonationWidget,
|
||||
resendDonationPopup,
|
||||
resetDonationGoal,
|
||||
} from "./service";
|
||||
|
||||
function common(isAdmin = true) {
|
||||
return {
|
||||
isAdmin: mock(async () => isAdmin),
|
||||
capture: mock(() => {}),
|
||||
publish: mock(() => {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("donation admin guards", () => {
|
||||
test("all admin operations reject before mutation, analytics, or SSE", async () => {
|
||||
const deps = common(false);
|
||||
const resetGoal = mock(async () => {});
|
||||
const resetSent = mock(async () => undefined);
|
||||
const findImage = mock(async () => Buffer.from("image"));
|
||||
const actions = [
|
||||
() => resetDonationGoal({ ...deps, resetGoal, now: () => new Date(0) }),
|
||||
() => publishTestPopup(deps),
|
||||
() => reloadDonationWidget(deps),
|
||||
() =>
|
||||
resendDonationPopup("id", {
|
||||
...deps,
|
||||
resetSent,
|
||||
imageToDataUrl: async () => "data:image/jpeg;base64,aW1hZ2U=",
|
||||
}),
|
||||
() => getDonationImage("id", { isAdmin: deps.isAdmin, findImage }),
|
||||
];
|
||||
for (const action of actions)
|
||||
await expect(action()).rejects.toThrow("Unauthorized");
|
||||
expect(resetGoal).not.toHaveBeenCalled();
|
||||
expect(resetSent).not.toHaveBeenCalled();
|
||||
expect(findImage).not.toHaveBeenCalled();
|
||||
expect(deps.capture).not.toHaveBeenCalled();
|
||||
expect(deps.publish).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("donation admin operations", () => {
|
||||
test("resets the goal at one timestamp and publishes an update", async () => {
|
||||
const deps = common();
|
||||
const at = new Date("2026-08-30T12:00:00Z");
|
||||
const resetGoal = mock(async () => {});
|
||||
await resetDonationGoal({ ...deps, resetGoal, now: () => at });
|
||||
expect(resetGoal).toHaveBeenCalledWith(at);
|
||||
expect(deps.capture).toHaveBeenCalledWith({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_goal_reset",
|
||||
});
|
||||
expect(deps.publish).toHaveBeenCalledWith("update", null);
|
||||
});
|
||||
|
||||
test("publishes the stable test popup payload", async () => {
|
||||
const deps = common();
|
||||
await publishTestPopup(deps);
|
||||
expect(deps.publish).toHaveBeenCalledWith("ping", {
|
||||
id: "test",
|
||||
name: "Mr. Buzz",
|
||||
message: "นี่คือข้อความทดสอบโดเนท",
|
||||
amount: 67,
|
||||
});
|
||||
});
|
||||
|
||||
test("publishes a widget refresh and analytics", async () => {
|
||||
const deps = common();
|
||||
await reloadDonationWidget(deps);
|
||||
expect(deps.capture).toHaveBeenCalledWith({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_widget_reload",
|
||||
});
|
||||
expect(deps.publish).toHaveBeenCalledWith("refresh", null);
|
||||
});
|
||||
|
||||
test("resets and republishes a donation with normalized message and image", async () => {
|
||||
const deps = common();
|
||||
const resetSent = mock(async () => ({
|
||||
id: "donation-1",
|
||||
name: "Buzz",
|
||||
amount: 50,
|
||||
message: null,
|
||||
image: Buffer.from("image"),
|
||||
}));
|
||||
const imageToDataUrl = mock(async () => "data:image/jpeg;base64,aW1hZ2U=");
|
||||
await resendDonationPopup("donation-1", {
|
||||
...deps,
|
||||
resetSent,
|
||||
imageToDataUrl,
|
||||
});
|
||||
expect(resetSent).toHaveBeenCalledWith("donation-1");
|
||||
expect(deps.publish).toHaveBeenCalledWith("ping", {
|
||||
id: "donation-1",
|
||||
name: "Buzz",
|
||||
amount: 50,
|
||||
message: "",
|
||||
image: "data:image/jpeg;base64,aW1hZ2U=",
|
||||
});
|
||||
expect(deps.capture).toHaveBeenCalledWith({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_resend",
|
||||
properties: { id: "donation-1" },
|
||||
});
|
||||
});
|
||||
|
||||
test("fails resend when the donation does not exist", async () => {
|
||||
const deps = common();
|
||||
await expect(
|
||||
resendDonationPopup("missing", {
|
||||
...deps,
|
||||
resetSent: async () => undefined,
|
||||
imageToDataUrl: async () => "unused",
|
||||
}),
|
||||
).rejects.toThrow("not found");
|
||||
expect(deps.capture).not.toHaveBeenCalled();
|
||||
expect(deps.publish).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns an image and distinguishes missing rows from empty images", async () => {
|
||||
const isAdmin = async () => true;
|
||||
expect(
|
||||
await getDonationImage("id", {
|
||||
isAdmin,
|
||||
findImage: async () => Buffer.from("image"),
|
||||
}),
|
||||
).toEqual(Buffer.from("image"));
|
||||
await expect(
|
||||
getDonationImage("id", { isAdmin, findImage: async () => undefined }),
|
||||
).rejects.toThrow("not found");
|
||||
await expect(
|
||||
getDonationImage("id", { isAdmin, findImage: async () => null }),
|
||||
).rejects.toThrow("no image");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
type AdminDependencies = {
|
||||
isAdmin: () => Promise<unknown>;
|
||||
capture: (event: {
|
||||
distinctId: string;
|
||||
event: string;
|
||||
properties?: Record<string, unknown>;
|
||||
}) => unknown;
|
||||
publish: (event: "update" | "ping" | "refresh", data: unknown) => unknown;
|
||||
};
|
||||
|
||||
async function authorize(dependencies: AdminDependencies) {
|
||||
if (!(await dependencies.isAdmin())) throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
export async function resetDonationGoal(
|
||||
dependencies: AdminDependencies & {
|
||||
resetGoal: (at: Date) => Promise<unknown>;
|
||||
now: () => Date;
|
||||
},
|
||||
) {
|
||||
await authorize(dependencies);
|
||||
dependencies.capture({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_goal_reset",
|
||||
});
|
||||
await dependencies.resetGoal(dependencies.now());
|
||||
dependencies.publish("update", null);
|
||||
}
|
||||
|
||||
export async function publishTestPopup(dependencies: AdminDependencies) {
|
||||
await authorize(dependencies);
|
||||
dependencies.capture({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_test_popup",
|
||||
});
|
||||
dependencies.publish("ping", {
|
||||
id: "test",
|
||||
name: "Mr. Buzz",
|
||||
message: "นี่คือข้อความทดสอบโดเนท",
|
||||
amount: 67,
|
||||
});
|
||||
}
|
||||
|
||||
export async function reloadDonationWidget(dependencies: AdminDependencies) {
|
||||
await authorize(dependencies);
|
||||
dependencies.capture({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_widget_reload",
|
||||
});
|
||||
dependencies.publish("refresh", null);
|
||||
}
|
||||
|
||||
export async function resendDonationPopup<
|
||||
T extends {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
message: string | null;
|
||||
image: Buffer | null;
|
||||
},
|
||||
>(
|
||||
id: string,
|
||||
dependencies: AdminDependencies & {
|
||||
resetSent: (id: string) => Promise<T | undefined>;
|
||||
imageToDataUrl: (image: Buffer) => Promise<string>;
|
||||
},
|
||||
) {
|
||||
await authorize(dependencies);
|
||||
const donation = await dependencies.resetSent(id);
|
||||
if (!donation) throw new Error("not found");
|
||||
dependencies.capture({
|
||||
distinctId: "admin",
|
||||
event: "donation_admin_resend",
|
||||
properties: { id },
|
||||
});
|
||||
dependencies.publish("ping", {
|
||||
...donation,
|
||||
message: donation.message ?? "",
|
||||
image: donation.image
|
||||
? await dependencies.imageToDataUrl(donation.image)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDonationImage(
|
||||
id: string,
|
||||
dependencies: Pick<AdminDependencies, "isAdmin"> & {
|
||||
findImage: (id: string) => Promise<Buffer | null | undefined>;
|
||||
},
|
||||
) {
|
||||
if (!(await dependencies.isAdmin())) throw new Error("Unauthorized");
|
||||
const image = await dependencies.findImage(id);
|
||||
if (image === undefined) throw new Error("not found");
|
||||
if (image === null) throw new Error("no image");
|
||||
return image;
|
||||
}
|
||||
Reference in New Issue
Block a user