UNPKG

@sync-in/server

Version:

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

452 lines (451 loc) 19.3 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 _axios = require("@nestjs/axios"); const _config = require("@nestjs/config"); const _jwt = require("@nestjs/jwt"); const _testing = require("@nestjs/testing"); const _authmanagerservice = require("../../../authentication/services/auth-manager.service"); const _cacheservice = require("../../../infrastructure/cache/services/cache.service"); const _contextmanagerservice = require("../../../infrastructure/context/services/context-manager.service"); const _constants = require("../../../infrastructure/database/constants"); const _fileslockmanagerservice = require("../../files/services/files-lock-manager.service"); const _filesmanagerservice = require("../../files/services/files-manager.service"); const _filesqueriesservice = require("../../files/services/files-queries.service"); const _notificationsmanagerservice = require("../../notifications/services/notifications-manager.service"); const _sharesmanagerservice = require("../../shares/services/shares-manager.service"); const _sharesqueriesservice = require("../../shares/services/shares-queries.service"); const _spacesmanagerservice = require("../../spaces/services/spaces-manager.service"); const _spacesqueriesservice = require("../../spaces/services/spaces-queries.service"); const _usermodel = require("../../users/models/user.model"); const _adminusersmanagerservice = require("../../users/services/admin-users-manager.service"); const _adminusersqueriesservice = require("../../users/services/admin-users-queries.service"); const _usersmanagerservice = require("../../users/services/users-manager.service"); const _usersqueriesservice = require("../../users/services/users-queries.service"); const _avatar = require("../../users/utils/avatar"); const _linksmanagerservice = require("./links-manager.service"); const _linksqueriesservice = require("./links-queries.service"); jest.mock('../../users/utils/avatar', ()=>({ getAvatarBase64: jest.fn() })); describe(_linksmanagerservice.LinksManager.name, ()=>{ let service; let linksQueriesMock; let usersManagerMock; let filesManagerMock; let spacesManagerMock; let authManagerMock; const identity = { id: 42, login: 'visitor' }; const baseLink = { uuid: 'uuid-123', user: { id: 7, login: 'john', isActive: true }, requireAuth: false, limitAccess: 0, nbAccess: 0, expiresAt: null }; beforeAll(async ()=>{ linksQueriesMock = { linkFromUUID: jest.fn(), spaceLink: jest.fn(), incrementLinkNbAccess: jest.fn().mockResolvedValue(undefined) }; usersManagerMock = { compareUserPassword: jest.fn(), updateAccesses: jest.fn() }; filesManagerMock = { sendFileFromSpace: jest.fn() }; spacesManagerMock = { spaceEnv: jest.fn() }; authManagerMock = { setCookies: jest.fn() }; const module = await _testing.Test.createTestingModule({ providers: [ { provide: _constants.DB_TOKEN_PROVIDER, useValue: {} }, { provide: _cacheservice.Cache, useValue: {} }, { provide: _contextmanagerservice.ContextManager, useValue: {} }, { provide: _notificationsmanagerservice.NotificationsManager, useValue: {} }, { provide: _axios.HttpService, useValue: {} }, { provide: _fileslockmanagerservice.FilesLockManager, useValue: {} }, { provide: _config.ConfigService, useValue: {} }, { provide: _jwt.JwtService, useValue: {} }, { provide: _authmanagerservice.AuthManager, useValue: authManagerMock }, { provide: _usersmanagerservice.UsersManager, useValue: usersManagerMock }, { provide: _usersqueriesservice.UsersQueries, useValue: {} }, { provide: _adminusersmanagerservice.AdminUsersManager, useValue: {} }, { provide: _adminusersqueriesservice.AdminUsersQueries, useValue: {} }, { provide: _filesqueriesservice.FilesQueries, useValue: {} }, { provide: _filesmanagerservice.FilesManager, useValue: filesManagerMock }, { provide: _spacesqueriesservice.SpacesQueries, useValue: {} }, { provide: _spacesmanagerservice.SpacesManager, useValue: spacesManagerMock }, { provide: _sharesqueriesservice.SharesQueries, useValue: {} }, { provide: _sharesmanagerservice.SharesManager, useValue: {} }, { provide: _linksqueriesservice.LinksQueries, useValue: linksQueriesMock }, _linksmanagerservice.LinksManager ] }).compile(); module.useLogger([ 'fatal' ]); service = module.get(_linksmanagerservice.LinksManager); }); beforeEach(()=>{ jest.clearAllMocks(); }); it('should be defined', ()=>{ expect(service).toBeDefined(); }); describe('linkValidation', ()=>{ it('returns ok with sanitized owner and avatar when link is valid', async ()=>{ const link = { ...baseLink }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); const spaceLink = { owner: { login: 'jane' }, share: { isDir: true, alias: 's-alias', name: 'Docs' }, space: null }; linksQueriesMock.spaceLink.mockResolvedValueOnce(spaceLink); _avatar.getAvatarBase64.mockResolvedValueOnce('base64-xxx'); const res = await service.linkValidation(identity, link.uuid); expect(res.ok).toBe(true); expect(res.error).toBeNull(); expect(res.link).toBe(spaceLink); expect(spaceLink.owner.login).toBeUndefined(); expect(linksQueriesMock.spaceLink).toHaveBeenCalledWith(link.uuid); expect(_avatar.getAvatarBase64).toHaveBeenCalledWith('jane'); // extra coverage: directly assert private checkLink with default ignoreAuth=false const directCheck = service.checkLink(identity, link); expect(directCheck).toBe(true); }); it('returns error and null link when uuid is not found', async ()=>{ linksQueriesMock.linkFromUUID.mockResolvedValueOnce(null); const res = await service.linkValidation(identity, 'missing-uuid'); expect(res.ok).toBe(false); expect(res.error).toBeDefined(); expect(res.link).toBeNull(); }); it('returns unauthorized when auth is required and identity differs', async ()=>{ const link = { ...baseLink, requireAuth: true }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); const res = await service.linkValidation(identity, link.uuid); expect(res.ok).toBe(false); expect(String(res.error).toLowerCase()).toContain('unauthorized'); expect(res.link).toBeNull(); // extra coverage: directly assert private checkLink with default ignoreAuth=false const directCheck = service.checkLink(identity, link); expect(String(directCheck).toLowerCase()).toContain('unauthorized'); }); it('returns exceeded when nbAccess >= limitAccess', async ()=>{ const link = { ...baseLink, limitAccess: 5, nbAccess: 5 }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); const res = await service.linkValidation(identity, link.uuid); expect(res.ok).toBe(false); expect(String(res.error).toLowerCase()).toContain('exceeded'); }); }); describe('linkAccess', ()=>{ const req = { ip: '127.0.0.1' }; const res = {}; it('streams a file when link targets a single file', async ()=>{ const link = { ...baseLink, limitAccess: 10 }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); const spaceLink = { space: null, share: { isDir: false, name: 'file.txt', alias: 'share-alias' } }; linksQueriesMock.spaceLink.mockResolvedValueOnce(spaceLink); spacesManagerMock.spaceEnv.mockResolvedValueOnce({}); const streamable = { some: 'stream' }; filesManagerMock.sendFileFromSpace.mockReturnValueOnce({ checks: jest.fn().mockResolvedValueOnce(undefined), stream: jest.fn().mockResolvedValueOnce(streamable) }); // cover: incrementLinkNbAccess.catch(...) should log an error when the query rejects const logErrorSpy = jest.spyOn(service.logger, 'error').mockImplementation(()=>undefined); linksQueriesMock.incrementLinkNbAccess.mockRejectedValueOnce(new Error('increment boom')); const result = await service.linkDownload(identity, link.uuid, req, res); expect(result).toBe(streamable); expect(filesManagerMock.sendFileFromSpace).toHaveBeenCalled(); expect(linksQueriesMock.incrementLinkNbAccess).toHaveBeenCalledWith(link.uuid); // Assert repository selection for a share file (space falsy) // should call spacesManager.spaceEnv with SHARES and share alias when link.space is falsy expect(spacesManagerMock.spaceEnv).toHaveBeenCalledWith(expect.anything(), [ 'shares', 'share-alias' ]); // should log error when increment fails expect(logErrorSpy).toHaveBeenCalled(); }); it('authenticates and returns cookies when link targets a directory and user is different', async ()=>{ const link = { ...baseLink, requireAuth: false, user: { id: 7, login: 'john', isActive: true } }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); const spaceLink = { space: { alias: 'files', name: 'My Space' }, share: { isDir: true, name: 'ignored', alias: 'ignored' } }; linksQueriesMock.spaceLink.mockResolvedValueOnce(spaceLink); const loginDto = { token: 'jwt' }; authManagerMock.setCookies.mockResolvedValueOnce(loginDto); // cover: usersManager.updateAccesses.catch(...) should log an error when updateAccesses rejects const logErrorSpy = jest.spyOn(service.logger, 'error').mockImplementation(()=>undefined); usersManagerMock.updateAccesses.mockRejectedValueOnce(new Error('updateAccesses boom')); const result = await service.linkAccess(identity, link.uuid, req, res); expect(result).toBe(loginDto); expect(usersManagerMock.updateAccesses).toHaveBeenCalledWith(expect.anything(), req.ip, true); expect(authManagerMock.setCookies).toHaveBeenCalled(); // additionally cover the "space truthy" branch by calling the private helper directly // should call spacesManager.spaceEnv with FILES and space alias when link.space is truthy await service.spaceEnvFromLink(new _usermodel.UserModel(link.user), spaceLink); expect(spacesManagerMock.spaceEnv).toHaveBeenCalledWith(expect.anything(), [ 'files', 'files' ]); // should log error when updateAccesses fails expect(logErrorSpy).toHaveBeenCalled(); }); it('throws BAD_REQUEST when link is invalid', async ()=>{ const disabledLink = { ...baseLink, user: { id: 7, login: 'john', isActive: false } }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(disabledLink); await expect(service.linkAccess(identity, disabledLink.uuid, req, res)).rejects.toMatchObject({ status: 400 }); }); it('returns undefined for already authenticated directory access (same user) and does not set cookies or increment', async ()=>{ const sameUserIdentity = { id: baseLink.user.id, login: 'john' }; const link = { ...baseLink, requireAuth: true }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); const spaceLink = { space: { alias: 'files', name: 'Space' }, share: { isDir: true, name: 'dir', alias: 'share' } }; linksQueriesMock.spaceLink.mockResolvedValueOnce(spaceLink); const result = await service.linkAccess(sameUserIdentity, link.uuid, req, res); expect(result.user.id).toEqual(baseLink.user.id); expect(authManagerMock.setCookies).not.toHaveBeenCalled(); expect(linksQueriesMock.incrementLinkNbAccess).not.toHaveBeenCalled(); }); it('throws INTERNAL_SERVER_ERROR when file checks fail during streaming', async ()=>{ const link = { ...baseLink, limitAccess: 10 }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); const spaceLink = { space: null, share: { isDir: false, name: 'bad.txt', alias: 'share' } }; linksQueriesMock.spaceLink.mockReset(); linksQueriesMock.spaceLink.mockResolvedValueOnce(spaceLink); spacesManagerMock.spaceEnv.mockResolvedValueOnce({}); filesManagerMock.sendFileFromSpace.mockReturnValueOnce({ checks: jest.fn().mockRejectedValueOnce(new Error('disk error')), stream: jest.fn() }); await expect(service.linkDownload(identity, link.uuid, req, res)).rejects.toMatchObject({ status: 500 }); expect(linksQueriesMock.incrementLinkNbAccess).toHaveBeenCalledWith(link.uuid); }); }); describe('linkAuthentication', ()=>{ const req = { ip: '10.0.0.1' }; const res = {}; it('returns cookies when password is correct', async ()=>{ const link = { ...baseLink, requireAuth: true }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); usersManagerMock.compareUserPassword.mockResolvedValueOnce(true); // usersManagerMock.updateAccesses.mockResolvedValueOnce(undefined) // removed to ensure rejection is hit const loginDto = { token: 'abc' }; authManagerMock.setCookies.mockResolvedValueOnce(loginDto); // cover: usersManager.updateAccesses.catch(...) should log an error when updateAccesses rejects (success flow) const logErrorSpy = jest.spyOn(service.logger, 'error').mockImplementation(()=>undefined); usersManagerMock.updateAccesses.mockRejectedValueOnce(new Error('updateAccesses auth success boom')); const result = await service.linkAuthentication(identity, link.uuid, { password: 'secret' }, req, res); expect(result).toBe(loginDto); expect(usersManagerMock.compareUserPassword).toHaveBeenCalledWith(link.user.id, 'secret'); expect(usersManagerMock.updateAccesses).toHaveBeenCalledWith(expect.anything(), req.ip, true); expect(authManagerMock.setCookies).toHaveBeenCalled(); // should log error when updateAccesses fails in successful authentication expect(logErrorSpy).toHaveBeenCalled(); }); it('throws FORBIDDEN when password is incorrect', async ()=>{ const link = { ...baseLink, requireAuth: true }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(link); usersManagerMock.compareUserPassword.mockResolvedValueOnce(false); usersManagerMock.updateAccesses.mockResolvedValueOnce(undefined); await expect(service.linkAuthentication(identity, link.uuid, { password: 'bad' }, req, res)).rejects.toMatchObject({ status: 403 }); }); it('throws BAD_REQUEST when link is invalid (e.g., expired)', async ()=>{ const expired = { ...baseLink, expiresAt: new Date(Date.now() - 60_000) }; linksQueriesMock.linkFromUUID.mockResolvedValueOnce(expired); await expect(service.linkAuthentication(identity, expired.uuid, { password: 'whatever' }, req, res)).rejects.toMatchObject({ status: 400 }); }); }); }); //# sourceMappingURL=links-manager.service.spec.js.map