diff --git a/commitlint.yaml b/commitlint.yaml index d25d7a104ee..627b41fae58 100644 --- a/commitlint.yaml +++ b/commitlint.yaml @@ -28,6 +28,7 @@ rules: - news_app - nextcloud - nextcloud_test + - nextcloud_test_api - nextcloud_test_presets - notes_app - notifications_app diff --git a/packages/nextcloud/dart_test.yaml b/packages/nextcloud/dart_test.yaml new file mode 100644 index 00000000000..3f08823039e --- /dev/null +++ b/packages/nextcloud/dart_test.yaml @@ -0,0 +1,16 @@ +platforms: + - vm + - chrome + +define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: chromium + +override_platforms: + chrome: + settings: + arguments: --disable-web-security + executable: chromium diff --git a/packages/nextcloud/packages/nextcloud_test/lib/nextcloud_test.dart b/packages/nextcloud/packages/nextcloud_test/lib/nextcloud_test.dart index 85338ec1eab..c21ca5e4525 100644 --- a/packages/nextcloud/packages/nextcloud_test/lib/nextcloud_test.dart +++ b/packages/nextcloud/packages/nextcloud_test/lib/nextcloud_test.dart @@ -1,2 +1,7 @@ -export 'src/fixtures.dart' hide appendFixture, validateFixture; -export 'src/presets.dart'; +/// Nextcloud test library. +/// +/// Handles the server setup and allows to run tests against different test presets. +/// All http requests of a test will be validated against predefined fixtures. +library; + +export 'src/nextcloud_test.dart' show NextcloudTester, NextcloudTesterCallback, closeFixture, presets, resetFixture; diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/models/models.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/models/models.dart deleted file mode 100644 index 1541057f305..00000000000 --- a/packages/nextcloud/packages/nextcloud_test/lib/src/models/models.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'nextcloud_tester.dart'; -export 'preset.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/models/nextcloud_tester.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/models/nextcloud_tester.dart deleted file mode 100644 index 99e200e8511..00000000000 --- a/packages/nextcloud/packages/nextcloud_test/lib/src/models/nextcloud_tester.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:async'; - -import 'package:nextcloud/nextcloud.dart'; -import 'package:nextcloud_test/nextcloud_test.dart'; -import 'package:nextcloud_test/src/models/models.dart'; -import 'package:nextcloud_test/src/test_target/test_target.dart'; -import 'package:version/version.dart'; - -/// Class that manages the creation of nextcloud api clients and the test environment. -final class NextcloudTester { - /// Creates a new Nextcloud tester for the given [appName] and [version]. - NextcloudTester({ - required String appName, - required Version version, - String username = defaultTestUsername, - }) : _preset = (name: appName, version: version), - _username = username; - - final Preset _preset; - - final String _username; - - /// The app version tested. - Version get version => _preset.version; - - /// URL where the target is available from itself. - Uri get targetURL { - final target = _target; - if (target == null) { - throw StateError('The tester has not been initialized'); - } - - return target.targetURL; - } - - /// Creates a new [NextcloudClient] for a given [username]. - /// - /// It is expected that the password of the user matches its [username]. - FutureOr createClient({String? username = 'user1'}) { - final target = _target; - if (target == null) { - throw StateError('The tester has not been initialized'); - } - - return target.createClient(username: username); - } - - /// The Nextcloud api client for the default user. - /// - /// Use [createClient] to create a separate one. - NextcloudClient get client { - final client = _client; - if (client == null) { - throw StateError('The tester has not been initialized'); - } - - return client; - } - - late NextcloudClient? _client; - late TestTargetInstance? _target; - - /// Initializes the tester creating the target and default client. - Future init() async { - _target = await TestTargetFactory.instance.spawn(_preset); - _client = await _target!.createClient(username: _username); - } - - /// Closes the tester. - Future close() async { - client.close(); - await _target!.destroy(); - } -} diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/models/preset.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/models/preset.dart deleted file mode 100644 index acd8d0b6662..00000000000 --- a/packages/nextcloud/packages/nextcloud_test/lib/src/models/preset.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:version/version.dart'; - -/// Combination of preset `name` and preset `version`. -typedef Preset = ({String name, Version version}); diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test.dart new file mode 100644 index 00000000000..e1763c925c5 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test.dart @@ -0,0 +1,4 @@ +export 'nextcloud_test/fixture_interceptor.dart'; +export 'nextcloud_test/fixtures.dart'; +export 'nextcloud_test/nextcloud_tester.dart'; +export 'nextcloud_test/presets.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/fixture_interceptor.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/fixture_interceptor.dart similarity index 100% rename from packages/nextcloud/packages/nextcloud_test/lib/src/fixture_interceptor.dart rename to packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/fixture_interceptor.dart diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/fixtures.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/fixtures.dart similarity index 54% rename from packages/nextcloud/packages/nextcloud_test/lib/src/fixtures.dart rename to packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/fixtures.dart index 0d64da38473..deb9bd05f64 100644 --- a/packages/nextcloud/packages/nextcloud_test/lib/src/fixtures.dart +++ b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/fixtures.dart @@ -1,10 +1,8 @@ -import 'package:built_collection/built_collection.dart'; import 'package:meta/meta.dart'; -import 'package:nextcloud/webdav.dart'; -import 'package:nextcloud_test/src/models/models.dart'; +import 'package:nextcloud_test_api/client.dart'; +import 'package:test/expect.dart'; // ignore: implementation_imports import 'package:test_api/src/backend/invoker.dart'; -import 'package:universal_io/io.dart'; var _closed = false; final _fixture = []; @@ -31,9 +29,9 @@ void resetFixture() { /// Validates that the requests match the stored fixtures. /// -/// If there is no stored fixture a new one is created. +/// If there is no stored fixture an empty one is created. @internal -void validateFixture(NextcloudTester tester) { +Future validateFixture(NextcloudTestApiClient client, Preset preset) async { if (_fixture.isEmpty) { return; } @@ -55,37 +53,31 @@ void validateFixture(NextcloudTester tester) { // Remove the groups that are the preset name and the preset version and the app is kept. for (var i = 0; i <= 2; i++) { - if (groups[i] == '${tester.version.major}.${tester.version.minor}') { + if (groups[i] == '${preset.version.major}.${preset.version.minor}') { groups.removeAt(i); break; } } - final fixturesPath = PathUri( - isAbsolute: false, - isDirectory: true, - pathSegments: BuiltList.from([ - 'test', - 'fixtures', - ...groups.map(_formatName), - ]), + final fixtureName = _formatName(Invoker.current!.liveTest.individualName); + + final fixturePath = [ + ...groups.map(_formatName), + '$fixtureName.regexp', + ]; + + final response = await client.validateFixture( + fixturePath: fixturePath, ); - final fixturesDir = Directory(fixturesPath.path); - if (!fixturesDir.existsSync()) { - fixturesDir.createSync(recursive: true); - } + final pattern = response.fixture; - final fixtureName = _formatName(Invoker.current!.liveTest.individualName.toLowerCase()); - final fixtureFile = File(fixturesPath.join(PathUri.parse('$fixtureName.regexp')).path); - if (fixtureFile.existsSync()) { - final pattern = fixtureFile.readAsStringSync(); - final hasMatch = RegExp('^$pattern\$').hasMatch(data); - if (!hasMatch) { - throw Exception('$data\ndoes not match\n$pattern'); - } - } else { - fixtureFile.writeAsStringSync(RegExp.escape(data)); - throw Exception('Missing fixture $fixtureFile'); + if (pattern == null) { + fail( + 'Missing fixture: ' + '$data', + ); + } else if (!RegExp('^$pattern\$').hasMatch(data)) { + fail('$data\ndoes not match\n$pattern'); } _closed = false; diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/nextcloud_tester.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/nextcloud_tester.dart new file mode 100644 index 00000000000..4eb3492d9f6 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/nextcloud_tester.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:built_collection/built_collection.dart'; +import 'package:cookie_store/cookie_store.dart'; +import 'package:http/http.dart' as http; +import 'package:interceptor_http_client/interceptor_http_client.dart'; +import 'package:meta/meta.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud_test/src/nextcloud_test.dart'; +import 'package:nextcloud_test_api/client.dart'; + +/// The shared base http client. +/// +/// Both the nextcloud communication and the communication to the test manager +/// are handled through this client. +@internal +final http.Client httpClient = http.Client(); + +/// Class that manages the creation of nextcloud api clients and the test environment. +final class NextcloudTester { + /// Creates a new Nextcloud tester for the given [preset] with the initial [username]. + NextcloudTester({ + required NextcloudTestApiClient testClient, + required Preset preset, + required String username, + }) : _preset = preset, + _username = username, + _testClient = testClient; + + Preset _preset; + + final String _username; + + final NextcloudTestApiClient _testClient; + + late Uri _hostURL; + + /// URL where the target is available from itself. + late Uri targetURL; + + /// The Nextcloud api client for the default user. + /// + /// Use [createClient] to create a separate one. + late NextcloudClient client; + + /// The app version tested. + Version get version => _preset.version; + + /// Creates a new [NextcloudClient] for a given [username]. + /// + /// It is expected that the password of the user matches its [username]. + Future createClient({String username = 'user1'}) async { + final appPassword = await _testClient.createAppPassword( + preset: _preset, + username: username, + ); + + final interceptedClient = InterceptorHttpClient( + baseClient: httpClient, + interceptors: BuiltList([ + CookieStoreInterceptor( + cookieStore: CookieStore(), + ), + const FixtureInterceptor(appendFixture: appendFixture), + ]), + ); + + return NextcloudClient( + _hostURL, + loginName: username, + password: username, + appPassword: appPassword, + httpClient: interceptedClient, + ); + } + + /// Initializes the tester creating the target and default client. + Future init(String platform) async { + _preset = _preset.rebuild((b) { + b.platform = platform; + }); + + final response = await _testClient.setup( + preset: _preset, + ); + _hostURL = response.hostURL; + targetURL = response.targetURL; + + client = await createClient(username: _username); + } + + /// Closes the tester. + Future close() async { + await _testClient.tearDown( + preset: _preset, + ); + } +} diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/presets.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/presets.dart new file mode 100644 index 00000000000..d2c55642686 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test/lib/src/nextcloud_test/presets.dart @@ -0,0 +1,91 @@ +import 'dart:convert'; + +import 'package:meta/meta.dart'; +import 'package:nextcloud_test/src/nextcloud_test.dart'; +import 'package:nextcloud_test_api/client.dart'; +import 'package:test/test.dart'; +// ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart'; + +/// Signature for callback to [presets]. +typedef NextcloudTesterCallback = void Function(NextcloudTester tester); + +final _testClient = NextcloudTestApiClient.localhost( + httpClient: httpClient, +); + +/// All tests for apps that depend on the server version must be wrapped with this method and pass along the preset. +@isTestGroup +Future presets( + String presetGroup, + String app, + NextcloudTesterCallback body, { + String username = defaultTestUsername, + String? testOn, + Timeout? timeout, + Object? skip, + Object? tags, + Map? onPlatform, + int? retry, +}) async { + final response = await _testClient.getPresets(group: presetGroup); + final presets = response.presets; + + if (presets.isEmpty) { + throw Exception('Unknown preset type "$presetGroup"'); + } + + group( + presetGroup, + () { + void innerBody() { + for (final version in presets) { + group('${version.major}.${version.minor}', () { + final preset = Preset((b) { + b + ..groupName = presetGroup + ..appName = app + ..version = version; + }); + + final tester = NextcloudTester( + preset: preset, + username: username, + testClient: _testClient, + ); + + setUpAll(() async { + final platform = jsonEncode( + Invoker.current!.liveTest.suite.platform.serialize(), + ); + + await tester.init(platform); + }); + + tearDownAll(() async { + await tester.close(); + }); + + tearDown(() async { + await validateFixture(_testClient, preset); + }); + + body(tester); + }); + } + } + + if (app != presetGroup) { + group(app, innerBody); + } else { + innerBody(); + } + }, + testOn: testOn, + timeout: timeout, + skip: skip, + tags: tags, + onPlatform: onPlatform, + retry: retry, + ); +} diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/presets.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/presets.dart deleted file mode 100644 index 8a17fcaa0fc..00000000000 --- a/packages/nextcloud/packages/nextcloud_test/lib/src/presets.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:nextcloud_test/src/fixtures.dart'; -import 'package:nextcloud_test/src/models/models.dart'; -import 'package:nextcloud_test/src/test_target/test_target.dart'; -import 'package:test/test.dart'; - -/// Signature for callback to [presets]. -typedef NextcloudTesterCallback = void Function(NextcloudTester tester); - -/// The default username used in nextcloud tests. -const String defaultTestUsername = 'user1'; - -/// All tests for apps that depend on the server version must be wrapped with this method and pass along the preset. -@isTestGroup -void presets( - String presetGroup, - String app, - NextcloudTesterCallback body, { - String username = defaultTestUsername, - int? retry, - Timeout? timeout, -}) { - final presets = TestTargetFactory.instance.presets; - if (!presets.containsKey(presetGroup)) { - throw Exception('Unknown preset type "$presetGroup"'); - } - - void innerBody() { - for (final presetVersion in presets[presetGroup]) { - group('${presetVersion.major}.${presetVersion.minor}', () { - final tester = NextcloudTester( - appName: presetGroup, - version: presetVersion, - username: username, - ); - - setUpAll(() async { - await tester.init(); - }); - - tearDownAll(() async { - await tester.close(); - }); - - tearDown(() { - validateFixture(tester); - }); - - body(tester); - }); - } - } - - group( - presetGroup, - () { - if (app != presetGroup) { - group(app, innerBody); - } else { - innerBody(); - } - }, - retry: retry, - timeout: timeout, - ); -} diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/test_target/test_target.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/test_target/test_target.dart deleted file mode 100644 index 1ea74e3e67d..00000000000 --- a/packages/nextcloud/packages/nextcloud_test/lib/src/test_target/test_target.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:built_collection/built_collection.dart'; -import 'package:cookie_store/cookie_store.dart'; -import 'package:http/http.dart' as http; -import 'package:interceptor_http_client/interceptor_http_client.dart'; -import 'package:meta/meta.dart'; -import 'package:nextcloud/nextcloud.dart'; -import 'package:nextcloud_test/src/fixture_interceptor.dart'; -import 'package:nextcloud_test/src/fixtures.dart'; -import 'package:nextcloud_test/src/models/models.dart'; -import 'package:nextcloud_test/src/test_target/docker_container.dart'; -import 'package:nextcloud_test/src/test_target/local.dart'; -import 'package:version/version.dart'; - -/// Factory for creating [TestTargetInstance]s. -@internal -abstract class TestTargetFactory { - /// The instance of the [TestTargetFactory]. - static final TestTargetFactory instance = TestTargetFactory._create(); - - /// Creates a new [TestTargetFactory]. - static TestTargetFactory _create() { - final dir = Platform.environment['DIR']; - final url = Platform.environment['URL']; - - if (url != null || dir != null) { - // Fail hard if the variables are not properly set to avoid a fallback to docker. - return LocalFactory( - dir: dir!, - url: Uri.parse(url!), - ); - } - - return DockerContainerFactory(); - } - - /// Spawns a new [T]. - FutureOr spawn(Preset preset); - - /// Returns the available presets for the factory. - late BuiltListMultimap presets = getPresets(); - - /// Generates the presets. - /// - /// Use the cached version [presets] instead. - @protected - BuiltListMultimap getPresets(); -} - -/// Instance of a test target. -@internal -abstract class TestTargetInstance { - /// Destroys the instance. - FutureOr destroy(); - - /// URL where the target is available from the host side. - Uri get hostURL; - - /// URL where the target is available from itself. - Uri get targetURL; - - /// Creates an app password for [username] on the instance. - Future createAppPassword(String username); - - /// Creates a new [NextcloudClient] for a given [username]. - /// - /// It is expected that the password of the user matches the its [username]. - Future createClient({ - String? username = 'user1', - }) async { - String? appPassword; - if (username != null) { - appPassword = await createAppPassword(username); - } - - final httpClient = InterceptorHttpClient( - baseClient: http.Client(), - interceptors: BuiltList([ - CookieStoreInterceptor( - cookieStore: CookieStore(), - ), - const FixtureInterceptor(appendFixture: appendFixture), - ]), - ); - - return NextcloudClient( - hostURL, - loginName: username, - password: username, - appPassword: appPassword, - httpClient: httpClient, - ); - } -} diff --git a/packages/nextcloud/packages/nextcloud_test/pubspec.yaml b/packages/nextcloud/packages/nextcloud_test/pubspec.yaml index aac707cfb7a..28d0fee0896 100644 --- a/packages/nextcloud/packages/nextcloud_test/pubspec.yaml +++ b/packages/nextcloud/packages/nextcloud_test/pubspec.yaml @@ -6,7 +6,7 @@ environment: sdk: ^3.0.0 dependencies: - built_collection: ^5.1.1 + built_collection: ^5.0.0 cookie_store: git: url: https://github.com/nextcloud/neon @@ -19,13 +19,18 @@ dependencies: path: packages/interceptor_http_client meta: ^1.0.0 nextcloud: ^7.0.0 + nextcloud_test_api: + git: + url: https://github.com/nextcloud/neon + path: packages/nextcloud/packages/nextcloud_test_api path: ^1.9.0 test: ^1.24.0 test_api: ^0.7.0 - universal_io: ^2.0.0 - version: ^3.0.0 dev_dependencies: + build_runner: ^2.4.11 + built_value_generator: ^8.9.2 + built_value_test: ^8.9.2 neon_lints: git: url: https://github.com/nextcloud/neon diff --git a/packages/nextcloud/packages/nextcloud_test/pubspec_overrides.yaml b/packages/nextcloud/packages/nextcloud_test/pubspec_overrides.yaml index cfe67bc0b7e..833ec46c42e 100644 --- a/packages/nextcloud/packages/nextcloud_test/pubspec_overrides.yaml +++ b/packages/nextcloud/packages/nextcloud_test/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: cookie_store,dynamite_runtime,interceptor_http_client,neon_lints,nextcloud +# melos_managed_dependency_overrides: cookie_store,dynamite_runtime,interceptor_http_client,neon_lints,nextcloud,nextcloud_test_api dependency_overrides: cookie_store: path: ../../../cookie_store @@ -10,3 +10,5 @@ dependency_overrides: path: ../../../neon_lints nextcloud: path: ../.. + nextcloud_test_api: + path: ../nextcloud_test_api diff --git a/packages/nextcloud/packages/nextcloud_test_api/LICENSE b/packages/nextcloud/packages/nextcloud_test_api/LICENSE new file mode 120000 index 00000000000..f0b83dad961 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/LICENSE @@ -0,0 +1 @@ +../../../../assets/AGPL-3.0.txt \ No newline at end of file diff --git a/packages/nextcloud/packages/nextcloud_test_api/README.md b/packages/nextcloud/packages/nextcloud_test_api/README.md new file mode 100644 index 00000000000..9ff9e574330 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/README.md @@ -0,0 +1,3 @@ +# nextcloud_test + +A helper package for running tests in the [nextcloud](https://github.com/nextcloud/neon/tree/main/packages/nextcloud) package. diff --git a/packages/nextcloud/packages/nextcloud_test_api/analysis_options.yaml b/packages/nextcloud/packages/nextcloud_test_api/analysis_options.yaml new file mode 100644 index 00000000000..f0ac4b52756 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:neon_lints/dart.yaml + +analyzer: + exclude: + - build diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/api.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/api.dart new file mode 100644 index 00000000000..c67bdfa4438 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/api.dart @@ -0,0 +1,8 @@ +/// Nextcloud Test API Server-Side Library +library; + +export 'src/middleware/middleware.dart' show nextcloudTestStateProvider; + +export 'src/models/models.dart'; +export 'src/nextcloud_test_api.dart' show NextcloudTestState; +export 'src/utils/utils.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/client.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/client.dart new file mode 100644 index 00000000000..591c3261e03 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/client.dart @@ -0,0 +1,7 @@ +/// Nextcloud Test API Client-Side Library +library; + +export 'src/client/nextcloud_test_api_client.dart'; + +export 'src/models/models.dart'; +export 'src/utils/utils.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/client/nextcloud_test_api_client.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/client/nextcloud_test_api_client.dart new file mode 100644 index 00000000000..ce76cafbb5f --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/client/nextcloud_test_api_client.dart @@ -0,0 +1,241 @@ +import 'dart:convert' show jsonDecode, jsonEncode; +import 'dart:io' show ContentType, HttpHeaders, HttpStatus; + +import 'package:http/http.dart' as http; +import 'package:nextcloud_test_api/api.dart'; +import 'package:nextcloud_test_api/client.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; +import 'package:path/path.dart' as p; + +/// {@template nextcloud_test_api_malformed_response} +/// An exception thrown when there is a problem decoded the response body. +/// {@endtemplate} +class NextcloudTestApiMalformedResponse implements Exception { + /// {@macro nextcloud_test_api_malformed_response} + const NextcloudTestApiMalformedResponse({required this.error}); + + /// The associated error. + final Object error; + + @override + String toString() { + return 'NextcloudTestApiMalformedResponse: $error'; + } +} + +/// {@template nextcloud_test_api_request_failure} +/// An exception thrown when an http request failure occurs. +/// {@endtemplate} +class NextcloudTestApiRequestFailure implements Exception { + /// {@macro nextcloud_test_api_request_failure} + const NextcloudTestApiRequestFailure({ + required this.statusCode, + required this.body, + }); + + /// The associated http status code. + final int statusCode; + + /// The associated response body. + final Object body; + + @override + String toString() { + return 'NextcloudTestApiRequestFailure: statusCode: $statusCode, body $body'; + } +} + +/// {@template nextcloud_test_api_client} +/// A Dart API client for the Nextcloud Test API. +/// {@endtemplate} +class NextcloudTestApiClient { + /// Create an instance of [NextcloudTestApiClient] that integrates + /// with a local instance of the API (http://localhost). + /// + /// {@macro nextcloud_test_api_client} + NextcloudTestApiClient.localhost({ + int port = 8080, + http.Client? httpClient, + }) : this._( + baseUrl: 'http://localhost:$port', + httpClient: httpClient, + ); + + /// {@macro nextcloud_test_api_client} + NextcloudTestApiClient._({ + required String baseUrl, + http.Client? httpClient, + }) : _baseUrl = baseUrl, + _httpClient = httpClient ?? http.Client(); + + final String _baseUrl; + final http.Client _httpClient; + + /// GET `/api/v1/presets/` + /// Requests the presets for a test group. + /// + /// Supported parameters: + /// * [group] - The test group for which the presets are requested. + Future getPresets({ + required String group, + }) async { + final uri = Uri.parse('$_baseUrl/api/v1/presets/$group'); + final response = await _httpClient.get( + uri, + headers: _getRequestHeaders(), + ); + + if (response.statusCode != HttpStatus.ok) { + throw NextcloudTestApiRequestFailure( + body: response.body, + statusCode: response.statusCode, + ); + } + + return GetPresetsResponse.fromJson( + response.json() as Map, + ); + } + + /// GET `/api/v1/fixtures?fixtureID=` + /// Requests a test fixture. + /// + /// Supported parameters: + /// * [fixturePath] - The path of the fixture. + Future validateFixture({ + required List fixturePath, + }) async { + // All fixture paths are sent as posix paths. + // The server will decode the path and re join it according to the running platform. + final fixtureID = p.posix.joinAll(fixturePath); + final uri = Uri.parse('$_baseUrl/api/v1/fixtures?fixtureID=${Uri.encodeQueryComponent(fixtureID)}'); + final response = await _httpClient.get( + uri, + headers: _getRequestHeaders(), + ); + + switch (response.statusCode) { + case HttpStatus.ok: + case HttpStatus.notFound: + return ValidateFixtureResponse.fromJson( + response.json() as Map, + ); + + default: + throw NextcloudTestApiRequestFailure( + body: response.body, + statusCode: response.statusCode, + ); + } + } + + /// POST `/api/v1/setup` + /// Requests to setup a test target. + /// + /// Supported parameters: + /// * [preset] - The preset to setup. + Future setup({ + required Preset preset, + }) async { + final uri = Uri.parse('$_baseUrl/api/v1/setup'); + final response = await _httpClient.post( + uri, + headers: _getRequestHeaders(), + body: jsonEncode(preset), + ); + + switch (response.statusCode) { + case HttpStatus.ok: + return SetupResponse.fromJson( + response.json() as Map, + ); + + default: + throw NextcloudTestApiRequestFailure( + body: response.body, + statusCode: response.statusCode, + ); + } + } + + /// POST `/api/v1/teardown` + /// Requests to destroy a test target. + /// + /// Supported parameters: + /// * [preset] - The preset to tear down. + Future tearDown({ + required Preset preset, + }) async { + final uri = Uri.parse('$_baseUrl/api/v1/teardown'); + final response = await _httpClient.post( + uri, + headers: _getRequestHeaders(), + body: jsonEncode(preset), + ); + + switch (response.statusCode) { + case HttpStatus.ok: + return; + + default: + throw NextcloudTestApiRequestFailure( + body: response.body, + statusCode: response.statusCode, + ); + } + } + + /// POST `/api/v1/create_app_password?username=` + /// Requests the app password for a given user. + /// + /// Supported parameters: + /// * [preset] - The running preset. + /// * [username] - The name of the user to create a password for. + Future createAppPassword({ + required Preset preset, + required String username, + }) async { + final uri = Uri.parse('$_baseUrl/api/v1/create_app_password?username=${Uri.encodeQueryComponent(username)}'); + final response = await _httpClient.post( + uri, + headers: _getRequestHeaders(), + body: jsonEncode(preset), + ); + + switch (response.statusCode) { + case HttpStatus.ok: + return CreateAppPasswordResponse.fromJson( + response.json() as Map, + ).appPassword; + + case HttpStatus.noContent: + return null; + + default: + throw NextcloudTestApiRequestFailure( + body: response.body, + statusCode: response.statusCode, + ); + } + } + + static Map _getRequestHeaders() { + return { + HttpHeaders.contentTypeHeader: ContentType.json.value, + HttpHeaders.acceptHeader: ContentType.json.value, + }; + } +} + +extension on http.Response { + Object json() { + try { + return jsonDecode(body) as Object; + } catch (error, stackTrace) { + Error.throwWithStackTrace( + NextcloudTestApiMalformedResponse(error: error), + stackTrace, + ); + } + } +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/middleware/middleware.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/middleware/middleware.dart new file mode 100644 index 00000000000..272bb9325e3 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/middleware/middleware.dart @@ -0,0 +1 @@ +export 'nextcloud_test_state_provider.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/middleware/nextcloud_test_state_provider.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/middleware/nextcloud_test_state_provider.dart new file mode 100644 index 00000000000..66cb87c7bf6 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/middleware/nextcloud_test_state_provider.dart @@ -0,0 +1,11 @@ +import 'package:dart_frog/dart_frog.dart'; +import 'package:nextcloud_test_api/src/nextcloud_test_state/nextcloud_test_state.dart'; + +final _nextcloudTestState = NextcloudTestState(); + +/// Provides a [NextcloudTestState] to the current [RequestContext]. +Middleware nextcloudTestStateProvider() { + return (handler) { + return handler.use(provider((_) => _nextcloudTestState)); + }; +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/create_app_password_response.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/create_app_password_response.dart new file mode 100644 index 00000000000..924c7070e25 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/create_app_password_response.dart @@ -0,0 +1,31 @@ +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; + +part 'create_app_password_response.g.dart'; + +/// {@template create_app_password_response} +/// The response when a app password has been created successfully. +/// {@endtemplate} +abstract class CreateAppPasswordResponse implements Built { + /// {@macro create_app_password_response} + factory CreateAppPasswordResponse([void Function(CreateAppPasswordResponseBuilder) updates]) = + _$CreateAppPasswordResponse; + CreateAppPasswordResponse._(); + + /// Converts the current instance to a `Map`. + Map toJson() { + return serializers.serializeWith(CreateAppPasswordResponse.serializer, this)! as Map; + } + + /// Converts a `Map` into a [CreateAppPasswordResponse] instance. + static CreateAppPasswordResponse fromJson(Map json) { + return serializers.deserializeWith(CreateAppPasswordResponse.serializer, json)!; + } + + /// The serializer that serializes this value. + static Serializer get serializer => _$createAppPasswordResponseSerializer; + + /// The created app password. + String get appPassword; +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/create_app_password_response.g.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/create_app_password_response.g.dart new file mode 100644 index 00000000000..3d53bf61165 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/create_app_password_response.g.dart @@ -0,0 +1,147 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_app_password_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$createAppPasswordResponseSerializer = + new _$CreateAppPasswordResponseSerializer(); + +class _$CreateAppPasswordResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [ + CreateAppPasswordResponse, + _$CreateAppPasswordResponse + ]; + @override + final String wireName = 'CreateAppPasswordResponse'; + + @override + Iterable serialize( + Serializers serializers, CreateAppPasswordResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'appPassword', + serializers.serialize(object.appPassword, + specifiedType: const FullType(String)), + ]; + + return result; + } + + @override + CreateAppPasswordResponse deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new CreateAppPasswordResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'appPassword': + result.appPassword = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + } + } + + return result.build(); + } +} + +class _$CreateAppPasswordResponse extends CreateAppPasswordResponse { + @override + final String appPassword; + + factory _$CreateAppPasswordResponse( + [void Function(CreateAppPasswordResponseBuilder)? updates]) => + (new CreateAppPasswordResponseBuilder()..update(updates))._build(); + + _$CreateAppPasswordResponse._({required this.appPassword}) : super._() { + BuiltValueNullFieldError.checkNotNull( + appPassword, r'CreateAppPasswordResponse', 'appPassword'); + } + + @override + CreateAppPasswordResponse rebuild( + void Function(CreateAppPasswordResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + CreateAppPasswordResponseBuilder toBuilder() => + new CreateAppPasswordResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is CreateAppPasswordResponse && + appPassword == other.appPassword; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, appPassword.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'CreateAppPasswordResponse') + ..add('appPassword', appPassword)) + .toString(); + } +} + +class CreateAppPasswordResponseBuilder + implements + Builder { + _$CreateAppPasswordResponse? _$v; + + String? _appPassword; + String? get appPassword => _$this._appPassword; + set appPassword(String? appPassword) => _$this._appPassword = appPassword; + + CreateAppPasswordResponseBuilder(); + + CreateAppPasswordResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _appPassword = $v.appPassword; + _$v = null; + } + return this; + } + + @override + void replace(CreateAppPasswordResponse other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$CreateAppPasswordResponse; + } + + @override + void update(void Function(CreateAppPasswordResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + CreateAppPasswordResponse build() => _build(); + + _$CreateAppPasswordResponse _build() { + final _$result = _$v ?? + new _$CreateAppPasswordResponse._( + appPassword: BuiltValueNullFieldError.checkNotNull( + appPassword, r'CreateAppPasswordResponse', 'appPassword')); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/get_presets_response.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/get_presets_response.dart new file mode 100644 index 00000000000..d95f36fe4c4 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/get_presets_response.dart @@ -0,0 +1,31 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; + +part 'get_presets_response.g.dart'; + +/// {@template get_presets_response} +/// Response with the requested test presets. +/// {@endtemplate} +abstract class GetPresetsResponse implements Built { + /// {@macro get_presets_response} + factory GetPresetsResponse([void Function(GetPresetsResponseBuilder) updates]) = _$GetPresetsResponse; + GetPresetsResponse._(); + + /// Converts the current instance to a `Map`. + Map toJson() { + return serializers.serializeWith(GetPresetsResponse.serializer, this)! as Map; + } + + /// Converts a `Map` into a [GetPresetsResponse] instance. + static GetPresetsResponse fromJson(Map json) { + return serializers.deserializeWith(GetPresetsResponse.serializer, json)!; + } + + /// The serializer that serializes this value. + static Serializer get serializer => _$getPresetsResponseSerializer; + + /// The list of versions for the requested app. + BuiltSet get presets; +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/get_presets_response.g.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/get_presets_response.g.dart new file mode 100644 index 00000000000..ec1f93fcc50 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/get_presets_response.g.dart @@ -0,0 +1,156 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'get_presets_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$getPresetsResponseSerializer = + new _$GetPresetsResponseSerializer(); + +class _$GetPresetsResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [GetPresetsResponse, _$GetPresetsResponse]; + @override + final String wireName = 'GetPresetsResponse'; + + @override + Iterable serialize( + Serializers serializers, GetPresetsResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'presets', + serializers.serialize(object.presets, + specifiedType: + const FullType(BuiltSet, const [const FullType(Version)])), + ]; + + return result; + } + + @override + GetPresetsResponse deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new GetPresetsResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'presets': + result.presets.replace(serializers.deserialize(value, + specifiedType: const FullType( + BuiltSet, const [const FullType(Version)]))! + as BuiltSet); + break; + } + } + + return result.build(); + } +} + +class _$GetPresetsResponse extends GetPresetsResponse { + @override + final BuiltSet presets; + + factory _$GetPresetsResponse( + [void Function(GetPresetsResponseBuilder)? updates]) => + (new GetPresetsResponseBuilder()..update(updates))._build(); + + _$GetPresetsResponse._({required this.presets}) : super._() { + BuiltValueNullFieldError.checkNotNull( + presets, r'GetPresetsResponse', 'presets'); + } + + @override + GetPresetsResponse rebuild( + void Function(GetPresetsResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + GetPresetsResponseBuilder toBuilder() => + new GetPresetsResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is GetPresetsResponse && presets == other.presets; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, presets.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'GetPresetsResponse') + ..add('presets', presets)) + .toString(); + } +} + +class GetPresetsResponseBuilder + implements Builder { + _$GetPresetsResponse? _$v; + + SetBuilder? _presets; + SetBuilder get presets => + _$this._presets ??= new SetBuilder(); + set presets(SetBuilder? presets) => _$this._presets = presets; + + GetPresetsResponseBuilder(); + + GetPresetsResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _presets = $v.presets.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(GetPresetsResponse other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$GetPresetsResponse; + } + + @override + void update(void Function(GetPresetsResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + GetPresetsResponse build() => _build(); + + _$GetPresetsResponse _build() { + _$GetPresetsResponse _$result; + try { + _$result = _$v ?? new _$GetPresetsResponse._(presets: presets.build()); + } catch (_) { + late String _$failedField; + try { + _$failedField = 'presets'; + presets.build(); + } catch (e) { + throw new BuiltValueNestedFieldError( + r'GetPresetsResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/models.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/models.dart new file mode 100644 index 00000000000..a6bceb6278d --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/models.dart @@ -0,0 +1,8 @@ +export 'package:version/version.dart'; + +export 'create_app_password_response.dart'; +export 'get_presets_response.dart'; +export 'preset.dart'; +export 'serializers.dart'; +export 'setup_response.dart'; +export 'validate_fixture_response.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/preset.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/preset.dart new file mode 100644 index 00000000000..c4c64184a50 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/preset.dart @@ -0,0 +1,45 @@ +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; + +part 'preset.g.dart'; + +/// {@template preset} +/// A test preset containing the app name and version. +/// {@endtemplate} +abstract class Preset implements Built { + /// {@macro preset} + factory Preset([void Function(PresetBuilder) updates]) = _$Preset; + Preset._(); + + /// Converts the current instance to a `Map`. + Map toJson() { + return serializers.serializeWith(Preset.serializer, this)! as Map; + } + + /// Converts a `Map` into a [Preset] instance. + static Preset fromJson(Map json) { + return serializers.deserializeWith(Preset.serializer, json)!; + } + + /// The serializer that serializes this value. + static Serializer get serializer => _$presetSerializer; + + /// The name of the container. + String get groupName; + + /// The name of the app. + String get appName; + + /// The app version. + Version get version; + + /// The platform executing the test. + String? get platform; + + /// The tag of the docker image. + @memoized + String get imageTag { + return '$groupName-${version.major}.${version.minor}'; + } +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/preset.g.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/preset.g.dart new file mode 100644 index 00000000000..405cb79d84c --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/preset.g.dart @@ -0,0 +1,205 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'preset.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$presetSerializer = new _$PresetSerializer(); + +class _$PresetSerializer implements StructuredSerializer { + @override + final Iterable types = const [Preset, _$Preset]; + @override + final String wireName = 'Preset'; + + @override + Iterable serialize(Serializers serializers, Preset object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'groupName', + serializers.serialize(object.groupName, + specifiedType: const FullType(String)), + 'appName', + serializers.serialize(object.appName, + specifiedType: const FullType(String)), + 'version', + serializers.serialize(object.version, + specifiedType: const FullType(Version)), + ]; + Object? value; + value = object.platform; + if (value != null) { + result + ..add('platform') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + return result; + } + + @override + Preset deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new PresetBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'groupName': + result.groupName = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + case 'appName': + result.appName = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + case 'version': + result.version = serializers.deserialize(value, + specifiedType: const FullType(Version))! as Version; + break; + case 'platform': + result.platform = serializers.deserialize(value, + specifiedType: const FullType(String)) as String?; + break; + } + } + + return result.build(); + } +} + +class _$Preset extends Preset { + @override + final String groupName; + @override + final String appName; + @override + final Version version; + @override + final String? platform; + String? __imageTag; + + factory _$Preset([void Function(PresetBuilder)? updates]) => + (new PresetBuilder()..update(updates))._build(); + + _$Preset._( + {required this.groupName, + required this.appName, + required this.version, + this.platform}) + : super._() { + BuiltValueNullFieldError.checkNotNull(groupName, r'Preset', 'groupName'); + BuiltValueNullFieldError.checkNotNull(appName, r'Preset', 'appName'); + BuiltValueNullFieldError.checkNotNull(version, r'Preset', 'version'); + } + + @override + String get imageTag => __imageTag ??= super.imageTag; + + @override + Preset rebuild(void Function(PresetBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + PresetBuilder toBuilder() => new PresetBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is Preset && + groupName == other.groupName && + appName == other.appName && + version == other.version && + platform == other.platform; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, groupName.hashCode); + _$hash = $jc(_$hash, appName.hashCode); + _$hash = $jc(_$hash, version.hashCode); + _$hash = $jc(_$hash, platform.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'Preset') + ..add('groupName', groupName) + ..add('appName', appName) + ..add('version', version) + ..add('platform', platform)) + .toString(); + } +} + +class PresetBuilder implements Builder { + _$Preset? _$v; + + String? _groupName; + String? get groupName => _$this._groupName; + set groupName(String? groupName) => _$this._groupName = groupName; + + String? _appName; + String? get appName => _$this._appName; + set appName(String? appName) => _$this._appName = appName; + + Version? _version; + Version? get version => _$this._version; + set version(Version? version) => _$this._version = version; + + String? _platform; + String? get platform => _$this._platform; + set platform(String? platform) => _$this._platform = platform; + + PresetBuilder(); + + PresetBuilder get _$this { + final $v = _$v; + if ($v != null) { + _groupName = $v.groupName; + _appName = $v.appName; + _version = $v.version; + _platform = $v.platform; + _$v = null; + } + return this; + } + + @override + void replace(Preset other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$Preset; + } + + @override + void update(void Function(PresetBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + Preset build() => _build(); + + _$Preset _build() { + final _$result = _$v ?? + new _$Preset._( + groupName: BuiltValueNullFieldError.checkNotNull( + groupName, r'Preset', 'groupName'), + appName: BuiltValueNullFieldError.checkNotNull( + appName, r'Preset', 'appName'), + version: BuiltValueNullFieldError.checkNotNull( + version, r'Preset', 'version'), + platform: platform); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/serializers.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/serializers.dart new file mode 100644 index 00000000000..f6a0a982431 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/serializers.dart @@ -0,0 +1,46 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_value/standard_json_plugin.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; + +part 'serializers.g.dart'; + +/// The serializer for the api response models. +@SerializersFor([ + CreateAppPasswordResponse, + GetPresetsResponse, + Preset, + SetupResponse, + ValidateFixtureResponse, +]) +final Serializers serializers = (_$serializers.toBuilder() + ..addPlugin(StandardJsonPlugin()) + ..add(const _VersionSerializer())) + .build(); + +final class _VersionSerializer implements PrimitiveSerializer { + const _VersionSerializer(); + + @override + Iterable get types => const [Version]; + @override + String get wireName => 'Version'; + + @override + Object serialize( + Serializers serializers, + Version version, { + FullType specifiedType = FullType.unspecified, + }) { + return version.toString(); + } + + @override + Version deserialize( + Serializers serializers, + Object? serialized, { + FullType specifiedType = FullType.unspecified, + }) { + return Version.parse(serialized! as String); + } +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/serializers.g.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/serializers.g.dart new file mode 100644 index 00000000000..0a5483d6360 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/serializers.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'serializers.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializers _$serializers = (new Serializers().toBuilder() + ..add(CreateAppPasswordResponse.serializer) + ..add(GetPresetsResponse.serializer) + ..add(Preset.serializer) + ..add(SetupResponse.serializer) + ..add(ValidateFixtureResponse.serializer) + ..addBuilderFactory( + const FullType(BuiltSet, const [const FullType(Version)]), + () => new SetBuilder())) + .build(); + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/setup_response.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/setup_response.dart new file mode 100644 index 00000000000..9e7ddf49aa8 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/setup_response.dart @@ -0,0 +1,35 @@ +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; + +part 'setup_response.g.dart'; + +/// {@template setup_response} +/// The response when a preset has been set up successfully. +/// +/// Contains the test target information like [hostURL] and [targetURL]. +/// {@endtemplate} +abstract class SetupResponse implements Built { + /// {@macro setup_response} + factory SetupResponse([void Function(SetupResponseBuilder) updates]) = _$SetupResponse; + SetupResponse._(); + + /// Converts the current instance to a `Map`. + Map toJson() { + return serializers.serializeWith(SetupResponse.serializer, this)! as Map; + } + + /// Converts a `Map` into a [SetupResponse] instance. + static SetupResponse fromJson(Map json) { + return serializers.deserializeWith(SetupResponse.serializer, json)!; + } + + /// The serializer that serializes this value. + static Serializer get serializer => _$setupResponseSerializer; + + /// URL where the target is available from the host side. + Uri get hostURL; + + /// URL where the target is available from itself. + Uri get targetURL; +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/setup_response.g.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/setup_response.g.dart new file mode 100644 index 00000000000..fe98366fe88 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/setup_response.g.dart @@ -0,0 +1,158 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'setup_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$setupResponseSerializer = + new _$SetupResponseSerializer(); + +class _$SetupResponseSerializer implements StructuredSerializer { + @override + final Iterable types = const [SetupResponse, _$SetupResponse]; + @override + final String wireName = 'SetupResponse'; + + @override + Iterable serialize(Serializers serializers, SetupResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'hostURL', + serializers.serialize(object.hostURL, specifiedType: const FullType(Uri)), + 'targetURL', + serializers.serialize(object.targetURL, + specifiedType: const FullType(Uri)), + ]; + + return result; + } + + @override + SetupResponse deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new SetupResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'hostURL': + result.hostURL = serializers.deserialize(value, + specifiedType: const FullType(Uri))! as Uri; + break; + case 'targetURL': + result.targetURL = serializers.deserialize(value, + specifiedType: const FullType(Uri))! as Uri; + break; + } + } + + return result.build(); + } +} + +class _$SetupResponse extends SetupResponse { + @override + final Uri hostURL; + @override + final Uri targetURL; + + factory _$SetupResponse([void Function(SetupResponseBuilder)? updates]) => + (new SetupResponseBuilder()..update(updates))._build(); + + _$SetupResponse._({required this.hostURL, required this.targetURL}) + : super._() { + BuiltValueNullFieldError.checkNotNull(hostURL, r'SetupResponse', 'hostURL'); + BuiltValueNullFieldError.checkNotNull( + targetURL, r'SetupResponse', 'targetURL'); + } + + @override + SetupResponse rebuild(void Function(SetupResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + SetupResponseBuilder toBuilder() => new SetupResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is SetupResponse && + hostURL == other.hostURL && + targetURL == other.targetURL; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, hostURL.hashCode); + _$hash = $jc(_$hash, targetURL.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'SetupResponse') + ..add('hostURL', hostURL) + ..add('targetURL', targetURL)) + .toString(); + } +} + +class SetupResponseBuilder + implements Builder { + _$SetupResponse? _$v; + + Uri? _hostURL; + Uri? get hostURL => _$this._hostURL; + set hostURL(Uri? hostURL) => _$this._hostURL = hostURL; + + Uri? _targetURL; + Uri? get targetURL => _$this._targetURL; + set targetURL(Uri? targetURL) => _$this._targetURL = targetURL; + + SetupResponseBuilder(); + + SetupResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _hostURL = $v.hostURL; + _targetURL = $v.targetURL; + _$v = null; + } + return this; + } + + @override + void replace(SetupResponse other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$SetupResponse; + } + + @override + void update(void Function(SetupResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + SetupResponse build() => _build(); + + _$SetupResponse _build() { + final _$result = _$v ?? + new _$SetupResponse._( + hostURL: BuiltValueNullFieldError.checkNotNull( + hostURL, r'SetupResponse', 'hostURL'), + targetURL: BuiltValueNullFieldError.checkNotNull( + targetURL, r'SetupResponse', 'targetURL')); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/validate_fixture_response.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/validate_fixture_response.dart new file mode 100644 index 00000000000..8754b5d04a1 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/validate_fixture_response.dart @@ -0,0 +1,30 @@ +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; + +part 'validate_fixture_response.g.dart'; + +/// {@template validate_fixture_response} +/// The response with the retrieved fixture content. +/// {@endtemplate} +abstract class ValidateFixtureResponse implements Built { + /// {@macro validate_fixture_response} + factory ValidateFixtureResponse([void Function(ValidateFixtureResponseBuilder) updates]) = _$ValidateFixtureResponse; + ValidateFixtureResponse._(); + + /// Converts the current instance to a `Map`. + Map toJson() { + return serializers.serializeWith(ValidateFixtureResponse.serializer, this)! as Map; + } + + /// Converts a `Map` into a [ValidateFixtureResponse] instance. + static ValidateFixtureResponse fromJson(Map json) { + return serializers.deserializeWith(ValidateFixtureResponse.serializer, json)!; + } + + /// The serializer that serializes this value. + static Serializer get serializer => _$validateFixtureResponseSerializer; + + /// The fixture content. + String? get fixture; +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/validate_fixture_response.g.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/validate_fixture_response.g.dart new file mode 100644 index 00000000000..bfc27dcf6e8 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/models/validate_fixture_response.g.dart @@ -0,0 +1,143 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'validate_fixture_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$validateFixtureResponseSerializer = + new _$ValidateFixtureResponseSerializer(); + +class _$ValidateFixtureResponseSerializer + implements StructuredSerializer { + @override + final Iterable types = const [ + ValidateFixtureResponse, + _$ValidateFixtureResponse + ]; + @override + final String wireName = 'ValidateFixtureResponse'; + + @override + Iterable serialize( + Serializers serializers, ValidateFixtureResponse object, + {FullType specifiedType = FullType.unspecified}) { + final result = []; + Object? value; + value = object.fixture; + if (value != null) { + result + ..add('fixture') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + return result; + } + + @override + ValidateFixtureResponse deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new ValidateFixtureResponseBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'fixture': + result.fixture = serializers.deserialize(value, + specifiedType: const FullType(String)) as String?; + break; + } + } + + return result.build(); + } +} + +class _$ValidateFixtureResponse extends ValidateFixtureResponse { + @override + final String? fixture; + + factory _$ValidateFixtureResponse( + [void Function(ValidateFixtureResponseBuilder)? updates]) => + (new ValidateFixtureResponseBuilder()..update(updates))._build(); + + _$ValidateFixtureResponse._({this.fixture}) : super._(); + + @override + ValidateFixtureResponse rebuild( + void Function(ValidateFixtureResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ValidateFixtureResponseBuilder toBuilder() => + new ValidateFixtureResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is ValidateFixtureResponse && fixture == other.fixture; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, fixture.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'ValidateFixtureResponse') + ..add('fixture', fixture)) + .toString(); + } +} + +class ValidateFixtureResponseBuilder + implements + Builder { + _$ValidateFixtureResponse? _$v; + + String? _fixture; + String? get fixture => _$this._fixture; + set fixture(String? fixture) => _$this._fixture = fixture; + + ValidateFixtureResponseBuilder(); + + ValidateFixtureResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _fixture = $v.fixture; + _$v = null; + } + return this; + } + + @override + void replace(ValidateFixtureResponse other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$ValidateFixtureResponse; + } + + @override + void update(void Function(ValidateFixtureResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + ValidateFixtureResponse build() => _build(); + + _$ValidateFixtureResponse _build() { + final _$result = _$v ?? new _$ValidateFixtureResponse._(fixture: fixture); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_api.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_api.dart new file mode 100644 index 00000000000..e5a05802cee --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_api.dart @@ -0,0 +1,5 @@ +export 'nextcloud_test_state/_docker_test_target.dart'; +export 'nextcloud_test_state/_local_test_target.dart'; +export 'nextcloud_test_state/fixtures.dart'; +export 'nextcloud_test_state/nextcloud_test_state.dart'; +export 'nextcloud_test_state/test_target.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/test_target/docker_container.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_state/_docker_test_target.dart similarity index 91% rename from packages/nextcloud/packages/nextcloud_test/lib/src/test_target/docker_container.dart rename to packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_state/_docker_test_target.dart index 2123ebd0731..1c2688b2845 100644 --- a/packages/nextcloud/packages/nextcloud_test/lib/src/test_target/docker_container.dart +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_state/_docker_test_target.dart @@ -1,18 +1,15 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:math'; import 'package:built_collection/built_collection.dart'; import 'package:glob/glob.dart'; import 'package:glob/list_local_fs.dart'; import 'package:meta/meta.dart'; -import 'package:nextcloud_test/src/models/models.dart'; -import 'package:nextcloud_test/src/test_target/test_target.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; +import 'package:nextcloud_test_api/src/nextcloud_test_api.dart'; +import 'package:nextcloud_test_api/src/utils/utils.dart'; import 'package:path/path.dart' as p; -import 'package:version/version.dart'; - -int _randomPort() => 1024 + Random().nextInt(65535 - 1024); /// Factory for spawning docker containers as test targets. @internal @@ -20,7 +17,7 @@ final class DockerContainerFactory extends TestTargetFactory spawn(Preset preset) async { - final dockerImageName = 'ghcr.io/nextcloud/neon/dev:${preset.name}-${preset.version.major}.${preset.version.minor}'; + final dockerImageName = 'ghcr.io/nextcloud/neon/dev:${preset.imageTag}'; var result = await Process.run( 'docker', @@ -41,7 +38,7 @@ final class DockerContainerFactory extends TestTargetFactory getPresets(String groupName) { + return factory.presets[groupName]; + } + + final Map _targetInstances = {}; + + /// Spawns [TestTargetInstance] for the given [preset]. + Future setup({ + required Preset preset, + }) async { + return _targetInstances[preset] ??= await factory.spawn(preset); + } + + /// Destroys the [TestTargetInstance] for the given [preset]. + Future teardown({ + required Preset preset, + }) async { + final instance = _targetInstances.remove(preset); + await instance?.destroy(); + } + + /// Creates a new appPassword. + Future createAppPassword({ + required Preset preset, + required String username, + }) async { + return _targetInstances[preset]?.createAppPassword(username); + } + + /// Loads the fixture identified by [fixtureID]. + String? getFixture({ + required String? fixtureID, + }) { + if (fixtureID == null) { + return null; + } + + return loadFixture(fixtureID); + } +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_state/test_target.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_state/test_target.dart new file mode 100644 index 00000000000..0be0020c5a1 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/nextcloud_test_state/test_target.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:built_collection/built_collection.dart'; +import 'package:meta/meta.dart'; +import 'package:nextcloud_test_api/src/models/models.dart'; + +/// Factory for creating [TestTargetInstance]s. +@internal +abstract class TestTargetFactory { + /// Spawns a new [T]. + FutureOr spawn(Preset preset); + + /// Returns the available presets for the factory. + late BuiltListMultimap presets = getPresets(); + + /// Generates the presets. + /// + /// Use the cached version [presets] instead. + @protected + BuiltListMultimap getPresets(); +} + +/// Instance of a test target. +@internal +abstract class TestTargetInstance { + /// Destroys the instance. + FutureOr destroy(); + + /// URL where the target is available from the host side. + Uri get hostURL; + + /// URL where the target is available from itself. + Uri get targetURL; + + /// Creates an app password for [username] on the instance. + Future createAppPassword(String username); +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/lib/src/utils/utils.dart b/packages/nextcloud/packages/nextcloud_test_api/lib/src/utils/utils.dart new file mode 100644 index 00000000000..f4a4ed242aa --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/lib/src/utils/utils.dart @@ -0,0 +1,7 @@ +import 'dart:math'; + +/// The default username used in nextcloud tests. +const String defaultTestUsername = 'user1'; + +/// Generates a random unprivileged port. +int randomPort() => 1024 + Random().nextInt(65535 - 1024); diff --git a/packages/nextcloud/packages/nextcloud_test_api/pubspec.yaml b/packages/nextcloud/packages/nextcloud_test_api/pubspec.yaml new file mode 100644 index 00000000000..8e3bc038eea --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/pubspec.yaml @@ -0,0 +1,26 @@ +name: nextcloud_test_api +version: 1.0.0 +publish_to: none + +environment: + sdk: ^3.0.0 + +dependencies: + built_collection: ^5.0.0 + built_value: ^8.9.2 + dart_frog: ^1.1.0 + glob: ^2.1.2 + http: ^1.2.0 + meta: ^1.0.0 + path: ^1.9.0 + version: ^3.0.0 + +dev_dependencies: + build_runner: ^2.4.11 + built_value_generator: ^8.9.2 + built_value_test: ^8.9.2 + dart_frog_cli: ^1.2.3 + neon_lints: + git: + url: https://github.com/nextcloud/neon + path: packages/neon_lints diff --git a/packages/nextcloud/packages/nextcloud_test_api/pubspec_overrides.yaml b/packages/nextcloud/packages/nextcloud_test_api/pubspec_overrides.yaml new file mode 100644 index 00000000000..0e5d8123538 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +# melos_managed_dependency_overrides: neon_lints +dependency_overrides: + neon_lints: + path: ../../../neon_lints diff --git a/packages/nextcloud/packages/nextcloud_test_api/routes/_middleware.dart b/packages/nextcloud/packages/nextcloud_test_api/routes/_middleware.dart new file mode 100644 index 00000000000..98b29ed9d94 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/routes/_middleware.dart @@ -0,0 +1,6 @@ +import 'package:dart_frog/dart_frog.dart'; +import 'package:nextcloud_test_api/api.dart'; + +Handler middleware(Handler handler) { + return handler.use(requestLogger()).use(nextcloudTestStateProvider()); +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/create_app_password.dart b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/create_app_password.dart new file mode 100644 index 00000000000..0525b06cda1 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/create_app_password.dart @@ -0,0 +1,50 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dart_frog/dart_frog.dart'; +import 'package:nextcloud_test_api/api.dart'; + +FutureOr onRequest(RequestContext context) async { + switch (context.request.method) { + case HttpMethod.post: + return _post(context); + case HttpMethod.get: + case HttpMethod.delete: + case HttpMethod.head: + case HttpMethod.options: + case HttpMethod.patch: + case HttpMethod.put: + return Response.json(statusCode: HttpStatus.methodNotAllowed); + } +} + +Future _post(RequestContext context) async { + final request = context.request; + final params = request.uri.queryParameters; + final username = params['username'] ?? defaultTestUsername; + + final body = await request.body(); + final preset = Preset.fromJson( + jsonDecode(body) as Map, + ); + + final dataSource = context.read(); + + final appPassword = await dataSource.createAppPassword( + preset: preset, + username: username, + ); + + if (appPassword == null) { + return Response.json( + statusCode: HttpStatus.noContent, + ); + } + + return Response.json( + body: CreateAppPasswordResponse((b) { + b.appPassword = appPassword; + }), + ); +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/fixtures/index.dart b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/fixtures/index.dart new file mode 100644 index 00000000000..f093aabfba1 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/fixtures/index.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_frog/dart_frog.dart'; +import 'package:nextcloud_test_api/api.dart'; + +FutureOr onRequest(RequestContext context) async { + switch (context.request.method) { + case HttpMethod.get: + return _put(context); + case HttpMethod.post: + case HttpMethod.put: + case HttpMethod.delete: + case HttpMethod.head: + case HttpMethod.options: + case HttpMethod.patch: + return Response.json(statusCode: HttpStatus.methodNotAllowed); + } +} + +Future _put(RequestContext context) async { + final request = context.request; + final params = request.uri.queryParameters; + final fixtureID = params['fixtureID']; + + final dataSource = context.read(); + + final fixture = dataSource.getFixture(fixtureID: fixtureID); + + return Response.json( + body: ValidateFixtureResponse((b) { + b.fixture = fixture; + }), + statusCode: fixture != null ? HttpStatus.ok : HttpStatus.notFound, + ); +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/presets/[group]/index.dart b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/presets/[group]/index.dart new file mode 100644 index 00000000000..516d5d6b487 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/presets/[group]/index.dart @@ -0,0 +1,34 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_frog/dart_frog.dart'; +import 'package:nextcloud_test_api/api.dart'; + +FutureOr onRequest(RequestContext context, String group) async { + switch (context.request.method) { + case HttpMethod.get: + return _post(context, group); + case HttpMethod.post: + case HttpMethod.delete: + case HttpMethod.head: + case HttpMethod.options: + case HttpMethod.patch: + case HttpMethod.put: + return Response.json(statusCode: HttpStatus.methodNotAllowed); + } +} + +Future _post(RequestContext context, String group) async { + final dataSource = context.read(); + final presets = dataSource.getPresets(group); + + if (presets.isEmpty) { + return Response.json(statusCode: HttpStatus.notFound); + } + + return Response.json( + body: GetPresetsResponse((b) { + b.presets.addAll(presets); + }), + ); +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/setup.dart b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/setup.dart new file mode 100644 index 00000000000..a4f88cc6a11 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/setup.dart @@ -0,0 +1,42 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dart_frog/dart_frog.dart'; +import 'package:nextcloud_test_api/api.dart'; + +FutureOr onRequest(RequestContext context) async { + switch (context.request.method) { + case HttpMethod.post: + return _post(context); + case HttpMethod.get: + case HttpMethod.delete: + case HttpMethod.head: + case HttpMethod.options: + case HttpMethod.patch: + case HttpMethod.put: + return Response.json(statusCode: HttpStatus.methodNotAllowed); + } +} + +Future _post(RequestContext context) async { + final request = context.request; + final body = await request.body(); + final preset = Preset.fromJson( + jsonDecode(body) as Map, + ); + + final dataSource = context.read(); + + final target = await dataSource.setup( + preset: preset, + ); + + return Response.json( + body: SetupResponse((b) { + b + ..hostURL = target.hostURL + ..targetURL = target.targetURL; + }), + ); +} diff --git a/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/teardown.dart b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/teardown.dart new file mode 100644 index 00000000000..22d09af2506 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test_api/routes/api/v1/teardown.dart @@ -0,0 +1,38 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dart_frog/dart_frog.dart'; +import 'package:nextcloud_test_api/api.dart'; + +FutureOr onRequest(RequestContext context) async { + switch (context.request.method) { + case HttpMethod.post: + return _post(context); + case HttpMethod.get: + case HttpMethod.delete: + case HttpMethod.head: + case HttpMethod.options: + case HttpMethod.patch: + case HttpMethod.put: + return Response.json(statusCode: HttpStatus.methodNotAllowed); + } +} + +Future _post(RequestContext context) async { + final request = context.request; + final body = await request.body(); + final preset = Preset.fromJson( + jsonDecode(body) as Map, + ); + + final dataSource = context.read(); + + await dataSource.teardown( + preset: preset, + ); + + return Response.json( + body: 'closed ${preset.groupName} tester', + ); +} diff --git a/packages/nextcloud/pubspec_overrides.yaml b/packages/nextcloud/pubspec_overrides.yaml index d31cf22a4a6..eb19e7241c7 100644 --- a/packages/nextcloud/pubspec_overrides.yaml +++ b/packages/nextcloud/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: cookie_store,dynamite,dynamite_runtime,interceptor_http_client,neon_lints,nextcloud_test +# melos_managed_dependency_overrides: cookie_store,dynamite,dynamite_runtime,interceptor_http_client,neon_lints,nextcloud_test,nextcloud_test_api dependency_overrides: cookie_store: path: ../cookie_store @@ -12,3 +12,5 @@ dependency_overrides: path: ../neon_lints nextcloud_test: path: packages/nextcloud_test + nextcloud_test_api: + path: packages/nextcloud_test_api diff --git a/packages/nextcloud/test/api/cookbook/cookbook_test.dart b/packages/nextcloud/test/api/cookbook/cookbook_test.dart index 5d517771c7c..db2e3ce480a 100644 --- a/packages/nextcloud/test/api/cookbook/cookbook_test.dart +++ b/packages/nextcloud/test/api/cookbook/cookbook_test.dart @@ -4,8 +4,8 @@ import 'package:nextcloud/cookbook.dart' as cookbook; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('cookbook', 'cookbook', (tester) { +void main() async { + await presets('cookbook', 'cookbook', (tester) { group('CookbookVersionCheck', () { test('Is supported', () async { final response = await tester.client.cookbook.getVersionCheck(); diff --git a/packages/nextcloud/test/api/core/core_test.dart b/packages/nextcloud/test/api/core/core_test.dart index 34b71ddcac4..2ec390871a2 100644 --- a/packages/nextcloud/test/api/core/core_test.dart +++ b/packages/nextcloud/test/api/core/core_test.dart @@ -7,8 +7,8 @@ import 'package:nextcloud/webdav.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('server', 'core', (tester) { +void main() async { + await presets('server', 'core', (tester) { test('Is supported from capabilities', () async { final response = await tester.client.core.ocs.getCapabilities(); @@ -114,22 +114,28 @@ void main() { }); group('Preview', () { - test('Get', () async { - final file = File('test/files/test.png'); - await tester.client.webdav.putFile(file, file.statSync(), PathUri.parse('preview.png')); - addTearDown(() async { - closeFixture(); - await tester.client.webdav.delete(PathUri.parse('preview.png')); - }); - - final response = await tester.client.core.preview.getPreview( - file: 'preview.png', - ); - expect(response.statusCode, 200); - expect(() => response.headers, isA()); - - expect(response.body, isNotEmpty); - }); + test( + 'Get', + () async { + final file = File('test/files/test.png'); + await tester.client.webdav.putFile(file, file.statSync(), PathUri.parse('preview.png')); + addTearDown(() async { + closeFixture(); + await tester.client.webdav.delete(PathUri.parse('preview.png')); + }); + + final response = await tester.client.core.preview.getPreview( + file: 'preview.png', + ); + expect(response.statusCode, 200); + expect(() => response.headers, isA()); + + expect(response.body, isNotEmpty); + }, + onPlatform: const { + 'browser': [Skip()], // TODO: fix this issue + }, + ); }); group('Avatar', () { diff --git a/packages/nextcloud/test/api/dashboard/dashboard_test.dart b/packages/nextcloud/test/api/dashboard/dashboard_test.dart index 8be8f915693..4c5f74d60a0 100644 --- a/packages/nextcloud/test/api/dashboard/dashboard_test.dart +++ b/packages/nextcloud/test/api/dashboard/dashboard_test.dart @@ -2,8 +2,8 @@ import 'package:nextcloud/dashboard.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('server', 'dashboard', (tester) { +void main() async { + await presets('server', 'dashboard', (tester) { test('Get widgets', () async { final response = await tester.client.dashboard.dashboardApi.getWidgets(); expect(response.statusCode, 200); diff --git a/packages/nextcloud/test/api/files_sharing/files_sharing_test.dart b/packages/nextcloud/test/api/files_sharing/files_sharing_test.dart index 23ba4ad8f04..6cbe3558052 100644 --- a/packages/nextcloud/test/api/files_sharing/files_sharing_test.dart +++ b/packages/nextcloud/test/api/files_sharing/files_sharing_test.dart @@ -1,3 +1,6 @@ +@TestOn('vm') +library; + import 'dart:io'; import 'package:nextcloud/core.dart' as core; @@ -6,8 +9,8 @@ import 'package:nextcloud/webdav.dart' as webdav; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('server', 'files_sharing', (tester) { +void main() async { + await presets('server', 'files_sharing', (tester) { group('shareapi', () { test('createShare', () async { final file = File('test/files/test.png'); diff --git a/packages/nextcloud/test/api/news/news_test.dart b/packages/nextcloud/test/api/news/news_test.dart index 51a1a72b5e5..bba6327dcd9 100644 --- a/packages/nextcloud/test/api/news/news_test.dart +++ b/packages/nextcloud/test/api/news/news_test.dart @@ -5,8 +5,8 @@ import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('news', 'news', (tester) { +void main() async { + await presets('news', 'news', (tester) { tearDown(() async { closeFixture(); diff --git a/packages/nextcloud/test/api/notes/notes_test.dart b/packages/nextcloud/test/api/notes/notes_test.dart index a50be7e59c6..b0d2df9140e 100644 --- a/packages/nextcloud/test/api/notes/notes_test.dart +++ b/packages/nextcloud/test/api/notes/notes_test.dart @@ -5,8 +5,8 @@ import 'package:nextcloud/notes.dart' as notes; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('notes', 'notes', (tester) { +void main() async { + await presets('notes', 'notes', (tester) { tearDown(() async { closeFixture(); diff --git a/packages/nextcloud/test/api/notifications/notifications_test.dart b/packages/nextcloud/test/api/notifications/notifications_test.dart index afe49c746f1..aeb5e8315cf 100644 --- a/packages/nextcloud/test/api/notifications/notifications_test.dart +++ b/packages/nextcloud/test/api/notifications/notifications_test.dart @@ -6,8 +6,8 @@ import 'package:nextcloud/src/utils/date_time.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('server', 'notifications', username: 'admin', (tester) { +void main() async { + await presets('server', 'notifications', username: 'admin', (tester) { Future sendTestNotification() async { await tester.client.notifications.api.generateNotification( userId: 'admin', diff --git a/packages/nextcloud/test/api/provisioning_api/provisioning_api_test.dart b/packages/nextcloud/test/api/provisioning_api/provisioning_api_test.dart index 6d5abec9d3c..7fdb8888dae 100644 --- a/packages/nextcloud/test/api/provisioning_api/provisioning_api_test.dart +++ b/packages/nextcloud/test/api/provisioning_api/provisioning_api_test.dart @@ -3,8 +3,8 @@ import 'package:nextcloud/provisioning_api.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('server', 'provisioning_api', username: 'admin', (tester) { +void main() async { + await presets('server', 'provisioning_api', username: 'admin', (tester) { group('Users', () { test('Get current user', () async { final response = await tester.client.provisioningApi.users.getCurrentUser(); diff --git a/packages/nextcloud/test/api/settings/settings_test.dart b/packages/nextcloud/test/api/settings/settings_test.dart index 44dff1be421..28607068407 100644 --- a/packages/nextcloud/test/api/settings/settings_test.dart +++ b/packages/nextcloud/test/api/settings/settings_test.dart @@ -4,8 +4,8 @@ import 'package:nextcloud/settings.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('server', 'settings', username: 'admin', (tester) { +void main() async { + await presets('server', 'settings', username: 'admin', (tester) { group('Logs', () { test('Download', () async { final response = await tester.client.settings.logSettings.download(); diff --git a/packages/nextcloud/test/api/spreed/spreed_test.dart b/packages/nextcloud/test/api/spreed/spreed_test.dart index 19e9a28d0d5..e43f8eb4b0d 100644 --- a/packages/nextcloud/test/api/spreed/spreed_test.dart +++ b/packages/nextcloud/test/api/spreed/spreed_test.dart @@ -9,8 +9,8 @@ import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; import 'package:version/version.dart'; -void main() { - presets('spreed', 'spreed', (tester) { +void main() async { + await presets('spreed', 'spreed', (tester) { Future createTestRoom() async => (await tester.client.spreed.room.createRoom( $body: spreed.RoomCreateRoomRequestApplicationJson( (b) => b @@ -525,47 +525,53 @@ void main() { expect(response.body.ocs.data.sipDialinInfo, ''); }); - test('Send and receive messages', () async { - final room = await createTestRoom(); + test( + 'Send and receive messages', + () async { + final room = await createTestRoom(); - final room1 = (await tester.client.spreed.room.joinRoom(token: room.token)).body.ocs.data; - await tester.client.spreed.call.joinCall(token: room.token); + final room1 = (await tester.client.spreed.room.joinRoom(token: room.token)).body.ocs.data; + await tester.client.spreed.call.joinCall(token: room.token); - final client2 = await tester.createClient( - username: 'user2', - ); + final client2 = await tester.createClient( + username: 'user2', + ); - final room2 = (await client2.spreed.room.joinRoom(token: room.token)).body.ocs.data; - await client2.spreed.call.joinCall(token: room.token); + final room2 = (await client2.spreed.room.joinRoom(token: room.token)).body.ocs.data; + await client2.spreed.call.joinCall(token: room.token); - await tester.client.spreed.internalSignaling.signalingSendMessages( - token: room.token, - $body: spreed.SignalingSendMessagesRequestApplicationJson( - (b) => b - ..messages = json.encode([ - { - 'ev': 'message', - 'sessionId': room1.sessionId, - 'fn': json.encode({ - 'to': room2.sessionId, - }), - }, - ]), - ), - ); + await tester.client.spreed.internalSignaling.signalingSendMessages( + token: room.token, + $body: spreed.SignalingSendMessagesRequestApplicationJson( + (b) => b + ..messages = json.encode([ + { + 'ev': 'message', + 'sessionId': room1.sessionId, + 'fn': json.encode({ + 'to': room2.sessionId, + }), + }, + ]), + ), + ); - await Future.delayed(const Duration(seconds: 1)); - - final messages = - (await client2.spreed.internalSignaling.signalingPullMessages(token: room.token)).body.ocs.data; - expect(messages, hasLength(2)); - expect(messages[0].type, 'message'); - expect(json.decode(messages[0].data.string!), {'to': room2.sessionId, 'from': room1.sessionId}); - expect(messages[1].type, 'usersInRoom'); - expect(messages[1].data.builtListSignalingSession, hasLength(2)); - expect(messages[1].data.builtListSignalingSession![0].userId, 'user1'); - expect(messages[1].data.builtListSignalingSession![1].userId, 'user2'); - }); + await Future.delayed(const Duration(seconds: 1)); + + final messages = + (await client2.spreed.internalSignaling.signalingPullMessages(token: room.token)).body.ocs.data; + expect(messages, hasLength(2)); + expect(messages[0].type, 'message'); + expect(json.decode(messages[0].data.string!), {'to': room2.sessionId, 'from': room1.sessionId}); + expect(messages[1].type, 'usersInRoom'); + expect(messages[1].data.builtListSignalingSession, hasLength(2)); + expect(messages[1].data.builtListSignalingSession![0].userId, 'user1'); + expect(messages[1].data.builtListSignalingSession![1].userId, 'user2'); + }, + onPlatform: const { + 'browser': [Skip()], // TODO: fix this issue + }, + ); }); group('Reaction', () { diff --git a/packages/nextcloud/test/api/tables/tables_test.dart b/packages/nextcloud/test/api/tables/tables_test.dart index 42962e52d69..e02f7ba6729 100644 --- a/packages/nextcloud/test/api/tables/tables_test.dart +++ b/packages/nextcloud/test/api/tables/tables_test.dart @@ -5,8 +5,8 @@ import 'package:test/test.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:version/version.dart'; -void main() { - presets('tables', 'tables', (tester) { +void main() async { + await presets('tables', 'tables', (tester) { test('Is supported', () async { final response = await tester.client.core.ocs.getCapabilities(); diff --git a/packages/nextcloud/test/api/uppush/uppush_test.dart b/packages/nextcloud/test/api/uppush/uppush_test.dart index ca268616761..e4b97c6513a 100644 --- a/packages/nextcloud/test/api/uppush/uppush_test.dart +++ b/packages/nextcloud/test/api/uppush/uppush_test.dart @@ -2,8 +2,8 @@ import 'package:nextcloud/uppush.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('uppush', 'uppush', username: 'admin', (tester) { +void main() async { + await presets('uppush', 'uppush', username: 'admin', (tester) { test('Is installed', () async { final response = await tester.client.uppush.check(); expect(response.statusCode, 200); diff --git a/packages/nextcloud/test/api/user_status/user_status_test.dart b/packages/nextcloud/test/api/user_status/user_status_test.dart index 02e874b8f81..b53c292b6bf 100644 --- a/packages/nextcloud/test/api/user_status/user_status_test.dart +++ b/packages/nextcloud/test/api/user_status/user_status_test.dart @@ -4,8 +4,8 @@ import 'package:nextcloud/user_status.dart' as user_status; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; -void main() { - presets('server', 'user_status', (tester) { +void main() async { + await presets('server', 'user_status', (tester) { setUp(() async { await tester.client.userStatus.userStatus.setStatus( $body: user_status.UserStatusSetStatusRequestApplicationJson( diff --git a/packages/nextcloud/test/api/weather_status/weather_status_test.dart b/packages/nextcloud/test/api/weather_status/weather_status_test.dart index cc87874b97b..616c246ef52 100644 --- a/packages/nextcloud/test/api/weather_status/weather_status_test.dart +++ b/packages/nextcloud/test/api/weather_status/weather_status_test.dart @@ -5,8 +5,8 @@ import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; import 'package:version/version.dart'; -void main() { - presets('server', 'weather_status', (tester) { +void main() async { + await presets('server', 'weather_status', (tester) { test('Set mode', () async { final response = await tester.client.weatherStatus.weatherStatus.setMode( $body: weather_status.WeatherStatusSetModeRequestApplicationJson( diff --git a/packages/nextcloud/test/api/webdav/webdav_test.dart b/packages/nextcloud/test/api/webdav/webdav_test.dart index 2fa4a52a482..fc69c4649a6 100644 --- a/packages/nextcloud/test/api/webdav/webdav_test.dart +++ b/packages/nextcloud/test/api/webdav/webdav_test.dart @@ -1,3 +1,6 @@ +@TestOn('vm') +library; + import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; @@ -16,7 +19,7 @@ class MockCallbackFunction extends Mock { void progressCallback(double progress); } -void main() { +void main() async { test('Chunked responses', () async { await HttpServer.bind('127.0.0.1', 0).then((server) async { server.listen((request) { @@ -66,7 +69,7 @@ void main() { }); }); - presets('server', 'webdav', (tester) { + await presets('server', 'webdav', (tester) { setUpAll(() async { await tester.client.webdav.mkcol(PathUri.parse('test')); resetFixture(); diff --git a/packages/nextcloud/test/nextcloud_client_test.dart b/packages/nextcloud/test/nextcloud_client_test.dart index 2523a7321c2..bf1248c30d4 100644 --- a/packages/nextcloud/test/nextcloud_client_test.dart +++ b/packages/nextcloud/test/nextcloud_client_test.dart @@ -1,10 +1,36 @@ import 'package:http_client_conformance_tests/http_client_conformance_tests.dart'; import 'package:nextcloud/nextcloud.dart'; +import 'package:test/test.dart'; void main() { - testAll( - () => NextcloudClient(Uri()), - canReceiveSetCookieHeaders: true, - canSendCookieHeaders: true, + group( + 'Interceptor Client VM conformance test', + () { + testAll( + () => NextcloudClient(Uri()), + canReceiveSetCookieHeaders: true, + canSendCookieHeaders: true, + ); + }, + onPlatform: const { + 'browser': [Skip()], + }, + ); + + group( + 'Interceptor Client browser conformance test', + () { + testAll( + () => NextcloudClient(Uri()), + redirectAlwaysAllowed: true, + canStreamRequestBody: false, + canStreamResponseBody: false, + canWorkInIsolates: false, + supportsMultipartRequest: false, + ); + }, + onPlatform: const { + 'dart-vm': [Skip()], + }, ); }