diff --git a/src/index.ts b/src/index.ts index 9e7603c..032c7fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,29 @@ import app from "./app"; +const args = process.argv.slice(2); + +if (args.includes("--version") || args.includes("-v")) { + process.stdout.write("fleet-e2e-toy v1.0.0\n"); + process.exit(0); +} + +if (args.includes("--help") || args.includes("-h") || args[0] === "help") { + process.stdout.write( + "Usage: node src/index.ts [options]\n" + + "\n" + + "Options:\n" + + " --version, -v Print version and exit\n" + + " --help, -h Print this help message and exit\n" + + "\n" + + "Subcommands:\n" + + " help Print this help message and exit\n" + + "\n" + + "Environment Variables:\n" + + " PORT Port to listen on (default: 3000)\n" + ); + process.exit(0); +} + const PORT = process.env.PORT ?? 3000; app.listen(PORT, () => { diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 9cfb289..87eef61 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -20,8 +20,8 @@ export function validateCreateInput( errors.push({ field: "title", message: "Title is required and must be a non-empty string" }); } - if (typeof obj.content !== "string") { - errors.push({ field: "content", message: "Content must be a string" }); + if (typeof obj.content !== "string" || obj.content.trim().length === 0) { + errors.push({ field: "content", message: "Content is required and must be a non-empty string" }); } if (obj.tags !== undefined) { @@ -36,7 +36,7 @@ export function validateCreateInput( valid: true, data: { title: (obj.title as string).trim(), - content: obj.content as string, + content: (obj.content as string).trim(), tags: (obj.tags as string[] | undefined) ?? [], }, }; @@ -57,8 +57,8 @@ export function validateUpdateInput( errors.push({ field: "title", message: "Title must be a non-empty string" }); } - if (obj.content !== undefined && typeof obj.content !== "string") { - errors.push({ field: "content", message: "Content must be a string" }); + if (obj.content !== undefined && (typeof obj.content !== "string" || obj.content.trim().length === 0)) { + errors.push({ field: "content", message: "Content must be a non-empty string" }); } if (obj.tags !== undefined) { @@ -71,7 +71,7 @@ export function validateUpdateInput( const data: UpdateNoteInput = {}; if (obj.title !== undefined) data.title = (obj.title as string).trim(); - if (obj.content !== undefined) data.content = obj.content as string; + if (obj.content !== undefined) data.content = (obj.content as string).trim(); if (obj.tags !== undefined) data.tags = obj.tags as string[]; return { valid: true, data }; diff --git a/tests/notes.test.ts b/tests/notes.test.ts index 45553cf..7127014 100644 --- a/tests/notes.test.ts +++ b/tests/notes.test.ts @@ -87,6 +87,30 @@ describe("POST /api/notes", () => { expect(res.status).toBe(400); expect(res.body.errors).toBeDefined(); }); + + it("returns 400 for whitespace-only title", async () => { + const res = await request(app) + .post("/api/notes") + .send({ title: " ", content: "Body" }); + expect(res.status).toBe(400); + expect(res.body.errors).toBeDefined(); + }); + + it("returns 400 for empty content", async () => { + const res = await request(app) + .post("/api/notes") + .send({ title: "Note", content: "" }); + expect(res.status).toBe(400); + expect(res.body.errors).toBeDefined(); + }); + + it("returns 400 for whitespace-only content", async () => { + const res = await request(app) + .post("/api/notes") + .send({ title: "Note", content: " " }); + expect(res.status).toBe(400); + expect(res.body.errors).toBeDefined(); + }); }); describe("PUT /api/notes/:id", () => { @@ -110,6 +134,30 @@ describe("PUT /api/notes/:id", () => { .send({ title: "Nope" }); expect(res.status).toBe(404); }); + + it("returns 400 for whitespace-only title on update", async () => { + const create = await request(app) + .post("/api/notes") + .send({ title: "Original", content: "Body", tags: [] }); + + const res = await request(app) + .put(`/api/notes/${create.body.id}`) + .send({ title: " " }); + expect(res.status).toBe(400); + expect(res.body.errors).toBeDefined(); + }); + + it("returns 400 for whitespace-only content on update", async () => { + const create = await request(app) + .post("/api/notes") + .send({ title: "Original", content: "Body", tags: [] }); + + const res = await request(app) + .put(`/api/notes/${create.body.id}`) + .send({ content: " " }); + expect(res.status).toBe(400); + expect(res.body.errors).toBeDefined(); + }); }); describe("DELETE /api/notes/:id", () => { diff --git a/tests/validation.test.ts b/tests/validation.test.ts index f55f70f..e334e30 100644 --- a/tests/validation.test.ts +++ b/tests/validation.test.ts @@ -42,6 +42,38 @@ describe("validateCreateInput", () => { expect(result.errors[0].field).toBe("tags"); } }); + + it("rejects whitespace-only title", () => { + const result = validateCreateInput({ title: " ", content: "Body" }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("title"); + } + }); + + it("rejects empty content", () => { + const result = validateCreateInput({ title: "Note", content: "" }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("content"); + } + }); + + it("rejects whitespace-only content", () => { + const result = validateCreateInput({ title: "Note", content: " " }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("content"); + } + }); + + it("trims content on valid input", () => { + const result = validateCreateInput({ title: "Note", content: " Body " }); + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.data.content).toBe("Body"); + } + }); }); describe("validateUpdateInput", () => { @@ -63,4 +95,28 @@ describe("validateUpdateInput", () => { const result = validateUpdateInput({}); expect(result.valid).toBe(true); }); + + it("rejects whitespace-only title", () => { + const result = validateUpdateInput({ title: " " }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("title"); + } + }); + + it("rejects empty content string", () => { + const result = validateUpdateInput({ content: "" }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("content"); + } + }); + + it("rejects whitespace-only content string", () => { + const result = validateUpdateInput({ content: " " }); + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.errors[0].field).toBe("content"); + } + }); });