Skip to content

Commit b8c8a36

Browse files
Merge pull request #36 from ani1609/tests
Tests
2 parents 6b7ebc1 + 6cd55f9 commit b8c8a36

18 files changed

Lines changed: 3832 additions & 2975 deletions

β€Ž.github/workflows/test.ymlβ€Ž

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Tests
2+
3+
on:
4+
pull_request:
5+
branches: ["*"]
6+
push:
7+
branches: [main, master]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Setup pnpm
18+
uses: pnpm/action-setup@v4
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: 20
24+
cache: pnpm
25+
26+
- name: Install dependencies
27+
run: pnpm install --frozen-lockfile
28+
29+
- name: Run tests
30+
run: pnpm test

β€Ž.husky/pre-commitβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pnpm exec lint-staged

β€Žapp/api/generate-mock-shipment-status/route.tsβ€Ž

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { connectToDatabase } from "@/lib/mongodb";
44
import MockStatus from "@/models/mock-status";
55
import { faker } from "@faker-js/faker";
66
import { Event, ShipmentStatusResponse } from "@/types/shipment";
7+
import { resolveMockDeliveryDate } from "@/lib/mock-status-logic";
78

89
const MONTHS = [
910
"Jan",
@@ -49,19 +50,25 @@ export async function POST(req: Request) {
4950
}));
5051

5152
if (!mockStatus) {
52-
// Create new document if tracking ID is not found
5353
mockStatus = new MockStatus({
5454
trackingId,
55-
expectedDeliveryDate: expectedDeliveryDate ?? fakeFutureDate(),
55+
expectedDeliveryDate: resolveMockDeliveryDate({
56+
exists: false,
57+
storedDeliveryDate: null,
58+
providedDeliveryDate: expectedDeliveryDate,
59+
generatedDeliveryDate: fakeFutureDate(),
60+
}),
5661
events: newEvents,
5762
});
5863
} else {
5964
// Prepend new events to the beginning of the array (that is how bluedart works)
6065
mockStatus.events.unshift(...newEvents);
61-
// Update delivery date if explicitly provided, otherwise keep existing
62-
if (expectedDeliveryDate !== undefined) {
63-
mockStatus.expectedDeliveryDate = expectedDeliveryDate;
64-
}
66+
mockStatus.expectedDeliveryDate = resolveMockDeliveryDate({
67+
exists: true,
68+
storedDeliveryDate: mockStatus.expectedDeliveryDate,
69+
providedDeliveryDate: expectedDeliveryDate,
70+
generatedDeliveryDate: fakeFutureDate(),
71+
});
6572
}
6673

6774
await mockStatus.save();

