UNPKG

@sync-in/server

Version:

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

1,139 lines (1,138 loc) 109 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 _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' } } ]