Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions __tests__/admin/AdminReportsPage.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";

vi.mock("@/lib/config/font.config", () => ({
poppins_400: { className: "" },
poppins_500: { className: "" },
poppins_600: { className: "" },
}));

vi.mock("@/components/auth/AdminTierGuard", () => ({
default: ({ children }) => children,
}));

const hookState = vi.hoisted(() => ({ current: null }));
vi.mock("@/hooks/useAdminReports", () => ({ default: () => hookState.current }));

const reasons = [
{ value: "does-not-violate-policy", label: "Doesn't violate policy" },
{ value: "already-handled", label: "Already handled" },
{ value: "insufficient-evidence", label: "Insufficient evidence" },
{ value: "duplicate", label: "Duplicate" },
];

const reports = [
{
id: "R-5521",
subject: "Comment on Seerah Q&A",
contentType: "comment",
contentPreview: "Needs review",
reporter: { id: "user-101", name: "Hafsa Ali", priorReportCount: 0 },
},
];

beforeEach(() => {
Element.prototype.scrollIntoView = vi.fn();
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
hookState.current = {
reports,
isLoading: false,
isDismissing: false,
error: null,
refresh: vi.fn(),
dismiss: vi.fn(),
};
});

describe("AdminReportsPage", () => {
it("renders the report queue and dismissal action", async () => {
const { default: AdminReportsPage } = await import(
"@/app/[locale]/dashboard/admin/reports/page"
);
render(<AdminReportsPage />);

expect(screen.getByText("Report queue")).toBeInTheDocument();
expect(screen.getByText(/reported by hafsa ali/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /dismiss report/i })).toBeInTheDocument();
});

it("shows the first-time reporter notification default after opening the dialog", async () => {
const { default: AdminReportsPage } = await import(
"@/app/[locale]/dashboard/admin/reports/page"
);
render(<AdminReportsPage />);
fireEvent.click(screen.getByRole("button", { name: /dismiss report/i }));

expect(await screen.findByText(/notifications default on for first-time reporters/i)).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: /send courtesy notification/i })).toHaveAttribute(
"data-state",
"checked"
);

// Keep the supported choices close to the UI contract even though Radix
// renders them in a portal only after the trigger is opened.
expect(reasons.map(({ label }) => label)).toEqual([
"Doesn't violate policy",
"Already handled",
"Insufficient evidence",
"Duplicate",
]);
});
});
76 changes: 76 additions & 0 deletions __tests__/admin/admin-reports.service.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, it, expect, vi } from "vitest";

vi.mock("@/lib/admin/audit", () => ({
AUDIT_ACTIONS: { REPORT_DISMISSED: "moderation.report_dismissed" },
logAuditEvent: vi.fn(),
}));

import {
DISMISSAL_REASONS,
dismissReport,
getDefaultNotificationPreference,
isFirstTimeReporter,
listReports,
} from "@/lib/actions/admin-reports";

describe("admin reports service", () => {
it("exposes all supported dismissal reasons", () => {
expect(DISMISSAL_REASONS.map(({ value }) => value)).toEqual([
"does-not-violate-policy",
"already-handled",
"insufficient-evidence",
"duplicate",
]);
});

it("identifies first-time reporters and applies smart defaults", () => {
expect(isFirstTimeReporter({ priorReportCount: 0 })).toBe(true);
expect(isFirstTimeReporter({ priorReportCount: 3 })).toBe(false);
expect(getDefaultNotificationPreference({ priorReportCount: 0 })).toBe(true);
expect(getDefaultNotificationPreference({ priorReportCount: 1 })).toBe(false);
});

it("decorates listed reports with their notification default", async () => {
const { reports } = await listReports();
expect(reports.find((report) => report.id === "R-5521").notifyReporterByDefault).toBe(true);
expect(reports.find((report) => report.id === "R-5490").notifyReporterByDefault).toBe(false);
});

it("dismisses a report without queuing a notification when disabled", async () => {
const { report, notification } = await dismissReport({
reportId: "R-5521",
reason: "insufficient-evidence",
notifyReporter: false,
});

expect(report.status).toBe("dismissed");
expect(report.dismissalReason).toBe("insufficient-evidence");
expect(report.notifyReporter).toBe(false);
expect(notification).toBeNull();
});

it("returns the courtesy notification stub when enabled", async () => {
const { report, notification } = await dismissReport({
reportId: "R-5490",
reason: "duplicate",
notifyReporter: true,
});

expect(report.notifyReporter).toBe(true);
expect(notification).toMatchObject({
status: "stubbed",
queued: false,
reportId: "R-5490",
template: "report-reviewed",
});
});

it("rejects missing or unsupported dismissal reasons", async () => {
await expect(dismissReport({ reportId: "R-5521" })).rejects.toThrow(
"Choose a valid dismissal reason"
);
await expect(
dismissReport({ reportId: "R-5521", reason: "not-a-reason" })
).rejects.toThrow("Choose a valid dismissal reason");
});
});
2 changes: 1 addition & 1 deletion __tests__/library/bookProgress.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
progressPercent,
readBookProgress,
saveBookProgress,
} from "@/app/dashboard/library/read/[bookid]/bookProgress";
} from "@/app/[locale]/dashboard/library/read/[bookid]/bookProgress";

