Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b82c503
Added general services for GET all and POST
davdwan21 Feb 14, 2026
4174824
Implemented APIs for id specific requests
davdwan21 Feb 15, 2026
546df81
DELETE may be unsafe
davdwan21 Feb 15, 2026
bf5de36
Exception handling and DB - item linkage
davdwan21 Feb 23, 2026
ce2d496
Adjustments to models, services, and general APIs
davdwan21 Feb 26, 2026
f471bb9
filteredGet, lean, and schema enforcement
davdwan21 Mar 2, 2026
ebcdabe
Set up unit tests for route and route id files
davdwan21 Mar 31, 2026
3d490e0
Merge branch 'main' into inventory-service-apis
davdwan21 Apr 8, 2026
0d07570
Roles with updated values
davdwan21 Apr 11, 2026
ddafe09
Moved connections to services instead of APIs
davdwan21 Apr 11, 2026
89145ab
Updated tests to follow updated AudienceValues
davdwan21 Apr 12, 2026
763e662
Removed connect() from lib/db and replaced with connectToDatabase()
davdwan21 Apr 16, 2026
1da47d6
Updated configs and setups to match main
davdwan21 Apr 21, 2026
8e7c3f6
Jest tests pass
davdwan21 Apr 23, 2026
6dc0a34
Updated mongoose file for jest testing
davdwan21 Apr 23, 2026
a474c7d
Added general services for GET all and POST
davdwan21 Feb 14, 2026
cd35349
Implemented APIs for id specific requests
davdwan21 Feb 15, 2026
9318c80
DELETE may be unsafe
davdwan21 Feb 15, 2026
0a35b9f
Exception handling and DB - item linkage
davdwan21 Feb 23, 2026
ca58ada
Adjustments to models, services, and general APIs
davdwan21 Feb 26, 2026
5af3960
filteredGet, lean, and schema enforcement
davdwan21 Mar 2, 2026
13570ff
Set up unit tests for route and route id files
davdwan21 Mar 31, 2026
6b063f6
Roles with updated values
davdwan21 Apr 11, 2026
7d6e149
Moved connections to services instead of APIs
davdwan21 Apr 11, 2026
9936e6a
Updated tests to follow updated AudienceValues
davdwan21 Apr 12, 2026
0a0d147
Removed connect() from lib/db and replaced with connectToDatabase()
davdwan21 Apr 16, 2026
60a0329
Updated configs and setups to match main
davdwan21 Apr 21, 2026
4b00a75
Jest tests pass
davdwan21 Apr 23, 2026
485d6ee
Updated mongoose file for jest testing
davdwan21 Apr 23, 2026
9950154
Merge branch 'main' into inventory-service-apis
arnavjk007 Apr 26, 2026
e900565
Working on frontend, had to resolve merge conflicts
davdwan21 Apr 27, 2026
e2588eb
Merge branch 'inventory-service-apis' of https://github.com/CSES-Dev/…
davdwan21 Apr 27, 2026
e1b96c8
Cleanup config after merge corrupts
davdwan21 Apr 27, 2026
f8232b0
Frontend (marketplace page)
davdwan21 Apr 27, 2026
5575154
Merge branch 'main' into inventory-service-apis
davdwan21 Apr 27, 2026
7c93235
fixing merge conflicts
davdwan21 Apr 27, 2026
34e240d
include test:ci specifically for ci cd pipeline
davdwan21 Apr 27, 2026
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
node_modules/
dist/
.env.local
.env
.env.test
.DS_Store
.env
coverage/
Expand Down
10 changes: 10 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid"
}
12 changes: 12 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.detectIndentation": false,
"[typescript]": {
"editor.tabSize": 4
},
"[typescriptreact]": {
"editor.tabSize": 4
},
"typescript.tsdk": "node_modules/typescript/lib"
}
196 changes: 196 additions & 0 deletions app/api/__tests__/route-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { GET, PUT, DELETE } from "../inventory/[id]/route";
import { getItem, updateItem, deleteItem } from "@/services/items";

