@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
1,139 lines (1,138 loc) • 109 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 _fileerror = require("../../files/models/file-error");
const _filelockerror = require("../../files/models/file-lock-error");
const _fileslockmanagerservice = require("../../files/services/files-lock-manager.service");
const _filesmanagerservice = require("../../files/services/files-manager.service");
const _files = require("../../files/utils/files");
const _spaces = require("../../spaces/constants/spaces");
const _paths = /*#__PURE__*/ _interop_require_wildcard(require("../../spaces/utils/paths"));
const _permissions = require("../../spaces/utils/permissions");
const _webdav = require("../constants/webdav");
const _ifheader = /*#__PURE__*/ _interop_require_wildcard(require("../utils/if-header"));
const _webdavmethodsservice = require("./webdav-methods.service");
const _webdavspacesservice = require("./webdav-spaces.service");
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function(nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
return {
default: obj
};
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {
__proto__: null
};
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
for(var key in obj){
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
// Mock external dependencies
jest.mock('../../files/utils/files', ()=>({
isPathExists: jest.fn().mockReturnValue(false),
isPathIsDir: jest.fn(),
fileName: jest.fn().mockReturnValue('fileName'),
dirName: jest.fn(),
genEtag: jest.fn().mockReturnValue('W/"etag-123"')
}));
jest.mock('../../spaces/utils/permissions', ()=>({
haveSpaceEnvPermissions: jest.fn()
}));
jest.mock('../../spaces/utils/paths', ()=>{
const actual = jest.requireActual('../../spaces/utils/paths');
return {
...actual,
dbFileFromSpace: jest.fn()
};
});
jest.mock('../decorators/if-header.decorator', ()=>({
IfHeaderDecorator: ()=>(_target, _key, _desc)=>undefined
}));
describe('WebDAVMethods', ()=>{
let service;
let filesManager;
let filesLockManager;
let webDAVSpaces;
// Helper to create a mocked response object
const createMockResponse = ()=>{
const res = {
statusCode: undefined,
body: undefined,
headers: {},
contentType: undefined,
status (code) {
this.statusCode = code;
return this;
},
send (payload) {
this.body = payload;
return this;
},
header (name, value) {
this.headers[name.toLowerCase()] = value;
return this;
},
type (ct) {
this.contentType = ct;
return this;
}
};
return res;
};
// Helper to create a base request object
const createBaseRequest = (overrides = {})=>({
method: 'GET',
user: {
id: 1,
login: 'test-user',
fullName: 'Test User',
email: 'test-user@sync-in.com'
},
dav: {
url: '/webdav/test/file.txt',
depth: '0',
httpVersion: 'HTTP/1.1',
body: '<lockinfo/>',
lock: {
timeout: 60,
lockscope: 'exclusive',
owner: 'test-user',
token: 'opaquelocktoken:abc123'
},
ifHeaders: []
},
space: {
id: 1,
alias: 'test-space',
url: '/webdav/test/file.txt',
realPath: '/real/path/to/file.txt',
inSharesList: false,
dbFile: {
path: 'file.txt',
spaceId: 1,
inTrash: false
}
},
...overrides
});
beforeEach(async ()=>{
// Initialize mocks
filesManager = {
sendFileFromSpace: jest.fn(),
mkFile: jest.fn(),
saveStream: jest.fn(),
delete: jest.fn(),
touch: jest.fn(),
mkDir: jest.fn(),
copyMove: jest.fn()
};
filesLockManager = {
create: jest.fn(),
isLockedWithToken: jest.fn(),
removeLock: jest.fn(),
browseLocks: jest.fn(),
browseParentChildLocks: jest.fn(),
checkConflicts: jest.fn(),
getLocksByPath: jest.fn(),
getLockByToken: jest.fn(),
refreshLockTimeout: jest.fn(),
genDAVToken: jest.fn().mockReturnValue('opaquelocktoken:new-token')
};
webDAVSpaces = {
propfind: jest.fn(),
spaceEnv: jest.fn()
};
const module = await _testing.Test.createTestingModule({
providers: [
_webdavmethodsservice.WebDAVMethods,
{
provide: _webdavspacesservice.WebDAVSpaces,
useValue: webDAVSpaces
},
{
provide: _filesmanagerservice.FilesManager,
useValue: filesManager
},
{
provide: _fileslockmanagerservice.FilesLockManager,
useValue: filesLockManager
}
]
}).compile();
module.useLogger([
'fatal'
]);
service = module.get(_webdavmethodsservice.WebDAVMethods);
// Reset global mocks
jest.clearAllMocks();
_files.isPathExists.mockResolvedValue(true);
_files.dirName.mockReturnValue('/real/path/to');
_permissions.haveSpaceEnvPermissions.mockReturnValue(true);
});
afterEach(()=>{
jest.restoreAllMocks();
});
describe('Service initialization', ()=>{
it('should be defined', ()=>{
expect(service).toBeDefined();
expect(service).toBeInstanceOf(_webdavmethodsservice.WebDAVMethods);
});
});
describe('headOrGet', ()=>{
describe('Success cases', ()=>{
it('should stream file when repository is FILES and not in shares list', async ()=>{
const req = createBaseRequest();
const res = createMockResponse();
const streamable = {
stream: 'file-content'
};
const sendFile = {
checks: jest.fn().mockResolvedValue(undefined),
stream: jest.fn().mockResolvedValue(streamable)
};
filesManager.sendFileFromSpace.mockReturnValue(sendFile);
const result = await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(filesManager.sendFileFromSpace).toHaveBeenCalledWith(req.space);
expect(sendFile.checks).toHaveBeenCalledTimes(1);
expect(sendFile.stream).toHaveBeenCalledWith(req, res);
expect(result).toBe(streamable);
});
});
describe('Error cases', ()=>{
it('should return 403 when repository is not FILES', async ()=>{
const req = createBaseRequest();
const res = createMockResponse();
await service.headOrGet(req, res, 'OTHER_REPO');
expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
expect(res.body).toBe('Not allowed on this resource');
});
it('should return 403 when resource is in shares list', async ()=>{
const req = createBaseRequest({
space: {
...createBaseRequest().space,
inSharesList: true
}
});
const res = createMockResponse();
await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
expect(res.body).toBe('Not allowed on this resource');
});
it('should handle errors from sendFile.checks', async ()=>{
const req = createBaseRequest();
const res = createMockResponse();
const error = new Error('File check failed');
const sendFile = {
checks: jest.fn().mockRejectedValue(error),
stream: jest.fn()
};
filesManager.sendFileFromSpace.mockReturnValue(sendFile);
jest.spyOn(service, 'handleError').mockReturnValue('error-handled');
const result = await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(result).toBe('error-handled');
});
it('should handle errors from sendFile.stream', async ()=>{
const req = createBaseRequest();
const res = createMockResponse();
const error = new Error('Stream failed');
const sendFile = {
checks: jest.fn().mockResolvedValue(undefined),
stream: jest.fn().mockRejectedValue(error)
};
filesManager.sendFileFromSpace.mockReturnValue(sendFile);
jest.spyOn(service, 'handleError').mockReturnValue('error-handled');
const result = await service.headOrGet(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(result).toBe('error-handled');
});
});
});
describe('lock', ()=>{
describe('Lock refresh (without body)', ()=>{
it('should return 400 if resource does not exist for lock refresh', async ()=>{
;
_files.isPathExists.mockResolvedValue(false);
const req = createBaseRequest({
dav: {
...createBaseRequest().dav,
body: undefined
}
});
const res = createMockResponse();
await service.lock(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
expect(res.body).toBe('Lock refresh must specify an existing resource');
});
it('should delegate to lockRefresh when resource exists and no body', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
dav: {
...createBaseRequest().dav,
body: undefined
}
});
const res = createMockResponse();
const lockRefreshSpy = jest.spyOn(service, 'lockRefresh').mockResolvedValue('refresh-ok');
const result = await service.lock(req, res);
expect(lockRefreshSpy).toHaveBeenCalledWith(req, res, req.space.dbFile.path);
expect(result).toBe('refresh-ok');
});
});
describe('Lock creation on existing resource', ()=>{
it('should create lock successfully and return 200', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest();
const res = createMockResponse();
filesLockManager.create.mockImplementation(async (_user, _dbFile, _app, _depth, options, _timeout)=>{
return [
true,
{
owner: {
fullName: 'LockOwner',
email: 'lock-owner@sync-in.com'
},
dbFilePath: _dbFile?.path,
options: {
lockRoot: options.lockRoot,
lockToken: options.lockToken,
lockScope: options.lockScope,
lockInfo: options.lockInfo
}
}
];
});
await service.lock(req, res);
expect(filesLockManager.create).toHaveBeenCalledTimes(1);
expect(res.statusCode).toBe(_common.HttpStatus.OK);
expect(res.contentType).toBe('application/xml; charset=utf-8');
expect(res.headers['lock-token']).toContain('opaquelocktoken:new-token');
expect(res.body).toBeDefined();
expect(typeof res.body).toBe('string');
});
});
describe('Lock creation on non-existent resource', ()=>{
it('should return 403 when user lacks ADD permission', async ()=>{
;
_files.isPathExists.mockResolvedValue(false);
_permissions.haveSpaceEnvPermissions.mockReturnValue(false);
const req = createBaseRequest();
const res = createMockResponse();
await service.lock(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
expect(res.body).toBe('You are not allowed to do this action');
expect(filesLockManager.create).not.toHaveBeenCalled();
});
it('should return 409 when parent directory does not exist', async ()=>{
;
_files.isPathExists.mockResolvedValueOnce(false) // resource
.mockResolvedValueOnce(false) // parent
;
_permissions.haveSpaceEnvPermissions.mockReturnValue(true);
_files.dirName.mockReturnValue('/real/path/missing');
const req = createBaseRequest();
const res = createMockResponse();
await service.lock(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.CONFLICT);
expect(res.body).toBe('Parent must exists');
expect(filesLockManager.create).not.toHaveBeenCalled();
});
it('should create empty file and lock, return 201', async ()=>{
;
_files.isPathExists.mockResolvedValueOnce(false) // resource
.mockResolvedValueOnce(true); // parent exists
const req = createBaseRequest();
const res = createMockResponse();
filesLockManager.create.mockImplementation(async (_user, _dbFile, _app, _depth, options)=>{
return [
true,
{
owner: {
fullName: 'LockOwner',
email: 'lock-owner@sync-in.com'
},
dbFilePath: _dbFile?.path,
options: {
lockRoot: options.lockRoot,
lockToken: options.lockToken,
lockScope: options.lockScope,
lockInfo: options.lockInfo
}
}
];
});
await service.lock(req, res);
expect(filesManager.mkFile).toHaveBeenCalledWith(req.user, req.space, false, false, false);
expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
expect(res.headers['lock-token']).toContain('opaquelocktoken:new-token');
});
});
describe('Lock conflict', ()=>{
it('should return 423 when lock conflict occurs', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest();
const res = createMockResponse();
filesLockManager.create.mockResolvedValue([
false,
{
owner: {
fullName: 'LockOwner',
email: 'lock-owner@sync-in.com'
},
dbFilePath: 'file.txt',
options: {
lockRoot: '/webdav/locked/resource'
}
}
]);
await service.lock(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
expect(res.contentType).toBe('application/xml; charset=utf-8');
});
});
});
describe('unlock', ()=>{
describe('Success cases', ()=>{
it('should unlock resource and return 204', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.isLockedWithToken.mockResolvedValue({
owner: {
id: 1,
login: 'test-user'
},
key: 'lock-key-123'
});
const req = createBaseRequest();
const res = createMockResponse();
await service.unlock(req, res);
expect(filesLockManager.removeLock).toHaveBeenCalledWith('lock-key-123');
expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
});
});
describe('Error cases', ()=>{
it('should return 404 when resource does not exist', async ()=>{
;
_files.isPathExists.mockResolvedValue(false);
const req = createBaseRequest();
const res = createMockResponse();
await service.unlock(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
expect(res.body).toBe(req.dav.url);
});
it('should return 409 when lock token does not exist', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.isLockedWithToken.mockResolvedValue(null);
const req = createBaseRequest();
const res = createMockResponse();
await service.unlock(req, res);
expect(filesLockManager.isLockedWithToken).toHaveBeenCalledWith(req.dav.lock.token, req.space.dbFile.path);
expect(res.statusCode).toBe(_common.HttpStatus.CONFLICT);
});
it('should return 403 when lock owner is different user', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.isLockedWithToken.mockResolvedValue({
owner: {
id: 999,
login: 'other-user'
},
key: 'lock-key-456'
});
const req = createBaseRequest();
const res = createMockResponse();
await service.unlock(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.FORBIDDEN);
expect(res.body).toBe('Token was created by another user');
expect(filesLockManager.removeLock).not.toHaveBeenCalled();
});
});
});
describe('propfind', ()=>{
describe('Base cases', ()=>{
it('should return 404 when resource does not exist in FILES repository', async ()=>{
;
_files.isPathExists.mockResolvedValue(false);
const req = createBaseRequest({
dav: {
...createBaseRequest().dav,
propfindMode: 'prop'
}
});
const res = createMockResponse();
const result = await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(result).toBe(res);
expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
expect(res.body).toBe(req.dav.url);
});
it('should return multistatus with property names in PROPNAME mode', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
dav: {
...createBaseRequest().dav,
propfindMode: 'propname'
}
});
const res = createMockResponse();
webDAVSpaces.propfind.mockImplementation(async function*() {
yield {
href: '/webdav/test/file.txt',
name: 'file.txt'
};
});
await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
expect(res.contentType).toContain('application/xml');
expect(typeof res.body).toBe('string');
expect(res.body).toContain('/webdav/test/file.txt');
});
it('should return multistatus with property values in PROP mode', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
dav: {
...createBaseRequest().dav,
propfindMode: 'prop',
body: {
propfind: {
prop: {
[_webdav.STANDARD_PROPS[0]]: ''
}
}
}
}
});
const res = createMockResponse();
webDAVSpaces.propfind.mockImplementation(async function*() {
yield {
href: '/webdav/test/file.txt',
name: 'file.txt',
getlastmodified: 'Mon, 01 Jan 2024 00:00:00 GMT'
};
});
await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
expect(res.contentType).toContain('application/xml');
expect(typeof res.body).toBe('string');
});
});
describe('Lock discovery', ()=>{
it('should collect locks with depth 0', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
dav: {
...createBaseRequest().dav,
propfindMode: 'prop',
depth: _webdav.DEPTH.RESOURCE,
body: {
propfind: {
prop: {
[_webdav.LOCK_DISCOVERY_PROP]: ''
}
}
}
}
});
const res = createMockResponse();
webDAVSpaces.propfind.mockImplementation(async function*() {
yield {
href: '/webdav/test/file.txt',
name: 'file.txt'
};
});
filesLockManager.browseLocks.mockResolvedValue({
'file.txt': {
owner: {
fullName: 'LockOwner',
email: 'lock-owner@sync-in.com'
},
options: {
lockRoot: '/webdav/test/file.txt'
}
}
});
await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(filesLockManager.browseLocks).toHaveBeenCalledWith(req.space.dbFile);
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
});
it('should collect parent and child locks with depth infinity', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
dav: {
...createBaseRequest().dav,
propfindMode: 'prop',
depth: 'infinity',
body: {
propfind: {
prop: {
[_webdav.LOCK_DISCOVERY_PROP]: ''
}
}
}
}
});
const res = createMockResponse();
webDAVSpaces.propfind.mockImplementation(async function*() {
yield {
href: '/webdav/test/file.txt',
name: 'file.txt'
};
});
filesLockManager.browseParentChildLocks.mockResolvedValue({
'file.txt': {
owner: {
fullName: 'LockOwner',
email: 'lock-owner@sync-in.com'
},
options: {
lockRoot: '/webdav/test/file.txt'
}
}
});
await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(filesLockManager.browseParentChildLocks).toHaveBeenCalledWith(req.space.dbFile);
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
});
it('should not collect locks for PROPNAME mode', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
dav: {
...createBaseRequest().dav,
propfindMode: _webdav.PROPSTAT.PROPNAME
}
});
const res = createMockResponse();
webDAVSpaces.propfind.mockImplementation(async function*() {
yield {
href: '/webdav/test',
name: 'test'
};
});
await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(filesLockManager.browseLocks).not.toHaveBeenCalled();
expect(filesLockManager.browseParentChildLocks).not.toHaveBeenCalled();
});
it('should not collect locks for shares list', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
space: {
...createBaseRequest().space,
inSharesList: true
},
dav: {
...createBaseRequest().dav,
propfindMode: 'prop',
body: {
propfind: {
prop: {
[_webdav.LOCK_DISCOVERY_PROP]: ''
}
}
}
}
});
const res = createMockResponse();
webDAVSpaces.propfind.mockImplementation(async function*() {
yield {
href: '/webdav/shares',
name: 'shares'
};
});
await service.propfind(req, res, _spaces.SPACE_REPOSITORY.FILES);
expect(filesLockManager.browseLocks).not.toHaveBeenCalled();
});
});
});
describe('put', ()=>{
describe('Success cases', ()=>{
it('should return 204 when updating existing file', async ()=>{
filesManager.saveStream.mockResolvedValue(true); // file existed
const req = createBaseRequest({
method: 'PUT'
});
const res = createMockResponse();
const result = await service.put(req, res);
expect(filesManager.saveStream).toHaveBeenCalledWith(req.user, req.space, req, expect.objectContaining({
dav: expect.any(Object)
}));
expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
expect(res.headers['etag']).toBeDefined();
expect(result).toBe(res);
});
it('should return 201 when creating new file', async ()=>{
filesManager.saveStream.mockResolvedValue(false); // file didn't exist
const req = createBaseRequest({
method: 'PUT'
});
const res = createMockResponse();
const result = await service.put(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.CREATED);
expect(res.headers['etag']).toBeDefined();
expect(result).toBe(res);
});
it('should extract and pass lock tokens from if-headers', async ()=>{
filesManager.saveStream.mockResolvedValue(true);
const req = createBaseRequest({
method: 'PUT',
dav: {
...createBaseRequest().dav,
ifHeaders: [
{
token: {
value: 'opaquelocktoken:xyz',
mustMatch: true
}
}
]
}
});
const res = createMockResponse();
jest.spyOn(_ifheader, 'extractAllTokens').mockReturnValue([
'opaquelocktoken:xyz'
]);
await service.put(req, res);
expect(filesManager.saveStream).toHaveBeenCalledWith(req.user, req.space, req, expect.objectContaining({
dav: expect.objectContaining({
lockTokens: [
'opaquelocktoken:xyz'
]
})
}));
});
});
describe('Error handling', ()=>{
it('should handle lock conflict error', async ()=>{
const lockError = new _filelockerror.LockConflict({
dbFilePath: 'file.txt',
options: {
lockRoot: '/webdav/locked'
}
}, 'Lock conflict');
filesManager.saveStream.mockRejectedValue(lockError);
const req = createBaseRequest({
method: 'PUT'
});
const res = createMockResponse();
await service.put(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
});
it('should handle file error', async ()=>{
const fileError = new _fileerror.FileError(409, 'File conflict');
filesManager.saveStream.mockRejectedValue(fileError);
const req = createBaseRequest({
method: 'PUT'
});
const res = createMockResponse();
await service.put(req, res);
expect(res.statusCode).toBe(409);
expect(res.body).toBe('File conflict');
});
it('should throw HttpException for unexpected errors', async ()=>{
const unexpectedError = new Error('Unexpected error');
filesManager.saveStream.mockRejectedValue(unexpectedError);
const req = createBaseRequest({
method: 'PUT'
});
const res = createMockResponse();
await expect(service.put(req, res)).rejects.toThrow(_common.HttpException);
});
});
});
describe('delete', ()=>{
describe('Success cases', ()=>{
it('should delete resource and return 204', async ()=>{
filesManager.delete.mockResolvedValue(undefined);
const req = createBaseRequest({
method: 'DELETE'
});
const res = createMockResponse();
const result = await service.delete(req, res);
expect(filesManager.delete).toHaveBeenCalledWith(req.user, req.space, expect.objectContaining({
lockTokens: expect.any(Array)
}));
expect(res.statusCode).toBe(_common.HttpStatus.NO_CONTENT);
expect(result).toBe(res);
});
it('should extract lock tokens from if-headers', async ()=>{
filesManager.delete.mockResolvedValue(undefined);
const req = createBaseRequest({
method: 'DELETE',
dav: {
...createBaseRequest().dav,
ifHeaders: [
{
token: {
value: 'opaquelocktoken:abc',
mustMatch: true
}
}
]
}
});
const res = createMockResponse();
jest.spyOn(_ifheader, 'extractAllTokens').mockReturnValue([
'opaquelocktoken:abc'
]);
await service.delete(req, res);
expect(filesManager.delete).toHaveBeenCalledWith(req.user, req.space, expect.objectContaining({
lockTokens: [
'opaquelocktoken:abc'
]
}));
});
});
describe('Error handling', ()=>{
it('should handle lock conflict', async ()=>{
const lockError = new _filelockerror.LockConflict({
dbFilePath: 'file.txt',
options: {
lockRoot: '/webdav/locked'
}
}, 'Lock conflict');
filesManager.delete.mockRejectedValue(lockError);
const req = createBaseRequest({
method: 'DELETE'
});
const res = createMockResponse();
await service.delete(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.LOCKED);
});
it('should handle file errors', async ()=>{
const fileError = new _fileerror.FileError(404, 'File not found');
filesManager.delete.mockRejectedValue(fileError);
const req = createBaseRequest({
method: 'DELETE'
});
const res = createMockResponse();
await service.delete(req, res);
expect(res.statusCode).toBe(404);
expect(res.body).toBe('File not found');
});
it('should throw HttpException for unexpected errors', async ()=>{
const unexpectedError = new Error('Database error');
filesManager.delete.mockRejectedValue(unexpectedError);
const req = createBaseRequest({
method: 'DELETE'
});
const res = createMockResponse();
await expect(service.delete(req, res)).rejects.toThrow(_common.HttpException);
});
});
});
describe('proppatch', ()=>{
describe('Base cases', ()=>{
it('should return 404 when resource does not exist', async ()=>{
;
_files.isPathExists.mockResolvedValue(false);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
body: {
propertyupdate: {
set: {
prop: [
{
lastmodified: '2024-01-01'
}
]
}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.NOT_FOUND);
expect(res.body).toBe(req.dav.url);
});
it('should return 400 for unknown action tag', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
body: {
propertyupdate: {
invalidaction: {}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
expect(res.body).toContain('Unknown tag');
});
it('should return 400 when missing prop tag', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
body: {
propertyupdate: {
set: {
notprop: {}
}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.BAD_REQUEST);
expect(res.body).toContain('Unknown tag');
});
});
describe('SET action', ()=>{
it('should successfully modify lastmodified property', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.checkConflicts.mockResolvedValue(undefined);
filesManager.touch.mockResolvedValue(undefined);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
httpVersion: 'HTTP/1.1',
body: {
propertyupdate: {
set: {
prop: [
{
lastmodified: '2024-01-01'
}
]
}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(filesManager.touch).toHaveBeenCalled();
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
expect(res.contentType).toContain('application/xml');
expect(typeof res.body).toBe('string');
expect(res.body).toContain('lastmodified');
expect(res.body).toContain('200');
});
it('should return 207 with 403 for unsupported properties', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.checkConflicts.mockResolvedValue(undefined);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
httpVersion: 'HTTP/1.1',
body: {
propertyupdate: {
set: {
prop: [
{
unsupportedProp: 'value'
}
]
}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
expect(res.body).toContain('unsupportedProp');
expect(res.body).toContain('403');
});
it('should handle Win32 properties correctly', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.checkConflicts.mockResolvedValue(undefined);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
httpVersion: 'HTTP/1.1',
body: {
propertyupdate: {
set: {
prop: [
{
Win32CreationTime: '2024-01-01'
}
]
}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(filesManager.touch).not.toHaveBeenCalled();
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
expect(res.body).toContain('Win32CreationTime');
expect(res.body).toContain('200');
});
it('should return 424 failed dependency when touch fails', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.checkConflicts.mockResolvedValue(undefined);
filesManager.touch.mockRejectedValue(new Error('Touch failed'));
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
httpVersion: 'HTTP/1.1',
body: {
propertyupdate: {
set: {
prop: [
{
lastmodified: '2024-01-01'
}
]
}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
expect(res.body).toContain('424');
});
it('should mark supported props as 424 when unsupported prop fails', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.checkConflicts.mockResolvedValue(undefined);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
httpVersion: 'HTTP/1.1',
body: {
propertyupdate: {
set: {
prop: [
{
unsupportedProp: 'fail'
},
{
Win32CreationTime: 'ok'
}
]
}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
expect(res.body).toContain('unsupportedProp');
expect(res.body).toContain('403');
expect(res.body).toContain('Win32CreationTime');
expect(res.body).toContain('424');
});
});
describe('REMOVE action', ()=>{
it('should handle REMOVE action on supported property', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.checkConflicts.mockResolvedValue(undefined);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
httpVersion: 'HTTP/1.1',
body: {
propertyupdate: {
remove: {
prop: [
{
Win32CreationTime: ''
}
]
}
}
}
}
});
const res = createMockResponse();
await service.proppatch(req, res);
expect(filesManager.touch).not.toHaveBeenCalled();
expect(res.statusCode).toBe(_common.HttpStatus.MULTI_STATUS);
expect(res.body).toContain('Win32CreationTime');
expect(res.body).toContain('200');
});
});
describe('Data normalization', ()=>{
it('should normalize array of propertyupdate items', async ()=>{
;
_files.isPathExists.mockResolvedValue(true);
filesLockManager.checkConflicts.mockResolvedValue(undefined);
filesManager.touch.mockResolvedValue(undefined);
const req = createBaseRequest({
method: 'PROPPATCH',
dav: {
...createBaseRequest().dav,
httpVersion: 'HTTP/1.1',
body: {
propertyupdate: {
set: [
{
prop: {
lastmodified: '2024-01-01'
}
},
{
prop: {
Win32CreationTime: 'ok'
}
}
]