Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 50s
203 lines
8.3 KiB
TypeScript
203 lines
8.3 KiB
TypeScript
// src/tests/integration/auth.integration.test.ts
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import app from '../../../server';
|
|
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
|
|
import { cleanupDb } from '../utils/cleanup';
|
|
import type { UserProfile } from '../../types';
|
|
|
|
/**
|
|
* @vitest-environment node
|
|
*/
|
|
|
|
const request = supertest(app);
|
|
|
|
/**
|
|
* These are integration tests that verify the authentication flow against a running backend server.
|
|
* Make sure your Express server is running before executing these tests.
|
|
*
|
|
* To run only these tests: `vitest run src/tests/auth.integration.test.ts`
|
|
*/
|
|
describe('Authentication API Integration', () => {
|
|
let testUserEmail: string;
|
|
let testUser: UserProfile;
|
|
const createdUserIds: string[] = [];
|
|
|
|
beforeAll(async () => {
|
|
// Use a unique email for this test suite to prevent collisions with other tests.
|
|
const email = `auth-integration-test-${Date.now()}@example.com`;
|
|
({ user: testUser } = await createAndLoginUser({ email, fullName: 'Auth Test User', request }));
|
|
testUserEmail = testUser.user.email;
|
|
createdUserIds.push(testUser.user.user_id);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await cleanupDb({ userIds: createdUserIds });
|
|
});
|
|
|
|
// This test migrates the logic from the old DevTestRunner.tsx component.
|
|
it('should successfully log in a registered user', async () => {
|
|
// The `rememberMe` parameter is required. For a test, `false` is a safe default.
|
|
const response = await request
|
|
.post('/api/auth/login')
|
|
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: false });
|
|
const data = response.body;
|
|
|
|
if (response.status !== 200) {
|
|
console.error('[DEBUG] Login failed:', response.status, JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
// Assert that the API returns the expected structure
|
|
expect(data).toBeDefined();
|
|
expect(response.status).toBe(200);
|
|
expect(data.userprofile).toBeDefined();
|
|
expect(data.userprofile.user.email).toBe(testUserEmail);
|
|
expect(data.userprofile.user.user_id).toBeTypeOf('string');
|
|
expect(data.token).toBeTypeOf('string');
|
|
});
|
|
|
|
it('should fail to log in with an incorrect password', async () => {
|
|
// Use the user we just created
|
|
const adminEmail = testUserEmail;
|
|
const wrongPassword = 'wrongpassword';
|
|
|
|
// The loginUser function returns a Response object. We check its status.
|
|
const response = await request
|
|
.post('/api/auth/login')
|
|
.send({ email: adminEmail, password: wrongPassword, rememberMe: false });
|
|
expect(response.status).toBe(401);
|
|
const errorData = response.body;
|
|
expect(errorData.message).toBe('Incorrect email or password.');
|
|
});
|
|
|
|
it('should fail to log in with a non-existent email', async () => {
|
|
const nonExistentEmail = 'nobody-here@example.com';
|
|
const anyPassword = 'any-password';
|
|
|
|
// The loginUser function returns a Response object. We check its status.
|
|
const response = await request
|
|
.post('/api/auth/login')
|
|
.send({ email: nonExistentEmail, password: anyPassword, rememberMe: false });
|
|
expect(response.status).toBe(401);
|
|
const errorData = response.body;
|
|
// Security best practice: the error message should be identical for wrong password and wrong email
|
|
// to prevent user enumeration attacks.
|
|
expect(errorData.message).toBe('Incorrect email or password.');
|
|
});
|
|
|
|
it('should allow registration with an empty string for avatar_url and save it as null', async () => {
|
|
// Arrange: Define user data with an empty avatar_url.
|
|
const email = `empty-avatar-user-${Date.now()}@example.com`;
|
|
const userData = {
|
|
email,
|
|
password: TEST_PASSWORD,
|
|
full_name: 'Empty Avatar',
|
|
avatar_url: '',
|
|
};
|
|
|
|
// Act: Register the new user.
|
|
const registerResponse = await request.post('/api/auth/register').send(userData);
|
|
|
|
// Assert 1: Check that the registration was successful and the returned profile is correct.
|
|
expect(registerResponse.status).toBe(201);
|
|
const registeredProfile = registerResponse.body.userprofile;
|
|
const registeredToken = registerResponse.body.token;
|
|
expect(registeredProfile.user.email).toBe(email);
|
|
expect(registeredProfile.avatar_url).toBeNull(); // The API should return null for the avatar_url.
|
|
|
|
// Add the newly created user's ID to the array for cleanup in afterAll.
|
|
createdUserIds.push(registeredProfile.user.user_id);
|
|
|
|
// Assert 2 (Verification): Fetch the profile using the new token to confirm the value in the DB is null.
|
|
const profileResponse = await request
|
|
.get('/api/users/profile')
|
|
.set('Authorization', `Bearer ${registeredToken}`);
|
|
|
|
expect(profileResponse.status).toBe(200);
|
|
expect(profileResponse.body.avatar_url).toBeNull();
|
|
});
|
|
|
|
it('should successfully refresh an access token using a refresh token cookie', async () => {
|
|
// Arrange: Log in to get a fresh, valid refresh token cookie for this specific test.
|
|
// This ensures the test is self-contained and not affected by other tests.
|
|
const loginResponse = await request
|
|
.post('/api/auth/login')
|
|
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: true });
|
|
const refreshTokenCookie = loginResponse.headers['set-cookie'][0].split(';')[0];
|
|
|
|
expect(refreshTokenCookie).toBeDefined();
|
|
|
|
// Act: Make a request to the refresh-token endpoint, including the cookie.
|
|
const response = await request
|
|
.post('/api/auth/refresh-token')
|
|
.set('Cookie', refreshTokenCookie!);
|
|
|
|
// Assert: Check for a successful response and a new access token.
|
|
expect(response.status).toBe(200);
|
|
const data = response.body;
|
|
expect(data.token).toBeTypeOf('string');
|
|
});
|
|
|
|
it('should fail to refresh an access token with an invalid refresh token cookie', async () => {
|
|
// Arrange: Create a fake/invalid cookie.
|
|
const invalidRefreshTokenCookie = 'refreshToken=this-is-not-a-valid-token';
|
|
|
|
// Act: Make a request to the refresh-token endpoint with the invalid cookie.
|
|
const response = await request
|
|
.post('/api/auth/refresh-token')
|
|
.set('Cookie', invalidRefreshTokenCookie);
|
|
|
|
// Assert: Check for a 403 Forbidden response.
|
|
expect(response.status).toBe(403);
|
|
const data = response.body;
|
|
expect(data.message).toBe('Invalid or expired refresh token.');
|
|
});
|
|
|
|
it('should successfully log out and clear the refresh token cookie', async () => {
|
|
// Arrange: Log in to get a valid refresh token cookie.
|
|
const loginResponse = await request
|
|
.post('/api/auth/login')
|
|
.send({ email: testUserEmail, password: TEST_PASSWORD, rememberMe: true });
|
|
const refreshTokenCookie = loginResponse.headers['set-cookie'][0].split(';')[0];
|
|
expect(refreshTokenCookie).toBeDefined();
|
|
|
|
// Act: Make a request to the new logout endpoint, including the cookie.
|
|
const response = await request.post('/api/auth/logout').set('Cookie', refreshTokenCookie!);
|
|
|
|
// Assert: Check for a successful response and a cookie-clearing header.
|
|
expect(response.status).toBe(200);
|
|
const logoutSetCookieHeader = response.headers['set-cookie'][0];
|
|
expect(logoutSetCookieHeader).toContain('refreshToken=;');
|
|
expect(logoutSetCookieHeader).toContain('Max-Age=0');
|
|
});
|
|
|
|
describe('Rate Limiting', () => {
|
|
it('should block requests to /forgot-password after exceeding the limit', async () => {
|
|
const email = testUserEmail; // Use the user created in beforeAll
|
|
const limit = 5; // Based on the configuration in auth.routes.ts
|
|
|
|
// Send requests up to the limit. These should all pass.
|
|
for (let i = 0; i < limit; i++) {
|
|
const response = await request
|
|
.post('/api/auth/forgot-password')
|
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
|
.send({ email });
|
|
|
|
// The endpoint returns 200 even for non-existent users to prevent email enumeration.
|
|
expect(response.status).toBe(200);
|
|
}
|
|
|
|
// The next request (the 6th one) should be blocked.
|
|
const blockedResponse = await request
|
|
.post('/api/auth/forgot-password')
|
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
|
.send({ email });
|
|
|
|
expect(blockedResponse.status).toBe(429);
|
|
expect(blockedResponse.text).toContain(
|
|
'Too many password reset requests from this IP, please try again after 15 minutes.',
|
|
);
|
|
}, 15000); // Increase timeout to handle multiple sequential requests
|
|
});
|
|
});
|