// GET by id 200 success
// GET id 404 not found
// PUT 200 by id success
// PUT 400 invalid input
// DELETE 200 success
// DELETE 404 not found failure

jest.mock("@/services/items", () => ({
getItem: jest.fn(),
updateItem: jest.fn(),
deleteItem: jest.fn(),
}));

describe("api/inventory/[id]/", () => {
const mockItem = {
id: "1234abcd1234abcd1234abcd",
labId: "lab1",
name: "Gloves",
category: "consumable",
quantity: 10,
threshold: {
minQuantity: 2,
enabled: true,
lastAlertSentAt: "2026-03-09",
},
notificationPolicy: {
event: "LOW_STOCK",
audience: "LAB_ADMINS",
},
};

beforeEach(() => {
jest.clearAllMocks();
});

describe("GET /api/inventory/[id]/", () => {
it("GET by id returns with item and status 200", async () => {
(getItem as jest.Mock).mockResolvedValue(mockItem);

const request = new Request(
"http://localhost:3000/api/inventory/1234abcd1234abcd1234abcd"
);
const response = await GET(request, {
params: { id: "1234abcd1234abcd1234abcd" },
});

const body = await response.json();

expect(getItem).toHaveBeenCalledWith("1234abcd1234abcd1234abcd");
expect(response.status).toBe(200);
expect(body).toEqual(mockItem);
});

it("GET by id returns with status 404 when item not found", async () => {
(getItem as jest.Mock).mockResolvedValue(null);

const request = new Request(
"http://localhost:3000/api/inventory/22223333444455556666aaaa"
);
const response = await GET(request, {
params: { id: "22223333444455556666aaaa" },
});

const body = await response.json();

expect(getItem).toHaveBeenCalledWith("22223333444455556666aaaa");
expect(response.status).toBe(404);
expect(body).toEqual({
message: "Item not found",
});
});
});

describe("PUT /api/inventory/[id]/", () => {
it("PUT successfully updates item and returns 200", async () => {
const updatedItem = {
...mockItem,
name: "Gloves 2",
quantity: 20,
};

(updateItem as jest.Mock).mockResolvedValue(updatedItem);

const request = new Request(
"http://localhost:3000/api/inventory/1234abcd1234abcd1234abcd",
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Gloves 2",
quantity: 20,
category: "consumable",
threshold: {
minQuantity: 3,
enabled: true,
lastAlertSentAt: "2026-03-09",
},
notificationPolicy: {
event: "LOW_STOCK",
audience: "LAB_MANAGER",
},
}),
}
);

const response = await PUT(request, {
params: { id: "1234abcd1234abcd1234abcd" },
});

const body = await response.json();

expect(updateItem).toHaveBeenCalled();
expect(updateItem).toHaveBeenCalledWith(
"1234abcd1234abcd1234abcd",
expect.objectContaining({
name: "Gloves 2",
quantity: 20,
})
);
expect(response.status).toBe(200);
expect(body).toEqual(updatedItem);
});

it("PUT returns 400 on invalid input", async () => {
const request = new Request(
"http://localhost:3000/api/inventory/1234abcd1234abcd1234abcd",
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
quantity: -10,
}),
}
);

const response = await PUT(request, {
params: { id: "1234abcd1234abcd1234abcd" },
});

const body = await response.json();

expect(updateItem).not.toHaveBeenCalled();
expect(response.status).toBe(400);
expect(body).toHaveProperty("message");
});
});

