diff --git a/apps/backend/db/db_setup.sql b/apps/backend/db/db_setup.sql index 0cef946..8143e3e 100644 --- a/apps/backend/db/db_setup.sql +++ b/apps/backend/db/db_setup.sql @@ -14,6 +14,7 @@ CREATE TABLE users ( CREATE TABLE projects ( project_id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, + description TEXT NOT NULL, total_budget NUMERIC(12,2), start_date DATE, end_date DATE, @@ -71,10 +72,10 @@ INSERT INTO users (name, email, is_admin) VALUES ('Renee Reddy', 'renee@branch.org', TRUE), ('Nour Shoreibah', 'nour@branch.org', TRUE); -INSERT INTO projects (name, total_budget, start_date, end_date, currency) VALUES -('Clinician Communication Study', 500000, '2025-01-01', '2026-01-01', 'USD'), -('Health Education Initiative', 300000, '2025-03-01', '2026-03-01', 'USD'), -('Policy Advocacy Program', 200000, '2025-06-01', '2026-06-01', 'USD'); +INSERT INTO projects (name, description, total_budget, start_date, end_date, currency) VALUES +('Clinician Communication Study', 'Study of clinician-patient communication patterns', 500000, '2025-01-01', '2026-01-01', 'USD'), +('Health Education Initiative', 'Community health education and outreach', 300000, '2025-03-01', '2026-03-01', 'USD'), +('Policy Advocacy Program', 'Advocacy and policy change efforts', 200000, '2025-06-01', '2026-06-01', 'USD'); INSERT INTO donors (organization, contact_name, contact_email) VALUES ('NIH', 'Dr. Sarah Lee', 'sarah@nih.gov'), diff --git a/apps/backend/lambdas/auth/db-types.d.ts b/apps/backend/lambdas/auth/db-types.d.ts index 3d190d8..5ca734e 100644 --- a/apps/backend/lambdas/auth/db-types.d.ts +++ b/apps/backend/lambdas/auth/db-types.d.ts @@ -53,6 +53,7 @@ export interface BranchProjects { created_at: Generated; currency: Generated; end_date: Timestamp | null; + description: string; name: string; project_id: Generated; start_date: Timestamp | null; diff --git a/apps/backend/lambdas/donors/db-types.d.ts b/apps/backend/lambdas/donors/db-types.d.ts index b2bc948..a44eaf8 100644 --- a/apps/backend/lambdas/donors/db-types.d.ts +++ b/apps/backend/lambdas/donors/db-types.d.ts @@ -53,6 +53,7 @@ export interface BranchProjects { created_at: Generated; currency: Generated; end_date: Timestamp | null; + description: string; name: string; project_id: Generated; start_date: Timestamp | null; diff --git a/apps/backend/lambdas/expenditures/db-types.d.ts b/apps/backend/lambdas/expenditures/db-types.d.ts index b2bc948..a44eaf8 100644 --- a/apps/backend/lambdas/expenditures/db-types.d.ts +++ b/apps/backend/lambdas/expenditures/db-types.d.ts @@ -53,6 +53,7 @@ export interface BranchProjects { created_at: Generated; currency: Generated; end_date: Timestamp | null; + description: string; name: string; project_id: Generated; start_date: Timestamp | null; diff --git a/apps/backend/lambdas/projects/db-types.d.ts b/apps/backend/lambdas/projects/db-types.d.ts index b2bc948..a44eaf8 100644 --- a/apps/backend/lambdas/projects/db-types.d.ts +++ b/apps/backend/lambdas/projects/db-types.d.ts @@ -53,6 +53,7 @@ export interface BranchProjects { created_at: Generated; currency: Generated; end_date: Timestamp | null; + description: string; name: string; project_id: Generated; start_date: Timestamp | null; diff --git a/apps/backend/lambdas/projects/handler.ts b/apps/backend/lambdas/projects/handler.ts index 49ccc05..75554d6 100644 --- a/apps/backend/lambdas/projects/handler.ts +++ b/apps/backend/lambdas/projects/handler.ts @@ -35,7 +35,7 @@ export const handler = async (event: any): Promise => { .updateTable("branch.projects") .set(body) .where("project_id", "=", Number(id)) - .returning(["project_id", "name", "total_budget"]) // control returned fields + .returning(["project_id", "name", "description", "total_budget"]) // control returned fields .executeTakeFirst(); if (!updatedProject) return json(404, { message: `Project not found for id: ${id}` }); return json(200, updatedProject); @@ -73,11 +73,15 @@ export const handler = async (event: any): Promise => { if (!currencyResult.isValid) return json(400, { message: currencyResult.error }); if (currencyResult.value !== null) values.currency = currencyResult.value; + const descriptionResult = ProjectValidationUtils.validateDescription(body.description); + if (!descriptionResult.isValid) return json(400, { message: descriptionResult.error }); + values.description = descriptionResult.value; + try { const inserted = await db .insertInto('branch.projects') .values(values) - .returning(['project_id','name','total_budget','currency','start_date','end_date','created_at']) + .returning(['project_id','name','description','total_budget','currency','start_date','end_date','created_at']) .executeTakeFirst(); return json(201, inserted); diff --git a/apps/backend/lambdas/projects/openapi.yaml b/apps/backend/lambdas/projects/openapi.yaml index 7b45fbb..7e60194 100644 --- a/apps/backend/lambdas/projects/openapi.yaml +++ b/apps/backend/lambdas/projects/openapi.yaml @@ -35,6 +35,8 @@ paths: type: string total_budget: type: number + description: + type: string responses: '200': description: OK diff --git a/apps/backend/lambdas/projects/test/crud.test.ts b/apps/backend/lambdas/projects/test/crud.test.ts index 70a2200..202b987 100644 --- a/apps/backend/lambdas/projects/test/crud.test.ts +++ b/apps/backend/lambdas/projects/test/crud.test.ts @@ -38,6 +38,11 @@ test("get projects test 🌞", async () => { let body = await res.json(); console.log(body); expect(body.length).toBeGreaterThan(0); + body.forEach((project: any) => { + expect(project.description).toBeDefined(); + expect(project.description).not.toBeNull(); + expect(typeof project.description).toBe('string'); + }); }); test("update project test 🌞", async () => { let res = await fetch("http://localhost:3000/projects/1", { @@ -49,6 +54,20 @@ test("update project test 🌞", async () => { expect(body.project_id).toBe(1); expect(body.name).toContain("Project 1 Updated"); expect(Number(body.total_budget)).toBe(Number(2000.00)); + expect(body.description).toBeDefined(); + expect(body.description).not.toBeNull(); + expect(typeof body.description).toBe('string'); +}); + +test("update project with new description test 🌞", async () => { + const newDesc = "Updated project description"; + let res = await fetch("http://localhost:3000/projects/1", { + method: "PUT", + body: JSON.stringify({ name: "Project 1", description: newDesc }), + }); + expect(res.status).toBe(200); + let body = await res.json(); + expect(body.description).toBe(newDesc); }); test("project put 404 test 🌞", async () => { diff --git a/apps/backend/lambdas/projects/test/projects.e2e.test.ts b/apps/backend/lambdas/projects/test/projects.e2e.test.ts index cacc763..88f7178 100644 --- a/apps/backend/lambdas/projects/test/projects.e2e.test.ts +++ b/apps/backend/lambdas/projects/test/projects.e2e.test.ts @@ -36,6 +36,7 @@ describe('POST /projects (e2e)', () => { start_date: '2025-03-01', end_date: '2025-09-30', currency: 'EUR', + description: 'End-to-end project description', }), }); expect(res.status).toBe(201); @@ -45,6 +46,7 @@ describe('POST /projects (e2e)', () => { expect(json.start_date).toContain('2025-03-01'); expect(json.end_date).toContain('2025-09-30'); expect(json.currency).toBe('EUR'); + expect(json.description).toBe('End-to-end project description'); }); test('400 when name missing', async () => { @@ -72,5 +74,42 @@ describe('POST /projects (e2e)', () => { body: JSON.stringify({ name: 'Minimal' }), }); expect(res.status).toBe(201); + const json = await res.json(); + expect(json.description).toBe(''); // description defaults to empty string + }); + + test('201: creates project with empty string description', async () => { + const res = await fetch(`${base}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'EmptyDesc', description: '' }), + }); + expect(res.status).toBe(201); + const json = await res.json(); + expect(json.description).toBe(''); + }); + + test('400: description exceeds 1000 characters', async () => { + const longDesc = 'a'.repeat(1001); + const res = await fetch(`${base}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'LongDesc', description: longDesc }), + }); + expect(res.status).toBe(400); + const json = await res.json(); + expect(json.message).toContain('1000'); + }); + + test('201: creates project with exactly 1000 character description', async () => { + const desc1000 = 'a'.repeat(1000); + const res = await fetch(`${base}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'MaxDesc', description: desc1000 }), + }); + expect(res.status).toBe(201); + const json = await res.json(); + expect(json.description).toBe(desc1000); }); }); \ No newline at end of file diff --git a/apps/backend/lambdas/projects/test/projects.unit.test.ts b/apps/backend/lambdas/projects/test/projects.unit.test.ts index 04ac428..f6a9770 100644 --- a/apps/backend/lambdas/projects/test/projects.unit.test.ts +++ b/apps/backend/lambdas/projects/test/projects.unit.test.ts @@ -35,6 +35,22 @@ test('201: creates project with numeric string budget', async () => { test('201: creates minimal project with only name', async () => { const res = await handler(event({ name: 'Minimal' })); expect(res.statusCode).toBe(201); + const json = JSON.parse(res.body); + expect(json.description).toBe(''); +}); + +test('201: creates project with empty string description', async () => { + const res = await handler(event({ name: 'EmptyDesc', description: '' })); + expect(res.statusCode).toBe(201); + const json = JSON.parse(res.body); + expect(json.description).toBe(''); +}); + +test('201: creates project with whitespace-only description', async () => { + const res = await handler(event({ name: 'WhitespaceDesc', description: ' ' })); + expect(res.statusCode).toBe(201); + const json = JSON.parse(res.body); + expect(json.description).toBe(''); }); test('201: creates project with all fields', async () => { @@ -45,8 +61,9 @@ test('201: creates project with all fields', async () => { name: 'AllFieldsUnit', total_budget: 12345.67, start_date: '2025-01-01', - end_date: '2025-12-31', - currency: 'USD', + end_date: '2025-12-31', + currency: 'USD', + description: 'Unit test project description', }), } as any); expect(res.statusCode).toBe(201); @@ -56,6 +73,15 @@ test('201: creates project with all fields', async () => { expect(json.start_date).toContain('2025-01-01'); expect(json.end_date).toContain('2025-12-31'); expect(json.currency).toBe('USD'); + expect(json.description).toBe('Unit test project description'); +}); + +test('201: creates project with exactly 1000 character description', async () => { + const desc1000 = 'a'.repeat(1000); + const res = await handler(event({ name: 'MaxDesc', description: desc1000 })); + expect(res.statusCode).toBe(201); + const json = JSON.parse(res.body); + expect(json.description).toBe(desc1000); }); // Validation errors (400) @@ -86,3 +112,11 @@ test('400: currency empty or too long', async () => { const tooLong = await handler(event({ name: 'X', currency: 'ABCDEFGHIJK' })); // 11 chars expect(tooLong.statusCode).toBe(400); }); + +test('400: description exceeds 1000 characters', async () => { + const longDesc = 'a'.repeat(1001); + const res = await handler(event({ name: 'LongDesc', description: longDesc })); + expect(res.statusCode).toBe(400); + const json = JSON.parse(res.body); + expect(json.message).toContain('1000'); +}); diff --git a/apps/backend/lambdas/projects/validation-utils.ts b/apps/backend/lambdas/projects/validation-utils.ts index b2124ec..16cb9b8 100644 --- a/apps/backend/lambdas/projects/validation-utils.ts +++ b/apps/backend/lambdas/projects/validation-utils.ts @@ -72,4 +72,19 @@ export class ProjectValidationUtils { } return { isValid: true, value: c }; } + + // validates description field - required, defaults to empty string if not provided + static validateDescription(input: unknown): ValidationResult { + if (input === undefined || input === null || input === '') { + return { isValid: true, value: '' }; + } + if (typeof input !== 'string') { + return { isValid: true, value: '' }; + } + const d = input.trim(); + if (d.length > 1000) { + return { isValid: false, error: "'description' must be <= 1000 chars" }; + } + return { isValid: true, value: d.length === 0 ? '' : d }; + } } diff --git a/apps/backend/lambdas/users/db-types.d.ts b/apps/backend/lambdas/users/db-types.d.ts index b2bc948..a44eaf8 100644 --- a/apps/backend/lambdas/users/db-types.d.ts +++ b/apps/backend/lambdas/users/db-types.d.ts @@ -53,6 +53,7 @@ export interface BranchProjects { created_at: Generated; currency: Generated; end_date: Timestamp | null; + description: string; name: string; project_id: Generated; start_date: Timestamp | null;