nextjs-hackathon-stack 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +4 -9
  2. package/dist/index.js +8 -2
  3. package/package.json +2 -2
  4. package/template/.cursor/rules/ai.mdc +4 -4
  5. package/template/.cursor/rules/general.mdc +2 -2
  6. package/template/.husky/pre-commit +1 -0
  7. package/template/README.md +6 -7
  8. package/template/_env.example +7 -7
  9. package/template/package.json.tmpl +8 -2
  10. package/template/src/app/__tests__/auth-callback.test.ts +72 -0
  11. package/template/src/app/__tests__/error.test.tsx +71 -0
  12. package/template/src/app/__tests__/layout.test.tsx +22 -0
  13. package/template/src/app/__tests__/login-page.test.tsx +34 -0
  14. package/template/src/app/__tests__/protected-layout.test.tsx +43 -0
  15. package/template/src/app/__tests__/protected-page.test.tsx +41 -0
  16. package/template/src/app/api/chat/route.ts +3 -1
  17. package/template/src/features/auth/__tests__/login-form.test.tsx +53 -2
  18. package/template/src/features/auth/__tests__/login.action.test.ts +82 -0
  19. package/template/src/features/auth/__tests__/logout.action.test.ts +32 -0
  20. package/template/src/features/auth/actions/login.action.ts +1 -1
  21. package/template/src/features/auth/components/login-form.tsx +4 -3
  22. package/template/src/features/chat/__tests__/chat-ui.test.tsx +97 -7
  23. package/template/src/features/chat/__tests__/route.test.ts +66 -3
  24. package/template/src/features/chat/__tests__/use-chat.test.ts +15 -0
  25. package/template/src/features/chat/components/chat-ui.tsx +1 -1
  26. package/template/src/shared/__tests__/middleware.test.ts +34 -0
  27. package/template/src/shared/__tests__/schema.test.ts +16 -3
  28. package/template/src/shared/__tests__/supabase-middleware.test.ts +162 -0
  29. package/template/src/shared/__tests__/supabase-server.test.ts +76 -3
  30. package/template/src/shared/__tests__/ui-button.test.tsx +52 -0
  31. package/template/src/shared/__tests__/ui-card.test.tsx +78 -0
  32. package/template/src/shared/__tests__/utils.test.ts +30 -0
  33. package/template/src/shared/lib/ai.ts +2 -2
  34. package/template/vitest.config.ts +5 -0
  35. package/template/src/features/tts/__tests__/route.test.ts +0 -13
  36. package/template/src/features/tts/__tests__/tts-player.test.tsx +0 -21
  37. package/template/src/features/tts/api/route.ts +0 -14
  38. package/template/src/features/tts/components/tts-player.tsx +0 -59
  39. package/template/src/features/video/__tests__/route.test.ts +0 -13
  40. package/template/src/features/video/__tests__/video-generator.test.tsx +0 -21
  41. package/template/src/features/video/api/route.ts +0 -14
  42. package/template/src/features/video/components/video-generator.tsx +0 -56
  43. package/template/src/shared/__tests__/minimax-media.test.ts +0 -58
  44. package/template/src/shared/lib/minimax-media.ts +0 -63
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const mockSignInWithPassword = vi.fn();
4
+ const mockRedirect = vi.fn();
5
+
6
+ vi.mock("@/shared/lib/supabase/server", () => ({
7
+ createClient: vi.fn(() =>
8
+ Promise.resolve({
9
+ auth: { signInWithPassword: mockSignInWithPassword },
10
+ })
11
+ ),
12
+ }));
13
+
14
+ vi.mock("next/navigation", () => ({
15
+ redirect: (url: string) => mockRedirect(url),
16
+ }));
17
+
18
+ describe("loginAction", () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ it("returns error when email is invalid", async () => {
24
+ // Arrange
25
+ const { loginAction } = await import("../actions/login.action");
26
+ const fd = new FormData();
27
+ fd.set("email", "not-an-email");
28
+ fd.set("password", "password123");
29
+
30
+ // Act
31
+ const result = await loginAction({ status: "idle" }, fd);
32
+
33
+ // Assert
34
+ expect(result.status).toBe("error");
35
+ });
36
+
37
+ it("returns error when password is too short", async () => {
38
+ // Arrange
39
+ const { loginAction } = await import("../actions/login.action");
40
+ const fd = new FormData();
41
+ fd.set("email", "user@example.com");
42
+ fd.set("password", "short");
43
+
44
+ // Act
45
+ const result = await loginAction({ status: "idle" }, fd);
46
+
47
+ // Assert
48
+ expect(result.status).toBe("error");
49
+ });
50
+
51
+ it("returns error on auth failure", async () => {
52
+ // Arrange
53
+ mockSignInWithPassword.mockResolvedValue({
54
+ error: { message: "Invalid credentials" },
55
+ });
56
+ const { loginAction } = await import("../actions/login.action");
57
+ const fd = new FormData();
58
+ fd.set("email", "user@example.com");
59
+ fd.set("password", "password123");
60
+
61
+ // Act
62
+ const result = await loginAction({ status: "idle" }, fd);
63
+
64
+ // Assert
65
+ expect(result).toEqual({ status: "error", message: "Invalid credentials" });
66
+ });
67
+
68
+ it("redirects to / on successful login", async () => {
69
+ // Arrange
70
+ mockSignInWithPassword.mockResolvedValue({ error: null });
71
+ const { loginAction } = await import("../actions/login.action");
72
+ const fd = new FormData();
73
+ fd.set("email", "user@example.com");
74
+ fd.set("password", "password123");
75
+
76
+ // Act
77
+ await loginAction({ status: "idle" }, fd);
78
+
79
+ // Assert
80
+ expect(mockRedirect).toHaveBeenCalledWith("/");
81
+ });
82
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const mockSignOut = vi.fn();
4
+ const mockRedirect = vi.fn();
5
+
6
+ vi.mock("@/shared/lib/supabase/server", () => ({
7
+ createClient: vi.fn(() =>
8
+ Promise.resolve({
9
+ auth: { signOut: mockSignOut },
10
+ })
11
+ ),
12
+ }));
13
+
14
+ vi.mock("next/navigation", () => ({
15
+ redirect: (url: string) => mockRedirect(url),
16
+ }));
17
+
18
+ describe("logoutAction", () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ mockSignOut.mockResolvedValue({});
22
+ });
23
+
24
+ it("calls signOut and redirects to /login", async () => {
25
+ const { logoutAction } = await import("../actions/logout.action");
26
+
27
+ await logoutAction();
28
+
29
+ expect(mockSignOut).toHaveBeenCalled();
30
+ expect(mockRedirect).toHaveBeenCalledWith("/login");
31
+ });
32
+ });
@@ -25,7 +25,7 @@ export async function loginAction(
25
25
  });
