-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add signin page #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
fc83b7d
chore: add sonner dep
peppescg 5d249d1
feat: signin page
peppescg a155343
fix: env var doc
peppescg 710ab41
feat: redirect to signin page in case of unauthenticated session
peppescg e93d1d3
feat: sign in page
peppescg a439b51
test: add signin tests case
peppescg a23a6e2
leftover
peppescg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,46 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import { expect, test } from "vitest"; | ||
| import { beforeEach, expect, test, vi } from "vitest"; | ||
| import Home from "../src/app/page"; | ||
|
|
||
| test("Home page renders welcome heading", () => { | ||
| render(<Home />); | ||
| // Mock Next.js modules | ||
| vi.mock("next/headers", () => ({ | ||
| headers: vi.fn(() => Promise.resolve(new Headers())), | ||
| })); | ||
|
|
||
| vi.mock("next/navigation", () => ({ | ||
| redirect: vi.fn(), | ||
| })); | ||
|
|
||
| // Mock auth module | ||
| vi.mock("@/lib/auth", () => ({ | ||
| auth: { | ||
| api: { | ||
| getSession: vi.fn(() => | ||
| Promise.resolve({ | ||
| user: { | ||
| email: "test@example.com", | ||
| name: "Test User", | ||
| }, | ||
| }), | ||
| ), | ||
| }, | ||
| }, | ||
| })); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| test("Home page renders welcome heading when user is logged in", async () => { | ||
| render(await Home()); | ||
|
|
||
| expect( | ||
| screen.getByRole("heading", { | ||
| level: 1, | ||
| name: /Welcome to ToolHive Cloud UI/i, | ||
| }), | ||
| ).toBeDefined(); | ||
|
|
||
| expect(screen.getByText(/You are logged in as/i)).toBeDefined(); | ||
| expect(screen.getByText(/test@example.com/i)).toBeDefined(); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import { cleanup, render, screen, waitFor } from "@testing-library/react"; | ||
| import userEvent from "@testing-library/user-event"; | ||
| import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; | ||
| import SignInPage from "@/app/signin/page"; | ||
|
|
||
| // Mock Next.js Image component | ||
| vi.mock("next/image", () => ({ | ||
| default: () => null, | ||
| })); | ||
|
|
||
| // Mock sonner toast | ||
| vi.mock("sonner", () => ({ | ||
| toast: { | ||
| error: vi.fn(), | ||
| success: vi.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| // Mock auth client | ||
| vi.mock("@/lib/auth-client", () => ({ | ||
| authClient: { | ||
| signIn: { | ||
| oauth2: vi.fn(), | ||
| }, | ||
| }, | ||
| })); | ||
|
|
||
| describe("SignInPage", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| cleanup(); | ||
| }); | ||
|
|
||
| test("renders signin page with all elements", () => { | ||
| render(<SignInPage />); | ||
|
|
||
| // Check main heading | ||
| expect( | ||
| screen.getByRole("heading", { | ||
| level: 2, | ||
| name: /Sign in/i, | ||
| }), | ||
| ).toBeDefined(); | ||
|
|
||
| // Check description text | ||
| expect( | ||
| screen.getByText(/Sign in using your company credentials/i), | ||
| ).toBeDefined(); | ||
|
|
||
| // Check Toolhive branding | ||
| expect( | ||
| screen.getByRole("heading", { | ||
| level: 1, | ||
| name: /Toolhive/i, | ||
| }), | ||
| ).toBeDefined(); | ||
|
|
||
| // Check Okta button | ||
| expect(screen.getByRole("button", { name: /Okta/i })).toBeDefined(); | ||
| }); | ||
|
|
||
| test("calls authClient.signIn.oauth2 when button is clicked", async () => { | ||
| const user = userEvent.setup(); | ||
| const { authClient } = await import("@/lib/auth-client"); | ||
| vi.mocked(authClient.signIn.oauth2).mockResolvedValue({ error: null }); | ||
|
|
||
| render(<SignInPage />); | ||
|
|
||
| const oktaButton = screen.getByRole("button", { name: /Okta/i }); | ||
| await user.click(oktaButton); | ||
|
|
||
| await waitFor(() => { | ||
| expect(authClient.signIn.oauth2).toHaveBeenCalledWith({ | ||
| providerId: "oidc", | ||
| callbackURL: "/catalog", | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| test("shows error toast when signin fails with error", async () => { | ||
| const user = userEvent.setup(); | ||
| const { toast } = await import("sonner"); | ||
| const { authClient } = await import("@/lib/auth-client"); | ||
|
|
||
| vi.mocked(authClient.signIn.oauth2).mockResolvedValue({ | ||
| error: { | ||
| message: "Invalid credentials", | ||
| }, | ||
| }); | ||
|
|
||
| render(<SignInPage />); | ||
|
|
||
| const oktaButton = screen.getByRole("button", { name: /Okta/i }); | ||
| await user.click(oktaButton); | ||
|
|
||
| await waitFor(() => { | ||
| expect(toast.error).toHaveBeenCalledWith("Signin failed", { | ||
| description: "Invalid credentials", | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| test("shows error toast when signin throws exception", async () => { | ||
| const user = userEvent.setup(); | ||
| const { toast } = await import("sonner"); | ||
| const { authClient } = await import("@/lib/auth-client"); | ||
|
|
||
| vi.mocked(authClient.signIn.oauth2).mockRejectedValue( | ||
| new Error("Network error"), | ||
| ); | ||
|
|
||
| render(<SignInPage />); | ||
|
|
||
| const oktaButton = screen.getByRole("button", { name: /Okta/i }); | ||
| await user.click(oktaButton); | ||
|
|
||
| await waitFor(() => { | ||
| expect(toast.error).toHaveBeenCalledWith("Signin error", { | ||
| description: "Network error", | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| test("shows generic error message for unknown errors", async () => { | ||
| const user = userEvent.setup(); | ||
| const { toast } = await import("sonner"); | ||
| const { authClient } = await import("@/lib/auth-client"); | ||
|
|
||
| vi.mocked(authClient.signIn.oauth2).mockRejectedValue( | ||
| "Something went wrong", | ||
| ); | ||
|
|
||
| render(<SignInPage />); | ||
|
|
||
| const oktaButton = screen.getByRole("button", { name: /Okta/i }); | ||
| await user.click(oktaButton); | ||
|
|
||
| await waitFor(() => { | ||
| expect(toast.error).toHaveBeenCalledWith("Signin error", { | ||
| description: "An unexpected error occurred", | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { headers } from "next/headers"; | ||
| import { redirect } from "next/navigation"; | ||
| import { SignOut } from "@/components/sign-out-button"; | ||
| import { auth } from "@/lib/auth"; | ||
|
|
||
| export default async function CatalogPage() { | ||
| const session = await auth.api.getSession({ | ||
| headers: await headers(), | ||
| }); | ||
|
|
||
| if (!session) { | ||
| redirect("/signin"); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black"> | ||
| <main className="flex flex-col items-center gap-8 rounded-lg bg-white p-12 shadow-lg dark:bg-zinc-900"> | ||
| <h1 className="text-3xl font-bold text-black dark:text-white"> | ||
| Hello World! 🎉 | ||
| </h1> | ||
|
|
||
| <div className="flex flex-col gap-4 text-center"> | ||
| <p className="text-zinc-600 dark:text-zinc-400"> | ||
| You are successfully authenticated! | ||
| </p> | ||
|
|
||
| <div className="rounded-lg bg-zinc-100 p-4 dark:bg-zinc-800"> | ||
| <p className="text-sm font-semibold text-zinc-800 dark:text-zinc-200"> | ||
| User Info: | ||
| </p> | ||
| <p className="text-zinc-600 dark:text-zinc-400"> | ||
| Email: <strong>{session.user.email || "Not provided"}</strong> | ||
| </p> | ||
| <p className="text-zinc-600 dark:text-zinc-400"> | ||
| User ID: <strong>{session.user.id}</strong> | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <SignOut /> | ||
| </main> | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.