import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:nextcloud/nextcloud.dart'; import 'package:test/test.dart'; import 'helper.dart'; void main() { test('constructUri', () { var baseURL = Uri.parse('http://cloud.example.com'); expect(WebDavClient.constructUri(baseURL).toString(), '$baseURL$webdavBasePath'); expect(WebDavClient.constructUri(baseURL, Uri(path: '/')).toString(), '$baseURL$webdavBasePath'); expect(WebDavClient.constructUri(baseURL, Uri(path: 'test')).toString(), '$baseURL$webdavBasePath/test'); baseURL = Uri.parse('http://cloud.example.com/subdir'); expect(WebDavClient.constructUri(baseURL, Uri(path: 'test')).toString(), '$baseURL$webdavBasePath/test'); expect(() => WebDavClient.constructUri(baseURL, Uri(path: '/test')), throwsA(isA())); baseURL = Uri.parse('http://cloud.example.com/'); expect(() => WebDavClient.constructUri(baseURL), throwsA(isA())); }); group( 'webdav', () { late DockerImage image; setUpAll(() async => image = await getDockerImage()); late DockerContainer container; late TestNextcloudClient client; setUp(() async { container = await getDockerContainer(image); client = await getTestClient(container); }); tearDown(() => container.destroy()); test('List directory', () async { final responses = (await client.webdav.propfind( Uri(path: '/'), prop: WebDavPropWithoutValues.fromBools( nchaspreview: true, davgetcontenttype: true, davgetlastmodified: true, ocsize: true, ), )) .responses; expect(responses, hasLength(10)); final props = responses.singleWhere((final response) => response.href!.endsWith('/Nextcloud.png')).propstats.first.prop; expect(props.nchaspreview, isTrue); expect(props.davgetcontenttype, 'image/png'); expect(webdavDateFormat.parseUtc(props.davgetlastmodified!).isBefore(DateTime.now()), isTrue); expect(props.ocsize, 50598); }); test('List directory recursively', () async { final responses = (await client.webdav.propfind( Uri(path: '/'), depth: WebDavDepth.infinity, )) .responses; expect(responses, hasLength(48)); }); test('Get file props', () async { final response = (await client.webdav.propfind( Uri(path: 'Nextcloud.png'), prop: WebDavPropWithoutValues.fromBools( davgetlastmodified: true, davgetetag: true, davgetcontenttype: true, davgetcontentlength: true, davresourcetype: true, ocid: true, ocfileid: true, ocfavorite: true, occommentshref: true, occommentscount: true, occommentsunread: true, ocdownloadurl: true, ocownerid: true, ocownerdisplayname: true, ocsize: true, ocpermissions: true, ncnote: true, ncdatafingerprint: true, nchaspreview: true, ncmounttype: true, ncisencrypted: true, ncmetadataetag: true, ncuploadtime: true, nccreationtime: true, ncrichworkspace: true, ocssharepermissions: true, ocmsharepermissions: true, ), )) .toWebDavFiles() .single; expect(response.path, '/Nextcloud.png'); expect(response.id, isNotEmpty); expect(response.fileId, isNotEmpty); expect(response.isCollection, isFalse); expect(response.mimeType, 'image/png'); expect(response.etag, isNotEmpty); expect(response.size, 50598); expect(response.ownerId, 'user1'); expect(response.ownerDisplay, 'User One'); expect(response.lastModified!.isBefore(DateTime.now()), isTrue); expect(response.isDirectory, isFalse); expect(response.uploadedDate, DateTime.utc(1970)); expect(response.createdDate, DateTime.utc(1970)); expect(response.favorite, isFalse); expect(response.hasPreview, isTrue); expect(response.name, 'Nextcloud.png'); expect(response.isDirectory, isFalse); expect(webdavDateFormat.parseUtc(response.props.davgetlastmodified!).isBefore(DateTime.now()), isTrue); expect(response.props.davgetetag, isNotEmpty); expect(response.props.davgetcontenttype, 'image/png'); expect(response.props.davgetcontentlength, 50598); expect(response.props.davresourcetype!.collection, isNull); expect(response.props.ocid, isNotEmpty); expect(response.props.ocfileid, isNotEmpty); expect(response.props.ocfavorite, 0); expect(response.props.occommentshref, isNotEmpty); expect(response.props.occommentscount, 0); expect(response.props.occommentsunread, 0); expect(response.props.ocdownloadurl, isNull); expect(response.props.ocownerid, 'user1'); expect(response.props.ocownerdisplayname, 'User One'); expect(response.props.ocsize, 50598); expect(response.props.ocpermissions, 'RGDNVW'); expect(response.props.ncnote, isNull); expect(response.props.ncdatafingerprint, isNull); expect(response.props.nchaspreview, isTrue); expect(response.props.ncmounttype, isNull); expect(response.props.ncisencrypted, isNull); expect(response.props.ncmetadataetag, isNull); expect(response.props.ncuploadtime, 0); expect(response.props.nccreationtime, 0); expect(response.props.ncrichworkspace, isNull); expect(response.props.ocssharepermissions, 19); expect(json.decode(response.props.ocmsharepermissions!), ['share', 'read', 'write']); }); test('Get directory props', () async { final data = utf8.encode('test') as Uint8List; await client.webdav.mkcol(Uri(path: 'test')); await client.webdav.put(data, Uri(path: 'test/test.txt')); final response = (await client.webdav.propfind( Uri(path: 'test'), prop: WebDavPropWithoutValues.fromBools( davgetcontenttype: true, davgetlastmodified: true, davresourcetype: true, ocsize: true, ), depth: WebDavDepth.zero, )) .toWebDavFiles() .single; expect(response.path, '/test/'); expect(response.isCollection, isTrue); expect(response.mimeType, isNull); expect(response.size, data.lengthInBytes); expectDateInReasonableTimeRange(response.lastModified!, DateTime.now()); expect(response.name, 'test'); expect(response.isDirectory, isTrue); expect(response.props.davgetcontenttype, isNull); expectDateInReasonableTimeRange(webdavDateFormat.parseUtc(response.props.davgetlastmodified!), DateTime.now()); expect(response.props.davresourcetype!.collection, isNotNull); expect(response.props.ocsize, data.lengthInBytes); }); test('Filter files', () async { final response = await client.webdav.put(utf8.encode('test') as Uint8List, Uri(path: 'test.txt')); final id = response.headers['oc-fileid']!.first; await client.webdav.proppatch( Uri(path: 'test.txt'), set: WebDavProp( ocfavorite: 1, ), ); final responses = (await client.webdav.report( Uri(path: '/'), WebDavOcFilterRules( ocfavorite: 1, ), prop: WebDavPropWithoutValues.fromBools( ocid: true, ocfavorite: true, ), )) .responses; expect(responses, hasLength(1)); final props = responses.singleWhere((final response) => response.href!.endsWith('/test.txt')).propstats.first.prop; expect(props.ocid, id); expect(props.ocfavorite, 1); }); test('Set properties', () async { final lastModifiedDate = DateTime.utc(1972, 3); final createdDate = DateTime.utc(1971, 2); final uploadTime = DateTime.now(); await client.webdav.put( utf8.encode('test') as Uint8List, Uri(path: 'test.txt'), lastModified: lastModifiedDate, created: createdDate, ); final updated = await client.webdav.proppatch( Uri(path: 'test.txt'), set: WebDavProp( ocfavorite: 1, ), ); expect(updated, isTrue); final props = (await client.webdav.propfind( Uri(path: 'test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, davgetlastmodified: true, nccreationtime: true, ncuploadtime: true, ), )) .responses .single .propstats .first .prop; expect(props.ocfavorite, 1); expect(webdavDateFormat.parseUtc(props.davgetlastmodified!), lastModifiedDate); expect(DateTime.fromMillisecondsSinceEpoch(props.nccreationtime! * 1000).isAtSameMomentAs(createdDate), isTrue); expectDateInReasonableTimeRange(DateTime.fromMillisecondsSinceEpoch(props.ncuploadtime! * 1000), uploadTime); }); test('Remove properties', () async { await client.webdav.put(utf8.encode('test') as Uint8List, Uri(path: 'test.txt')); var updated = await client.webdav.proppatch( Uri(path: 'test.txt'), set: WebDavProp( ocfavorite: 1, ), ); expect(updated, isTrue); var props = (await client.webdav.propfind( Uri(path: 'test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, nccreationtime: true, ncuploadtime: true, ), )) .responses .single .propstats .first .prop; expect(props.ocfavorite, 1); updated = await client.webdav.proppatch( Uri(path: 'test.txt'), remove: WebDavPropWithoutValues.fromBools( ocfavorite: true, ), ); expect(updated, isFalse); props = (await client.webdav.propfind( Uri(path: 'test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, ), )) .responses .single .propstats .first .prop; expect(props.ocfavorite, 0); }); test('Upload and download file', () async { final destinationDir = Directory.systemTemp.createTempSync(); final destination = File('${destinationDir.path}/test.png'); final source = File('test/files/test.png'); final progressValues = []; await client.webdav.putFile( source, source.statSync(), Uri(path: 'test.png'), onProgress: progressValues.add, ); await client.webdav.getFile( Uri(path: 'test.png'), destination, onProgress: progressValues.add, ); expect(progressValues, containsAll([1.0, 1.0])); expect(destination.readAsBytesSync(), source.readAsBytesSync()); destinationDir.deleteSync(recursive: true); }); group('litmus', () { group('basic', () { test('options', () async { final options = await client.webdav.options(); expect(options.capabilities, contains('1')); expect(options.capabilities, contains('3')); // Nextcloud only contains a fake plugin for Class 2 support: https://github.com/nextcloud/server/blob/master/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php // It does not actually support locking and is only there for compatibility reasons. expect(options.capabilities, isNot(contains('2'))); }); for (final (name, path) in [ ('put_get', 'res'), ('put_get_utf8_segment', 'res-%e2%82%ac'), ]) { test(name, () async { final content = utf8.encode('This is a test file') as Uint8List; final response = await client.webdav.put(content, Uri(path: path)); expect(response.statusCode, 201); final downloadedContent = await client.webdav.get(Uri(path: path)); expect(downloadedContent, equals(content)); }); } test('put_no_parent', () async { expect( () => client.webdav.put(Uint8List(0), Uri(path: '409me/noparent.txt')), // https://github.com/nextcloud/server/issues/39625 throwsA(predicate((final e) => e.statusCode == 409)), ); }); test('delete', () async { await client.webdav.put(Uint8List(0), Uri(path: 'test.txt')); final response = await client.webdav.delete(Uri(path: 'test.txt')); expect(response.statusCode, 204); }); test('delete_null', () async { expect( () => client.webdav.delete(Uri(path: 'test.txt')), throwsA(predicate((final e) => e.statusCode == 404)), ); }); // delete_fragment: This test is not applicable because the fragment is already removed on the client side test('mkcol', () async { final response = await client.webdav.mkcol(Uri(path: 'test')); expect(response.statusCode, 201); }); test('mkcol_again', () async { await client.webdav.mkcol(Uri(path: 'test')); expect( () => client.webdav.mkcol(Uri(path: 'test')), throwsA(predicate((final e) => e.statusCode == 405)), ); }); test('delete_coll', () async { var response = await client.webdav.mkcol(Uri(path: 'test')); response = await client.webdav.delete(Uri(path: 'test')); expect(response.statusCode, 204); }); test('mkcol_no_parent', () async { expect( () => client.webdav.mkcol(Uri(path: '409me/noparent')), throwsA(predicate((final e) => e.statusCode == 409)), ); }); // mkcol_with_body: This test is not applicable because we only write valid request bodies }); group('copymove', () { test('copy_simple', () async { await client.webdav.mkcol(Uri(path: 'src')); final response = await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst')); expect(response.statusCode, 201); }); test('copy_overwrite', () async { await client.webdav.mkcol(Uri(path: 'src')); await client.webdav.mkcol(Uri(path: 'dst')); expect( () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst')), throwsA(predicate((final e) => e.statusCode == 412)), ); final response = await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst'), overwrite: true); expect(response.statusCode, 204); }); test('copy_nodestcoll', () async { await client.webdav.mkcol(Uri(path: 'src')); expect( () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'nonesuch/dst')), throwsA(predicate((final e) => e.statusCode == 409)), ); }); test('copy_coll', () async { await client.webdav.mkcol(Uri(path: 'src')); await client.webdav.mkcol(Uri(path: 'src/sub')); for (var i = 0; i < 10; i++) { await client.webdav.put(Uint8List(0), Uri(path: 'src/$i.txt')); } await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst1')); await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst2')); expect( () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst1')), throwsA(predicate((final e) => e.statusCode == 412)), ); var response = await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst2'), overwrite: true); expect(response.statusCode, 204); for (var i = 0; i < 10; i++) { response = await client.webdav.delete(Uri(path: 'dst1/$i.txt')); expect(response.statusCode, 204); } response = await client.webdav.delete(Uri(path: 'dst1/sub')); expect(response.statusCode, 204); response = await client.webdav.delete(Uri(path: 'dst2')); expect(response.statusCode, 204); }); // copy_shallow: Does not work on litmus, let's wait for https://github.com/nextcloud/server/issues/39627 test('move', () async { await client.webdav.put(Uint8List(0), Uri(path: 'src1.txt')); await client.webdav.put(Uint8List(0), Uri(path: 'src2.txt')); await client.webdav.mkcol(Uri(path: 'coll')); var response = await client.webdav.move(Uri(path: 'src1.txt'), Uri(path: 'dst.txt')); expect(response.statusCode, 201); expect( () => client.webdav.move(Uri(path: 'src2.txt'), Uri(path: 'dst.txt')), throwsA(predicate((final e) => e.statusCode == 412)), ); response = await client.webdav.move(Uri(path: 'src2.txt'), Uri(path: 'dst.txt'), overwrite: true); expect(response.statusCode, 204); }); test('move_coll', () async { await client.webdav.mkcol(Uri(path: 'src')); await client.webdav.mkcol(Uri(path: 'src/sub')); for (var i = 0; i < 10; i++) { await client.webdav.put(Uint8List(0), Uri(path: 'src/$i.txt')); } await client.webdav.put(Uint8List(0), Uri(path: 'noncoll')); await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst2')); await client.webdav.move(Uri(path: 'src'), Uri(path: 'dst1')); expect( () => client.webdav.move(Uri(path: 'dst1'), Uri(path: 'dst2')), throwsA(predicate((final e) => e.statusCode == 412)), ); await client.webdav.move(Uri(path: 'dst2'), Uri(path: 'dst1'), overwrite: true); await client.webdav.copy(Uri(path: 'dst1'), Uri(path: 'dst2')); for (var i = 0; i < 10; i++) { final response = await client.webdav.delete(Uri(path: 'dst1/$i.txt')); expect(response.statusCode, 204); } final response = await client.webdav.delete(Uri(path: 'dst1/sub')); expect(response.statusCode, 204); expect( () => client.webdav.move(Uri(path: 'dst2'), Uri(path: 'noncoll')), throwsA(predicate((final e) => e.statusCode == 412)), ); }); }); group('largefile', () { final largefileSize = pow(10, 9).toInt(); // 1GB // large_put: Already covered by large_get test('large_get', () async { final response = await client.webdav.put(Uint8List(largefileSize), Uri(path: 'test.txt')); expect(response.statusCode, 201); final downloadedContent = await client.webdav.get(Uri(path: 'test.txt')); expect(downloadedContent, hasLength(largefileSize)); }); }); // props: Most of them are either not applicable or hard/impossible to implement because we don't allow just writing any props }); }, retry: retryCount, timeout: timeout, ); }