β€Žapp/api/shipment-monitoring/route.tsβ€Ž

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import {
77
import { connectToDatabase } from "@/lib/mongodb";
88
import { NextResponse } from "next/server";
99
import Shipment from "@/models/shipment";
10+
import { resolveMonitoringAction } from "@/lib/monitoring-logic";
11+
import {
12+
buildNewEventsMessage,
13+
buildDateChangedMessage,
14+
} from "@/lib/message-builders";
1015

1116
export async function GET() {
1217
try {
@@ -31,55 +36,47 @@ export async function GET() {
3136
const latestExpectedDeliveryDate =
3237
latestShipmentStatusData.data.expectedDeliveryDate;
3338
const currentExpectedDeliveryDate = shipmentFromDb.expectedDeliveryDate;
34-
const deliveryDateChanged =
35-
latestExpectedDeliveryDate !== currentExpectedDeliveryDate;
3639

37-
const hasNewEvents =
38-
latestShipmentStatusData.data.events.length >
39-
shipmentFromDb.events.length;
40+
const action = resolveMonitoringAction({
41+
storedEvents: shipmentFromDb.events,
42+
storedDeliveryDate: currentExpectedDeliveryDate,
43+
latestEvents: latestShipmentStatusData.data.events,
44+
latestDeliveryDate: latestExpectedDeliveryDate,
45+
});
4046

41-
if (hasNewEvents) {
42-
const newEvents = latestShipmentStatusData.data.events.slice(
43-
0,
44-
latestShipmentStatusData.data.events.length -
45-
shipmentFromDb.events.length,
46-
);
47+
if (action.type === "new_events") {
48+
const { newEvents, updatedDeliveryDate } = action;
4749

4850
shipmentFromDb.events = [...newEvents, ...shipmentFromDb.events];
49-
shipmentFromDb.expectedDeliveryDate = latestExpectedDeliveryDate;
50-
51-
const message =
52-
`\n━━━━━━━━━━━━━━━━━━━━━\n` +
53-
`πŸ“¦ **Shipment Update!** πŸ“¦\n\n` +
54-
`🚚 Your shipment **${shipmentFromDb.title}** has new events:\n\n` +
55-
`πŸ“ **Location:** ${newEvents[0].location}\n` +
56-
`πŸ“ **Details:** ${newEvents[0].details}\n` +
57-
`πŸ“… **Date:** ${newEvents[0].date}\n` +
58-
`⏰ **Time:** ${newEvents[0].time}\n` +
59-
(latestExpectedDeliveryDate
60-
? `πŸ“† **Expected Delivery:** ${latestExpectedDeliveryDate}\n`
61-
: "");
51+
// Never overwrite a known date with null (scraping fluke guard)
52+
if (updatedDeliveryDate !== null) {
53+
shipmentFromDb.expectedDeliveryDate = updatedDeliveryDate;
54+
}
55+
56+
const message = buildNewEventsMessage({
57+
trackingId: shipmentFromDb.trackingId,
58+
title: shipmentFromDb.title,
59+
latestEvent: newEvents[0],
60+
updatedDeliveryDate,
61+
});
6262

6363
await sendMessage({
6464
userDiscordId: shipmentFromDb.userDiscordId,
6565
message,
6666
});
6767

6868
await shipmentFromDb.save();
69-
} else if (deliveryDateChanged) {
70-
// No new scan events, but delivery date changed (or became available for the first time)
71-
shipmentFromDb.expectedDeliveryDate = latestExpectedDeliveryDate;
72-
73-
const message =
74-
`\n━━━━━━━━━━━━━━━━━━━━━\n` +
75-
`πŸ“¦ **Shipment Update!** πŸ“¦\n\n` +
76-
`🚚 Your shipment **${shipmentFromDb.title}** has an updated delivery date:\n\n` +
77-
(currentExpectedDeliveryDate
78-
? `πŸ“† **Previous Delivery Date:** ${currentExpectedDeliveryDate}\n`
79-
: "") +
80-
(latestExpectedDeliveryDate
81-
? `πŸ“† **New Expected Delivery:** ${latestExpectedDeliveryDate}\n`
82-
: `πŸ“† **Expected Delivery Date:** Not available yet\n`);
69+
} else if (action.type === "date_changed") {
70+
// Delivery date changed or became available for the first time
71+
const { previousDate, updatedDeliveryDate } = action;
72+
shipmentFromDb.expectedDeliveryDate = updatedDeliveryDate;
73+
74+
const message = buildDateChangedMessage({
75+
trackingId: shipmentFromDb.trackingId,
76+
title: shipmentFromDb.title,
77+
previousDate,
78+
updatedDeliveryDate,
79+
});
8380

8481
await sendMessage({
8582
userDiscordId: shipmentFromDb.userDiscordId,

β€Žapp/api/shipment/[trackingId]/route.tsβ€Ž

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
handleResourceNotFoundError,
88
} from "@/lib/utils";
99
import { ShipmentResponse } from "@/types/shipment";
10+
import { validateAuthToken } from "@/lib/shipment-validators";
1011

1112
export async function GET(
1213
req: Request,
@@ -41,15 +42,14 @@ export async function DELETE(
4142
await connectToDatabase();
4243

4344
// Authenticate request
44-
const authHeader = req.headers.get("authorization");
45-
const token = authHeader?.replace("Bearer ", "");
46-
47-
if (!token) {
48-
return handleAuthError("Auth token missing");
49-
}
50-
51-
if (token !== process.env.AUTH_TOKEN) {
52-
return handleAuthError("Invalid auth token");
45+
const authResult = validateAuthToken(
46+
req.headers.get("authorization"),
47+
process.env.AUTH_TOKEN,
48+
);
49+
if (!authResult.valid) {
50+
return authResult.reason === "missing"
51+
? handleAuthError("Auth token missing")
52+
: handleAuthError("Invalid auth token");
5353
}
5454

5555
const { trackingId } = await params;

β€Žapp/api/shipment/route.tsβ€Ž

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import {
1313
ShipmentsResponse,
1414
} from "@/types/shipment";
1515
import { fetchShipmentStatus, sendMessage } from "@/lib/utils";
16+
import {
17+
validateAuthToken,
18+
validateAddShipmentBody,
19+
} from "@/lib/shipment-validators";
20+
import { buildAddShipmentMessage } from "@/lib/message-builders";
1621

1722
export async function GET() {
1823
try {
@@ -36,22 +41,21 @@ export async function POST(req: Request) {
3641
await connectToDatabase();
3742

3843
// Authenticate request
39-
const authHeader = req.headers.get("authorization");
40-
const token = authHeader?.replace("Bearer ", "");
41-
42-
if (!token) {
43-
return handleAuthError("Auth token missing");
44-
}
45-
46-
if (token !== process.env.AUTH_TOKEN) {
47-
return handleAuthError("Invalid auth token");
44+
const authResult = validateAuthToken(
45+
req.headers.get("authorization"),
46+
process.env.AUTH_TOKEN,
47+
);
48+
if (!authResult.valid) {
49+
return authResult.reason === "missing"
50+
? handleAuthError("Auth token missing")
51+
: handleAuthError("Invalid auth token");
4852
}
4953

5054
const body: AddShipmentRequest = await req.json();
5155
const { title, trackingId, userDiscordId } = body;
5256

5357
// Validate input
54-
if (!trackingId || !userDiscordId || !title) {
58+
if (!validateAddShipmentBody({ title, trackingId, userDiscordId })) {
5559
return handleMissingParamsError(
5660
"Missing required fields (trackingId, userDiscordId, title)",
5761
);
@@ -86,16 +90,12 @@ export async function POST(req: Request) {
8690
}
8791

8892
// Notify user on Discord
89-
const message =
90-
`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
91-
`πŸš€ **Shipment Tracking Activated!** πŸš€\n\n` +
92-
`Your shipment with tracking ID **${trackingId}** has been added for event alerts!\n\n` +
93-
`Expected Delivery Date: ${shipmentStatus.data.expectedDeliveryDate || "Not specified"}\n\n` +
94-
`The latest event for your shipment **${title}** is:\n` +
95-
`πŸ“ **Location:** ${shipmentStatus.data.events[0].location}\n` +
96-
`πŸ“ **Details:** ${shipmentStatus.data.events[0].details}\n` +
97-
`πŸ“… **Date:** ${shipmentStatus.data.events[0].date}\n` +
98-
`⏰ **Time:** ${shipmentStatus.data.events[0].time}`;
93+
const message = buildAddShipmentMessage({
94+
trackingId,
95+
title,
96+
expectedDeliveryDate: shipmentStatus.data.expectedDeliveryDate,
97+
latestEvent: shipmentStatus.data.events[0],
98+
});
9999

100100
const discordMessageSent = await sendMessage({
101101
userDiscordId,

β€Žlib/message-builders.tsβ€Ž

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
interface Event {
2+
location: string;
3+
details: string;
4+
date: string;
5+
time: string;
6+
}
7+
8+
const SEP = `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
9+
10+
export function buildAddShipmentMessage({
11+
trackingId,
12+
title,
13+
expectedDeliveryDate,
14+
latestEvent,
15+
}: {
16+
trackingId: string;
17+
title: string;
18+
expectedDeliveryDate: string | null;
19+
latestEvent: Event;
20+
}): string {
21+
return (
22+
SEP +
23+
`πŸš€ **Shipment Tracking Activated!** πŸš€\n\n` +
24+
`Your shipment **${title}** with tracking ID **${trackingId}** has been added for event alerts!\n\n` +
25+
`Expected Delivery Date: ${expectedDeliveryDate || "Not specified"}\n\n` +
26+
`The latest event for your shipment is:\n` +
27+
`πŸ“ **Location:** ${latestEvent.location}\n` +
28+
`πŸ“ **Details:** ${latestEvent.details}\n` +
29+
`πŸ“… **Date:** ${latestEvent.date}\n` +
30+
`⏰ **Time:** ${latestEvent.time}` +
31+
SEP
32+
);
33+
}
34+
35+
export function buildNewEventsMessage({
36+
trackingId,
37+
title,
38+
latestEvent,
39+
updatedDeliveryDate,
40+
}: {
41+
trackingId: string;
42+
title: string;
43+
latestEvent: Event;
44+
updatedDeliveryDate: string | null;
45+
}): string {
46+
return (
47+
SEP +
48+
`πŸ“¦ **Shipment Update!** πŸ“¦\n\n` +
49+
`🚚 Your shipment **${title}** (${trackingId}) has new events:\n\n` +
50+
`πŸ“ **Location:** ${latestEvent.location}\n` +
51+
`πŸ“ **Details:** ${latestEvent.details}\n` +
52+
`πŸ“… **Date:** ${latestEvent.date}\n` +
53+
`⏰ **Time:** ${latestEvent.time}\n` +
54+
(updatedDeliveryDate
55+
? `πŸ“† **Expected Delivery:** ${updatedDeliveryDate}\n`
56+
: "") +
57+
SEP
58+
);
59+
}
60+
61+
export function buildDateChangedMessage({
62+
trackingId,
63+
title,
64+
previousDate,
65+
updatedDeliveryDate,
66+
}: {
67+
trackingId: string;
68+
title: string;
69+
previousDate: string | null;
70+
updatedDeliveryDate: string;
71+
}): string {
72+
return (
73+
SEP +
74+
`πŸ“¦ **Shipment Update!** πŸ“¦\n\n` +
75+
`🚚 Your shipment **${title}** (${trackingId}) has an updated delivery date:\n\n` +
76+
(previousDate ? `πŸ“† **Previous Delivery Date:** ${previousDate}\n` : "") +
77+
`πŸ“† **New Expected Delivery:** ${updatedDeliveryDate}\n` +
78+
SEP
79+
);
80+
}

β€Žlib/mock-status-logic.tsβ€Ž

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Pure logic for resolving the expected delivery date in the
3+
* generate-mock-shipment-status route (create vs update branching).
4+
*/
5+
export function resolveMockDeliveryDate({
6+
exists,
7+
storedDeliveryDate,
8+
providedDeliveryDate,
9+
generatedDeliveryDate,
10+
}: {
11+
exists: boolean;
12+
storedDeliveryDate: string | null;
13+
providedDeliveryDate: string | undefined;
14+
generatedDeliveryDate: string;
15+
}): string | null {
16+
if (!exists) {
17+
// Create: use explicitly provided value, fall back to a generated future date
18+
return providedDeliveryDate ?? generatedDeliveryDate;
19+
}
20+
// Update: only overwrite when caller explicitly passes a value (even null)
21+
return providedDeliveryDate !== undefined
22+
? providedDeliveryDate
23+
: storedDeliveryDate;
24+
}

0 commit comments

Comments
Β (0)