nextjs-hackathon-stack 0.1.10 → 0.1.12

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 (35) hide show
  1. package/dist/index.js +7 -0
  2. package/package.json +1 -1
  3. package/template/.husky/pre-commit +1 -0
  4. package/template/_env.example +7 -5
  5. package/template/package.json.tmpl +8 -2
  6. package/template/src/app/(protected)/page.tsx +9 -1
  7. package/template/src/app/__tests__/auth-callback.test.ts +72 -0
  8. package/template/src/app/__tests__/error.test.tsx +71 -0
  9. package/template/src/app/__tests__/layout.test.tsx +22 -0
  10. package/template/src/app/__tests__/login-page.test.tsx +34 -0
  11. package/template/src/app/__tests__/protected-layout.test.tsx +43 -0
  12. package/template/src/app/__tests__/protected-page.test.tsx +41 -0
  13. package/template/src/app/api/chat/route.ts +3 -1
  14. package/template/src/features/auth/__tests__/login-form.test.tsx +53 -2
  15. package/template/src/features/auth/__tests__/login.action.test.ts +82 -0
  16. package/template/src/features/auth/__tests__/logout.action.test.ts +32 -0
  17. package/template/src/features/auth/actions/login.action.ts +1 -1
  18. package/template/src/features/auth/components/login-form.tsx +4 -3
  19. package/template/src/features/chat/__tests__/chat-ui.test.tsx +97 -7
  20. package/template/src/features/chat/__tests__/route.test.ts +66 -3
  21. package/template/src/features/chat/__tests__/use-chat.test.ts +15 -0
  22. package/template/src/features/chat/components/chat-ui.tsx +1 -1
  23. package/template/src/features/tts/__tests__/route.test.ts +78 -3
  24. package/template/src/features/tts/__tests__/tts-player.test.tsx +65 -3
  25. package/template/src/features/video/__tests__/route.test.ts +78 -3
  26. package/template/src/features/video/__tests__/video-generator.test.tsx +65 -3
  27. package/template/src/shared/__tests__/middleware.test.ts +34 -0
  28. package/template/src/shared/__tests__/schema.test.ts +16 -3
  29. package/template/src/shared/__tests__/supabase-middleware.test.ts +162 -0
  30. package/template/src/shared/__tests__/supabase-server.test.ts +76 -3
  31. package/template/src/shared/__tests__/ui-button.test.tsx +52 -0
  32. package/template/src/shared/__tests__/ui-card.test.tsx +78 -0
  33. package/template/src/shared/__tests__/utils.test.ts +30 -0
  34. package/template/src/shared/lib/ai.ts +4 -4
  35. package/template/vitest.config.ts +5 -0
@@ -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
+ });
@@ -1,22 +1,95 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
2
 
3
+ const mockSet = vi.fn();
4
+ const mockGetAll = vi.fn().mockReturnValue([]);
5
+ const mockCreateServerClient = vi.fn();
6
+
3
7
  vi.mock("@supabase/ssr", () => ({
4
- createServerClient: vi.fn(() => ({ auth: {} })),
8
+ createServerClient: (...args: unknown[]) => mockCreateServerClient(...args),
5
9
  }));
6
10
 
7
11
  vi.mock("next/headers", () => ({
8
12
  cookies: vi.fn(() =>
9
13
  Promise.resolve({
10
- getAll: () => [],
11
- set: vi.fn(),
14
+ getAll: mockGetAll,
15
+ set: mockSet,
12
16
  })
13
17
  ),
14
18
  }));
15
19
 
