Files
flyer-crawler.projectium.com/src/tests/integration/auth.integration.test.ts
Torben Sorensen 6b2079ef2c
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 32m44s
fix the dang integration tests
2026-01-05 22:38:21 -08:00

206 lines
8.5 KiB
TypeScript

// src/tests/integration/auth.integration.test.ts
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import supertest from 'supertest';
import { createAndLoginUser, TEST_PASSWORD } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
import type { UserProfile } from '../../types';
/**
* @vitest-environment node
*/
/**
* 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 request: ReturnType<typeof supertest>;
let testUserEmail: string;
let testUser: UserProfile;
const createdUserIds: string[] = [];
beforeAll(async () => {
vi.stubEnv('FRONTEND_URL', 'https://example.com');
const app = (await import('../../../server')).default;
request = supertest(app);
// 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 () => {
vi.unstubAllEnvs();
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
});
});