Skip to content

Commit ce4f382

Browse files
authored
Merge pull request #35 from GTBitsOfGood/alexchen/list-items-query
updated list unclaimed items query (with date filtering)
2 parents 1e317d2 + eba1a8d commit ce4f382

File tree

3 files changed

+236
-23
lines changed

3 files changed

+236
-23
lines changed

src/app/api/unclaimedItems/route.test.ts

+140-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as appHandler from "./route";
33
import { expect, test } from "@jest/globals";
44
// import { authMock } from "@/test/authMock";
55
import { validateSession, invalidateSession } from "@/test/util/authMockUtils";
6-
import { fillDbMockWithManyUnclaimedItems } from "@/test/util/dbMockUtils";
6+
import { fillDbMockWithManyItems } from "@/test/util/dbMockUtils";
77
import { dbMock } from "@/test/dbMock";
88

99
test("Should return 401 for invalid session", async () => {
@@ -36,18 +36,155 @@ test("Should give correct database queries", async () => {
3636
await testApiHandler({
3737
appHandler,
3838
async test({ fetch }) {
39-
await fillDbMockWithManyUnclaimedItems(3);
39+
await fillDbMockWithManyItems(3);
4040
validateSession("ADMIN");
4141

4242
const res = await fetch({ method: "GET" });
4343
await expect(res.status).toBe(200);
4444

4545
// Check that the response json was written correctly
4646
const expectedRet = {
47-
unclaimedItems: await dbMock.unclaimedItem.findMany(),
47+
items: await dbMock.item.findMany(),
4848
};
4949
const json = await res.json();
5050
await expect(json).toEqual(JSON.parse(JSON.stringify(expectedRet))); // Needed to stringify and parse because the expiration field would cause an error because Date != ISOstring
5151
},
5252
});
5353
});
54+
55+
test("Should return 400 on invalid expirationDateAfter", async () => {
56+
await testApiHandler({
57+
appHandler,
58+
requestPatcher(request) {
59+
request.nextUrl.searchParams.set("expirationDateAfter", "foo");
60+
request.nextUrl.searchParams.set(
61+
"expirationDateBefore",
62+
"2025-02-10T20:21:11+00:00"
63+
);
64+
},
65+
async test({ fetch }) {
66+
await fillDbMockWithManyItems(3);
67+
validateSession("ADMIN");
68+
69+
const res = await fetch({ method: "GET" });
70+
await expect(res.status).toBe(400);
71+
await expect(res.json()).resolves.toStrictEqual({
72+
message: "expirationDateAfter must be a valid ISO-8601 timestamp",
73+
});
74+
},
75+
});
76+
});
77+
78+
test("Should return 400 on invalid expirationDateBefore", async () => {
79+
await testApiHandler({
80+
appHandler,
81+
requestPatcher(request) {
82+
request.nextUrl.searchParams.set(
83+
"expirationDateAfter",
84+
"2025-02-10T20:21:11+00:00"
85+
);
86+
request.nextUrl.searchParams.set("expirationDateBefore", "foo");
87+
},
88+
async test({ fetch }) {
89+
await fillDbMockWithManyItems(3);
90+
validateSession("ADMIN");
91+
92+
const res = await fetch({ method: "GET" });
93+
await expect(res.status).toBe(400);
94+
await expect(res.json()).resolves.toStrictEqual({
95+
message: "expirationDateBefore must be a valid ISO-8601 timestamp",
96+
});
97+
},
98+
});
99+
});
100+
101+
test("Should be successful when both expirationDateBefore, expirationDateAfter valid", async () => {
102+
await testApiHandler({
103+
appHandler,
104+
requestPatcher(request) {
105+
request.nextUrl.searchParams.set(
106+
"expirationDateAfter",
107+
"2025-02-10T00:00:00.000Z"
108+
);
109+
request.nextUrl.searchParams.set(
110+
"expirationDateBefore",
111+
"2025-02-14T00:00:00.000Z"
112+
);
113+
},
114+
async test({ fetch }) {
115+
await fillDbMockWithManyItems(3, [
116+
new Date("2025-02-11"),
117+
new Date("2025-02-12"),
118+
new Date("2025-02-13"),
119+
]);
120+
validateSession("ADMIN");
121+
122+
const res = await fetch({ method: "GET" });
123+
await expect(res.status).toBe(200);
124+
125+
const expectedRet = {
126+
items: await dbMock.item.findMany(),
127+
};
128+
const json = await res.json();
129+
await expect(json).toEqual(JSON.parse(JSON.stringify(expectedRet)));
130+
},
131+
});
132+
});
133+
134+
test("Should be successful when expirationDateBefore valid, expirationDateAfter missing", async () => {
135+
await testApiHandler({
136+
appHandler,
137+
requestPatcher(request) {
138+
request.nextUrl.searchParams.set(
139+
"expirationDateBefore",
140+
"2025-02-14T00:00:00.000Z"
141+
);
142+
},
143+
async test({ fetch }) {
144+
await fillDbMockWithManyItems(3, [
145+
new Date("2025-02-11"),
146+
new Date("2025-02-12"),
147+
new Date("2025-02-13"),
148+
]);
149+
validateSession("ADMIN");
150+
151+
const res = await fetch({ method: "GET" });
152+
await expect(res.status).toBe(200);
153+
154+
const expectedRet = {
155+
items: await dbMock.item.findMany(),
156+
};
157+
const json = await res.json();
158+
await expect(json).toEqual(JSON.parse(JSON.stringify(expectedRet)));
159+
},
160+
});
161+
});
162+
163+
test("Should be successful when expirationDateBefore missing, expirationDateAfter valid", async () => {
164+
await testApiHandler({
165+
appHandler,
166+
requestPatcher(request) {
167+
request.nextUrl.searchParams.set(
168+
"expirationDateAfter",
169+
"2025-02-10T00:00:00.000Z"
170+
);
171+
},
172+
async test({ fetch }) {
173+
await fillDbMockWithManyItems(3, [
174+
new Date("2025-02-11"),
175+
new Date("2025-02-12"),
176+
new Date("2025-02-13"),
177+
]);
178+
validateSession("ADMIN");
179+
180+
const res = await fetch({ method: "GET" });
181+
await expect(res.status).toBe(200);
182+
183+
const expectedRet = {
184+
items: await dbMock.item.findMany(),
185+
};
186+
const json = await res.json();
187+
await expect(json).toEqual(JSON.parse(JSON.stringify(expectedRet)));
188+
},
189+
});
190+
});

src/app/api/unclaimedItems/route.ts

+73-12
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,98 @@
1-
// import { NextRequest } from 'next/server';
21
import { auth } from "@/auth";
3-
import { authenticationError } from "@/util/responses";
2+
import { authenticationError, argumentError } from "@/util/responses";
43
import { db } from "@/db";
5-
import { NextResponse } from "next/server";
4+
import { NextRequest, NextResponse } from "next/server";
65

76
// Response for GET /api/unclaimedItems
8-
interface UnclaimedItemsResponse {
9-
unclaimedItems: {
7+
interface ItemsResponse {
8+
items: {
109
id: number;
11-
name: string;
10+
title: string;
11+
category: string;
1212
quantity: number;
1313
expirationDate: Date | null;
14+
unitSize: number;
15+
unitType: string;
16+
datePosted: Date;
17+
lotNumber: number;
18+
donorName: string;
19+
unitPrice: number;
20+
maxRequestLimit: string;
1421
}[];
1522
}
1623

24+
/**
25+
* Takes a date string, validates it, and parses it into a Date object.
26+
* @params dateString: the date string to parse
27+
* @returns undefined if the date string is undefined/null
28+
* @returns null if the date string is defined but invalid
29+
* @returns a Date object if the date string is valid
30+
*/
31+
function parseDateIfDefined(
32+
dateString: string | null
33+
): Date | null | undefined {
34+
// see https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript
35+
if (!dateString) {
36+
return undefined;
37+
}
38+
const date = new Date(dateString);
39+
if (
40+
Object.prototype.toString.call(date) === "[object Date]" &&
41+
!isNaN(date.getTime())
42+
) {
43+
return new Date(dateString);
44+
}
45+
return null;
46+
}
47+
1748
/**
1849
* Handles GET requests to retrieve unclaimed items from the unclaimedItem database.
50+
* Parameters are passed in the URL query string.
51+
* @params expirationDateBefore: ISO-8601 timestamp that returned items expire before
52+
* @params expirationDateAfter: ISO-8601 timestamp that returned items expire after
1953
* @returns 401 if the session is invalid
20-
* @returns 500 if an unknown error occurs
54+
* @returns 400 if expirationDateAfter or expirationDateBefore are invalid ISO-8601 timestamps
2155
* @returns 200 and a json response with the unclaimed items
2256
*/
23-
export async function GET() {
57+
export async function GET(request: NextRequest) {
2458
const session = await auth();
2559
if (!session) return authenticationError("Session required");
2660

2761
if (!session?.user) {
2862
return authenticationError("User not found");
2963
}
3064

31-
// Get all unclaimed items
32-
const unclaimedItems = await db.unclaimedItem.findMany();
65+
const params = request.nextUrl.searchParams;
66+
const expirationDateBefore = parseDateIfDefined(
67+
params.get("expirationDateBefore")
68+
);
69+
const expirationDateAfter = parseDateIfDefined(
70+
params.get("expirationDateAfter")
71+
);
72+
73+
if (expirationDateBefore === null) {
74+
return argumentError(
75+
"expirationDateBefore must be a valid ISO-8601 timestamp"
76+
);
77+
}
78+
79+
if (expirationDateAfter === null) {
80+
return argumentError(
81+
"expirationDateAfter must be a valid ISO-8601 timestamp"
82+
);
83+
}
84+
85+
// Get all unclaimed items that expire after expirationDateAfter and before expirationDateBefore
86+
const items = await db.item.findMany({
87+
where: {
88+
expirationDate: {
89+
gt: expirationDateAfter,
90+
lt: expirationDateBefore,
91+
},
92+
},
93+
});
3394

3495
return NextResponse.json({
35-
unclaimedItems: unclaimedItems,
36-
} as UnclaimedItemsResponse);
96+
items: items,
97+
} as ItemsResponse);
3798
}

src/test/util/dbMockUtils.ts

+23-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { dbMock } from "@/test/dbMock";
2-
import { UnallocatedItemRequest, UnclaimedItem } from "@prisma/client";
2+
import { UnallocatedItemRequest, Item } from "@prisma/client";
33

44
// Helper util methods for testing
55

@@ -9,10 +9,15 @@ import { UnallocatedItemRequest, UnclaimedItem } from "@prisma/client";
99
* @param num Number of items to create
1010
* @returns Array of UnclaimedItems returned by db.unclaimedItem.findMany mock
1111
*/
12-
export async function fillDbMockWithManyUnclaimedItems(
13-
num: number
14-
): Promise<UnclaimedItem[]> {
15-
const items: UnclaimedItem[] = [];
12+
export async function fillDbMockWithManyItems(
13+
num: number,
14+
dates?: Date[]
15+
): Promise<Item[]> {
16+
if (dates && dates.length !== num) {
17+
throw new Error("Number of dates must match number of items");
18+
}
19+
20+
const items: Item[] = [];
1621
const generatedIds = new Set<number>();
1722

1823
for (let i = 0; i < num; i++) {
@@ -25,13 +30,23 @@ export async function fillDbMockWithManyUnclaimedItems(
2530

2631
items.push({
2732
id: id,
28-
name: `Test Item ${id}`,
33+
title: `Test Item ${id}`,
34+
category: `Test Category ${Math.floor(Math.random() * 3)}`,
2935
quantity: Math.floor(Math.random() * 1000),
30-
expirationDate: new Date(Date.now() + Math.floor(Math.random() * 10000)),
36+
expirationDate: dates
37+
? dates[i]
38+
: new Date(Date.now() + Math.floor(Math.random() * 10000)),
39+
unitSize: Math.floor(Math.random() * 100),
40+
unitType: `Unit Type ${Math.floor(Math.random() * 3)}`,
41+
datePosted: new Date(Date.now() + Math.floor(Math.random() * 10000)),
42+
lotNumber: Math.floor(Math.random() * 100),
43+
donorName: "Chris Evans <3",
44+
unitPrice: Math.random() * 100,
45+
maxRequestLimit: "abc",
3146
});
3247
}
3348

34-
dbMock.unclaimedItem.findMany.mockResolvedValue(items);
49+
dbMock.item.findMany.mockResolvedValue(items);
3550
return items;
3651
}
3752

0 commit comments

Comments
 (0)