@waelhabbaldev/next-jwt-auth
Version:
A secure, lightweight JWT authentication solution for Next.js, providing access and refresh token handling, middleware support, and easy React integration.
105 lines (95 loc) • 3.47 kB
text/typescript
import { describe, it, expect, mock, beforeEach } from "bun:test";
import { protectPage, protectAction, protectApi } from "./protection";
import {
NotAuthenticatedError,
ForbiddenError,
IdentityForbiddenError,
} from "../common/errors";
import { mockUser } from "../common/authentication.test-helpers";
import { EffectiveAuthConfig, SessionFailureReason } from "./authentication";
import { NextResponse } from "next/server";
import { AuthSession, UserIdentity } from "../common/types";
const mockRedirect = mock((path: string) => {
throw new Error(`NEXT_REDIRECT:${path}`);
});
mock.module("next/navigation", () => ({ redirect: mockRedirect }));
type GetSession<T extends UserIdentity> = () => Promise<{
session: AuthSession<T>;
failureReason?: SessionFailureReason;
}>;
const mockConfig: EffectiveAuthConfig<any> = {
redirects: {
unauthenticated: "/login",
unauthorized: "/unauthorized",
forbidden: "/forbidden",
},
errorMessages: {
NotAuthenticatedError: "Auth required",
ForbiddenError: "Permission denied",
IdentityForbiddenError: "Account suspended",
},
baseUrl: "http://localhost:3000",
dal: {} as any,
secrets: {} as any,
cookies: {} as any,
jwt: {} as any,
debug: false,
refreshTokenRotationIntervalSeconds: 0,
rateLimit: async () => false,
logger: () => {},
providers: {},
csrfEnabled: false,
};
const getMockHeaders = (pathname: string) =>
new Headers({ "x-next-pathname": pathname });
describe("server/protection", () => {
beforeEach(() => {
mockRedirect.mockClear();
});
describe("protectPage", () => {
// This test now also implicitly checks that callbackUrl is added correctly
it("should redirect to unauthenticated path with callbackUrl if no session", async () => {
const getSession: GetSession<UserIdentity> = async () => ({
session: null,
});
const expectedRedirectPath = `${mockConfig.redirects.unauthenticated}?callbackUrl=%2Fdashboard`;
await expect(
protectPage(getSession, mockConfig, getMockHeaders("/dashboard"))
).rejects.toThrow(`NEXT_REDIRECT:${expectedRedirectPath}`);
});
// NEW TEST CASE: Verify custom redirects are used
it("should use custom redirect path from options if provided", async () => {
const getSession: GetSession<UserIdentity> = async () => ({
session: null,
});
const options = {
unauthenticatedRedirect: "/custom-login-path",
context: {},
};
const expectedRedirectPath = `${options.unauthenticatedRedirect}?callbackUrl=%2Fdashboard`;
await expect(
protectPage(
getSession,
mockConfig,
getMockHeaders("/dashboard"),
options
)
).rejects.toThrow(`NEXT_REDIRECT:${expectedRedirectPath}`);
});
});
describe("protectApi", () => {
// NEW TEST CASE: Verify success path for protectApi
it("should return session object on success", async () => {
const getSession: GetSession<UserIdentity> = async () => ({
session: { identity: mockUser },
});
const result = await protectApi(getSession, mockConfig);
// Use type assertion to check the successful shape of the return value
expect(
(result as { session: NonNullable<AuthSession<UserIdentity>> }).session
.identity
).toEqual(mockUser);
expect((result as { response: NextResponse }).response).toBeUndefined();
});
});
});