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_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 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 serverLogs() async => (await runExecutableArguments( + 'docker', + [ + 'logs', + id, + ], + stdoutEncoding: utf8, + )) + .stdout as String; + + /// Reads the Nextcloud logs. + Future 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 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 create( + final DockerContainer container, { + final String? username = 'user1', + }) async { + String? appPassword; + if (username != null) { + final inputStream = StreamController>(); + 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.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