Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@ jobs:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/geev_test
run: npx prisma migrate deploy



- name: Run tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/geev_test
JWT_SECRET: test-secret-key
NEXTAUTH_SECRET: test-secret-key
NODE_ENV: test
run: npm run test:ci

Expand Down
7 changes: 2 additions & 5 deletions app/app/(auth)/logout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,10 @@ export async function POST(request: Request) {
expires: new Date(0), // Set to epoch to ensure expiration
};

// 1. Clear the legacy auth-token
response.cookies.set("auth-token", "", cookieOptions);

// 2. Clear Auth.js session token (standard)
// 1. Clear Auth.js session token (standard)
response.cookies.set("next-auth.session-token", "", cookieOptions);

// 3. Clear Auth.js session token (secure version used in production/HTTPS)
// 2. Clear Auth.js session token (secure version used in production/HTTPS)
response.cookies.set("__Secure-next-auth.session-token", "", cookieOptions);

return response;
Expand Down
80 changes: 0 additions & 80 deletions app/app/(auth)/session/route.ts

This file was deleted.

4 changes: 2 additions & 2 deletions app/app/api/indexer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { apiError } from "@/lib/api-response";
/**
* POST handler - Trigger indexer run
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
export async function POST(request: NextRequest): Promise<Response> {
try {
const admin = await getCurrentAdmin();
if (!admin) return apiError("Forbidden", 403);
Expand Down Expand Up @@ -51,7 +51,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
/**
* GET handler - Get indexer statistics
*/
export async function GET(): Promise<NextResponse> {
export async function GET(): Promise<Response> {
try {
const admin = await getCurrentAdmin();
if (!admin) return apiError("Forbidden", 403);
Expand Down
2 changes: 1 addition & 1 deletion app/app/api/wallet/withdraw/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export async function POST(request: NextRequest) {
originalCurrency: "USD",
convertedAmount: result.transaction.convertedAmount,
convertedCurrency: "XLM",
rate: (result.transaction.convertedAmount / amount).toFixed(7),
rate: ((result.transaction.convertedAmount ?? amount) / amount).toFixed(7),
},
},
"Withdrawal initiated successfully",
Expand Down
21 changes: 6 additions & 15 deletions app/lib/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(
process.env.NEXTAUTH_SECRET || "your-secret-key-change-in-production",
);
const secretValue = process.env.NEXTAUTH_SECRET;
if (!secretValue) {
throw new Error("NEXTAUTH_SECRET is missing. It must be set in production config.");
}

const secret = new TextEncoder().encode(secretValue);

export interface JWTPayload {
userId: string;
Expand Down Expand Up @@ -47,17 +50,5 @@ export function getTokenFromRequest(request: Request): string | null {
return authHeader.slice(7);
}

// Try to get from cookies
const cookieHeader = request.headers.get("cookie");
if (cookieHeader) {
const cookies = cookieHeader.split(";");
for (const cookie of cookies) {
const [name, value] = cookie.trim().split("=");
if (name === "token" || name === "auth-token") {
return value;
}
}
}

return null;
}
9 changes: 1 addition & 8 deletions app/tests/api/auth/logout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,14 @@ import { POST } from "@/app/(auth)/logout/route";
import { NextRequest } from "next/server";

describe("Logout API Route", () => {
it("should clear the legacy auth-token and Auth.js session cookies", async () => {
it("should clear Auth.js session cookies", async () => {
const request = new NextRequest("http://localhost/api/auth/logout", {
method: "POST",
});

const response = await POST(request);
const cookies = response.cookies.getAll();

// Check for legacy auth-token
const authTokenCookie = cookies.find((c) => c.name === "auth-token");
expect(authTokenCookie).toBeDefined();
expect(authTokenCookie?.value).toBe("");
expect(authTokenCookie?.maxAge).toBe(0);

// Check for Auth.js cookies
const sessionTokenCookie = cookies.find(
(c) => c.name === "next-auth.session-token"
Expand Down Expand Up @@ -60,7 +54,6 @@ describe("Logout API Route", () => {
const cookies = response.cookies.getAll();

// The response should still contain the "clear" instructions (Max-Age=0)
expect(cookies.some((c) => c.name === "auth-token")).toBe(true);
expect(cookies.some((c) => c.name === "next-auth.session-token")).toBe(true);
expect(cookies.some((c) => c.name === "__Secure-next-auth.session-token")).toBe(true);
});
Expand Down
8 changes: 4 additions & 4 deletions app/tests/api/entries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,15 @@ describe('Entry API Endpoints', () => {

prisma.entry.count = vi.fn().mockResolvedValue(0);
prisma.entry.updateMany = vi.fn().mockResolvedValue({ count: 0 });
prisma.postWinner = {
(prisma as any).postWinner = {
count: vi.fn().mockResolvedValue(0),
createMany: vi.fn().mockResolvedValue({ count: 0 }),
} as any;
};
prisma.post.update = vi.fn().mockResolvedValue(post);
prisma.notification = {
(prisma as any).notification = {
create: vi.fn().mockResolvedValue({}),
createMany: vi.fn().mockResolvedValue({ count: 0 }),
} as any;
};
});

describe('POST /api/posts/[id]/entries', () => {
Expand Down
8 changes: 2 additions & 6 deletions app/tests/api/indexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ describe("GET /api/indexer/stats", () => {
it("returns 403 for unauthenticated callers", async () => {
mockGetCurrentAdmin.mockResolvedValue(null);

const response = await GET(
createMockRequest("http://localhost:3000/api/indexer/stats"),
);
const response = await GET();
const { status, data } = await parseResponse(response);

expect(status).toBe(403);
Expand All @@ -114,9 +112,7 @@ describe("GET /api/indexer/stats", () => {
status: "synced",
});

const response = await GET(
createMockRequest("http://localhost:3000/api/indexer/stats"),
);
const response = await GET();
const { status, data } = await parseResponse(response);

expect(status).toBe(200);
Expand Down
17 changes: 9 additions & 8 deletions app/tests/api/post-slug-generation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { NextRequest } from 'next/server';

const mockPrisma = vi.hoisted(() => ({
$transaction: vi.fn(),
Expand Down Expand Up @@ -80,13 +81,13 @@ describe('POST /api/posts slug generation', () => {
xp: 50,
rankId: 'newcomer',
});
prisma.$transaction.mockImplementation(async (callback: any) => callback(prisma));
prisma.post.findUnique.mockResolvedValue(null);
prisma.postRequirements.create.mockResolvedValue({
(prisma.$transaction as any).mockImplementation(async (callback: any) => callback(prisma));
(prisma.post.findUnique as any).mockResolvedValue(null);
(prisma.postRequirements.create as any).mockResolvedValue({
id: 'requirements_123',
proofRequired: false,
});
prisma.post.create.mockImplementation((args: any) => Promise.resolve({
(prisma.post.create as any).mockImplementation((args: any) => Promise.resolve({
id: 'post_123',
...args.data,
createdAt: new Date(),
Expand All @@ -105,11 +106,11 @@ describe('POST /api/posts slug generation', () => {
endsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
};

prisma.post.findUnique
(prisma.post.findUnique as any)
.mockResolvedValueOnce({ id: 'existing_post' })
.mockResolvedValueOnce(null);

const request = createMockRequest('http://localhost:3000/api/posts', postData);
const request = createMockRequest('http://localhost:3000/api/posts', postData) as unknown as NextRequest;

const response = await POST(request);
const { status, data } = await parseResponse(response);
Expand All @@ -133,9 +134,9 @@ describe('POST /api/posts slug generation', () => {
endsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
};

prisma.post.create.mockRejectedValue({ code: 'P2002' });
(prisma.post.create as any).mockRejectedValue({ code: 'P2002' });

const request = createMockRequest('http://localhost:3000/api/posts', postData);
const request = createMockRequest('http://localhost:3000/api/posts', postData) as unknown as NextRequest;

const response = await POST(request);
const { status, data } = await parseResponse(response);
Expand Down
9 changes: 4 additions & 5 deletions app/tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { test, expect, describe } from "vitest";
import { test, expect, describe, vi } from "vitest";

vi.stubEnv("NEXTAUTH_SECRET", "test-secret-key");

import { createToken, verifyToken } from "@/lib/jwt";

describe("Authentication System", () => {
Expand Down Expand Up @@ -58,13 +61,9 @@ describe("Authentication System", () => {
const response = await POST(request);
const cookies = response.cookies.getAll();

expect(cookies.some(c => c.name === "auth-token")).toBe(true);
expect(cookies.some(c => c.name === "next-auth.session-token")).toBe(true);
expect(response.status).toBe(200);
});
test.skip("GET /api/auth/session should return 401 without token", async () => {
// Skipped
});
});

describe("Authentication Middleware", () => {
Expand Down
25 changes: 25 additions & 0 deletions app/tests/jwt-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { test, expect, describe, vi, beforeEach } from "vitest";

describe("JWT Configuration Fail-Fast", () => {
beforeEach(() => {
vi.resetModules(); // Ensure we get a fresh import of jwt.ts each time
});

test("should throw an Error on import if NEXTAUTH_SECRET is missing", async () => {
// Explicitly unset the secret to simulate missing production config
vi.stubEnv("NEXTAUTH_SECRET", "");

await expect(async () => {
await import("@/lib/jwt");
}).rejects.toThrow("NEXTAUTH_SECRET is missing. It must be set in production config.");
});

test("should successfully import if NEXTAUTH_SECRET is provided", async () => {
// Provide a valid secret
vi.stubEnv("NEXTAUTH_SECRET", "valid-secret-for-test");

const jwt = await import("@/lib/jwt");
expect(jwt.createToken).toBeDefined();
expect(jwt.verifyToken).toBeDefined();
});
});
Loading