16
20
  describe("supabase server", () => {
17
21
  it("createClient returns a supabase client", async () => {
22
+ // Arrange
23
+ mockCreateServerClient.mockReturnValue({ auth: {} });
18
24
  const { createClient } = await import("../lib/supabase/server");
25
+
26
+ // Act
19
27
  const client = await createClient();
28
+
29
+ // Assert
20
30
  expect(client).toBeDefined();
21
31
  });
32
+
33
+ it("passes cookie getAll to server client config", async () => {
34
+ // Arrange
35
+ let capturedGetAll: (() => unknown) | undefined;
36
+ mockCreateServerClient.mockImplementation(
37
+ (_u: unknown, _k: unknown, opts: { cookies: { getAll: () => unknown } }) => {
38
+ capturedGetAll = opts.cookies.getAll;
39
+ return { auth: {} };
40
+ }
41
+ );
42
+ const { createClient } = await import("../lib/supabase/server");
43
+
44
+ // Act
45
+ await createClient();
46
+ capturedGetAll!();
47
+
48
+ // Assert
49
+ expect(mockGetAll).toHaveBeenCalled();
50
+ });
51
+
52
+ it("setAll sets cookies via cookieStore", async () => {
53
+ // Arrange
54
+ let capturedSetAll:
55
+ | ((cookies: { name: string; value: string; options: object }[]) => void)
56
+ | undefined;
57
+ mockCreateServerClient.mockImplementation(
58
+ (_u: unknown, _k: unknown, opts: { cookies: { setAll: (c: { name: string; value: string; options: object }[]) => void } }) => {
59
+ capturedSetAll = opts.cookies.setAll;
60
+ return { auth: {} };
61
+ }
62
+ );
63
+ const { createClient } = await import("../lib/supabase/server");
64
+ await createClient();
65
+
66
+ // Act
67
+ capturedSetAll!([{ name: "token", value: "abc", options: { path: "/" } }]);
68
+
69
+ // Assert
70
+ expect(mockSet).toHaveBeenCalledWith("token", "abc", { path: "/" });
71
+ });
72
+
73
+ it("setAll silently swallows errors from server components", async () => {
74
+ // Arrange
75
+ mockSet.mockImplementation(() => {
76
+ throw new Error("Cannot set cookie");
77
+ });
78
+ let capturedSetAll:
79
+ | ((cookies: { name: string; value: string; options: object }[]) => void)
80
+ | undefined;
81
+ mockCreateServerClient.mockImplementation(
82
+ (_u: unknown, _k: unknown, opts: { cookies: { setAll: (c: { name: string; value: string; options: object }[]) => void } }) => {
83
+ capturedSetAll = opts.cookies.setAll;
84
+ return { auth: {} };
85
+ }
86
+ );
87
+ const { createClient } = await import("../lib/supabase/server");
88
+ await createClient();
89
+
90
+ // Act + Assert
91
+ expect(() =>
92
+ capturedSetAll!([{ name: "token", value: "abc", options: {} }])
93
+ ).not.toThrow();
94
+ });
22
95
  });
