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
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"@hmcts/list-types-common": "workspace:*",
"@hmcts/location": "workspace:*",
"@hmcts/london-administrative-court-daily-cause-list": "workspace:*",
"@hmcts/magistrates-public-list": "workspace:*",
"@hmcts/magistrates-standard-list": "workspace:*",
"@hmcts/public-pages": "workspace:*",
"@hmcts/rcj-standard-daily-cause-list": "workspace:*",
"@hmcts/sjp-press-list": "workspace:*",
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { moduleRoot as familyDailyCauseListModuleRoot } from "@hmcts/family-dail
import { moduleRoot as listTypesCommonModuleRoot } from "@hmcts/list-types-common/config";
import { apiRoutes as locationApiRoutes } from "@hmcts/location/config";
import { moduleRoot as londonAdminModuleRoot } from "@hmcts/london-administrative-court-daily-cause-list/config";
import { moduleRoot as magistratesPublicListModuleRoot } from "@hmcts/magistrates-public-list/config";
import { moduleRoot as magistratesStandardListModuleRoot } from "@hmcts/magistrates-standard-list/config";
import {
apiRoutes as publicPagesApiRoutes,
fileUploadRoutes as publicPagesFileUploadRoutes,
Expand Down Expand Up @@ -102,7 +104,9 @@ export async function createApp(): Promise<Express> {
civilAppealModuleRoot,
adminCourtModuleRoot,
systemAdminModuleRoot,
publicPagesModuleRoot
publicPagesModuleRoot,
magistratesStandardListModuleRoot,
magistratesPublicListModuleRoot
];

await configureGovuk(app, modulePaths, {
Expand Down
213 changes: 213 additions & 0 deletions apps/web/src/pages/(list-types)/magistrates-public-list/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { readFile } from "node:fs/promises";
import { renderMagistratesListData, validateMagistratesPublicList } from "@hmcts/magistrates-public-list";
import { prisma } from "@hmcts/postgres-prisma";
import { canAccessPublicationData, getArtefactById } from "@hmcts/publication";
import type { Request, Response } from "express";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { GET } from "./index.js";

vi.mock("node:fs/promises");
vi.mock("@hmcts/postgres-prisma", () => ({
prisma: {
artefact: {
findUnique: vi.fn()
},
listType: {
findUnique: vi.fn()
}
}
}));
vi.mock("@hmcts/publication", async (importOriginal) => {
const actual = await importOriginal<typeof import("@hmcts/publication")>();
return {
...actual,
getArtefactById: vi.fn(),
canAccessPublicationData: vi.fn()
};
});
vi.mock("@hmcts/magistrates-public-list");

describe("magistrates-public-list controller", () => {
let req: Partial<Request>;
let res: Partial<Response>;

const mockListType = {
id: 21,
allowedProvenance: ["MANUAL_UPLOAD"],
isNonStrategic: false
};

const mockArtefact = {
artefactId: "test-id",
locationId: "1",
listTypeId: 21,
contentDate: new Date("2025-01-13"),
sensitivity: "PUBLIC",
language: "ENGLISH",
displayFrom: new Date("2025-01-13"),
displayTo: new Date("2025-01-20"),
lastReceivedDate: new Date("2025-01-13"),
isFlatFile: false,
provenance: "MANUAL_UPLOAD",
supersededCount: 0,
noMatch: false
} as any;

const mockJsonData = {
document: { publicationDate: "2025-01-13T00:00:00.000Z" },
venue: { venueName: "Test Court", venueAddress: { line: ["Address"], postCode: "AB1 2CD" } },
courtLists: []
};

const mockRenderedData = {
header: { locationName: "Test Court", addressLines: [], contentDate: "13 January 2025", lastUpdated: "13 January 2025" },
openJustice: { venueName: "Test Court", email: "", phone: "" },
listData: mockJsonData
} as any;

beforeEach(() => {
vi.clearAllMocks();
req = { query: {} };
res = {
locals: { locale: "en" },
status: vi.fn().mockReturnThis(),
render: vi.fn()
};
vi.mocked(prisma.listType.findUnique).mockResolvedValue(mockListType as any);
vi.mocked(canAccessPublicationData).mockReturnValue(true);
});

describe("GET", () => {
it("should return 400 when artefactId is missing", async () => {
req.query = {};

await GET(req as Request, res as Response);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.render).toHaveBeenCalledWith(
"errors/common",
expect.objectContaining({
errorTitle: expect.any(String),
errorMessage: expect.any(String)
})
);
});

it("should return 404 when artefact is not found", async () => {
req.query = { artefactId: "nonexistent-id" };
vi.mocked(getArtefactById).mockResolvedValue(null);

await GET(req as Request, res as Response);

expect(getArtefactById).toHaveBeenCalledWith("nonexistent-id");
expect(res.status).toHaveBeenCalledWith(404);
expect(res.render).toHaveBeenCalledWith("errors/common", expect.any(Object));
});

it("should return 404 when JSON file cannot be read", async () => {
req.query = { artefactId: "test-id" };
vi.mocked(getArtefactById).mockResolvedValue(mockArtefact);
vi.mocked(readFile).mockRejectedValue(new Error("File not found"));

await GET(req as Request, res as Response);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.render).toHaveBeenCalledWith("errors/common", expect.any(Object));
});

it("should return 400 when JSON validation fails", async () => {
req.query = { artefactId: "test-id" };
vi.mocked(getArtefactById).mockResolvedValue(mockArtefact);
vi.mocked(readFile).mockResolvedValue(JSON.stringify({ invalid: "data" }));
vi.mocked(validateMagistratesPublicList).mockReturnValue({
isValid: false,
errors: ["Validation error"]
} as any);

await GET(req as Request, res as Response);

expect(validateMagistratesPublicList).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.render).toHaveBeenCalledWith("errors/common", expect.any(Object));
});

it("should successfully render in English", async () => {
req.query = { artefactId: "test-id" };
res.locals = { locale: "en" };
vi.mocked(getArtefactById).mockResolvedValue(mockArtefact);
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockJsonData));
vi.mocked(validateMagistratesPublicList).mockReturnValue({ isValid: true, errors: [] } as any);
vi.mocked(renderMagistratesListData).mockResolvedValue(mockRenderedData);

await GET(req as Request, res as Response);

expect(renderMagistratesListData).toHaveBeenCalledWith(mockJsonData, {
locationId: "1",
contentDate: mockArtefact.contentDate,
locale: "en"
});
const renderCall = vi.mocked(res.render).mock.calls[0];
expect(renderCall[0]).toBe("magistrates-public-list");
expect(renderCall[1]).toMatchObject({ dataSource: "Manual Upload" });
expect(renderCall[1]).toHaveProperty("en");
expect(renderCall[1]).toHaveProperty("cy");
expect(renderCall[1]).toHaveProperty("header");
expect(renderCall[1]).toHaveProperty("openJustice");
expect(renderCall[1]).toHaveProperty("listData");
expect(renderCall[1]).toHaveProperty("t");
});

it("should successfully render in Welsh", async () => {
req.query = { artefactId: "test-id" };
res.locals = { locale: "cy" };
vi.mocked(getArtefactById).mockResolvedValue(mockArtefact);
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockJsonData));
vi.mocked(validateMagistratesPublicList).mockReturnValue({ isValid: true, errors: [] } as any);
vi.mocked(renderMagistratesListData).mockResolvedValue(mockRenderedData);

await GET(req as Request, res as Response);

expect(renderMagistratesListData).toHaveBeenCalledWith(mockJsonData, {
locationId: "1",
contentDate: mockArtefact.contentDate,
locale: "cy"
});
expect(res.render).toHaveBeenCalledWith("magistrates-public-list", expect.any(Object));
});

