From 13f3ae7030a3910d2855fb51b07c02599ae928b4 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 10:04:11 -0500 Subject: [PATCH] fix(schema): require custCompanyName / custFName / custLName to match DB NOT NULL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup/TimeTracker.sql defines all three columns as text NOT NULL, but customer.schema.js marked them \`.optional()\`. Same class of drift as #265's custState fix: callers omitting any of these fields passed zod and surfaced as a 500 at the postgres INSERT layer ("null value in column 'custFName' violates not-null constraint") instead of a clean 400 with a field-level message. Removed \`.optional()\` from the three required fields. The whitelist message and the surrounding doc comment are updated to call out the DB-NOT-NULL contract explicitly so future readers don't undo it. Test impact: four api-tests (customer-create / customer-bulk / validation / idempotency) sent partial bodies expecting the controller's authKey-missing 403 to short-circuit. With required fields now enforced by zod earlier in the chain, those tests now hit a 400 before reaching auth. Each test was updated to send a complete required-field body — preserving the original intent (testing auth-fail behavior) while accommodating the new validation contract. 760 tests still pass (was 760, no count change — same paths just with non-partial bodies). Co-Authored-By: Claude Opus 4.7 (1M context) --- app/schemas/customer.schema.js | 12 +++++++++--- tests/api/customer-bulk.test.js | 4 +++- tests/api/customer-create.test.js | 7 +++++-- tests/api/idempotency.test.js | 5 ++++- tests/api/validation.test.js | 5 ++++- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/schemas/customer.schema.js b/app/schemas/customer.schema.js index 9ba6be9..6a9aeb3 100644 --- a/app/schemas/customer.schema.js +++ b/app/schemas/customer.schema.js @@ -22,9 +22,15 @@ const intIdParam = z.object({ * zod issue. */ const createCustomerBody = z.object({ - custCompanyName: z.string().max(255).optional(), - custFName: z.string().max(255).optional(), - custLName: z.string().max(255).optional(), + // custCompanyName / custFName / custLName mirror the DB columns + // (text NOT NULL on each — see setup/TimeTracker.sql). Without + // the required flag, callers omitting them passed zod and tripped + // the postgres "null value violates not-null constraint" path, + // surfacing as a 500 instead of a clean 400. Same drift class as + // #265's custState fix. + custCompanyName: z.string().max(255), + custFName: z.string().max(255), + custLName: z.string().max(255), custAddress1: z.string().max(255).optional(), custAddress2: z.string().max(255).optional(), custCity: z.string().max(255).optional(), diff --git a/tests/api/customer-bulk.test.js b/tests/api/customer-bulk.test.js index 861fefe..98b0b8c 100644 --- a/tests/api/customer-bulk.test.js +++ b/tests/api/customer-bulk.test.js @@ -50,9 +50,11 @@ beforeAll(async () => { describe('POST /v1/customer/bulk auth contract', () => { test('returns 403 when authKey header is missing', async () => { + // Full required-field body so body-validation passes and the + // authKey check inside makeBulkCreate is reached. const res = await request(app) .post('/v1/customer/bulk') - .send({ customers: [{ custCompanyName: 'Acme' }] }); + .send({ customers: [{ custCompanyName: 'Acme', custFName: 'Test', custLName: 'User' }] }); expect(res.status).toBe(403); }); }); diff --git a/tests/api/customer-create.test.js b/tests/api/customer-create.test.js index 1b6c4b0..791f89f 100644 --- a/tests/api/customer-create.test.js +++ b/tests/api/customer-create.test.js @@ -36,9 +36,12 @@ beforeAll(async () => { describe('POST /v1/customer', () => { test('returns 403 when authKey header is missing', async () => { + // Send a full required-field body so we exit body validation + // and reach the controller's authKey check. custCompanyName / + // custFName / custLName are required (NOT NULL in the DB). const res = await request(app) .post('/v1/customer') - .send({ custCompanyName: 'Acme' }); + .send({ custCompanyName: 'Acme', custFName: 'Test', custLName: 'User' }); expect(res.status).toBe(403); expect(res.body).toMatchObject({ message: expect.stringMatching(/Authorization key not sent/i), @@ -59,7 +62,7 @@ describe('POST /v1/customer', () => { const res = await request(app) .post('/v1/customer') .set('authKey', 'definitely-not-a-real-key') - .send({ custCompanyName: 'Acme', custCompId: 1 }); + .send({ custCompanyName: 'Acme', custFName: 'Test', custLName: 'User', custCompId: 1 }); // With no DB, IsMaster returns false, GetCompanyId returns -1, // and the controller emits 403 "Invalid Authorization Key." expect(res.status).toBe(403); diff --git a/tests/api/idempotency.test.js b/tests/api/idempotency.test.js index d01ab32..44ae174 100644 --- a/tests/api/idempotency.test.js +++ b/tests/api/idempotency.test.js @@ -78,7 +78,10 @@ describe('Idempotency middleware: mounted on POST routes', () => { .post('/v1/customer') .set('authKey', 'any') .set('Idempotency-Key', '01HFTESTKEY12345') - .send({ custCompanyName: 'Acme' }); + // Full required-field body so the schema validator passes + // and we exercise the idempotency middleware itself rather + // than short-circuiting on a 400 from zod. + .send({ custCompanyName: 'Acme', custFName: 'Test', custLName: 'User' }); // Whatever the controller decides (likely 403 from inline // auth, since the mock returns []), the idempotency layer // should NOT have short-circuited with 400/409. That's the diff --git a/tests/api/validation.test.js b/tests/api/validation.test.js index 3b08502..efc44c6 100644 --- a/tests/api/validation.test.js +++ b/tests/api/validation.test.js @@ -85,7 +85,10 @@ describe('body validation — POST /v1/customer', () => { const res = await request(app) .post('/v1/customer') .set('authKey', 'unknown-key') // will still 403 at controller - .send({ custCompanyName: 'Acme', custCompId: 1 }); + // Full required-field body — custCompanyName / custFName / + // custLName are NOT NULL in the DB and required in the + // zod schema. + .send({ custCompanyName: 'Acme', custFName: 'Test', custLName: 'User', custCompId: 1 }); // After zod passes, the controller's auth check 403s on the // unknown key — that's the expected post-validation path. // We just need to confirm we got past the 400 validator.