diff --git a/backend/package-lock.json b/backend/package-lock.json
index 10ff6243..a7b8393c 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -42,6 +42,7 @@
"class-validator": "^0.14.3",
"date-fns": "^4.1.0",
"exceljs": "^4.4.0",
+ "helmet": "^8.0.0",
"json2csv": "^6.0.0-alpha.2",
"multer": "^2.0.2",
"nestjs-i18n": "^10.5.1",
@@ -9548,6 +9549,18 @@
"node": ">= 0.4"
}
},
+ "node_modules/helmet": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
+ "integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/EvanHahn"
+ }
+ },
"node_modules/hookified": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
diff --git a/frontend/__tests__/api-client.test.tsx b/frontend/__tests__/api-client.test.tsx
index 82cd9660..bebdbfed 100644
--- a/frontend/__tests__/api-client.test.tsx
+++ b/frontend/__tests__/api-client.test.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-require-imports */
import { api } from '@/lib/api';
import axios from 'axios';
diff --git a/frontend/__tests__/asset-detail-page.test.tsx b/frontend/__tests__/asset-detail-page.test.tsx
new file mode 100644
index 00000000..ec6e3ef8
--- /dev/null
+++ b/frontend/__tests__/asset-detail-page.test.tsx
@@ -0,0 +1,67 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import AssetDetailPage from "@/app/(dashboard)/assets/[id]/page";
+
+const push = jest.fn();
+const asset = {
+ id: "asset-1", assetId: "AST-001", name: "MacBook Pro", status: "ACTIVE", condition: "GOOD",
+ createdAt: "2026-01-01", updatedAt: "2026-01-02", imageUrls: [], tags: [],
+};
+let mockAsset: typeof asset | undefined = asset;
+
+jest.mock("next/navigation", () => ({
+ useParams: () => ({ id: "asset-1" }),
+ useRouter: () => ({ push }),
+}));
+
+jest.mock("@/lib/query/hooks/useAsset", () => ({
+ useAsset: () => ({ data: mockAsset, isLoading: false }),
+ useAssetHistory: () => ({ data: [], isLoading: false }),
+ useAssetDocuments: () => ({ data: [], isLoading: false }),
+ useMaintenanceRecords: () => ({ data: [], isLoading: false }),
+ useAssetNotes: () => ({ data: [], isLoading: false }),
+ useDeleteAsset: () => ({ mutate: jest.fn(), isPending: false }),
+ useUploadDocument: () => ({ mutate: jest.fn(), isPending: false }),
+ useDeleteDocument: () => ({ mutate: jest.fn(), isPending: false }),
+ useCreateMaintenanceRecord: () => ({ mutate: jest.fn(), isPending: false }),
+ useUpdateMaintenanceStatus: () => ({ mutate: jest.fn(), isPending: false }),
+ useCreateNote: () => ({ mutate: jest.fn(), isPending: false }),
+ useDeleteNote: () => ({ mutate: jest.fn(), isPending: false }),
+}));
+
+jest.mock("@/components/assets/status-badge", () => ({ StatusBadge: () => Active }));
+jest.mock("@/components/assets/condition-badge", () => ({ ConditionBadge: () => Good }));
+jest.mock("@/components/assets/inline-edit", () => ({ InlineEdit: ({ display }: { display: React.ReactNode }) => <>{display}> }));
+jest.mock("@/components/assets/photo-gallery", () => ({ PhotoGallery: () =>
Photos
}));
+jest.mock("@/components/assets/edit-asset-modal", () => ({ EditAssetModal: () => Edit Asset Form
}));
+
+describe("AssetDetailPage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockAsset = asset;
+ global.fetch = jest.fn().mockResolvedValue({ ok: false });
+ });
+
+ it("loads the asset and switches across core tabs", () => {
+ render();
+ expect(screen.getByRole("heading", { name: "MacBook Pro" })).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "History" }));
+ expect(screen.getByText("Change History")).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Notes" }));
+ expect(screen.getByRole("heading", { name: "Add Note" })).toBeInTheDocument();
+ });
+
+ it("exposes edit, transfer, and status-change actions", () => {
+ render();
+ expect(screen.getByRole("button", { name: /transfer/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /update status/i })).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Edit" }));
+ expect(screen.getByRole("dialog")).toHaveTextContent("Edit Asset Form");
+ });
+
+ it("renders a not-found state for an invalid asset id", () => {
+ mockAsset = undefined;
+ render();
+ expect(screen.getByText("Asset not found.")).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/licenses-page.test.tsx b/frontend/__tests__/licenses-page.test.tsx
new file mode 100644
index 00000000..a75e7482
--- /dev/null
+++ b/frontend/__tests__/licenses-page.test.tsx
@@ -0,0 +1,51 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import LicensesPage from "@/app/(dashboard)/licenses/page";
+
+const license = {
+ id: "lic-1",
+ name: "Design Suite",
+ vendorId: "Acme",
+ type: "SUBSCRIPTION",
+ billingPeriod: "YEARLY",
+ seatsTotal: 5,
+ seatsUsed: 2,
+ cost: 1200,
+ currency: "USD",
+ expiryDate: "2026-09-01T00:00:00.000Z",
+ renewsSoon: true,
+};
+
+jest.mock("@/lib/query/hooks/useLicenses", () => ({
+ useLicenses: () => ({ data: [license], isLoading: false }),
+ useCreateLicense: () => ({ mutateAsync: jest.fn(), isPending: false }),
+ useUpdateLicense: () => ({ mutateAsync: jest.fn(), isPending: false }),
+ useDeleteLicense: () => ({ mutateAsync: jest.fn(), isPending: false }),
+ useRevealLicenseKey: () => ({ mutateAsync: jest.fn(), isPending: false }),
+ useLicenseAssignments: () => ({ data: [], isLoading: false }),
+ useAssignSeat: () => ({ mutateAsync: jest.fn(), isPending: false }),
+ useUnassignSeat: () => ({ mutate: jest.fn(), isPending: false }),
+}));
+
+jest.mock("@/lib/query/hooks/useAssets", () => ({
+ useUsers: () => ({ data: [{ id: "user-1", name: "Jane User" }] }),
+}));
+
+describe("LicensesPage", () => {
+ it("renders licenses and highlights an upcoming renewal", () => {
+ render();
+
+ expect(screen.getByText("Design Suite")).toBeInTheDocument();
+ expect(screen.getAllByText(/renewal/i).length).toBeGreaterThan(0);
+ expect(screen.getByText(/9\/1\/2026|09\/01\/2026/)).toBeInTheDocument();
+ });
+
+ it("opens the seat assignment UI from a license row", () => {
+ render();
+ fireEvent.click(screen.getByText("Design Suite"));
+
+ expect(screen.getByText("Seat Assignments")).toBeInTheDocument();
+ expect(screen.getByRole("option", { name: "Jane User" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Assign" })).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/locations-page.test.tsx b/frontend/__tests__/locations-page.test.tsx
new file mode 100644
index 00000000..a8618bb7
--- /dev/null
+++ b/frontend/__tests__/locations-page.test.tsx
@@ -0,0 +1,46 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import LocationsPage from "@/app/(dashboard)/locations/page";
+
+jest.mock("next/navigation", () => ({
+ useRouter: () => ({ push: jest.fn() }),
+}));
+
+const createLocation = jest.fn();
+
+jest.mock("@/lib/query/hooks/useLocations", () => ({
+ useLocations: () => ({
+ data: [
+ { id: "root", name: "HQ", code: "HQ", type: "BUILDING", totalAssetCount: 2 },
+ { id: "child", name: "Floor 1", code: "HQ-1", type: "FLOOR", parentLocationId: "root", totalAssetCount: 1 },
+ ],
+ isLoading: false,
+ }),
+ useCreateLocation: () => ({ mutateAsync: createLocation, isPending: false }),
+ useUpdateLocation: () => ({ mutateAsync: jest.fn(), isPending: false }),
+ useDeleteLocation: () => ({ mutateAsync: jest.fn(), isPending: false }),
+}));
+
+describe("LocationsPage", () => {
+ it("renders the location tree and expands child locations", () => {
+ render();
+
+ expect(screen.getByRole("heading", { name: "Locations" })).toBeInTheDocument();
+ expect(screen.getAllByText("HQ").length).toBeGreaterThan(0);
+ expect(screen.queryByText("Floor 1")).not.toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("button", { name: "Expand" }));
+ expect(screen.getByText("Floor 1")).toBeInTheDocument();
+ });
+
+ it("opens create and edit entry points", () => {
+ render();
+
+ fireEvent.click(screen.getByRole("button", { name: "Add Root Location" }));
+ expect(screen.getByRole("heading", { name: "New Location" })).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
+
+ fireEvent.click(screen.getByTitle("Edit location"));
+ expect(screen.getByRole("heading", { name: "Edit Location" })).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/__tests__/maintenance-page.test.tsx b/frontend/__tests__/maintenance-page.test.tsx
new file mode 100644
index 00000000..149a0630
--- /dev/null
+++ b/frontend/__tests__/maintenance-page.test.tsx
@@ -0,0 +1,49 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import MaintenancePage from "@/app/(dashboard)/maintenance/page";
+
+const updateStatus = jest.fn();
+
+jest.mock("@dnd-kit/core", () => ({
+ DndContext: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ DragOverlay: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ PointerSensor: class {},
+ useDroppable: () => ({ setNodeRef: jest.fn(), isOver: false }),
+ useDraggable: () => ({ attributes: {}, listeners: {}, setNodeRef: jest.fn(), transform: null, isDragging: false }),
+ useSensor: jest.fn(),
+ useSensors: jest.fn(),
+}));
+
+jest.mock("@/lib/query/hooks/useMaintenance", () => ({
+ useMaintenanceRecords: () => ({
+ data: [{ id: "mnt-1", assetId: "asset-1", type: "SCHEDULED", status: "SCHEDULED", title: "Quarterly service", scheduledDate: "2099-01-15", cost: 0, currency: "USD" }],
+ isLoading: false,
+ }),
+ useCreateMaintenanceRecord: () => ({ mutateAsync: jest.fn(), isPending: false }),
+ useUpdateMaintenanceStatus: () => ({ mutate: updateStatus, isPending: false }),
+}));
+
+jest.mock("@/lib/query/hooks/useAssets", () => ({
+ useAssets: () => ({ data: { data: [{ id: "asset-1", name: "Laptop" }] } }),
+ useDepartmentsList: () => ({ data: [] }),
+}));
+
+describe("MaintenancePage", () => {
+ it("renders maintenance records and switches to the calendar", () => {
+ render();
+
+ expect(screen.getByRole("heading", { name: "Maintenance" })).toBeInTheDocument();
+ expect(screen.getByText("Quarterly service")).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Calendar" }));
+ expect(screen.getByText("Sun")).toBeInTheDocument();
+ });
+
+ it("opens scheduling from the page and exposes completion entry point", () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "New Maintenance" }));
+ expect(screen.getByRole("heading", { name: "New Maintenance" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Schedule" })).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
+ expect(screen.getByText("Quarterly service")).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js
index d4abab8d..4179f9d5 100644
--- a/frontend/jest.setup.js
+++ b/frontend/jest.setup.js
@@ -1,2 +1,3 @@
// jest.setup.js
+import "@testing-library/jest-dom";
import '@testing-library/jest-dom';
\ No newline at end of file