diff --git a/commitlint.yaml b/commitlint.yaml index ee82dc00..f91b8ff0 100644 --- a/commitlint.yaml +++ b/commitlint.yaml @@ -25,4 +25,5 @@ rules: - neon_notifications - neon_lints - nextcloud + - nextcloud_test - sort_box diff --git a/cspell.json b/cspell.json index 7ccaa88e..1ee6fa6c 100644 --- a/cspell.json +++ b/cspell.json @@ -14,7 +14,7 @@ "packages/dynamite/dynamite_petstore_example/lib", "packages/file_icons/lib/src/data.dart", "packages/neon_lints/lib", - "tool/dev/static" + "packages/nextcloud_test/docker/static" ], "dictionaries": [ "bash", diff --git a/packages/nextcloud/pubspec.yaml b/packages/nextcloud/pubspec.yaml index e196e101..95ff44d4 100644 --- a/packages/nextcloud/pubspec.yaml +++ b/packages/nextcloud/pubspec.yaml @@ -37,6 +37,10 @@ dev_dependencies: git: url: https://github.com/nextcloud/neon path: packages/neon_lints + nextcloud_test: + git: + url: https://github.com/nextcloud/neon + path: packages/nextcloud_test path: ^1.8.3 process_run: ^0.13.3 test: ^1.24.9 diff --git a/packages/nextcloud/pubspec_overrides.yaml b/packages/nextcloud/pubspec_overrides.yaml index b3ed199c..ac5350aa 100644 --- a/packages/nextcloud/pubspec_overrides.yaml +++ b/packages/nextcloud/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: dynamite,dynamite_runtime,neon_lints +# melos_managed_dependency_overrides: dynamite,dynamite_runtime,neon_lints,nextcloud_test dependency_overrides: dynamite: path: ../dynamite/dynamite @@ -6,3 +6,5 @@ dependency_overrides: path: ../dynamite/dynamite_runtime neon_lints: path: ../neon_lints + nextcloud_test: + path: ../nextcloud_test diff --git a/packages/nextcloud/test/core_test.dart b/packages/nextcloud/test/core_test.dart index 86c17154..56ab9196 100644 --- a/packages/nextcloud/test/core_test.dart +++ b/packages/nextcloud/test/core_test.dart @@ -1,18 +1,17 @@ import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'core', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient(container); + container = await DockerContainer.create(); + client = await TestNextcloudClient.create(container); }); tearDown(() => container.destroy()); diff --git a/packages/nextcloud/test/dashboard_test.dart b/packages/nextcloud/test/dashboard_test.dart index 97ba4f1a..fec46ecc 100644 --- a/packages/nextcloud/test/dashboard_test.dart +++ b/packages/nextcloud/test/dashboard_test.dart @@ -1,17 +1,17 @@ import 'package:nextcloud/dashboard.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'dashboard', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient(container); + container = await DockerContainer.create(); + client = await TestNextcloudClient.create(container); }); tearDown(() => container.destroy()); diff --git a/packages/nextcloud/test/helper.dart b/packages/nextcloud/test/helper.dart deleted file mode 100644 index 20c93025..00000000 --- a/packages/nextcloud/test/helper.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; - -import 'package:nextcloud/core.dart' as core; -import 'package:nextcloud/nextcloud.dart'; -import 'package:process_run/cmd_run.dart'; -import 'package:test/test.dart'; -import 'package:universal_io/io.dart'; - -const retryCount = 3; -const timeout = Timeout(Duration(seconds: 30)); - -class DockerContainer { - DockerContainer({ - required this.id, - required this.port, - }); - - final String id; - - final int port; - - Future<void> runOccCommand(final List<String> args) async { - final result = await runExecutableArguments( - 'docker', - [ - 'exec', - id, - 'php', - '-f', - 'occ', - ...args, - ], - stdout: stdout, - stderr: stderr, - ); - if (result.exitCode != 0) { - throw Exception('Failed to run occ command'); - } - } - - void destroy() => unawaited( - runExecutableArguments( - 'docker', - [ - 'kill', - id, - ], - ), - ); - - Future<String> serverLogs() async => (await runExecutableArguments( - 'docker', - [ - 'logs', - id, - ], - stdoutEncoding: utf8, - )) - .stdout as String; - - Future<String> nextcloudLogs() async => (await runExecutableArguments( - 'docker', - [ - 'exec', - id, - 'cat', - 'data/nextcloud.log', - ], - stdoutEncoding: utf8, - )) - .stdout as String; - - Future<String> allLogs() async => '${await serverLogs()}\n\n${await nextcloudLogs()}'; -} - -class TestNextcloudClient extends NextcloudClient { - TestNextcloudClient( - super.baseURL, { - super.loginName, - super.password, - super.appPassword, - super.language, - super.appType, - super.userAgentOverride, - super.cookieJar, - }); -} - -Future<TestNextcloudClient> getTestClient( - final DockerContainer container, { - final String? username = 'user1', - final AppType appType = AppType.unknown, - final String? userAgentOverride, -}) async { - String? appPassword; - if (username != null) { - final inputStream = StreamController<List<int>>(); - final process = runExecutableArguments( - 'docker', - [ - 'exec', - '-i', - container.id, - 'php', - '-f', - 'occ', - 'user:add-app-password', - username, - ], - stdin: inputStream.stream, - ); - inputStream.add(utf8.encode(username)); - await inputStream.close(); - - final result = await process; - if (result.exitCode != 0) { - throw Exception('Failed to run generate app password command\n${result.stderr}\n${result.stdout}'); - } - appPassword = (result.stdout as String).split('\n')[1]; - } - - final client = TestNextcloudClient( - Uri( - scheme: 'http', - host: 'localhost', - port: container.port, - ), - loginName: username, - password: username, - appPassword: appPassword, - appType: appType, - userAgentOverride: userAgentOverride, - cookieJar: CookieJar(), - ); - - var i = 0; - while (true) { - try { - await client.core.getStatus(); - break; - } catch (error) { - if (error is HttpException || error is DynamiteApiException) { - i++; - await Future<void>.delayed(const Duration(milliseconds: 100)); - if (i >= 300) { - throw TimeoutException('Failed to get the status of the Server. $error'); - } - } else { - rethrow; - } - } - } - - return client; -} - -Future<DockerContainer> getDockerContainer() async { - const dockerImageName = 'ghcr.io/nextcloud/neon/dev'; - - var result = await runExecutableArguments( - 'docker', - [ - 'images', - '-q', - dockerImageName, - ], - ); - if (result.exitCode != 0) { - throw Exception('Querying docker image failed: ${result.stderr}'); - } - if (result.stdout.toString().isEmpty) { - throw Exception('Missing docker image $dockerImageName. Please build it using ./tool/build-dev-container.sh'); - } - - late int port; - while (true) { - port = randomPort(); - result = await runExecutableArguments( - 'docker', - [ - 'run', - '--rm', - '-d', - '--add-host', - 'host.docker.internal:host-gateway', - '-p', - '$port:80', - dockerImageName, - ], - ); - // 125 means the docker run command itself has failed which indicated the port is already used - if (result.exitCode != 125) { - break; - } - } - - if (result.exitCode != 0) { - throw Exception('Failed to run docker container: ${result.stderr}'); - } - - return DockerContainer( - id: result.stdout.toString().replaceAll('\n', ''), - port: port, - ); -} - -class TestNextcloudUser { - TestNextcloudUser( - this.username, - this.password, { - this.displayName, - }); - - final String username; - final String password; - final String? displayName; -} - -int randomPort() => 1024 + Random().nextInt(65535 - 1024); - -void expectDateInReasonableTimeRange(final DateTime actual, final DateTime expected) { - const duration = Duration(seconds: 10); - expect(actual.isAfter(expected.subtract(duration)), isTrue); - expect(actual.isBefore(expected.add(duration)), isTrue); -} diff --git a/packages/nextcloud/test/news_test.dart b/packages/nextcloud/test/news_test.dart index 6e73b2a4..0f469985 100644 --- a/packages/nextcloud/test/news_test.dart +++ b/packages/nextcloud/test/news_test.dart @@ -2,19 +2,18 @@ import 'dart:async'; import 'package:nextcloud/news.dart' as news; import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'news', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient(container); + container = await DockerContainer.create(); + client = await TestNextcloudClient.create(container); }); tearDown(() => container.destroy()); diff --git a/packages/nextcloud/test/notes_test.dart b/packages/nextcloud/test/notes_test.dart index 795bd521..7f4bc8bc 100644 --- a/packages/nextcloud/test/notes_test.dart +++ b/packages/nextcloud/test/notes_test.dart @@ -1,19 +1,18 @@ import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/notes.dart' as notes; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'notes', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient(container); + container = await DockerContainer.create(); + client = await TestNextcloudClient.create(container); }); tearDown(() => container.destroy()); diff --git a/packages/nextcloud/test/notifications_test.dart b/packages/nextcloud/test/notifications_test.dart index 0520bf84..2fbe8740 100644 --- a/packages/nextcloud/test/notifications_test.dart +++ b/packages/nextcloud/test/notifications_test.dart @@ -1,17 +1,17 @@ import 'dart:async'; +import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/notifications.dart'; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group('notifications', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient( + container = await DockerContainer.create(); + client = await TestNextcloudClient.create( container, username: 'admin', ); @@ -42,7 +42,10 @@ void main() { expect(response.ocs.data[0].notificationId, 2); expect(response.ocs.data[0].app, 'admin_notifications'); expect(response.ocs.data[0].user, 'admin'); - expectDateInReasonableTimeRange(DateTime.parse(response.ocs.data[0].datetime), startTime); + expect( + DateTime.parse(response.ocs.data[0].datetime).millisecondsSinceEpoch, + closeTo(startTime.millisecondsSinceEpoch, 10E3), + ); expect(response.ocs.data[0].objectType, 'admin_notifications'); expect(response.ocs.data[0].objectId, isNotNull); expect(response.ocs.data[0].subject, '123'); @@ -67,7 +70,10 @@ void main() { expect(response.body.ocs.data.notificationId, 2); expect(response.body.ocs.data.app, 'admin_notifications'); expect(response.body.ocs.data.user, 'admin'); - expectDateInReasonableTimeRange(DateTime.parse(response.body.ocs.data.datetime), startTime); + expect( + DateTime.parse(response.body.ocs.data.datetime).millisecondsSinceEpoch, + closeTo(startTime.millisecondsSinceEpoch, 10E3), + ); expect(response.body.ocs.data.objectType, 'admin_notifications'); expect(response.body.ocs.data.objectId, isNotNull); expect(response.body.ocs.data.subject, '123'); @@ -101,10 +107,10 @@ void main() { group('Push', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient( + container = await DockerContainer.create(); + client = await TestNextcloudClient.create( container, username: 'admin', ); diff --git a/packages/nextcloud/test/provisioning_api_test.dart b/packages/nextcloud/test/provisioning_api_test.dart index 6f175ced..41d60519 100644 --- a/packages/nextcloud/test/provisioning_api_test.dart +++ b/packages/nextcloud/test/provisioning_api_test.dart @@ -1,17 +1,17 @@ +import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/provisioning_api.dart'; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'provisioning_api', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient( + container = await DockerContainer.create(); + client = await TestNextcloudClient.create( container, username: 'admin', ); diff --git a/packages/nextcloud/test/settings_test.dart b/packages/nextcloud/test/settings_test.dart index 9ad42508..bb1fc9b0 100644 --- a/packages/nextcloud/test/settings_test.dart +++ b/packages/nextcloud/test/settings_test.dart @@ -1,19 +1,19 @@ import 'dart:convert'; +import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/settings.dart'; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'settings', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient( + container = await DockerContainer.create(); + client = await TestNextcloudClient.create( container, username: 'admin', ); diff --git a/packages/nextcloud/test/spreed_test.dart b/packages/nextcloud/test/spreed_test.dart index b1774301..341d688b 100644 --- a/packages/nextcloud/test/spreed_test.dart +++ b/packages/nextcloud/test/spreed_test.dart @@ -3,20 +3,20 @@ import 'dart:convert'; import 'package:built_value/json_object.dart'; import 'package:nextcloud/core.dart' as core; +import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/spreed.dart' as spreed; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'spreed', () { late DockerContainer container; - late TestNextcloudClient client1; + late NextcloudClient client1; setUp(() async { - container = await getDockerContainer(); - client1 = await getTestClient(container); + container = await DockerContainer.create(); + client1 = await TestNextcloudClient.create(container); }); tearDown(() => container.destroy()); @@ -185,10 +185,7 @@ void main() { expect(response.body.ocs.data!.actorType, spreed.ActorType.users.name); expect(response.body.ocs.data!.actorId, 'user1'); expect(response.body.ocs.data!.actorDisplayName, 'User One'); - expectDateInReasonableTimeRange( - DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data!.timestamp * 1000), - startTime, - ); + expect(response.body.ocs.data!.timestamp * 1000, closeTo(startTime.millisecondsSinceEpoch, 10E3)); expect(response.body.ocs.data!.message, 'bla'); expect(response.body.ocs.data!.messageType, spreed.MessageType.comment.name); }); @@ -229,10 +226,7 @@ void main() { expect(response.body.ocs.data[0].actorType, spreed.ActorType.users.name); expect(response.body.ocs.data[0].actorId, 'user1'); expect(response.body.ocs.data[0].actorDisplayName, 'User One'); - expectDateInReasonableTimeRange( - DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[0].timestamp * 1000), - startTime, - ); + expect(response.body.ocs.data[0].timestamp * 1000, closeTo(startTime.millisecondsSinceEpoch, 10E3)); expect(response.body.ocs.data[0].message, '123'); expect(response.body.ocs.data[0].messageType, spreed.MessageType.comment.name); @@ -240,10 +234,7 @@ void main() { expect(response.body.ocs.data[0].parent!.actorType, spreed.ActorType.users.name); expect(response.body.ocs.data[0].parent!.actorId, 'user1'); expect(response.body.ocs.data[0].parent!.actorDisplayName, 'User One'); - expectDateInReasonableTimeRange( - DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[0].parent!.timestamp * 1000), - startTime, - ); + expect(response.body.ocs.data[0].parent!.timestamp * 1000, closeTo(startTime.millisecondsSinceEpoch, 10E3)); expect(response.body.ocs.data[0].parent!.message, 'bla'); expect(response.body.ocs.data[0].parent!.messageType, spreed.MessageType.comment.name); @@ -251,10 +242,7 @@ void main() { expect(response.body.ocs.data[1].actorType, spreed.ActorType.users.name); expect(response.body.ocs.data[1].actorId, 'user1'); expect(response.body.ocs.data[1].actorDisplayName, 'User One'); - expectDateInReasonableTimeRange( - DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[1].timestamp * 1000), - startTime, - ); + expect(response.body.ocs.data[1].timestamp * 1000, closeTo(startTime.millisecondsSinceEpoch, 10E3)); expect(response.body.ocs.data[1].message, 'bla'); expect(response.body.ocs.data[1].messageType, spreed.MessageType.comment.name); @@ -262,10 +250,7 @@ void main() { expect(response.body.ocs.data[2].actorType, spreed.ActorType.users.name); expect(response.body.ocs.data[2].actorId, 'user1'); expect(response.body.ocs.data[2].actorDisplayName, 'User One'); - expectDateInReasonableTimeRange( - DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[2].timestamp * 1000), - startTime, - ); + expect(response.body.ocs.data[2].timestamp * 1000, closeTo(startTime.millisecondsSinceEpoch, 10E3)); expect(response.body.ocs.data[2].message, 'You created the conversation'); expect(response.body.ocs.data[2].systemMessage, 'conversation_created'); expect(response.body.ocs.data[2].messageType, spreed.MessageType.system.name); @@ -302,10 +287,7 @@ void main() { expect(response.body.ocs.data[0].actorType, spreed.ActorType.users.name); expect(response.body.ocs.data[0].actorId, 'user1'); expect(response.body.ocs.data[0].actorDisplayName, 'User One'); - expectDateInReasonableTimeRange( - DateTime.fromMillisecondsSinceEpoch(response.body.ocs.data[0].timestamp * 1000), - startTime, - ); + expect(response.body.ocs.data[0].timestamp * 1000, closeTo(startTime.millisecondsSinceEpoch, 10E3)); expect(response.body.ocs.data[0].message, '123'); expect(response.body.ocs.data[0].messageType, spreed.MessageType.comment.name); }); @@ -379,7 +361,7 @@ void main() { final room1 = (await client1.spreed.room.joinRoom(token: room.token)).body.ocs.data; await client1.spreed.call.joinCall(token: room.token); - final client2 = await getTestClient( + final client2 = await TestNextcloudClient.create( container, username: 'user2', ); diff --git a/packages/nextcloud/test/uppush_test.dart b/packages/nextcloud/test/uppush_test.dart index 2ecd3e7f..a611cd48 100644 --- a/packages/nextcloud/test/uppush_test.dart +++ b/packages/nextcloud/test/uppush_test.dart @@ -1,17 +1,17 @@ +import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/uppush.dart'; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'uppush', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient( + container = await DockerContainer.create(); + client = await TestNextcloudClient.create( container, username: 'admin', ); diff --git a/packages/nextcloud/test/user_status_test.dart b/packages/nextcloud/test/user_status_test.dart index b53905f9..21cce042 100644 --- a/packages/nextcloud/test/user_status_test.dart +++ b/packages/nextcloud/test/user_status_test.dart @@ -1,17 +1,17 @@ +import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/user_status.dart' as user_status; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -import 'helper.dart'; - void main() { group( 'user_status', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient(container); + container = await DockerContainer.create(); + client = await TestNextcloudClient.create(container); }); tearDown(() => container.destroy()); diff --git a/packages/nextcloud/test/webdav_test.dart b/packages/nextcloud/test/webdav_test.dart index 2155aca1..fbbf1f06 100644 --- a/packages/nextcloud/test/webdav_test.dart +++ b/packages/nextcloud/test/webdav_test.dart @@ -3,11 +3,10 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; import 'package:universal_io/io.dart'; -import 'helper.dart'; - void main() { group('constructUri', () { for (final values in [ @@ -139,11 +138,11 @@ void main() { 'webdav', () { late DockerContainer container; - late TestNextcloudClient client; + late NextcloudClient client; setUp(() async { - container = await getDockerContainer(); - client = await getTestClient(container); + container = await DockerContainer.create(); + client = await TestNextcloudClient.create(container); }); tearDown(() => container.destroy()); @@ -281,12 +280,18 @@ void main() { expect(response.isCollection, isTrue); expect(response.mimeType, isNull); expect(response.size, data.lengthInBytes); - expectDateInReasonableTimeRange(response.lastModified!, DateTime.now()); + expect( + response.lastModified!.millisecondsSinceEpoch, + closeTo(DateTime.now().millisecondsSinceEpoch, 10E3), + ); expect(response.name, 'test'); expect(response.isDirectory, isTrue); expect(response.props.davgetcontenttype, isNull); - expectDateInReasonableTimeRange(webdavDateFormat.parseUtc(response.props.davgetlastmodified!), DateTime.now()); + expect( + webdavDateFormat.parseUtc(response.props.davgetlastmodified!).millisecondsSinceEpoch, + closeTo(DateTime.now().millisecondsSinceEpoch, 10E3), + ); expect(response.props.davresourcetype!.collection, isNotNull); expect(response.props.ocsize, data.lengthInBytes); }); @@ -355,8 +360,8 @@ void main() { .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); + expect(props.nccreationtime! * 1000, createdDate.millisecondsSinceEpoch); + expect(props.ncuploadtime! * 1000, closeTo(uploadTime.millisecondsSinceEpoch, 10E3)); }); test('Remove properties', () async { diff --git a/packages/nextcloud_test/LICENSE b/packages/nextcloud_test/LICENSE new file mode 120000 index 00000000..af8c58b1 --- /dev/null +++ b/packages/nextcloud_test/LICENSE @@ -0,0 +1 @@ +../../assets/AGPL-3.0.txt \ No newline at end of file diff --git a/packages/nextcloud_test/README.md b/packages/nextcloud_test/README.md new file mode 100644 index 00000000..6dcbca86 --- /dev/null +++ b/packages/nextcloud_test/README.md @@ -0,0 +1,3 @@ +# nextcloud_test + +A helper package for running tests in the [nextcloud](../nextcloud) package. diff --git a/packages/nextcloud_test/analysis_options.yaml b/packages/nextcloud_test/analysis_options.yaml new file mode 100644 index 00000000..4db3c296 --- /dev/null +++ b/packages/nextcloud_test/analysis_options.yaml @@ -0,0 +1 @@ +include: package:neon_lints/dart.yaml diff --git a/tool/Dockerfile.dev b/packages/nextcloud_test/docker/Dockerfile similarity index 98% rename from tool/Dockerfile.dev rename to packages/nextcloud_test/docker/Dockerfile index b9b4571b..875194f8 100644 --- a/tool/Dockerfile.dev +++ b/packages/nextcloud_test/docker/Dockerfile @@ -18,7 +18,7 @@ RUN (sh /entrypoint.sh php -S 0.0.0.0:80 &) && \ # Do not setup the demo user here for user in admin user1 user2; do curl -u "$user:$user" -H "ocs-apirequest: true" -s -o /dev/null http://localhost/ocs/v2.php/cloud/user; done -COPY dev/static /usr/src/nextcloud/static +COPY static /usr/src/nextcloud/static ENV PHP_CLI_SERVER_WORKERS=10 CMD ["php", "-S", "0.0.0.0:80"] diff --git a/tool/dev/static/nasa.xml b/packages/nextcloud_test/docker/static/nasa.xml similarity index 100% rename from tool/dev/static/nasa.xml rename to packages/nextcloud_test/docker/static/nasa.xml diff --git a/tool/dev/static/wikipedia.xml b/packages/nextcloud_test/docker/static/wikipedia.xml similarity index 100% rename from tool/dev/static/wikipedia.xml rename to packages/nextcloud_test/docker/static/wikipedia.xml diff --git a/packages/nextcloud_test/lib/nextcloud_test.dart b/packages/nextcloud_test/lib/nextcloud_test.dart new file mode 100644 index 00000000..a7517d2b --- /dev/null +++ b/packages/nextcloud_test/lib/nextcloud_test.dart @@ -0,0 +1,3 @@ +export 'src/defaults.dart'; +export 'src/docker_container.dart'; +export 'src/test_client.dart'; diff --git a/packages/nextcloud_test/lib/src/defaults.dart b/packages/nextcloud_test/lib/src/defaults.dart new file mode 100644 index 00000000..d3d5ef9a --- /dev/null +++ b/packages/nextcloud_test/lib/src/defaults.dart @@ -0,0 +1,7 @@ +import 'package:test/test.dart'; + +/// Default retry count for test groups. +const retryCount = 3; + +/// Default timeout for test groups. +const timeout = Timeout(Duration(seconds: 30)); diff --git a/packages/nextcloud_test/lib/src/docker_container.dart b/packages/nextcloud_test/lib/src/docker_container.dart new file mode 100644 index 00000000..752410e6 --- /dev/null +++ b/packages/nextcloud_test/lib/src/docker_container.dart @@ -0,0 +1,112 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:process_run/process_run.dart'; + +int _randomPort() => 1024 + Random().nextInt(65535 - 1024); + +/// Represents a docker container on the system. +class DockerContainer { + DockerContainer._({ + required this.id, + required this.port, + }); + + /// Creates a new docker container and returns its representation. + static Future<DockerContainer> create() async { + const dockerImageName = 'ghcr.io/nextcloud/neon/dev'; + + var result = await runExecutableArguments( + 'docker', + [ + 'images', + '-q', + dockerImageName, + ], + ); + if (result.exitCode != 0) { + throw Exception('Querying docker image failed: ${result.stderr}'); + } + if (result.stdout.toString().isEmpty) { + throw Exception('Missing docker image $dockerImageName. Please build it using ./tool/build-dev-container.sh'); + } + + late int port; + while (true) { + port = _randomPort(); + result = await runExecutableArguments( + 'docker', + [ + 'run', + '--rm', + '-d', + '--add-host', + 'host.docker.internal:host-gateway', + '-p', + '$port:80', + dockerImageName, + ], + ); + // 125 means the docker run command itself has failed which indicated the port is already used + if (result.exitCode != 125) { + break; + } + } + + if (result.exitCode != 0) { + throw Exception('Failed to run docker container: ${result.stderr}'); + } + + return DockerContainer._( + id: result.stdout.toString().replaceAll('\n', ''), + port: port, + ); + } + + /// ID of the docker container. + final String id; + + /// Assigned port of docker container. + final int port; + + /// Removes the docker container from the system. + void destroy() => unawaited( + runExecutableArguments( + 'docker', + [ + 'kill', + id, + ], + ), + ); + + /// Reads the server logs. + Future<String> serverLogs() async => (await runExecutableArguments( + 'docker', + [ + 'logs', + id, + ], + stdoutEncoding: utf8, + )) + .stdout as String; + + /// Reads the Nextcloud logs. + Future<String> nextcloudLogs() async => (await runExecutableArguments( + 'docker', + [ + 'exec', + id, + 'cat', + 'data/nextcloud.log', + ], + stdoutEncoding: utf8, + )) + .stdout as String; + + /// Reads all logs. + /// + /// Combines the output of [serverLogs] and [nextcloudLogs]. + Future<String> allLogs() async => '${await serverLogs()}\n\n${await nextcloudLogs()}'; +} diff --git a/packages/nextcloud_test/lib/src/test_client.dart b/packages/nextcloud_test/lib/src/test_client.dart new file mode 100644 index 00000000..a422dc5d --- /dev/null +++ b/packages/nextcloud_test/lib/src/test_client.dart @@ -0,0 +1,79 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:nextcloud/core.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud_test/src/docker_container.dart'; +import 'package:process_run/process_run.dart'; +import 'package:universal_io/io.dart'; + +/// An extension for creating [NextcloudClient]s based on [DockerContainer]s. +extension TestNextcloudClient on NextcloudClient { + /// Creates a new [NextcloudClient] for a given [container] and [username]. + /// + /// It is expected that the password of the user matches the its [username]. + /// This is the case for the available test docker containers. + static Future<NextcloudClient> create( + final DockerContainer container, { + final String? username = 'user1', + }) async { + String? appPassword; + if (username != null) { + final inputStream = StreamController<List<int>>(); + final process = runExecutableArguments( + 'docker', + [ + 'exec', + '-i', + container.id, + 'php', + '-f', + 'occ', + 'user:add-app-password', + username, + ], + stdin: inputStream.stream, + ); + inputStream.add(utf8.encode(username)); + await inputStream.close(); + + final result = await process; + if (result.exitCode != 0) { + throw Exception('Failed to run generate app password command\n${result.stderr}\n${result.stdout}'); + } + appPassword = (result.stdout as String).split('\n')[1]; + } + + final client = NextcloudClient( + Uri( + scheme: 'http', + host: 'localhost', + port: container.port, + ), + loginName: username, + password: username, + appPassword: appPassword, + cookieJar: CookieJar(), + ); + + var i = 0; + while (true) { + try { + await client.core.getStatus(); + break; + } catch (error) { + if (error is HttpException || error is DynamiteApiException) { + i++; + await Future<void>.delayed(const Duration(milliseconds: 100)); + if (i >= 300) { + throw TimeoutException('Failed to get the status of the Server. $error'); + } + } else { + rethrow; + } + } + } + + return client; + } +} diff --git a/packages/nextcloud_test/pubspec.yaml b/packages/nextcloud_test/pubspec.yaml new file mode 100644 index 00000000..d7d13a07 --- /dev/null +++ b/packages/nextcloud_test/pubspec.yaml @@ -0,0 +1,21 @@ +name: nextcloud_test +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.1.0 <4.0.0' + +dependencies: + nextcloud: + git: + url: https://github.com/nextcloud/neon + path: packages/nextcloud + process_run: ^0.13.0 + test: ^1.24.0 + universal_io: ^2.0.0 + +dev_dependencies: + neon_lints: + git: + url: https://github.com/nextcloud/neon + path: packages/neon_lints diff --git a/packages/nextcloud_test/pubspec_overrides.yaml b/packages/nextcloud_test/pubspec_overrides.yaml new file mode 100644 index 00000000..067c89d1 --- /dev/null +++ b/packages/nextcloud_test/pubspec_overrides.yaml @@ -0,0 +1,8 @@ +# melos_managed_dependency_overrides: dynamite_runtime,neon_lints,nextcloud +dependency_overrides: + dynamite_runtime: + path: ../dynamite/dynamite_runtime + neon_lints: + path: ../neon_lints + nextcloud: + path: ../nextcloud diff --git a/tool/build-dev-container.sh b/tool/build-dev-container.sh index ce091348..49a9cb07 100755 --- a/tool/build-dev-container.sh +++ b/tool/build-dev-container.sh @@ -6,4 +6,4 @@ source tool/common.sh tag="$(image_tag "dev:latest")" # shellcheck disable=SC2046 -docker buildx build --tag "$tag" $(cache_build_args "$tag") -f - ./tool < tool/Dockerfile.dev +docker buildx build --tag "$tag" $(cache_build_args "$tag") -f - ./packages/nextcloud_test/docker < packages/nextcloud_test/docker/Dockerfile