From ab869f042af10b6b704034643d9b2a67f650178a Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 4 Feb 2026 20:58:22 -0800 Subject: [PATCH] Add some auth tests --- .../pages/api/auth/[...nextauth].test.js | 100 ++++++++++++++++++ src/__tests__/pages/auth/signin.test.jsx | 72 +++++++++++++ vitest.setup.js | 6 ++ 3 files changed, 178 insertions(+) create mode 100644 src/__tests__/pages/api/auth/[...nextauth].test.js create mode 100644 src/__tests__/pages/auth/signin.test.jsx diff --git a/src/__tests__/pages/api/auth/[...nextauth].test.js b/src/__tests__/pages/api/auth/[...nextauth].test.js new file mode 100644 index 000000000..d901013fd --- /dev/null +++ b/src/__tests__/pages/api/auth/[...nextauth].test.js @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { nextAuthMock } = vi.hoisted(() => ({ + nextAuthMock: vi.fn((options) => ({ options })), +})); + +vi.mock("next-auth", () => ({ + default: nextAuthMock, +})); + +describe("pages/api/auth/[...nextauth]", () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + nextAuthMock.mockClear(); + process.env = { ...originalEnv }; + delete process.env.NEXTAUTH_SECRET; + delete process.env.NEXTAUTH_URL; + }); + + it("configures no providers when auth is disabled", async () => { + const mod = await import("pages/api/auth/[...nextauth]"); + + expect(nextAuthMock).toHaveBeenCalledTimes(1); + expect(mod.default.options.providers).toEqual([]); + expect(mod.default.options.pages?.signIn).toBe("/auth/signin"); + }); + + it("maps HOMEPAGE_AUTH_SECRET and HOMEPAGE_EXTERNAL_URL to NextAuth envs", async () => { + process.env.HOMEPAGE_AUTH_SECRET = "secret"; + process.env.HOMEPAGE_EXTERNAL_URL = "https://homepage.example"; + + const mod = await import("pages/api/auth/[...nextauth]"); + + expect(process.env.NEXTAUTH_SECRET).toBe("secret"); + expect(process.env.NEXTAUTH_URL).toBe("https://homepage.example"); + expect(mod.default.options.secret).toBe("secret"); + }); + + it("throws when auth is enabled but required settings are missing", async () => { + process.env.HOMEPAGE_AUTH_ENABLED = "true"; + + await expect(import("pages/api/auth/[...nextauth]")).rejects.toThrow( + /OIDC auth is enabled but required settings are missing/i, + ); + }); + + it("builds an OIDC provider when enabled and maps profile fields", async () => { + process.env.HOMEPAGE_AUTH_ENABLED = "true"; + process.env.HOMEPAGE_OIDC_ISSUER = "https://issuer.example/"; + process.env.HOMEPAGE_OIDC_CLIENT_ID = "client-id"; + process.env.HOMEPAGE_OIDC_CLIENT_SECRET = "client-secret"; + process.env.HOMEPAGE_AUTH_SECRET = "auth-secret"; + process.env.HOMEPAGE_EXTERNAL_URL = "https://homepage.example"; + process.env.HOMEPAGE_OIDC_NAME = "My OIDC"; + process.env.HOMEPAGE_OIDC_SCOPE = "openid email"; + + const mod = await import("pages/api/auth/[...nextauth]"); + const [provider] = mod.default.options.providers; + + expect(provider).toMatchObject({ + id: "homepage-oidc", + name: "My OIDC", + type: "oauth", + idToken: true, + issuer: "https://issuer.example", + wellKnown: "https://issuer.example/.well-known/openid-configuration", + clientId: "client-id", + clientSecret: "client-secret", + }); + expect(provider.authorization.params.scope).toBe("openid email"); + + expect( + provider.profile({ + sub: "sub", + preferred_username: "user", + email: "user@example.com", + picture: "https://example.com/p.png", + }), + ).toEqual({ + id: "sub", + name: "user", + email: "user@example.com", + image: "https://example.com/p.png", + }); + + expect( + provider.profile({ + id: "id", + name: "name", + }), + ).toEqual({ + id: "id", + name: "name", + email: null, + image: null, + }); + }); +}); diff --git a/src/__tests__/pages/auth/signin.test.jsx b/src/__tests__/pages/auth/signin.test.jsx new file mode 100644 index 000000000..8f734a3f6 --- /dev/null +++ b/src/__tests__/pages/auth/signin.test.jsx @@ -0,0 +1,72 @@ +// @vitest-environment jsdom + +import { render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +const { getSettingsMock } = vi.hoisted(() => ({ + getSettingsMock: vi.fn(), +})); + +vi.mock("utils/config/config", () => ({ + getSettings: getSettingsMock, +})); + +import { getProviders } from "next-auth/react"; +import SignInPage, { getServerSideProps } from "pages/auth/signin"; + +describe("pages/auth/signin", () => { + it("renders an error state when no providers are configured", async () => { + render( + , + ); + + expect(screen.getByText("Authentication not configured")).toBeInTheDocument(); + + await waitFor(() => { + expect(document.documentElement.classList.contains("dark")).toBe(true); + expect(document.documentElement.classList.contains("scheme-dark")).toBe(true); + expect(document.documentElement.classList.contains("theme-slate")).toBe(true); + }); + }); + + it("renders provider buttons when providers are available", () => { + render( + , + ); + + expect(screen.getByText("Sign in")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /login via oidc/i })).toBeInTheDocument(); + }); + + it("getServerSideProps returns providers and settings", async () => { + getProviders.mockResolvedValueOnce({ foo: { id: "foo", name: "Foo" } }); + getSettingsMock.mockReturnValueOnce({ theme: "dark" }); + + const res = await getServerSideProps({}); + + expect(getProviders).toHaveBeenCalled(); + expect(getSettingsMock).toHaveBeenCalled(); + expect(res).toEqual({ + props: { + providers: { foo: { id: "foo", name: "Foo" } }, + settings: { theme: "dark" }, + }, + }); + }); +}); diff --git a/vitest.setup.js b/vitest.setup.js index 87fe7a4a8..a5d61893a 100644 --- a/vitest.setup.js +++ b/vitest.setup.js @@ -8,6 +8,12 @@ afterEach(() => { if (typeof document !== "undefined") cleanup(); }); +// Avoid NextAuth client-side fetches during unit tests. +vi.mock("next-auth/react", () => ({ + SessionProvider: ({ children }) => children ?? null, + getProviders: vi.fn(async () => ({})), +})); + // implement a couple of common formatters mocked in next-i18next vi.mock("next-i18next", () => ({ // Keep app/page components importable in unit tests.