|
|
|
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<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;
|
|
|
|
} 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<DockerContainer> 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<DockerImage> getDockerImage({
|
|
|
|
final String? serverVersion,
|
|
|
|
}) async {
|
|
|
|
final buildArgs = <String, String>{
|
|
|
|
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<List<int>>();
|
|
|
|
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);
|
|
|
|
}
|