diff --git a/meet-ce/backend/src/models/zod-schemas/user.schema.ts b/meet-ce/backend/src/models/zod-schemas/user.schema.ts index f854b198..9d89fdb2 100644 --- a/meet-ce/backend/src/models/zod-schemas/user.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/user.schema.ts @@ -15,7 +15,7 @@ export const UserOptionsSchema: z.ZodType = z.object({ export const UserFiltersSchema: z.ZodType = z.object({ userId: z.string().optional(), name: z.string().optional(), - fields: z.string().optional(), + role: z.nativeEnum(MeetUserRole).optional(), maxItems: z.coerce .number() .positive('maxItems must be a positive number') diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index 5bbe2a03..49790050 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -292,43 +292,43 @@ export const changePassword = async ( }; }; -export const resetUserPassword = async (userId: string, newPassword: string) => { +export const resetUserPassword = async (userId: string, newPassword: string, accessToken?: string) => { checkAppIsRunning(); - const { accessToken } = await loginRootAdmin(); + const { accessToken: rootAdminAccessToken } = await loginRootAdmin(); return await request(app) .put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users/${userId}/password`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken ?? rootAdminAccessToken) .send({ newPassword }); }; -export const updateUserRole = async (userId: string, role: string) => { +export const updateUserRole = async (userId: string, role: string, accessToken?: string) => { checkAppIsRunning(); - const { accessToken } = await loginRootAdmin(); + const { accessToken: rootAdminAccessToken } = await loginRootAdmin(); return await request(app) .put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users/${userId}/role`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken ?? rootAdminAccessToken) .send({ role }); }; -export const deleteUser = async (userId: string) => { +export const deleteUser = async (userId: string, accessToken?: string) => { checkAppIsRunning(); - const { accessToken } = await loginRootAdmin(); + const { accessToken: rootAdminAccessToken } = await loginRootAdmin(); return await request(app) .delete(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users/${userId}`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken ?? rootAdminAccessToken) .send(); }; -export const bulkDeleteUsers = async (userIds: string[]) => { +export const bulkDeleteUsers = async (userIds: string[], accessToken?: string) => { checkAppIsRunning(); - const { accessToken } = await loginRootAdmin(); + const { accessToken: rootAdminAccessToken } = await loginRootAdmin(); return await request(app) .delete(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken ?? rootAdminAccessToken) .query({ userIds: userIds.join(',') }); }; diff --git a/meet-ce/backend/tests/integration/api/users/bulk-delete-users.test.ts b/meet-ce/backend/tests/integration/api/users/bulk-delete-users.test.ts new file mode 100644 index 00000000..e56af053 --- /dev/null +++ b/meet-ce/backend/tests/integration/api/users/bulk-delete-users.test.ts @@ -0,0 +1,285 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetRoomMemberRole, MeetUserRole } from '@openvidu-meet/typings'; +import { MEET_ENV } from '../../../../src/environment.js'; +import { expectValidationError } from '../../../helpers/assertion-helpers.js'; +import { + bulkDeleteUsers, + createRoom, + createRoomMember, + deleteAllRooms, + deleteAllUsers, + getRoom, + getRoomMember, + getUser, + startTestServer +} from '../../../helpers/request-helpers.js'; +import { setupTestUsers, setupUser } from '../../../helpers/test-scenarios.js'; +import { TestUsers, UserData } from '../../../interfaces/scenarios.js'; + +describe('Users API Tests', () => { + let testUsers: TestUsers; + + beforeAll(async () => { + await startTestServer(); + testUsers = await setupTestUsers(); + }); + + afterAll(async () => { + await deleteAllRooms(); + await deleteAllUsers(); + }); + + const createUserWithRole = async (role: MeetUserRole): Promise => { + const userId = `user_${Date.now()}`; + const userData = await setupUser({ + userId, + name: 'Test User', + password: 'password123', + role + }); + return userData; + }; + + describe('Bulk Delete Users Tests', () => { + it('should successfully delete multiple users with different roles', async () => { + const { user: user1 } = await createUserWithRole(MeetUserRole.USER); + const { user: user2 } = await createUserWithRole(MeetUserRole.ADMIN); + const { user: user3 } = await createUserWithRole(MeetUserRole.ROOM_MEMBER); + + const response = await bulkDeleteUsers([user1.userId, user2.userId, user3.userId]); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message', 'All users deleted successfully'); + + expect(response.body).toHaveProperty('deleted'); + expect(response.body.deleted).toHaveLength(3); + expect(response.body.deleted).toContain(user1.userId); + expect(response.body.deleted).toContain(user2.userId); + expect(response.body.deleted).toContain(user3.userId); + + // Verify users no longer exist + const getUser1Response = await getUser(user1.userId); + expect(getUser1Response.status).toBe(404); + + const getUser2Response = await getUser(user2.userId); + expect(getUser2Response.status).toBe(404); + + const getUser3Response = await getUser(user3.userId); + expect(getUser3Response.status).toBe(404); + }); + + it('should fail to delete root admin and return it in failed list', async () => { + const response = await bulkDeleteUsers([MEET_ENV.INITIAL_ADMIN_USER], testUsers.admin.accessToken); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('1 user(s) could not be deleted'); + + expect(response.body).toHaveProperty('deleted'); + expect(response.body).toHaveProperty('failed'); + expect(response.body.deleted).toHaveLength(0); + expect(response.body.failed).toHaveLength(1); + expect(response.body.failed[0]).toHaveProperty('userId', MEET_ENV.INITIAL_ADMIN_USER); + expect(response.body.failed[0]).toHaveProperty('error', 'Cannot delete the root admin user'); + }); + + it('should fail to delete own account and return it in failed list', async () => { + const response = await bulkDeleteUsers([testUsers.admin.user.userId], testUsers.admin.accessToken); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('1 user(s) could not be deleted'); + + expect(response.body).toHaveProperty('deleted'); + expect(response.body).toHaveProperty('failed'); + expect(response.body.deleted).toHaveLength(0); + expect(response.body.failed).toHaveLength(1); + expect(response.body.failed[0]).toHaveProperty('userId', testUsers.admin.user.userId); + expect(response.body.failed[0]).toHaveProperty('error', 'Cannot delete your own account'); + }); + + it('should return nonexistent users in failed list', async () => { + const nonexistentId = 'nonexistent_user_123'; + + const response = await bulkDeleteUsers([nonexistentId]); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('1 user(s) could not be deleted'); + + expect(response.body).toHaveProperty('deleted'); + expect(response.body).toHaveProperty('failed'); + expect(response.body.deleted).toHaveLength(0); + expect(response.body.failed).toHaveLength(1); + expect(response.body.failed[0]).toHaveProperty('userId', nonexistentId); + expect(response.body.failed[0]).toHaveProperty('error', 'User not found'); + }); + + it('should handle mixed success and failure results', async () => { + const { user: user1 } = await createUserWithRole(MeetUserRole.USER); + const { user: user2 } = await createUserWithRole(MeetUserRole.USER); + const nonexistentId = 'nonexistent_user_123'; + + const response = await bulkDeleteUsers([ + user1.userId, + user2.userId, + MEET_ENV.INITIAL_ADMIN_USER, + nonexistentId + ]); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('2 user(s) could not be deleted'); + + expect(response.body).toHaveProperty('deleted'); + expect(response.body).toHaveProperty('failed'); + expect(response.body.deleted).toHaveLength(2); + expect(response.body.deleted).toContain(user1.userId); + expect(response.body.deleted).toContain(user2.userId); + expect(response.body.failed).toHaveLength(2); + }); + + it('should fail when no users can be deleted', async () => { + const response = await bulkDeleteUsers([MEET_ENV.INITIAL_ADMIN_USER, 'nonexistent_user_123']); + expect(response.status).toBe(400); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('2 user(s) could not be deleted'); + + expect(response.body).toHaveProperty('deleted'); + expect(response.body).toHaveProperty('failed'); + expect(response.body.deleted).toHaveLength(0); + expect(response.body.failed).toHaveLength(2); + }); + + it('should transfer ownership of multiple rooms from multiple users', async () => { + const user1Data = await createUserWithRole(MeetUserRole.ADMIN); + const user2Data = await createUserWithRole(MeetUserRole.USER); + + // Create rooms owned by each user + const room1 = await createRoom(undefined, user1Data.accessToken); + const room2 = await createRoom(undefined, user1Data.accessToken); + const room3 = await createRoom(undefined, user2Data.accessToken); + + // Delete both users + const deleteResponse = await bulkDeleteUsers([user1Data.user.userId, user2Data.user.userId]); + expect(deleteResponse.status).toBe(200); + expect(deleteResponse.body.deleted).toHaveLength(2); + + // Verify all rooms have ownership transferred to root admin + const getRoom1Response = await getRoom(room1.roomId); + expect(getRoom1Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + const getRoom2Response = await getRoom(room2.roomId); + expect(getRoom2Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + const getRoom3Response = await getRoom(room3.roomId); + expect(getRoom3Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + }); + + it('should remove memberships from multiple rooms for multiple users', async () => { + const user1Data = await createUserWithRole(MeetUserRole.USER); + const user2Data = await createUserWithRole(MeetUserRole.ROOM_MEMBER); + + // Create rooms + const room1 = await createRoom(); + const room2 = await createRoom(); + + // Add both users as members to both rooms + await createRoomMember(room1.roomId, { + userId: user1Data.user.userId, + baseRole: MeetRoomMemberRole.MODERATOR + }); + await createRoomMember(room1.roomId, { + userId: user2Data.user.userId, + baseRole: MeetRoomMemberRole.SPEAKER + }); + await createRoomMember(room2.roomId, { + userId: user1Data.user.userId, + baseRole: MeetRoomMemberRole.SPEAKER + }); + await createRoomMember(room2.roomId, { + userId: user2Data.user.userId, + baseRole: MeetRoomMemberRole.MODERATOR + }); + + // Delete both users + const deleteResponse = await bulkDeleteUsers([user1Data.user.userId, user2Data.user.userId]); + expect(deleteResponse.status).toBe(200); + expect(deleteResponse.body.deleted).toHaveLength(2); + + // Verify all memberships are removed + const getMember1Room1Response = await getRoomMember(room1.roomId, user1Data.user.userId); + expect(getMember1Room1Response.status).toBe(404); + + const getMember2Room1Response = await getRoomMember(room1.roomId, user2Data.user.userId); + expect(getMember2Room1Response.status).toBe(404); + + const getMember1Room2Response = await getRoomMember(room2.roomId, user1Data.user.userId); + expect(getMember1Room2Response.status).toBe(404); + + const getMember2Room2Response = await getRoomMember(room2.roomId, user2Data.user.userId); + expect(getMember2Room2Response.status).toBe(404); + }); + + it('should handle complex cleanup with multiple users having mixed roles', async () => { + // Create users with different room relationships + const owner1Data = await createUserWithRole(MeetUserRole.ADMIN); + const owner2Data = await createUserWithRole(MeetUserRole.USER); + const memberData = await createUserWithRole(MeetUserRole.ROOM_MEMBER); + + // Create rooms with various ownership + const room1 = await createRoom(undefined, owner1Data.accessToken); + const room2 = await createRoom(undefined, owner2Data.accessToken); + const room3 = await createRoom(); + + // Add members to rooms + await createRoomMember(room1.roomId, { + userId: memberData.user.userId, + baseRole: MeetRoomMemberRole.SPEAKER + }); + await createRoomMember(room2.roomId, { + userId: memberData.user.userId, + baseRole: MeetRoomMemberRole.SPEAKER + }); + await createRoomMember(room3.roomId, { + userId: owner2Data.user.userId, + baseRole: MeetRoomMemberRole.MODERATOR + }); + await createRoomMember(room3.roomId, { + userId: memberData.user.userId, + baseRole: MeetRoomMemberRole.SPEAKER + }); + + // Delete all three users + const deleteResponse = await bulkDeleteUsers([ + owner1Data.user.userId, + owner2Data.user.userId, + memberData.user.userId + ]); + expect(deleteResponse.status).toBe(200); + expect(deleteResponse.body.deleted).toHaveLength(3); + + // Verify owned rooms transferred to root admin + const getRoom1Response = await getRoom(room1.roomId); + expect(getRoom1Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + const getRoom2Response = await getRoom(room2.roomId); + expect(getRoom2Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + // Verify all memberships removed + const getMember1Response = await getRoomMember(room1.roomId, memberData.user.userId); + expect(getMember1Response.status).toBe(404); + + const getMember2Response = await getRoomMember(room2.roomId, memberData.user.userId); + expect(getMember2Response.status).toBe(404); + + const getMember3Response = await getRoomMember(room3.roomId, owner2Data.user.userId); + expect(getMember3Response.status).toBe(404); + + const getMember4Response = await getRoomMember(room3.roomId, memberData.user.userId); + expect(getMember4Response.status).toBe(404); + }); + }); + + describe('Bulk Delete Users Validation Tests', () => { + it('should fail when userIds parameter is empty', async () => { + const response = await bulkDeleteUsers([]); + expectValidationError(response, 'userIds', 'At least one userId is required'); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/users/create-user.test.ts b/meet-ce/backend/tests/integration/api/users/create-user.test.ts new file mode 100644 index 00000000..c7af0a9d --- /dev/null +++ b/meet-ce/backend/tests/integration/api/users/create-user.test.ts @@ -0,0 +1,267 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetUserOptions, MeetUserRole } from '@openvidu-meet/typings'; +import { expectValidationError } from '../../../helpers/assertion-helpers.js'; +import { createUser, deleteAllUsers, getUser, loginReq, startTestServer } from '../../../helpers/request-helpers.js'; + +describe('Users API Tests', () => { + beforeAll(async () => { + await startTestServer(); + }); + + afterAll(async () => { + await deleteAllUsers(); + }); + + describe('Create User Tests', () => { + it('should successfully create a USER with all required fields', async () => { + const userId = `user_${Date.now()}`; + const userOptions = { + userId, + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }; + + const response = await createUser(userOptions); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty('userId', userId); + expect(response.body).toHaveProperty('name', 'Test User'); + expect(response.body).toHaveProperty('role', MeetUserRole.USER); + expect(response.body).toHaveProperty('registrationDate'); + expect(response.body).not.toHaveProperty('passwordHash'); + expect(response.body).not.toHaveProperty('mustChangePassword'); + + // Verify Location header is set correctly + expect(response.headers).toHaveProperty('location'); + expect(response.headers.location).toContain(`/users/${userId}`); + }); + + it('should successfully create an ADMIN user', async () => { + const userId = `admin_${Date.now()}`; + const userOptions = { + userId, + name: 'Test Admin', + password: 'admin_pass', + role: MeetUserRole.ADMIN + }; + + const response = await createUser(userOptions); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty('userId', userId); + expect(response.body).toHaveProperty('role', MeetUserRole.ADMIN); + }); + + it('should successfully create a ROOM_MEMBER user', async () => { + const userId = `rm_${Date.now()}`; + const userOptions = { + userId, + name: 'Test Room Member', + password: 'member_pass', + role: MeetUserRole.ROOM_MEMBER + }; + + const response = await createUser(userOptions); + + expect(response.status).toBe(201); + expect(response.body).toHaveProperty('userId', userId); + expect(response.body).toHaveProperty('role', MeetUserRole.ROOM_MEMBER); + }); + + it('should allow newly created user to login with the provided password', async () => { + const userId = `user_${Date.now()}`; + const password = 'password123'; + const userOptions = { + userId, + name: 'Test User', + password, + role: MeetUserRole.USER + }; + + const createResponse = await createUser(userOptions); + expect(createResponse.status).toBe(201); + + // Try to login with the created user + const loginResponse = await loginReq({ userId, password }); + expect(loginResponse.status).toBe(200); + }); + + it('should create user with mustChangePassword flag set to true', async () => { + const userId = `user_${Date.now()}`; + const password = 'password123'; + const userOptions = { + userId, + name: 'Test User', + password, + role: MeetUserRole.USER + }; + + const createResponse = await createUser(userOptions); + expect(createResponse.status).toBe(201); + + // Login to verify mustChangePassword is true + const loginResponse = await loginReq({ userId, password }); + expect(loginResponse.status).toBe(200); + expect(loginResponse.body).toHaveProperty('mustChangePassword', true); + }); + + it('should retrieve created user with correct information', async () => { + const userId = `user_${Date.now()}`; + const userOptions = { + userId, + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }; + + const createResponse = await createUser(userOptions); + expect(createResponse.status).toBe(201); + + // Get user to verify it was created correctly + const getResponse = await getUser(userId); + expect(getResponse.status).toBe(200); + expect(getResponse.body).toHaveProperty('userId', userId); + expect(getResponse.body).toHaveProperty('name', 'Test User'); + expect(getResponse.body).toHaveProperty('role', MeetUserRole.USER); + }); + + it('should fail when trying to create a user with duplicate userId', async () => { + const userId = `user_${Date.now()}`; + const userOptions = { + userId, + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }; + + // Create user first time + const firstResponse = await createUser(userOptions); + expect(firstResponse.status).toBe(201); + + // Try to create the same user again + const secondResponse = await createUser(userOptions); + expect(secondResponse.status).toBe(409); + expect(secondResponse.body).toHaveProperty('message'); + expect(secondResponse.body.message).toContain('already exists'); + }); + }); + + describe('Create User Validation Tests', () => { + it('should fail when userId is missing', async () => { + const response = await createUser({ + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + } as MeetUserOptions); + expectValidationError(response, 'userId', 'Required'); + }); + + it('should fail when name is missing', async () => { + const response = await createUser({ + userId: `user_${Date.now()}`, + password: 'password123', + role: MeetUserRole.USER + } as MeetUserOptions); + expectValidationError(response, 'name', 'Required'); + }); + + it('should fail when name is empty', async () => { + const response = await createUser({ + userId: `user_${Date.now()}`, + name: '', + password: 'password123', + role: MeetUserRole.USER + }); + expectValidationError(response, 'name', 'cannot be empty'); + }); + + it('should fail when password is missing', async () => { + const response = await createUser({ + userId: `user_${Date.now()}`, + name: 'Test User', + role: MeetUserRole.USER + } as MeetUserOptions); + expectValidationError(response, 'password', 'Required'); + }); + + it('should fail when role is missing', async () => { + const response = await createUser({ + userId: `user_${Date.now()}`, + name: 'Test User', + password: 'password123' + } as MeetUserOptions); + expectValidationError(response, 'role', 'Required'); + }); + + it('should fail when userId exceeds maximum length', async () => { + const response = await createUser({ + userId: 'a'.repeat(21), // Max is 20 characters + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }); + expectValidationError(response, 'userId', 'cannot exceed 20 characters'); + }); + + it('should fail when userId is too short', async () => { + const response = await createUser({ + userId: 'abcd', // Min is 5 characters + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }); + expectValidationError(response, 'userId', 'at least 5 characters'); + }); + + it('should fail when userId contains uppercase letters', async () => { + const response = await createUser({ + userId: 'User123', + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }); + expectValidationError(response, 'userId', 'lowercase letters, numbers, and underscores'); + }); + + it('should fail when userId contains invalid special characters', async () => { + const response = await createUser({ + userId: 'user-123', + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }); + expectValidationError(response, 'userId', 'lowercase letters, numbers, and underscores'); + }); + + it('should fail when name exceeds maximum length', async () => { + const response = await createUser({ + userId: `user_${Date.now()}`, + name: 'a'.repeat(51), // Max is 50 characters + password: 'password123', + role: MeetUserRole.USER + }); + expectValidationError(response, 'name', 'cannot exceed 50 characters'); + }); + + it('should fail when password is too short', async () => { + const response = await createUser({ + userId: `user_${Date.now()}`, + name: 'Test User', + password: '1234', // Min is 5 characters + role: MeetUserRole.USER + }); + expectValidationError(response, 'password', 'at least 5 characters'); + }); + + it('should fail when role is invalid', async () => { + const response = await createUser({ + userId: `user_${Date.now()}`, + name: 'Test User', + password: 'password123', + role: 'invalid' as MeetUserRole + }); + expectValidationError(response, 'role', 'Invalid enum value'); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/users/delete-user.test.ts b/meet-ce/backend/tests/integration/api/users/delete-user.test.ts new file mode 100644 index 00000000..887c2442 --- /dev/null +++ b/meet-ce/backend/tests/integration/api/users/delete-user.test.ts @@ -0,0 +1,299 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetRoomMemberRole, MeetUserRole } from '@openvidu-meet/typings'; +import { MEET_ENV } from '../../../../src/environment.js'; +import { + createRoom, + createRoomMember, + deleteAllRooms, + deleteAllUsers, + deleteUser, + getRoom, + getRoomMember, + getUser, + startTestServer +} from '../../../helpers/request-helpers.js'; +import { setupTestUsers, setupUser } from '../../../helpers/test-scenarios.js'; +import { TestUsers, UserData } from '../../../interfaces/scenarios.js'; + +describe('Users API Tests', () => { + let testUsers: TestUsers; + + beforeAll(async () => { + await startTestServer(); + testUsers = await setupTestUsers(); + }); + + afterAll(async () => { + await deleteAllRooms(); + await deleteAllUsers(); + }); + + const createUserWithRole = async (role: MeetUserRole): Promise => { + const userId = `user_${Date.now()}`; + const userData = await setupUser({ + userId, + name: 'Test User', + password: 'password123', + role + }); + return userData; + }; + + describe('Delete User Tests', () => { + it('should successfully delete USER', async () => { + const { user } = await createUserWithRole(MeetUserRole.USER); + + const response = await deleteUser(user.userId); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('deleted successfully'); + expect(response.body.message).toContain(user.userId); + }); + + it('should successfully delete ADMIN user', async () => { + const { user } = await createUserWithRole(MeetUserRole.ADMIN); + + const response = await deleteUser(user.userId); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('deleted successfully'); + }); + + it('should successfully delete ROOM_MEMBER user', async () => { + const { user } = await createUserWithRole(MeetUserRole.ROOM_MEMBER); + + const response = await deleteUser(user.userId); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('deleted successfully'); + }); + + it('should verify user is actually deleted', async () => { + const { user } = await createUserWithRole(MeetUserRole.USER); + + // Delete user + const deleteResponse = await deleteUser(user.userId); + expect(deleteResponse.status).toBe(200); + + // Verify user no longer exists + const getUserResponse = await getUser(user.userId); + expect(getUserResponse.status).toBe(404); + expect(getUserResponse.body).toHaveProperty('message'); + expect(getUserResponse.body.message).toContain('not found'); + }); + + it('should fail when trying to delete root admin user', async () => { + const response = await deleteUser(MEET_ENV.INITIAL_ADMIN_USER); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot delete the root admin user'); + }); + + it('should fail when admin tries to delete own account', async () => { + const response = await deleteUser(testUsers.admin.user.userId, testUsers.admin.accessToken); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot delete your own account'); + }); + + it('should fail when root admin tries to delete own account', async () => { + const response = await deleteUser(MEET_ENV.INITIAL_ADMIN_USER); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot delete the root admin user'); + }); + + it('should fail when user does not exist', async () => { + const response = await deleteUser('nonexistent_user_123'); + expect(response.status).toBe(404); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('not found'); + }); + + it('should transfer room ownership to root admin when deleting room owner', async () => { + // Create user who will own a room + const userData = await createUserWithRole(MeetUserRole.ADMIN); + + // Create room owned by this user + const room = await createRoom(undefined, userData.accessToken); + + // Verify initial ownership + const getRoomResponse = await getRoom(room.roomId); + expect(getRoomResponse.status).toBe(200); + expect(getRoomResponse.body).toHaveProperty('owner', userData.user.userId); + + // Delete the user + const deleteResponse = await deleteUser(userData.user.userId); + expect(deleteResponse.status).toBe(200); + + // Verify room ownership transferred to root admin + const getRoomAfterResponse = await getRoom(room.roomId); + expect(getRoomAfterResponse.status).toBe(200); + expect(getRoomAfterResponse.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + }); + + it('should transfer ownership of multiple rooms when deleting room owner', async () => { + // Create user who will own multiple rooms + const userData = await createUserWithRole(MeetUserRole.USER); + + // Create 3 rooms owned by this user + const room1 = await createRoom(undefined, userData.accessToken); + const room2 = await createRoom(undefined, userData.accessToken); + const room3 = await createRoom(undefined, userData.accessToken); + + // Delete the user + const deleteResponse = await deleteUser(userData.user.userId); + expect(deleteResponse.status).toBe(200); + + // Verify all rooms have ownership transferred to root admin + const getRoom1Response = await getRoom(room1.roomId); + expect(getRoom1Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + const getRoom2Response = await getRoom(room2.roomId); + expect(getRoom2Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + const getRoom3Response = await getRoom(room3.roomId); + expect(getRoom3Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + }); + + it('should remove user memberships when deleting a room member', async () => { + // Create user who will be a room member + const userData = await createUserWithRole(MeetUserRole.USER); + const userId = userData.user.userId; + + // Create a room + const room = await createRoom(); + + // Add user as member to the room + await createRoomMember(room.roomId, { userId, baseRole: MeetRoomMemberRole.MODERATOR }); + + // Verify membership exists + const getMemberResponse = await getRoomMember(room.roomId, userId); + expect(getMemberResponse.status).toBe(200); + expect(getMemberResponse.body).toHaveProperty('memberId', userId); + + // Delete the user + const deleteResponse = await deleteUser(userId); + expect(deleteResponse.status).toBe(200); + + // Verify membership no longer exists + const getMemberAfterResponse = await getRoomMember(room.roomId, userId); + expect(getMemberAfterResponse.status).toBe(404); + expect(getMemberAfterResponse.body).toHaveProperty('message'); + expect(getMemberAfterResponse.body.message).toContain('not found'); + }); + + it('should remove memberships from multiple rooms when deleting a room member', async () => { + // Create user who will be a member of multiple rooms + const userData = await createUserWithRole(MeetUserRole.USER); + const userId = userData.user.userId; + + // Create 3 rooms and add user as member to each + const room1 = await createRoom(); + const room2 = await createRoom(); + const room3 = await createRoom(); + + await createRoomMember(room1.roomId, { userId, baseRole: MeetRoomMemberRole.MODERATOR }); + await createRoomMember(room2.roomId, { userId, baseRole: MeetRoomMemberRole.MODERATOR }); + await createRoomMember(room3.roomId, { userId, baseRole: MeetRoomMemberRole.SPEAKER }); + + // Verify memberships exist + const getMember1Response = await getRoomMember(room1.roomId, userId); + expect(getMember1Response.status).toBe(200); + expect(getMember1Response.body).toHaveProperty('memberId', userId); + + const getMember2Response = await getRoomMember(room2.roomId, userId); + expect(getMember2Response.status).toBe(200); + expect(getMember2Response.body).toHaveProperty('memberId', userId); + + const getMember3Response = await getRoomMember(room3.roomId, userId); + expect(getMember3Response.status).toBe(200); + expect(getMember3Response.body).toHaveProperty('memberId', userId); + + // Delete the user + const deleteResponse = await deleteUser(userId); + expect(deleteResponse.status).toBe(200); + + // Verify all memberships are removed + const getMember1AfterResponse = await getRoomMember(room1.roomId, userId); + expect(getMember1AfterResponse.status).toBe(404); + + const getMember2AfterResponse = await getRoomMember(room2.roomId, userId); + expect(getMember2AfterResponse.status).toBe(404); + + const getMember3AfterResponse = await getRoomMember(room3.roomId, userId); + expect(getMember3AfterResponse.status).toBe(404); + }); + + it('should handle both room ownership transfer and membership removal when deleting user', async () => { + // Create user who will own some rooms and be member of others + const userData = await createUserWithRole(MeetUserRole.USER); + const userId = userData.user.userId; + + // Create rooms owned by this user + const ownedRoom1 = await createRoom(undefined, userData.accessToken); + const ownedRoom2 = await createRoom(undefined, userData.accessToken); + + // Create rooms where user is just a member + const memberRoom1 = await createRoom(); + const memberRoom2 = await createRoom(); + + await createRoomMember(memberRoom1.roomId, { userId, baseRole: MeetRoomMemberRole.SPEAKER }); + await createRoomMember(memberRoom2.roomId, { userId, baseRole: MeetRoomMemberRole.MODERATOR }); + + // Delete the user + const deleteResponse = await deleteUser(userId); + expect(deleteResponse.status).toBe(200); + + // Verify owned rooms transferred to root admin + const getOwnedRoom1Response = await getRoom(ownedRoom1.roomId); + expect(getOwnedRoom1Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + const getOwnedRoom2Response = await getRoom(ownedRoom2.roomId); + expect(getOwnedRoom2Response.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + // Verify memberships removed + const getMember1Response = await getRoomMember(memberRoom1.roomId, userId); + expect(getMember1Response.status).toBe(404); + + const getMember2Response = await getRoomMember(memberRoom2.roomId, userId); + expect(getMember2Response.status).toBe(404); + }); + + it('should not affect other room members when deleting a room owner', async () => { + // Create owner user + const ownerData = await createUserWithRole(MeetUserRole.USER); + const ownerId = ownerData.user.userId; + + // Create another user who will be a member + const memberData = await createUserWithRole(MeetUserRole.USER); + const memberId = memberData.user.userId; + + // Create room owned by owner + const room = await createRoom(undefined, ownerData.accessToken); + + // Add member to the room + await createRoomMember(room.roomId, { userId: memberId, baseRole: MeetRoomMemberRole.SPEAKER }); + + // Verify both owner and member before deletion + const getRoomBeforeResponse = await getRoom(room.roomId); + expect(getRoomBeforeResponse.body).toHaveProperty('owner', ownerId); + + const getMemberBeforeResponse = await getRoomMember(room.roomId, memberId); + expect(getMemberBeforeResponse.status).toBe(200); + + // Delete the owner + const deleteResponse = await deleteUser(ownerId); + expect(deleteResponse.status).toBe(200); + + // Verify room ownership transferred + const getRoomAfterResponse = await getRoom(room.roomId); + expect(getRoomAfterResponse.body).toHaveProperty('owner', MEET_ENV.INITIAL_ADMIN_USER); + + // Verify member still exists + const getMemberAfterResponse = await getRoomMember(room.roomId, memberId); + expect(getMemberAfterResponse.status).toBe(200); + expect(getMemberAfterResponse.body).toHaveProperty('memberId', memberId); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/users/get-user.test.ts b/meet-ce/backend/tests/integration/api/users/get-user.test.ts new file mode 100644 index 00000000..73543989 --- /dev/null +++ b/meet-ce/backend/tests/integration/api/users/get-user.test.ts @@ -0,0 +1,66 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetUserRole } from '@openvidu-meet/typings'; +import { MEET_ENV } from '../../../../src/environment.js'; +import { deleteAllUsers, getUser, startTestServer } from '../../../helpers/request-helpers.js'; +import { setupTestUsers } from '../../../helpers/test-scenarios.js'; +import { TestUsers } from '../../../interfaces/scenarios.js'; + +describe('Users API Tests', () => { + let testUsers: TestUsers; + + beforeAll(async () => { + await startTestServer(); + testUsers = await setupTestUsers(); + }); + + afterAll(async () => { + await deleteAllUsers(); + }); + + describe('Get User Tests', () => { + it('should successfully get a user by userId', async () => { + const response = await getUser(testUsers.admin.user.userId); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('userId', testUsers.admin.user.userId); + expect(response.body).toHaveProperty('name', testUsers.admin.user.name); + expect(response.body).toHaveProperty('role', MeetUserRole.ADMIN); + expect(response.body).toHaveProperty('registrationDate'); + }); + + it('should get root admin user', async () => { + const response = await getUser(MEET_ENV.INITIAL_ADMIN_USER); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('userId', MEET_ENV.INITIAL_ADMIN_USER); + expect(response.body).toHaveProperty('name', 'Admin'); + expect(response.body).toHaveProperty('role', MeetUserRole.ADMIN); + }); + + it('should get user with different roles', async () => { + const userResponse = await getUser(testUsers.user.user.userId); + expect(userResponse.status).toBe(200); + expect(userResponse.body).toHaveProperty('role', MeetUserRole.USER); + + const roomMemberResponse = await getUser(testUsers.roomMember.user.userId); + expect(roomMemberResponse.status).toBe(200); + expect(roomMemberResponse.body).toHaveProperty('role', MeetUserRole.ROOM_MEMBER); + }); + + it('should not expose sensitive fields', async () => { + const response = await getUser(testUsers.admin.user.userId); + expect(response.status).toBe(200); + expect(response.body).not.toHaveProperty('passwordHash'); + expect(response.body).not.toHaveProperty('mustChangePassword'); + expect(response.body).toHaveProperty('userId'); + expect(response.body).toHaveProperty('name'); + expect(response.body).toHaveProperty('role'); + expect(response.body).toHaveProperty('registrationDate'); + }); + + it('should return 404 when user does not exist', async () => { + const response = await getUser('nonexistent_user_123'); + expect(response.status).toBe(404); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('not found'); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/users/get-users.test.ts b/meet-ce/backend/tests/integration/api/users/get-users.test.ts new file mode 100644 index 00000000..b5f09561 --- /dev/null +++ b/meet-ce/backend/tests/integration/api/users/get-users.test.ts @@ -0,0 +1,260 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetUser, MeetUserRole } from '@openvidu-meet/typings'; +import { MEET_ENV } from '../../../../src/environment.js'; +import { expectValidationError } from '../../../helpers/assertion-helpers.js'; +import { createUser, deleteAllUsers, getUsers, startTestServer } from '../../../helpers/request-helpers.js'; + +describe('Users API Tests', () => { + beforeAll(async () => { + await startTestServer(); + + // Create a timestamp to ensure unique IDs (max 6 digits to fit in 20 char limit) + const ts = String(Date.now()).slice(-6); + + // Create users sequentially to have predictable registration order + await createUser({ + userId: `alice_${ts}`, + name: 'Alice Anderson', + password: 'password123', + role: MeetUserRole.ADMIN + }); + await createUser({ + userId: `bob_${ts}`, + name: 'Bob Brown', + password: 'password123', + role: MeetUserRole.USER + }); + await createUser({ + userId: `charlie_${ts}`, + name: 'Charlie Clark', + password: 'password123', + role: MeetUserRole.ROOM_MEMBER + }); + await createUser({ + userId: `diana_${ts}`, + name: 'Diana Davis', + password: 'password123', + role: MeetUserRole.ADMIN + }); + await createUser({ + userId: `eve_${ts}`, + name: 'Eve Evans', + password: 'password123', + role: MeetUserRole.USER + }); + }); + + afterAll(async () => { + await deleteAllUsers(); + }); + + describe('Get Users Tests', () => { + it('should successfully get all users without filters', async () => { + const response = await getUsers(); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('users'); + expect(Array.isArray(response.body.users)).toBe(true); + + // Root admin + 5 test users = 6 total + expect(response.body.users.length).toBe(6); + expect(response.body).toHaveProperty('pagination'); + expect(response.body.pagination).toHaveProperty('isTruncated', false); + }); + + it('should filter users by userId using partial match', async () => { + const response = await getUsers({ userId: 'alice' }); + expect(response.status).toBe(200); + expect(response.body.users).toHaveLength(1); + expect(response.body.users[0].userId).toContain('alice'); + }); + + it('should filter users by userId case-insensitive', async () => { + const response = await getUsers({ userId: 'ALICE' }); + expect(response.status).toBe(200); + expect(response.body.users).toHaveLength(1); + expect(response.body.users[0].userId).toContain('alice'); + }); + + it('should filter users by name using partial match', async () => { + const response = await getUsers({ name: 'Anderson' }); + expect(response.status).toBe(200); + expect(response.body.users).toHaveLength(1); + expect(response.body.users[0].name).toContain('Anderson'); + }); + + it('should filter users by name case-insensitive', async () => { + const response = await getUsers({ name: 'brown' }); + expect(response.status).toBe(200); + expect(response.body.users).toHaveLength(1); + expect(response.body.users[0].name).toContain('Brown'); + }); + + it('should filter root admin by userId', async () => { + const response = await getUsers({ userId: MEET_ENV.INITIAL_ADMIN_USER }); + expect(response.status).toBe(200); + expect(response.body.users).toHaveLength(1); + expect(response.body.users[0]).toHaveProperty('userId', MEET_ENV.INITIAL_ADMIN_USER); + expect(response.body.users[0]).toHaveProperty('role', MeetUserRole.ADMIN); + }); + + it('should filter users by role', async () => { + const response = await getUsers({ role: MeetUserRole.ADMIN }); + expect(response.status).toBe(200); + + // We created 2 admins + 1 root admin = 3 total + expect(response.body.users).toHaveLength(3); + response.body.users.forEach((user: MeetUser) => { + expect(user).toHaveProperty('role', MeetUserRole.ADMIN); + }); + }); + + it('should return empty array when no users match filter', async () => { + const response = await getUsers({ userId: 'nonexistent123xyz' }); + expect(response.status).toBe(200); + expect(response.body.users).toHaveLength(0); + }); + + it('should respect maxItems parameter', async () => { + const response = await getUsers({ maxItems: 2 }); + expect(response.status).toBe(200); + expect(response.body.users.length).toBe(2); + expect(response.body.pagination).toHaveProperty('maxItems', 2); + }); + + it('should limit maxItems to 100', async () => { + const response = await getUsers({ maxItems: 150 }); + expect(response.status).toBe(200); + expect(response.body.pagination).toHaveProperty('maxItems', 100); + }); + + it('should use default maxItems of 10 when not specified', async () => { + const response = await getUsers(); + expect(response.status).toBe(200); + expect(response.body.pagination).toHaveProperty('maxItems', 10); + }); + + it('should handle pagination with isTruncated flag', async () => { + // Request only 3 users when we have 6 total + const response = await getUsers({ maxItems: 3 }); + expect(response.status).toBe(200); + expect(response.body.users).toHaveLength(3); + expect(response.body.pagination).toHaveProperty('isTruncated', true); + expect(response.body.pagination).toHaveProperty('nextPageToken'); + }); + + it('should support pagination with nextPageToken', async () => { + // First page: 3 users + const firstResponse = await getUsers({ maxItems: 3 }); + expect(firstResponse.status).toBe(200); + expect(firstResponse.body.users).toHaveLength(3); + expect(firstResponse.body.pagination.isTruncated).toBe(true); + + // Second page: next 3 users + const secondResponse = await getUsers({ + maxItems: 3, + nextPageToken: firstResponse.body.pagination.nextPageToken + }); + expect(secondResponse.status).toBe(200); + expect(secondResponse.body.users).toHaveLength(3); + expect(secondResponse.body.pagination.isTruncated).toBe(false); + }); + + it('should sort users by registrationDate in descending order by default', async () => { + const response = await getUsers(); + expect(response.status).toBe(200); + expect(response.body.users.length).toBe(6); + + // Eve was created last, should be first (most recent) + expect(response.body.users[0].userId).toContain('eve'); + // Root admin was created first, should be last (oldest) + expect(response.body.users[5].userId).toBe('admin'); + + // Verify all dates are in descending order + for (let i = 0; i < response.body.users.length - 1; i++) { + expect(response.body.users[i].registrationDate).toBeGreaterThanOrEqual( + response.body.users[i + 1].registrationDate + ); + } + }); + + it('should sort users by registrationDate in ascending order', async () => { + const response = await getUsers({ sortField: 'registrationDate', sortOrder: 'asc' }); + expect(response.status).toBe(200); + expect(response.body.users.length).toBe(6); + + // Root admin was created first, should be first (oldest) + expect(response.body.users[0].userId).toBe('admin'); + // Eve was created last, should be last (most recent) + expect(response.body.users[5].userId).toContain('eve'); + + // Verify all dates are in ascending order + for (let i = 0; i < response.body.users.length - 1; i++) { + expect(response.body.users[i].registrationDate).toBeLessThanOrEqual( + response.body.users[i + 1].registrationDate + ); + } + }); + + it('should sort users by name in descending order', async () => { + const response = await getUsers({ sortField: 'name', sortOrder: 'desc' }); + expect(response.status).toBe(200); + expect(response.body.users.length).toBe(6); + + // Verify names are in reverse alphabetical order (case-insensitive) + for (let i = 0; i < response.body.users.length - 1; i++) { + const currentName = response.body.users[i].name.toLowerCase(); + const nextName = response.body.users[i + 1].name.toLowerCase(); + expect(currentName.localeCompare(nextName)).toBeGreaterThanOrEqual(0); + } + }); + + it('should sort users by name in ascending order', async () => { + const response = await getUsers({ sortField: 'name', sortOrder: 'asc' }); + expect(response.status).toBe(200); + expect(response.body.users.length).toBe(6); + + // Verify names are in alphabetical order (case-insensitive) + for (let i = 0; i < response.body.users.length - 1; i++) { + const currentName = response.body.users[i].name.toLowerCase(); + const nextName = response.body.users[i + 1].name.toLowerCase(); + expect(currentName.localeCompare(nextName)).toBeLessThanOrEqual(0); + } + }); + + it('should not expose sensitive fields in user objects', async () => { + const response = await getUsers({ maxItems: 3 }); + expect(response.status).toBe(200); + + response.body.users.forEach((user: MeetUser) => { + expect(user).not.toHaveProperty('passwordHash'); + expect(user).not.toHaveProperty('mustChangePassword'); + expect(user).toHaveProperty('userId'); + expect(user).toHaveProperty('name'); + expect(user).toHaveProperty('role'); + expect(user).toHaveProperty('registrationDate'); + }); + }); + }); + + describe('Get Users Validation Tests', () => { + it('should fail when maxItems is zero', async () => { + const response = await getUsers({ maxItems: 0 }); + expectValidationError(response, 'maxItems', 'must be a positive number'); + }); + + it('should fail when maxItems is negative', async () => { + const response = await getUsers({ maxItems: -5 }); + expectValidationError(response, 'maxItems', 'must be a positive number'); + }); + + it('should fail when sortField is invalid', async () => { + const response = await getUsers({ sortField: 'userId' }); + expectValidationError(response, 'sortField', 'Invalid enum value'); + }); + + it('should fail when sortOrder is invalid', async () => { + const response = await getUsers({ sortOrder: 'invalid' }); + expectValidationError(response, 'sortOrder', 'Invalid enum value'); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/users/reset-user-password.test.ts b/meet-ce/backend/tests/integration/api/users/reset-user-password.test.ts new file mode 100644 index 00000000..9f3c3ccc --- /dev/null +++ b/meet-ce/backend/tests/integration/api/users/reset-user-password.test.ts @@ -0,0 +1,171 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetUserRole } from '@openvidu-meet/typings'; +import { MEET_ENV } from '../../../../src/environment.js'; +import { expectValidationError } from '../../../helpers/assertion-helpers.js'; +import { + createUser, + deleteAllUsers, + loginReq, + resetUserPassword, + startTestServer +} from '../../../helpers/request-helpers.js'; +import { setupTestUsers } from '../../../helpers/test-scenarios.js'; +import { TestUsers } from '../../../interfaces/scenarios.js'; + +describe('Users API Tests', () => { + let testUsers: TestUsers; + + beforeAll(async () => { + await startTestServer(); + testUsers = await setupTestUsers(); + }); + + afterAll(async () => { + await deleteAllUsers(); + }); + + describe('Reset User Password Tests', () => { + it('should successfully reset user password by admin', async () => { + const newPassword = 'newpassword123'; + const response = await resetUserPassword( + testUsers.user.user.userId, + newPassword, + testUsers.admin.accessToken + ); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('reset successfully'); + expect(response.body.message).toContain('must change password on next login'); + }); + + it('should allow user to login with new password after reset', async () => { + const userId = `user_${Date.now()}`; + const initialPassword = 'password123'; + const newPassword = 'resetpassword456'; + + // Create user + await createUser({ + userId, + name: 'Test User', + password: initialPassword, + role: MeetUserRole.USER + }); + + // Reset password + const response = await resetUserPassword(userId, newPassword); + expect(response.status).toBe(200); + + // Verify old password no longer works + const loginOldResponse = await loginReq({ userId, password: initialPassword }); + expect(loginOldResponse.status).toBe(404); + + // Verify new password works + const loginResponse = await loginReq({ userId, password: newPassword }); + expect(loginResponse.status).toBe(200); + expect(loginResponse.body).toHaveProperty('accessToken'); + expect(loginResponse.body).toHaveProperty('mustChangePassword', true); + }); + + it('should set mustChangePassword flag to true after password reset', async () => { + const newPassword = 'resetpassword456'; + const response = await resetUserPassword(testUsers.user.user.userId, newPassword); + expect(response.status).toBe(200); + + // Login and verify mustChangePassword is true + const loginResponse = await loginReq({ + userId: testUsers.user.user.userId, + password: newPassword + }); + expect(loginResponse.status).toBe(200); + expect(loginResponse.body).toHaveProperty('mustChangePassword', true); + }); + + it('should reset password for ADMIN users', async () => { + const userId = `admin_${Date.now()}`; + const newPassword = 'newadminpass123'; + + // Create ADMIN user + await createUser({ + userId, + name: 'Test User', + password: 'password123', + role: MeetUserRole.ADMIN + }); + + // Reset password + const response = await resetUserPassword(userId, newPassword, testUsers.admin.accessToken); + expect(response.status).toBe(200); + + // Verify admin can login with new password + const loginResponse = await loginReq({ + userId, + password: newPassword + }); + expect(loginResponse.status).toBe(200); + expect(loginResponse.body).toHaveProperty('mustChangePassword', true); + }); + + it('should reset password for ROOM_MEMBER users', async () => { + const newPassword = 'newroompass123'; + const response = await resetUserPassword(testUsers.roomMember.user.userId, newPassword); + expect(response.status).toBe(200); + + // Verify room member can login with new password + const loginResponse = await loginReq({ + userId: testUsers.roomMember.user.userId, + password: newPassword + }); + expect(loginResponse.status).toBe(200); + expect(loginResponse.body).toHaveProperty('mustChangePassword', true); + }); + + it.skip('should fail when trying to reset own password', async () => { + const response = await resetUserPassword( + testUsers.admin.user.userId, + 'newpassword123', + testUsers.admin.accessToken + ); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot reset your own password'); + }); + + it('should fail when root admin tries to reset own password', async () => { + const response = await resetUserPassword(MEET_ENV.INITIAL_ADMIN_USER, 'newpassword123'); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot reset your own password'); + }); + + it('should fail when trying to reset root admin password', async () => { + const response = await resetUserPassword( + MEET_ENV.INITIAL_ADMIN_USER, + 'newpassword123', + testUsers.admin.accessToken + ); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot reset password for the root admin user'); + }); + + it('should fail when user does not exist', async () => { + const response = await resetUserPassword('nonexistent_user_123', 'newpassword123'); + expect(response.status).toBe(404); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('not found'); + }); + }); + + describe('Reset User Password Validation Tests', () => { + it('should fail when newPassword is missing', async () => { + const response = await resetUserPassword(testUsers.user.user.userId, undefined as unknown as string); + expectValidationError(response, 'newPassword', 'Required'); + }); + + it('should fail when newPassword is too short', async () => { + const response = await resetUserPassword(testUsers.user.user.userId, '1234'); + expectValidationError(response, 'newPassword', 'at least 5 characters'); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/users/update-user-role.test.ts b/meet-ce/backend/tests/integration/api/users/update-user-role.test.ts new file mode 100644 index 00000000..ed760ebb --- /dev/null +++ b/meet-ce/backend/tests/integration/api/users/update-user-role.test.ts @@ -0,0 +1,154 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetUserRole } from '@openvidu-meet/typings'; +import { MEET_ENV } from '../../../../src/environment.js'; +import { expectValidationError } from '../../../helpers/assertion-helpers.js'; +import { + createUser, + deleteAllUsers, + getUser, + startTestServer, + updateUserRole +} from '../../../helpers/request-helpers.js'; +import { setupTestUsers } from '../../../helpers/test-scenarios.js'; +import { TestUsers } from '../../../interfaces/scenarios.js'; + +describe('Users API Tests', () => { + let testUsers: TestUsers; + + beforeAll(async () => { + await startTestServer(); + testUsers = await setupTestUsers(); + }); + + afterAll(async () => { + await deleteAllUsers(); + }); + + describe('Update User Role Tests', () => { + it('should successfully update user role to ADMIN', async () => { + const response = await updateUserRole(testUsers.user.user.userId, MeetUserRole.ADMIN); + expect(response.status).toBe(200); + + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('updated successfully'); + expect(response.body.message).toContain(MeetUserRole.ADMIN); + + expect(response.body).toHaveProperty('user'); + expect(response.body.user).toHaveProperty('userId', testUsers.user.user.userId); + expect(response.body.user).toHaveProperty('role', MeetUserRole.ADMIN); + }); + + it('should successfully update user role to USER', async () => { + // Create user with ADMIN role first + const userId = `user_${Date.now()}`; + await createUser({ + userId, + name: 'Test User', + password: 'password123', + role: MeetUserRole.ADMIN + }); + + // Update role to USER + const response = await updateUserRole(userId, MeetUserRole.USER); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('user'); + expect(response.body.user).toHaveProperty('role', MeetUserRole.USER); + }); + + it('should successfully update user role to ROOM_MEMBER', async () => { + // Create user with USER role first + const userId = `user_${Date.now()}`; + await createUser({ + userId, + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }); + + // Update role to ROOM_MEMBER + const response = await updateUserRole(userId, MeetUserRole.ROOM_MEMBER); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('user'); + expect(response.body.user).toHaveProperty('role', MeetUserRole.ROOM_MEMBER); + }); + + it('should persist role change after update', async () => { + // Create user with USER role first + const userId = `user_${Date.now()}`; + await createUser({ + userId, + name: 'Test User', + password: 'password123', + role: MeetUserRole.USER + }); + + // Update role to ADMIN + const updateResponse = await updateUserRole(userId, MeetUserRole.ADMIN); + expect(updateResponse.status).toBe(200); + + // Verify role persisted by getting user + const getUserResponse = await getUser(userId); + expect(getUserResponse.status).toBe(200); + expect(getUserResponse.body).toHaveProperty('role', MeetUserRole.ADMIN); + }); + + it('should not expose sensitive fields in response', async () => { + const response = await updateUserRole(testUsers.user.user.userId, MeetUserRole.ADMIN); + expect(response.status).toBe(200); + expect(response.body.user).not.toHaveProperty('passwordHash'); + expect(response.body.user).not.toHaveProperty('mustChangePassword'); + expect(response.body.user).toHaveProperty('userId'); + expect(response.body.user).toHaveProperty('name'); + expect(response.body.user).toHaveProperty('role'); + expect(response.body.user).toHaveProperty('registrationDate'); + }); + + it('should fail when trying to update own role', async () => { + const response = await updateUserRole( + testUsers.admin.user.userId, + MeetUserRole.USER, + testUsers.admin.accessToken + ); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot change your own role'); + }); + + it('should fail when root admin tries to update own role', async () => { + const response = await updateUserRole(MEET_ENV.INITIAL_ADMIN_USER, MeetUserRole.USER); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot change the role of the root admin user'); + }); + + it('should fail when trying to update root admin role', async () => { + const response = await updateUserRole( + MEET_ENV.INITIAL_ADMIN_USER, + MeetUserRole.USER, + testUsers.admin.accessToken + ); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Cannot change the role of the root admin user'); + }); + + it('should fail when user does not exist', async () => { + const response = await updateUserRole('nonexistent_user_123', MeetUserRole.ADMIN); + expect(response.status).toBe(404); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('not found'); + }); + }); + + describe('Update User Role Validation Tests', () => { + it('should fail when role is missing', async () => { + const response = await updateUserRole(testUsers.user.user.userId, undefined as unknown as MeetUserRole); + expectValidationError(response, 'role', 'Required'); + }); + + it('should fail when role is invalid', async () => { + const response = await updateUserRole(testUsers.user.user.userId, 'INVALID_ROLE' as MeetUserRole); + expectValidationError(response, 'role', 'Invalid enum value'); + }); + }); +});