it("should return 500 when an unexpected error occurs", async () => {
req.query = { artefactId: "test-id" };
vi.mocked(getArtefactById).mockRejectedValue(new Error("Database error"));

await GET(req as Request, res as Response);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.render).toHaveBeenCalledWith("errors/common", expect.any(Object));
});

it("should use provenance label for data source", async () => {
req.query = { artefactId: "test-id" };
vi.mocked(getArtefactById).mockResolvedValue(mockArtefact);
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockJsonData));
vi.mocked(validateMagistratesPublicList).mockReturnValue({ isValid: true, errors: [] } as any);
vi.mocked(renderMagistratesListData).mockResolvedValue(mockRenderedData);

await GET(req as Request, res as Response);

expect(res.render).toHaveBeenCalledWith("magistrates-public-list", expect.objectContaining({ dataSource: "Manual Upload" }));
});

it("should use raw provenance when label not found", async () => {
req.query = { artefactId: "test-id" };
const unknownProvenanceArtefact = { ...mockArtefact, provenance: "UNKNOWN_PROVENANCE" };
vi.mocked(getArtefactById).mockResolvedValue(unknownProvenanceArtefact);
vi.mocked(readFile).mockResolvedValue(JSON.stringify(mockJsonData));
vi.mocked(validateMagistratesPublicList).mockReturnValue({ isValid: true, errors: [] } as any);
vi.mocked(renderMagistratesListData).mockResolvedValue(mockRenderedData);

await GET(req as Request, res as Response);

expect(res.render).toHaveBeenCalledWith("magistrates-public-list", expect.objectContaining({ dataSource: "UNKNOWN_PROVENANCE" }));
});
});
});
126 changes: 126 additions & 0 deletions apps/web/src/pages/(list-types)/magistrates-public-list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
magistratesPublicListCy as cy,
magistratesPublicListEn as en,
renderMagistratesListData,
validateMagistratesPublicList
} from "@hmcts/magistrates-public-list";
import { prisma } from "@hmcts/postgres-prisma";
import { canAccessPublicationData, getArtefactById, type ListType, PROVENANCE_LABELS } from "@hmcts/publication";
import type { Request, Response } from "express";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const MONOREPO_ROOT = path.join(__dirname, "..", "..", "..", "..", "..", "..");
const TEMP_UPLOAD_DIR = path.join(MONOREPO_ROOT, "storage", "temp", "uploads");

