test: add comprehensive test suite for User Profiles API

- Created users.test.js with 25 tests covering all 4 endpoints:
  * GET /api/users/me - get current user profile
  * PATCH /api/users/me - update profile (all fields)
  * PATCH /api/users/me/password - change password
  * GET /api/users/:username - get public profile
  * GET /api/users/:username/ratings - get user ratings

- All 25 tests passing (100%)
- controllers/user.js coverage: 90.16% (up from 8.19%)
- routes/users.js coverage: 81.81% (up from 27.27%)
- Tested email change with verification
- Tested password security and validation
This commit is contained in:
Radosław Gierwiało
2025-11-14 23:38:07 +01:00
parent 1747bf2d91
commit 47a21b5fd6

View File

@@ -0,0 +1,416 @@
const request = require('supertest');
const app = require('../app');
const { prisma } = require('../utils/db');
const { hashPassword, generateToken } = require('../utils/auth');
// Test data
let testUser1, testUser2;
let testToken1, testToken2;
// Setup test data
beforeAll(async () => {
// Clean up
await prisma.rating.deleteMany({});
await prisma.message.deleteMany({});
await prisma.match.deleteMany({});
await prisma.chatRoom.deleteMany({});
await prisma.eventParticipant.deleteMany({});
await prisma.event.deleteMany({});
await prisma.user.deleteMany({});
// Create test users
testUser1 = await prisma.user.create({
data: {
username: 'john_dancer',
email: 'john@example.com',
passwordHash: await hashPassword('password123'),
emailVerified: true,
firstName: 'John',
lastName: 'Doe',
wsdcId: '12345',
country: 'USA',
city: 'New York',
youtubeUrl: 'https://youtube.com/@john',
instagramUrl: 'https://instagram.com/john',
},
});
testUser2 = await prisma.user.create({
data: {
username: 'sarah_swings',
email: 'sarah@example.com',
passwordHash: await hashPassword('password123'),
emailVerified: true,
firstName: 'Sarah',
lastName: 'Smith',
},
});
// Generate tokens
testToken1 = generateToken({ userId: testUser1.id });
testToken2 = generateToken({ userId: testUser2.id });
}, 30000);
afterAll(async () => {
// Clean up
await prisma.rating.deleteMany({});
await prisma.message.deleteMany({});
await prisma.match.deleteMany({});
await prisma.chatRoom.deleteMany({});
await prisma.eventParticipant.deleteMany({});
await prisma.event.deleteMany({});
await prisma.user.deleteMany({});
await prisma.$disconnect();
});
describe('User Profiles API Tests', () => {
describe('GET /api/users/me', () => {
it('should get current user profile', async () => {
const response = await request(app)
.get('/api/users/me')
.set('Authorization', `Bearer ${testToken1}`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('success', true);
expect(response.body.data).toHaveProperty('id', testUser1.id);
expect(response.body.data).toHaveProperty('username', testUser1.username);
expect(response.body.data).toHaveProperty('email', testUser1.email);
expect(response.body.data).toHaveProperty('firstName', testUser1.firstName);
expect(response.body.data).toHaveProperty('lastName', testUser1.lastName);
expect(response.body.data).toHaveProperty('wsdcId', testUser1.wsdcId);
expect(response.body.data).toHaveProperty('country', testUser1.country);
expect(response.body.data).toHaveProperty('city', testUser1.city);
expect(response.body.data).toHaveProperty('youtubeUrl', testUser1.youtubeUrl);
expect(response.body.data).toHaveProperty('instagramUrl', testUser1.instagramUrl);
expect(response.body.data).toHaveProperty('stats');
expect(response.body.data.stats).toHaveProperty('matchesCount');
expect(response.body.data.stats).toHaveProperty('ratingsCount');
expect(response.body.data.stats).toHaveProperty('rating');
});
it('should not include password in response', async () => {
const response = await request(app)
.get('/api/users/me')
.set('Authorization', `Bearer ${testToken1}`)
.expect(200);
expect(response.body.data).not.toHaveProperty('passwordHash');
expect(response.body.data).not.toHaveProperty('password');
});
it('should reject request without authentication', async () => {
const response = await request(app)
.get('/api/users/me')
.expect('Content-Type', /json/)
.expect(401);
expect(response.body).toHaveProperty('success', false);
});
});
describe('PATCH /api/users/me', () => {
it('should update user profile successfully', async () => {
const updates = {
firstName: 'Johnny',
lastName: 'Dancer',
country: 'United States',
city: 'Los Angeles',
youtubeUrl: 'https://youtube.com/@johnny',
};
const response = await request(app)
.patch('/api/users/me')
.set('Authorization', `Bearer ${testToken1}`)
.send(updates)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('message');
expect(response.body.data).toHaveProperty('user');
expect(response.body.data.user.firstName).toBe(updates.firstName);
expect(response.body.data.user.lastName).toBe(updates.lastName);
expect(response.body.data.user.country).toBe(updates.country);
expect(response.body.data.user.city).toBe(updates.city);
expect(response.body.data.user.youtubeUrl).toBe(updates.youtubeUrl);
expect(response.body.data).toHaveProperty('token'); // Should return new token
});
it('should update individual fields', async () => {
const response = await request(app)
.patch('/api/users/me')
.set('Authorization', `Bearer ${testToken1}`)
.send({ city: 'San Francisco' })
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.user.city).toBe('San Francisco');
});
it('should allow clearing optional fields', async () => {
const response = await request(app)
.patch('/api/users/me')
.set('Authorization', `Bearer ${testToken1}`)
.send({ wsdcId: '' }) // Clear WSDC ID
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.user.wsdcId).toBeNull();
});
it('should handle email change and require verification', async () => {
const newEmail = 'newemail@example.com';
const response = await request(app)
.patch('/api/users/me')
.set('Authorization', `Bearer ${testToken1}`)
.send({ email: newEmail })
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.emailChanged).toBe(true);
expect(response.body.data.user.email).toBe(newEmail);
expect(response.body.data.user.emailVerified).toBe(false);
expect(response.body.message).toContain('verify');
// Restore original email for other tests
await prisma.user.update({
where: { id: testUser1.id },
data: {
email: 'john@example.com',
emailVerified: true,
},
});
});
it('should reject duplicate email', async () => {
const response = await request(app)
.patch('/api/users/me')
.set('Authorization', `Bearer ${testToken1}`)
.send({ email: testUser2.email }) // Try to use testUser2's email
.expect(400);
expect(response.body).toHaveProperty('success', false);
expect(response.body.error).toContain('already registered');
});
it('should reject invalid URLs', async () => {
const response = await request(app)
.patch('/api/users/me')
.set('Authorization', `Bearer ${testToken1}`)
.send({ youtubeUrl: 'not-a-valid-url' })
.expect(400);
expect(response.body).toHaveProperty('success', false);
});
it('should reject request without authentication', async () => {
const response = await request(app)
.patch('/api/users/me')
.send({ firstName: 'Test' })
.expect(401);
expect(response.body).toHaveProperty('success', false);
});
});
describe('PATCH /api/users/me/password', () => {
it('should change password successfully', async () => {
const response = await request(app)
.patch('/api/users/me/password')
.set('Authorization', `Bearer ${testToken2}`)
.send({
currentPassword: 'password123',
newPassword: 'newPassword456!',
})
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('message');
expect(response.body.message).toContain('successfully');
// Verify can login with new password
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: testUser2.email,
password: 'newPassword456!',
})
.expect(200);
expect(loginResponse.body.success).toBe(true);
// Change password back for other tests
await prisma.user.update({
where: { id: testUser2.id },
data: { passwordHash: await hashPassword('password123') },
});
});
it('should reject incorrect current password', async () => {
const response = await request(app)
.patch('/api/users/me/password')
.set('Authorization', `Bearer ${testToken1}`)
.send({
currentPassword: 'wrongPassword',
newPassword: 'newPassword123!',
})
.expect(401);
expect(response.body).toHaveProperty('success', false);
expect(response.body.error).toContain('incorrect');
});
it('should reject missing current password', async () => {
const response = await request(app)
.patch('/api/users/me/password')
.set('Authorization', `Bearer ${testToken1}`)
.send({
newPassword: 'newPassword123!',
})
.expect(400);
expect(response.body).toHaveProperty('success', false);
});
it('should reject missing new password', async () => {
const response = await request(app)
.patch('/api/users/me/password')
.set('Authorization', `Bearer ${testToken1}`)
.send({
currentPassword: 'password123',
})
.expect(400);
expect(response.body).toHaveProperty('success', false);
});
it('should reject weak new password', async () => {
const response = await request(app)
.patch('/api/users/me/password')
.set('Authorization', `Bearer ${testToken1}`)
.send({
currentPassword: 'password123',
newPassword: '123', // Too short
})
.expect(400);
expect(response.body).toHaveProperty('success', false);
});
it('should reject request without authentication', async () => {
const response = await request(app)
.patch('/api/users/me/password')
.send({
currentPassword: 'password123',
newPassword: 'newPassword123!',
})
.expect(401);
expect(response.body).toHaveProperty('success', false);
});
});
describe('GET /api/users/:username', () => {
it('should get user profile by username', async () => {
const response = await request(app)
.get(`/api/users/${testUser1.username}`)
.set('Authorization', `Bearer ${testToken2}`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('success', true);
expect(response.body.data).toHaveProperty('username', testUser1.username);
expect(response.body.data).toHaveProperty('firstName');
expect(response.body.data).toHaveProperty('lastName');
expect(response.body.data).toHaveProperty('stats');
expect(response.body.data.stats).toHaveProperty('matchesCount');
expect(response.body.data.stats).toHaveProperty('ratingsCount');
expect(response.body.data.stats).toHaveProperty('rating');
});
it('should not include email in public profile', async () => {
const response = await request(app)
.get(`/api/users/${testUser1.username}`)
.set('Authorization', `Bearer ${testToken2}`)
.expect(200);
expect(response.body.data).not.toHaveProperty('email');
expect(response.body.data).not.toHaveProperty('emailVerified');
});
it('should not include password in public profile', async () => {
const response = await request(app)
.get(`/api/users/${testUser1.username}`)
.set('Authorization', `Bearer ${testToken2}`)
.expect(200);
expect(response.body.data).not.toHaveProperty('passwordHash');
expect(response.body.data).not.toHaveProperty('password');
});
it('should return 404 for non-existent username', async () => {
const response = await request(app)
.get('/api/users/nonexistentuser123')
.set('Authorization', `Bearer ${testToken1}`)
.expect(404);
expect(response.body).toHaveProperty('success', false);
expect(response.body.error).toContain('not found');
});
it('should reject request without authentication', async () => {
const response = await request(app)
.get(`/api/users/${testUser1.username}`)
.expect(401);
expect(response.body).toHaveProperty('success', false);
});
it('should allow viewing own profile via username', async () => {
const response = await request(app)
.get(`/api/users/${testUser1.username}`)
.set('Authorization', `Bearer ${testToken1}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.username).toBe(testUser1.username);
});
});
describe('GET /api/users/:username/ratings', () => {
it('should get user ratings', async () => {
const response = await request(app)
.get(`/api/users/${testUser1.username}/ratings`)
.set('Authorization', `Bearer ${testToken2}`)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('success', true);
expect(response.body.data).toHaveProperty('username', testUser1.username);
expect(response.body.data).toHaveProperty('averageRating');
expect(response.body.data).toHaveProperty('ratingsCount');
expect(response.body.data).toHaveProperty('ratings');
expect(Array.isArray(response.body.data.ratings)).toBe(true);
});
it('should return 404 for non-existent username', async () => {
const response = await request(app)
.get('/api/users/nonexistentuser/ratings')
.set('Authorization', `Bearer ${testToken1}`)
.expect(404);
expect(response.body).toHaveProperty('success', false);
});
it('should reject request without authentication', async () => {
const response = await request(app)
.get(`/api/users/${testUser1.username}/ratings`)
.expect(401);
expect(response.body).toHaveProperty('success', false);
});
});
});