add changes
This commit is contained in:
595
backend/tests/test-update-profile.js
Normal file
595
backend/tests/test-update-profile.js
Normal file
@@ -0,0 +1,595 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const API_URL = 'http://localhost:3000/api';
|
||||
|
||||
// Test users
|
||||
const testUser = {
|
||||
username: 'profiletest',
|
||||
email: 'profiletest@example.com',
|
||||
password: 'Test123!@#'
|
||||
};
|
||||
|
||||
const secondUser = {
|
||||
username: 'profiletest2',
|
||||
email: 'profiletest2@example.com',
|
||||
password: 'Test123!@#'
|
||||
};
|
||||
|
||||
let userToken;
|
||||
let userId;
|
||||
let secondUserToken;
|
||||
let secondUserId;
|
||||
|
||||
// Test setup
|
||||
async function setup() {
|
||||
console.log('Setting up test data...\n');
|
||||
|
||||
try {
|
||||
// Register/login first user
|
||||
try {
|
||||
const registerRes = await axios.post(`${API_URL}/auth/register`, testUser);
|
||||
userToken = registerRes.data.data.token;
|
||||
userId = registerRes.data.data.user.id;
|
||||
console.log('✓ First user registered');
|
||||
} catch (error) {
|
||||
const loginRes = await axios.post(`${API_URL}/auth/login`, {
|
||||
email: testUser.email,
|
||||
password: testUser.password
|
||||
});
|
||||
userToken = loginRes.data.data.token;
|
||||
userId = loginRes.data.data.user.id;
|
||||
console.log('✓ First user logged in');
|
||||
}
|
||||
|
||||
// Register/login second user
|
||||
try {
|
||||
const registerRes = await axios.post(`${API_URL}/auth/register`, secondUser);
|
||||
secondUserToken = registerRes.data.data.token;
|
||||
secondUserId = registerRes.data.data.user.id;
|
||||
console.log('✓ Second user registered');
|
||||
} catch (error) {
|
||||
const loginRes = await axios.post(`${API_URL}/auth/login`, {
|
||||
email: secondUser.email,
|
||||
password: secondUser.password
|
||||
});
|
||||
secondUserToken = loginRes.data.data.token;
|
||||
secondUserId = loginRes.data.data.user.id;
|
||||
console.log('✓ Second user logged in');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error('Setup failed:', error.response?.data || error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
const tests = [
|
||||
{
|
||||
name: 'Test 1: Update username successfully',
|
||||
run: async () => {
|
||||
const newUsername = 'profiletestupdated'; // No underscore - alphanumeric only
|
||||
|
||||
const response = await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: newUsername
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
if (!response.data.success) throw new Error('Request failed');
|
||||
if (response.data.data.user.username !== newUsername) {
|
||||
throw new Error('Username not updated');
|
||||
}
|
||||
if (!response.data.data.changedFields.includes('username')) {
|
||||
throw new Error('changedFields missing username');
|
||||
}
|
||||
|
||||
// Revert username
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: testUser.username
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
return '✓ Username update works';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 2: Update email successfully',
|
||||
run: async () => {
|
||||
const newEmail = 'profiletestnew@example.com';
|
||||
|
||||
const response = await axios.put(`${API_URL}/users/${userId}`, {
|
||||
email: newEmail
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
if (!response.data.success) throw new Error('Request failed');
|
||||
if (response.data.data.user.email !== newEmail) {
|
||||
throw new Error('Email not updated');
|
||||
}
|
||||
if (!response.data.data.changedFields.includes('email')) {
|
||||
throw new Error('changedFields missing email');
|
||||
}
|
||||
|
||||
// Revert email immediately and verify
|
||||
const revertResponse = await axios.put(`${API_URL}/users/${userId}`, {
|
||||
email: testUser.email
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
if (revertResponse.data.data.user.email !== testUser.email) {
|
||||
throw new Error(`Email revert failed. Expected: ${testUser.email}, Got: ${revertResponse.data.data.user.email}`);
|
||||
}
|
||||
|
||||
// Small delay to ensure database write completes
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
return '✓ Email update works';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 3: Update password successfully',
|
||||
run: async () => {
|
||||
const newPassword = 'NewPass123!@#';
|
||||
|
||||
// Skip the verification login - the token should still be valid
|
||||
// Just proceed with the password change
|
||||
|
||||
const response = await axios.put(`${API_URL}/users/${userId}`, {
|
||||
currentPassword: testUser.password,
|
||||
newPassword: newPassword
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
if (!response.data.success) throw new Error('Password update request failed');
|
||||
if (!response.data.data.changedFields.includes('password')) {
|
||||
throw new Error('changedFields missing password');
|
||||
}
|
||||
|
||||
// Test login with new password
|
||||
try {
|
||||
const loginRes = await axios.post(`${API_URL}/auth/login`, {
|
||||
email: testUser.email,
|
||||
password: newPassword
|
||||
});
|
||||
|
||||
if (!loginRes.data.success) throw new Error('Login with new password failed');
|
||||
userToken = loginRes.data.data.token; // Update token
|
||||
} catch (err) {
|
||||
throw new Error(`Login with new password failed. Email: ${testUser.email}, NewPwd: ${newPassword}, Error: ${err.response?.data?.message || err.message}`);
|
||||
}
|
||||
|
||||
// Revert password
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
currentPassword: newPassword,
|
||||
newPassword: testUser.password
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
// Get new token with original password
|
||||
const loginRes2 = await axios.post(`${API_URL}/auth/login`, {
|
||||
email: testUser.email,
|
||||
password: testUser.password
|
||||
});
|
||||
userToken = loginRes2.data.data.token;
|
||||
|
||||
return '✓ Password update works';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 4: Update profile image successfully',
|
||||
run: async () => {
|
||||
const imageUrl = 'https://example.com/profile.jpg';
|
||||
|
||||
const response = await axios.put(`${API_URL}/users/${userId}`, {
|
||||
profileImage: imageUrl
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
if (!response.data.success) throw new Error('Request failed');
|
||||
if (response.data.data.user.profileImage !== imageUrl) {
|
||||
throw new Error('Profile image not updated');
|
||||
}
|
||||
if (!response.data.data.changedFields.includes('profileImage')) {
|
||||
throw new Error('changedFields missing profileImage');
|
||||
}
|
||||
|
||||
return '✓ Profile image update works';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 5: Remove profile image (set to null)',
|
||||
run: async () => {
|
||||
const response = await axios.put(`${API_URL}/users/${userId}`, {
|
||||
profileImage: null
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
if (!response.data.success) throw new Error('Request failed');
|
||||
if (response.data.data.user.profileImage !== null) {
|
||||
throw new Error('Profile image not removed');
|
||||
}
|
||||
|
||||
return '✓ Profile image removal works';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 6: Update multiple fields at once',
|
||||
run: async () => {
|
||||
const updates = {
|
||||
username: 'multifieldtest',
|
||||
email: 'multifield@example.com',
|
||||
profileImage: 'https://example.com/multi.jpg'
|
||||
};
|
||||
|
||||
const response = await axios.put(`${API_URL}/users/${userId}`, updates, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
if (!response.data.success) throw new Error('Request failed');
|
||||
if (response.data.data.user.username !== updates.username) {
|
||||
throw new Error('Username not updated');
|
||||
}
|
||||
if (response.data.data.user.email !== updates.email) {
|
||||
throw new Error('Email not updated');
|
||||
}
|
||||
if (response.data.data.user.profileImage !== updates.profileImage) {
|
||||
throw new Error('Profile image not updated');
|
||||
}
|
||||
if (response.data.data.changedFields.length !== 3) {
|
||||
throw new Error('Should have 3 changed fields');
|
||||
}
|
||||
|
||||
// Revert
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: testUser.username,
|
||||
email: testUser.email,
|
||||
profileImage: null
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
return '✓ Multiple field update works';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 7: Reject duplicate username',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: secondUser.username // Try to use second user's username
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 409) {
|
||||
throw new Error(`Expected 409, got ${error.response?.status}`);
|
||||
}
|
||||
if (!error.response.data.message.toLowerCase().includes('username')) {
|
||||
throw new Error('Error message should mention username');
|
||||
}
|
||||
return '✓ Duplicate username rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 8: Reject duplicate email',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
email: secondUser.email // Try to use second user's email
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 409) {
|
||||
throw new Error(`Expected 409, got ${error.response?.status}`);
|
||||
}
|
||||
if (!error.response.data.message.toLowerCase().includes('email')) {
|
||||
throw new Error('Error message should mention email');
|
||||
}
|
||||
return '✓ Duplicate email rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 9: Reject invalid email format',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
email: 'invalid-email-format'
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Invalid email rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 10: Reject short username',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: 'ab' // Too short (min 3)
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Short username rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 11: Reject invalid username characters',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: 'user@name!' // Invalid characters
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Invalid username characters rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 12: Reject short password',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
currentPassword: testUser.password,
|
||||
newPassword: '12345' // Too short (min 6)
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
// Controller validates current password first (returns 401 if wrong)
|
||||
// Then validates new password length (returns 400)
|
||||
// Since we're providing correct current password, we should get 400
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Short password rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 13: Reject password change without current password',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
newPassword: 'NewPassword123'
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
if (!error.response.data.message.toLowerCase().includes('current password')) {
|
||||
throw new Error('Error should mention current password');
|
||||
}
|
||||
return '✓ Password change without current password rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 14: Reject incorrect current password',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
currentPassword: 'WrongPassword123',
|
||||
newPassword: 'NewPassword123'
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 401) {
|
||||
throw new Error(`Expected 401, got ${error.response?.status}`);
|
||||
}
|
||||
if (!error.response.data.message.toLowerCase().includes('incorrect')) {
|
||||
throw new Error('Error should mention incorrect password');
|
||||
}
|
||||
return '✓ Incorrect current password rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 15: Reject empty update (no fields provided)',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
if (!error.response.data.message.toLowerCase().includes('no fields')) {
|
||||
throw new Error('Error should mention no fields');
|
||||
}
|
||||
return '✓ Empty update rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 16: Cross-user update blocked',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${secondUserId}`, {
|
||||
username: 'hackedusername'
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` } // Using first user's token
|
||||
});
|
||||
throw new Error('Should have been blocked');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 403) {
|
||||
throw new Error(`Expected 403, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Cross-user update blocked';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 17: Unauthenticated request blocked',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: 'newusername'
|
||||
});
|
||||
throw new Error('Should have been blocked');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 401) {
|
||||
throw new Error(`Expected 401, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Unauthenticated blocked';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 18: Invalid UUID format returns 400',
|
||||
run: async () => {
|
||||
try {
|
||||
await axios.put(`${API_URL}/users/invalid-uuid`, {
|
||||
username: 'newusername'
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have failed');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Invalid UUID returns 400';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 19: Non-existent user returns 404',
|
||||
run: async () => {
|
||||
try {
|
||||
const fakeUuid = '00000000-0000-0000-0000-000000000000';
|
||||
await axios.put(`${API_URL}/users/${fakeUuid}`, {
|
||||
username: 'newusername'
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have failed');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 404) {
|
||||
throw new Error(`Expected 404, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Non-existent user returns 404';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 20: Profile image URL too long rejected',
|
||||
run: async () => {
|
||||
try {
|
||||
const longUrl = 'https://example.com/' + 'a'.repeat(250); // Over 255 chars
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
profileImage: longUrl
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
throw new Error('Should have been rejected');
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 400) {
|
||||
throw new Error(`Expected 400, got ${error.response?.status}`);
|
||||
}
|
||||
return '✓ Long profile image URL rejected';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test 21: Response excludes password field',
|
||||
run: async () => {
|
||||
const response = await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: 'passwordtest'
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
if (response.data.data.user.password !== undefined) {
|
||||
throw new Error('Password should not be in response');
|
||||
}
|
||||
|
||||
// Revert
|
||||
await axios.put(`${API_URL}/users/${userId}`, {
|
||||
username: testUser.username
|
||||
}, {
|
||||
headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
});
|
||||
|
||||
return '✓ Password excluded from response';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Run tests
|
||||
async function runTests() {
|
||||
console.log('============================================================');
|
||||
console.log('UPDATE USER PROFILE API TESTS');
|
||||
console.log('============================================================\n');
|
||||
|
||||
await setup();
|
||||
|
||||
console.log('Running tests...\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
const result = await test.run();
|
||||
console.log(result);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.log(`✗ ${test.name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
if (error.response?.data) {
|
||||
console.log(` Response:`, JSON.stringify(error.response.data, null, 2));
|
||||
}
|
||||
failed++;
|
||||
}
|
||||
// Small delay to avoid rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
console.log('\n============================================================');
|
||||
console.log(`RESULTS: ${passed} passed, ${failed} failed out of ${tests.length} tests`);
|
||||
console.log('============================================================');
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
Reference in New Issue
Block a user