@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
424 lines (423 loc) • 15.2 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 _common = require("@nestjs/common");
const _testing = require("@nestjs/testing");
const _files = require("../../files/utils/files");
const _spaces = require("../../spaces/constants/spaces");
const _spacesbrowserservice = require("../../spaces/services/spaces-browser.service");
const _spacesmanagerservice = require("../../spaces/services/spaces-manager.service");
const _permissions = require("../../spaces/utils/permissions");
const _routes = require("../constants/routes");
const _webdav = require("../constants/webdav");
const _webdavspacesservice = require("./webdav-spaces.service");
// mocks for file utils and permissions
jest.mock('../../files/utils/files', ()=>({
getProps: jest.fn(),
isPathExists: jest.fn(),
isPathIsDir: jest.fn()
}));
jest.mock('../../spaces/utils/permissions', ()=>({
canAccessToSpaceUrl: jest.fn()
}));
// mock for WEBDAV path-to-space segments
jest.mock('../utils/routes', ()=>({
WEBDAV_PATH_TO_SPACE_SEGMENTS: jest.fn(()=>[
'files',
'personal'
])
}));
// small helper to collect results from an AsyncGenerator
async function collectGenerator(gen) {
const all = [];
for await (const item of gen){
all.push(item);
}
return all;
}
describe(_webdavspacesservice.WebDAVSpaces.name, ()=>{
let service;
let spacesManager;
let spacesBrowser;
const user = {
id: 1,
login: 'john',
isAdmin: false
};
beforeAll(async ()=>{
const spacesManagerMock = {
spaceEnv: jest.fn(),
listSpaces: jest.fn(),
listTrashes: jest.fn()
};
const spacesBrowserMock = {
browse: jest.fn()
};
const module = await _testing.Test.createTestingModule({
providers: [
{
provide: _spacesbrowserservice.SpacesBrowser,
useValue: spacesBrowserMock
},
{
provide: _spacesmanagerservice.SpacesManager,
useValue: spacesManagerMock
},
_webdavspacesservice.WebDAVSpaces
]
}).compile();
module.useLogger([
'fatal'
]);
service = module.get(_webdavspacesservice.WebDAVSpaces);
spacesManager = module.get(_spacesmanagerservice.SpacesManager);
spacesBrowser = module.get(_spacesbrowserservice.SpacesBrowser);
});
beforeEach(()=>{
jest.clearAllMocks();
});
it('should be defined', ()=>{
expect(service).toBeDefined();
});
describe('spaceEnv', ()=>{
it('returns space when manager resolves', async ()=>{
const fakeSpace = {
alias: 'personal',
id: 0
};
spacesManager.spaceEnv.mockResolvedValue(fakeSpace);
const res = await service.spaceEnv(user, '/webdav/personal');
expect(res).toBe(fakeSpace);
expect(spacesManager.spaceEnv).toHaveBeenCalled();
});
it('returns null when manager throws', async ()=>{
;
spacesManager.spaceEnv.mockRejectedValue(new Error('boom'));
const res = await service.spaceEnv(user, '/webdav/personal');
expect(res).toBeNull();
});
it('returns null when space not found', async ()=>{
;
spacesManager.spaceEnv.mockResolvedValue(null);
const res = await service.spaceEnv(user, '/webdav/personal');
expect(res).toBeNull();
});
});
describe('propfind - server root', ()=>{
it('yields server root and webdav child when depth=1', async ()=>{
const req = {
user,
dav: {
url: '/',
depth: _webdav.DEPTH.MEMBERS
}
};
const items = await collectGenerator(service.propfind(req, _routes.WEBDAV_NS.SERVER));
expect(items.map((i)=>i.name)).toEqual([
_routes.WEBDAV_NS.SERVER,
_routes.WEBDAV_NS.WEBDAV
]);
});
it('yields only server root when depth=0', async ()=>{
const req = {
user,
dav: {
url: '/',
depth: _webdav.DEPTH.RESOURCE
}
};
const items = await collectGenerator(service.propfind(req, _routes.WEBDAV_NS.SERVER));
expect(items.map((i)=>i.name)).toEqual([
_routes.WEBDAV_NS.SERVER
]);
});
});
describe('propfind - webdav listing', ()=>{
it('filters repositories by user access', async ()=>{
;
_permissions.canAccessToSpaceUrl.mockImplementation((_u, repos)=>repos?.includes(_spaces.SPACE_REPOSITORY.FILES));
const req = {
user,
dav: {
url: '/webdav',
depth: _webdav.DEPTH.MEMBERS
}
};
const items = await collectGenerator(service.propfind(req, _routes.WEBDAV_NS.WEBDAV));
// should list only roots that include FILES in their repository (personal, spaces), not server/webdav
expect(items.map((i)=>i.name)).toEqual([
_routes.WEBDAV_NS.WEBDAV,
'personal',
'spaces'
]);
});
it('lists only itself when depth=0', async ()=>{
const req = {
user,
dav: {
url: '/webdav',
depth: _webdav.DEPTH.RESOURCE
}
};
const items = await collectGenerator(service.propfind(req, _routes.WEBDAV_NS.WEBDAV));
expect(items.map((i)=>i.name)).toEqual([
_routes.WEBDAV_NS.WEBDAV
]);
});
});
describe('propfind - spaces listing', ()=>{
it('yields spaces root then user spaces when depth=1', async ()=>{
const now = new Date();
spacesManager.listSpaces.mockResolvedValue([
{
id: 10,
name: 'Team A',
alias: 'team-a',
createdAt: now.toISOString(),
modifiedAt: now.toISOString()
},
{
id: 11,
name: 'Team B',
alias: 'team-b',
createdAt: now.toISOString(),
modifiedAt: now.toISOString()
}
]);
const req = {
user,
dav: {
url: '/webdav/spaces',
depth: _webdav.DEPTH.MEMBERS
}
};
const items = await collectGenerator(service.propfind(req, _routes.WEBDAV_NS.SPACES));
expect(items.map((i)=>i.name)).toEqual([
_routes.WEBDAV_NS.SPACES,
'Team A',
'Team B'
]);
});
it('yields only spaces root when depth=0', async ()=>{
;
spacesManager.listSpaces.mockResolvedValue([]);
const req = {
user,
dav: {
url: '/webdav/spaces',
depth: _webdav.DEPTH.RESOURCE
}
};
const items = await collectGenerator(service.propfind(req, _routes.WEBDAV_NS.SPACES));
expect(items.map((i)=>i.name)).toEqual([
_routes.WEBDAV_NS.SPACES
]);
});
});
describe('propfind - trashes listing', ()=>{
it('yields trash root then each trash bucket when depth=1', async ()=>{
;
spacesManager.listTrashes.mockResolvedValue([
{
id: 1,
alias: 'personal',
nb: 2,
mtime: 3,
ctime: 4,
name: 'Personal files'
},
{
id: 2,
alias: 'team-a',
nb: 5,
mtime: 6,
ctime: 7,
name: 'Team A'
}
]);
const req = {
user,
dav: {
url: '/webdav/trash',
depth: _webdav.DEPTH.MEMBERS
}
};
const items = await collectGenerator(service.propfind(req, _routes.WEBDAV_NS.TRASH));
expect(items.map((i)=>i.name)).toEqual([
_routes.WEBDAV_NS.TRASH,
'personal (2)',
'team-a (5)'
]);
});
it('yields only trash root when depth=0', async ()=>{
;
spacesManager.listTrashes.mockResolvedValue([]);
const req = {
user,
dav: {
url: '/webdav/trash',
depth: _webdav.DEPTH.RESOURCE
}
};
const items = await collectGenerator(service.propfind(req, _routes.WEBDAV_NS.TRASH));
expect(items.map((i)=>i.name)).toEqual([
_routes.WEBDAV_NS.TRASH
]);
});
});
describe('propfind - files listing (shares list)', ()=>{
it('yields shares root and children when inSharesList=true and depth=1', async ()=>{
;
spacesBrowser.browse.mockResolvedValue({
files: [
{
id: 1,
name: 'doc.txt',
isDir: false,
size: 12,
ctime: 1,
mtime: 2,
mime: 'text/plain'
}
]
});
const req = {
user,
dav: {
url: '/webdav/files/personal',
depth: _webdav.DEPTH.MEMBERS
},
space: {
inSharesList: true,
realPath: '/any/ignored'
}
};
const items = await collectGenerator(service.propfind(req, _spaces.SPACE_REPOSITORY.FILES));
expect(items.map((i)=>i.name)).toEqual([
'shares',
'doc.txt'
]);
});
});
describe('propfind - files listing (path cases)', ()=>{
it('throws 404 when path does not exist', async ()=>{
;
_files.isPathExists.mockResolvedValue(false);
const req = {
user,
dav: {
url: '/webdav/files/personal/current',
depth: _webdav.DEPTH.RESOURCE
},
space: {
inSharesList: false,
realPath: '/path/not/found'
}
};
await expect(collectGenerator(service.propfind(req, _spaces.SPACE_REPOSITORY.FILES))).rejects.toBeInstanceOf(_common.HttpException);
await expect(collectGenerator(service.propfind(req, _spaces.SPACE_REPOSITORY.FILES))).rejects.toMatchObject({
status: _common.HttpStatus.NOT_FOUND
});
});
it('yields current directory and children when path exists and is dir', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
_files.getProps.mockResolvedValue({
id: 100,
name: 'current',
isDir: true,
size: 0,
ctime: Date.now(),
mtime: Date.now(),
mime: undefined
});
_files.isPathIsDir.mockResolvedValue(true);
spacesBrowser.browse.mockResolvedValue({
files: [
{
id: 2,
name: 'child.txt',
isDir: false,
size: 1,
ctime: 1,
mtime: 2,
mime: 'text/plain'
}
]
});
const req = {
user,
dav: {
url: '/webdav/files/personal/current',
depth: _webdav.DEPTH.MEMBERS
},
space: {
inSharesList: false,
realPath: '/path/current'
}
};
const items = await collectGenerator(service.propfind(req, _spaces.SPACE_REPOSITORY.FILES));
expect(items.map((i)=>i.name)).toEqual([
'current',
'child.txt'
]);
});
it('does not list children when path exists but is not a directory', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
_files.getProps.mockResolvedValue({
id: 101,
name: 'current',
isDir: false,
size: 123,
ctime: Date.now(),
mtime: Date.now(),
mime: 'text/plain'
});
_files.isPathIsDir.mockResolvedValue(false);
spacesBrowser.browse.mockResolvedValue({
files: []
});
const req = {
user,
dav: {
url: '/webdav/files/personal/current',
depth: _webdav.DEPTH.MEMBERS
},
space: {
inSharesList: false,
realPath: '/path/current'
}
};
const items = await collectGenerator(service.propfind(req, _spaces.SPACE_REPOSITORY.FILES));
expect(items.map((i)=>i.name)).toEqual([
'current'
]);
expect(spacesBrowser.browse).not.toHaveBeenCalled();
});
});
describe('propfind - unknown space', ()=>{
it('throws not found for unknown space', ()=>{
const req = {
user,
dav: {
url: '/webdav/unknown',
depth: _webdav.DEPTH.RESOURCE
}
};
try {
service.propfind(req, 'unknown');
fail('Expected HttpException to be thrown');
} catch (err) {
expect(err).toBeInstanceOf(_common.HttpException);
expect(err.getStatus()).toBe(_common.HttpStatus.NOT_FOUND);
}
});
});
});
//# sourceMappingURL=webdav-spaces.service.spec.js.map