26
26
 
27
27
  if (!parsed.success) {
28
- return { status: "error", message: parsed.error.issues[0]?.message ?? "Invalid input" };
28
+ return { status: "error", message: parsed.error.issues[0]!.message };
29
29
  }
30
30
 
31
31
  const supabase = await createClient();
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
- import { useActionState } from "react";
4
+ import { useActionState, useTransition } from "react";
5
5
  import { useForm } from "react-hook-form";
6
6
  import { z } from "zod";
7
7
 
@@ -22,7 +22,8 @@ type FormValues = z.infer<typeof schema>;
22
22
  const initialState: LoginActionState = { status: "idle" };
23
23
 
24
24
  export function LoginForm() {
25
- const [state, formAction, isPending] = useActionState(loginAction, initialState);
25
+ const [state, formAction] = useActionState(loginAction, initialState);
26
+ const [isPending, startTransition] = useTransition();
26
27
 
27
28
  const {
28
29
  register,
@@ -34,7 +35,7 @@ export function LoginForm() {
34
35
  const fd = new FormData();
35
36
  fd.set("email", data.email);
36
37
  fd.set("password", data.password);
37
- formAction(fd);
38
+ startTransition(() => formAction(fd));
38
39
  });
39
40
 
40
41
  return (
@@ -3,29 +3,119 @@ import { describe, it, expect, vi } from "vitest";
3
3
 
4
4
  import { ChatUi } from "../components/chat-ui";
5
5
 
6
+ const mockUseChat = vi.fn(() => ({
7
+ messages: [] as { id: string; role: string; content: string }[],
8
+ input: "",
9
+ handleInputChange: vi.fn(),
10
+ handleSubmit: vi.fn(),
11
+ status: "idle",
12
+ }));
13
+
6
14
  vi.mock("@ai-sdk/react", () => ({
7
- useChat: vi.fn(() => ({
8
- messages: [],
9
- input: "",
10
- handleInputChange: vi.fn(),
11
- handleSubmit: vi.fn(),
12
- isLoading: false,
13
- })),
15
+ useChat: (...args: unknown[]) => mockUseChat(...args),
14
16
  }));
15
17
 
16
18
  describe("ChatUi", () => {
17
19
  it("renders chat input", () => {
20
+ // Arrange + Act
18
21
  render(<ChatUi />);
22
+
23
+ // Assert
19
24
  expect(screen.getByTestId("chat-input")).toBeInTheDocument();
20
25
  });
21
26
 
22
27
  it("renders send button", () => {
28
+ // Arrange + Act
23
29
  render(<ChatUi />);
30
+
31
+ // Assert
24
32
  expect(screen.getByRole("button", { name: /send/i })).toBeInTheDocument();
25
33
  });
26
34
 
27
35
  it("renders messages list area", () => {
36
+ // Arrange + Act
28
37
  render(<ChatUi />);
38
+
39
+ // Assert
29
40
  expect(screen.getByTestId("chat-ui")).toBeInTheDocument();
30
41
  });
42
+
43
+ it("renders user and assistant messages", () => {
44
+ // Arrange
45
+ mockUseChat.mockReturnValueOnce({
46
+ messages: [
47
+ { id: "1", role: "user", content: "Hello" },
48
+ { id: "2", role: "assistant", content: "Hi there!" },
49
+ ],
50
+ input: "",
51
+ handleInputChange: vi.fn(),
52
+ handleSubmit: vi.fn(),
53
+ status: "idle",
54
+ });
55
+
56
+ // Act
57
+ render(<ChatUi />);
58
+
59
+ // Assert
60
+ expect(screen.getByText("Hello")).toBeInTheDocument();
61
+ expect(screen.getByText("Hi there!")).toBeInTheDocument();
62
+ });
63
+
64
+ it("shows loading indicator when status is streaming", () => {
65
+ // Arrange
66
+ mockUseChat.mockReturnValueOnce({
67
+ messages: [],
68
+ input: "",
69
+ handleInputChange: vi.fn(),
70
+ handleSubmit: vi.fn(),
71
+ status: "streaming",
72
+ });
73
+
74
+ // Act
75
+ render(<ChatUi />);
76
+
77
+ // Assert
78
+ expect(screen.getByText(/thinking/i)).toBeInTheDocument();
79
+ });
80
+
81
+ it("shows loading indicator when status is submitted", () => {
82
+ // Arrange
83
+ mockUseChat.mockReturnValueOnce({
84
+ messages: [],
85
+ input: "",
86
+ handleInputChange: vi.fn(),
87
+ handleSubmit: vi.fn(),
88
+ status: "submitted",
89
+ });
90
+
91
+ // Act
92
+ render(<ChatUi />);
93
+
94
+ // Assert
95
+ expect(screen.getByText(/thinking/i)).toBeInTheDocument();
96
+ });
97
+
98
+ it("strips <think> tags from message content", () => {
99
+ // Arrange
100
+ mockUseChat.mockReturnValueOnce({
101
+ messages: [
102
+ {
103
+ id: "1",
104
+ role: "assistant",
105
+ content: "<think>internal reasoning</think>Visible answer",
106
+ },
107
+ ],
108
+ input: "",
109
+ handleInputChange: vi.fn(),
110
+ handleSubmit: vi.fn(),
111
+ status: "idle",
112
+ });
113
+
114
+ // Act
115
+ render(<ChatUi />);
116
+
117
+ // Assert
118
+ expect(screen.getByText("Visible answer")).toBeInTheDocument();
119
+ expect(screen.queryByText(/internal reasoning/)).not.toBeInTheDocument();
120
+ });
31
121
  });
@@ -1,8 +1,13 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
2
 
3
+ let capturedGetErrorMessage: ((error: unknown) => string) | undefined;
4
+
3
5
  vi.mock("ai", () => ({
4
6
  streamText: vi.fn(() => ({
5
- toDataStreamResponse: vi.fn(() => new Response("data: done\n\n")),
7
+ toDataStreamResponse: vi.fn((opts?: { getErrorMessage?: (e: unknown) => string }) => {
8
+ capturedGetErrorMessage = opts?.getErrorMessage;
9
+ return new Response("data: done\n\n");
10
+ }),
6
11
  })),
7
12
  }));
8
13
 
@@ -11,9 +16,67 @@ vi.mock("@/shared/lib/ai", () => ({
11
16
  }));
12
17
 
13
18
  describe("chat route", () => {
14
- it("module is importable", async () => {
15
- const mod = await import("../api/route");
19
+ it("exports runtime as edge and POST handler", async () => {
20
+ // Arrange + Act
21
+ const mod = await import("../../../app/api/chat/route");
22
+
23
+ // Assert
16
24
  expect(mod.runtime).toBe("edge");
17
25
  expect(typeof mod.POST).toBe("function");
18
26
  });
27
+
28
+ it("calls streamText and returns a response", async () => {
29
+ // Arrange
30
+ const mod = await import("../../../app/api/chat/route");
31
+ const req = new Request("http://localhost/api/chat", {
32
+ method: "POST",
33
+ body: JSON.stringify({ messages: [{ role: "user", content: "hello" }] }),
34
+ });
35
+
36
+ // Act
37
+ const response = await mod.POST(req);
38
+
39
+ // Assert
40
+ expect(response).toBeInstanceOf(Response);
41
+ });
42
+
43
+ it("getErrorMessage returns error.message for Error instances", async () => {
44
+ // Arrange
45
+ const mod = await import("../../../app/api/chat/route");
46
+ const req = new Request("http://localhost/api/chat", {
47
+ method: "POST",
48
+ body: JSON.stringify({ messages: [] }),
49
+ });
50
+ await mod.POST(req);
51
+
52
+ // Act + Assert
53
+ expect(capturedGetErrorMessage).toBeDefined();
54
+ expect(capturedGetErrorMessage!(new Error("boom"))).toBe("boom");
55
+ });
56
+
57
+ it("getErrorMessage returns JSON for plain objects", async () => {
58
+ // Arrange
59
+ const mod = await import("../../../app/api/chat/route");
60
+ const req = new Request("http://localhost/api/chat", {
61
+ method: "POST",
62
+ body: JSON.stringify({ messages: [] }),
63
+ });
64
+ await mod.POST(req);
65
+
66
+ // Act + Assert
67
+ expect(capturedGetErrorMessage!({ code: 42 })).toBe('{"code":42}');
68
+ });
69
+
70
+ it("getErrorMessage returns String() for primitives", async () => {
71
+ // Arrange
72
+ const mod = await import("../../../app/api/chat/route");
73
+ const req = new Request("http://localhost/api/chat", {
74
+ method: "POST",
75
+ body: JSON.stringify({ messages: [] }),
76
+ });
77
+ await mod.POST(req);
78
+
79
+ // Act + Assert
80
+ expect(capturedGetErrorMessage!("plain string")).toBe("plain string");
81
+ });
19
82
  });
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ vi.mock("@ai-sdk/react", () => ({
4
+ useChat: vi.fn(() => ({ messages: [], input: "" })),
5
+ }));
6
+
7
+ describe("use-chat re-export", () => {
8
+ it("re-exports useChat from @ai-sdk/react", async () => {
9
+ // Arrange + Act
10
+ const mod = await import("../hooks/use-chat");
11
+
12
+ // Assert
13
+ expect(typeof mod.useChat).toBe("function");
14
+ });
15
+ });
@@ -18,7 +18,7 @@ export function ChatUi() {
18
18
  message.role === "user" ? "bg-primary/10 ml-8" : "bg-muted mr-8"
19
19
  }`}
20
20
  >
21
- <p className="text-sm">{message.content}</p>
21
+ <p className="text-sm">{message.content.replace(/<think>[\s\S]*?<\/think>\s*/g, "")}</p>
22
22
  </div>
23
23
  ))}
24
24
  {isLoading && (
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { NextRequest } from "next/server";
3
+
4
+ const mockUpdateSession = vi.fn();
5
+
6
+ vi.mock("@/shared/lib/supabase/middleware", () => ({
7
+ updateSession: (...args: unknown[]) => mockUpdateSession(...args),
8
+ }));
9
+
10
+ describe("middleware", () => {
11
+ it("delegates to updateSession", async () => {
12
+ // Arrange
13
+ const mockResponse = new Response(null, { status: 200 });
14
+ mockUpdateSession.mockResolvedValue(mockResponse);
15
+ const { middleware } = await import("../../middleware");
16
+ const req = new NextRequest("http://localhost/dashboard");
17
+
18
+ // Act
19
+ const response = await middleware(req);
20
+
21
+ // Assert
22
+ expect(mockUpdateSession).toHaveBeenCalledWith(req);
23
+ expect(response).toBe(mockResponse);
24
+ });
25
+
26
+ it("exports a matcher config", async () => {
27
+ // Arrange + Act
28
+ const { config } = await import("../../middleware");
29
+
30
+ // Assert
31
+ expect(config.matcher).toBeDefined();
32
+ expect(Array.isArray(config.matcher)).toBe(true);
33
+ });
34
+ });
@@ -4,16 +4,29 @@ import { insertProfileSchema, selectProfileSchema } from "../db/schema";
4
4
 
5
5
  describe("profiles schema", () => {
6
6
  it("insertProfileSchema validates required fields", () => {
7
- const result = insertProfileSchema.safeParse({ id: "uuid", email: "test@example.com" });
7
+ // Arrange + Act
8
+ const result = insertProfileSchema.safeParse({
9
+ id: "123e4567-e89b-12d3-a456-426614174000",
10
+ email: "test@example.com",
11
+ });
12
+
13
+ // Assert
8
14
  expect(result.success).toBe(true);
9
15
  });
10
16
 
11
- it("insertProfileSchema rejects invalid email", () => {
12
- const result = insertProfileSchema.safeParse({ id: "uuid", email: "not-email" });
17
+ it("insertProfileSchema rejects invalid uuid", () => {
18
+ // Arrange + Act
19
+ const result = insertProfileSchema.safeParse({
20
+ id: "not-a-uuid",
21
+ email: "test@example.com",
22
+ });
23
+
24
+ // Assert
13
25
  expect(result.success).toBe(false);
14
26
  });
15
27
 
16
28
  it("selectProfileSchema is defined", () => {
29
+ // Arrange + Act + Assert
17
30
  expect(selectProfileSchema).toBeDefined();
18
31
  });
19
32
  });
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const mockGetUser = vi.fn();
4
+ const mockCreateServerClient = vi.fn();
5
+ const mockNextResponseNext = vi.fn();
6
+ const mockNextResponseRedirect = vi.fn();
7
+
8
+ vi.mock("@supabase/ssr", () => ({
9
+ createServerClient: (...args: unknown[]) => mockCreateServerClient(...args),
10
+ }));
11
+
12
+ vi.mock("next/server", async () => {
13
+ const actual = await vi.importActual<typeof import("next/server")>("next/server");
14
+ return {
15
+ ...actual,
16
+ NextResponse: {
17
+ next: (...args: unknown[]) => mockNextResponseNext(...args),
18
+ redirect: (...args: unknown[]) => mockNextResponseRedirect(...args),
19
+ },
20
+ };
21
+ });
22
+
23
+ type CookieOptions = { path?: string; maxAge?: number };
24
+ type CookieItem = { name: string; value: string; options: CookieOptions };
25
+ type CookieCallbacks = {
26
+ getAll: () => CookieItem[];
27
+ setAll: (cookies: CookieItem[]) => void;
28
+ };
29
+
30
+ function makeRequest(pathname: string) {
31
+ return {
32
+ nextUrl: {
33
+ pathname,
34
+ clone: () => ({ pathname, toString: () => `http://localhost${pathname}` }),
35
+ },
36
+ cookies: { getAll: vi.fn(() => []), set: vi.fn() },
37
+ headers: new Headers(),
38
+ url: `http://localhost${pathname}`,
39
+ } as unknown as import("next/server").NextRequest;
40
+ }
41
+
42
+ describe("supabase middleware (updateSession)", () => {
43
+ let capturedCookieCallbacks: CookieCallbacks | undefined;
44
+ const fakeResponse = {
45
+ cookies: { set: vi.fn(), getAll: vi.fn(() => []) },
46
+ status: 200,
47
+ headers: new Headers(),
48
+ };
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ capturedCookieCallbacks = undefined;
53
+ mockNextResponseNext.mockReturnValue(fakeResponse);
54
+ mockNextResponseRedirect.mockImplementation((url: unknown) => ({
55
+ status: 307,
56
+ headers: new Headers({ location: String(url) }),
57
+ }));
58
+ mockCreateServerClient.mockImplementation(
59
+ (_u: unknown, _k: unknown, opts: { cookies: CookieCallbacks }) => {
60
+ capturedCookieCallbacks = opts.cookies;
61
+ return { auth: { getUser: mockGetUser } };
62
+ }
63
+ );
64
+ });
65
+
66
+ it("returns nextResponse for authenticated user on protected route", async () => {
67
+ // Arrange
68
+ mockGetUser.mockResolvedValue({ data: { user: { id: "user-1" } } });
69
+ const { updateSession } = await import("../lib/supabase/middleware");
70
+ const req = makeRequest("/dashboard");
71
+
72
+ // Act
73
+ const response = await updateSession(req);
74
+
75
+ // Assert
76
+ expect(response).toBeDefined();
77
+ expect(mockNextResponseRedirect).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it("invokes getAll cookie callback", async () => {
81
+ // Arrange
82
+ mockGetUser.mockResolvedValue({ data: { user: { id: "user-1" } } });
83
+ const { updateSession } = await import("../lib/supabase/middleware");
84
+ const req = makeRequest("/dashboard");
85
+ await updateSession(req);
86
+
87
+ // Act
88
+ capturedCookieCallbacks!.getAll();
89
+
90
+ // Assert
91
+ expect(req.cookies.getAll).toHaveBeenCalled();
92
+ });
93
+
94
+ it("invokes setAll cookie callback and updates request + response cookies", async () => {
95
+ // Arrange
96
+ mockGetUser.mockResolvedValue({ data: { user: { id: "user-1" } } });
97
+ const { updateSession } = await import("../lib/supabase/middleware");
98
+ const req = makeRequest("/dashboard");
99
+ await updateSession(req);
100
+
101
+ // Act
102
+ capturedCookieCallbacks!.setAll([
103
+ { name: "sb-token", value: "abc", options: { path: "/" } },
104
+ ]);
105
+
106
+ // Assert
107
+ expect(req.cookies.set).toHaveBeenCalledWith("sb-token", "abc");
108
+ expect(fakeResponse.cookies.set).toHaveBeenCalledWith("sb-token", "abc", { path: "/" });
109
+ });
110
+
111
+ it("redirects unauthenticated user from protected route", async () => {
112
+ // Arrange
113
+ mockGetUser.mockResolvedValue({ data: { user: null } });
114
+ const { updateSession } = await import("../lib/supabase/middleware");
115
+ const req = makeRequest("/dashboard");
116
+
117
+ // Act
118
+ await updateSession(req);
119
+
120
+ // Assert
121
+ expect(mockNextResponseRedirect).toHaveBeenCalled();
122
+ });
123
+
124
+ it("does not redirect unauthenticated user on login route", async () => {
125
+ // Arrange
126
+ mockGetUser.mockResolvedValue({ data: { user: null } });
127
+ const { updateSession } = await import("../lib/supabase/middleware");
128
+ const req = makeRequest("/login");
129
+
130
+ // Act
131
+ await updateSession(req);
132
+
133
+ // Assert
134
+ expect(mockNextResponseRedirect).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it("does not redirect unauthenticated user on root path", async () => {
138
+ // Arrange
139
+ mockGetUser.mockResolvedValue({ data: { user: null } });
140
+ const { updateSession } = await import("../lib/supabase/middleware");
141
+ const req = makeRequest("/");
142
+
143
+ // Act
144
+ await updateSession(req);
145
+
146
+ // Assert
147
+ expect(mockNextResponseRedirect).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it("does not redirect on /api/auth routes", async () => {
151
+ // Arrange
152
+ mockGetUser.mockResolvedValue({ data: { user: null } });
153
+ const { updateSession } = await import("../lib/supabase/middleware");
154
+ const req = makeRequest("/api/auth/callback");
155
+
156
+ // Act
157
+ await updateSession(req);
158
+
159
+ // Assert
160
+ expect(mockNextResponseRedirect).not.toHaveBeenCalled();
161
+ });
162
+ });