From 1386232615030b577faa8f277ccb9a5964b754a8 Mon Sep 17 00:00:00 2001 From: Andrei Toterman Date: Tue, 10 Dec 2024 20:42:31 +0100 Subject: [PATCH] make some files that were using crlf use just lf --- src/client/gui/lib/grpc_client.dart | 602 ++++++++++---------- src/platform/platform_shared.h | 78 +-- tests/test_global_settings_handlers.cpp | 722 ++++++++++++------------ 3 files changed, 701 insertions(+), 701 deletions(-) diff --git a/src/client/gui/lib/grpc_client.dart b/src/client/gui/lib/grpc_client.dart index b9cedad4677..603ac751ed3 100644 --- a/src/client/gui/lib/grpc_client.dart +++ b/src/client/gui/lib/grpc_client.dart @@ -1,301 +1,301 @@ -import 'dart:io'; - -import 'package:async/async.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:grpc/grpc.dart'; -import 'package:protobuf/protobuf.dart' hide RpcClient; -import 'package:rxdart/rxdart.dart'; - -import 'logger.dart'; -import 'providers.dart'; -import 'update_available.dart'; - -export 'generated/multipass.pbgrpc.dart'; - -typedef Status = InstanceStatus_Status; -typedef VmInfo = DetailedInfoItem; -typedef ImageInfo = FindReply_ImageInfo; -typedef MountPaths = MountInfo_MountPaths; -typedef RpcMessage = GeneratedMessage; - -extension on RpcMessage { - String get repr => '$runtimeType${toProto3Json()}'; -} - -void checkForUpdate(RpcMessage message) { - final updateInfo = switch (message) { - LaunchReply launchReply => launchReply.updateInfo, - InfoReply infoReply => infoReply.updateInfo, - ListReply listReply => listReply.updateInfo, - NetworksReply networksReply => networksReply.updateInfo, - StartReply startReply => startReply.updateInfo, - RestartReply restartReply => restartReply.updateInfo, - VersionReply versionReply => versionReply.updateInfo, - _ => UpdateInfo(), - }; - - providerContainer.read(updateProvider.notifier).set(updateInfo); -} - -void Function(StreamNotification) logGrpc(RpcMessage request) { - return (notification) { - switch (notification.kind) { - case NotificationKind.data: - final reply = notification.requireDataValue.deepCopy(); - if (reply is SSHInfoReply) { - for (final info in reply.sshInfo.values) { - info.privKeyBase64 = '*hidden*'; - } - } - if (reply is LaunchReply) { - final percent = reply.launchProgress.percentComplete; - if (!['0', '100', '-1'].contains(percent)) return; - } - logger.i('${request.repr} received ${reply.repr}'); - case NotificationKind.error: - final es = notification.errorAndStackTraceOrNull; - logger.e( - '${request.repr} received an error', - error: es?.error, - stackTrace: es?.stackTrace, - ); - case NotificationKind.done: - logger.i('${request.repr} is done'); - } - }; -} - -class GrpcClient { - final RpcClient _client; - - GrpcClient(this._client); - - Stream?> launch( - LaunchRequest request, { - List mountRequests = const [], - Future? cancel, - }) async* { - logger.i('Sent ${request.repr}'); - final launchStream = _client.launch(Stream.value(request)); - cancel?.then((_) => launchStream.cancel()); - yield* launchStream - .doOnData(checkForUpdate) - .doOnEach(logGrpc(request)) - .map(Either.left); - for (final mountRequest in mountRequests) { - logger.i('Sent ${mountRequest.repr}'); - yield* _client - .mount(Stream.value(mountRequest)) - .doOnEach(logGrpc(mountRequest)) - .map(Either.right); - } - } - - Future start(Iterable names) { - final request = StartRequest( - instanceNames: InstanceNames(instanceName: names), - ); - logger.i('Sent ${request.repr}'); - return _client - .start(Stream.value(request)) - .doOnData(checkForUpdate) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future stop(Iterable names) { - final request = StopRequest( - instanceNames: InstanceNames(instanceName: names), - ); - logger.i('Sent ${request.repr}'); - return _client - .stop(Stream.value(request)) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future suspend(Iterable names) { - final request = SuspendRequest( - instanceNames: InstanceNames(instanceName: names), - ); - logger.i('Sent ${request.repr}'); - return _client - .suspend(Stream.value(request)) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future restart(Iterable names) { - final request = RestartRequest( - instanceNames: InstanceNames(instanceName: names), - ); - logger.i('Sent ${request.repr}'); - return _client - .restart(Stream.value(request)) - .doOnData(checkForUpdate) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future delete(Iterable names) { - final request = DeleteRequest( - instanceSnapshotPairs: names.map( - (name) => InstanceSnapshotPair(instanceName: name), - ), - ); - logger.i('Sent ${request.repr}'); - return _client - .delet(Stream.value(request)) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future recover(Iterable names) { - final request = RecoverRequest( - instanceNames: InstanceNames(instanceName: names), - ); - logger.i('Sent ${request.repr}'); - return _client - .recover(Stream.value(request)) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future purge(Iterable names) { - final request = DeleteRequest( - instanceSnapshotPairs: names.map( - (name) => InstanceSnapshotPair(instanceName: name), - ), - purge: true, - ); - logger.i('Sent ${request.repr}'); - return _client - .delet(Stream.value(request)) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future> info([Iterable names = const []]) { - final request = InfoRequest( - instanceSnapshotPairs: names.map( - (name) => InstanceSnapshotPair(instanceName: name), - ), - ); - return _client - .info(Stream.value(request)) - .doOnData(checkForUpdate) - .last - .then((r) => r.details.toList()); - } - - Future mount(MountRequest request) { - logger.i('Sent ${request.repr}'); - return _client - .mount(Stream.value(request)) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future umount(String name, [String? path]) { - final request = UmountRequest( - targetPaths: [TargetPathInfo(instanceName: name, targetPath: path)], - ); - logger.i('Sent ${request.repr}'); - return _client - .umount(Stream.value(request)) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future find({bool images = true, bool blueprints = true}) { - final request = FindRequest( - showImages: images, - showBlueprints: blueprints, - ); - logger.i('Sent ${request.repr}'); - return _client.find(Stream.value(request)).doOnEach(logGrpc(request)).last; - } - - Future> networks() { - final request = NetworksRequest(); - logger.i('Sent ${request.repr}'); - return _client - .networks(Stream.value(request)) - .doOnData(checkForUpdate) - .doOnEach(logGrpc(request)) - .last - .then((r) => r.interfaces); - } - - Future version() { - final request = VersionRequest(); - logger.i('Sent ${request.repr}'); - return _client - .version(Stream.value(request)) - .doOnData(checkForUpdate) - .doOnEach(logGrpc(request)) - .last - .then((reply) => reply.version); - } - - Future get(String key) { - final request = GetRequest(key: key); - logger.i('Sent ${request.repr}'); - return _client - .get(Stream.value(request)) - .doOnEach(logGrpc(request)) - .last - .then((reply) => reply.value); - } - - Future set(String key, String value) { - final request = SetRequest(key: key, val: value); - logger.i('Sent ${request.repr}'); - return _client - .set(Stream.value(request)) - .doOnEach(logGrpc(request)) - .firstOrNull; - } - - Future sshInfo(String name) { - final request = SSHInfoRequest(instanceName: [name]); - logger.i('Sent ${request.repr}'); - return _client - .ssh_info(Stream.value(request)) - .doOnEach(logGrpc(request)) - .first - .then((reply) => reply.sshInfo[name]); - } - - Future daemonInfo() { - final request = DaemonInfoRequest(); - logger.i('Sent ${request.repr}'); - return _client - .daemon_info(Stream.value(request)) - .doOnEach(logGrpc(request)) - .last; - } -} - -class CustomChannelCredentials extends ChannelCredentials { - final List certificateChain; - final List certificateKey; - - CustomChannelCredentials({ - super.authority, - required List certificate, - required this.certificateKey, - }) : certificateChain = certificate, - super.secure( - certificates: certificate, - onBadCertificate: allowBadCertificates, - ); - - @override - SecurityContext get securityContext { - final ctx = super.securityContext!; - ctx.useCertificateChainBytes(certificateChain); - ctx.usePrivateKeyBytes(certificateKey); - return ctx; - } -} +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:grpc/grpc.dart'; +import 'package:protobuf/protobuf.dart' hide RpcClient; +import 'package:rxdart/rxdart.dart'; + +import 'logger.dart'; +import 'providers.dart'; +import 'update_available.dart'; + +export 'generated/multipass.pbgrpc.dart'; + +typedef Status = InstanceStatus_Status; +typedef VmInfo = DetailedInfoItem; +typedef ImageInfo = FindReply_ImageInfo; +typedef MountPaths = MountInfo_MountPaths; +typedef RpcMessage = GeneratedMessage; + +extension on RpcMessage { + String get repr => '$runtimeType${toProto3Json()}'; +} + +void checkForUpdate(RpcMessage message) { + final updateInfo = switch (message) { + LaunchReply launchReply => launchReply.updateInfo, + InfoReply infoReply => infoReply.updateInfo, + ListReply listReply => listReply.updateInfo, + NetworksReply networksReply => networksReply.updateInfo, + StartReply startReply => startReply.updateInfo, + RestartReply restartReply => restartReply.updateInfo, + VersionReply versionReply => versionReply.updateInfo, + _ => UpdateInfo(), + }; + + providerContainer.read(updateProvider.notifier).set(updateInfo); +} + +void Function(StreamNotification) logGrpc(RpcMessage request) { + return (notification) { + switch (notification.kind) { + case NotificationKind.data: + final reply = notification.requireDataValue.deepCopy(); + if (reply is SSHInfoReply) { + for (final info in reply.sshInfo.values) { + info.privKeyBase64 = '*hidden*'; + } + } + if (reply is LaunchReply) { + final percent = reply.launchProgress.percentComplete; + if (!['0', '100', '-1'].contains(percent)) return; + } + logger.i('${request.repr} received ${reply.repr}'); + case NotificationKind.error: + final es = notification.errorAndStackTraceOrNull; + logger.e( + '${request.repr} received an error', + error: es?.error, + stackTrace: es?.stackTrace, + ); + case NotificationKind.done: + logger.i('${request.repr} is done'); + } + }; +} + +class GrpcClient { + final RpcClient _client; + + GrpcClient(this._client); + + Stream?> launch( + LaunchRequest request, { + List mountRequests = const [], + Future? cancel, + }) async* { + logger.i('Sent ${request.repr}'); + final launchStream = _client.launch(Stream.value(request)); + cancel?.then((_) => launchStream.cancel()); + yield* launchStream + .doOnData(checkForUpdate) + .doOnEach(logGrpc(request)) + .map(Either.left); + for (final mountRequest in mountRequests) { + logger.i('Sent ${mountRequest.repr}'); + yield* _client + .mount(Stream.value(mountRequest)) + .doOnEach(logGrpc(mountRequest)) + .map(Either.right); + } + } + + Future start(Iterable names) { + final request = StartRequest( + instanceNames: InstanceNames(instanceName: names), + ); + logger.i('Sent ${request.repr}'); + return _client + .start(Stream.value(request)) + .doOnData(checkForUpdate) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future stop(Iterable names) { + final request = StopRequest( + instanceNames: InstanceNames(instanceName: names), + ); + logger.i('Sent ${request.repr}'); + return _client + .stop(Stream.value(request)) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future suspend(Iterable names) { + final request = SuspendRequest( + instanceNames: InstanceNames(instanceName: names), + ); + logger.i('Sent ${request.repr}'); + return _client + .suspend(Stream.value(request)) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future restart(Iterable names) { + final request = RestartRequest( + instanceNames: InstanceNames(instanceName: names), + ); + logger.i('Sent ${request.repr}'); + return _client + .restart(Stream.value(request)) + .doOnData(checkForUpdate) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future delete(Iterable names) { + final request = DeleteRequest( + instanceSnapshotPairs: names.map( + (name) => InstanceSnapshotPair(instanceName: name), + ), + ); + logger.i('Sent ${request.repr}'); + return _client + .delet(Stream.value(request)) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future recover(Iterable names) { + final request = RecoverRequest( + instanceNames: InstanceNames(instanceName: names), + ); + logger.i('Sent ${request.repr}'); + return _client + .recover(Stream.value(request)) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future purge(Iterable names) { + final request = DeleteRequest( + instanceSnapshotPairs: names.map( + (name) => InstanceSnapshotPair(instanceName: name), + ), + purge: true, + ); + logger.i('Sent ${request.repr}'); + return _client + .delet(Stream.value(request)) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future> info([Iterable names = const []]) { + final request = InfoRequest( + instanceSnapshotPairs: names.map( + (name) => InstanceSnapshotPair(instanceName: name), + ), + ); + return _client + .info(Stream.value(request)) + .doOnData(checkForUpdate) + .last + .then((r) => r.details.toList()); + } + + Future mount(MountRequest request) { + logger.i('Sent ${request.repr}'); + return _client + .mount(Stream.value(request)) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future umount(String name, [String? path]) { + final request = UmountRequest( + targetPaths: [TargetPathInfo(instanceName: name, targetPath: path)], + ); + logger.i('Sent ${request.repr}'); + return _client + .umount(Stream.value(request)) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future find({bool images = true, bool blueprints = true}) { + final request = FindRequest( + showImages: images, + showBlueprints: blueprints, + ); + logger.i('Sent ${request.repr}'); + return _client.find(Stream.value(request)).doOnEach(logGrpc(request)).last; + } + + Future> networks() { + final request = NetworksRequest(); + logger.i('Sent ${request.repr}'); + return _client + .networks(Stream.value(request)) + .doOnData(checkForUpdate) + .doOnEach(logGrpc(request)) + .last + .then((r) => r.interfaces); + } + + Future version() { + final request = VersionRequest(); + logger.i('Sent ${request.repr}'); + return _client + .version(Stream.value(request)) + .doOnData(checkForUpdate) + .doOnEach(logGrpc(request)) + .last + .then((reply) => reply.version); + } + + Future get(String key) { + final request = GetRequest(key: key); + logger.i('Sent ${request.repr}'); + return _client + .get(Stream.value(request)) + .doOnEach(logGrpc(request)) + .last + .then((reply) => reply.value); + } + + Future set(String key, String value) { + final request = SetRequest(key: key, val: value); + logger.i('Sent ${request.repr}'); + return _client + .set(Stream.value(request)) + .doOnEach(logGrpc(request)) + .firstOrNull; + } + + Future sshInfo(String name) { + final request = SSHInfoRequest(instanceName: [name]); + logger.i('Sent ${request.repr}'); + return _client + .ssh_info(Stream.value(request)) + .doOnEach(logGrpc(request)) + .first + .then((reply) => reply.sshInfo[name]); + } + + Future daemonInfo() { + final request = DaemonInfoRequest(); + logger.i('Sent ${request.repr}'); + return _client + .daemon_info(Stream.value(request)) + .doOnEach(logGrpc(request)) + .last; + } +} + +class CustomChannelCredentials extends ChannelCredentials { + final List certificateChain; + final List certificateKey; + + CustomChannelCredentials({ + super.authority, + required List certificate, + required this.certificateKey, + }) : certificateChain = certificate, + super.secure( + certificates: certificate, + onBadCertificate: allowBadCertificates, + ); + + @override + SecurityContext get securityContext { + final ctx = super.securityContext!; + ctx.useCertificateChainBytes(certificateChain); + ctx.usePrivateKeyBytes(certificateKey); + return ctx; + } +} diff --git a/src/platform/platform_shared.h b/src/platform/platform_shared.h index 4c2c73661fe..f592415ad95 100644 --- a/src/platform/platform_shared.h +++ b/src/platform/platform_shared.h @@ -1,39 +1,39 @@ -/* - * Copyright (C) Canonical, Ltd. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#ifndef MULTIPASS_PLATFORM_SHARED_H -#define MULTIPASS_PLATFORM_SHARED_H - -#include -#include - -namespace multipass::platform -{ -const std::unordered_set supported_snapcraft_aliases{ - "core18", - "18.04", - "core20", - "20.04", - "core22", - "22.04", - "core24", - "24.04", - "devel", -}; -} // namespace multipass::platform - -#endif // MULTIPASS_PLATFORM_SHARED_H +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_PLATFORM_SHARED_H +#define MULTIPASS_PLATFORM_SHARED_H + +#include +#include + +namespace multipass::platform +{ +const std::unordered_set supported_snapcraft_aliases{ + "core18", + "18.04", + "core20", + "20.04", + "core22", + "22.04", + "core24", + "24.04", + "devel", +}; +} // namespace multipass::platform + +#endif // MULTIPASS_PLATFORM_SHARED_H diff --git a/tests/test_global_settings_handlers.cpp b/tests/test_global_settings_handlers.cpp index 8f4134fdf28..ec329296a33 100644 --- a/tests/test_global_settings_handlers.cpp +++ b/tests/test_global_settings_handlers.cpp @@ -1,361 +1,361 @@ -/* - * Copyright (C) Canonical, Ltd. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "common.h" -#include "mock_platform.h" -#include "mock_qsettings.h" -#include "mock_settings.h" -#include "mock_standard_paths.h" -#include "mock_utils.h" - -#include -#include -#include -#include - -#include - -#include - -namespace mp = multipass; -namespace mpt = mp::test; -using namespace testing; - -namespace -{ -struct TestGlobalSettingsHandlers : public Test -{ - void SetUp() override - { - ON_CALL(mock_platform, default_privileged_mounts).WillByDefault(Return("true")); - ON_CALL(mock_platform, is_backend_supported).WillByDefault(Return(true)); - - EXPECT_CALL(mock_settings, - register_handler(Pointer(WhenDynamicCastTo(NotNull())))) - .WillOnce([this](auto uptr) { - handler = std::move(uptr); - return handler.get(); - }); - } - - void inject_mock_qsettings() // moves the mock, so call once only, after setting expectations - { - EXPECT_CALL(*mock_qsettings, fileName) - .WillRepeatedly(Return(QDir::temp().absoluteFilePath("missing_file.conf"))); - EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(_, Eq(QSettings::IniFormat))) - .WillOnce(Return(ByMove(std::move(mock_qsettings)))); - } - - void inject_default_returning_mock_qsettings() - { - EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings) - .WillRepeatedly(WithArg<0>(Invoke(make_default_returning_mock_qsettings))); - } - - void expect_setting_values(const std::map& setting_values) - { - for (const auto& [k, v] : setting_values) - { - EXPECT_EQ(handler->get(k), v); - } - } - - template - void assert_unrecognized_keys(Ts... keys) - { - for (const char* key : {keys...}) - { - MP_ASSERT_THROW_THAT(handler->get(key), mp::UnrecognizedSettingException, mpt::match_what(HasSubstr(key))); - } - } - - static std::unique_ptr make_default_returning_mock_qsettings(const QString& filename) - { - auto mock_qsettings = std::make_unique>(); - EXPECT_CALL(*mock_qsettings, value_impl).WillRepeatedly(ReturnArg<1>()); - EXPECT_CALL(*mock_qsettings, fileName).WillRepeatedly(Return(filename)); - - return mock_qsettings; - } - - static mp::SettingSpec::Set to_setting_set(const std::map& setting_defaults) - { - mp::SettingSpec::Set ret; - for (const auto& [k, v] : setting_defaults) - ret.insert(std::make_unique(k, v)); - - return ret; - } - -public: - mpt::MockQSettingsProvider::GuardedMock mock_qsettings_injection = - mpt::MockQSettingsProvider::inject(); /* strict to ensure that, other than explicitly injected, no - QSettings are used */ - mpt::MockQSettingsProvider* mock_qsettings_provider = mock_qsettings_injection.first; - - std::unique_ptr> mock_qsettings = std::make_unique>(); - - mpt::MockSettings::GuardedMock mock_settings_injection = mpt::MockSettings::inject(); - mpt::MockSettings& mock_settings = *mock_settings_injection.first; - - mpt::MockPlatform::GuardedMock mock_platform_injection = mpt::MockPlatform::inject(); - mpt::MockPlatform& mock_platform = *mock_platform_injection.first; - - std::unique_ptr handler = nullptr; -}; - -TEST_F(TestGlobalSettingsHandlers, clientsRegisterPersistentHandlerWithClientFilename) -{ - auto config_location = QStringLiteral("/a/b/c"); - auto expected_filename = config_location + "/multipass/multipass.conf"; - - EXPECT_CALL(mpt::MockStandardPaths::mock_instance(), writableLocation(mp::StandardPaths::GenericConfigLocation)) - .WillOnce(Return(config_location)); - - mp::client::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(Eq(expected_filename), _)) - .WillOnce(WithArg<0>(Invoke(make_default_returning_mock_qsettings))); - handler->set(mp::petenv_key, "goo"); -} - -TEST_F(TestGlobalSettingsHandlers, clientsRegisterPersistentHandlerForClientSettings) -{ - mp::client::register_global_settings_handlers(); - - inject_default_returning_mock_qsettings(); - - expect_setting_values({{mp::petenv_key, "primary"}}); -} - -TEST_F(TestGlobalSettingsHandlers, clientsRegisterPersistentHandlerWithOverridingPlatformSettings) -{ - const auto platform_defaults = std::map{{"client.a.setting", "a reasonably long value for this"}, - {mp::petenv_key, "secondary"}, - {"client.empty.setting", ""}, - {"client.an.int", "-12345"}, - {"client.a.float.with.a.long_key", "3.14"}}; - - EXPECT_CALL(mock_platform, extra_client_settings).WillOnce(Return(ByMove(to_setting_set(platform_defaults)))); - mp::client::register_global_settings_handlers(); - inject_default_returning_mock_qsettings(); - - expect_setting_values(platform_defaults); -} - -TEST_F(TestGlobalSettingsHandlers, clientsDoNotRegisterPersistentHandlerForDaemonSettings) -{ - mp::client::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(_, _)).Times(0); - assert_unrecognized_keys(mp::driver_key, mp::bridged_interface_key, mp::mounts_key, mp::passphrase_key); -} - -struct TestGoodPetEnvSetting : public TestGlobalSettingsHandlers, WithParamInterface -{ -}; - -TEST_P(TestGoodPetEnvSetting, clientsRegisterHandlerThatAcceptsValidPetenv) -{ - auto key = mp::petenv_key, val = GetParam(); - mp::client::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings, setValue(Eq(key), Eq(val))); - inject_mock_qsettings(); - - ASSERT_NO_THROW(handler->set(key, val)); -} - -INSTANTIATE_TEST_SUITE_P(TestGoodPetEnvSetting, TestGoodPetEnvSetting, Values("valid-primary", "")); - -struct TestBadPetEnvSetting : public TestGlobalSettingsHandlers, WithParamInterface -{ -}; - -TEST_P(TestBadPetEnvSetting, clientsRegisterHandlerThatRejectsInvalidPetenv) -{ - auto key = mp::petenv_key, val = GetParam(); - mp::client::register_global_settings_handlers(); - - MP_ASSERT_THROW_THAT(handler->set(key, val), - mp::InvalidSettingException, - mpt::match_what(AllOf(HasSubstr(key), HasSubstr(val)))); -} - -INSTANTIATE_TEST_SUITE_P(TestBadPetEnvSetting, TestBadPetEnvSetting, Values("-", "-a-b-", "_asd", "_1", "1-2-3")); - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersPersistentHandlerWithDaemonFilename) -{ - auto config_location = QStringLiteral("/a/b/c"); - auto expected_filename = config_location + "/multipassd.conf"; - - EXPECT_CALL(mock_platform, daemon_config_home).WillOnce(Return(config_location)); - - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(Eq(expected_filename), _)) - .WillOnce(WithArg<0>(Invoke(make_default_returning_mock_qsettings))); - handler->set(mp::bridged_interface_key, "bridge"); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersPersistentHandlerForDaemonSettings) -{ - const auto driver = "conductor"; - const auto mount = "false"; - - EXPECT_CALL(mock_platform, default_driver).WillOnce(Return(driver)); - EXPECT_CALL(mock_platform, default_privileged_mounts).WillOnce(Return(mount)); - - mp::daemon::register_global_settings_handlers(); - inject_default_returning_mock_qsettings(); - - expect_setting_values({{mp::driver_key, driver}, {mp::bridged_interface_key, ""}, {mp::mounts_key, mount}}); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersPersistentHandlerForDaemonPlatformSettings) -{ - const auto platform_defaults = std::map{{"local.blah", "blargh"}, - {mp::driver_key, "platform-hypervisor"}, - {"local.a.bool", "false"}, - {mp::bridged_interface_key, "platform-bridge"}, - {"local.foo", "barrrr"}, - {mp::mounts_key, "false"}, - {"local.a.long.number", "1234567890"}}; - - EXPECT_CALL(mock_platform, default_driver).WillOnce(Return("unused")); - EXPECT_CALL(mock_platform, default_privileged_mounts).WillOnce(Return("true")); - EXPECT_CALL(mock_platform, extra_daemon_settings).WillOnce(Return(ByMove(to_setting_set(platform_defaults)))); - - mp::daemon::register_global_settings_handlers(); - inject_default_returning_mock_qsettings(); - - expect_setting_values(platform_defaults); -} - -TEST_F(TestGlobalSettingsHandlers, daemonDoesNotRegisterPersistentHandlerForClientSettings) -{ - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(_, _)).Times(0); - assert_unrecognized_keys(mp::petenv_key, mp::winterm_key); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatAcceptsValidBackend) -{ - auto key = mp::driver_key, val = "good driver"; - - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(mock_platform, is_backend_supported(Eq(val))).WillOnce(Return(true)); - EXPECT_CALL(*mock_qsettings, setValue(Eq(key), Eq(val))); - inject_mock_qsettings(); - - ASSERT_NO_THROW(handler->set(key, val)); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatTransformsHyperVDriver) -{ - const auto key = mp::driver_key; - const auto val = "hyper-v"; - const auto transformed_val = "hyperv"; - - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings, setValue(Eq(key), Eq(transformed_val))).Times(1); - inject_mock_qsettings(); - - ASSERT_NO_THROW(handler->set(key, val)); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatTransformsVBoxDriver) -{ - const auto key = mp::driver_key; - const auto val = "vbox"; - const auto transformed_val = "virtualbox"; - - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings, setValue(Eq(key), Eq(transformed_val))).Times(1); - inject_mock_qsettings(); - - ASSERT_NO_THROW(handler->set(key, val)); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatRejectsInvalidBackend) -{ - auto key = mp::driver_key, val = "bad driver"; - - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(mock_platform, is_backend_supported(Eq(val))).WillOnce(Return(false)); - - MP_ASSERT_THROW_THAT(handler->set(key, val), - mp::InvalidSettingException, - mpt::match_what(AllOf(HasSubstr(key), HasSubstr(val)))); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatAcceptsBoolMounts) -{ - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings, setValue(Eq(mp::mounts_key), Eq("true"))); - inject_mock_qsettings(); - - ASSERT_NO_THROW(handler->set(mp::mounts_key, "1")); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatAcceptsBrigedInterface) -{ - const auto val = "bridge"; - - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings, setValue(Eq(mp::bridged_interface_key), Eq(val))); - inject_mock_qsettings(); - - ASSERT_NO_THROW(handler->set(mp::bridged_interface_key, val)); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatHashesNonEmptyPassword) -{ - const auto val = "correct horse battery staple"; - const auto hash = "xkcd"; - - auto [mock_utils, guard] = mpt::MockUtils::inject(); - EXPECT_CALL(*mock_utils, generate_scrypt_hash_for(Eq(val))).WillOnce(Return(hash)); - - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings, setValue(Eq(mp::passphrase_key), Eq(hash))); - inject_mock_qsettings(); - - ASSERT_NO_THROW(handler->set(mp::passphrase_key, val)); -} - -TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatResetsHashWhenPasswordIsEmpty) -{ - const auto val = ""; - - mp::daemon::register_global_settings_handlers(); - - EXPECT_CALL(*mock_qsettings, setValue(Eq(mp::passphrase_key), Eq(val))); - inject_mock_qsettings(); - - ASSERT_NO_THROW(handler->set(mp::passphrase_key, val)); -} - -} // namespace +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "common.h" +#include "mock_platform.h" +#include "mock_qsettings.h" +#include "mock_settings.h" +#include "mock_standard_paths.h" +#include "mock_utils.h" + +#include +#include +#include +#include + +#include + +#include + +namespace mp = multipass; +namespace mpt = mp::test; +using namespace testing; + +namespace +{ +struct TestGlobalSettingsHandlers : public Test +{ + void SetUp() override + { + ON_CALL(mock_platform, default_privileged_mounts).WillByDefault(Return("true")); + ON_CALL(mock_platform, is_backend_supported).WillByDefault(Return(true)); + + EXPECT_CALL(mock_settings, + register_handler(Pointer(WhenDynamicCastTo(NotNull())))) + .WillOnce([this](auto uptr) { + handler = std::move(uptr); + return handler.get(); + }); + } + + void inject_mock_qsettings() // moves the mock, so call once only, after setting expectations + { + EXPECT_CALL(*mock_qsettings, fileName) + .WillRepeatedly(Return(QDir::temp().absoluteFilePath("missing_file.conf"))); + EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(_, Eq(QSettings::IniFormat))) + .WillOnce(Return(ByMove(std::move(mock_qsettings)))); + } + + void inject_default_returning_mock_qsettings() + { + EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings) + .WillRepeatedly(WithArg<0>(Invoke(make_default_returning_mock_qsettings))); + } + + void expect_setting_values(const std::map& setting_values) + { + for (const auto& [k, v] : setting_values) + { + EXPECT_EQ(handler->get(k), v); + } + } + + template + void assert_unrecognized_keys(Ts... keys) + { + for (const char* key : {keys...}) + { + MP_ASSERT_THROW_THAT(handler->get(key), mp::UnrecognizedSettingException, mpt::match_what(HasSubstr(key))); + } + } + + static std::unique_ptr make_default_returning_mock_qsettings(const QString& filename) + { + auto mock_qsettings = std::make_unique>(); + EXPECT_CALL(*mock_qsettings, value_impl).WillRepeatedly(ReturnArg<1>()); + EXPECT_CALL(*mock_qsettings, fileName).WillRepeatedly(Return(filename)); + + return mock_qsettings; + } + + static mp::SettingSpec::Set to_setting_set(const std::map& setting_defaults) + { + mp::SettingSpec::Set ret; + for (const auto& [k, v] : setting_defaults) + ret.insert(std::make_unique(k, v)); + + return ret; + } + +public: + mpt::MockQSettingsProvider::GuardedMock mock_qsettings_injection = + mpt::MockQSettingsProvider::inject(); /* strict to ensure that, other than explicitly injected, no + QSettings are used */ + mpt::MockQSettingsProvider* mock_qsettings_provider = mock_qsettings_injection.first; + + std::unique_ptr> mock_qsettings = std::make_unique>(); + + mpt::MockSettings::GuardedMock mock_settings_injection = mpt::MockSettings::inject(); + mpt::MockSettings& mock_settings = *mock_settings_injection.first; + + mpt::MockPlatform::GuardedMock mock_platform_injection = mpt::MockPlatform::inject(); + mpt::MockPlatform& mock_platform = *mock_platform_injection.first; + + std::unique_ptr handler = nullptr; +}; + +TEST_F(TestGlobalSettingsHandlers, clientsRegisterPersistentHandlerWithClientFilename) +{ + auto config_location = QStringLiteral("/a/b/c"); + auto expected_filename = config_location + "/multipass/multipass.conf"; + + EXPECT_CALL(mpt::MockStandardPaths::mock_instance(), writableLocation(mp::StandardPaths::GenericConfigLocation)) + .WillOnce(Return(config_location)); + + mp::client::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(Eq(expected_filename), _)) + .WillOnce(WithArg<0>(Invoke(make_default_returning_mock_qsettings))); + handler->set(mp::petenv_key, "goo"); +} + +TEST_F(TestGlobalSettingsHandlers, clientsRegisterPersistentHandlerForClientSettings) +{ + mp::client::register_global_settings_handlers(); + + inject_default_returning_mock_qsettings(); + + expect_setting_values({{mp::petenv_key, "primary"}}); +} + +TEST_F(TestGlobalSettingsHandlers, clientsRegisterPersistentHandlerWithOverridingPlatformSettings) +{ + const auto platform_defaults = std::map{{"client.a.setting", "a reasonably long value for this"}, + {mp::petenv_key, "secondary"}, + {"client.empty.setting", ""}, + {"client.an.int", "-12345"}, + {"client.a.float.with.a.long_key", "3.14"}}; + + EXPECT_CALL(mock_platform, extra_client_settings).WillOnce(Return(ByMove(to_setting_set(platform_defaults)))); + mp::client::register_global_settings_handlers(); + inject_default_returning_mock_qsettings(); + + expect_setting_values(platform_defaults); +} + +TEST_F(TestGlobalSettingsHandlers, clientsDoNotRegisterPersistentHandlerForDaemonSettings) +{ + mp::client::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(_, _)).Times(0); + assert_unrecognized_keys(mp::driver_key, mp::bridged_interface_key, mp::mounts_key, mp::passphrase_key); +} + +struct TestGoodPetEnvSetting : public TestGlobalSettingsHandlers, WithParamInterface +{ +}; + +TEST_P(TestGoodPetEnvSetting, clientsRegisterHandlerThatAcceptsValidPetenv) +{ + auto key = mp::petenv_key, val = GetParam(); + mp::client::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings, setValue(Eq(key), Eq(val))); + inject_mock_qsettings(); + + ASSERT_NO_THROW(handler->set(key, val)); +} + +INSTANTIATE_TEST_SUITE_P(TestGoodPetEnvSetting, TestGoodPetEnvSetting, Values("valid-primary", "")); + +struct TestBadPetEnvSetting : public TestGlobalSettingsHandlers, WithParamInterface +{ +}; + +TEST_P(TestBadPetEnvSetting, clientsRegisterHandlerThatRejectsInvalidPetenv) +{ + auto key = mp::petenv_key, val = GetParam(); + mp::client::register_global_settings_handlers(); + + MP_ASSERT_THROW_THAT(handler->set(key, val), + mp::InvalidSettingException, + mpt::match_what(AllOf(HasSubstr(key), HasSubstr(val)))); +} + +INSTANTIATE_TEST_SUITE_P(TestBadPetEnvSetting, TestBadPetEnvSetting, Values("-", "-a-b-", "_asd", "_1", "1-2-3")); + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersPersistentHandlerWithDaemonFilename) +{ + auto config_location = QStringLiteral("/a/b/c"); + auto expected_filename = config_location + "/multipassd.conf"; + + EXPECT_CALL(mock_platform, daemon_config_home).WillOnce(Return(config_location)); + + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(Eq(expected_filename), _)) + .WillOnce(WithArg<0>(Invoke(make_default_returning_mock_qsettings))); + handler->set(mp::bridged_interface_key, "bridge"); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersPersistentHandlerForDaemonSettings) +{ + const auto driver = "conductor"; + const auto mount = "false"; + + EXPECT_CALL(mock_platform, default_driver).WillOnce(Return(driver)); + EXPECT_CALL(mock_platform, default_privileged_mounts).WillOnce(Return(mount)); + + mp::daemon::register_global_settings_handlers(); + inject_default_returning_mock_qsettings(); + + expect_setting_values({{mp::driver_key, driver}, {mp::bridged_interface_key, ""}, {mp::mounts_key, mount}}); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersPersistentHandlerForDaemonPlatformSettings) +{ + const auto platform_defaults = std::map{{"local.blah", "blargh"}, + {mp::driver_key, "platform-hypervisor"}, + {"local.a.bool", "false"}, + {mp::bridged_interface_key, "platform-bridge"}, + {"local.foo", "barrrr"}, + {mp::mounts_key, "false"}, + {"local.a.long.number", "1234567890"}}; + + EXPECT_CALL(mock_platform, default_driver).WillOnce(Return("unused")); + EXPECT_CALL(mock_platform, default_privileged_mounts).WillOnce(Return("true")); + EXPECT_CALL(mock_platform, extra_daemon_settings).WillOnce(Return(ByMove(to_setting_set(platform_defaults)))); + + mp::daemon::register_global_settings_handlers(); + inject_default_returning_mock_qsettings(); + + expect_setting_values(platform_defaults); +} + +TEST_F(TestGlobalSettingsHandlers, daemonDoesNotRegisterPersistentHandlerForClientSettings) +{ + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings_provider, make_wrapped_qsettings(_, _)).Times(0); + assert_unrecognized_keys(mp::petenv_key, mp::winterm_key); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatAcceptsValidBackend) +{ + auto key = mp::driver_key, val = "good driver"; + + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(mock_platform, is_backend_supported(Eq(val))).WillOnce(Return(true)); + EXPECT_CALL(*mock_qsettings, setValue(Eq(key), Eq(val))); + inject_mock_qsettings(); + + ASSERT_NO_THROW(handler->set(key, val)); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatTransformsHyperVDriver) +{ + const auto key = mp::driver_key; + const auto val = "hyper-v"; + const auto transformed_val = "hyperv"; + + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings, setValue(Eq(key), Eq(transformed_val))).Times(1); + inject_mock_qsettings(); + + ASSERT_NO_THROW(handler->set(key, val)); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatTransformsVBoxDriver) +{ + const auto key = mp::driver_key; + const auto val = "vbox"; + const auto transformed_val = "virtualbox"; + + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings, setValue(Eq(key), Eq(transformed_val))).Times(1); + inject_mock_qsettings(); + + ASSERT_NO_THROW(handler->set(key, val)); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatRejectsInvalidBackend) +{ + auto key = mp::driver_key, val = "bad driver"; + + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(mock_platform, is_backend_supported(Eq(val))).WillOnce(Return(false)); + + MP_ASSERT_THROW_THAT(handler->set(key, val), + mp::InvalidSettingException, + mpt::match_what(AllOf(HasSubstr(key), HasSubstr(val)))); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatAcceptsBoolMounts) +{ + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings, setValue(Eq(mp::mounts_key), Eq("true"))); + inject_mock_qsettings(); + + ASSERT_NO_THROW(handler->set(mp::mounts_key, "1")); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatAcceptsBrigedInterface) +{ + const auto val = "bridge"; + + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings, setValue(Eq(mp::bridged_interface_key), Eq(val))); + inject_mock_qsettings(); + + ASSERT_NO_THROW(handler->set(mp::bridged_interface_key, val)); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatHashesNonEmptyPassword) +{ + const auto val = "correct horse battery staple"; + const auto hash = "xkcd"; + + auto [mock_utils, guard] = mpt::MockUtils::inject(); + EXPECT_CALL(*mock_utils, generate_scrypt_hash_for(Eq(val))).WillOnce(Return(hash)); + + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings, setValue(Eq(mp::passphrase_key), Eq(hash))); + inject_mock_qsettings(); + + ASSERT_NO_THROW(handler->set(mp::passphrase_key, val)); +} + +TEST_F(TestGlobalSettingsHandlers, daemonRegistersHandlerThatResetsHashWhenPasswordIsEmpty) +{ + const auto val = ""; + + mp::daemon::register_global_settings_handlers(); + + EXPECT_CALL(*mock_qsettings, setValue(Eq(mp::passphrase_key), Eq(val))); + inject_mock_qsettings(); + + ASSERT_NO_THROW(handler->set(mp::passphrase_key, val)); +} + +} // namespace