15 changed files with 239 additions and 3 deletions
			
			
		| @ -0,0 +1,3 @@ | |||||||
|  | # nextcloud_test | ||||||
|  | 
 | ||||||
|  | A helper package for running tests in the [nextcloud](../nextcloud) package. | ||||||
| @ -0,0 +1 @@ | |||||||
|  | include: package:neon_lints/dart.yaml | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | export 'src/defaults.dart'; | ||||||
|  | export 'src/docker_container.dart'; | ||||||
|  | export 'src/test_client.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)); | ||||||
| @ -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()}'; | ||||||
|  | } | ||||||
| @ -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; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -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 | ||||||
| @ -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 | ||||||
					Loading…
					
					
				
		Reference in new issue