@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
452 lines (451 loc) • 19.3 kB
JavaScript
/*
* 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