describe("DELETE /api/inventory/[id]/", () => {
it("DELETE successfully deletes item and returns 200", async () => {
(deleteItem as jest.Mock).mockResolvedValue(mockItem);

const request = new Request(
"http://localhost:3000/api/inventory/1234abcd1234abcd1234abcd",
{ method: "DELETE" }
);

const response = await DELETE(request, {
params: { id: "1234abcd1234abcd1234abcd" },
});

const body = await response.json();

expect(deleteItem).toHaveBeenCalledWith("1234abcd1234abcd1234abcd");
expect(response.status).toBe(200);
expect(body).toEqual(mockItem);
});

it("DELETE returns 404 when item not found", async () => {
(deleteItem as jest.Mock).mockResolvedValue(null);

const request = new Request(
"http://localhost:3000/api/inventory/2234abcd1234abcd1234abcd",
{ method: "DELETE" }
);

const response = await DELETE(request, {
params: { id: "2234abcd1234abcd1234abcd" },
});

const body = await response.json();

expect(deleteItem).toHaveBeenCalledWith("2234abcd1234abcd1234abcd");
expect(response.status).toBe(404);
expect(body).toEqual({
message: "Item not found",
});
});
});
});
161 changes: 161 additions & 0 deletions app/api/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { GET, POST } from "../inventory/route";
import { filteredGet, addItem } from "@/services/items";

// GET 200 success
// GET 500 failure
// POST 201 success
// POST 400 invalid input
// POST 500 server error

jest.mock("@/services/items", () => ({
filteredGet: jest.fn(),
addItem: jest.fn(),
}));

// Helper function to make post tests cleaner
function makePostRequest(body: unknown) {
return new Request("http://localhost:3000/api/inventory", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
}

describe("/api/inventory/", () => {
describe("GET /api/inventory/", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("GET returns items successfully with status 200", async () => {
const mockItems = {
data: [
{ id: "1", name: "Gloves", quantity: 10 },
{ id: "2", name: "Goggles", quantity: 5 },
],
pagination: {
page: 1,
limit: 10,
total: 2,
totalPages: 1,
},
};

(filteredGet as jest.Mock).mockResolvedValue(mockItems);

const response = await GET();
const body = await response.json();

expect(response.status).toBe(200);
expect(body).toEqual(mockItems);

expect(filteredGet).toHaveBeenCalledWith({
page: 1,
limit: 10,
});
});

it("GET returns with status 500 on failure", async () => {
(filteredGet as jest.Mock).mockRejectedValue(
new Error("DB failure")
);

const response = await GET();
const body = await response.json();

expect(response.status).toBe(500);
expect(body).toEqual({ message: "Failed to fetch items" });
});
});

describe("POST /api/inventory/", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("POST returns 201 when item successfully created", async () => {
const validBody = {
labId: "test-lab",
name: "Tubes",
category: "consumable",
quantity: 5,
threshold: {
minQuantity: 2,
enabled: true,
lastAlertSentAt: "2026-03-09",
},
notificationPolicy: {
event: "LOW_STOCK",
audience: "LAB_MANAGER",
},
};

const createdItem = {
id: "1",
...validBody,
};

(addItem as jest.Mock).mockResolvedValue(createdItem);

const request = makePostRequest(validBody);
const response = await POST(request);
const body = await response.json();

expect(response.status).toBe(201);
expect(body).toEqual(createdItem);

expect(addItem).toHaveBeenCalledTimes(1);
});

it("POST returns 400 when item input is invalid", async () => {
const invalidBody = {
labId: "",
};

const request = makePostRequest(invalidBody);
const response = await POST(request);
const body = await response.json();

expect(response.status).toBe(400);
expect(body).toEqual({
success: false,
message: "Invalid request body.",
});

expect(addItem).not.toHaveBeenCalled();
});

it("POST returns 500 when service throws error", async () => {
const validBody = {
labId: "test-lab",
name: "Tubes",
category: "consumable",
quantity: 5,
threshold: {
minQuantity: 2,
enabled: true,
lastAlertSentAt: "2026-03-09",
},
notificationPolicy: {
event: "LOW_STOCK",
audience: "LAB_MANAGER",
},
};

(addItem as jest.Mock).mockRejectedValue(new Error("DB failed"));

const request = makePostRequest(validBody);
const response = await POST(request);
const body = await response.json();

expect(response.status).toBe(500);
expect(body).toEqual({
message: "Error occured while creating item",
});

expect(addItem).toHaveBeenCalledTimes(1);
});
});
});
Loading
Loading