Skip to content
Open
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
111 changes: 111 additions & 0 deletions apps/backend/src/__tests__/auth-validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import cookiePlugin from '@fastify/cookie';
import jwtPlugin from '@fastify/jwt';
import Fastify, { type FastifyInstance } from 'fastify';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

import { authRoutes } from '../routes/auth.js';

async function buildTestApp() {

Check warning on line 8 in apps/backend/src/__tests__/auth-validation.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
const app = Fastify({ logger: false });
await app.register(cookiePlugin as any);
await app.register(jwtPlugin as any, { secret: 'test-secret-for-unit-tests-only' });

app.decorate('prisma', {
refreshToken: {
findUnique: vi.fn(),
update: vi.fn(),
create: vi.fn(),
updateMany: vi.fn(),
},
user: {
findUnique: vi.fn()
}
} as any);

app.decorate('redis', {
getdel: vi.fn(),
set: vi.fn(),
exists: vi.fn(),
} as any);

app.decorate('authenticate', async () => {});

await app.register(authRoutes, { prefix: '/auth' });
await app.ready();
return app;
}

describe('auth validation', () => {
let app: FastifyInstance;
Comment thread
Chaitanya-970 marked this conversation as resolved.

beforeEach(async () => {
app = await buildTestApp();
});

afterEach(async () => {
await app.close();
});

describe('POST /auth/mobile/exchange', () => {
it('rejects missing code', async () => {
const res = await app.inject({
method: 'POST',
url: '/auth/mobile/exchange',
payload: {}
});
expect(res.statusCode).toBe(400);
expect(res.json().error).toBe('Invalid request body');
});

it('rejects non-string code', async () => {
const res = await app.inject({
method: 'POST',
url: '/auth/mobile/exchange',
payload: { code: 123 }
});
expect(res.statusCode).toBe(400);
expect(res.json().error).toBe('Invalid request body');
});

it('rejects empty code', async () => {
const res = await app.inject({
method: 'POST',
url: '/auth/mobile/exchange',
payload: { code: '' }
});
expect(res.statusCode).toBe(400);
expect(res.json().error).toBe('Invalid request body');
});
});

describe('POST /auth/refresh', () => {
it('rejects empty refresh_token in body', async () => {
const res = await app.inject({
method: 'POST',
url: '/auth/refresh',
payload: { refresh_token: '' }
});
expect(res.statusCode).toBe(400);
expect(res.json().error).toBe('Invalid request body');
});

it('rejects non-string refresh_token in body', async () => {
const res = await app.inject({
method: 'POST',
url: '/auth/refresh',
payload: { refresh_token: 123 }
});
expect(res.statusCode).toBe(400);
expect(res.json().error).toBe('Invalid request body');
});

it('fails when no token is provided', async () => {
const res = await app.inject({
method: 'POST',
url: '/auth/refresh'
});
expect(res.statusCode).toBe(401);
expect(res.json().error).toBe('Refresh token missing');
});
});
});
16 changes: 14 additions & 2 deletions apps/backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { handleDbError, isGitHubTokenError, isGoogleTokenError } from '../utils/
import { extractRawJwt, blocklistKey, signAccessToken } from '../utils/jwt.js';
import { buildOAuthState, getMobileRedirectUri } from '../utils/oauth.js';
import { generateRefreshToken, hashIp, hashRefreshToken } from '../utils/refreshToken.js';
import { mobileExchangeSchema, refreshTokenSchema } from '../validations/auth.validation.js';

import type { GitHubTokenErrorResponse, GitHubTokenResponse } from '../utils/error.util.js';
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
Expand Down Expand Up @@ -495,7 +496,14 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
});

app.post('/refresh', async(request: FastifyRequest, reply: FastifyReply) => {
const refreshToken = request.cookies.refresh_token ?? (request.body as { refresh_token?: string })?.refresh_token;
if (request.body && typeof request.body === 'object' && 'refresh_token' in request.body) {
const parsed = refreshTokenSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.flatten() });
}
}

const refreshToken = request.cookies.refresh_token ?? (request.body as { refresh_token?: string })?.refresh_token;

if (!refreshToken) {
return reply.status(401).send({
Expand Down Expand Up @@ -600,7 +608,11 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
})

app.post('/mobile/exchange', async (request: FastifyRequest<{Body: {code: string}}>, reply: FastifyReply) => {
const { code } = request.body;
const parsed = mobileExchangeSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.flatten() });
}
const { code } = parsed.data;
Comment thread
Chaitanya-970 marked this conversation as resolved.
const raw = await app.redis.getdel(`mobile_exchange:${code}`);
if (!raw) {return reply.status(400).send({ error: 'Invalid or expired exchange code' });}

Expand Down
15 changes: 15 additions & 0 deletions apps/backend/src/validations/auth.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from 'zod';

export const mobileExchangeSchema = z.object({
code: z.string({
required_error: 'missing code',
invalid_type_error: 'code must be a string'
}).trim().min(1, 'code is empty')
});
Comment thread
Chaitanya-970 marked this conversation as resolved.

export const refreshTokenSchema = z.object({
refresh_token: z.string({
required_error: 'missing refresh_token',
invalid_type_error: 'refresh_token must be a string'
}).trim().min(1, 'refresh_token is empty')
});
Comment thread
Chaitanya-970 marked this conversation as resolved.