Skip to content

Commit

Permalink
Adds test that ensures all api routes are protected (#399)
Browse files Browse the repository at this point in the history
* chore: adding test:debug for ava debugging + a route guard test that ensures all endpoints are auth guarded.

* feat: exclusion list for the auth guard test so public routes can be added

* feat: reformat, got eslint and prettier working correctly

* fix: comitted by mistake

* fix: can be used for health check pings

* feat: added more comments

* fix: simplify the call

* fix: put comment in test

* fix: void 0 out, undefined in
  • Loading branch information
thomhickey authored Sep 4, 2024
1 parent 6f1c66d commit 7ac442c
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/backend/routers/_app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { router } from "../trpc";
import { admin } from "./admin";
import { publicRouter } from "./public";
import { case_manager } from "./case_manager";
import { file } from "./file";
import { iep } from "./iep";
Expand All @@ -10,6 +11,7 @@ import { user } from "./user";
export const trpcRouter = router({
admin,
case_manager,
public: publicRouter,
file,
iep,
para,
Expand Down
7 changes: 7 additions & 0 deletions src/backend/routers/public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { publicProcedure, router } from "../trpc";

export const publicRouter = router({
healthCheck: publicProcedure.query(() => {
return "Ok";
}),
});
84 changes: 84 additions & 0 deletions src/backend/routers/routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import test from "ava";
import { trpcRouter } from "@/backend/routers/_app";
import { createContext } from "@/backend/context";
import type { NextApiRequest, NextApiResponse } from "next";
import { TRPCError } from "@trpc/server";

test("All API endpoints are auth guarded", async (t) => {
/**
* This test verifies that all API endpoints in the application are properly protected by authentication.
*
* It works by:
* 1. Creating a mock request and response object.
* 2. Generating a context with no authentication.
* 3. Creating a trpc caller with this unauthenticated context.
* 4. Iterating through all procedures in the trpcRouter.
* 5. Attempting to call each procedure without authentication.
* 6. Expecting an UNAUTHORIZED error for each call (except for explicitly excluded endpoints).
*
* This ensures that no sensitive endpoints are accidentally left unprotected,
* maintaining the security of the API.
*/

// create a mock request object for our calls. purpose of this is to have no auth
const mockReq = {
headers: {},
cookies: {},
} as unknown as NextApiRequest;

// create a mock response object that will be passed to the context
const mockRes = {
getHeader: () => undefined,
setCookie: () => undefined,
setHeader: () => undefined,
} as unknown as NextApiResponse;

// Create a mock context with no authentication
const ctx = await createContext({
req: mockReq,
res: mockRes,
});

// Define an exclude list for certain router/procedures. these routes are public
// routes, so we don't want to test for auth on them.
const excludeList = ["public.healthCheck"];

// Create a caller with this context
const caller = trpcRouter.createCaller(ctx);

// Get all procedure names
const procedureNames = Object.keys(trpcRouter._def.procedures);

for (const fullProcedureName of procedureNames) {
if (excludeList.includes(fullProcedureName)) {
continue;
}
// pull apart the router name and procedure name
const [routerName, procedureName] = fullProcedureName.split(".");
try {
// call the procedure without any arguments
await (
caller[routerName as keyof typeof caller] as Record<
string,
() => Promise<unknown>
>
)[procedureName]();
// if we get here, the procedure was not auth guarded
t.fail(`${fullProcedureName} is not auth guarded`);
} catch (error: unknown) {
// if we get here, the procedure was auth guarded, make sure we get UNAUTHORIZED
if (error instanceof TRPCError && error.code === "UNAUTHORIZED") {
t.pass(`${fullProcedureName} is auth guarded`);
} else {
// if we get here, the procedure was auth guarded, but we got an unexpected error
// since the auth guard fires first, this should never happen
console.error(`Unexpected error for ${fullProcedureName}:`, error);
t.fail(
`${fullProcedureName} threw an unexpected error: ${
(error as Error).message
}`
);
}
}
}
});
1 change: 1 addition & 0 deletions src/backend/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ const isAdmin = t.middleware(({ next, ctx }) => {

// Define and export the tRPC router
export const router = t.router;
export const publicProcedure = t.procedure;
export const authenticatedProcedure = t.procedure.use(isAuthenticated);
export const adminProcedure = t.procedure.use(isAuthenticated).use(isAdmin);

0 comments on commit 7ac442c

Please sign in to comment.