beforeEach(() => {
window.localStorage.clear();
Expand Down
2 changes: 1 addition & 1 deletion __tests__/verification/VerificationPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ beforeEach(async () => {
mockPush.mockClear();
mockFetchSignedUrl.mockClear();
if (!_VerificationPage) {
const mod = await import("@/app/account/verification/page");
const mod = await import("@/app/[locale]/account/verification/page");
_VerificationPage = mod.default;
}
});
Expand Down
6 changes: 5 additions & 1 deletion app/[locale]/(pages)/(auth)/login/page.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import LoginForm from '@/components/organisms/auth/login-form'
import ForgetPassword from '@/components/organisms/auth/forget-password'
import { GalleryVerticalEnd } from 'lucide-react'
import React from 'react'
import React, { Suspense } from 'react'
import Image from "next/image"
import Link from 'next/link'
import { siteUrl, siteName } from "@/lib/config/site.config"
import SessionEndedNotice from "@/components/auth/SessionEndedNotice"

export const metadata = {
title: { absolute: "Sign in | Deen Bridge" },
Expand Down Expand Up @@ -43,6 +44,9 @@ const page = () => {
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-xs">
<Suspense fallback={null}>
<SessionEndedNotice />
</Suspense>
<LoginForm />
</div>
</div>
Expand Down
12 changes: 6 additions & 6 deletions app/[locale]/admin/audit-logs/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,9 @@ export default function AuditLogsPage() {
<div className="grid gap-4 md:grid-cols-3">
{/* Actor Filter */}
<div className="space-y-2">
<label className={cn(poppins_500.className, "text-sm")}>Admin Actor</label>
<label htmlFor="audit-actor-filter" className={cn(poppins_500.className, "text-sm")}>Admin Actor</label>
<Select value={actorFilter} onValueChange={setActorFilter}>
<SelectTrigger>
<SelectTrigger id="audit-actor-filter">
<SelectValue placeholder="Select actor" />
</SelectTrigger>
<SelectContent>
Expand All @@ -243,9 +243,9 @@ export default function AuditLogsPage() {

{/* Category Filter */}
<div className="space-y-2">
<label className={cn(poppins_500.className, "text-sm")}>Action Category</label>
<label htmlFor="audit-category-filter" className={cn(poppins_500.className, "text-sm")}>Action Category</label>
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger>
<SelectTrigger id="audit-category-filter">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
Expand All @@ -264,10 +264,10 @@ export default function AuditLogsPage() {

{/* Date Range */}
<div className="space-y-2">
<label className={cn(poppins_500.className, "text-sm")}>Date Range</label>
<label htmlFor="audit-date-range" className={cn(poppins_500.className, "text-sm")}>Date Range</label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal">
<Button id="audit-date-range" variant="outline" className="w-full justify-start text-left font-normal">
<CalendarIcon className="mr-2 h-4 w-4" />
{dateRange.from ? (
dateRange.to ? (
Expand Down
4 changes: 2 additions & 2 deletions app/[locale]/admin/reconciliation/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,10 @@ export default function PayoutReconciliationPage() {
<CardContent>
<div className="flex flex-wrap gap-4 items-end">
<div className="space-y-2">
<label className={cn(poppins_500.className, "text-sm")}>Date Range</label>
<label htmlFor="reconciliation-date-range" className={cn(poppins_500.className, "text-sm")}>Date Range</label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-[280px] justify-start text-left font-normal">
<Button id="reconciliation-date-range" variant="outline" className="w-[280px] justify-start text-left font-normal">
<CalendarIcon className="mr-2 h-4 w-4" />
{dateRange.from ? (
dateRange.to ? (
Expand Down
Loading