import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:nextcloud/nextcloud.dart'; import 'package:process_run/cmd_run.dart'; import 'package:test/test.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 runOccCommand(final List 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 serverLogs() async => (await runExecutableArguments( 'docker', [ 'logs', id, ], stdoutEncoding: utf8, )) .stdout as String; Future nextcloudLogs() async => (await runExecutableArguments( 'docker', [ 'exec', id, 'cat', 'data/nextcloud.log', ], stdoutEncoding: utf8, )) .stdout as String; Future 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 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>(); 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; } on CoreApiException catch (error) { i++; await Future.delayed(const Duration(milliseconds: 100)); if (i >= 30) { throw TimeoutException('Failed to get the status of the Server. $error'); } } } return client; } Future getDockerContainer(final DockerImage image, {final bool useApache = false}) async { late ProcessResult result; late int port; while (true) { port = randomPort(); result = await runExecutableArguments( 'docker', [ 'run', '--rm', '-d', '--add-host', 'host.docker.internal:host-gateway', '-p', '$port:80', image, if (!useApache) ...[ 'php', '-S', '0.0.0.0:80', ], ], ); // 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, ); } typedef DockerImage = String; Future getDockerImage({ final String? serverVersion, }) async { final buildArgs = { if (serverVersion != null) 'SERVER_VERSION': serverVersion, }; final dockerImageName = 'nextcloud-neon-test${buildArgs.isNotEmpty ? '-' : ''}${buildArgs.entries.map((final buildArg) => '${buildArg.key.toLowerCase().replaceAll('_', '-')}-${buildArg.value}').join('-')}'; final inputStream = StreamController>(); final process = runExecutableArguments( 'docker', [ 'build', '-t', dockerImageName, ...buildArgs.entries .map((final buildArg) => ['--build-arg', '${buildArg.key}=${buildArg.value}']) .expand((final buildArg) => buildArg), '-f', '-', '../../tool', ], stdin: inputStream.stream, ); inputStream.add(utf8.encode(File('../../tool/Dockerfile.dev').readAsStringSync())); await inputStream.close(); final result = await process; if (result.exitCode != 0) { throw Exception('Failed to build docker image:\n${result.stdout as String}\n${result.stderr as String}'); } return dockerImageName; } 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); }