Skip to content

Commit

Permalink
refactor: rework submit logic and errors (denoland#608)
Browse files Browse the repository at this point in the history
This change moves the logic for `POST /api/items` to the more
appropriate `POST /submit` and adds a simple error message if the form
was submitted incorrectly.
  • Loading branch information
iuioiua authored Sep 25, 2023
1 parent 362a6ea commit eae9e2b
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 58 deletions.
30 changes: 8 additions & 22 deletions e2e_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,20 +228,12 @@ Deno.test("[e2e] GET /api/items", async () => {
assertArrayIncludes(values, [item1, item2]);
});

Deno.test("[e2e] POST /api/items", async (test) => {
const url = "http://localhost/api/items";
Deno.test("[e2e] POST /submit", async (test) => {
const url = "http://localhost/submit";
const user = randomUser();
await createUser(user);

await test.step("serves unauthorized response if the session user is not signed in", async () => {
const resp = await handler(new Request(url, { method: "POST" }));

assertEquals(resp.status, Status.Unauthorized);
assertText(resp);
assertEquals(await resp.text(), "User must be signed in");
});

await test.step("serves bad request response if item is missing title", async () => {
await test.step("redirects to `/submit?error` if item is missing title", async () => {
const body = new FormData();
const resp = await handler(
new Request(url, {
Expand All @@ -251,12 +243,10 @@ Deno.test("[e2e] POST /api/items", async (test) => {
}),
);

assertEquals(resp.status, Status.BadRequest);
assertText(resp);
assertEquals(await resp.text(), "Title is missing");
assertRedirect(resp, "/submit?error");
});

await test.step("serves bad request response if item is missing URL", async () => {
await test.step("redirects to `/submit?error` if item is missing URL", async () => {
const body = new FormData();
body.set("title", "Title text");
const resp = await handler(
Expand All @@ -267,12 +257,10 @@ Deno.test("[e2e] POST /api/items", async (test) => {
}),
);

assertEquals(resp.status, Status.BadRequest);
assertText(resp);
assertEquals(await resp.text(), "URL is invalid or missing");
assertRedirect(resp, "/submit?error");
});

await test.step("serves bad request response if item has an invalid URL", async () => {
await test.step("redirects to `/submit?error` if item has an invalid URL", async () => {
const body = new FormData();
body.set("title", "Title text");
body.set("url", "invalid-url");
Expand All @@ -284,9 +272,7 @@ Deno.test("[e2e] POST /api/items", async (test) => {
}),
);

assertEquals(resp.status, Status.BadRequest);
assertText(resp);
assertEquals(await resp.text(), "URL is invalid or missing");
assertRedirect(resp, "/submit?error");
});

await test.step("creates an item and redirects to the home page", async () => {
Expand Down
37 changes: 4 additions & 33 deletions routes/api/items/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
// Copyright 2023 the Deno authors. All rights reserved. MIT license.

import { collectValues, listItems } from "@/utils/db.ts";
import { getCursor } from "@/utils/http.ts";
import { type Handlers } from "$fresh/server.ts";
import { createItem, type Item } from "@/utils/db.ts";
import { redirect } from "@/utils/http.ts";
import { assertSignedIn, State } from "@/plugins/session.ts";
import { createHttpError } from "std/http/http_errors.ts";
import { ulid } from "std/ulid/mod.ts";
import { Status } from "std/http/http_status.ts";
import type { Handlers } from "$fresh/server.ts";

// Copyright 2023 the Deno authors. All rights reserved. MIT license.
export const handler: Handlers<undefined, State> = {
export const handler: Handlers = {
async GET(req) {
const url = new URL(req.url);
const iter = listItems({
Expand All @@ -20,28 +15,4 @@ export const handler: Handlers<undefined, State> = {
const values = await collectValues(iter);
return Response.json({ values, cursor: iter.cursor });
},
async POST(req, ctx) {
assertSignedIn(ctx);

const form = await req.formData();
const title = form.get("title");
const url = form.get("url");

if (typeof title !== "string") {
throw createHttpError(Status.BadRequest, "Title is missing");
}
if (typeof url !== "string" || !URL.canParse(url)) {
throw createHttpError(Status.BadRequest, "URL is invalid or missing");
}

const item: Item = {
id: ulid(),
userLogin: ctx.state.sessionUser.login,
title,
url,
score: 0,
};
await createItem(item);
return redirect("/");
},
};
40 changes: 37 additions & 3 deletions routes/submit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,36 @@ import { HEADING_STYLES, INPUT_STYLES } from "@/utils/constants.ts";
import Head from "@/components/Head.tsx";
import IconCheckCircle from "tabler_icons_tsx/circle-check.tsx";
import IconCircleX from "tabler_icons_tsx/circle-x.tsx";
import { defineRoute } from "$fresh/server.ts";
import { defineRoute, Handlers } from "$fresh/server.ts";
import { createItem } from "@/utils/db.ts";
import { redirect } from "@/utils/http.ts";
import type { SignedInState } from "@/plugins/session.ts";
import { ulid } from "std/ulid/mod.ts";
import IconInfo from "tabler_icons_tsx/info-circle.tsx";

export const handler: Handlers<undefined, SignedInState> = {
async POST(req, ctx) {
const form = await req.formData();
const title = form.get("title");
const url = form.get("url");

if (
typeof url !== "string" || !URL.canParse(url) ||
typeof title !== "string" || title === ""
) {
return redirect("/submit?error");
}

await createItem({
id: ulid(),
userLogin: ctx.state.sessionUser.login,
title,
url,
score: 0,
});
return redirect("/");
},
};

export default defineRoute((_req, ctx) => {
return (
Expand Down Expand Up @@ -43,9 +72,8 @@ export default defineRoute((_req, ctx) => {
<form
class="flex-1 flex flex-col justify-center"
method="post"
action="/api/items"
>
<div class="mt-4">
<div>
<label
htmlFor="submit_title"
class="block text-sm font-medium leading-6 text-gray-900"
Expand Down Expand Up @@ -77,6 +105,12 @@ export default defineRoute((_req, ctx) => {
placeholder="https://my-awesome-project.com"
/>
</div>
{ctx.url.searchParams.has("error") && (
<div class="w-full text-red-500 mt-4">
<IconInfo class="inline-block" />{" "}
Title and valid URL are required
</div>
)}
<div class="w-full rounded-lg bg-gradient-to-tr from-secondary to-primary p-px mt-8">
<button class="w-full text-white text-center rounded-[7px] transition duration-300 px-4 py-2 block hover:(bg-white text-black dark:(bg-gray-900 !text-white))">
Submit
Expand Down

0 comments on commit eae9e2b

Please sign in to comment.