From 12f436112d111782a28b7d4f66888ff0820c78f6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 06:11:03 +0000 Subject: [PATCH 1/2] Store work entry dates as YYYY-MM-DD instead of epoch milliseconds --- .../__tests__/routes/workEntries.date.test.js | 92 +++++++++++++++++++ backend/src/__tests__/utils/date.test.js | 22 +++++ backend/src/routes/workEntries.js | 5 +- backend/src/utils/date.js | 14 +++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 backend/src/__tests__/routes/workEntries.date.test.js create mode 100644 backend/src/__tests__/utils/date.test.js create mode 100644 backend/src/utils/date.js diff --git a/backend/src/__tests__/routes/workEntries.date.test.js b/backend/src/__tests__/routes/workEntries.date.test.js new file mode 100644 index 00000000..072062c0 --- /dev/null +++ b/backend/src/__tests__/routes/workEntries.date.test.js @@ -0,0 +1,92 @@ +const request = require('supertest'); +const express = require('express'); +const workEntryRoutes = require('../../routes/workEntries'); +const { getDatabase } = require('../../database/init'); + +jest.mock('../../database/init'); +jest.mock('../../middleware/auth', () => ({ + authenticateUser: (req, res, next) => { + req.userEmail = 'test@example.com'; + next(); + } +})); + +const app = express(); +app.use(express.json()); +app.use('/api/work-entries', workEntryRoutes); +// Add error handler for Joi validation +app.use((err, req, res, next) => { + if (err.isJoi) { + return res.status(400).json({ error: 'Validation error' }); + } + res.status(500).json({ error: 'Internal server error' }); +}); + +describe('Work Entry Routes - Date Persistence', () => { + let mockDb; + let consoleErrorSpy; + + beforeEach(() => { + mockDb = { + all: jest.fn(), + get: jest.fn(), + run: jest.fn() + }; + getDatabase.mockReturnValue(mockDb); + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + jest.clearAllMocks(); + }); + + const mockWriteSucceeds = () => { + mockDb.get + .mockImplementationOnce((query, params, callback) => callback(null, { id: 1 })) + .mockImplementationOnce((query, params, callback) => callback(null, { id: 10 })); + mockDb.run.mockImplementation(function (query, params, callback) { + callback.call({ lastID: 10, changes: 1 }, null); + }); + }; + + test('POST should store the date as a YYYY-MM-DD string, not epoch milliseconds', async () => { + mockWriteSucceeds(); + + const response = await request(app) + .post('/api/work-entries') + .send({ clientId: 1, hours: 7.5, date: '2024-03-09' }); + + expect(response.status).toBe(201); + const [, params] = mockDb.run.mock.calls[0]; + expect(params[4]).toBe('2024-03-09'); + }); + + test('POST should truncate a full ISO timestamp to the calendar date', async () => { + mockWriteSucceeds(); + + await request(app) + .post('/api/work-entries') + .send({ clientId: 1, hours: 1, date: '2024-03-09T18:30:00.000Z' }); + + const [, params] = mockDb.run.mock.calls[0]; + expect(params[4]).toBe('2024-03-09'); + }); + + test('PUT should store an updated date as a YYYY-MM-DD string', async () => { + mockDb.get + .mockImplementationOnce((query, params, callback) => callback(null, { id: 5 })) + .mockImplementationOnce((query, params, callback) => callback(null, { id: 5 })); + mockDb.run.mockImplementation(function (query, params, callback) { + callback.call({ changes: 1 }, null); + }); + + const response = await request(app) + .put('/api/work-entries/5') + .send({ date: '2024-03-09' }); + + expect(response.status).toBe(200); + const [, values] = mockDb.run.mock.calls[0]; + expect(values[0]).toBe('2024-03-09'); + }); +}); diff --git a/backend/src/__tests__/utils/date.test.js b/backend/src/__tests__/utils/date.test.js new file mode 100644 index 00000000..54a5b990 --- /dev/null +++ b/backend/src/__tests__/utils/date.test.js @@ -0,0 +1,22 @@ +const { toDateOnly } = require('../../utils/date'); + +describe('toDateOnly', () => { + test('should convert a Date to a YYYY-MM-DD string', () => { + expect(toDateOnly(new Date('2024-01-05T00:00:00.000Z'))).toBe('2024-01-05'); + }); + + test('should drop the time component of a Date', () => { + expect(toDateOnly(new Date('2024-01-05T23:59:59.999Z'))).toBe('2024-01-05'); + }); + + test('should pass through a string unchanged', () => { + expect(toDateOnly('2024-01-05')).toBe('2024-01-05'); + }); + + test.each([ + ['null', null], + ['undefined', undefined] + ])('should pass through %s unchanged', (_label, value) => { + expect(toDateOnly(value)).toBe(value); + }); +}); diff --git a/backend/src/routes/workEntries.js b/backend/src/routes/workEntries.js index 63649d73..cf57c8b5 100644 --- a/backend/src/routes/workEntries.js +++ b/backend/src/routes/workEntries.js @@ -2,6 +2,7 @@ const express = require('express'); const { getDatabase } = require('../database/init'); const { authenticateUser } = require('../middleware/auth'); const { workEntrySchema, updateWorkEntrySchema } = require('../validation/schemas'); +const { toDateOnly } = require('../utils/date'); const router = express.Router(); @@ -104,7 +105,7 @@ router.post('/', (req, res, next) => { // Create work entry db.run( 'INSERT INTO work_entries (client_id, user_email, hours, description, date) VALUES (?, ?, ?, ?, ?)', - [clientId, req.userEmail, hours, description || null, date], + [clientId, req.userEmail, hours, description || null, toDateOnly(date)], function(err) { if (err) { console.error('Database error:', err); @@ -214,7 +215,7 @@ router.put('/:id', (req, res, next) => { if (value.date !== undefined) { updates.push('date = ?'); - values.push(value.date); + values.push(toDateOnly(value.date)); } updates.push('updated_at = CURRENT_TIMESTAMP'); diff --git a/backend/src/utils/date.js b/backend/src/utils/date.js new file mode 100644 index 00000000..7353901c --- /dev/null +++ b/backend/src/utils/date.js @@ -0,0 +1,14 @@ +// Joi coerces validated `date` fields into JS Date objects, which the sqlite3 +// driver binds as epoch milliseconds. Work entry dates are day-granular, so +// normalize them to a YYYY-MM-DD string before they reach the database. +function toDateOnly(value) { + if (value instanceof Date) { + return value.toISOString().split('T')[0]; + } + + return value; +} + +module.exports = { + toDateOnly +}; From 65685e9bc3a215c350de3d90a6f3b2293d62f94f Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:09:20 +0000 Subject: [PATCH 2/2] Apply 1 edit across 1 file --- backend/src/__tests__/utils/date.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/src/__tests__/utils/date.test.js b/backend/src/__tests__/utils/date.test.js index 54a5b990..a9bc882c 100644 --- a/backend/src/__tests__/utils/date.test.js +++ b/backend/src/__tests__/utils/date.test.js @@ -13,6 +13,16 @@ describe('toDateOnly', () => { expect(toDateOnly('2024-01-05')).toBe('2024-01-05'); }); + test('should pass through an epoch-millisecond number unchanged', () => { + expect(toDateOnly(1786406400000)).toBe(1786406400000); + }); + + test('should truncate a Date based on UTC, not local time', () => { + // 2024-01-05T23:30 in a +05:30 offset is still 2024-01-05 18:00 UTC, + // so the UTC calendar date must be returned regardless of runner timezone. + expect(toDateOnly(new Date('2024-01-05T23:30:00.000+05:30'))).toBe('2024-01-05'); + }); + test.each([ ['null', null], ['undefined', undefined]