UNPKG

@sync-in/server

Version:

The secure, open-source platform for file storage, sharing, collaboration, and sync

778 lines (777 loc) 30.5 kB
/* * Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com> * This file is part of Sync-in | The open source file sync and share solution * See the LICENSE file for licensing details */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const _common = require("@nestjs/common"); const _testing = require("@nestjs/testing"); const _authmanagerservice = require("../../../authentication/services/auth-manager.service"); const _group = require("../constants/group"); const _user = require("../constants/user"); const _usermodel = require("../models/user.model"); const _adminusersmanagerservice = require("./admin-users-manager.service"); const _adminusersqueriesservice = require("./admin-users-queries.service"); // mock file utils used by the service (delete/rename user space) jest.mock('../../files/utils/files', ()=>({ isPathExists: jest.fn(), moveFiles: jest.fn(), removeFiles: jest.fn() })); // mock hash/anonymize utilities (preserve other module exports) jest.mock('../../../common/functions', ()=>{ const actual = jest.requireActual('../../../common/functions'); return { ...actual, hashPassword: jest.fn(async (pwd)=>`hashed:${pwd}`), anonymizePassword: jest.fn((dto)=>({ ...dto, password: '***' })) }; }); // Alias FS mocks (avoid repetitions) const fs = jest.requireMock('../../files/utils/files'); // Helper utilities const expectHttp = async (p)=>expect(p).rejects.toBeInstanceOf(_common.HttpException); const spyMakePaths = ()=>jest.spyOn(_usermodel.UserModel.prototype, 'makePaths').mockResolvedValueOnce(undefined); describe(_adminusersmanagerservice.AdminUsersManager.name, ()=>{ let service; // deep mocks let authManagerMock; let adminQueriesMock; const setUser = (u)=>adminQueriesMock.listUsers.mockResolvedValueOnce(u); const setGuest = (g)=>adminQueriesMock.usersQueries.listGuests.mockResolvedValueOnce(g); const baseUser = { id: 10, login: 'john', email: 'john@example.com', isActive: true, role: _user.USER_ROLE.USER, groups: [ { id: 1 }, { id: 3 } ] }; beforeAll(async ()=>{ authManagerMock = { setCookies: jest.fn() }; adminQueriesMock = { listUsers: jest.fn(), usersQueries: { listGuests: jest.fn(), from: jest.fn(), createUserOrGuest: jest.fn(), updateUserOrGuest: jest.fn(), deleteUser: jest.fn(), compareUserPassword: jest.fn(), checkGroupNameExists: jest.fn(), checkUserExists: jest.fn(), searchUsersOrGroups: jest.fn(), clearWhiteListCaches: jest.fn() }, updateUserGroups: jest.fn(), updateGuestManagers: jest.fn(), deleteUser: jest.fn(), groupFromName: jest.fn(), browseGroupMembers: jest.fn(), browseRootGroupMembers: jest.fn(), groupFromId: jest.fn(), createGroup: jest.fn(), updateGroup: jest.fn(), deleteGroup: jest.fn(), addUsersToGroup: jest.fn(), updateUserFromGroup: jest.fn(), removeUserFromGroup: jest.fn() }; const module = await _testing.Test.createTestingModule({ providers: [ _adminusersmanagerservice.AdminUsersManager, { provide: _authmanagerservice.AuthManager, useValue: authManagerMock }, { provide: _adminusersqueriesservice.AdminUsersQueries, useValue: adminQueriesMock } ] }).compile(); module.useLogger([ 'fatal' ]); service = module.get(_adminusersmanagerservice.AdminUsersManager); }); beforeEach(()=>{ jest.clearAllMocks(); }); it('should be defined', ()=>{ expect(service).toBeDefined(); }); describe('getUser / getGuest', ()=>{ it('user ok + not found + guest ok', async ()=>{ setUser(baseUser); expect(await service.getUser(10)).toEqual(baseUser); expect(adminQueriesMock.listUsers).toHaveBeenCalledWith(10); setUser(null); await expectHttp(service.getUser(999)); const guest = { id: 22, login: 'guest', email: 'g@x', managers: [], role: _user.USER_ROLE.GUEST }; setGuest(guest); expect(await service.getGuest(22)).toEqual(guest); expect(adminQueriesMock.usersQueries.listGuests).toHaveBeenCalledWith(22, 0, true); }); }); describe('createUserOrGuest', ()=>{ it.each([ { role: _user.USER_ROLE.USER, asAdmin: false, id: 101, exp: 'UserModel' }, { role: _user.USER_ROLE.GUEST, asAdmin: true, id: 202, exp: 'AdminGuest' }, { role: _user.USER_ROLE.USER, asAdmin: true, id: 707, exp: 'AdminUser' }, { role: _user.USER_ROLE.GUEST, asAdmin: false, id: 808, exp: 'UserModel' } ])('creation matrix ($role / asAdmin=$asAdmin)', async ({ role, asAdmin, id, exp })=>{ const dto = { login: 'alice', email: 'a@x', password: 'pwd', managers: role === _user.USER_ROLE.GUEST ? [ 1 ] : [] }; adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(false); adminQueriesMock.usersQueries.createUserOrGuest.mockResolvedValueOnce(id); spyMakePaths(); if (exp === 'AdminUser') setUser({ id, login: 'alice', role }); if (exp === 'AdminGuest') setGuest({ id, login: 'alice', role }); const res = await service.createUserOrGuest(dto, role, asAdmin); if (exp === 'UserModel') { expect(res).toBeInstanceOf(_usermodel.UserModel); expect(res.id).toBe(id); } else if (exp === 'AdminUser') { expect(adminQueriesMock.listUsers).toHaveBeenCalledWith(id); } else { expect(adminQueriesMock.usersQueries.listGuests).toHaveBeenCalledWith(id, 0, true); } expect(adminQueriesMock.usersQueries.createUserOrGuest).toHaveBeenCalledWith(expect.objectContaining({ login: 'alice', email: 'a@x', password: 'hashed:pwd' }), role); }); it('creation errors: duplication and DB error', async ()=>{ adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce({ login: 'dup', email: 'dup@x' }); await expectHttp(service.createUserOrGuest({ login: 'dup', email: 'dup@x', password: 'p', managers: [] }, _user.USER_ROLE.USER, false)); adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(false); adminQueriesMock.usersQueries.createUserOrGuest.mockRejectedValueOnce(new Error('db fail')); await expectHttp(service.createUserOrGuest({ login: 'bob', email: 'b@x', password: 'p', managers: [] }, _user.USER_ROLE.USER, false)); }); }); describe('updateUserOrGuest - USER branch', ()=>{ it('full update + FS rename + groups diff', async ()=>{ const current = { ...baseUser, groups: [ { id: 1 }, { id: 3 } ] }; setUser(current); const updated = { ...current, login: 'johnny', email: 'j@new' }; setUser(updated); adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(false); adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(false); fs.isPathExists.mockResolvedValueOnce(true); fs.isPathExists.mockResolvedValueOnce(false); fs.moveFiles.mockResolvedValueOnce(undefined); adminQueriesMock.usersQueries.updateUserOrGuest.mockResolvedValueOnce(true); adminQueriesMock.updateUserGroups.mockResolvedValueOnce(undefined); const dto = { login: 'johnny', email: 'j@new', isActive: true, password: 'newpwd', groups: [ 3, 5 ] }; const res = await service.updateUserOrGuest(current.id, dto); expect(adminQueriesMock.usersQueries.updateUserOrGuest).toHaveBeenCalledWith(current.id, expect.objectContaining({ login: 'johnny', email: 'j@new', isActive: true, passwordAttempts: 0, password: 'hashed:newpwd' }), undefined); expect(adminQueriesMock.updateUserGroups).toHaveBeenCalledWith(current.id, { add: [ 5 ], delete: [ 1 ] }); expect(res).toEqual(updated); }); it('login/email conflict', async ()=>{ const current = { ...baseUser }; setUser(current); adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(true); await expectHttp(service.updateUserOrGuest(current.id, { login: 'taken' })); setUser(current); adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(true); await expectHttp(service.updateUserOrGuest(current.id, { email: 'dup@x' })); expect(adminQueriesMock.usersQueries.updateUserOrGuest).not.toHaveBeenCalled(); }); it('renameUserSpace impossible (new space exists)', async ()=>{ const current = { ...baseUser }; setUser(current); adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(false); fs.isPathExists.mockResolvedValueOnce(true); // current fs.isPathExists.mockResolvedValueOnce(true); // new already exists fs.moveFiles.mockResolvedValue(undefined); await expectHttp(service.updateUserOrGuest(current.id, { login: 'new' })); }); it('DB update false => INTERNAL_SERVER_ERROR', async ()=>{ const current = { ...baseUser }; setUser(current); adminQueriesMock.usersQueries.updateUserOrGuest.mockResolvedValueOnce(false); await expectHttp(service.updateUserOrGuest(current.id, { email: 'e@x' })); }); it('updateUserGroups fails', async ()=>{ const current = { ...baseUser, groups: [ { id: 1 } ] }; setUser(current); adminQueriesMock.usersQueries.updateUserOrGuest.mockResolvedValueOnce(true); adminQueriesMock.updateUserGroups.mockRejectedValueOnce(new Error('group error')); await expectHttp(service.updateUserOrGuest(current.id, { groups: [ 2 ] })); }); it('no change when login or email unchanged', async ()=>{ const current = { ...baseUser }; setUser(current); setUser(current); expect(await service.updateUserOrGuest(current.id, { login: current.login })).toEqual(current); expect(adminQueriesMock.usersQueries.checkUserExists).not.toHaveBeenCalled(); setUser(current); setUser(current); expect(await service.updateUserOrGuest(current.id, { email: current.email })).toEqual(current); expect(adminQueriesMock.usersQueries.updateUserOrGuest).not.toHaveBeenCalled(); }); it('default branch (unknown field)', async ()=>{ const current = { ...baseUser }; setUser(current); adminQueriesMock.usersQueries.updateUserOrGuest.mockResolvedValueOnce(true); const updated = { ...current, language: 'fr' }; setUser(updated); const res = await service.updateUserOrGuest(current.id, { language: 'fr' }); expect(adminQueriesMock.usersQueries.updateUserOrGuest).toHaveBeenCalledWith(current.id, expect.objectContaining({ language: 'fr' }), undefined); expect(res).toEqual(updated); }); }); describe('updateUserOrGuest - GUEST branch', ()=>{ it('update guest + managers diff', async ()=>{ const guest = { id: 33, login: 'g', email: 'g@x', managers: [ { id: 2 }, { id: 7 } ], role: _user.USER_ROLE.GUEST }; setGuest(guest); const updatedGuest = { ...guest, email: 'new@x' }; setGuest(updatedGuest); adminQueriesMock.usersQueries.updateUserOrGuest.mockResolvedValueOnce(true); adminQueriesMock.updateGuestManagers.mockResolvedValueOnce(undefined); const res = await service.updateUserOrGuest(guest.id, { email: 'new@x', managers: [ 7, 9 ] }, _user.USER_ROLE.GUEST); expect(adminQueriesMock.usersQueries.updateUserOrGuest).toHaveBeenCalledWith(guest.id, { email: 'new@x' }, _user.USER_ROLE.GUEST); expect(adminQueriesMock.updateGuestManagers).toHaveBeenCalledWith(guest.id, { add: [ 9 ], delete: [ 2 ] }); expect(res).toEqual(updatedGuest); }); it('validations updateGuest', async ()=>{ expect(()=>service.updateGuest(1, {})).toThrow(/no changes to update/i); expect(()=>service.updateGuest(1, { managers: [] })).toThrow(/guest must have at least one manager/i); }); it('updateGuestManagers échoue', async ()=>{ const guest = { id: 33, login: 'g', email: 'g@x', managers: [ { id: 2 } ], role: _user.USER_ROLE.GUEST }; setGuest(guest); adminQueriesMock.usersQueries.updateUserOrGuest.mockResolvedValueOnce(true); adminQueriesMock.updateGuestManagers.mockRejectedValueOnce(new Error('mgr error')); await expectHttp(service.updateUserOrGuest(guest.id, { managers: [ 3 ] }, _user.USER_ROLE.GUEST)); }); }); describe('deleteUserOrGuest / deleteGuest', ()=>{ it('delete user + optional space and errors', async ()=>{ adminQueriesMock.deleteUser.mockResolvedValueOnce(true); fs.isPathExists.mockResolvedValueOnce(true); fs.removeFiles.mockResolvedValueOnce(undefined); await expect(service.deleteUserOrGuest(10, 'john', { deleteSpace: true, isGuest: false })).resolves.toBeUndefined(); expect(adminQueriesMock.deleteUser).toHaveBeenCalledWith(10, 'john'); expect(fs.isPathExists).toHaveBeenCalled(); expect(fs.removeFiles).toHaveBeenCalled(); adminQueriesMock.deleteUser.mockRejectedValueOnce(new Error('db crash')); await expectHttp(service.deleteUserOrGuest(10, 'john', { deleteSpace: false })); }); }); describe('deleteUserFromAdmin', ()=>{ it('admin password incorrect / deletion ok', async ()=>{ adminQueriesMock.usersQueries.compareUserPassword.mockResolvedValueOnce(false); await expectHttp(service.deleteUserOrGuestFromAdmin(10, { isGuest: false })); adminQueriesMock.usersQueries.compareUserPassword.mockResolvedValueOnce(true); adminQueriesMock.usersQueries.from.mockResolvedValueOnce({ id: 10, login: 'to-del' }); adminQueriesMock.deleteUser.mockResolvedValueOnce(true); await service.deleteUserOrGuestFromAdmin(10, { isGuest: false, deleteSpace: true }); expect(adminQueriesMock.deleteUser).toHaveBeenCalledWith(10, 'to-del'); }); }); describe('groups', ()=>{ it('browseGroups with/without name + NOT_FOUND', async ()=>{ adminQueriesMock.groupFromName.mockResolvedValueOnce({ id: 5, name: 'dev', type: _group.GROUP_TYPE.USER }); adminQueriesMock.browseGroupMembers.mockResolvedValueOnce([ { id: 1 }, { id: 2 } ]); const withName = await service.browseGroups('dev', _group.GROUP_TYPE.USER); expect(withName.parentGroup).toEqual({ id: 5, name: 'dev', type: _group.GROUP_TYPE.USER }); expect(withName.members).toHaveLength(2); adminQueriesMock.browseRootGroupMembers.mockResolvedValueOnce([ { id: 3 } ]); const root = await service.browseGroups(undefined, _group.GROUP_TYPE.USER); expect(root.parentGroup).toBeUndefined(); expect(root.members).toEqual([ { id: 3 } ]); adminQueriesMock.groupFromName.mockResolvedValueOnce(null); await expectHttp(service.browseGroups('unknown')); }); it('getGroup OK / NOT_FOUND', async ()=>{ adminQueriesMock.groupFromId.mockResolvedValueOnce({ id: 9 }); expect(await service.getGroup(9)).toEqual({ id: 9 }); adminQueriesMock.groupFromId.mockResolvedValueOnce(null); await expectHttp(service.getGroup(999)); }); it('createGroup OK + validations + creation error', async ()=>{ const dto = { name: 'team', type: _group.GROUP_TYPE.USER }; adminQueriesMock.usersQueries.checkGroupNameExists.mockResolvedValueOnce(false); adminQueriesMock.createGroup.mockResolvedValueOnce(123); adminQueriesMock.groupFromId.mockResolvedValueOnce({ id: 123, name: 'team' }); expect(await service.createGroup(dto)).toEqual({ id: 123, name: 'team' }); await expectHttp(service.createGroup({})); adminQueriesMock.usersQueries.checkGroupNameExists.mockResolvedValueOnce(true); await expectHttp(service.createGroup(dto)); expect(adminQueriesMock.createGroup).toHaveBeenCalledTimes(1); // only the first one OK adminQueriesMock.usersQueries.checkGroupNameExists.mockResolvedValueOnce(false); adminQueriesMock.createGroup.mockRejectedValueOnce(new Error('db err')); await expectHttp(service.createGroup(dto)); }); it('updateGroup success / failure', async ()=>{ adminQueriesMock.usersQueries.checkGroupNameExists.mockResolvedValueOnce(false); adminQueriesMock.updateGroup.mockResolvedValueOnce(true); adminQueriesMock.groupFromId.mockResolvedValueOnce({ id: 5, name: 'new' }); expect(await service.updateGroup(5, { name: 'new' })).toEqual({ id: 5, name: 'new' }); adminQueriesMock.updateGroup.mockResolvedValueOnce(false); await expectHttp(service.updateGroup(5, {})); }); it('deleteGroup success / fail', async ()=>{ adminQueriesMock.deleteGroup.mockResolvedValueOnce(true); await expect(service.deleteGroup(5)).resolves.toBeUndefined(); adminQueriesMock.deleteGroup.mockResolvedValueOnce(false); await expectHttp(service.deleteGroup(6)); }); it('addUsersToGroup: NOT_FOUND + error', async ()=>{ adminQueriesMock.groupFromId.mockResolvedValueOnce(null); await expectHttp(service.addUsersToGroup(1, [ 2, 3 ])); adminQueriesMock.groupFromId.mockResolvedValueOnce({ id: 1, type: _group.GROUP_TYPE.USER }); adminQueriesMock.addUsersToGroup.mockRejectedValueOnce(new Error('bad users')); await expectHttp(service.addUsersToGroup(1, [ 2 ])); }); it('updateUserFromGroup / removeUserFromGroup BAD_REQUEST errors', async ()=>{ adminQueriesMock.updateUserFromGroup.mockRejectedValueOnce(new Error('bad role')); await expectHttp(service.updateUserFromGroup(1, 2, { role: _user.USER_GROUP_ROLE.MEMBER })); adminQueriesMock.removeUserFromGroup.mockRejectedValueOnce(new Error('not member')); await expectHttp(service.removeUserFromGroup(1, 2)); }); }); describe('searchMembers', ()=>{ it('forwards to usersQueries.searchUsersOrGroups', async ()=>{ const dto = { search: 'jo' }; adminQueriesMock.usersQueries.searchUsersOrGroups.mockResolvedValueOnce([ { id: 1 } ]); expect(await service.searchMembers(dto)).toEqual([ { id: 1 } ]); expect(adminQueriesMock.usersQueries.searchUsersOrGroups).toHaveBeenCalledWith(dto); }); }); describe('impersonation', ()=>{ const res = {}; it('self / bad password / ok + logout (guard + non-admin + admin)', async ()=>{ const admin = new _usermodel.UserModel({ id: 5 }, true); await expectHttp(service.impersonateUser(admin, 5, res)); adminQueriesMock.usersQueries.compareUserPassword.mockResolvedValueOnce(false); await expectHttp(service.impersonateUser(admin, 6, res)); const admin2 = new _usermodel.UserModel({ id: 5, clientId: 'c1' }, true); adminQueriesMock.usersQueries.compareUserPassword.mockResolvedValueOnce(true); adminQueriesMock.usersQueries.from.mockResolvedValueOnce({ id: 6, login: 'user' }); authManagerMock.setCookies.mockResolvedValueOnce({ accessToken: 't' }); expect(await service.impersonateUser(admin2, 6, res)).toEqual({ accessToken: 't' }); const notImpersonated = new _usermodel.UserModel({ id: 1 }, true); await expectHttp(service.logoutImpersonateUser(notImpersonated, res)); const impersonated = new _usermodel.UserModel({ id: 2, impersonatedFromId: 9, impersonatedClientId: 'X' }, true); adminQueriesMock.usersQueries.from.mockResolvedValueOnce({ id: 9, role: _user.USER_ROLE.USER }); await expectHttp(service.logoutImpersonateUser(impersonated, res)); adminQueriesMock.usersQueries.from.mockResolvedValueOnce({ id: 9, role: _user.USER_ROLE.ADMINISTRATOR }); authManagerMock.setCookies.mockResolvedValueOnce({ accessToken: 'admin' }); expect(await service.logoutImpersonateUser(impersonated, res)).toEqual({ accessToken: 'admin' }); }); }); describe('listing', ()=>{ it('forwards listUsers and listGuests', async ()=>{ const users = [ { id: 1 } ]; adminQueriesMock.listUsers.mockResolvedValueOnce(users); expect(await service.listUsers()).toEqual(users); expect(adminQueriesMock.listUsers).toHaveBeenCalledWith(); const guests = [ { id: 2 } ]; adminQueriesMock.usersQueries.listGuests.mockResolvedValueOnce(guests); expect(await service.listGuests()).toEqual(guests); expect(adminQueriesMock.usersQueries.listGuests).toHaveBeenCalledWith(null, null, true); }); }); describe('createGuest', ()=>{ it('adds the creator as default manager and returns admin guest', async ()=>{ const creator = new _usermodel.UserModel({ id: 88 }, true); const dto = { login: 'gg', email: 'g@x', password: 'pwd', managers: [] }; adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(false); adminQueriesMock.usersQueries.createUserOrGuest.mockResolvedValueOnce(505); spyMakePaths(); const expectedGuest = { id: 505, login: 'gg', role: _user.USER_ROLE.GUEST }; setGuest(expectedGuest); expect(await service.createGuest(creator, dto)).toEqual(expectedGuest); expect(adminQueriesMock.usersQueries.createUserOrGuest).toHaveBeenCalledWith(expect.objectContaining({ login: 'gg', email: 'g@x', managers: [ 88 ] }), _user.USER_ROLE.GUEST); }); }); describe('updateGuest wrapper', ()=>{ it('updateGuest() -> success', async ()=>{ const guest = { id: 33, login: 'g', email: 'g@x', managers: [ { id: 2 } ], role: _user.USER_ROLE.GUEST }; const updatedGuest = { ...guest, email: 'new@x' }; setGuest(guest); adminQueriesMock.usersQueries.updateUserOrGuest.mockResolvedValueOnce(true); setGuest(updatedGuest); expect(await service.updateGuest(guest.id, { email: 'new@x' })).toEqual(updatedGuest); }); }); describe('deleteUserSpace', ()=>{ it('space not existing / removeFiles failure', async ()=>{ fs.isPathExists.mockResolvedValueOnce(false); await expect(service.deleteUserSpace('nobody')).resolves.toBeUndefined(); fs.isPathExists.mockResolvedValueOnce(true); fs.removeFiles.mockRejectedValueOnce(new Error('fs error')); await expectHttp(service.deleteUserSpace('bob')); }); }); describe('renameUserSpace error handling', ()=>{ it('moveFiles throws then restore', async ()=>{ const current = { ...baseUser }; setUser(current); adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(false); fs.isPathExists.mockResolvedValueOnce(true); fs.isPathExists.mockResolvedValueOnce(false); fs.moveFiles.mockRejectedValueOnce(new Error('io error')); fs.moveFiles.mockResolvedValueOnce(undefined); await expectHttp(service.updateUserOrGuest(current.id, { login: 'new-login' })); expect(fs.moveFiles).toHaveBeenCalledTimes(2); }); it('current space missing -> early return', async ()=>{ const current = { ...baseUser }; setUser(current); adminQueriesMock.usersQueries.checkUserExists.mockResolvedValueOnce(false); fs.isPathExists.mockResolvedValueOnce(false); await expectHttp(service.updateUserOrGuest(current.id, { login: 'new-login' })); expect(fs.moveFiles).not.toHaveBeenCalled(); }); }); }); //# sourceMappingURL=admin-users-manager.service.spec.js.map