@@ -0,0 +1,52 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ import { Button } from "../components/ui/button";
5
+
6
+ describe("Button", () => {
7
+ it("renders with default variant", () => {
8
+ // Arrange + Act
9
+ render(<Button>Click me</Button>);
10
+
11
+ // Assert
12
+ expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument();
13
+ });
14
+
15
+ it("renders as disabled when disabled prop is set", () => {
16
+ // Arrange + Act
17
+ render(<Button disabled>Submit</Button>);
18
+
19
+ // Assert
20
+ expect(screen.getByRole("button")).toBeDisabled();
21
+ });
22
+
23
+ it("renders with custom className", () => {
24
+ // Arrange + Act
25
+ render(<Button className="custom-class">Click</Button>);
26
+
27
+ // Assert
28
+ expect(screen.getByRole("button")).toHaveClass("custom-class");
29
+ });
30
+
31
+ it("renders as a Slot (asChild) when asChild is true", () => {
32
+ // Arrange + Act
33
+ render(
34
+ <Button asChild>
35
+ <a href="/home">Go home</a>
36
+ </Button>
37
+ );
38
+
39
+ // Assert
40
+ expect(screen.getByRole("link", { name: /go home/i })).toBeInTheDocument();
41
+ });
42
+
43
+ it("renders with destructive variant", () => {
44
+ // Arrange + Act
45
+ render(<Button variant="destructive">Delete</Button>);
46
+
47
+ // Assert
48
+ const btn = screen.getByRole("button", { name: /delete/i });
49
+ expect(btn).toBeInTheDocument();
50
+ expect(btn.className).toContain("destructive");
51
+ });
52
+ });
@@ -0,0 +1,78 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ import {
5
+ Card,
6
+ CardAction,
7
+ CardContent,
8
+ CardDescription,
9
+ CardFooter,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "../components/ui/card";
13
+
14
+ describe("Card components", () => {
15
+ it("renders Card with children", () => {
16
+ // Arrange + Act
17
+ render(<Card data-testid="card">content</Card>);
18
+
19
+ // Assert
20
+ expect(screen.getByTestId("card")).toBeInTheDocument();
21
+ });
22
+
23
+ it("renders CardHeader", () => {
24
+ // Arrange + Act
25
+ render(<CardHeader data-testid="header">header</CardHeader>);
26
+
27
+ // Assert
28
+ expect(screen.getByTestId("header")).toBeInTheDocument();
29
+ });
30
+
31
+ it("renders CardTitle", () => {
32
+ // Arrange + Act
33
+ render(<CardTitle>My Title</CardTitle>);
34
+
35
+ // Assert
36
+ expect(screen.getByText("My Title")).toBeInTheDocument();
37
+ });
38
+
39
+ it("renders CardDescription", () => {
40
+ // Arrange + Act
41
+ render(<CardDescription>My Description</CardDescription>);
42
+
43
+ // Assert
44
+ expect(screen.getByText("My Description")).toBeInTheDocument();
45
+ });
46
+
47
+ it("renders CardContent", () => {
48
+ // Arrange + Act
49
+ render(<CardContent data-testid="content">body</CardContent>);
50
+
51
+ // Assert
52
+ expect(screen.getByTestId("content")).toBeInTheDocument();
53
+ });
54
+
55
+ it("renders CardAction", () => {
56
+ // Arrange + Act
57
+ render(<CardAction data-testid="action">action</CardAction>);
58
+
59
+ // Assert
60
+ expect(screen.getByTestId("action")).toBeInTheDocument();
61
+ });
62
+
63
+ it("renders CardFooter", () => {
64
+ // Arrange + Act
65
+ render(<CardFooter data-testid="footer">footer</CardFooter>);
66
+
67
+ // Assert
68
+ expect(screen.getByTestId("footer")).toBeInTheDocument();
69
+ });
70
+
71
+ it("applies custom className to Card", () => {
72
+ // Arrange + Act
73
+ render(<Card className="custom-class" data-testid="card">content</Card>);
74
+
75
+ // Assert
76
+ expect(screen.getByTestId("card")).toHaveClass("custom-class");
77
+ });
78
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ import { cn } from "../lib/utils";
4
+
5
+ describe("cn", () => {
6
+ it("merges multiple class names", () => {
7
+ // Arrange + Act + Assert
8
+ expect(cn("foo", "bar")).toBe("foo bar");
9
+ });
10
+
11
+ it("omits falsy conditional classes", () => {
12
+ // Arrange + Act + Assert
13
+ expect(cn("foo", false && "bar", "baz")).toBe("foo baz");
14
+ });
15
+
16
+ it("handles undefined values", () => {
17
+ // Arrange + Act + Assert
18
+ expect(cn("foo", undefined, "bar")).toBe("foo bar");
19
+ });
20
+
21
+ it("deduplicates conflicting tailwind classes (last one wins)", () => {
22
+ // Arrange + Act + Assert
23
+ expect(cn("p-2", "p-4")).toBe("p-4");
24
+ });
25
+
26
+ it("handles empty call", () => {
27
+ // Arrange + Act + Assert
28
+ expect(cn()).toBe("");
29
+ });
30
+ });
@@ -1,8 +1,8 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
2
 
3
- const gateway = createOpenAI({
4
- baseURL: process.env.AI_GATEWAY_URL ?? "https://ai-gateway.vercel.sh/v1",
5
- apiKey: process.env.AI_GATEWAY_API_KEY ?? "",
3
+ const provider = createOpenAI({
4
+ baseURL: process.env.AI_BASE_URL ?? "https://api.minimax.io/v1",
5
+ apiKey: process.env.AI_API_KEY ?? "",
6
6
  });
7
7
 
8
- export const aiModel = gateway("minimax/minimax-m2.7");
8
+ export const aiModel = provider(process.env.AI_MODEL ?? "MiniMax-M2.7");
@@ -5,8 +5,10 @@ import { resolve } from "path";
5
5
  export default defineConfig({
6
6
  plugins: [react()],
7
7
  test: {
8
+ globals: true,
8
9
  environment: "jsdom",
9
10
  setupFiles: ["./src/test-setup.ts"],
11
+ exclude: ["node_modules/**", "src/e2e/**"],
10
12
  coverage: {
11
13
  provider: "v8",
12
14
  thresholds: {
@@ -22,6 +24,9 @@ export default defineConfig({
22
24
  "**/*.config.*",
23
25
  "**/index.ts",
24
26
  "src/app/globals.css",
27
+ ".next/**",
28
+ "next-env.d.ts",
29
+ "**/*.d.ts",
25
30
  ],
26
31
  },
27
32
  },