Skip to content

Commit 095237c

Browse files
committed
add more unit tests to ensure the timezone handling works as expected
1 parent ec56385 commit 095237c

File tree

2 files changed

+375
-0
lines changed

2 files changed

+375
-0
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { PGlite } from "@electric-sql/pglite"
2+
import { drizzle } from "drizzle-orm/pglite"
3+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
4+
5+
import { DrizzleQueueAdapter } from "~/index"
6+
import * as schema from "~/schema"
7+
8+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/consistent-type-imports
9+
const { pushSchema } = require("drizzle-kit/api") as typeof import("drizzle-kit/api")
10+
11+
describe("Timezone Edge Cases", () => {
12+
let db: ReturnType<typeof drizzle<typeof schema>>
13+
let adapter: DrizzleQueueAdapter
14+
let client: PGlite
15+
16+
beforeEach(async () => {
17+
client = new PGlite()
18+
db = drizzle(client, { schema })
19+
20+
const { apply } = await pushSchema(schema, db as never)
21+
await apply()
22+
23+
adapter = new DrizzleQueueAdapter(db, "timezone-edge-test")
24+
await adapter.connect()
25+
})
26+
27+
afterEach(async () => {
28+
await adapter.disconnect()
29+
await client.close()
30+
})
31+
32+
it("should handle dates with explicit timezone offsets", async () => {
33+
// User passes date with timezone offset
34+
const dateWithOffset = new Date("2024-01-15T14:00:00+02:00") // 2 PM in +2 timezone = 12 PM UTC
35+
36+
const job = await adapter.addJob({
37+
name: "offset-test",
38+
payload: { test: "data" },
39+
status: "pending",
40+
priority: 2,
41+
attempts: 0,
42+
maxAttempts: 3,
43+
processAt: dateWithOffset,
44+
progress: 0,
45+
repeatCount: 0,
46+
})
47+
48+
// Should store the UTC equivalent (12 PM UTC, not 2 PM UTC)
49+
expect(job.processAt.toISOString()).toBe("2024-01-15T12:00:00.000Z")
50+
expect(job.processAt.getTime()).toBe(dateWithOffset.getTime())
51+
})
52+
53+
it("should handle negative timezone offsets", async () => {
54+
// User passes date with negative timezone offset
55+
const dateWithNegativeOffset = new Date("2024-01-15T14:00:00-05:00") // 2 PM EST = 7 PM UTC
56+
57+
const job = await adapter.addJob({
58+
name: "negative-offset-test",
59+
payload: { test: "data" },
60+
status: "pending",
61+
priority: 2,
62+
attempts: 0,
63+
maxAttempts: 3,
64+
processAt: dateWithNegativeOffset,
65+
progress: 0,
66+
repeatCount: 0,
67+
})
68+
69+
// Should store the UTC equivalent (7 PM UTC)
70+
expect(job.processAt.toISOString()).toBe("2024-01-15T19:00:00.000Z")
71+
expect(job.processAt.getTime()).toBe(dateWithNegativeOffset.getTime())
72+
})
73+
74+
it("should handle server in different timezone", async () => {
75+
// Simulate server running in different timezone by creating dates in different ways
76+
const utcDate = new Date("2024-01-15T14:00:00Z") // Explicit UTC
77+
const localDate = new Date("2024-01-15T14:00:00") // Local time (depends on server timezone)
78+
79+
const utcJob = await adapter.addJob({
80+
name: "utc-job",
81+
payload: { type: "utc" },
82+
status: "pending",
83+
priority: 2,
84+
attempts: 0,
85+
maxAttempts: 3,
86+
processAt: utcDate,
87+
progress: 0,
88+
repeatCount: 0,
89+
})
90+
91+
const localJob = await adapter.addJob({
92+
name: "local-job",
93+
payload: { type: "local" },
94+
status: "pending",
95+
priority: 2,
96+
attempts: 0,
97+
maxAttempts: 3,
98+
processAt: localDate,
99+
progress: 0,
100+
repeatCount: 0,
101+
})
102+
103+
// UTC date should be stored exactly as provided
104+
expect(utcJob.processAt.toISOString()).toBe("2024-01-15T14:00:00.000Z")
105+
106+
// Local date gets stored as whatever the server interprets it as
107+
// This is the key test - we store whatever Date object represents in UTC
108+
expect(localJob.processAt).toBeInstanceOf(Date)
109+
expect(localJob.processAt.getTime()).toBe(localDate.getTime())
110+
})
111+
112+
it("should handle daylight saving time transitions", async () => {
113+
// Spring forward: March 10, 2024 in America/New_York
114+
const springForwardDate = new Date("2024-03-10T07:00:00Z") // 2 AM EST becomes 3 AM EDT
115+
116+
// Fall back: November 3, 2024 in America/New_York
117+
const fallBackDate = new Date("2024-11-03T06:00:00Z") // 2 AM EDT becomes 1 AM EST
118+
119+
const springJob = await adapter.addJob({
120+
name: "spring-job",
121+
payload: { dst: "spring" },
122+
status: "pending",
123+
priority: 2,
124+
attempts: 0,
125+
maxAttempts: 3,
126+
processAt: springForwardDate,
127+
progress: 0,
128+
repeatCount: 0,
129+
})
130+
131+
const fallJob = await adapter.addJob({
132+
name: "fall-job",
133+
payload: { dst: "fall" },
134+
status: "pending",
135+
priority: 2,
136+
attempts: 0,
137+
maxAttempts: 3,
138+
processAt: fallBackDate,
139+
progress: 0,
140+
repeatCount: 0,
141+
})
142+
143+
// Both should store exact UTC timestamps regardless of DST
144+
expect(springJob.processAt.toISOString()).toBe("2024-03-10T07:00:00.000Z")
145+
expect(fallJob.processAt.toISOString()).toBe("2024-11-03T06:00:00.000Z")
146+
})
147+
148+
it("should handle various date formats consistently", async () => {
149+
const testCases = [
150+
{
151+
name: "ISO with Z",
152+
date: new Date("2024-01-15T14:00:00Z"),
153+
expected: "2024-01-15T14:00:00.000Z"
154+
},
155+
{
156+
name: "ISO with +00:00",
157+
date: new Date("2024-01-15T14:00:00+00:00"),
158+
expected: "2024-01-15T14:00:00.000Z"
159+
},
160+
{
161+
name: "ISO with +02:00",
162+
date: new Date("2024-01-15T14:00:00+02:00"),
163+
expected: "2024-01-15T12:00:00.000Z" // 2 hours earlier in UTC
164+
},
165+
{
166+
name: "ISO with -05:00",
167+
date: new Date("2024-01-15T14:00:00-05:00"),
168+
expected: "2024-01-15T19:00:00.000Z" // 5 hours later in UTC
169+
},
170+
{
171+
name: "Timestamp",
172+
date: new Date(1705327200000), // 2024-01-15T14:00:00Z
173+
expected: "2024-01-15T14:00:00.000Z"
174+
}
175+
]
176+
177+
for (const testCase of testCases) {
178+
const job = await adapter.addJob({
179+
name: `format-test-${testCase.name}`,
180+
payload: { format: testCase.name },
181+
status: "pending",
182+
priority: 2,
183+
attempts: 0,
184+
maxAttempts: 3,
185+
processAt: testCase.date,
186+
progress: 0,
187+
repeatCount: 0,
188+
})
189+
190+
expect(job.processAt.toISOString()).toBe(testCase.expected)
191+
}
192+
})
193+
194+
it("should maintain timestamp consistency across retrieval", async () => {
195+
// Test that what goes in comes out exactly the same
196+
const originalTimestamp = 1705327200000 // 2024-01-15T14:00:00Z
197+
const originalDate = new Date(originalTimestamp)
198+
199+
const job = await adapter.addJob({
200+
name: "consistency-test",
201+
payload: { test: "data" },
202+
status: "pending",
203+
priority: 2,
204+
attempts: 0,
205+
maxAttempts: 3,
206+
processAt: originalDate,
207+
progress: 0,
208+
repeatCount: 0,
209+
})
210+
211+
// Retrieve the job
212+
const retrievedJob = await adapter.getNextJob()
213+
214+
expect(retrievedJob).toBeTruthy()
215+
expect(retrievedJob?.processAt.getTime()).toBe(originalTimestamp)
216+
expect(retrievedJob?.processAt.toISOString()).toBe(originalDate.toISOString())
217+
})
218+
})
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { PGlite } from "@electric-sql/pglite"
2+
import { drizzle } from "drizzle-orm/pglite"
3+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
4+
5+
import { DrizzleQueueAdapter } from "~/index"
6+
import * as schema from "~/schema"
7+
8+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/consistent-type-imports
9+
const { pushSchema } = require("drizzle-kit/api") as typeof import("drizzle-kit/api")
10+
11+
describe("UTC Storage Verification", () => {
12+
let db: ReturnType<typeof drizzle<typeof schema>>
13+
let adapter: DrizzleQueueAdapter
14+
let client: PGlite
15+
16+
beforeEach(async () => {
17+
client = new PGlite()
18+
db = drizzle(client, { schema })
19+
20+
const { apply } = await pushSchema(schema, db as never)
21+
await apply()
22+
23+
adapter = new DrizzleQueueAdapter(db, "utc-test-queue")
24+
await adapter.connect()
25+
})
26+
27+
afterEach(async () => {
28+
await adapter.disconnect()
29+
await client.close()
30+
})
31+
32+
it("should store all timestamps as UTC in database", async () => {
33+
const now = new Date()
34+
35+
// Add job with timezone context
36+
const job = await adapter.addJob({
37+
name: "timezone-test",
38+
payload: { test: "data" },
39+
status: "pending",
40+
priority: 2,
41+
attempts: 0,
42+
maxAttempts: 3,
43+
processAt: new Date("2024-01-15T14:00:00Z"), // 9 AM EST = 2 PM UTC
44+
progress: 0,
45+
cron: "0 9 * * *",
46+
repeatEvery: undefined,
47+
repeatLimit: undefined,
48+
repeatCount: 0,
49+
})
50+
51+
// Verify the job was stored
52+
expect(job.processAt).toBeInstanceOf(Date)
53+
54+
// The key test: processAt should be the exact UTC time we provided
55+
expect(job.processAt.toISOString()).toBe("2024-01-15T14:00:00.000Z")
56+
57+
// Verify no timezone field exists
58+
expect(job).not.toHaveProperty("timezone")
59+
60+
// Verify createdAt is also UTC
61+
expect(job.createdAt.getTimezoneOffset()).toBe(new Date().getTimezoneOffset()) // Should be system UTC
62+
})
63+
64+
it("should handle cron jobs with timezone conversion at creation", async () => {
65+
// This simulates what happens when user adds a cron job with timezone
66+
const processAt = new Date("2024-01-15T14:00:00Z") // Pre-converted to UTC
67+
68+
const job = await adapter.addJob({
69+
name: "cron-timezone-test",
70+
payload: { timezone: "America/New_York" }, // Timezone info in payload only
71+
status: "delayed",
72+
priority: 2,
73+
attempts: 0,
74+
maxAttempts: 3,
75+
processAt, // Already UTC
76+
progress: 0,
77+
cron: "0 9 * * *", // Original cron expression
78+
repeatEvery: undefined,
79+
repeatLimit: undefined,
80+
repeatCount: 0,
81+
})
82+
83+
// Database should store exact UTC timestamp
84+
expect(job.processAt.toISOString()).toBe("2024-01-15T14:00:00.000Z")
85+
expect(job.cron).toBe("0 9 * * *")
86+
expect(job).not.toHaveProperty("timezone")
87+
})
88+
89+
it("should retrieve jobs with UTC timestamps", async () => {
90+
// Add a job
91+
await adapter.addJob({
92+
name: "retrieval-test",
93+
payload: { test: "data" },
94+
status: "pending",
95+
priority: 2,
96+
attempts: 0,
97+
maxAttempts: 3,
98+
processAt: new Date("2024-01-15T14:00:00Z"),
99+
progress: 0,
100+
repeatCount: 0,
101+
})
102+
103+
// Retrieve the job
104+
const job = await adapter.getNextJob()
105+
106+
expect(job).toBeTruthy()
107+
expect(job?.processAt.toISOString()).toBe("2024-01-15T14:00:00.000Z")
108+
expect(job).not.toHaveProperty("timezone")
109+
})
110+
111+
it("should handle delayed jobs with UTC timestamps", async () => {
112+
const futureUTC = new Date(Date.now() + 3600000) // 1 hour from now in UTC
113+
114+
const job = await adapter.addJob({
115+
name: "delayed-test",
116+
payload: { test: "data" },
117+
status: "delayed",
118+
priority: 2,
119+
attempts: 0,
120+
maxAttempts: 3,
121+
processAt: futureUTC,
122+
progress: 0,
123+
repeatCount: 0,
124+
})
125+
126+
// Should store exact UTC timestamp
127+
expect(job.processAt.getTime()).toBe(futureUTC.getTime())
128+
expect(job.status).toBe("delayed")
129+
})
130+
131+
it("should verify database schema has no timezone columns", async () => {
132+
// This is more of a schema verification test
133+
const job = await adapter.addJob({
134+
name: "schema-test",
135+
payload: { test: "data" },
136+
status: "pending",
137+
priority: 2,
138+
attempts: 0,
139+
maxAttempts: 3,
140+
processAt: new Date(),
141+
progress: 0,
142+
repeatCount: 0,
143+
})
144+
145+
// Verify the job object structure
146+
const jobKeys = Object.keys(job)
147+
expect(jobKeys).not.toContain("timezone")
148+
149+
// Verify required UTC fields exist
150+
expect(jobKeys).toContain("createdAt")
151+
expect(jobKeys).toContain("processAt")
152+
153+
// All date fields should be Date objects (UTC)
154+
expect(job.createdAt).toBeInstanceOf(Date)
155+
expect(job.processAt).toBeInstanceOf(Date)
156+
})
157+
})

0 commit comments

Comments
 (0)