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.