From afc7d57ba2a282159d9e906ff01a46e2d45c3699 Mon Sep 17 00:00:00 2001 From: operate-services-sdk-bot Date: Thu, 9 Feb 2023 18:11:04 +0000 Subject: [PATCH] Release v1.0.0-beta.2 --- CHANGELOG.md | 21 +- License.md | 4 +- README.md | 2 +- .../Deploy/CloudCodeDeploymentServiceTests.cs | 33 +-- ...ity.Services.Cli.CloudCode.UnitTest.csproj | 2 +- .../Deploy/CloudCodeDeploymentService.cs | 28 +-- .../Deploy/CloudCodeScriptsLoader.cs | 13 +- .../Deploy/ICloudCodeScriptsLoader.cs | 2 +- .../Unity.Services.Cli.CloudCode.csproj | 5 +- .../CommonModuleTests.cs | 18 +- .../Exceptions/ExceptionHelperTests.cs | 13 - .../Features/FeaturesFactoryTests.cs | 67 ----- .../Features/StaticFeaturesTests.cs | 47 ---- .../Middleware/ContextBinderTests.cs | 8 + .../Telemetry/DiagnosticsTests.cs | 2 +- .../Telemetry/MetricsUtilsTests.cs | 25 ++ .../Unity.Services.Cli.Common.UnitTest.csproj | 2 +- .../Unity.Services.Cli.Common/CommonModule.cs | 28 ++- .../Exceptions/ExceptionHelper.cs | 24 +- .../Features/AlwaysEnabledFeatures.cs | 6 - .../Features/FeaturesFactory.cs | 46 ---- .../Features/IFeatures.cs | 6 - .../Features/StaticFeatures.cs | 18 -- .../Logging/JsonLogMessage.cs | 1 + .../Middleware/ContextBinder.cs | 14 +- .../Telemetry/AnalyticEvent/AnalyticEvent.cs | 46 ++++ .../AnalyticEventFactory.cs | 23 ++ .../IAnalyticEventFactory.cs | 10 + .../AnalyticEvent/AnalyticEventUtils.cs | 19 ++ .../Telemetry/AnalyticEvent/IAnalyticEvent.cs | 9 + .../Telemetry/TagKeys.cs | 4 + .../Unity.Services.Cli.Common.csproj | 6 +- .../Handlers/DeployHandlerTests.cs | 52 +++- .../Handlers/FetchHandlerTests.cs | 91 +++++++ .../Handlers/NewFileHandlerTests.cs | 51 ++++ .../Model/FetchResultTests.cs | 74 ++++++ .../Service/DeployFileServiceTests.cs | 86 ++++++- .../Unity.Services.Cli.Deploy.UnitTest.csproj | 2 +- .../Unity.Services.Cli.Deploy/DeployModule.cs | 9 +- .../Unity.Services.Cli.Deploy/FetchModule.cs | 45 ++++ .../Handlers/DeployHandler.cs | 19 +- .../Handlers/FetchHandler.cs | 82 +++++++ .../Handlers/NewFileHandler.cs | 38 +++ .../Input/DeployInput.cs | 5 +- .../Input/FetchInput.cs | 34 +++ .../Input/NewFileInput.cs | 15 ++ .../Model/FetchResult.cs | 90 +++++++ .../Service/DeployFileService.cs | 49 +++- .../Service/IDeployFileService.cs | 3 +- .../Service/IDeploymentService.cs | 10 +- .../Service/IFetchService.cs | 16 ++ .../Templates/IFileTemplate.cs | 17 ++ .../Unity.Services.Cli.Deploy.csproj | 4 +- ...y.Services.Cli.Environment.UnitTest.csproj | 2 +- .../Unity.Services.Cli.Environment.csproj | 2 +- .../Common/UgsCliFixture.cs | 6 + .../ConfigTests/ConfigTests.cs | 10 +- .../Deploy/DeployTests.cs | 18 +- .../EnvTests/EnvTests.cs | 18 +- .../FetchTest/FetchTests.cs | 231 ++++++++++++++++++ .../NewFile/NewFileTests.cs | 19 ++ .../Unity.Services.Cli.IntegrationTest.csproj | 3 +- .../Unity.Services.Cli.Lobby.UnitTest.csproj | 2 +- .../MappingModelUtils.cs | 3 +- .../Unity.Services.Cli.MockServer.csproj | 4 +- .../CliRemoteConfigDeploymentHandlerTests.cs | 20 +- .../Deploy/JsonConverterTests.cs | 5 +- .../RemoteConfigDeploymentServiceTests.cs | 84 ++++--- .../Deploy/RemoteConfigFetchServiceTests.cs | 141 +++++++++++ .../RemoteConfigModuleTests.cs | 16 ++ ....Services.Cli.RemoteConfig.UnitTest.csproj | 7 +- .../CliRemoteConfigDeploymentHandler.cs | 4 +- .../Deploy/FileReader.cs | 11 - .../Deploy/FileSystem.cs | 21 ++ .../Deploy/JsonConverter.cs | 3 +- .../Deploy/RemoteConfigDeploymentService.cs | 31 ++- .../Deploy/RemoteConfigFetchService.cs | 76 ++++++ .../Exceptions/ApiException.cs | 19 ++ .../RemoteConfigModule.cs | 18 +- .../Service/RemoteConfigService.cs | 9 +- .../Templates/RemoteConfigTemplate.cs | 37 +++ .../Unity.Services.Cli.RemoteConfig.csproj | 6 +- ...rviceAccountAuthentication.UnitTest.csproj | 2 +- Unity.Services.Cli/Unity.Services.Cli.sln | 2 + .../Unity.Services.Cli/Program.cs | 48 +++- .../Unity.Services.Cli.csproj | 6 +- .../Unity.Services.Cli/appsettings.json | 7 - Unity.Services.Cli/nuget.config | 2 +- commands.api | 44 ++++ commands.full.api | 44 ++++ docs/deploy-command.md | 16 +- docs/fetch-command.md | 31 +++ docs/module-remote-config.md | 15 +- docs/project-roles.md | 38 +-- 94 files changed, 1958 insertions(+), 467 deletions(-) delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Features/FeaturesFactoryTests.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Features/StaticFeaturesTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/MetricsUtilsTests.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Features/AlwaysEnabledFeatures.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Features/FeaturesFactory.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Features/IFeatures.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Features/StaticFeatures.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEvent.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventFactory/AnalyticEventFactory.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventFactory/IAnalyticEventFactory.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventUtils.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/IAnalyticEvent.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/FetchHandlerTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/NewFileHandlerTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Model/FetchResultTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy/FetchModule.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/FetchHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/NewFileHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/FetchInput.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/NewFileInput.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy/Model/FetchResult.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IFetchService.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Deploy/Templates/IFileTemplate.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/FetchTest/FetchTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/NewFile/NewFileTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigFetchServiceTests.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/FileReader.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/FileSystem.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Exceptions/ApiException.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Templates/RemoteConfigTemplate.cs create mode 100644 commands.api create mode 100644 commands.full.api create mode 100644 docs/fetch-command.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6021dec..b457763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,26 @@ All notable changes to UGS CLI will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [1.0.0-beta.1] +## [1.0.0-beta.2] - 2023-02-03 + +### Added + +- Remote Config new-file command to create a default remote config file. +- Deploy Module with `fetch` command to fetch files from all services that implement `IFetchService` + - Currently supports RemoteConfig only +- Command metrics tracking + +### Changed +- Subcommands are now sorted in alphabetical order + +### Fixed + +- Cancelling ``ugs deploy `` doesn't logs stacktraces anymore. +- `-e/--environment-name` and `-p/--project-id` options enabled for `deploy` command. +- `ugs deploy` with duplicate path will remove duplicate and deploy. +- `ugs deploy` with directory missing permission will log correct error. + +## [1.0.0-beta.1] - 2023-01-16 ### Added - CLI Core Module: Configuration, Service Account Authentication Commands. diff --git a/License.md b/License.md index 8de787b..d42374e 100644 --- a/License.md +++ b/License.md @@ -1,5 +1,5 @@ Unity Gaming Services Command-line Interface (UGS CLI) copyright © 2022 Unity Technologies. -This software is subject to, and made available under, the terms of service for Unity Gaming Services Command-line Interface (UGS CLI) (see https://unity3d.com/legal/one-operate-services-terms-of-service), and is an "Operate Service" as defined therein. +This software is subject to, and made available under, the terms of service for Unity Gaming Services Command-line Interface (UGS CLI)(see https://unity.com/legal). Your use of this software constitutes your acceptance of such terms. -Your use of the Services constitutes your acceptance of such terms. Unless expressly provided otherwise, the software under this license is made available strictly on an “AS IS” BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the terms of service for details on these and other terms and conditions. +Unless expressly provided otherwise, the software under this license is made available strictly on an “AS IS” BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the terms of service for details on these and other terms and conditions. diff --git a/README.md b/README.md index d5601b4..1df357f 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Requires [Windows] 10 or later. ### Linux -Requires [Ubuntu], [Alpine] and most other major distros. +Requires [Ubuntu], [Alpine] or most other major distros. ### macOS diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeDeploymentServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeDeploymentServiceTests.cs index 62a87cd..2b2162b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeDeploymentServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeDeploymentServiceTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -6,15 +5,12 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -using Spectre.Console; using Unity.Services.Cli.CloudCode.Deploy; using Unity.Services.Cli.CloudCode.Exceptions; using Unity.Services.Cli.CloudCode.Input; using Unity.Services.Cli.CloudCode.Service; using Unity.Services.Cli.CloudCode.UnitTest.Utils; -using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Deploy.Model; -using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Deploy.Service; using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment; using Unity.Services.CloudCode.Authoring.Editor.Core.Model; @@ -35,14 +31,12 @@ public class CloudCodeDeploymentServiceTests "test_b.js" }; - readonly Mock m_MockUnityEnvironment = new(); readonly Mock m_MockCloudCodeClient = new(); readonly Mock m_MockEnvironmentProvider = new(); readonly Mock m_MockCloudCodeInputParser = new(); readonly Mock m_MockCloudCodeService = new(); readonly Mock m_MockCloudCodeDeploymentHandler = new(); readonly Mock m_MockServicesWrapper = new(); - readonly Mock m_MockDeployFileService = new(); readonly Mock m_MockCliDeploymentOutputHandler = new(); readonly Mock m_MockCloudCodeScriptsLoader = new(); readonly Mock m_MockLogger = new(); @@ -65,14 +59,12 @@ public class CloudCodeDeploymentServiceTests [SetUp] public void SetUp() { - m_MockUnityEnvironment.Reset(); m_MockCloudCodeClient.Reset(); m_MockEnvironmentProvider.Reset(); m_MockCloudCodeInputParser.Reset(); m_MockCloudCodeService.Reset(); m_MockCloudCodeDeploymentHandler.Reset(); m_MockServicesWrapper.Reset(); - m_MockDeployFileService.Reset(); m_MockLogger.Reset(); m_MockCliDeploymentOutputHandler.Reset(); m_MockCloudCodeScriptsLoader.Reset(); @@ -110,7 +102,7 @@ public void SetUp() .Returns(m_MockCloudCodeScriptsLoader.Object); - m_DeploymentService = new CloudCodeDeploymentService(m_MockUnityEnvironment.Object, m_MockServicesWrapper.Object); + m_DeploymentService = new CloudCodeDeploymentService(m_MockServicesWrapper.Object); m_MockCloudCodeScriptsLoader.Setup( c => c.LoadScriptsAsync( @@ -130,17 +122,15 @@ public async Task DeployAsync_CallsLoadFilePathsFromInputCorrectly() { CloudProjectId = TestValues.ValidProjectId, }; - m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync()) - .ReturnsAsync(TestValues.ValidEnvironmentId); - m_MockDeployFileService.Setup(d => d.ListFilesToDeploy(input.Paths, "*.js")) - .Returns(k_ValidFilePaths); var result = await m_DeploymentService!.Deploy( input, - (StatusContext)null!, + k_ValidFilePaths, + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + null!, CancellationToken.None); - m_MockUnityEnvironment.Verify(x => x.FetchIdentifierAsync(), Times.Once); m_MockCloudCodeClient.Verify( x => x.Initialize( TestValues.ValidEnvironmentId, @@ -148,7 +138,7 @@ public async Task DeployAsync_CallsLoadFilePathsFromInputCorrectly() CancellationToken.None), Times.Once); m_MockEnvironmentProvider.VerifySet(x => { x.Current = TestValues.ValidEnvironmentId; }, Times.Once); - m_MockCloudCodeDeploymentHandler.Verify(x => x.DeployAsync(It.IsAny>()), Times.Once); + m_MockCloudCodeDeploymentHandler.Verify(x => x.DeployAsync(It.IsAny>(), false), Times.Once); Assert.AreEqual(k_DeployedContents, result.Deployed); Assert.AreEqual(k_FailedContents, result.Failed); } @@ -163,16 +153,15 @@ public void DeployAsync_DoesNotThrowOnApiException() }; m_MockCloudCodeDeploymentHandler.Setup(ex => ex - .DeployAsync(It.IsAny>())).ThrowsAsync(new ApiException()); - m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync()) - .ReturnsAsync(TestValues.ValidEnvironmentId); - m_MockDeployFileService.Setup(d => d.ListFilesToDeploy(input.Paths, "*.js")) - .Returns(k_ValidFilePaths); + .DeployAsync(It.IsAny>(), false)).ThrowsAsync(new ApiException()); Assert.DoesNotThrowAsync( () => m_DeploymentService!.Deploy( input, - (StatusContext)null!, + k_ValidFilePaths, + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + null!, CancellationToken.None)); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Unity.Services.Cli.CloudCode.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Unity.Services.Cli.CloudCode.UnitTest.csproj index ec9a4c2..a4333d3 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Unity.Services.Cli.CloudCode.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Unity.Services.Cli.CloudCode.UnitTest.csproj @@ -10,7 +10,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeDeploymentService.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeDeploymentService.cs index 2a475c1..c1e9e15 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeDeploymentService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeDeploymentService.cs @@ -2,7 +2,6 @@ using Unity.Services.Cli.CloudCode.Service; using Unity.Services.Cli.Deploy.Input; using Unity.Services.Cli.Deploy.Model; -using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Deploy.Service; using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment; using Unity.Services.Gateway.CloudCodeApiV1.Generated.Client; @@ -11,7 +10,6 @@ namespace Unity.Services.Cli.CloudCode.Deploy; internal class CloudCodeDeploymentService : IDeploymentService { - readonly IUnityEnvironment m_UnityEnvironment; readonly ICloudCodeInputParser m_CloudCodeInputParser; readonly ICloudCodeService m_CloudCodeService; readonly ICliDeploymentOutputHandler m_CliDeploymentOutputHandler; @@ -20,15 +18,11 @@ internal class CloudCodeDeploymentService : IDeploymentService readonly ICliCloudCodeClient m_CliCloudCodeClient; readonly ICloudCodeDeploymentHandler m_CloudCodeDeploymentHandler; string m_ServiceType; - string m_DeployFileExtension; public CloudCodeDeploymentService( - IUnityEnvironment unityEnvironment, ICloudCodeServicesWrapper servicesWrapper ) { - m_UnityEnvironment = unityEnvironment; - m_CloudCodeInputParser = servicesWrapper.CloudCodeInputParser; m_CloudCodeService = servicesWrapper.CloudCodeService; m_CliDeploymentOutputHandler = servicesWrapper.CliDeploymentOutputHandler; @@ -38,38 +32,42 @@ ICloudCodeServicesWrapper servicesWrapper m_CloudCodeDeploymentHandler = servicesWrapper.CloudCodeDeploymentHandler; m_ServiceType = "Cloud Code"; - m_DeployFileExtension = ".js"; + DeployFileExtension = ".js"; } string IDeploymentService.ServiceType => m_ServiceType; - - string IDeploymentService.DeployFileExtension => m_DeployFileExtension; + public string DeployFileExtension { get; } public async Task Deploy( DeployInput input, + IReadOnlyList filePaths, + string projectId, + string environmentId, StatusContext? loadingContext, CancellationToken cancellationToken) { - var environmentId = await m_UnityEnvironment.FetchIdentifierAsync(); - - m_CliCloudCodeClient.Initialize(environmentId, input.CloudProjectId!, cancellationToken); + m_CliCloudCodeClient.Initialize(environmentId, projectId, cancellationToken); m_EnvironmentProvider.Current = environmentId; loadingContext?.Status($"Reading {m_ServiceType} Scripts..."); var scriptList = await m_CloudCodeScriptsLoader.LoadScriptsAsync( - input.Paths, + filePaths, m_ServiceType, - m_DeployFileExtension, + DeployFileExtension, m_CloudCodeInputParser, m_CloudCodeService, m_CliDeploymentOutputHandler.Contents, cancellationToken); loadingContext?.Status($"Deploying {m_ServiceType} Scripts..."); + + var dryrun = false; + + try { - await m_CloudCodeDeploymentHandler.DeployAsync(scriptList); + await m_CloudCodeDeploymentHandler.DeployAsync(scriptList, dryrun); } catch (ApiException) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeScriptsLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeScriptsLoader.cs index 09a57b1..add369a 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeScriptsLoader.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeScriptsLoader.cs @@ -1,22 +1,14 @@ using Unity.Services.Cli.CloudCode.Exceptions; using Unity.Services.Cli.CloudCode.Service; using Unity.Services.Cli.Deploy.Model; -using Unity.Services.Cli.Deploy.Service; using Unity.Services.CloudCode.Authoring.Editor.Core.Model; namespace Unity.Services.Cli.CloudCode.Deploy; internal class CloudCodeScriptsLoader : ICloudCodeScriptsLoader { - readonly IDeployFileService m_DeployFileService; - - public CloudCodeScriptsLoader(IDeployFileService deployFileService) - { - m_DeployFileService = deployFileService; - } - public async Task> LoadScriptsAsync( - ICollection paths, + IReadOnlyCollection paths, string serviceType, string extension, ICloudCodeInputParser cloudCodeInputParser, @@ -25,8 +17,7 @@ public async Task> LoadScriptsAsync( CancellationToken cancellationToken) { var scriptList = new List(); - var filePaths = m_DeployFileService.ListFilesToDeploy(paths, extension); - foreach (var path in filePaths) + foreach (var path in paths) { try { diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ICloudCodeScriptsLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ICloudCodeScriptsLoader.cs index 4500474..4872136 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ICloudCodeScriptsLoader.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ICloudCodeScriptsLoader.cs @@ -7,7 +7,7 @@ namespace Unity.Services.Cli.CloudCode.Deploy; interface ICloudCodeScriptsLoader { Task> LoadScriptsAsync( - ICollection paths, + IReadOnlyCollection paths, string serviceType, string extension, ICloudCodeInputParser cloudCodeInputParser, diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Unity.Services.Cli.CloudCode.csproj b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Unity.Services.Cli.CloudCode.csproj index 123e6d9..adc23d5 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Unity.Services.Cli.CloudCode.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Unity.Services.Cli.CloudCode.csproj @@ -22,14 +22,15 @@ - + - + $(DefineConstants);$(ExtraDefineConstants) + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/CommonModuleTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/CommonModuleTests.cs index 685395f..4787c1c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/CommonModuleTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/CommonModuleTests.cs @@ -8,12 +8,12 @@ using NUnit.Framework; using Spectre.Console; using Unity.Services.Cli.Common.Console; -using Unity.Services.Cli.Common.Features; using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.Common.Models; using Unity.Services.Cli.Common.Networking; using Unity.Services.Cli.Common.SystemEnvironment; using Unity.Services.Cli.Common.Telemetry; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; using Unity.Services.Gateway.IdentityApiV1.Generated.Api; namespace Unity.Services.Cli.Common.UnitTest; @@ -25,6 +25,7 @@ class CommonModuleTests List? m_Services; Mock? m_MockedServiceCollection; readonly Mock m_MockSystemEnvironmentProvider; + readonly Mock m_MockAnalyticEventFactory = new(); Parser? m_Parser; public CommonModuleTests() @@ -36,11 +37,12 @@ public CommonModuleTests() public void SetUp() { m_MockSystemEnvironmentProvider.Reset(); + m_MockAnalyticEventFactory.Reset(); m_CommandLineBuilder = new(new RootCommand("Test root command")); m_Parser = m_CommandLineBuilder.UseHost(_ => Host.CreateDefaultBuilder(), host => { - CommonModule.ConfigureCommonServices(host, new Logger(), new StaticFeatures(), AnsiConsole.Create(new AnsiConsoleSettings())); + CommonModule.ConfigureCommonServices(host, new Logger(), AnsiConsole.Create(new AnsiConsoleSettings()), m_MockAnalyticEventFactory.Object); }).UseDefaults().AddGlobalCommonOptions().Build(); m_Parser!.InvokeAsync(""); @@ -90,6 +92,16 @@ public void CreateAndRegisterCliPromptServiceSucceeds() Assert.AreEqual(1, m_Services!.Count); } + [Test] + public void CreateAndRegisterCliAnalyticsSenderService_RegisterAllServices() + { + CommonModule.CreateAndRegisterCliAnalyticsSenderService(m_MockedServiceCollection!.Object); + Assert.NotNull(m_Services!.Find(p => p.ServiceType.Name is "AnalyticService")); + Assert.NotNull(m_Services!.Find(p => p.ServiceType.Name is "BigQueryExporter")); + Assert.NotNull(m_Services!.Find(p => p.ServiceType.Name is "IAnalyticConfiguration")); + Assert.NotNull(m_Services!.Find(p => p.ServiceType.Name is "AnalyticBuilder")); + } + [Test] public void CreateAndRegisterProgressBarServiceNoQuietAliasSetsConsole() { @@ -133,7 +145,7 @@ public void CreateTelemetrySender_SetsCorrectBasePath() public void CreateTelemetrySender_SetsBaseProductTags() { var telemetrySender = CommonModule.CreateTelemetrySender(m_MockSystemEnvironmentProvider.Object); - StringAssert.AreEqualIgnoringCase(telemetrySender.ProductTags[TagKeys.ProductName],"com.unity.ugs-cli"); + StringAssert.AreEqualIgnoringCase(telemetrySender.ProductTags[TagKeys.ProductName], CommonModule.m_CliProductName); StringAssert.AreEqualIgnoringCase(telemetrySender.ProductTags[TagKeys.CliVersion], TelemetryConfigurationProvider.GetCliVersion()); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Exceptions/ExceptionHelperTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Exceptions/ExceptionHelperTests.cs index 78f1656..3bba015 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Exceptions/ExceptionHelperTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Exceptions/ExceptionHelperTests.cs @@ -145,19 +145,6 @@ public void HandleForbidden403Exception() $"{m_ExceptionHelper!.HttpErrorTroubleshootingLinks[HttpStatusCode.Forbidden]}"); } - [Test] - public void HandlerCancelledOperation() - { - var cancelledException = new TaskCanceledException(); - - Assert.DoesNotThrow( - () => m_ExceptionHelper!.HandleException( - cancelledException, k_MockHelper.MockLogger.Object, m_Context!)); - - TestsHelper.VerifyLoggerWasCalled(k_MockHelper.MockLogger, expectedTimes: Times.Never); - Assert.AreEqual(ExitCode.Cancelled, m_Context!.ExitCode); - } - [Test] public void ExceptionHandler_DoesNotThrowWhenDiagnosticsFailToSend() { diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Features/FeaturesFactoryTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Features/FeaturesFactoryTests.cs deleted file mode 100644 index a25483d..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Features/FeaturesFactoryTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.FeatureManagement; -using Moq; -using NUnit.Framework; -using Unity.Services.Cli.Common.Features; - -namespace Unity.Services.Cli.Common.UnitTest.Features; - -public class FeaturesFactoryTests -{ - readonly Mock m_MockFeatureManager; - readonly Mock m_MockHostBuilder; - - public FeaturesFactoryTests() - { - var host = new Mock(); - m_MockHostBuilder = new Mock(); - m_MockHostBuilder.Setup(h => h.ConfigureAppConfiguration(It.IsAny>())) - .Returns(m_MockHostBuilder.Object); - m_MockHostBuilder.Setup(h => h.ConfigureServices(It.IsAny>())) - .Returns(m_MockHostBuilder.Object); - m_MockHostBuilder.Setup(h => h.Build()) - .Returns(host.Object); - - m_MockFeatureManager = new Mock(MockBehavior.Strict); - - var collection = new ServiceCollection(); - collection.AddSingleton(m_MockFeatureManager.Object); - - host.SetupGet(m => m.Services).Returns(collection.BuildServiceProvider()); - } - - [Test] - public async Task BuildAsync_ReturnsFeatures() - { - m_MockFeatureManager.Setup(m => m.GetFeatureNamesAsync()) - .Returns(ToAsyncEnumerable(new List())); - - var features = await FeaturesFactory.BuildAsync(m_MockHostBuilder.Object); - - Assert.IsNotNull(features); - } - - [Test] - public async Task BuildAsync_PassesKnownFeatures() - { - var feature = "TestFeature"; - m_MockFeatureManager.Setup(m => m.GetFeatureNamesAsync()) - .Returns(ToAsyncEnumerable(new List{ feature })); - m_MockFeatureManager.Setup(m => m.IsEnabledAsync(feature)) - .ReturnsAsync(true); - - var features = await FeaturesFactory.BuildAsync(m_MockHostBuilder.Object); - - Assert.IsTrue(features.IsEnabled(feature)); - } - - static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable enumerable) - { - foreach(var item in enumerable) - { - yield return await Task.FromResult(item); - } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Features/StaticFeaturesTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Features/StaticFeaturesTests.cs deleted file mode 100644 index 15288aa..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Features/StaticFeaturesTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using NUnit.Framework; -using Unity.Services.Cli.Common.Features; - -namespace Unity.Services.Cli.Common.UnitTest.Features; - -public class StaticFeaturesTests -{ - [Test] - public void DefaultConstructor_DoesNotThrow() - { - Assert.DoesNotThrow(() => - { - var features = new StaticFeatures(); - features.IsEnabled("IDoNotExist"); - }); - } - - [Test] - public void IsEnabled_WhenFeaturesDoesNotExist_ReturnsFalse() - { - var features = new StaticFeatures(new Dictionary()); - - Assert.IsFalse(features.IsEnabled("IDoNotExist")); - } - - [Test] - public void IsEnabled_WhenFeatureIsDisabled_ReturnsFalse() - { - var features = new StaticFeatures(new Dictionary - { - { "Feature", false } - }); - - Assert.IsFalse(features.IsEnabled("Feature")); - } - - [Test] - public void IsEnabled_WhenFeatureIsEnabled_ReturnsTrue() - { - var features = new StaticFeatures(new Dictionary - { - { "Feature", true } - }); - - Assert.IsTrue(features.IsEnabled("Feature")); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Middleware/ContextBinderTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Middleware/ContextBinderTests.cs index d1750e5..a8be99f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Middleware/ContextBinderTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Middleware/ContextBinderTests.cs @@ -12,6 +12,7 @@ using Unity.Services.Cli.Common.Networking; using Unity.Services.Cli.Common.Services; using Unity.Services.Cli.Common.SystemEnvironment; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; using Unity.Services.Cli.Common.Utils; using ContextBinder = Unity.Services.Cli.Common.Middleware.ContextBinder; using HostBuilderContext = Microsoft.Extensions.Hosting.HostBuilderContext; @@ -271,6 +272,9 @@ Parser BuildCommandWithInputParser(Command command) Mock mockUnityEnvironment = new Mock(); host.ConfigureServices(serviceCollection => serviceCollection .AddSingleton(mockUnityEnvironment.Object)); + Mock mockAnalyticEventFactory = new Mock(); + host.ConfigureServices(serviceCollection => serviceCollection + .AddSingleton(mockAnalyticEventFactory.Object)); }) .AddCommandInputParserMiddleware(); @@ -298,6 +302,10 @@ Parser BuildCommandWithInputParserWithMockedConfigModule(Command command) Mock mockUnityEnvironment = new Mock(); host.ConfigureServices(serviceCollection => serviceCollection .AddSingleton(mockUnityEnvironment.Object)); + + Mock mockAnalyticEventFactory = new Mock(); + host.ConfigureServices(serviceCollection => serviceCollection + .AddSingleton(mockAnalyticEventFactory.Object)); }) .AddCommandInputParserMiddleware(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/DiagnosticsTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/DiagnosticsTests.cs index a9dd91e..d856c0b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/DiagnosticsTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/DiagnosticsTests.cs @@ -26,7 +26,7 @@ public class DiagnosticsTests readonly Dictionary m_ExpectedPackageTags = new() { - [TagKeys.ProductName] = "com.unity.ugs-cli", + [TagKeys.ProductName] = CommonModule.m_CliProductName, [TagKeys.CliVersion] = TelemetryConfigurationProvider.GetCliVersion() }; diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/MetricsUtilsTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/MetricsUtilsTests.cs new file mode 100644 index 0000000..bbc1b63 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Telemetry/MetricsUtilsTests.cs @@ -0,0 +1,25 @@ +using System.CommandLine; +using System.CommandLine.Parsing; +using NUnit.Framework; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; + +namespace Unity.Services.Cli.Common.UnitTest.Telemetry; + +[TestFixture] +public class MetricsUtilsTests +{ + [Test] + public void ConvertSymbolResultToString_ParsesCorrectly() + { + var command = new Command("ugs"); + var subCommand1 = new Command("env"); + var subCommand2 = new Command("list"); + command.AddCommand(subCommand1); + subCommand1.AddCommand(subCommand2); + + var parser = new Parser(command); + var result = AnalyticEventUtils.ConvertSymbolResultToString( + parser.Parse(new[] { command.Name, subCommand1.Name, subCommand2.Name }).CommandResult); + StringAssert.AreEqualIgnoringCase($"{command.Name}_{subCommand1.Name}_{subCommand2.Name}", result); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Unity.Services.Cli.Common.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Unity.Services.Cli.Common.UnitTest.csproj index 3d4be13..d33c5a5 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Unity.Services.Cli.Common.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Common.UnitTest/Unity.Services.Cli.Common.UnitTest.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/CommonModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/CommonModule.cs index b135f17..f5471df 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/CommonModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/CommonModule.cs @@ -9,13 +9,15 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Configuration; using Spectre.Console; +using Unity.Analytics.Sender; using Unity.Services.Cli.Common.Console; -using Unity.Services.Cli.Common.Features; using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.Common.Input; using Unity.Services.Cli.Common.Networking; using Unity.Services.Cli.Common.SystemEnvironment; using Unity.Services.Cli.Common.Telemetry; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; using Unity.Services.Gateway.IdentityApiV1.Generated.Api; using IdentityClient = Unity.Services.Gateway.IdentityApiV1.Generated.Client; @@ -23,6 +25,7 @@ namespace Unity.Services.Cli.Common; public static class CommonModule { + public const string m_CliProductName = "com.unity.ugs-cli"; public static CommandLineBuilder UseTreePrinter(this CommandLineBuilder builder) { var printTreeFlag = new Option("--print-tree") @@ -53,8 +56,8 @@ public static CommandLineBuilder UseTreePrinter(this CommandLineBuilder builder) return builder; } - public static void ConfigureCommonServices(IHostBuilder hostBuilder, Logger logger, IFeatures features, - IAnsiConsole ansiConsole) + public static void ConfigureCommonServices(IHostBuilder hostBuilder, Logger logger, + IAnsiConsole ansiConsole, IAnalyticEventFactory analyticEventFactory) { var parseResult = hostBuilder.GetInvocationContext().ParseResult; bool silentAnsiConsole = parseResult.GetValueForOption(CommonInput.QuietOption) || @@ -67,13 +70,14 @@ public static void ConfigureCommonServices(IHostBuilder hostBuilder, Logger logg hostBuilder.ConfigureAppConfiguration(ConfigAppConfiguration); hostBuilder.ConfigureLogging(logBuilder => ConfigureLogging(parseResult, logBuilder, logger)); hostBuilder.ConfigureServices(collection => collection.AddSingleton(logger)); - hostBuilder.ConfigureServices(collection => collection.AddSingleton(features)); + hostBuilder.ConfigureServices(collection => collection.AddSingleton(analyticEventFactory)); hostBuilder.ConfigureServices(CreateAndRegisterIdentityApiServices); hostBuilder.ConfigureServices(serviceCollection => CreateAndRegisterProgressBarService(serviceCollection, usedConsole)); hostBuilder.ConfigureServices(serviceCollection => CreateAndRegisterLoadingIndicatorService(serviceCollection, usedConsole)); hostBuilder.ConfigureServices(CreateAndRegisterCliPromptService); + hostBuilder.ConfigureServices(CreateAndRegisterCliAnalyticsSenderService); } internal static void ConfigAppConfiguration(IConfigurationBuilder config) @@ -151,7 +155,7 @@ public static TelemetrySender CreateTelemetrySender(ISystemEnvironmentProvider s }; var productTags = new Dictionary { - [TagKeys.ProductName] = "com.unity.ugs-cli", + [TagKeys.ProductName] = m_CliProductName, [TagKeys.CliVersion] = TelemetryConfigurationProvider.GetCliVersion() }; @@ -164,4 +168,18 @@ public static TelemetrySender CreateTelemetrySender(ISystemEnvironmentProvider s return new TelemetrySender(telemetryApi, commonTags, productTags); } + + internal static void CreateAndRegisterCliAnalyticsSenderService(IServiceCollection serviceCollection) + { + serviceCollection.AddAnalytics(((x, _) => + x.WithSourceName(m_CliProductName) + .WithDefaultUnityBigQueryExporter() + .WithCommonHeader(new Dictionary + { + ["uuid"]= "" + }) + )); + var provider = serviceCollection.BuildServiceProvider(); + provider.InitAnalytics(); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs index ff8c90e..47b649c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs @@ -7,6 +7,9 @@ using CloudCodeApiException = Unity.Services.Gateway.CloudCodeApiV1.Generated.Client.ApiException; using EconomyApiException = Unity.Services.Gateway.EconomyApiV2.Generated.Client.ApiException; using LobbyApiException = Unity.Services.MpsLobby.LobbyApiV1.Generated.Client.ApiException; +using LeaderboardApiException = Unity.Services.Gateway.LeaderboardApiV1.Generated.Client.ApiException; +using AccountsApiException = Unity.Services.Gateway.AccountsApiV2.Generated.Client.ApiException; +using PlayerAuthException = Unity.Services.Gateway.PlayerAuthApiV1.Generated.Client.ApiException; namespace Unity.Services.Cli.Common.Exceptions; @@ -28,6 +31,13 @@ public ExceptionHelper(IDiagnostics diagnostics, IAnsiConsole ansiConsole) public void HandleException(Exception exception, ILogger logger, InvocationContext context) { + var cancellationToken = context.GetCancellationToken(); + if (cancellationToken.IsCancellationRequested) + { + context.ExitCode = ExitCode.Cancelled; + return; + } + switch (exception) { case CliException cliException: @@ -51,8 +61,14 @@ public void HandleException(Exception exception, ILogger logger, InvocationConte case LobbyApiException lobbyApiException: HandleApiException(exception, logger, context, lobbyApiException.ErrorCode); break; - case TaskCanceledException: - context.ExitCode = ExitCode.Cancelled; + case LeaderboardApiException leaderboardApiException: + HandleApiException(exception, logger, context, leaderboardApiException.ErrorCode); + break; + case AccountsApiException accountsApiException: + HandleApiException(exception, logger, context, accountsApiException.ErrorCode); + break; + case PlayerAuthException playerAuthApiException: + HandleApiException(exception, logger, context, playerAuthApiException.ErrorCode); break; case AggregateException aggregateException: HandleAggregateException(aggregateException, logger, context); @@ -82,19 +98,21 @@ void HandleAggregateException(AggregateException aggregateException, ILogger log // Check for CLI Exceptions in the aggregated exceptions var cliExceptions = aggregateException.InnerExceptions.Where(e => e is CliException); + // Log any CLI Exception found foreach (var e in cliExceptions) { logger.LogError(e.Message); } + // Sets handled error in case no unhandled error is found if (cliExceptions.Any()) { context.ExitCode = ExitCode.HandledError; } - var unhandledExceptions = aggregateException.InnerExceptions.Where(e => e is not CliException); + // Sets default flow in case any exception is unhandled if (unhandledExceptions.Any()) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Features/AlwaysEnabledFeatures.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Features/AlwaysEnabledFeatures.cs deleted file mode 100644 index 6b7b3d5..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Features/AlwaysEnabledFeatures.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Unity.Services.Cli.Common.Features; - -class AlwaysEnabledFeatures : IFeatures -{ - public bool IsEnabled(string flag) => true; -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Features/FeaturesFactory.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Features/FeaturesFactory.cs deleted file mode 100644 index 8c45017..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Features/FeaturesFactory.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.FeatureManagement; - -namespace Unity.Services.Cli.Common.Features; - -public static class FeaturesFactory -{ - /// New Builder to be consumed. Build will be called so it must not be shared. - /// Cancellation token. - public static async Task BuildAsync(IHostBuilder builder, CancellationToken cancellationToken = default) - { - if (ShouldEnableAllFeatureFlags()) - { - return new AlwaysEnabledFeatures(); - } - - var services = builder. - ConfigureAppConfiguration(CommonModule.ConfigAppConfiguration) - .ConfigureServices((_, collection) => - { - collection.AddFeatureManagement(); - }).Build(); - var featureManager = services.Services.GetRequiredService(); - return new StaticFeatures(await LoadAllFeatureFlagsAsync(featureManager, cancellationToken)); - } - - static async Task> LoadAllFeatureFlagsAsync(IFeatureManager featureManager, CancellationToken cancellationToken = default) - { - var flags = new Dictionary(); - await foreach (var name in featureManager.GetFeatureNamesAsync().WithCancellation(cancellationToken)) - { - flags[name] = await featureManager.IsEnabledAsync(name); - } - return flags; - } - - static bool ShouldEnableAllFeatureFlags() - { -#if ALL_FEATURE_FLAGS_ENABLED - return true; -#else - return false; -#endif - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Features/IFeatures.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Features/IFeatures.cs deleted file mode 100644 index 7d85598..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Features/IFeatures.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Unity.Services.Cli.Common.Features; - -public interface IFeatures -{ - bool IsEnabled(string flag); -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Features/StaticFeatures.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Features/StaticFeatures.cs deleted file mode 100644 index b8ca863..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Features/StaticFeatures.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Unity.Services.Cli.Common.Features; - -class StaticFeatures: IFeatures -{ - readonly IDictionary m_Features; - - public StaticFeatures(): this(new Dictionary()) { - } - - public StaticFeatures(IDictionary features) { - m_Features = features; - } - - public bool IsEnabled(string flag) - { - return m_Features.ContainsKey(flag) && m_Features[flag]; - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Logging/JsonLogMessage.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Logging/JsonLogMessage.cs index 292da46..18e9161 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Logging/JsonLogMessage.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Logging/JsonLogMessage.cs @@ -1,5 +1,6 @@ namespace Unity.Services.Cli.Common.Logging; +[Serializable] class JsonLogMessage { public string? Message { get; set; } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Middleware/ContextBinder.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Middleware/ContextBinder.cs index 9f1a965..d1e7c3a 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Middleware/ContextBinder.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Middleware/ContextBinder.cs @@ -13,6 +13,8 @@ using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.Common.Services; using Unity.Services.Cli.Common.Telemetry; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; using Unity.Services.Cli.Common.Utils; namespace Unity.Services.Cli.Common.Middleware; @@ -60,7 +62,7 @@ public static CommandLineBuilder AddCommandInputParserMiddleware(this CommandLin builder.AddMiddleware(AddInputParserToContext); return builder; - static void AddInputParserToContext(InvocationContext context) + void AddInputParserToContext(InvocationContext context) { var customInputTypes = GetCustomInputTypes(); foreach (var inputType in customInputTypes) @@ -77,6 +79,7 @@ static void AddInputParserToContext(InvocationContext context) SetInputFromConfigAsync(inputInstance, configService, logger, context, memberInfos).Wait(); SetInputFromParseResult(inputType, context.ParseResult, inputInstance); SetUnityEnvironment(inputInstance, context); + SetAnalyticEventFactory(inputInstance, context); return inputInstance; }); } @@ -92,6 +95,15 @@ static IEnumerable GetCustomInputTypes() } } + static void SetAnalyticEventFactory( + object inputInstance, + InvocationContext context) + { + var host = context.GetHost(); + var eventFactory = host.Services.GetRequiredService(); + eventFactory.ProjectId = ((CommonInput)inputInstance).CloudProjectId ?? ""; + } + static void SetUnityEnvironment(object inputInstance, InvocationContext context) { var host = context.GetHost(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEvent.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEvent.cs new file mode 100644 index 0000000..98bcd03 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEvent.cs @@ -0,0 +1,46 @@ +using Unity.Analytics.Sender; +using Unity.Services.Cli.Common.SystemEnvironment; + +namespace Unity.Services.Cli.Common.Telemetry.AnalyticEvent; + +public class AnalyticEvent : AnalyticEventBase, IAnalyticEvent +{ +#if USE_MOCKSERVER_ENDPOINTS || USE_STAGING_ENDPOINTS + const string k_MetricsName = "ugs.cli.metric.v3.stg"; +#else + const string k_MetricsName = "ugs.cli.metric.v3"; +#endif + + readonly ISystemEnvironmentProvider m_SystemEnvironmentProvider; + + bool IsDisabled => TelemetryConfigurationProvider.IsTelemetryDisabled(m_SystemEnvironmentProvider); + + public AnalyticEvent(ISystemEnvironmentProvider environmentProvider) : base(k_MetricsName) + { + m_SystemEnvironmentProvider = environmentProvider; + AddData(TagKeys.CicdPlatform, TelemetryConfigurationProvider.GetCicdPlatform(environmentProvider!)); + AddData(TagKeys.CliVersion, TelemetryConfigurationProvider.GetCliVersion()); + AddData(TagKeys.OperatingSystem, Environment.OSVersion.ToString()); + AddData(TagKeys.Platform, TelemetryConfigurationProvider.GetOsPlatform()); + } + + public new void AddData(string key, object value) + { + base.AddData(key, value); + } + + public override void Send() + { + if (IsDisabled) + return; + + try + { + base.Send(); + } + catch + { + // Metrics should fail silently as to not halt the execution of the application + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventFactory/AnalyticEventFactory.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventFactory/AnalyticEventFactory.cs new file mode 100644 index 0000000..55e0151 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventFactory/AnalyticEventFactory.cs @@ -0,0 +1,23 @@ +using System; +using Unity.Services.Cli.Common.SystemEnvironment; + +namespace Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; + +public class AnalyticEventFactory : IAnalyticEventFactory +{ + readonly ISystemEnvironmentProvider m_SystemEnvironmentProvider; + + public AnalyticEventFactory(ISystemEnvironmentProvider systemEnvironmentProvider) + { + m_SystemEnvironmentProvider = systemEnvironmentProvider; + } + + public string ProjectId { get; set; } = ""; + + public IAnalyticEvent CreateEvent() + { + var analyticEvent = new AnalyticEvent(m_SystemEnvironmentProvider); + analyticEvent.AddData(TagKeys.ProjectId, ProjectId); + return analyticEvent; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventFactory/IAnalyticEventFactory.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventFactory/IAnalyticEventFactory.cs new file mode 100644 index 0000000..1671ff5 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventFactory/IAnalyticEventFactory.cs @@ -0,0 +1,10 @@ +using System; + +namespace Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; + +public interface IAnalyticEventFactory +{ + string ProjectId { get; set; } + + IAnalyticEvent CreateEvent(); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventUtils.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventUtils.cs new file mode 100644 index 0000000..bb6b7fd --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/AnalyticEventUtils.cs @@ -0,0 +1,19 @@ +using System; +using System.CommandLine.Parsing; + +namespace Unity.Services.Cli.Common.Telemetry.AnalyticEvent; + +public static class AnalyticEventUtils +{ + public static string ConvertSymbolResultToString(SymbolResult symbol) + { + List symbolNames = new(); + while (symbol is not null) + { + symbolNames.Add(symbol.Symbol.Name); + symbol = symbol.Parent!; + } + symbolNames.Reverse(); + return string.Join("_", symbolNames); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/IAnalyticEvent.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/IAnalyticEvent.cs new file mode 100644 index 0000000..fda63f8 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/AnalyticEvent/IAnalyticEvent.cs @@ -0,0 +1,9 @@ +using System; + +namespace Unity.Services.Cli.Common.Telemetry.AnalyticEvent; + +public interface IAnalyticEvent +{ + public void AddData(string key, object value); + public void Send(); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/TagKeys.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/TagKeys.cs index 2eb420a..97be026 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/TagKeys.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Telemetry/TagKeys.cs @@ -12,6 +12,10 @@ internal static class TagKeys /// public const string Platform = "platform"; + public const string Timestamp = "time"; + + public const string ProjectId = "project_id"; + public const string ProductName = "product_name"; /// diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Unity.Services.Cli.Common.csproj b/Unity.Services.Cli/Unity.Services.Cli.Common/Unity.Services.Cli.Common.csproj index 9eef78d..1eb89a4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Unity.Services.Cli.Common.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Unity.Services.Cli.Common.csproj @@ -8,14 +8,18 @@ - + + + + + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/DeployHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/DeployHandlerTests.cs index d4f3630..bb8a048 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/DeployHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/DeployHandlerTests.cs @@ -12,9 +12,11 @@ using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.Common.Services; +using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Deploy.Handlers; using Unity.Services.Cli.Deploy.Input; using Unity.Services.Cli.Deploy.Model; +using Unity.Services.Cli.Deploy.Service; using Unity.Services.Cli.TestUtils; namespace Unity.Services.Cli.Deploy.UnitTest.Handlers; @@ -26,9 +28,11 @@ public class DeployHandlerTests readonly Mock m_Logger = new(); readonly Mock m_ServiceProvider = new(); readonly Mock m_DeploymentService = new(); - + readonly Mock m_UnityEnvironment = new(); + readonly Mock m_DeployFileService = new(); readonly ServiceTypesBridge m_Bridge = new(); + const string ValidEnvironmentId = "00000000-0000-0000-0000-000000000000"; public class TestDeploymentService : IDeploymentService { string m_ServiceType = "Test"; @@ -38,7 +42,8 @@ public class TestDeploymentService : IDeploymentService string IDeploymentService.DeployFileExtension => m_DeployFileExtension; - public Task Deploy(DeployInput input, StatusContext? loadingContext, CancellationToken cancellationToken) + public Task Deploy(DeployInput deployInput, IReadOnlyList filePaths, string projectId, string environmentId, + StatusContext? loadingContext, CancellationToken cancellationToken) { return Task.FromResult(new DeploymentResult(new List(), new List())); } @@ -53,7 +58,8 @@ public class TestDeploymentFailureService : IDeploymentService string IDeploymentService.DeployFileExtension => m_DeployFileExtension; - public Task Deploy(DeployInput input, StatusContext? loadingContext, CancellationToken cancellationToken) + public Task Deploy(DeployInput deployInput, IReadOnlyList filePaths, string projectId, string environmentId, + StatusContext? loadingContext, CancellationToken cancellationToken) { return Task.FromResult(new DeploymentResult( new List() @@ -76,11 +82,11 @@ public class TestDeploymentUnhandledExceptionService : IDeploymentService string IDeploymentService.DeployFileExtension => m_DeployFileExtension; - public Task Deploy(DeployInput input, StatusContext? loadingContext, CancellationToken cancellationToken) + public Task Deploy(DeployInput deployInput, IReadOnlyList filePaths, string projectId, string environmentId, + StatusContext? loadingContext, CancellationToken cancellationToken) { return Task.FromException(new NotImplementedException()); - // throw new NotImplementedException(); } } @@ -91,13 +97,23 @@ public void SetUp() m_ServiceProvider.Reset(); m_DeploymentService.Reset(); m_Logger.Reset(); - + m_UnityEnvironment.Reset(); + m_DeployFileService.Reset(); var collection = m_Bridge.CreateBuilder(new ServiceCollection()); collection.AddScoped(); var provider = m_Bridge.CreateServiceProvider(collection); m_Host.Setup(x => x.Services) .Returns(provider); + + m_UnityEnvironment.Setup(x => x.FetchIdentifierAsync()).Returns(Task.FromResult(ValidEnvironmentId)); + m_DeployFileService.Setup(x => x.ListFilesToDeploy(new[] + { + "" + }, "*.ext")).Returns(new[] + { + "" + }); } [Test] @@ -106,7 +122,13 @@ public async Task DeployAsync_WithLoadingIndicator_CallsLoadingIndicatorStartLoa var mockLoadingIndicator = new Mock(); await DeployHandler.DeployAsync( - null!, null!, null!, mockLoadingIndicator.Object, CancellationToken.None); + It.IsAny(), + It.IsAny(), + m_DeployFileService.Object, + m_UnityEnvironment.Object, + It.IsAny(), + mockLoadingIndicator.Object, + CancellationToken.None); mockLoadingIndicator.Verify( ex => ex.StartLoadingAsync(It.IsAny(), It.IsAny>()), Times.Once); @@ -117,7 +139,10 @@ await DeployHandler.DeployAsync( public async Task DeployAsync_CallsGetServicesCorrectly() { await DeployHandler.DeployAsync( - m_Host.Object, new DeployInput(), m_Logger.Object, (StatusContext)null!, CancellationToken.None); + m_Host.Object, new DeployInput(), + m_DeployFileService.Object, + m_UnityEnvironment.Object, + m_Logger.Object, (StatusContext)null!, CancellationToken.None); TestsHelper.VerifyLoggerWasCalled(m_Logger, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Once); } @@ -137,11 +162,13 @@ public void DeployAsync_DeploymentFailureThrowsDeploymentFailureException() Assert.ThrowsAsync(async () => { await DeployHandler.DeployAsync( - m_Host.Object, new DeployInput(), m_Logger.Object, (StatusContext)null!, CancellationToken.None); + m_Host.Object, new DeployInput(), + m_DeployFileService.Object, + m_UnityEnvironment.Object, + m_Logger.Object, (StatusContext)null!, CancellationToken.None); }); } - [Test] public void DeployAsync_DeploymentFailureThrowsAggregateException() { @@ -155,7 +182,10 @@ public void DeployAsync_DeploymentFailureThrowsAggregateException() Assert.ThrowsAsync(async () => { await DeployHandler.DeployAsync( - m_Host.Object, new DeployInput(), m_Logger.Object, (StatusContext)null!, CancellationToken.None); + m_Host.Object, new DeployInput(), + m_DeployFileService.Object, + m_UnityEnvironment.Object, + m_Logger.Object, (StatusContext)null!, CancellationToken.None); }); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/FetchHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/FetchHandlerTests.cs new file mode 100644 index 0000000..ef14643 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/FetchHandlerTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Spectre.Console; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.Common.Services; +using Unity.Services.Cli.Deploy.Handlers; +using Unity.Services.Cli.Deploy.Input; +using Unity.Services.Cli.Deploy.Model; +using Unity.Services.Cli.Deploy.Service; +using Unity.Services.Cli.TestUtils; + +namespace Unity.Services.Cli.Deploy.UnitTest.Handlers; + +[TestFixture] +public class FetchHandlerTests +{ + readonly Mock m_Host = new(); + readonly Mock m_Logger = new(); + readonly Mock m_ServiceProvider = new(); + readonly Mock m_DeploymentService = new(); + + class TestFetchService : IFetchService + { + string m_ServiceType = "Test"; + string m_DeployFileExtension = ".test"; + + string IFetchService.ServiceType => m_ServiceType; + + string IFetchService.FileExtension => m_DeployFileExtension; + + public Task FetchAsync(FetchInput input, StatusContext? loadingContext, CancellationToken cancellationToken) + { + var res = new FetchResult( + new[] { "updated1" }, + new[] { "deleted1" }, + Array.Empty(), + new[] { "file1" }, + Array.Empty()); + return Task.FromResult(res); + } + } + + [SetUp] + public void SetUp() + { + m_Host.Reset(); + m_ServiceProvider.Reset(); + m_DeploymentService.Reset(); + m_Logger.Reset(); + + var bridge = new ServiceTypesBridge(); + var collection = bridge.CreateBuilder(new ServiceCollection()); + collection.AddScoped(); + var provider = bridge.CreateServiceProvider(collection); + + m_Host.Setup(x => x.Services) + .Returns(provider); + } + + [Test] + public async Task FetchAsync_WithLoadingIndicator_CallsLoadingIndicatorStartLoading() + { + var mockLoadingIndicator = new Mock(); + + await FetchHandler.FetchAsync( + null!, null!, null!, mockLoadingIndicator.Object, CancellationToken.None); + + mockLoadingIndicator.Verify( + ex => ex.StartLoadingAsync(It.IsAny(), It.IsAny>()), Times.Once); + } + + + [Test] + public async Task FetchAsync_CallsGetServicesCorrectly() + { + var fetchInput = new FetchInput(); + + await FetchHandler.FetchAsync( + m_Host.Object, fetchInput, m_Logger.Object, (StatusContext)null!, CancellationToken.None); + + TestsHelper.VerifyLoggerWasCalled(m_Logger, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Once); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/NewFileHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/NewFileHandlerTests.cs new file mode 100644 index 0000000..dafc34c --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Handlers/NewFileHandlerTests.cs @@ -0,0 +1,51 @@ +using System.IO; +using System.IO.Abstractions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Unity.Services.Cli.Deploy.Handlers; +using Unity.Services.Cli.Deploy.Input; +using Unity.Services.Cli.Deploy.Templates; + +namespace Unity.Services.Cli.Deploy.UnitTest.Handlers; + +public class NewFileHandlerTests +{ + Mock? m_MockFile; + Mock? m_MockTemplate; + Mock? m_MockLogger; + CancellationToken m_CancellationToken; + + [SetUp] + public void SetUp() + { + m_MockFile = new Mock(); + m_MockTemplate = new Mock(); + m_MockTemplate.SetupGet(template => template.Extension).Returns(".test"); + m_MockLogger = new Mock(); + m_CancellationToken = CancellationToken.None; + } + + [Test] + public async Task NewFile_WithInvalidExtension_ReplacedWithValid() + { + var input = new NewFileInput + { + File = "test.txt" + }; + await NewFileHandler.NewFileAsync(input, m_MockFile!.Object, m_MockTemplate!.Object, m_MockLogger!.Object, m_CancellationToken); + Assert.That(Path.GetExtension(input.File), Is.EqualTo(".test")); + } + + [Test] + public async Task NewFile_CallsWriteAllText() + { + var file = "test"; + + await NewFileHandler.NewFileAsync(new NewFileInput { File = file }, m_MockFile!.Object, m_MockTemplate!.Object, m_MockLogger!.Object, m_CancellationToken); + + m_MockFile.Verify(f => f.WriteAllTextAsync(It.IsAny(), It.IsAny(), CancellationToken.None), Times.Once); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Model/FetchResultTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Model/FetchResultTests.cs new file mode 100644 index 0000000..5d419d5 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Model/FetchResultTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Services.Cli.Deploy.Model; + +namespace Unity.Services.Cli.Deploy.UnitTest.Model; + +[TestFixture] +public class FetchResultTests +{ + static readonly IReadOnlyList k_Updated = new[] + { + "thing1", + "thing2" + }; + + static readonly IReadOnlyList k_Deleted = new[] + { + "thing3" + }; + + static readonly IReadOnlyList k_Created = new[] + { + "thing4" + }; + + static readonly IReadOnlyList k_Fetched= new[] + { + "thing1" + }; + + static readonly IReadOnlyList k_Failed= new[] + { + "thing2" + }; + + + [Test] + public void ToStringFormatsFetchedAndFailedResults() + { + var fetchResult = new FetchResult( + k_Updated, + k_Deleted, + k_Created, + k_Fetched, + k_Failed); + var result = fetchResult.ToString(); + + Assert.IsTrue(result.Contains($"Successfully fetched into the following files:{System.Environment.NewLine} {k_Fetched[0]}")); + foreach (var fetchedFile in k_Failed) + { + var expected = $"Failed to fetch:{System.Environment.NewLine} '{fetchedFile}'"; + Assert.IsTrue(result.Contains(expected), + $"Missing or incorrect log for '{fetchedFile}'") ; + } + } + + + [Test] + public void ToStringFormatsNoContentDeployed() + { + var fetchResult = new FetchResult( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty()); + var result = fetchResult.ToString(); + + Assert.IsFalse(result.Contains($"Successfully fetched the following contents:{System.Environment.NewLine}")); + Assert.IsTrue(result.Contains("No content fetched")); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Service/DeployFileServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Service/DeployFileServiceTests.cs index 357e16c..43afe72 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Service/DeployFileServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Service/DeployFileServiceTests.cs @@ -1,7 +1,10 @@ +using System; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Moq; using NUnit.Framework; using Unity.Services.Cli.Common.Exceptions; @@ -15,11 +18,12 @@ public class DeployFileServiceTests { readonly Mock m_MockFile = new(); readonly Mock m_MockDirectory = new(); + readonly Mock m_MockPath = new(); readonly DeployFileService m_Service; public DeployFileServiceTests() { - m_Service = new DeployFileService(m_MockFile.Object, m_MockDirectory.Object); + m_Service = new DeployFileService(m_MockFile.Object, m_MockDirectory.Object, m_MockPath.Object); } [SetUp] @@ -27,13 +31,14 @@ public void SetUp() { m_MockFile.Reset(); m_MockDirectory.Reset(); + m_MockPath.Reset(); } [Test] public void ListFilesToDeployReturnsExistingFiles() { m_MockFile.Setup(f => f.Exists("test.mps")).Returns(true); - + m_MockPath.Setup(p => p.GetFullPath("test.mps")).Returns("test.mps"); var files = m_Service.ListFilesToDeploy(new List { "test.mps" @@ -53,10 +58,24 @@ public void ListFilesToDeployWhenDirectoryAndFileIsMissingThrowPathNotFoundExcep }, ".mps").ToList(); }); } + [Test] + public void ListFilesToDeployThrowExceptionForDirectoryWithoutAccessPermission() + { + m_MockPath.Setup(p => p.GetFullPath("foo")).Returns("foo"); + m_MockDirectory.Setup(f => f.Exists("foo")).Returns(true); + m_MockDirectory.Setup(d => d.GetFiles("foo", "*.mps", SearchOption.AllDirectories)) + .Throws(); + + Assert.Throws(() => m_Service.ListFilesToDeploy(new List + { + "foo" + }, ".mps")); + } [Test] public void ListFilesToDeployEnumeratesDirectories() { + m_MockPath.Setup(p => p.GetFullPath("foo")).Returns("foo"); m_MockDirectory.Setup(f => f.Exists("foo")).Returns(true); m_MockDirectory.Setup(d => d.GetFiles("foo", "*.mps", SearchOption.AllDirectories)) .Returns(new[] @@ -81,6 +100,29 @@ public void ListFilesToDeployEnumeratesDirectoriesSorted() "b.mps", "a.mps" }; + m_MockPath.Setup(p => p.GetFullPath("foo")).Returns("foo"); + m_MockDirectory.Setup(f => f.Exists("foo")).Returns(true); + m_MockDirectory.Setup(d => d.GetFiles("foo", "*.mps", SearchOption.AllDirectories)) + .Returns(expectedFiles.ToArray); + var files = m_Service.ListFilesToDeploy(new List + { + "foo" + }, ".mps"); + expectedFiles.Sort(); + CollectionAssert.AreEqual(expectedFiles, files); + } + + [Test] + public void ListFilesToDeployEnumeratesDirectoriesRemoveDuplicate() + { + var expectedFiles = new List + { + "c.mps", + "b.mps", + "a.mps", + "c.mps" + }; + m_MockPath.Setup(p => p.GetFullPath("foo")).Returns("foo"); m_MockDirectory.Setup(f => f.Exists("foo")).Returns(true); m_MockDirectory.Setup(d => d.GetFiles("foo", "*.mps", SearchOption.AllDirectories)) .Returns(expectedFiles.ToArray); @@ -88,6 +130,7 @@ public void ListFilesToDeployEnumeratesDirectoriesSorted() { "foo" }, ".mps"); + expectedFiles = expectedFiles.Distinct().ToList(); expectedFiles.Sort(); CollectionAssert.AreEqual(expectedFiles, files); } @@ -97,4 +140,43 @@ public void ListFilesToDeployOnEmptyInputThrowDeployException() { Assert.Throws(() => m_Service.ListFilesToDeploy(new List(), ".mps")); } + + [Test] + public async Task LoadContentAsyncSuccessful() + { + m_MockFile.Setup(f => f.Exists("foo")).Returns(true); + m_MockFile.Setup(f => f.ReadAllTextAsync("foo", CancellationToken.None)).ReturnsAsync("{}"); + var content = await m_Service.LoadContentAsync("foo", CancellationToken.None); + Assert.AreEqual("{}", content); + } + + [Test] + public void LoadContentAsyncFailedWithFileNotFound() + { + m_MockFile.Setup(f => f.Exists("foo")).Returns(true); + m_MockFile.Setup(f => f.ReadAllTextAsync("foo", CancellationToken.None)) + .ThrowsAsync(new FileNotFoundException()); + + Assert.ThrowsAsync(async() => await m_Service.LoadContentAsync("foo", CancellationToken.None)); + } + + [Test] + public void LoadContentAsyncFailedWithUnauthorizedAccess() + { + m_MockFile.Setup(f => f.Exists("foo")).Returns(true); + m_MockFile.Setup(f => f.ReadAllTextAsync("foo", CancellationToken.None)) + .ThrowsAsync(new UnauthorizedAccessException()); + + Assert.ThrowsAsync(async() => await m_Service.LoadContentAsync("foo", CancellationToken.None)); + } + + [Test] + public void LoadContentAsyncFailedWithUnexpectedException() + { + m_MockFile.Setup(f => f.Exists("foo")).Returns(true); + m_MockFile.Setup(f => f.ReadAllTextAsync("foo", CancellationToken.None)) + .ThrowsAsync(new Exception()); + + Assert.ThrowsAsync(async() => await m_Service.LoadContentAsync("foo", CancellationToken.None)); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Unity.Services.Cli.Deploy.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Unity.Services.Cli.Deploy.UnitTest.csproj index 4a6c278..835c72f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Unity.Services.Cli.Deploy.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy.UnitTest/Unity.Services.Cli.Deploy.UnitTest.csproj @@ -10,7 +10,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/DeployModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/DeployModule.cs index 9c552ee..5af6338 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy/DeployModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/DeployModule.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging; using Unity.Services.Cli.Common; using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Input; +using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Deploy.Input; using Unity.Services.Cli.Deploy.Handlers; using Unity.Services.Cli.Deploy.Service; @@ -25,11 +27,15 @@ public DeployModule() $"Deploy configuration files of supported services to the backend.{Environment.NewLine}" + "Services currently supported are: remote-config, cloud-code.") { - DeployInput.PathsArgument + DeployInput.PathsArgument, + CommonInput.EnvironmentNameOption, + CommonInput.CloudProjectIdOption }; ModuleRootCommand.SetHandler< IHost, DeployInput, + IDeployFileService, + IUnityEnvironment, ILogger, ILoadingIndicator, CancellationToken>( @@ -43,6 +49,7 @@ public static void RegisterServices(HostBuilderContext hostBuilderContext, IServ { serviceCollection.AddTransient(_ => new FileSystem().File); serviceCollection.AddTransient(_ => new FileSystem().Directory); + serviceCollection.AddTransient(_ => new FileSystem().Path); serviceCollection.AddTransient(); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/FetchModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/FetchModule.cs new file mode 100644 index 0000000..f30f7f1 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/FetchModule.cs @@ -0,0 +1,45 @@ +using System.CommandLine; +using System.IO.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Unity.Services.Cli.Common; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Input; +using Unity.Services.Cli.Deploy.Input; +using Unity.Services.Cli.Deploy.Handlers; +using Unity.Services.Cli.Deploy.Service; + +namespace Unity.Services.Cli.Deploy; + +/// +/// Deploy Module to achieve services deploy command +/// +public class FetchModule : ICommandModule +{ + readonly string[] m_SupportedServices = { "remote-confg" }; + public Command? ModuleRootCommand { get; } + + public FetchModule() + { + var servicesStr = string.Join(", ", m_SupportedServices); + ModuleRootCommand = new Command( + "fetch", + $"Fetch configuration files of supported services from the backend.{Environment.NewLine}" + + $"Services currently supported are: {servicesStr}.") + { + FetchInput.PathArgument, + FetchInput.ReconcileOption, + FetchInput.DryRunOption, + CommonInput.EnvironmentNameOption, + CommonInput.CloudProjectIdOption + }; + ModuleRootCommand.SetHandler< + IHost, + FetchInput, + ILogger, + ILoadingIndicator, + CancellationToken>( + FetchHandler.FetchAsync); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/DeployHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/DeployHandler.cs index dc5e77f..9897be7 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/DeployHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/DeployHandler.cs @@ -5,8 +5,10 @@ using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Deploy.Input; using Unity.Services.Cli.Deploy.Model; +using Unity.Services.Cli.Deploy.Service; namespace Unity.Services.Cli.Deploy.Handlers; @@ -15,6 +17,8 @@ static class DeployHandler public static async Task DeployAsync( IHost host, DeployInput input, + IDeployFileService deployFileService, + IUnityEnvironment unityEnvironment, ILogger logger, ILoadingIndicator loadingIndicator, CancellationToken cancellationToken @@ -24,6 +28,8 @@ await loadingIndicator.StartLoadingAsync($"Deploying files...", context => DeployAsync( host, input, + deployFileService, + unityEnvironment, logger, context, cancellationToken)); @@ -32,6 +38,8 @@ await loadingIndicator.StartLoadingAsync($"Deploying files...", internal static async Task DeployAsync( IHost host, DeployInput input, + IDeployFileService deployFileService, + IUnityEnvironment unityEnvironment, ILogger logger, StatusContext? loadingContext, CancellationToken cancellationToken @@ -45,7 +53,16 @@ CancellationToken cancellationToken logger.LogInformation("Currently supported services are: {SupportedServicesStr}", supportedServicesStr); } - var tasks = services.Select(m => m.Deploy(input, loadingContext, cancellationToken)).ToArray(); + var environmentId = await unityEnvironment.FetchIdentifierAsync(); + var projectId = input.CloudProjectId!; + var tasks = services.Select( + m => m.Deploy( + input, + deployFileService.ListFilesToDeploy(input.Paths, m.DeployFileExtension), + projectId, + environmentId, + loadingContext, + cancellationToken)).ToArray(); try { await Task.WhenAll(tasks); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/FetchHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/FetchHandler.cs new file mode 100644 index 0000000..5630d4f --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/FetchHandler.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Spectre.Console; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.Deploy.Input; +using Unity.Services.Cli.Deploy.Model; +using Unity.Services.Cli.Deploy.Service; + +namespace Unity.Services.Cli.Deploy.Handlers; + +static class FetchHandler +{ + public static async Task FetchAsync( + IHost host, + FetchInput input, + ILogger logger, + ILoadingIndicator loadingIndicator, + CancellationToken cancellationToken + ) + { + await loadingIndicator.StartLoadingAsync($"Fetching files...", + context => FetchAsync( + host, + input, + logger, + context, + cancellationToken)); + } + + internal static async Task FetchAsync( + IHost host, + FetchInput input, + ILogger logger, + StatusContext? loadingContext, + CancellationToken cancellationToken) + { + var services = host.Services.GetServices().ToList(); + + if (string.IsNullOrEmpty(input.Path)) + { + var supportedServicesStr = string.Join(", ", services.Select(s => s.ServiceType)); + logger.LogInformation("Currently supported services are: {supportedServicesStr}", supportedServicesStr); + } + + var fetchResult = Array.Empty(); + Task? fetchAll = null; + try + { + fetchAll = Task.WhenAll( + services.Select(m => m.FetchAsync( + input, + loadingContext, + cancellationToken))); + fetchResult = await fetchAll; + } + catch { /* will use fetchAll to find all errors */ } + + var totalResult = new FetchResult(fetchResult); + + logger.LogResultValue(totalResult); + + if (fetchAll!.IsFaulted) + { + var exception = fetchAll.Exception; + if (exception != null + && exception.InnerExceptions.Count == 1 + && exception.InnerException is CliException cliException) + { + throw new CliException(cliException.Message, cliException, cliException.ExitCode); + } + throw new CliException("Failed to fetch due to an unexpected error", fetchAll.Exception!, ExitCode.UnhandledError); + } + + if (totalResult.Failed.Any()) + { + throw new CliException($"One or more files failed to be fetched", ExitCode.HandledError); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/NewFileHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/NewFileHandler.cs new file mode 100644 index 0000000..00b6c41 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Handlers/NewFileHandler.cs @@ -0,0 +1,38 @@ +using System.CommandLine; +using System.IO.Abstractions; +using Microsoft.Extensions.Logging; +using Unity.Services.Cli.Common; +using Unity.Services.Cli.Deploy.Input; +using Unity.Services.Cli.Deploy.Templates; + +namespace Unity.Services.Cli.Deploy.Handlers; + +public static class NewFileHandler +{ + public static Command AddNewFileCommand(this Command? self, string serviceName) + where T : IFileTemplate + { + Command newFileCommand = new("new-file", $"Create new {serviceName} config file.") + { + NewFileInput.FileArgument + }; + + newFileCommand.SetHandler + ((input, file, logger, token) => NewFileAsync(input, file, Activator.CreateInstance(), logger, token)); + + return newFileCommand; + } + + public static async Task NewFileAsync( + NewFileInput input, + IFile file, + T template, + ILogger logger, + CancellationToken cancellationToken) + where T : IFileTemplate + { + input.File = Path.ChangeExtension(input.File!, template.Extension); + await file.WriteAllTextAsync(input.File, template.FileBodyText, cancellationToken); + logger.LogInformation("Config file {input.File!} created successfully!", input.File!); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/DeployInput.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/DeployInput.cs index ef4fda0..af77f26 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/DeployInput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/DeployInput.cs @@ -10,9 +10,10 @@ public class DeployInput : CommonInput { public static readonly Argument> PathsArgument = new( "paths", - $"The paths to deploy from. Accepts multiple directory or file paths. Specify '.' to deploy in current directory"); + "The paths to deploy from. Accepts multiple directory or file paths. Specify '.' to deploy in current directory"); [InputBinding(nameof(PathsArgument))] - public ICollection Paths { get; set; } = new List(); + public IReadOnlyList Paths { get; set; } = new List(); + } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/FetchInput.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/FetchInput.cs new file mode 100644 index 0000000..c6ff311 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/FetchInput.cs @@ -0,0 +1,34 @@ +using System.CommandLine; +using Unity.Services.Cli.Common.Input; + +namespace Unity.Services.Cli.Deploy.Input; + +/// +/// Fetch command input +/// +public class FetchInput : CommonInput +{ + public static readonly Argument PathArgument = new( + "path", + $"The path to fetch to. Accepts a directory path to fetch to. Specify '.' to fetch in current directory"); + + [InputBinding(nameof(PathArgument))] + public string Path { get; set; } = string.Empty; + + public static readonly Option ReconcileOption = new(new[] + { + "--reconcile" + }, "Content that is not updated will be created at the root."); + + [InputBinding(nameof(ReconcileOption))] + public bool Reconcile { get; set; } = false; + + public static readonly Option DryRunOption = new(new[] + { + "--dry-run" + }, "Perform a trial run with no changes made."); + + [InputBinding(nameof(DryRunOption))] + public bool DryRun { get; set; } = false; + +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/NewFileInput.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/NewFileInput.cs new file mode 100644 index 0000000..3ddaf56 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Input/NewFileInput.cs @@ -0,0 +1,15 @@ +using System.CommandLine; +using Unity.Services.Cli.Common.Input; + +namespace Unity.Services.Cli.Deploy.Input; + +public class NewFileInput : CommonInput +{ + public static readonly Argument FileArgument = new( + "file name", + () => "new_config", + "The name of the file to create"); + + [InputBinding(nameof(FileArgument))] + public string? File { get; set; } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Model/FetchResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Model/FetchResult.cs new file mode 100644 index 0000000..00fabdc --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Model/FetchResult.cs @@ -0,0 +1,90 @@ +using System.Text; + +namespace Unity.Services.Cli.Deploy.Model; + +[Serializable] +public class FetchResult +{ + public IReadOnlyList Fetched { get; } + public IReadOnlyList Failed { get; } + public IReadOnlyList Updated { get; } + public IReadOnlyList Created { get; } + public IReadOnlyList Deleted { get; } + + public FetchResult( + IReadOnlyList updated, + IReadOnlyList deleted, + IReadOnlyList created, + IReadOnlyList fetched, + IReadOnlyList failed) + { + Updated = updated; + Created = created; + Deleted = deleted; + Fetched = fetched; + Failed = failed; + } + + public FetchResult(IReadOnlyList results) + { + Updated = results.SelectMany( r=> r.Updated).ToList(); + Created = results.SelectMany( r=> r.Created).ToList(); + Deleted = results.SelectMany( r=> r.Deleted).ToList(); + Fetched = results.SelectMany(r => r.Fetched).ToList(); + Failed = results.SelectMany( r=> r.Failed).ToList(); + } + + public override string ToString() + { + var result = new StringBuilder(); + AddFetched(result); + AddFailed(result); + + BuildResult(result, Updated, "Updated"); + BuildResult(result, Deleted, "Deleted"); + BuildResult(result, Created, "Created"); + + return result.ToString(); + } + + private void AddFetched(StringBuilder result) + { + if (Fetched.Any()) + { + var deployedListString = string.Join($"{Environment.NewLine} ", Fetched); + result.Append( + $"Successfully fetched into the following files:{Environment.NewLine} {deployedListString}"); + } + else + { + result.Append("No content fetched"); + } + } + + private void AddFailed(StringBuilder result) + { + if (!Failed.Any()) + return; + + result.AppendLine(); + + foreach (var file in Failed) + { + result.Append($"{Environment.NewLine}Failed to fetch:"); + result.Append($"{Environment.NewLine} '{Path.GetFileName(file)}'"); + } + } + + static void BuildResult(StringBuilder strBuilder, IReadOnlyList results, string resultHeader) + { + if (!results.Any()) + return; + + strBuilder.Append(Environment.NewLine); + strBuilder.Append($"{Environment.NewLine}{resultHeader}:"); + foreach (var updated in results) + { + strBuilder.Append($"{Environment.NewLine} {updated}"); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/DeployFileService.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/DeployFileService.cs index d5611f2..6468bc8 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/DeployFileService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/DeployFileService.cs @@ -8,41 +8,70 @@ class DeployFileService : IDeployFileService { readonly IFile m_File; readonly IDirectory m_Directory; - - public DeployFileService(IFile file, IDirectory directory) + readonly IPath m_Path; + public DeployFileService(IFile file, IDirectory directory, IPath path) { m_File = file; m_Directory = directory; + m_Path = path; } - public IReadOnlyList ListFilesToDeploy(ICollection paths, string extension) + public IReadOnlyList ListFilesToDeploy(IReadOnlyList paths, string extension) { if (!paths.Any()) { - throw new DeployException("Please specify at least one path to deploy.", ExitCode.HandledError); + throw new DeployException("Please specify at least one path to deploy."); } var files = new List(); foreach (var path in paths) { - if (m_File.Exists(path)) + var fullPath = m_Path.GetFullPath(path); + + if (m_File.Exists(fullPath)) { - if (string.Equals(Path.GetExtension(path), extension)) + if (string.Equals(Path.GetExtension(fullPath), extension)) { - files.Add(path); + files.Add(fullPath); } } - else if (m_Directory.Exists(path)) + else if (m_Directory.Exists(fullPath)) { - files.AddRange(m_Directory.GetFiles(path, $"*{extension}", SearchOption.AllDirectories)); + try + { + files.AddRange(m_Directory.GetFiles(fullPath, $"*{extension}", SearchOption.AllDirectories)); + } + catch (UnauthorizedAccessException) + { + throw new CliException($"CLI does not have the permissions to access \"{fullPath}\"", ExitCode.HandledError); + } } else { - throw new PathNotFoundException(path); + throw new PathNotFoundException($"\"{fullPath}\""); } } + files = files.Distinct().ToList(); files.Sort(); return files; } + + public async Task LoadContentAsync(string filePath, CancellationToken cancellationToken) + { + try + { + return await m_File.ReadAllTextAsync(filePath, cancellationToken); + } + catch (FileNotFoundException exception) + { + throw new CliException(exception.Message, ExitCode.HandledError); + } + catch (UnauthorizedAccessException exception) + { + throw new CliException(string.Join(" ", exception.Message, + "Make sure that the CLI has the permissions to access the file and that the " + + "specified path points to a file and not a directory."), ExitCode.HandledError); + } + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IDeployFileService.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IDeployFileService.cs index 212c998..7fcecd5 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IDeployFileService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IDeployFileService.cs @@ -8,5 +8,6 @@ public interface IDeployFileService /// list of file or directory paths to evaluate /// target file extension. For example ".js" to look for java script file /// paths of files with target file extension - IReadOnlyList ListFilesToDeploy(ICollection paths, string extension); + IReadOnlyList ListFilesToDeploy(IReadOnlyList paths, string extension); + Task LoadContentAsync(string filePath, CancellationToken cancellationToken); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IDeploymentService.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IDeploymentService.cs index fab5095..3ef839d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IDeploymentService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IDeploymentService.cs @@ -1,15 +1,19 @@ using Spectre.Console; +using Unity.Services.Cli.Deploy.Input; using Unity.Services.Cli.Deploy.Model; -namespace Unity.Services.Cli.Deploy.Input; +namespace Unity.Services.Cli.Deploy.Service; public interface IDeploymentService { string ServiceType { get; } - protected string DeployFileExtension { get; } + public string DeployFileExtension { get; } Task Deploy( - DeployInput input, + DeployInput deployInput, + IReadOnlyList filePaths, + string projectId, + string environmentId, StatusContext? loadingContext, CancellationToken cancellationToken); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IFetchService.cs new file mode 100644 index 0000000..7f60a18 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Service/IFetchService.cs @@ -0,0 +1,16 @@ +using Spectre.Console; +using Unity.Services.Cli.Deploy.Input; +using Unity.Services.Cli.Deploy.Model; + +namespace Unity.Services.Cli.Deploy.Service; + +public interface IFetchService +{ + string ServiceType { get; } + protected string FileExtension { get; } + + Task FetchAsync( + FetchInput input, + StatusContext? loadingContext, + CancellationToken cancellationToken); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Templates/IFileTemplate.cs b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Templates/IFileTemplate.cs new file mode 100644 index 0000000..c581278 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Templates/IFileTemplate.cs @@ -0,0 +1,17 @@ +namespace Unity.Services.Cli.Deploy.Templates; + +/// +/// Interface to provide template for new file command +/// +public interface IFileTemplate +{ + /// + /// File extension + /// + string Extension { get; } + + /// + /// File body content + /// + string FileBodyText { get; } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Unity.Services.Cli.Deploy.csproj b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Unity.Services.Cli.Deploy.csproj index 6f67939..3e27fe6 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Deploy/Unity.Services.Cli.Deploy.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Deploy/Unity.Services.Cli.Deploy.csproj @@ -22,8 +22,6 @@ $(DefineConstants);$(ExtraDefineConstants) - - - TRACE; + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Cli.Environment.UnitTest/Unity.Services.Cli.Environment.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Environment.UnitTest/Unity.Services.Cli.Environment.UnitTest.csproj index 3f5f700..0fdad1b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Environment.UnitTest/Unity.Services.Cli.Environment.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Environment.UnitTest/Unity.Services.Cli.Environment.UnitTest.csproj @@ -10,7 +10,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Environment/Unity.Services.Cli.Environment.csproj b/Unity.Services.Cli/Unity.Services.Cli.Environment/Unity.Services.Cli.Environment.csproj index 02ba3bc..931e506 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Environment/Unity.Services.Cli.Environment.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Environment/Unity.Services.Cli.Environment.csproj @@ -14,7 +14,7 @@ - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliFixture.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliFixture.cs index 3d48da4..af6e793 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliFixture.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliFixture.cs @@ -120,6 +120,12 @@ public void DeleteLocalCredentials() } } + public void SetupProjectAndEnvironment() + { + SetConfigValue("project-id", CommonKeys.ValidProjectId); + SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); + } + string GetBackUpConfigFile(string original) => original + $".{GetType()}.back"; protected static UgsCliTestCase GetLoggedInCli() diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/ConfigTests/ConfigTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/ConfigTests/ConfigTests.cs index 4e441cf..51295a3 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/ConfigTests/ConfigTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/ConfigTests/ConfigTests.cs @@ -49,11 +49,11 @@ public async Task ConfigGetReadsFromConfigFile() [Test] public async Task ConfigGetJsonReturnsJson() { - const string expected = @"{ - ""Result"": ""some-value"", - ""Messages"": [] -} -"; + var expected = JsonConvert.SerializeObject(new + { + Result = "some-value", + Messages = Array.Empty() + }, Formatting.Indented); await new UgsCliTestCase() .Command("config set environment-name some-value") .Command("config get environment-name -j") diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Deploy/DeployTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Deploy/DeployTests.cs index 81fdba0..7394f86 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Deploy/DeployTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Deploy/DeployTests.cs @@ -18,7 +18,7 @@ public class DeployTests : UgsCliFixture { const string k_ConfigId = "config-id"; - static readonly string k_TestDirectory = Path.Combine(UgsCliBuilder.RootDirectory, ".tmp/FilesDir"); + static readonly string k_TestDirectory = Path.GetFullPath(Path.Combine(UgsCliBuilder.RootDirectory, ".tmp/FilesDir")); readonly IReadOnlyList m_DeployedTestCases = new[] { @@ -142,9 +142,9 @@ public async Task DeployInvalidPath() { SetConfigValue("project-id", CommonKeys.ValidProjectId); SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); - const string invalidDirectory = "invalid-directory"; + var invalidDirectory = Path.GetFullPath("invalid-directory"); var expectedOutput = $"[Error]: {Environment.NewLine}" - + $" Path {invalidDirectory} could not be found.{Environment.NewLine}"; + + $" Path \"{invalidDirectory}\" could not be found.{Environment.NewLine}"; await GetLoggedInCli() .Command($"deploy {invalidDirectory}") @@ -202,6 +202,18 @@ await GetLoggedInCli() .ExecuteAsync(); } + [Test] + public async Task DeployValidConfigWithOptionsSucceed() + { + await CreateDeployTestFilesAsync(m_DeployedTestCases, m_DeployedContents); + var deployedConfigFileString = string.Join(Environment.NewLine + " ", m_DeployedTestCases.Select(r => r.ConfigFileName)); + await GetLoggedInCli() + .Command($"deploy {k_TestDirectory} -p {CommonKeys.ValidProjectId} -e {CommonKeys.ValidEnvironmentName}") + .AssertStandardOutputContains($"Successfully deployed the following contents:{Environment.NewLine} {deployedConfigFileString}") + .AssertNoErrors() + .ExecuteAsync(); + } + [Test] public async Task DeployNoConfigFromDirectorySucceed() { diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/EnvTests/EnvTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/EnvTests/EnvTests.cs index b986fde..2b23bcc 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/EnvTests/EnvTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/EnvTests/EnvTests.cs @@ -1,9 +1,13 @@ +using System; using System.Linq; using System.Threading.Tasks; +using Newtonsoft.Json; using NUnit.Framework; using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.Common.Networking; using Unity.Services.Cli.MockServer; +using Unity.Services.Gateway.IdentityApiV1.Generated.Model; namespace Unity.Services.Cli.IntegrationTest.EnvTests; @@ -77,18 +81,14 @@ await GetLoggedInCli() [Test] public async Task EnvironmentListWithJsonOptionReturnsZeroExitCode() { - var expectedReturn = string.Format(@"{{ - ""Result"": [ - {{ - ""id"": ""{0}"", - ""projectId"": ""{1}"", - ""name"": ""production"", - ""isDefault"": true,", CommonKeys.ValidEnvironmentId, "390121ca-bb43-494f-b418-55be4e0c0faf"); - SetConfigValue("project-id", CommonKeys.ValidProjectId); await GetLoggedInCli() .Command("env list -j") - .AssertStandardOutputContains(expectedReturn) + .AssertStandardOutput(output => + { + Assert.DoesNotThrow(()=>JsonConvert.DeserializeObject(output)); + StringAssert.Contains(CommonKeys.ValidEnvironmentId, output); + }) .AssertNoErrors() .ExecuteAsync(); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/FetchTest/FetchTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/FetchTest/FetchTests.cs new file mode 100644 index 0000000..055113a --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/FetchTest/FetchTests.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NUnit.Framework; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.Deploy.Model; +using Unity.Services.Cli.IntegrationTest.Deploy; +using Unity.Services.Cli.IntegrationTest.EnvTests; +using Unity.Services.Cli.IntegrationTest.RemoteConfigTests.Mock; +using Unity.Services.Cli.MockServer; +using WireMock.Admin.Mappings; + +namespace Unity.Services.Cli.IntegrationTest.FetchTest; + +public class FetchTests : UgsCliFixture +{ + const string k_ConfigId = "config-id"; + + static readonly string k_TestDirectory = Path.Combine(UgsCliBuilder.RootDirectory, ".tmp/FilesDir"); + + readonly IReadOnlyList m_DeployedTestCases = new[] + { + new DeployTestCase( + "{ \"entries\" : { \"color\" : \"Red\" } }", + "color.rc", + "Remote Config", + 100, + "Deployed", + "Deployed Successfully", + k_TestDirectory), + new DeployTestCase( + "{ \"entries\" : { \"ready\" : \"True\" } }", + "ready.rc", + "Remote Config", + 100, + "Deployed", + "Deployed Successfully", + k_TestDirectory) + }; + + readonly IReadOnlyList m_FailedTestCases = new[] + { + new DeployTestCase( + "{ \"entries\" : { \"Ready : \"True\" } }", + "invalid1.rc", + "Remote Config", + 0, + "Failed To Read", + "Invalid character after parsing property name. Expected ':' but got: T. Path 'entries', line 1, position 26.", + k_TestDirectory) + }; + + // Since Remote Config open api is having issues we should use our mock to map the models for now + readonly RemoteConfigMock m_RemoteConfigMock = new(CommonKeys.ValidProjectId, CommonKeys.ValidEnvironmentId); + + IEnumerable? m_CloudCodeModels; + + const string k_CloudCodeOpenApiUrl = "https://services.docs.unity.com/specs/v1/636c6f75642d636f64652d61646d696e.yaml"; + + List m_DeployedContents = new(); + List m_FailedContents = new(); + + [OneTimeSetUp] + public void OneTimeSetUp() + { + m_RemoteConfigMock.MockApi.InitServer(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + m_RemoteConfigMock.MockApi.Server?.Dispose(); + } + + [SetUp] + public async Task SetUp() + { + DeleteLocalConfig(); + DeleteLocalCredentials(); + + if (Directory.Exists(k_TestDirectory)) + { + Directory.Delete(k_TestDirectory, true); + } + + m_DeployedContents.Clear(); + m_FailedContents.Clear(); + m_RemoteConfigMock.MockApi.Server?.ResetMappings(); + + m_RemoteConfigMock.MockGetAllConfigsFromEnvironmentAsync(k_ConfigId); + m_RemoteConfigMock.MockUpdateConfigAsync(k_ConfigId); + + Directory.CreateDirectory(k_TestDirectory); + + var environmentModels = await IdentityV1MockServerModels.GetModels(); + + m_RemoteConfigMock.MockApi.Server?.WithMapping(environmentModels.ToArray()); + + m_CloudCodeModels = await MappingModelUtils.ParseMappingModelsAsync( + k_CloudCodeOpenApiUrl, + new()); + m_CloudCodeModels = m_CloudCodeModels.Select( + m => m.ConfigMappingPathWithKey(CommonKeys.ProjectIdKey, CommonKeys.ValidProjectId) + .ConfigMappingPathWithKey(CommonKeys.EnvironmentIdKey, CommonKeys.ValidEnvironmentId)); + + MapCloudCodeModels(); + } + + [TearDown] + public void TearDown() + { + DeleteLocalConfig(); + DeleteLocalCredentials(); + + if (Directory.Exists(k_TestDirectory)) + { + Directory.Delete(k_TestDirectory, true); + } + } + + static async Task CreateDeployTestFilesAsync(IReadOnlyList testCases, ICollection contents) + { + foreach (var testCase in testCases) + { + await File.WriteAllTextAsync(testCase.ConfigFilePath, testCase.ConfigValue); + contents.Add(testCase.DeployedContent); + } + } + + [Test] + public async Task FetchInvalidPath() + { + SetConfigValue("project-id", CommonKeys.ValidProjectId); + SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); + var invalidDirectory = Path.GetFullPath("invalid-directory"); + var expectedOutput = $"[Error]: {Environment.NewLine}" + + $" Path \"{invalidDirectory}\" could not be found.{Environment.NewLine}"; + + await GetLoggedInCli() + .Command($"fetch {invalidDirectory}") + .AssertStandardOutputContains(expectedOutput) + .AssertExitCode(ExitCode.HandledError) + .ExecuteAsync(); + } + + [Test] + public async Task FetchValidConfigFromDirectorySucceed() + { + SetConfigValue("project-id", CommonKeys.ValidProjectId); + SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); + await CreateDeployTestFilesAsync(m_DeployedTestCases, m_DeployedContents); + + await GetLoggedInCli() + .Command($"fetch {k_TestDirectory}") + .AssertStandardOutput(output => + { + StringAssert.Contains($"Successfully fetched into the following files:{Environment.NewLine}", output); + foreach (var file in m_DeployedTestCases) + { + StringAssert.Contains(file.ConfigFileName, output); + } + }).AssertNoErrors() + .ExecuteAsync(); + } + + [Test] + public async Task FetchNoConfigFromDirectorySucceed() + { + SetConfigValue("project-id", CommonKeys.ValidProjectId); + SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); + await GetLoggedInCli() + .Command($"fetch {k_TestDirectory}") + .AssertStandardOutputContains($"No content fetched") + .AssertNoErrors() + .ExecuteAsync(); + } + + [Test] + public async Task FetchNoConfigFromDirectoryWithOptionSucceed() + { + await GetLoggedInCli() + .Command($"fetch {k_TestDirectory} -p {CommonKeys.ValidProjectId} -e {CommonKeys.ValidEnvironmentName}") + .AssertStandardOutputContains($"No content fetched") + .AssertNoErrors() + .ExecuteAsync(); + } + + [Test] + public async Task FetchValidConfigFromDirectorySucceedWithJsonOutput() + { + SetConfigValue("project-id", CommonKeys.ValidProjectId); + SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); + await CreateDeployTestFilesAsync(m_DeployedTestCases, m_DeployedContents); + var fetchedPaths = m_DeployedTestCases + .Select(t => Path.GetRelativePath(UgsCliBuilder.RootDirectory, t.ConfigFilePath)) + .ToArray(); + + var deletedKeys = m_DeployedTestCases + .Select(t => $"Key '{Path.GetFileNameWithoutExtension(t.ConfigFileName)}' " + + $"in file '{Path.GetFullPath(t.ConfigFilePath)}'") + .ToArray(); + + var logResult = new + { + Result = new FetchResult( + Array.Empty(), + deletedKeys, + Array.Empty(), + fetchedPaths, + Array.Empty()), + Messages = Array.Empty() + }; + var resultString = JsonConvert.SerializeObject(logResult, Formatting.Indented); + await GetLoggedInCli() + .Command($"fetch {k_TestDirectory} -j") + .AssertStandardOutputContains(resultString) + .AssertNoErrors() + .ExecuteAsync(); + } + + void MapCloudCodeModels() + { + var cloudCodeModels = m_CloudCodeModels as MappingModel[] ?? m_CloudCodeModels?.ToArray(); + cloudCodeModels = cloudCodeModels?.Select(m => m.ConfigMappingPathWithKey("scripts", "Script")).ToArray(); + m_RemoteConfigMock.MockApi.Server?.WithMapping(cloudCodeModels ?? Array.Empty()); + } +} + diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/NewFile/NewFileTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/NewFile/NewFileTests.cs new file mode 100644 index 0000000..1bbade7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/NewFile/NewFileTests.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Unity.Services.Cli.IntegrationTest.NewFile; + +public class NewFileTests : UgsCliFixture +{ + [Test] + public async Task NewFileCreatedWithNoErrorsAndCorrectOutput() + { + var newFileOutPutString = $"[Information]: {Environment.NewLine} Config file new_config.rc created successfully!{Environment.NewLine}"; + await new UgsCliTestCase() + .Command($"rc new-file") + .AssertStandardOutputContains(newFileOutPutString) + .AssertNoErrors() + .ExecuteAsync(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Unity.Services.Cli.IntegrationTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Unity.Services.Cli.IntegrationTest.csproj index 7119055..bff6d69 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Unity.Services.Cli.IntegrationTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Unity.Services.Cli.IntegrationTest.csproj @@ -6,10 +6,11 @@ false - + + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Unity.Services.Cli.Lobby.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Unity.Services.Cli.Lobby.UnitTest.csproj index 376acc0..da32eea 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Unity.Services.Cli.Lobby.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Unity.Services.Cli.Lobby.UnitTest.csproj @@ -10,7 +10,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.MockServer/MappingModelUtils.cs b/Unity.Services.Cli/Unity.Services.Cli.MockServer/MappingModelUtils.cs index 322d681..40a2bbe 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.MockServer/MappingModelUtils.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.MockServer/MappingModelUtils.cs @@ -38,7 +38,6 @@ public static MappingModel ConfigMappingPathWithKey(this MappingModel model, str /// model with replaced value public static MappingModel ConfigBodyAsJsonResponse(this MappingModel model, object response) { - //We are looking for the following pattern key/{old-value} or and replace with key/{value} model.Response.BodyAsJson = response; return model; } @@ -79,6 +78,8 @@ public static async Task> ParseMappingModelsAsync(stri else { var openApiPath = Path.Combine(k_RepositoryPath, inputSpec); + openApiPath = openApiPath.Replace('/', Path.DirectorySeparatorChar); + openApiPath = openApiPath.Replace('\\', Path.DirectorySeparatorChar); if (!File.Exists(openApiPath)) { throw new FileNotFoundException($"Open API source does not exist: {openApiPath}"); diff --git a/Unity.Services.Cli/Unity.Services.Cli.MockServer/Unity.Services.Cli.MockServer.csproj b/Unity.Services.Cli/Unity.Services.Cli.MockServer/Unity.Services.Cli.MockServer.csproj index 8bd22e1..9a54f79 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.MockServer/Unity.Services.Cli.MockServer.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.MockServer/Unity.Services.Cli.MockServer.csproj @@ -6,8 +6,8 @@ true - - + + diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/CliRemoteConfigDeploymentHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/CliRemoteConfigDeploymentHandlerTests.cs index 96f933c..c34b43a 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/CliRemoteConfigDeploymentHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/CliRemoteConfigDeploymentHandlerTests.cs @@ -1,12 +1,13 @@ +using System.IO.Abstractions; using Moq; using NUnit.Framework; using Unity.Services.Cli.Deploy.Model; using Unity.Services.Cli.RemoteConfig.Deploy; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Formatting; -using Unity.Services.RemoteConfig.Editor.Authoring.Core.IO; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Json; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Model; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Networking; +using IFileSystem = Unity.Services.RemoteConfig.Editor.Authoring.Core.IO.IFileSystem; namespace Unity.Services.Cli.RemoteConfig.UnitTest.Deploy; @@ -19,7 +20,7 @@ public class CliRemoteConfigDeploymentHandlerTests readonly Mock m_FormatValidator = new(); readonly Mock m_ConfigMerger = new(); readonly Mock m_JsonConverter = new(); - readonly Mock m_FileReader = new(); + readonly Mock m_FileReader = new(); class DeploymentHandlerForTest : CliRemoteConfigDeploymentHandler { @@ -44,8 +45,16 @@ public DeploymentHandlerForTest( IFormatValidator formatValidator, IConfigMerger configMerger, IJsonConverter jsonConverter, - IFileReader fileReader) - : base(remoteConfigClient, remoteConfigParser, remoteConfigValidator, formatValidator, configMerger, jsonConverter, fileReader) { } + IFileSystem fileSystem) + : base(remoteConfigClient, + remoteConfigParser, + remoteConfigValidator, + formatValidator, + configMerger, + jsonConverter, + fileSystem) + { + } } DeploymentHandlerForTest? m_DeploymentHandlerForTest; @@ -70,7 +79,8 @@ public void Deploy_DoesNotThrow() { Assert.DoesNotThrowAsync(async () => { - await m_DeploymentHandlerForTest!.DeployAsync(Array.Empty(), false); + await m_DeploymentHandlerForTest!.DeployAsync( + Array.Empty(), false); }); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/JsonConverterTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/JsonConverterTests.cs index 22e36fc..5477aca 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/JsonConverterTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/JsonConverterTests.cs @@ -1,5 +1,6 @@ +using Newtonsoft.Json; using NUnit.Framework; -using Unity.Services.Cli.RemoteConfig.Deploy; +using JsonConverter = Unity.Services.Cli.RemoteConfig.Deploy.JsonConverter; namespace Unity.Services.Cli.RemoteConfig.UnitTest.Deploy; @@ -17,7 +18,7 @@ class JsonConverterTests public JsonConverterTests() { - m_ExpectedJson = Newtonsoft.Json.JsonConvert.SerializeObject(m_TestData); + m_ExpectedJson = Newtonsoft.Json.JsonConvert.SerializeObject(m_TestData, Formatting.Indented); } [Test] diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentServiceTests.cs index 37d2449..5bd28cb 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentServiceTests.cs @@ -1,12 +1,11 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -using Spectre.Console; -using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Deploy.Input; using Unity.Services.Cli.Deploy.Model; using Unity.Services.Cli.Deploy.Service; using Unity.Services.Cli.RemoteConfig.Deploy; +using Unity.Services.Cli.RemoteConfig.Exceptions; using Unity.Services.Cli.RemoteConfig.Service; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Deployment; using Unity.Services.RemoteConfig.Editor.Authoring.Core.ErrorHandling; @@ -17,10 +16,9 @@ namespace Unity.Services.Cli.RemoteConfig.UnitTest.Deploy; [TestFixture] public class RemoteConfigDeploymentServiceTests { - RemoteConfigDeploymentService? m_RemoteConfigDeploymentService; + const string k_ValidProjectId = "a912b1fd-541d-42e1-89f2-85436f27aabd"; const string k_ValidEnvironmentId = "00000000-0000-0000-0000-000000000000"; - const string k_DeployFileExtension = ".rc"; static readonly List k_ValidFilePaths = new() { @@ -28,16 +26,16 @@ public class RemoteConfigDeploymentServiceTests "test_b.rc" }; - readonly Mock m_MockUnityEnvironment = new(); readonly Mock m_MockCliRemoteConfigClient = new(); readonly Mock m_MockCliDeploymentOutputHandler = new(); - readonly Mock m_MockDeployFileService = new(); readonly Mock m_MockRemoteConfigScriptsLoader = new(); readonly Mock m_MockRemoteConfigDeploymentHandler = new(); - readonly Mock m_MockRemoteConfigServicesWrapper = new(); + static readonly Mock k_MockRemoteConfigServicesWrapper = new(); readonly Mock m_MockLogger = new(); + RemoteConfigDeploymentService m_RemoteConfigDeploymentService = new (k_MockRemoteConfigServicesWrapper.Object); + DeployInput m_DefaultInput = new() { CloudProjectId = k_ValidProjectId @@ -59,37 +57,27 @@ public class RemoteConfigDeploymentServiceTests [SetUp] public void SetUp() { - m_MockUnityEnvironment.Reset(); m_MockCliRemoteConfigClient.Reset(); m_MockCliDeploymentOutputHandler.Reset(); - m_MockDeployFileService.Reset(); m_MockRemoteConfigScriptsLoader.Reset(); m_MockRemoteConfigDeploymentHandler.Reset(); - m_MockRemoteConfigServicesWrapper.Reset(); + k_MockRemoteConfigServicesWrapper.Reset(); m_MockLogger.Reset(); - m_MockRemoteConfigServicesWrapper.Setup(x => x.RemoteConfigClient) + k_MockRemoteConfigServicesWrapper.Setup(x => x.RemoteConfigClient) .Returns(m_MockCliRemoteConfigClient.Object); - m_MockRemoteConfigServicesWrapper.Setup(x => x.DeploymentOutputHandler) + k_MockRemoteConfigServicesWrapper.Setup(x => x.DeploymentOutputHandler) .Returns(m_MockCliDeploymentOutputHandler.Object); m_MockCliDeploymentOutputHandler.Setup(x => x.Contents) .Returns(new List()); - m_MockRemoteConfigServicesWrapper.Setup(x => x.DeployFileService) - .Returns(m_MockDeployFileService.Object); - m_MockRemoteConfigServicesWrapper.Setup(x => x.RemoteConfigScriptsLoader) + k_MockRemoteConfigServicesWrapper.Setup(x => x.RemoteConfigScriptsLoader) .Returns(m_MockRemoteConfigScriptsLoader.Object); - m_MockRemoteConfigServicesWrapper.Setup(x => x.DeploymentHandler) + k_MockRemoteConfigServicesWrapper.Setup(x => x.DeploymentHandler) .Returns(m_MockRemoteConfigDeploymentHandler.Object); m_MockCliDeploymentOutputHandler.SetupGet(c => c.Contents).Returns(m_Contents); - m_RemoteConfigDeploymentService = - new RemoteConfigDeploymentService(m_MockUnityEnvironment.Object, m_MockRemoteConfigServicesWrapper.Object); - - m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync()) - .ReturnsAsync(k_ValidEnvironmentId); - m_MockDeployFileService.Setup(d => d.ListFilesToDeploy(m_DefaultInput.Paths, k_DeployFileExtension)) - .Returns(k_ValidFilePaths); + m_RemoteConfigDeploymentService = new (k_MockRemoteConfigServicesWrapper.Object); } [Test] @@ -97,23 +85,32 @@ public void DeployAsync_DoesNotThrowOnRemoteConfigDeploymentException() { m_MockRemoteConfigDeploymentHandler.Setup(ex => ex //RemoteConfigDeploymentException is sealed, so we're throwing RequestFailedException which inherits from the sealed one - .DeployAsync(It.IsAny>(), false)).ThrowsAsync(new RequestFailedException(1, "")); + .DeployAsync(It.IsAny>(), false, false)) + .ThrowsAsync(new RequestFailedException(1, "")); Assert.DoesNotThrowAsync(() => m_RemoteConfigDeploymentService.Deploy( m_DefaultInput, - (StatusContext)null!, + k_ValidFilePaths, + k_ValidProjectId, + k_ValidEnvironmentId, + null!, CancellationToken.None)); } [Test] - public async Task DeployAsync_CallsFetchIdentifierAsync() + public void DeployAsync_DoesNotThrowOnApiException() { - await m_RemoteConfigDeploymentService.Deploy( - m_DefaultInput, - (StatusContext)null!, - CancellationToken.None); + m_MockRemoteConfigDeploymentHandler.Setup(ex => ex + .DeployAsync(It.IsAny>(), false, false)) + .ThrowsAsync(new ApiException("", 1)); - m_MockUnityEnvironment.Verify(x => x.FetchIdentifierAsync(), Times.Once); + Assert.DoesNotThrowAsync(() => m_RemoteConfigDeploymentService.Deploy( + m_DefaultInput, + k_ValidFilePaths, + k_ValidProjectId, + k_ValidEnvironmentId, + null!, + CancellationToken.None)); } [Test] @@ -121,7 +118,10 @@ public async Task DeployAsync_CallsInitializeCorrectly() { await m_RemoteConfigDeploymentService.Deploy( m_DefaultInput, - (StatusContext)null!, + k_ValidFilePaths, + k_ValidProjectId, + k_ValidEnvironmentId, + null!, CancellationToken.None); m_MockCliRemoteConfigClient.Verify( @@ -137,13 +137,16 @@ public async Task DeployAsync_CallsLoadScriptsAsyncCorrectly() { await m_RemoteConfigDeploymentService.Deploy( m_DefaultInput, - (StatusContext)null!, + k_ValidFilePaths, + k_ValidProjectId, + k_ValidEnvironmentId, + null!, CancellationToken.None); m_MockRemoteConfigScriptsLoader.Verify(x => x.LoadScriptsAsync( k_ValidFilePaths, - m_MockRemoteConfigServicesWrapper.Object.DeploymentOutputHandler.Contents), + k_MockRemoteConfigServicesWrapper.Object.DeploymentOutputHandler.Contents), Times.Once); } @@ -158,10 +161,14 @@ public async Task DeployAsync_CallsDeployAsyncCorrectly() await m_RemoteConfigDeploymentService.Deploy( m_DefaultInput, - (StatusContext)null!, + k_ValidFilePaths, + k_ValidProjectId, + k_ValidEnvironmentId, + null!, CancellationToken.None); - m_MockRemoteConfigDeploymentHandler.Verify(x => x.DeployAsync(expectedRemoteConfigFiles, false), Times.Once); + m_MockRemoteConfigDeploymentHandler.Verify(x => + x.DeployAsync(expectedRemoteConfigFiles, false, false), Times.Once); } [Test] @@ -169,7 +176,10 @@ public async Task DeployAsync_CallsLogDeploymentResultCorrectly() { var result = await m_RemoteConfigDeploymentService.Deploy( m_DefaultInput, - (StatusContext)null!, + k_ValidFilePaths, + k_ValidProjectId, + k_ValidEnvironmentId, + null!, CancellationToken.None); Assert.That(result.Deployed, Is.EqualTo(k_DeployedContents)); diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigFetchServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigFetchServiceTests.cs new file mode 100644 index 0000000..081eebd --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigFetchServiceTests.cs @@ -0,0 +1,141 @@ +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Spectre.Console; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Cli.Deploy.Input; +using Unity.Services.Cli.Deploy.Model; +using Unity.Services.Cli.Deploy.Service; +using Unity.Services.Cli.RemoteConfig.Deploy; +using Unity.Services.Cli.RemoteConfig.Service; +using Unity.Services.RemoteConfig.Editor.Authoring.Core.Deployment; +using Unity.Services.RemoteConfig.Editor.Authoring.Core.ErrorHandling; +using Unity.Services.RemoteConfig.Editor.Authoring.Core.Fetch; +using Unity.Services.RemoteConfig.Editor.Authoring.Core.Model; + +namespace Unity.Services.Cli.RemoteConfig.UnitTest.Deploy; + +[TestFixture] +public class RemoteConfigFetchServiceTests +{ + RemoteConfigFetchService? m_RemoteConfigDeploymentService; + const string k_ValidProjectId = "a912b1fd-541d-42e1-89f2-85436f27aabd"; + const string k_ValidEnvironmentId = "00000000-0000-0000-0000-000000000000"; + const string k_DeployFileExtension = ".rc"; + + static readonly List k_ValidFilePaths = new() + { + "test_a.rc", + "test_b.rc" + }; + + private List m_RemoteConfigFiles = new(); + + readonly Mock m_MockUnityEnvironment = new(); + readonly Mock m_MockCliRemoteConfigClient = new(); + readonly Mock m_MockDeployFileService = new(); + readonly Mock m_MockRemoteConfigScriptsLoader = new(); + readonly Mock m_MockRemoteConfigFetchHandler = new(); + + readonly Mock m_MockRemoteConfigServicesWrapper = new(); + readonly Mock m_MockLogger = new(); + + FetchInput m_DefaultInput = new() + { + CloudProjectId = k_ValidProjectId , + Reconcile = false + }; + + private Result m_Result; + + [SetUp] + public void SetUp() + { + m_MockUnityEnvironment.Reset(); + m_MockCliRemoteConfigClient.Reset(); + m_MockDeployFileService.Reset(); + m_MockRemoteConfigScriptsLoader.Reset(); + m_MockRemoteConfigFetchHandler.Reset(); + m_MockRemoteConfigServicesWrapper.Reset(); + m_MockLogger.Reset(); + + m_RemoteConfigDeploymentService = + new RemoteConfigFetchService( + m_MockUnityEnvironment.Object, + m_MockRemoteConfigFetchHandler.Object, + m_MockCliRemoteConfigClient.Object, + m_MockDeployFileService.Object, + m_MockRemoteConfigScriptsLoader.Object); + + m_MockUnityEnvironment.Setup(x => x.FetchIdentifierAsync()) + .ReturnsAsync(k_ValidEnvironmentId); + m_MockDeployFileService.Setup(d => d.ListFilesToDeploy(new [] {m_DefaultInput.Path}, k_DeployFileExtension)) + .Returns(k_ValidFilePaths); + + m_RemoteConfigFiles = new List(k_ValidFilePaths.Count); + foreach (var filePath in k_ValidFilePaths) + { + var rcFile = new RemoteConfigFile(filePath, filePath, new RemoteConfigFileContent()); + m_RemoteConfigFiles.Add(rcFile); + } + + m_Result = new Result( + Array.Empty<(string,string)>(), + new [] {("updated key", "updated file")}, + new [] {("deleted key", "deleted file")}, + m_RemoteConfigFiles, + Array.Empty() + ); + m_MockRemoteConfigFetchHandler + .Setup(ex => ex + .FetchAsync( + It.IsAny(), + It.IsAny>(), + false, + false, + CancellationToken.None)) + .Returns(Task.FromResult(m_Result)); + } + + [Test] + public void FetchAsync_MapsResultProperly() + { + var res = m_RemoteConfigDeploymentService!.FetchAsync( + m_DefaultInput, + (StatusContext)null!, + CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(res.Result.Deleted, Has.Count.EqualTo(1)); + Assert.That(res.Result.Updated, Has.Count.EqualTo(1)); + Assert.That(res.Result.Created, Is.Empty); + }); + } + + [Test] + public void FetchAsync_FormatsKeyFilePair() + { + var res = m_RemoteConfigDeploymentService!.FetchAsync( + m_DefaultInput, + (StatusContext)null!, + CancellationToken.None); + + var deletedKeyStr = string.Format( + m_RemoteConfigDeploymentService.m_KeyFileMessageFormat, + m_Result.Deleted[0].Key, + m_Result.Deleted[0].File); + + var updatedKeyStr = string.Format( + m_RemoteConfigDeploymentService.m_KeyFileMessageFormat, + m_Result.Updated[0].Key, + m_Result.Updated[0].File); + + + Assert.Multiple(() => + { + Assert.That(res.Result.Deleted[0], Is.EqualTo(deletedKeyStr)); + Assert.That(res.Result.Updated[0], Is.EqualTo(updatedKeyStr)); + }); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/RemoteConfigModuleTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/RemoteConfigModuleTests.cs index c8f90ee..bdff62b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/RemoteConfigModuleTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/RemoteConfigModuleTests.cs @@ -1,3 +1,4 @@ +using System.CommandLine; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; @@ -6,12 +7,16 @@ using Unity.Services.Cli.ServiceAccountAuthentication; using Unity.Services.Cli.TestUtils; using Unity.Services.Cli.Common.Validator; +using Unity.Services.Cli.Deploy.Handlers; +using Unity.Services.Cli.RemoteConfig.Templates; namespace Unity.Services.Cli.RemoteConfig.UnitTest; [TestFixture] class RemoteConfigModuleTests { + Mock m_MockTemplate = new Mock(); + [TestCase(typeof(IRemoteConfigServicesWrapper))] public void ConfigureServicesRegistersExpectedServices(Type serviceType) { @@ -27,4 +32,15 @@ public void ConfigureServicesRegistersExpectedServices(Type serviceType) Assert.That(services.FirstOrDefault(c => c.ServiceType == serviceType), Is.Not.Null); } + [Test] + public void RemoteConfigModule_HasNewFileCommand() + { + var newFileCommand = new Command("test", "test") + .AddNewFileCommand("Remote Config"); + + var module = new RemoteConfigModule(); + + TestsHelper.AssertContainsCommand(module.ModuleRootCommand!, newFileCommand.Name, out var resultCommand); + Assert.That(resultCommand, Is.EqualTo(newFileCommand)); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Unity.Services.Cli.RemoteConfig.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Unity.Services.Cli.RemoteConfig.UnitTest.csproj index b05fd34..343721f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Unity.Services.Cli.RemoteConfig.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Unity.Services.Cli.RemoteConfig.UnitTest.csproj @@ -10,7 +10,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -23,9 +23,6 @@ - FEATURE_REMOTE_CONFIG_NEW_FILE - - - FEATURE_REMOTE_CONFIG_NEW_FILE; + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/CliRemoteConfigDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/CliRemoteConfigDeploymentHandler.cs index 5a5330f..027a9a0 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/CliRemoteConfigDeploymentHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/CliRemoteConfigDeploymentHandler.cs @@ -19,8 +19,8 @@ public CliRemoteConfigDeploymentHandler( IFormatValidator formatValidator, IConfigMerger configMerger, IJsonConverter jsonConverter, - IFileReader fileReader) : - base(remoteConfigClient, remoteConfigParser, remoteConfigValidator, formatValidator, configMerger, jsonConverter, fileReader) + IFileSystem fileSystem) : + base(remoteConfigClient, remoteConfigParser, remoteConfigValidator, formatValidator, configMerger, jsonConverter, fileSystem) { } diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/FileReader.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/FileReader.cs deleted file mode 100644 index a87ca9f..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/FileReader.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Unity.Services.RemoteConfig.Editor.Authoring.Core.IO; - -namespace Unity.Services.Cli.RemoteConfig.Deploy; - -class FileReader : IFileReader -{ - public string ReadAllText(string path) - { - return File.ReadAllText(path); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/FileSystem.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/FileSystem.cs new file mode 100644 index 0000000..d00f733 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/FileSystem.cs @@ -0,0 +1,21 @@ +using Unity.Services.RemoteConfig.Editor.Authoring.Core.IO; + +namespace Unity.Services.Cli.RemoteConfig.Deploy; + +class FileSystem : IFileSystem +{ + public Task ReadAllText( + string path, + CancellationToken token = new CancellationToken()) + { + return File.ReadAllTextAsync(path, token); + } + + public Task WriteAllText( + string path, + string contents, + CancellationToken token = new CancellationToken()) + { + return File.WriteAllTextAsync(path, contents, token); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/JsonConverter.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/JsonConverter.cs index ce4a1de..3b9c0cd 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/JsonConverter.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/JsonConverter.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using Newtonsoft.Json.Converters; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Json; namespace Unity.Services.Cli.RemoteConfig.Deploy; @@ -20,6 +21,6 @@ public T DeserializeObject(string value, bool matchCamelCaseFieldName = false public string SerializeObject(T obj) { - return JsonConvert.SerializeObject(obj); + return JsonConvert.SerializeObject(obj, Formatting.Indented, new StringEnumConverter()); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentService.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentService.cs index 5093dfd..aaeab50 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentService.cs @@ -1,8 +1,8 @@ using Spectre.Console; -using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Deploy.Input; using Unity.Services.Cli.Deploy.Model; using Unity.Services.Cli.Deploy.Service; +using Unity.Services.Cli.RemoteConfig.Exceptions; using Unity.Services.Cli.RemoteConfig.Service; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Deployment; using Unity.Services.RemoteConfig.Editor.Authoring.Core.ErrorHandling; @@ -11,24 +11,19 @@ namespace Unity.Services.Cli.RemoteConfig.Deploy; internal class RemoteConfigDeploymentService : IDeploymentService { - IUnityEnvironment m_UnityEnvironment; string m_ServiceType; string m_DeployFileExtension; IRemoteConfigDeploymentHandler m_DeploymentHandler; ICliRemoteConfigClient m_RemoteConfigClient; - IDeployFileService m_DeployFileService; ICliDeploymentOutputHandler m_DeploymentOutputHandler; IRemoteConfigScriptsLoader m_RemoteConfigScriptsLoader; public RemoteConfigDeploymentService( - IUnityEnvironment unityEnvironment, IRemoteConfigServicesWrapper servicesWrapper ) { - m_UnityEnvironment = unityEnvironment; m_DeploymentHandler = servicesWrapper.DeploymentHandler; m_RemoteConfigClient = servicesWrapper.RemoteConfigClient; - m_DeployFileService = servicesWrapper.DeployFileService; m_DeploymentOutputHandler = servicesWrapper.DeploymentOutputHandler; m_RemoteConfigScriptsLoader = servicesWrapper.RemoteConfigScriptsLoader; m_ServiceType = "Remote Config"; @@ -39,24 +34,26 @@ IRemoteConfigServicesWrapper servicesWrapper string IDeploymentService.DeployFileExtension => m_DeployFileExtension; - public async Task Deploy(DeployInput input, StatusContext? loadingContext, CancellationToken cancellationToken) + public async Task Deploy(DeployInput deployInput, IReadOnlyList filePaths, string projectId, string environmentId, + StatusContext? loadingContext, CancellationToken cancellationToken) { - var environmentId = await m_UnityEnvironment.FetchIdentifierAsync(); - m_RemoteConfigClient.Initialize(input.CloudProjectId!, environmentId, cancellationToken); - var remoteConfigFiles = m_DeployFileService.ListFilesToDeploy(input.Paths, ".rc").ToList(); - var configFiles = await m_RemoteConfigScriptsLoader.LoadScriptsAsync(remoteConfigFiles, m_DeploymentOutputHandler.Contents); + m_RemoteConfigClient.Initialize(projectId, environmentId, cancellationToken); + var configFiles = await m_RemoteConfigScriptsLoader.LoadScriptsAsync(filePaths, m_DeploymentOutputHandler.Contents); loadingContext?.Status($"Deploying {m_ServiceType} Files..."); try { - bool reconcile = false; - await m_DeploymentHandler.DeployAsync(configFiles, reconcile); + var reconcile = false; + var dryRun = false; + + await m_DeploymentHandler.DeployAsync(configFiles, reconcile, dryRun); } catch (RemoteConfigDeploymentException) { - /* - * Ignoring this because we already catch exceptions from UpdateScriptStatus() for each script and we don't - * want to stop execution when a script generates an exception. - */ + // Ignoring it because this exception should already be logged into the deployment content status + } + catch (ApiException) + { + // Ignoring it because this exception should already be logged into the deployment content status } return new DeploymentResult(m_DeploymentOutputHandler.Contents.ToList()); diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs new file mode 100644 index 0000000..a6d3c06 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs @@ -0,0 +1,76 @@ +using Spectre.Console; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Cli.Deploy.Input; +using Unity.Services.Cli.Deploy.Model; +using Unity.Services.Cli.Deploy.Service; +using Unity.Services.RemoteConfig.Editor.Authoring.Core.Fetch; + +namespace Unity.Services.Cli.RemoteConfig.Deploy; + +class RemoteConfigFetchService : IFetchService +{ + private readonly IUnityEnvironment m_UnityEnvironment; + private readonly IRemoteConfigFetchHandler m_FetchHandler; + private readonly ICliRemoteConfigClient m_RemoteConfigClient; + private readonly IDeployFileService m_DeployFileService; + private readonly IRemoteConfigScriptsLoader m_RemoteConfigScriptsLoader; + private readonly string m_DeployFileExtension; + + internal readonly string m_KeyFileMessageFormat = "Key '{0}' in file '{1}'"; + public string ServiceType { get; } + + string IFetchService.FileExtension => m_DeployFileExtension; + + public RemoteConfigFetchService( + IUnityEnvironment unityEnvironment, + IRemoteConfigFetchHandler fetchHandler, + ICliRemoteConfigClient remoteConfigClient, + IDeployFileService deployFileService, + IRemoteConfigScriptsLoader remoteConfigScriptsLoader + ) + { + m_UnityEnvironment = unityEnvironment; + m_FetchHandler = fetchHandler; + m_RemoteConfigClient = remoteConfigClient; + m_DeployFileService = deployFileService; + m_RemoteConfigScriptsLoader = remoteConfigScriptsLoader; + ServiceType = "Remote Config"; + m_DeployFileExtension = ".rc"; + } + + public async Task FetchAsync( + FetchInput input, + StatusContext? loadingContext, + CancellationToken cancellationToken) + { + var environmentId = await m_UnityEnvironment.FetchIdentifierAsync(); + m_RemoteConfigClient.Initialize(input.CloudProjectId!, environmentId, cancellationToken); + var remoteConfigFiles = m_DeployFileService.ListFilesToDeploy(new[] {input.Path}, m_DeployFileExtension).ToList(); + + var contents = new List(); + var configFiles = await m_RemoteConfigScriptsLoader + .LoadScriptsAsync(remoteConfigFiles, contents); + loadingContext?.Status($"Fetching {ServiceType} Files..."); + + Result fetchResult = await m_FetchHandler.FetchAsync( + input.Path, + configFiles, + input.DryRun, + input.Reconcile, + cancellationToken); + + + + return new FetchResult( + fetchResult.Updated.Select(kvp => string.Format(m_KeyFileMessageFormat, kvp.Key, NormalizePath(kvp.File)) ).ToList(), + fetchResult.Deleted.Select(kvp => string.Format(m_KeyFileMessageFormat, kvp.Key, NormalizePath(kvp.File)) ).ToList(), + fetchResult.Created.Select(kvp => string.Format(m_KeyFileMessageFormat, kvp.Key, NormalizePath(kvp.File)) ).ToList(), + fetchResult.Fetched.Select(f => Path.GetRelativePath(".", f.Path) ).ToList(), + fetchResult.Failed.Select(f => Path.GetRelativePath(".", f.Path)).ToList()); + } + + static string NormalizePath(string path) + { + return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Exceptions/ApiException.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Exceptions/ApiException.cs new file mode 100644 index 0000000..c668506 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Exceptions/ApiException.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; +using Unity.Services.Cli.Common.Exceptions; + +namespace Unity.Services.Cli.RemoteConfig.Exceptions; + +public class ApiException : CliException +{ + protected ApiException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + public ApiException(string message, Exception? innerException, int exitCode) + : base(message, innerException, exitCode) { } + + public ApiException(string message, int exitCode) + : base(message, exitCode) { } + + public ApiException(int exitCode) + : base(exitCode) { } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/RemoteConfigModule.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/RemoteConfigModule.cs index 1c249ed..e12bf6f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/RemoteConfigModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/RemoteConfigModule.cs @@ -5,12 +5,17 @@ using Unity.Services.Cli.Deploy.Input; using Unity.Services.Cli.Deploy.Service; using Unity.Services.Cli.RemoteConfig.Deploy; +using Unity.Services.Cli.RemoteConfig.Templates; +using Unity.Services.Cli.Deploy.Handlers; using Unity.Services.Cli.RemoteConfig.Service; +using Unity.Services.RemoteConfig.Editor.Authoring.Core.Fetch; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Formatting; using Unity.Services.RemoteConfig.Editor.Authoring.Core.IO; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Json; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Model; using Unity.Services.RemoteConfig.Editor.Authoring.Core.Networking; +using FileSystem = Unity.Services.Cli.RemoteConfig.Deploy.FileSystem; +using IFileSystem = Unity.Services.RemoteConfig.Editor.Authoring.Core.IO.IFileSystem; namespace Unity.Services.Cli.RemoteConfig; @@ -23,7 +28,14 @@ public class RemoteConfigModule : ICommandModule public RemoteConfigModule() { - ModuleRootCommand = null; + ModuleRootCommand = new Command( + name: "remote-config", + description: "Manage RemoteConfig.") + { + ModuleRootCommand.AddNewFileCommand("Remote Config") + }; + + ModuleRootCommand.AddAlias("rc"); } /// @@ -39,7 +51,7 @@ public static void RegisterServices(HostBuilderContext hostBuilderContext, IServ serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); - serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddSingleton(); @@ -56,6 +68,8 @@ public static void RegisterServices(HostBuilderContext hostBuilderContext, IServ s.GetRequiredService(), s.GetRequiredService())); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Service/RemoteConfigService.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Service/RemoteConfigService.cs index d20be49..8cba096 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Service/RemoteConfigService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Service/RemoteConfigService.cs @@ -6,6 +6,7 @@ using Unity.Services.Cli.Common.Models; using Unity.Services.Cli.Common.Networking; using Unity.Services.Cli.Common.Validator; +using Unity.Services.Cli.RemoteConfig.Exceptions; using Unity.Services.Cli.RemoteConfig.Model; using Unity.Services.Cli.RemoteConfig.Types; using Unity.Services.Cli.ServiceAccountAuthentication; @@ -18,7 +19,7 @@ public class RemoteConfigService : IRemoteConfigService static readonly string k_BaseUrl = $"{EndpointHelper.GetCurrentEndpointFor()}/remote-config/v1"; - static readonly JsonSerializerSettings k_JsonSerializerSettings = new () + static readonly JsonSerializerSettings k_JsonSerializerSettings = new() { ContractResolver = new CamelCasePropertyNamesContractResolver() }; @@ -66,7 +67,7 @@ async Task CreateConfigAsync(string projectId, string body, Cancellation } catch (HttpRequestException exception) { - throw new CliException($"{nameof(CreateConfigAsync)} failed: {exception.Message}", exception, ExitCode.UnhandledError); + throw new ApiException($"{nameof(CreateConfigAsync)} failed: {exception.Message}", exception, ExitCode.UnhandledError); } } @@ -125,7 +126,7 @@ private async Task UpdateConfigInternalAsync(string projectId, string configId, } catch (HttpRequestException exception) { - throw new CliException($"{nameof(UpdateConfigInternalAsync)} failed: {exception.Message}", exception, ExitCode.HandledError); + throw new ApiException($"{nameof(UpdateConfigInternalAsync)} failed: {exception.Message}", exception, ExitCode.HandledError); } } @@ -161,7 +162,7 @@ private async Task GetAllConfigsFromEnvironmentInternalAsync(string proj } catch (HttpRequestException exception) { - throw new CliException($"{nameof(GetAllConfigsFromEnvironmentAsync)} failed: {exception.Message}", exception, ExitCode.HandledError); + throw new ApiException($"{nameof(GetAllConfigsFromEnvironmentAsync)} failed: {exception.Message}", exception, ExitCode.HandledError); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Templates/RemoteConfigTemplate.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Templates/RemoteConfigTemplate.cs new file mode 100644 index 0000000..08d8be3 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Templates/RemoteConfigTemplate.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Unity.Services.Cli.Deploy.Handlers; +using Unity.Services.Cli.Deploy.Templates; +using Unity.Services.RemoteConfig.Editor.Authoring.Core.Formatting; +using Unity.Services.RemoteConfig.Editor.Authoring.Core.Model; +using JsonConverter = Unity.Services.Cli.RemoteConfig.Deploy.JsonConverter; + +namespace Unity.Services.Cli.RemoteConfig.Templates; + +class RemoteConfigTemplate : RemoteConfigFileContent , IFileTemplate +{ + [JsonProperty("$schema")] + public string Value = "https://ugs-config-schemas.unity3d.com/v1/remote-config.schema.json"; + + public RemoteConfigTemplate() { + entries = new Dictionary() + { + { "string_key", "string_value" }, + { "int_key", 1 }, + { "bool_key", true }, + { "long_key", 10000 }, + { "float_key", 1 }, + { "json_key", JObject.Parse("{'sample_key': 'sample_value'}") } + }; + types = new Dictionary() + { + { "long_key", ConfigType.LONG }, + { "float_key", ConfigType.FLOAT } + }; + } + + [JsonIgnore] + public string Extension => ".rc"; + [JsonIgnore] + public string FileBodyText => new JsonConverter().SerializeObject(this); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj index 9b9a72a..3e6e77e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj @@ -20,12 +20,10 @@ - + $(DefineConstants);$(ExtraDefineConstants) - - - FEATURE_REMOTE_CONFIG_NEW_FILE; + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Cli.ServiceAccountAuthentication.UnitTest/Unity.Services.Cli.ServiceAccountAuthentication.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.ServiceAccountAuthentication.UnitTest/Unity.Services.Cli.ServiceAccountAuthentication.UnitTest.csproj index 9180aad..fd97e37 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.ServiceAccountAuthentication.UnitTest/Unity.Services.Cli.ServiceAccountAuthentication.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.ServiceAccountAuthentication.UnitTest/Unity.Services.Cli.ServiceAccountAuthentication.UnitTest.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.sln b/Unity.Services.Cli/Unity.Services.Cli.sln index 8fd5e03..330e7fa 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.sln +++ b/Unity.Services.Cli/Unity.Services.Cli.sln @@ -44,6 +44,8 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Cli.RemoteConfig.UnitTest", "Unity.Services.Cli.RemoteConfig.UnitTest\Unity.Services.Cli.RemoteConfig.UnitTest.csproj", "{F60B608F-8AED-4F6D-9853-C3A2D7CFA641}" EndProject EndProject +EndProject +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Unity.Services.Cli/Unity.Services.Cli/Program.cs b/Unity.Services.Cli/Unity.Services.Cli/Program.cs index ccc18da..31f7e68 100644 --- a/Unity.Services.Cli/Unity.Services.Cli/Program.cs +++ b/Unity.Services.Cli/Unity.Services.Cli/Program.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Builder; @@ -13,7 +14,6 @@ using Unity.Services.Cli.Lobby; using Unity.Services.Cli.Common; using Unity.Services.Cli.Common.Exceptions; -using Unity.Services.Cli.Common.Features; using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.Common.Middleware; using Unity.Services.Cli.Common.Services; @@ -22,6 +22,8 @@ using Unity.Services.Cli.ServiceAccountAuthentication; using Unity.Services.Cli.RemoteConfig; using Unity.Services.Cli.Common.Telemetry; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent.AnalyticEventFactory; using Unity.Services.Cli.Deploy; namespace Unity.Services.Cli; @@ -30,20 +32,19 @@ static class Program { static async Task Main(string[] args) { - var features = await FeaturesFactory.BuildAsync(Host.CreateDefaultBuilder()); var logger = new Logger(); var services = new ServiceTypesBridge(); var ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings()); TelemetrySender telemetrySender = null; - SystemEnvironmentProvider systemEnvironmentProvider = null; + SystemEnvironmentProvider systemEnvironmentProvider = new SystemEnvironmentProvider(); + IAnalyticEventFactory analyticEventFactory = new AnalyticEventFactory(systemEnvironmentProvider); var parser = BuildCommandLine() .UseHost(_ => Host.CreateDefaultBuilder(), host => { - systemEnvironmentProvider = new SystemEnvironmentProvider(); host.UseServiceProviderFactory(_ => services); - CommonModule.ConfigureCommonServices(host, logger, features, ansiConsole); + CommonModule.ConfigureCommonServices(host, logger, ansiConsole, analyticEventFactory); telemetrySender = CommonModule.CreateTelemetrySender(systemEnvironmentProvider); host.ConfigureServices(ConfigurationModule.RegisterServices); @@ -70,6 +71,10 @@ static async Task Main(string[] args) "cli/blob/main/docs/project-roles.md for required project roles." + $"{System.Environment.NewLine}")); + // Replace built-in subcommand help section by custom subcommand help section + helpSectionDelegates.Remove(HelpBuilder.Default.SubcommandsSection()); + helpSectionDelegates.Add(SubcommandsSectionDelegate(ctx, ansiConsole)); + return helpSectionDelegates.AsEnumerable(); }); }) @@ -93,11 +98,11 @@ static async Task Main(string[] args) .AddGlobalCommonOptions() .AddCommandInputParserMiddleware() .AddCliServicesMiddleware(services) - // Manually keep modules in alphabetical order .AddModule(new AuthenticationModule()) .AddModule(new CloudCodeModule()) .AddModule(new ConfigurationModule()) .AddModule(new DeployModule()) + .AddModule(new FetchModule()) .AddModule(new EnvironmentModule()) .AddModule(new LobbyModule()) .AddModule(new RemoteConfigModule()) @@ -107,6 +112,7 @@ static async Task Main(string[] args) .ContinueWith(commandTask => { logger.Write(); + TrySendCommandUsageMetric(analyticEventFactory, parser.Parse(args).CommandResult); return commandTask.Result; }); } @@ -116,4 +122,34 @@ static CommandLineBuilder BuildCommandLine() var root = new RootCommand("Unity Gaming Services CLI. Use the CLI to interact with Unity Dashboard."); return new CommandLineBuilder(root); } + + static HelpSectionDelegate SubcommandsSectionDelegate(HelpContext context, IAnsiConsole ansiConsole) + { + var subcommands = context.Command.Subcommands + .OrderBy(command => command.Name) + .Where(command => !command.IsHidden) + .Select(command => context.HelpBuilder.GetTwoColumnRow(command, context)) + .ToArray(); + + return WriteSubcommands(); + + HelpSectionDelegate WriteSubcommands() => helpContext + => + { + if (subcommands.Length <= 0) + return; + + ansiConsole.Markup($"Commands:{System.Environment.NewLine}"); + helpContext.HelpBuilder.WriteColumns(subcommands, helpContext); + }; + } + + static void TrySendCommandUsageMetric(IAnalyticEventFactory analyticEventFactory, SymbolResult symbol) + { + var command = AnalyticEventUtils.ConvertSymbolResultToString(symbol); + var analyticEvent = analyticEventFactory.CreateEvent(); + analyticEvent.AddData("command", command); + analyticEvent.AddData("time", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + analyticEvent.Send(); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj b/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj index 76cbf1d..a10af7b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj @@ -5,7 +5,7 @@ 10 ugs 1.0.0 - beta.1 + beta.2 true true true @@ -34,10 +34,10 @@ full - TRACE; + TRACE;_DEPLOY; - TRACE; + TRACE;_DEPLOY; $(DefineConstants);$(ExtraDefineConstants) diff --git a/Unity.Services.Cli/Unity.Services.Cli/appsettings.json b/Unity.Services.Cli/Unity.Services.Cli/appsettings.json index 23cdca2..4ec6a19 100644 --- a/Unity.Services.Cli/Unity.Services.Cli/appsettings.json +++ b/Unity.Services.Cli/Unity.Services.Cli/appsettings.json @@ -5,12 +5,5 @@ "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Warning" } - }, - "FeatureManagement": { - "Multiplay": false, - "MultiplayBuild": false, - "MultiplayAuthoring": false, - "Economy": false, - "Leaderboard": false } } diff --git a/Unity.Services.Cli/nuget.config b/Unity.Services.Cli/nuget.config index a7acbd5..0bdcac9 100644 --- a/Unity.Services.Cli/nuget.config +++ b/Unity.Services.Cli/nuget.config @@ -1,4 +1,4 @@ - +