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:
416
backend/src/__tests__/users.test.js
Normal file
416
backend/src/__tests__/users.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user