export const GET = async (req: Request, res: Response) => {
const locale = res.locals.locale || "en";
const t = locale === "cy" ? cy : en;

const artefactId = req.query.artefactId as string;

if (!artefactId) {
return res.status(400).render("errors/common", {
en,
cy,
errorTitle: t.errorTitle,
errorMessage: t.errorMessage
});
}

try {
const artefact = await getArtefactById(artefactId);

if (!artefact) {
return res.status(404).render("errors/common", {
en,
cy,
errorTitle: t.errorTitle,
errorMessage: t.errorMessage
});
}

const dbListType = await prisma.listType.findUnique({
where: { id: artefact.listTypeId }
});

const listType: ListType | undefined = dbListType
? {
id: dbListType.id,
provenance: dbListType.allowedProvenance,
isNonStrategic: dbListType.isNonStrategic
}
: undefined;

const canAccess = canAccessPublicationData(req.user, artefact, listType);

if (!canAccess) {
return res.status(403).render("errors/403", {
en: {
title: en.error403Title,
message: en.error403Message
},
cy: {
title: cy.error403Title,
message: cy.error403Message
}
});
}

const jsonFilePath = path.join(TEMP_UPLOAD_DIR, `${artefactId}.json`);

let jsonContent: string;
try {
jsonContent = await readFile(jsonFilePath, "utf-8");
} catch {
return res.status(404).render("errors/common", {
en,
cy,
errorTitle: t.errorTitle,
errorMessage: t.errorMessage
});
}

const jsonData = JSON.parse(jsonContent);

const validationResult = validateMagistratesPublicList(jsonData);
if (!validationResult.isValid) {
return res.status(400).render("errors/common", {
en,
cy,
errorTitle: t.errorTitle,
errorMessage: t.errorMessage
});
}

const { header, openJustice, listData } = await renderMagistratesListData(jsonData, {
locationId: artefact.locationId,
contentDate: artefact.contentDate,
locale
});

const dataSource = PROVENANCE_LABELS[artefact.provenance] || artefact.provenance;

res.render("magistrates-public-list", {
en,
cy,
title: t.title,
header,
openJustice,
listData,
dataSource,
t
});
} catch {
return res.status(500).render("errors/common", {
en,
cy,
errorTitle: t.errorTitle,
errorMessage: t.errorMessage
});
}
};
Loading
Loading