Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/features/about/aboutPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { describe, expect, test } from "bun:test";
import type { AppVariables } from "@shared/appVariables.ts";
import { pageRoutes } from "@shared/pageRoutes.ts";
import { Hono } from "hono";
import { aboutPage } from "./aboutPage.tsx";

describe("aboutPage", () => {
test("renders the about page HTML", async () => {
const app = new Hono<{ Variables: AppVariables }>().route("/", aboutPage);
const app = new Hono<{ Variables: AppVariables }>().route(
pageRoutes.ABOUT,
aboutPage,
);

const response = await app.fetch(new Request("http://localhost/"));
const response = await app.fetch(
new Request(`http://localhost${pageRoutes.ABOUT}`),
);
const html = await response.text();

expect(response.status).toBe(200);
expect(html).toContain("<title>ToDo</title>");
expect(html).toContain("<h1>About</h1>");
expect(html).toContain('<a href="/">Home</a>');
expect(html).toContain('<a href="/login">Login</a>');
expect(html).not.toContain('<a href="/about">About</a>');
expect(html).toContain("<h2>Powered By</h2>");
expect(html).toContain("<th>Name</th>");
expect(html).toContain("<th>Link</th>");
Expand Down
2 changes: 1 addition & 1 deletion src/features/about/aboutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const aboutPage = new Hono<{ Variables: AppVariables }>().get(
"/",
(c) => {
return c.html(
<Page>
<Page type="unauthenticated" currentPath={c.req.path}>
Comment thread
CharlesFarris marked this conversation as resolved.
<h1>About</h1>
<h2>Powered By</h2>
<table>
Expand Down
24 changes: 18 additions & 6 deletions src/features/add-todo/addToDoPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ describe("addToDoPage", () => {
});
const app = new Hono<{ Variables: AppVariables }>()
.use("*", createAppConfigMiddleware(appConfig))
.route("/", addToDoPage);
.route(pageRoutes.ADD_TODO, addToDoPage);

const response = await app.fetch(new Request("http://localhost/"));
const response = await app.fetch(
new Request(`http://localhost${pageRoutes.ADD_TODO}`),
);

expect(response.status).toBe(302);
expect(response.headers.get("Location")).toBe(pageRoutes.LOGIN);
Expand All @@ -26,13 +28,17 @@ describe("addToDoPage", () => {
const appConfig = createAppConfig({
JWT_SECRET: "12345678901234567890123456789012",
});
const token = await sign({ sub: "admin" }, appConfig.jwt.secret, "HS256");
const token = await sign(
{ sub: "1234", preferred_username: "admin" },
appConfig.jwt.secret,
"HS256",
);
const app = new Hono<{ Variables: AppVariables }>()
.use("*", createAppConfigMiddleware(appConfig))
.route("/", addToDoPage);
.route(pageRoutes.ADD_TODO, addToDoPage);

const response = await app.fetch(
new Request("http://localhost/", {
new Request(`http://localhost${pageRoutes.ADD_TODO}`, {
headers: {
Cookie: `${appConfig.jwt.cookieName}=${token}`,
},
Expand All @@ -41,6 +47,12 @@ describe("addToDoPage", () => {
const html = await response.text();

expect(response.status).toBe(200);
expect(html).toContain("Add ToDo");
expect(html).toContain("<title>ToDo</title>");
expect(html).toContain("<h1>Add ToDo</h1>");
expect(html).toContain('<a href="/">Home</a>');
expect(html).not.toContain('<a href="/add-todo">Add</a>');
expect(html).toContain('<a href="/about">About</a>');
expect(html).toContain("<li>admin</li>");
expect(html).toContain('<form action="/api/logout" method="post">');
});
});
5 changes: 4 additions & 1 deletion src/features/add-todo/addToDoPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { AppVariables } from "@shared/appVariables.ts";
import { Page } from "@shared/page.tsx";
import { pageJwtMiddleware } from "@shared/pageJwtMiddleware.ts";
import { getUsernameFromJwtPayload } from "@shared/utility.ts";
import { Hono } from "hono";

export const addToDoPage = new Hono<{ Variables: AppVariables }>()
.use("/", pageJwtMiddleware)
.get("/", (c) => {
const username = getUsernameFromJwtPayload(c.var.jwtPayload);

return c.html(
<Page>
<Page type="authenticated" currentPath={c.req.path} username={username}>
<h1>Add ToDo</h1>
</Page>,
);
Expand Down
10 changes: 9 additions & 1 deletion src/features/home/homePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ describe("homePage", () => {
const appConfig = createAppConfig({
JWT_SECRET: "12345678901234567890123456789012",
});
const token = await sign({ sub: "admin" }, appConfig.jwt.secret, "HS256");
const token = await sign(
{ sub: "1234", preferred_username: "admin" },
appConfig.jwt.secret,
"HS256",
);
const app = new Hono<{ Variables: AppVariables }>()
.use("*", createAppConfigMiddleware(appConfig))
.route("/", homePage);
Expand All @@ -43,6 +47,10 @@ describe("homePage", () => {
expect(response.status).toBe(200);
expect(html).toContain("<title>ToDo</title>");
expect(html).toContain("<h1>Home</h1>");
expect(html).not.toContain('<a href="/">Home</a>');
expect(html).toContain('<a href="/add-todo">Add</a>');
expect(html).toContain('<a href="/about">About</a>');
expect(html).toContain("<li>admin</li>");
expect(html).toContain('<form action="/api/logout" method="post">');
Comment thread
CharlesFarris marked this conversation as resolved.
Comment thread
CharlesFarris marked this conversation as resolved.
});
});
4 changes: 3 additions & 1 deletion src/features/home/homePage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { AppVariables } from "@shared/appVariables.ts";
import { Page } from "@shared/page.tsx";
import { pageJwtMiddleware } from "@shared/pageJwtMiddleware.ts";
import { getUsernameFromJwtPayload } from "@shared/utility.ts";
import { Hono } from "hono";

export const homePage = new Hono<{ Variables: AppVariables }>()
.use("/", pageJwtMiddleware)
.get("/", (c) => {
const username = getUsernameFromJwtPayload(c.var.jwtPayload);
return c.html(
<Page>
<Page type="authenticated" currentPath={c.req.path} username={username}>
<h1>Home</h1>
</Page>,
Comment thread
CharlesFarris marked this conversation as resolved.
Comment thread
CharlesFarris marked this conversation as resolved.
);
Expand Down
12 changes: 10 additions & 2 deletions src/features/login/loginPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { describe, expect, test } from "bun:test";
import { apiRoutes } from "@shared/apiRoutes.ts";
import type { AppVariables } from "@shared/appVariables.ts";
import { pageRoutes } from "@shared/pageRoutes.ts";
import { Hono } from "hono";
import { loginPage } from "./loginPage.tsx";

describe("loginPage", () => {
test("renders the login page HTML", async () => {
const app = new Hono<{ Variables: AppVariables }>().route("/", loginPage);
const app = new Hono<{ Variables: AppVariables }>().route(
pageRoutes.LOGIN,
loginPage,
);

const response = await app.fetch(new Request("http://localhost/"));
const response = await app.fetch(
new Request(`http://localhost${pageRoutes.LOGIN}`),
);
const html = await response.text();

expect(response.status).toBe(200);
Expand All @@ -25,6 +31,8 @@ describe("loginPage", () => {
);
expect(html).toContain('autocomplete="current-password"');
expect(html).toContain('<button type="submit">Login</button>');
expect(html).not.toContain('<a href="/login">Login</a>');
expect(html).toContain('<a href="/about">About</a>');
expect(html).toContain(apiRoutes.LOGIN);
expect(html).toContain("contentType: &#39;form&#39;");
});
Expand Down
2 changes: 1 addition & 1 deletion src/features/login/loginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const loginPage = new Hono<{ Variables: AppVariables }>().get(
"/",
(c) => {
return c.html(
<Page>
<Page type="unauthenticated" currentPath={c.req.path}>
Comment thread
CharlesFarris marked this conversation as resolved.
<h1>Login</h1>
<form
id="login"
Expand Down
3 changes: 2 additions & 1 deletion src/shared/appVariables.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { JwtVariables } from "hono/jwt";
import type { Logger } from "winston";
import type { AppConfig } from "./appConfig.ts";
import type { Clock } from "./clock.ts";
Expand All @@ -6,4 +7,4 @@ export type AppVariables = {
clock: Clock;
appConfig: AppConfig;
logger: Logger;
};
} & JwtVariables;
58 changes: 58 additions & 0 deletions src/shared/navigationBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, test } from "bun:test";
import { NavigationBar } from "./navigationBar.tsx";

describe("NavigationBar", () => {
test("renders authenticated navigation links, username, and logout action", () => {
const html = NavigationBar({
type: "authenticated",
currentPath: "/add-todo",
username: "test-user",
});
const htmlString = String(html);

expect(htmlString).toContain("<nav>");
expect(htmlString).toContain('<a href="/">Home</a>');
expect(htmlString).not.toContain('<a href="/add-todo">Add</a>');
expect(htmlString).toContain('<a href="/about">About</a>');
expect(htmlString).toContain("<li>test-user</li>");
expect(htmlString).toContain('<form action="/api/logout" method="post">');
expect(htmlString).toContain(
'<button class="nav-link-button" type="submit">',
);
});

test("hides the authenticated current route link", () => {
const html = NavigationBar({
type: "authenticated",
currentPath: "/",
username: "test-user",
});
const htmlString = String(html);

expect(htmlString).not.toContain('<a href="/">Home</a>');
expect(htmlString).toContain('<a href="/add-todo">Add</a>');
expect(htmlString).toContain('<a href="/about">About</a>');
expect(htmlString).toContain("<li>test-user</li>");
expect(htmlString).toContain('<form action="/api/logout" method="post">');
});

test("renders unauthenticated links", () => {
const html = NavigationBar({ type: "unauthenticated", currentPath: "/" });
const htmlString = String(html);

expect(htmlString).toContain('<a href="/login">Login</a>');
expect(htmlString).toContain('<a href="/about">About</a>');
expect(htmlString).not.toContain("Logout");
});

test("hides the unauthenticated current route link", () => {
const html = NavigationBar({
type: "unauthenticated",
currentPath: "/login",
});
const htmlString = String(html);

expect(htmlString).not.toContain('<a href="/login">Login</a>');
expect(htmlString).toContain('<a href="/about">About</a>');
});
});
104 changes: 81 additions & 23 deletions src/shared/navigationBar.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,85 @@
import { apiRoutes } from "./apiRoutes.ts";
import { NavigationBarItem } from "./navigationBarItem.tsx";
import { pageRoutes } from "./pageRoutes.ts";

export function navigationBar() {
return (
<nav>
<ul>
<li>
<a href={pageRoutes.HOME}>Home</a>
</li>
<li>
<a href={pageRoutes.ABOUT}>About</a>
</li>
<li>
<a href={pageRoutes.LOGIN}>Login</a>
</li>
<li>
<form action={apiRoutes.LOGOUT} method="post">
<button class="nav-link-button" type="submit">
Logout
</button>
</form>
</li>
</ul>
</nav>
);
export type SharedNavigationBarProps = Readonly<{
currentPath: string;
}>;

export type AuthenticatedNavigationBarProps = SharedNavigationBarProps &
Readonly<{
type: "authenticated";
username: string;
}>;

export type UnauthenticatedNavigationBarProps = SharedNavigationBarProps &
Readonly<{
type: "unauthenticated";
}>;

export type NavigationBarProps =
| AuthenticatedNavigationBarProps
| UnauthenticatedNavigationBarProps;

export function NavigationBar(props: NavigationBarProps) {
switch (props.type) {
case "authenticated":
return (
<nav>
<ul>
<NavigationBarItem
type="link"
label="Home"
url={pageRoutes.HOME}
currentPath={props.currentPath}
/>
<NavigationBarItem
type="link"
label="Add"
url={pageRoutes.ADD_TODO}
currentPath={props.currentPath}
/>
Comment thread
CharlesFarris marked this conversation as resolved.
<NavigationBarItem
type="link"
label="About"
url={pageRoutes.ABOUT}
currentPath={props.currentPath}
/>
<NavigationBarItem type="generic">
{props.username}
</NavigationBarItem>
<NavigationBarItem type="generic">
<form action={apiRoutes.LOGOUT} method="post">
<button class="nav-link-button" type="submit">
Logout
</button>
</form>
</NavigationBarItem>
</ul>
</nav>
);
case "unauthenticated":
return (
<nav>
<ul>
<NavigationBarItem
type="link"
label="Login"
url={pageRoutes.LOGIN}
currentPath={props.currentPath}
/>
<NavigationBarItem
type="link"
label="About"
url={pageRoutes.ABOUT}
currentPath={props.currentPath}
/>
</ul>
</nav>
);
default: {
const _unknownType: never = props;
return _unknownType;
}
}
}
Loading