From f61ff3a0b21ff574d7f35463638f96f29fbba9ed Mon Sep 17 00:00:00 2001 From: operate-services-sdk-bot Date: Thu, 23 Nov 2023 16:09:35 +0000 Subject: [PATCH] Release v1.2.0 --- CHANGELOG.md | 50 +-- .../CloudCodeModuleDeploymentServiceTests.cs | 295 ++---------------- .../Deploy/CloudCodeModuleLoaderTests.cs | 112 ++++--- .../CloudCodeModule.cs | 8 +- .../Deploy/CliCloudCodeDeploymentHandler.cs | 15 +- .../Deploy/CloudCodeModule.cs | 34 +- .../CloudCodeModuleDeploymentService.cs | 139 ++------- .../Deploy/CloudCodeModulesDownloader.cs | 6 +- .../Deploy/CloudCodeModulesLoader.cs | 73 ++++- .../Deploy/ICloudCodeModulesLoader.cs | 8 +- .../Deploy/ModuleDeployContent.cs | 111 +++++++ .../Deploy/NoopDeploymentAnalytics.cs | 5 + .../Unity.Services.Cli.CloudCode.csproj | 2 +- .../Unity.Services.Cli.Common.csproj | 2 +- .../Service/EconomyServiceTests.cs | 51 +++ .../Exceptions/InvalidResourceException.cs | 10 + .../Service/EconomyService.cs | 28 +- .../GameServerHostingModuleTests.cs | 4 +- .../GameServerHostingUnitTestsConstants.cs | 5 +- .../Handlers/FileDownloadHandlerTests.cs | 177 +++++++++++ .../Handlers/FileListHandlerTests.cs | 149 +++++++++ .../Handlers/FleetCreateHandlerTests.cs | 64 +++- .../Handlers/FleetUpdateHandlerTests.cs | 47 ++- .../Handlers/HandlerCommon.cs | 6 + .../Input/FileDownloadInputTests.cs | 104 ++++++ .../Input/FileListInputTests.cs | 43 +++ .../Input/FleetCreateInputTests.cs | 15 + .../Input/FleetUpdateInputTests.cs | 23 ++ .../Mocks/GameServerHostingFilesApiV1Mock.cs | 47 ++- .../Model/FilesItemFleetOutputTests.cs | 31 ++ .../Model/FilesItemMachineOutputTests.cs | 31 ++ .../Model/FilesItemOutputTests.cs | 52 +++ .../Model/FilesOutputTests.cs | 61 ++++ .../Model/FleetListItemOutputTests.cs | 5 +- .../Model/FleetListOutputTests.cs | 8 +- .../Services/CcdCloudStorageTests.cs | 36 +-- .../GameServerHostingModule.cs | 66 +++- .../BuildConfigurationUpdateHandler.cs | 2 + .../Handlers/FileDownloadHandler.cs | 95 ++++++ .../Handlers/FileListHandler.cs | 73 +++++ .../Handlers/FleetCreateHandler.cs | 21 +- .../Handlers/FleetUpdateHandler.cs | 8 + .../Input/FileDownloadInput.cs | 88 ++++++ .../Input/FileListInput.cs | 124 ++++++++ .../Input/FleetCreateInput.cs | 30 ++ .../Input/FleetUpdateInput.cs | 35 +++ .../Model/BuildConfigurationOutput.cs | 2 + .../Model/FilesItemMachineOutput.cs | 27 ++ .../Model/FilesItemOutput.cs | 38 +++ .../Model/FilesOutput.cs | 22 ++ .../Model/FleetGetOutput.cs | 4 + .../Model/FleetListItemOutput.cs | 3 + .../Services/CcdCloudStorageClient.cs | 22 +- ...nity.Services.Cli.GameServerHosting.csproj | 6 +- .../ServiceMocks/GameServerHosting/Keys.cs | 2 + .../GameServerHostingFleetTests.cs | 20 +- ...ameServerHostingServerFileDownloadTests.cs | 74 +++++ .../GameServerHostingServerFilesTests.cs | 17 +- .../LeaderboardTests/LeaderboardTests.cs | 2 + .../LeaderboardDeploymentHandlerTests.cs | 267 ---------------- .../Deploy/LeaderboardFetchHandlerTests.cs | 243 --------------- .../Deploy/LeaderboardPatchConverterTest.cs | 50 +++ ....Services.Cli.Leaderboards.UnitTest.csproj | 1 + .../Deploy/ILeaderboardsConfigLoader.cs | 2 +- .../Deploy/LeaderboardConfigFile.cs | 2 +- .../Deploy/LeaderboardPatchConverter.cs | 51 +++ .../Deploy/LeaderboardsClient.cs | 9 +- .../Deploy/LeaderboardsDeploymentService.cs | 2 +- .../Deploy/LeaderboardsFetchService.cs | 2 +- .../Deploy/LeaderboardsSerializer.cs | 2 +- .../LeaderboardsModule.cs | 1 + .../Unity.Services.Cli.Leaderboards.csproj | 5 +- Unity.Services.Cli/Unity.Services.Cli.sln | 6 - .../Unity.Services.Cli.csproj | 2 +- .../Batching/Batching.cs | 87 ------ .../Deploy/DeployResult.cs | 14 - .../Deploy/ILeaderboardsDeploymentHandler.cs | 15 - .../Deploy/LeaderboardsDeploymentHandler.cs | 140 --------- .../Fetch/FetchResult.cs | 14 - .../Fetch/ILeaderboardsFetchHandler.cs | 17 - .../Fetch/LeaderboardsFetchHandler.cs | 169 ---------- .../IO/IFileSystem.cs | 19 -- .../Model/ILeaderboardConfig.cs | 17 - .../Model/LeaderboardComparer.cs | 21 -- .../Model/LeaderboardConfig.cs | 100 ------ .../Model/ResetConfig.cs | 12 - .../Model/SortOrder.cs | 13 - .../Model/Statuses.cs | 20 -- .../Model/Strategy.cs | 16 - .../Model/Tier.cs | 8 - .../Model/TieringConfig.cs | 12 - .../Model/UpdateType.cs | 16 - .../Serialization/ILeaderboardSerializer.cs | 9 - .../Service/ILeaderboardsClient.cs | 18 -- ...ervices.Leaderboards.Authoring.Core.csproj | 24 -- .../DuplicateResourceValidation.cs | 43 --- 96 files changed, 2214 insertions(+), 1873 deletions(-) create mode 100644 Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ModuleDeployContent.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Economy/Exceptions/InvalidResourceException.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileDownloadHandlerTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileListHandlerTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FileDownloadInputTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FileListInputTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FleetUpdateInputTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemFleetOutputTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemMachineOutputTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemOutputTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesOutputTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileDownloadHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileListHandler.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FileDownloadInput.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FileListInput.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesItemMachineOutput.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesItemOutput.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesOutput.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFileDownloadTests.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentHandlerTests.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchHandlerTests.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardPatchConverterTest.cs create mode 100644 Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardPatchConverter.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Batching/Batching.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/DeployResult.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/ILeaderboardsDeploymentHandler.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/LeaderboardsDeploymentHandler.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/FetchResult.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/ILeaderboardsFetchHandler.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/LeaderboardsFetchHandler.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/IO/IFileSystem.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ILeaderboardConfig.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardComparer.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardConfig.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ResetConfig.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/SortOrder.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Statuses.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Strategy.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Tier.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/TieringConfig.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/UpdateType.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Serialization/ILeaderboardSerializer.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Service/ILeaderboardsClient.cs delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Unity.Services.Leaderboards.Authoring.Core.csproj delete mode 100644 Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Validations/DuplicateResourceValidation.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d61f1c2..400dc64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,29 +5,39 @@ 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.2.0] - 2023-11-14 + +### Added +- Added support for usage settings under `gsh fleet` commands +- Added `gsh server files list` and `gsh server files download` + +### Fixed +- Fixed Economy deserialization error message when receiving invalid response. +- Fixed issue where deploying a leaderboard would fail to remove Tiering and Reset config. + ## [1.1.0] - 2023-10-12 ### Added -* Bash installer to download and install the UGS CLI on MacOS and Linux -* Added config as code support for economy module - * Deploy - * Fetch -* Added config as code support for access module - * Deploy - * Fetch -* Added `new-file` commands for economy resources - * For inventory items - * For currencies - * For virtual purchases - * For real-item purchases - * For Cloud Code C# Modules - * For project access policies - * For triggers -* Added `gsh server files` command behind feature flag -* Added support for .sln files on deploy - * .sln files now are compiled and zipped into .ccm before deploying -* Added config as code support for triggers - * Deploy +- Bash installer to download and install the UGS CLI on MacOS and Linux +- Added config as code support for economy module + - Deploy + - Fetch +- Added config as code support for access module + - Deploy + - Fetch +- Added `new-file` commands for economy resources + - For inventory items + - For currencies + - For virtual purchases + - For real-item purchases + - For Cloud Code C# Modules + - For project access policies + - For triggers +- Added `gsh server files` command behind feature flag +- Added support for .sln files on deploy + - .sln files now are compiled and zipped into .ccm before deploying +- Added config as code support for triggers + - Deploy ### Changed - Services can support multiple file extensions diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeModuleDeploymentServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeModuleDeploymentServiceTests.cs index 1c42397..22158ca 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeModuleDeploymentServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeModuleDeploymentServiceTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,7 +14,6 @@ using Unity.Services.Cli.CloudCode.UnitTest.Utils; using Unity.Services.Cli.CloudCode.Utils; using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment; -using Unity.Services.CloudCode.Authoring.Editor.Core.IO; using Unity.Services.CloudCode.Authoring.Editor.Core.Model; using Unity.Services.DeploymentApi.Editor; using Unity.Services.Gateway.CloudCodeApiV1.Generated.Client; @@ -33,11 +31,6 @@ public class CloudCodeModuleDeploymentServiceTests "test_b.ccm" }; - static readonly List k_ValidSlnFilePaths = new() - { - "test_sln_a.sln" - }; - readonly Mock m_MockCloudCodeClient = new(); readonly Mock m_MockEnvironmentProvider = new(); readonly Mock m_MockCloudCodeInputParser = new(); @@ -46,14 +39,12 @@ public class CloudCodeModuleDeploymentServiceTests readonly Mock m_MockCloudCodeModulesLoader = new(); readonly Mock m_MockDeployFileService = new(); readonly Mock m_MockSolutionPublisher = new(); - readonly Mock m_MockModuleZipper = new(); - readonly Mock m_MockFileSystem = new(); static readonly IReadOnlyList k_DeployedContents = new[] { new CloudCodeModuleScript( "module.ccm", - "path", + "path/module.ccm", 100, DeploymentStatus.UpToDate) }; @@ -109,180 +100,18 @@ public void SetUp() m_MockCloudCodeModulesLoader.Object, m_MockEnvironmentProvider.Object, m_MockCloudCodeClient.Object, - m_MockDeployFileService.Object, - m_MockSolutionPublisher.Object, - m_MockModuleZipper.Object, - m_MockFileSystem.Object); + m_MockDeployFileService.Object); m_MockCloudCodeModulesLoader.Setup( - c => c.LoadPrecompiledModulesAsync( + c => c.LoadModulesAsync( k_ValidCcmFilePaths, - CloudCodeConstants.ServiceTypeModules)) - .ReturnsAsync(k_DeployedContents.OfType().ToList()); - } - - [Test] - public async Task DeployAsync_RemovesDuplicatesBeforeDeploy() - { - var outputCcmPath = "test_a.ccm"; - - CloudCodeInput input = new() - { - CloudProjectId = TestValues.ValidProjectId, - Paths = k_ValidSlnFilePaths, - }; - - m_MockCloudCodeModulesLoader.Reset(); - - m_MockDeployFileService.Setup( - c => c.ListFilesToDeploy( - It.IsAny>(), - CloudCodeConstants.FileExtensionModulesCcm, - false)) - .Returns(k_ValidCcmFilePaths); - - m_MockDeployFileService.Setup( - c => c.ListFilesToDeploy( - It.IsAny>(), - CloudCodeConstants.FileExtensionModulesSln, - false)) - .Returns(k_ValidSlnFilePaths); - - var fakeModuleName = "FakeModuleName"; - var testSlnDirName = "FakeSolutionDirName"; - var slnPath = "FakeSolutionPath"; - - m_MockSolutionPublisher.Setup( - x => x.PublishToFolder( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(fakeModuleName); - - m_MockFileSystem.Setup( - x => x.GetDirectoryName(It.IsAny())); - m_MockFileSystem.Setup( - x => x.GetFullPath(It.IsAny())) - .Returns(testSlnDirName); - - m_MockFileSystem.Setup( - x => x.Combine(testSlnDirName, CloudCodeModuleDeploymentService.OutputPath)) - .Returns(slnPath); - - m_MockModuleZipper.Setup( - x => x.ZipCompilation( - It.IsAny(), - fakeModuleName, - It.IsAny())) - .ReturnsAsync(outputCcmPath); - - await m_DeploymentService!.Deploy( - input, - k_ValidCcmFilePaths, - TestValues.ValidProjectId, - TestValues.ValidEnvironmentId, - null!, - CancellationToken.None); - - var slnName = Path.GetFileNameWithoutExtension(k_ValidSlnFilePaths.First()); - var dllOutputPath = Path.Combine(Path.GetTempPath(), slnName); - var moduleCompilationPath = Path.Combine(dllOutputPath, "module-compilation"); - - m_MockSolutionPublisher.Verify( - x => x.PublishToFolder( - k_ValidSlnFilePaths.First(), - moduleCompilationPath, - It.IsAny()), - Times.Once); - - m_MockCloudCodeModulesLoader.Verify( - x => x.LoadPrecompiledModulesAsync( - k_ValidCcmFilePaths, - It.IsAny()), - Times.Once); - } - - [Test] - public async Task DeployAsync_GenerateSolutionFromSlnInput() - { - var outputCcmPath = "test_result.ccm"; - - CloudCodeInput input = new() - { - CloudProjectId = TestValues.ValidProjectId, - Paths = k_ValidSlnFilePaths, - }; - - m_MockCloudCodeModulesLoader.Reset(); - - m_MockDeployFileService.Setup( - c => c.ListFilesToDeploy( It.IsAny>(), - CloudCodeConstants.FileExtensionModulesCcm, - false)) - .Returns(k_ValidCcmFilePaths); - - m_MockDeployFileService.Setup( - c => c.ListFilesToDeploy( - It.IsAny>(), - CloudCodeConstants.FileExtensionModulesSln, - false)) - .Returns(k_ValidSlnFilePaths); - - var fakeModuleName = "FakeModuleName"; - var testSlnDirName = "FakeSolutionDirName"; - var slnPath = "FakeSolutionPath"; - - m_MockSolutionPublisher.Setup( - x => x.PublishToFolder( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(fakeModuleName); - - m_MockFileSystem.Setup( - x => x.GetDirectoryName(It.IsAny())); - m_MockFileSystem.Setup( - x => x.GetFullPath(It.IsAny())) - .Returns(testSlnDirName); - m_MockFileSystem.Setup( - x => x.Combine(testSlnDirName, CloudCodeModuleDeploymentService.OutputPath)) - .Returns(slnPath); - - m_MockModuleZipper.Setup( - x => x.ZipCompilation( - It.IsAny(), - fakeModuleName, It.IsAny())) - .ReturnsAsync(outputCcmPath); - - await m_DeploymentService!.Deploy( - input, - k_ValidCcmFilePaths, - TestValues.ValidProjectId, - TestValues.ValidEnvironmentId, - null!, - CancellationToken.None); - - var slnName = Path.GetFileNameWithoutExtension(k_ValidSlnFilePaths.First()); - var dllOutputPath = Path.Combine(Path.GetTempPath(), slnName); - var moduleCompilationPath = Path.Combine(dllOutputPath, "module-compilation"); - - m_MockSolutionPublisher.Verify( - x => x.PublishToFolder( - k_ValidSlnFilePaths.First(), - moduleCompilationPath, - It.IsAny()), - Times.Once); - - var resultList = new List(); - resultList.AddRange(k_ValidCcmFilePaths); - resultList.Add(outputCcmPath); - m_MockCloudCodeModulesLoader.Verify( - x => x.LoadPrecompiledModulesAsync( - resultList, - It.IsAny()), - Times.Once); + .ReturnsAsync( + () => + { + return (k_DeployedContents.OfType().ToList(), new List()); + }); } [Test] @@ -319,11 +148,17 @@ public async Task DeployAsync_CallsFullPathCorrectly() { myModule }; - m_MockCloudCodeModulesLoader.Setup( - c => c.LoadPrecompiledModulesAsync( + m_MockCloudCodeModulesLoader + .Setup( + c => c.LoadModulesAsync( k_ValidCcmFilePaths, - It.IsAny())) - .ReturnsAsync(loadedResult); + It.IsAny>(), + It.IsAny())) + .ReturnsAsync( + () => + { + return (loadedResult, new List()); + }); var result = await m_DeploymentService!.Deploy( input, @@ -339,88 +174,13 @@ public async Task DeployAsync_CallsFullPathCorrectly() TestValues.ValidProjectId, CancellationToken.None), Times.Once); + m_MockEnvironmentProvider.VerifySet(x => { x.Current = TestValues.ValidEnvironmentId; }, Times.Once); m_DeploymentHandler.Verify(x => x.DeployAsync(loadedResult, false, false), Times.Once); - Assert.AreEqual(k_DeployedContents, result.Deployed); - Assert.AreEqual(k_FailedContents, result.Failed); - } - - [Test] - public async Task DeployAsync_FailsGeneration() - { - CloudCodeInput input = new() - { - CloudProjectId = TestValues.ValidProjectId, - Paths = k_ValidSlnFilePaths, - }; - - m_MockCloudCodeModulesLoader.Reset(); - - m_MockDeployFileService.Setup( - c => c.ListFilesToDeploy( - It.IsAny>(), - CloudCodeConstants.FileExtensionModulesCcm, - false)) - .Returns(k_ValidCcmFilePaths); - - m_MockDeployFileService.Setup( - c => c.ListFilesToDeploy( - It.IsAny>(), - CloudCodeConstants.FileExtensionModulesSln, - false)) - .Returns(k_ValidSlnFilePaths); - - var testFakeModuleName = "FakeModuleName"; - var testSlnDirName = "FakeSolutionDirName"; - var slnPath = "FakeSolutionPath"; - - m_MockSolutionPublisher.Setup( - x => x.PublishToFolder( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(testFakeModuleName); - m_MockFileSystem.Setup( - x => x.GetDirectoryName(It.IsAny())); - m_MockFileSystem.Setup( - x => x.GetFullPath(It.IsAny())) - .Returns(testSlnDirName); - m_MockFileSystem.Setup( - x => x.Combine(testSlnDirName, CloudCodeModuleDeploymentService.OutputPath)) - .Returns(slnPath); - - m_MockModuleZipper.Setup( - x => x.ZipCompilation( - It.IsAny(), - testFakeModuleName, - It.IsAny())) - .Throws(new Exception("Fake Exception")); - - IScript myModule = new Unity.Services.Cli.CloudCode.Deploy.CloudCodeModule( - new ScriptName("module.ccm"), - Language.JS, - "modules"); - var loadedResult = new List - { - myModule - }; - m_MockCloudCodeModulesLoader.Setup( - c => c.LoadPrecompiledModulesAsync( - It.IsAny>(), - It.IsAny())) - .ReturnsAsync(loadedResult); - - var result = await m_DeploymentService!.Deploy( - input, - k_ValidCcmFilePaths, - TestValues.ValidProjectId, - TestValues.ValidEnvironmentId, - null!, - CancellationToken.None); - - Assert.AreEqual(result.Failed.Count, k_FailedContents.Count + 1); - Assert.IsTrue(result.Failed.Any(x => x.Name == k_ValidSlnFilePaths.First())); + Assert.AreEqual(k_DeployedContents.First().Name.ToString(), result.Deployed.First().Name); + Assert.AreEqual(k_DeployedContents.First().Path, result.Deployed.First().Path); + Assert.AreEqual(k_FailedContents.Count, result.Failed.Count); } [Test] @@ -446,10 +206,15 @@ public async Task DeployReconcileAsync_WillCreateDeleteContent() m_MockCloudCodeModulesLoader.Reset(); m_MockCloudCodeModulesLoader.Setup( - c => c.LoadPrecompiledModulesAsync( + c => c.LoadModulesAsync( k_ValidCcmFilePaths, - CloudCodeConstants.ServiceTypeScripts)) - .ReturnsAsync(testModules.OfType().ToList()); + It.IsAny>(), + It.IsAny())) + .ReturnsAsync( + () => + { + return (testModules.OfType().ToList(), new List()); + }); m_MockDeployFileService.Setup( c => c.ListFilesToDeploy( diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeModuleLoaderTests.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeModuleLoaderTests.cs index 3e1a024..bfad2fa 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeModuleLoaderTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode.UnitTest/Deploy/CloudCodeModuleLoaderTests.cs @@ -1,72 +1,114 @@ +using System; using System.Collections.Generic; -using System.Linq; +using System.Threading; using System.Threading.Tasks; using Moq; using NUnit.Framework; -using Unity.Services.Cli.Authoring.Model; -using Unity.Services.Cli.Authoring.Service; using Unity.Services.Cli.CloudCode.Deploy; +using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment.ModuleGeneration; using Unity.Services.CloudCode.Authoring.Editor.Core.Model; +using CCModule = Unity.Services.Cli.CloudCode.Deploy.CloudCodeModule; + namespace Unity.Services.Cli.CloudCode.UnitTest.Deploy; [TestFixture] public class CloudCodeModuleLoaderTests { + readonly Mock m_MockModuleBuilder = new(); - readonly Mock m_MockCliDeploymentOutputHandler = new(); - - static readonly IReadOnlyCollection k_DeployedContents = new[] + static readonly List k_CcmPaths = new() { - new DeployContent("module.zip", "Cloud Code Modules", "path", 100, "Published"), + "new/path/to/test_a.ccm", + "new/path/to/test_b.ccm" }; - static readonly IReadOnlyCollection k_FailedContents = new[] + static readonly List k_SlnPaths = new() { - new DeployContent("invalid1.zip", "Cloud Code Modules", "path", 0, "Failed to Load"), - new DeployContent("invalid2.zip", "Cloud Code Modules", "path", 0, "Failed to Load"), + "new/path/to/sln/test_a.sln", + "new/path/to/sln/test_b.sln" }; - List m_Contents = k_DeployedContents.Concat(k_FailedContents).ToList(); + IScript m_TestAModule = new CCModule( + new ScriptName("test_a.ccm"), + Language.JS, + k_CcmPaths[0]); - static readonly List k_ValidZipPaths = new() - { - "new/path/to/test_a.zip", - "new/path/to/test_b.zip" - }; + IScript m_TestBModule = new CCModule( + new ScriptName("test_b.ccm"), + Language.JS, + k_CcmPaths[1]); [SetUp] public void SetUp() { - m_MockCliDeploymentOutputHandler.Reset(); - m_MockCliDeploymentOutputHandler.SetupGet(c => c.Contents).Returns(m_Contents); + m_MockModuleBuilder.Reset(); } [Test] - public async Task LoadPrecompiledModulesAsync_Deploys() + public async Task LoadPrecompiledModules() { - IScript test_a_module = new Unity.Services.Cli.CloudCode.Deploy.CloudCodeModule( - new ScriptName("test_a.zip"), - Language.JS, - "new/path/to/test_a.zip"); - - IScript test_b_module = new Unity.Services.Cli.CloudCode.Deploy.CloudCodeModule( - new ScriptName("test_b.zip"), - Language.JS, - "new/path/to/test_b.zip"); + var cloudCodeModulesLoader = new CloudCodeModulesLoader(m_MockModuleBuilder.Object); + + var (generatedModules, failedModules) = await cloudCodeModulesLoader.LoadModulesAsync( + k_CcmPaths, + new List(), + CancellationToken.None); + var expected = new List { - test_a_module, - test_b_module + m_TestAModule, + m_TestBModule }; + CompareScripts(expected, generatedModules); + } - var cloudCodeModulesLoader = new CloudCodeModulesLoader(); + [Test] + public async Task LoadFailedModules() + { + m_MockModuleBuilder + .Setup(x => x.CreateCloudCodeModuleFromSolution( + It.IsAny(), It.IsAny())) + .Throws(new Exception("failed")); + + var cloudCodeModulesLoader = new CloudCodeModulesLoader(m_MockModuleBuilder.Object); - var actual = await cloudCodeModulesLoader.LoadPrecompiledModulesAsync( - k_ValidZipPaths, - "Cloud Code Modules"); + var (_, failedModules) = + await cloudCodeModulesLoader.LoadModulesAsync(k_CcmPaths, k_SlnPaths, CancellationToken.None); - CompareScripts(expected, actual); + Assert.AreEqual(k_SlnPaths.Count, failedModules.Count); + for (int i = 0; i < k_SlnPaths.Count; i++) + { + Assert.AreEqual(k_SlnPaths[i], ((CCModule)failedModules[i]).SolutionPath); + } + } + + [Test] + public async Task LoadPrecompiledAndSolutions() + { + m_MockModuleBuilder.Setup( + x => x.CreateCloudCodeModuleFromSolution( + It.IsAny(), + It.IsAny())) + .Callback((m, _) => { m.CcmPath = m_TestBModule.Path; }); + + var cloudCodeModulesLoader = new CloudCodeModulesLoader(m_MockModuleBuilder.Object); + + var (generatedModules, failedModules) = await cloudCodeModulesLoader.LoadModulesAsync( + new List() { k_CcmPaths[0] }, + new List() { k_SlnPaths[1] }, + CancellationToken.None); + + var expected = new List + { + m_TestAModule, + m_TestBModule + }; + Assert.AreEqual(expected.Count, generatedModules.Count); + for (int i = 0; i < expected.Count; i++) + { + Assert.AreEqual(expected[i].Path, generatedModules[i].Path); + } } static void CompareScripts(List expected, List actual) diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/CloudCodeModule.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/CloudCodeModule.cs index 5178136..a8ccc2c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/CloudCodeModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/CloudCodeModule.cs @@ -32,6 +32,7 @@ using Unity.Services.Cli.CloudCode.Solution; using Unity.Services.Cli.CloudCode.Templates; using Unity.Services.Cli.CloudCode.Utils; +using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment.ModuleGeneration; using Unity.Services.CloudCode.Authoring.Editor.Core.Dotnet; using Unity.Services.CloudCode.Authoring.Editor.Core.IO; using Unity.Services.CloudCode.Authoring.Editor.Core.Solution; @@ -370,6 +371,8 @@ public static void RegisterServices(HostBuilderContext hostBuilderContext, IServ serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); } @@ -391,9 +394,6 @@ internal static CloudCodeModuleDeploymentService CreateCSharpDeployService(IServ provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService()); + provider.GetRequiredService()); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CliCloudCodeDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CliCloudCodeDeploymentHandler.cs index 39af0cb..9fc0a70 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CliCloudCodeDeploymentHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CliCloudCodeDeploymentHandler.cs @@ -22,20 +22,31 @@ public CliCloudCodeDeploymentHandler( protected override void UpdateScriptStatus( IScript script, string message, string detail, StatusSeverityLevel level = StatusSeverityLevel.None) { - if(script is DeployContent deployContent) + if (script is DeployContent deployContent) { deployContent.Status = new DeploymentStatus( message, detail, (SeverityLevel)Enum.Parse(typeof(SeverityLevel), level.ToString())); } + else if (script is ModuleDeployContent moduleDeployContent) + { + moduleDeployContent.Status = new DeploymentStatus( + message, + detail, + (SeverityLevel)Enum.Parse(typeof(SeverityLevel), level.ToString())); + } } protected override void UpdateScriptProgress(IScript script, float progress) { - if(script is DeployContent deployContent) + if (script is DeployContent deployContent) { deployContent.Progress = progress; } + else if (script is ModuleDeployContent modDeployContent) + { + modDeployContent.Progress = progress; + } } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModule.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModule.cs index f852fb6..d97e987 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModule.cs @@ -1,6 +1,5 @@ using System.Globalization; using Newtonsoft.Json; -using Unity.Services.Cli.Authoring.Model; using Unity.Services.Cli.CloudCode.Utils; using Unity.Services.CloudCode.Authoring.Editor.Core.Model; using Unity.Services.DeploymentApi.Editor; @@ -9,7 +8,7 @@ namespace Unity.Services.Cli.CloudCode.Deploy; -class CloudCodeModule : DeployContent, IScript +class CloudCodeModule : ModuleDeployContent, IScript, IModuleItem { [JsonConverter(typeof(ScriptNameJsonConverter))] // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global used for deserialization @@ -20,6 +19,19 @@ class CloudCodeModule : DeployContent, IScript public string LastPublishedDate { get; set; } [JsonIgnore] public string SignedUrl { get; set; } + + public CloudCodeModule(string solutionPath) + : this( + default, + default, + "", + "", + new List(), + "") + { + SolutionPath = solutionPath; + } + public CloudCodeModule() : this( default, @@ -101,4 +113,22 @@ public CloudCodeModule(GetModuleResponse response) LastPublishedDate = response.DateModified.ToString(CultureInfo.InvariantCulture); SignedUrl = ""; } + + public string SolutionPath { get; } = ""; + + public string CcmPath + { + get => Path; + set => Path = value; + } + + public string ModuleName + { + get => Name.ToString(); + set + { + Name = new ScriptName(value); + base.Name = System.IO.Path.GetFileNameWithoutExtension(value); + } + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModuleDeploymentService.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModuleDeploymentService.cs index 3917d58..bc3e611 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModuleDeploymentService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModuleDeploymentService.cs @@ -1,3 +1,5 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; using Spectre.Console; using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Model; @@ -6,6 +8,8 @@ using Unity.Services.Cli.CloudCode.Model; using Unity.Services.Cli.CloudCode.Utils; using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment; +using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment.ModuleGeneration; +using Unity.Services.CloudCode.Authoring.Editor.Core.Dotnet; using Unity.Services.CloudCode.Authoring.Editor.Core.IO; using Unity.Services.CloudCode.Authoring.Editor.Core.Model; using Unity.Services.DeploymentApi.Editor; @@ -24,8 +28,6 @@ class CloudCodeModuleDeploymentService : IDeploymentService public string ServiceName => m_ServiceName; - public static string OutputPath = "module-compilation"; - public IReadOnlyList FileExtensions { get; } = new[] { CloudCodeConstants.FileExtensionModulesCcm, @@ -35,28 +37,19 @@ class CloudCodeModuleDeploymentService : IDeploymentService readonly string m_ServiceType; readonly string m_ServiceName; readonly IDeployFileService m_DeployFileService; - readonly ISolutionPublisher m_SolutionPublisher; - readonly IModuleZipper m_ModuleZipper; - readonly IFileSystem m_FileSystem; public CloudCodeModuleDeploymentService( ICloudCodeDeploymentHandler deployHandler, ICloudCodeModulesLoader cloudCodeModulesLoader, ICliEnvironmentProvider environmentProvider, ICSharpClient client, - IDeployFileService deployFileService, - ISolutionPublisher solutionPublisher, - IModuleZipper moduleZipper, - IFileSystem fileSystem) + IDeployFileService deployFileService) { CloudCodeModulesLoader = cloudCodeModulesLoader; EnvironmentProvider = environmentProvider; CliCloudCodeClient = client; CloudCodeDeploymentHandler = deployHandler; m_DeployFileService = deployFileService; - m_SolutionPublisher = solutionPublisher; - m_ModuleZipper = moduleZipper; - m_FileSystem = fileSystem; m_ServiceType = CloudCodeConstants.ServiceTypeModules; m_ServiceName = CloudCodeConstants.ServiceNameModules; @@ -77,26 +70,10 @@ public async Task Deploy( var (ccmFilePaths, slnFilePaths) = ListFilesToDeploy(filePaths.ToList()); - var failedResultList = new List(); - - if (slnFilePaths.Count > 0) - { - loadingContext?.Status("Generating Cloud Code Modules for solution files..."); - - var (generatedCcmFilePaths, failedGenerationResult) = - await CompileModules(slnFilePaths, cancellationToken); - - ccmFilePaths.AddRange(generatedCcmFilePaths); - ccmFilePaths = ccmFilePaths.Distinct().ToList(); - - failedResultList.AddRange(failedGenerationResult); - } - loadingContext?.Status($"Loading {m_ServiceName} modules..."); - var loadResult = await CloudCodeModulesLoader.LoadPrecompiledModulesAsync( - ccmFilePaths, - m_ServiceType); + var (loadedModules, failedModules) = + await CloudCodeModulesLoader.LoadModulesAsync(ccmFilePaths, slnFilePaths, cancellationToken); loadingContext?.Status($"Deploying {m_ServiceType}..."); @@ -106,8 +83,8 @@ public async Task Deploy( try { - result = await CloudCodeDeploymentHandler.DeployAsync(loadResult, reconcile, dryrun); - failedResultList.AddRange(result.Failed); + result = await CloudCodeDeploymentHandler.DeployAsync(loadedModules, reconcile, dryrun); + failedModules.AddRange(result.Failed); } catch (ApiException) { @@ -121,45 +98,7 @@ public async Task Deploy( result = ex.Result; } - return ConstructResult(loadResult, result, deployInput, failedResultList); - } - - static CloudCodeModule SetUpFailedCloudCodeModule(ModuleGenerationResult failedGenerationSolution) - { - return new CloudCodeModule( - ScriptName.FromPath(failedGenerationSolution.SolutionPath).ToString(), - failedGenerationSolution.SolutionPath, - 0, - new DeploymentStatus(Statuses.FailedToRead, "Could not generate module for solution.")); - } - - async Task<(List, List)> CompileModules(List slnFilePaths, CancellationToken cancellationToken) - { - var ccmFilePaths = new List(); - var failedToGenerateList = new List(); - var generateTasks = new List>(); - foreach (var slnFilePath in slnFilePaths) - { - generateTasks.Add(CreateCloudCodeModuleFromSolution(slnFilePath, cancellationToken)); - } - - if (generateTasks.Count > 0) - { - var generationResultList = await Task.WhenAll(generateTasks); - foreach (var generationResult in generationResultList) - { - if (generationResult.Success) - { - ccmFilePaths.Add(generationResult.CcmPath); - } - else - { - failedToGenerateList.Add(SetUpFailedCloudCodeModule(generationResult)); - } - } - } - - return (ccmFilePaths, failedToGenerateList); + return ConstructResult(loadedModules, result, deployInput, failedModules); } (List, List) ListFilesToDeploy(List filePaths) @@ -181,6 +120,15 @@ static CloudCodeModule SetUpFailedCloudCodeModule(ModuleGenerationResult failedG return (ccmFilePaths, slnFilePaths); } + static IDeploymentItem SetPathAsSolutionWhenAvailable(IScript item) + { + return new CloudCodeModule( + item.Name.ToString(), + string.IsNullOrEmpty(((CloudCodeModule)item).SolutionPath) ? ((CloudCodeModule)item).Path : ((CloudCodeModule)item).SolutionPath, + ((CloudCodeModule)item).Progress, + ((CloudCodeModule)item).Status); + } + static DeploymentResult ConstructResult(List loadResult, DeployResult? result, DeployInput deployInput, List failedModules) { DeploymentResult deployResult; @@ -191,11 +139,11 @@ static DeploymentResult ConstructResult(List loadResult, DeployResult? else { deployResult = new DeploymentResult( - result.Updated.Select(item => item as IDeploymentItem).ToList() as IReadOnlyList, + result.Updated.Select(SetPathAsSolutionWhenAvailable).ToList() as IReadOnlyList, ToDeleteDeploymentItems(result.Deleted, deployInput.DryRun), - result.Created.Select(item => item as IDeploymentItem).ToList() as IReadOnlyList, - result.Deployed.Select(item => item as IDeploymentItem).ToList() as IReadOnlyList, - failedModules.Select(item => item as IDeploymentItem).ToList() as IReadOnlyList, + result.Created.Select(SetPathAsSolutionWhenAvailable).ToList() as IReadOnlyList, + result.Deployed.Select(SetPathAsSolutionWhenAvailable).ToList() as IReadOnlyList, + failedModules.Select(SetPathAsSolutionWhenAvailable).ToList() as IReadOnlyList, deployInput.DryRun); } @@ -219,45 +167,4 @@ static IReadOnlyList ToDeleteDeploymentItems(IReadOnlyList CreateCloudCodeModuleFromSolution( - string solutionPath, - CancellationToken cancellationToken) - { - var success = true; - - var slnName = Path.GetFileNameWithoutExtension(solutionPath); - var dllOutputPath = Path.Combine(Path.GetTempPath(), slnName); - var moduleCompilationPath = Path.Combine(dllOutputPath, "module-compilation"); - - var ccmPath = dllOutputPath; - try - { - var moduleName = await m_SolutionPublisher.PublishToFolder( - solutionPath, - moduleCompilationPath, - cancellationToken); - ccmPath = await m_ModuleZipper.ZipCompilation(moduleCompilationPath, moduleName, cancellationToken); - } - catch (Exception) - { - success = false; - } - - return new ModuleGenerationResult(solutionPath, ccmPath, success); - } - - class ModuleGenerationResult - { - public string SolutionPath { get; } - public string CcmPath { get; } - public bool Success { get; } - - public ModuleGenerationResult(string solutionPath, string ccmPath, bool success) - { - SolutionPath = solutionPath; - CcmPath = ccmPath; - Success = success; - } - } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModulesDownloader.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModulesDownloader.cs index be7cd30..295e906 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModulesDownloader.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModulesDownloader.cs @@ -2,13 +2,13 @@ namespace Unity.Services.Cli.CloudCode.Deploy; class CloudCodeModulesDownloader : ICloudCodeModulesDownloader { - readonly HttpClient _httpClient; - public CloudCodeModulesDownloader(HttpClient client) => _httpClient = client; + readonly HttpClient m_HttpClient; + public CloudCodeModulesDownloader(HttpClient client) => m_HttpClient = client; public Task DownloadModule( CloudCodeModule module, CancellationToken cancellationToken) { - return _httpClient.GetStreamAsync(module.SignedUrl, cancellationToken); + return m_HttpClient.GetStreamAsync(module.SignedUrl, cancellationToken); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModulesLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModulesLoader.cs index cd2a9da..7ad1d05 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModulesLoader.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModulesLoader.cs @@ -1,4 +1,5 @@ using Unity.Services.Cli.Authoring.Model; +using Unity.Services.CloudCode.Authoring.Editor.Core.Deployment.ModuleGeneration; using Unity.Services.CloudCode.Authoring.Editor.Core.Model; using Unity.Services.DeploymentApi.Editor; @@ -6,22 +7,76 @@ namespace Unity.Services.Cli.CloudCode.Deploy; class CloudCodeModulesLoader : ICloudCodeModulesLoader { - public Task> LoadPrecompiledModulesAsync( - IReadOnlyList paths, - string serviceType) + readonly IModuleBuilder m_ModuleBuilder; + + public CloudCodeModulesLoader(IModuleBuilder moduleBuilder) + { + m_ModuleBuilder = moduleBuilder; + } + + public async Task<(List, List)> LoadModulesAsync( + IReadOnlyList ccmFilePaths, IReadOnlyList solutionPaths, CancellationToken cancellationToken) + { + List failedModules = new List(); + + var loadResult = LoadPrecompiledModulesAsync(ccmFilePaths); + + if (solutionPaths.Count > 0) + { + List generatedModules; + (generatedModules, failedModules) = await LoadModulesFromSolutionsAsync(solutionPaths, cancellationToken); + loadResult.AddRange(generatedModules); + } + + return (loadResult, failedModules); + } + + async Task<(List, List)> LoadModulesFromSolutionsAsync( + IReadOnlyList solutionPaths, + CancellationToken cancellationToken) + { + var generationList = new List(); + + foreach (var solutionPath in solutionPaths) + { + var ccmr = new CloudCodeModule(solutionPath); + try + { + await m_ModuleBuilder + .CreateCloudCodeModuleFromSolution(ccmr, cancellationToken); + } + catch (Exception e) + { + ccmr.Status = + new DeploymentStatus("Failed to compile", e.Message, SeverityLevel.Error); + } + generationList.Add(ccmr); + } + + return ( + new List(generationList.Where(module => !string.IsNullOrEmpty(module.CcmPath)).ToList()), + new List(generationList.Where(module => string.IsNullOrEmpty(module.CcmPath)).ToList())); + } + + static List LoadPrecompiledModulesAsync(IReadOnlyList paths) { var modules = new List(); foreach (var path in paths) { modules.Add( - new CloudCodeModule( - ScriptName.FromPath(path).ToString(), - path, - 0, - new DeploymentStatus(Statuses.Loaded))); + CreateCloudCodeModule(path, new DeploymentStatus(Statuses.Loaded))); } - return Task.FromResult(modules); + return modules; + } + + static CloudCodeModule CreateCloudCodeModule(string ccmPath, DeploymentStatus deploymentStatus) + { + return new CloudCodeModule( + ScriptName.FromPath(ccmPath).ToString(), + ccmPath, + 0, + deploymentStatus); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ICloudCodeModulesLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ICloudCodeModulesLoader.cs index a08a2b3..4f92883 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ICloudCodeModulesLoader.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ICloudCodeModulesLoader.cs @@ -1,11 +1,11 @@ using Unity.Services.CloudCode.Authoring.Editor.Core.Model; -using Unity.Services.Gateway.CloudCodeApiV1.Generated.Model; namespace Unity.Services.Cli.CloudCode.Deploy; interface ICloudCodeModulesLoader { - Task> LoadPrecompiledModulesAsync( - IReadOnlyList paths, - string serviceType); + Task<(List, List)> LoadModulesAsync( + IReadOnlyList ccmFilePaths, + IReadOnlyList solutionFilePaths, + CancellationToken cancellationToken); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ModuleDeployContent.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ModuleDeployContent.cs new file mode 100644 index 0000000..664dfd7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/ModuleDeployContent.cs @@ -0,0 +1,111 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Newtonsoft.Json; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.CloudCode.Deploy; + +[Serializable] +public class ModuleDeployContent : IDeploymentItem, ITypedItem +{ + /// + /// Name of the deploy content + /// + public string Name { get; set; } + + /// + /// Type of the deploy content + /// + public string Type { get; } + + /// + /// Path of the deploy content + /// + public string Path { get; set; } + + /// + /// + /// + public float Progress + { + get => m_Progress; + set => SetField(ref m_Progress, value); + } + + public DeploymentStatus Status + { + get => m_Status; + set => SetField(ref m_Status, value); + } + + [JsonIgnore] + public ObservableCollection States { get; } + + DeploymentStatus m_Status; + float m_Progress; + + /// + /// Detail message for the status + /// + [JsonIgnore] + public string Detail => m_Status.MessageDetail; + + public ModuleDeployContent(string name, string type, string path, float progress = 0, DeploymentStatus? status = null) + { + Name = name; + Type = type; + Path = path; + States = new ObservableCollection(); + Progress = progress; + m_Status = status ?? DeploymentStatus.Empty; + } + + public ModuleDeployContent( + string name, + string type, + string path, + float progress, + string status, + string? detail = null, + SeverityLevel level = SeverityLevel.None) + : this(name, type, path, progress, new DeploymentStatus(status, detail ?? string.Empty, level)) + { + } + + public override string ToString() + { + return $"'{Path}'"; + } + + /// + /// Event will be raised when a property of the instance is changed + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Sets the field and raises an OnPropertyChanged event. + /// + /// The field to set. + /// The value to set. + /// The callback. + /// Name of the property to set. + /// Type of the parameter. + protected void SetField( + ref T field, + T value, + Action? onFieldChanged = null, + [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + return; + field = value; + OnPropertyChanged(propertyName!); + onFieldChanged?.Invoke(field); + } + + void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/NoopDeploymentAnalytics.cs b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/NoopDeploymentAnalytics.cs index 9d38d80..7f9df52 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/NoopDeploymentAnalytics.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/NoopDeploymentAnalytics.cs @@ -17,6 +17,11 @@ public IDisposable Scope() return new NoopDisposable(); } + public IDisposable BeginDeploySend(int fileSize, string fileType) + { + return new NoopDisposable(); + } + public IDisposable BeginDeploySend(int fileSize) { return new NoopDisposable(); 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 ca126f6..7d21f79 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 @@ -21,7 +21,7 @@ - + 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 fa0b0ff..d19c76d 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 @@ -24,7 +24,7 @@ - + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Economy.UnitTest/Service/EconomyServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Economy.UnitTest/Service/EconomyServiceTests.cs index fd81a6e..16eda4f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Economy.UnitTest/Service/EconomyServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Economy.UnitTest/Service/EconomyServiceTests.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; +using Newtonsoft.Json; using NUnit.Framework; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Models; using Unity.Services.Cli.Common.Validator; +using Unity.Services.Cli.Economy.Exceptions; using Unity.Services.Cli.Economy.Service; using Unity.Services.Cli.Economy.UnitTest.Mock; using Unity.Services.Cli.Economy.UnitTest.Utils; @@ -387,6 +390,54 @@ public async Task AddResourceAsync_WithValidInput_AddsResource() Times.Once); } + [Test] + public void AddResourceAsync_WithInvalidDependency_ShowsCorrectError() + { + var dictionary = new Dictionary + { + { "type", "problems/validation" }, + { "title", "Validation error" }, + { "status", 400 }, + { "detail", "Invalid Request" }, + { "instance", null! }, + { "code", 1007 }, + { "errors", new List> + { + new Dictionary + { + { "field", "costs.0" }, + { "messages", new List { "Cost resource not found." } } + } + } + } + }; + var errorMessage = JsonConvert.SerializeObject(dictionary, Formatting.Indented); + var expectedMessage = "Cost resource not found."; + var expectedException = new InvalidDataException("test exception", new InvalidDataException(errorMessage)); + + m_EconomyApiV2AsyncMock.DefaultApiAsyncObject.Setup( + api => + api.AddConfigResourceAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Throws(expectedException); + + CurrencyItemRequest currencyRequest = new CurrencyItemRequest("id", "name", CurrencyItemRequest.TypeEnum.CURRENCY); + AddConfigResourceRequest addRequest = new AddConfigResourceRequest(currencyRequest); + + + Assert.ThrowsAsync( + () => m_EconomyService!.AddAsync( + addRequest, + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + CancellationToken.None), + expectedMessage); + } + [Test] public void AddResourceAsync_InvalidProjectId_ThrowsConfigValidationException() { diff --git a/Unity.Services.Cli/Unity.Services.Cli.Economy/Exceptions/InvalidResourceException.cs b/Unity.Services.Cli/Unity.Services.Cli.Economy/Exceptions/InvalidResourceException.cs new file mode 100644 index 0000000..c1b1d9d --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Economy/Exceptions/InvalidResourceException.cs @@ -0,0 +1,10 @@ +using Unity.Services.Gateway.EconomyApiV2.Generated.Client; + +namespace Unity.Services.Cli.Economy.Exceptions; + +[Serializable] +public class InvalidResourceException : ApiException +{ + public InvalidResourceException(string message, Exception innerException) + : base(Common.Exceptions.ExitCode.HandledError, $"Economy resource file is invalid: {message}", innerException) { } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Economy/Service/EconomyService.cs b/Unity.Services.Cli/Unity.Services.Cli.Economy/Service/EconomyService.cs index f8310f9..76829c7 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Economy/Service/EconomyService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Economy/Service/EconomyService.cs @@ -1,7 +1,9 @@ using System.Net; +using System.Text.RegularExpressions; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Models; using Unity.Services.Cli.Common.Validator; +using Unity.Services.Cli.Economy.Exceptions; using Unity.Services.Cli.ServiceAccountAuthentication; using Unity.Services.Cli.ServiceAccountAuthentication.Token; using Unity.Services.Gateway.EconomyApiV2.Generated.Api; @@ -88,7 +90,7 @@ public async Task DeleteAsync(string resourceId, string projectId, string enviro m_ConfigValidator.ThrowExceptionIfConfigInvalid(Keys.ConfigKeys.ProjectId, projectId); m_ConfigValidator.ThrowExceptionIfConfigInvalid(Keys.ConfigKeys.EnvironmentId, environmentId); - await m_EconomyApiAsync.DeleteConfigResourceAsync(projectId, Guid.Parse(environmentId), resourceId, cancellationToken: cancellationToken ); + await m_EconomyApiAsync.DeleteConfigResourceAsync(projectId, Guid.Parse(environmentId), resourceId, cancellationToken: cancellationToken); } public async Task AddAsync(AddConfigResourceRequest request, string projectId, string environmentId, @@ -98,7 +100,27 @@ public async Task AddAsync(AddConfigResourceRequest request, string projectId, s m_ConfigValidator.ThrowExceptionIfConfigInvalid(Keys.ConfigKeys.ProjectId, projectId); m_ConfigValidator.ThrowExceptionIfConfigInvalid(Keys.ConfigKeys.EnvironmentId, environmentId); - await m_EconomyApiAsync.AddConfigResourceAsync(projectId, Guid.Parse(environmentId), request, cancellationToken: cancellationToken ); + try + { + await m_EconomyApiAsync.AddConfigResourceAsync(projectId, Guid.Parse(environmentId), request, cancellationToken: cancellationToken); + } + catch (Exception e) + { + var innerException = e.InnerException; + if (e.InnerException is InvalidDataException oldInnerException) + { + var oldMessage = oldInnerException.Message; + const string pattern = "messages\":\\[(.*?)\\]"; + var match = Regex.Match(oldMessage, pattern, RegexOptions.None, TimeSpan.FromMilliseconds(100)); + + if (match.Success) + { + var messagesData = match.Groups[1].Value; + innerException = new InvalidDataException(messagesData); + } + } + throw new InvalidResourceException(innerException!.Message, innerException); + } } public async Task EditAsync(string resourceId, AddConfigResourceRequest request, string projectId, string environmentId, @@ -108,6 +130,6 @@ public async Task EditAsync(string resourceId, AddConfigResourceRequest request, m_ConfigValidator.ThrowExceptionIfConfigInvalid(Keys.ConfigKeys.ProjectId, projectId); m_ConfigValidator.ThrowExceptionIfConfigInvalid(Keys.ConfigKeys.EnvironmentId, environmentId); - await m_EconomyApiAsync.EditConfigResourceAsync(projectId, Guid.Parse(environmentId), resourceId, request, cancellationToken: cancellationToken ); + await m_EconomyApiAsync.EditConfigResourceAsync(projectId, Guid.Parse(environmentId), resourceId, request, cancellationToken: cancellationToken); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingModuleTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingModuleTests.cs index 7de75bd..4f79455 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingModuleTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingModuleTests.cs @@ -75,7 +75,7 @@ out var resultCommand } [Test] - public void ValidatFleetRegionCommands() + public void ValidateFleetRegionCommands() { var commandLineBuilder = new CommandLineBuilder(); commandLineBuilder.AddModule(k_GshModule); @@ -113,6 +113,8 @@ out var resultCommand Assert.That(resultCommand, Is.EqualTo(k_GshModule.ServerCommand)); Assert.That(k_GshModule.ServerGetCommand.Handler, Is.Not.Null); Assert.That(k_GshModule.ServerListCommand.Handler, Is.Not.Null); + Assert.That(k_GshModule.ServerFilesListCommand.Handler, Is.Not.Null); + Assert.That(k_GshModule.ServerFilesDownloadCommand.Handler, Is.Not.Null); } ); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingUnitTestsConstants.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingUnitTestsConstants.cs index 59e9967..39b4ce6 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingUnitTestsConstants.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingUnitTestsConstants.cs @@ -50,7 +50,6 @@ public static class GameServerHostingUnitTestsConstants public const string ValidBuildConfigurationQueryType = "a2s"; - public const string ValidRegionId = "00000000-0000-0000-0000-000000000000"; public const string ValidRegionId2 = "00000000-0000-0000-0000-000000000001"; public const string InvalidRegionId = "00000000-0000-0000-0000-000000000002"; @@ -71,6 +70,10 @@ public static class GameServerHostingUnitTestsConstants public const long ValidLocationId = 111111L; public const string ValidLocationName = "us-west1"; + public const string ValidUsageSettingsJson = "{\"hardwareType\":\"CLOUD\", \"machineType\":\"GCP-N2\", \"maxServersPerMachine\":5}"; + + public const string ValidOutputDirectory = "test/output"; + // Machine specific constants public const long ValidMachineId = 654321L; public const long InvalidMachineId = 666L; diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileDownloadHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileDownloadHandlerTests.cs new file mode 100644 index 0000000..59e2509 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileDownloadHandlerTests.cs @@ -0,0 +1,177 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Spectre.Console; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.GameServerHosting.Exceptions; +using Unity.Services.Cli.GameServerHosting.Handlers; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.TestUtils; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Handlers; + +[TestFixture] +class FileDownloadHandlerTests : HandlerCommon +{ + [Test] + public async Task FileDownloadAsync_CallsLoadingIndicatorStartLoading() + { + var mockLoadingIndicator = new Mock(); + + await FileDownloadHandler.FileDownloadAsync( + null!, + MockUnityEnvironment.Object, + null!, + null!, + null!, + mockLoadingIndicator.Object, + CancellationToken.None); + + mockLoadingIndicator.Verify( + ex => ex.StartLoadingAsync( + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } + + [Test] + public async Task FileDownloadAsync_CallsFetchIdentifierAsync() + { + FileDownloadInput input = new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + ServerId = ValidServerId.ToString(), + Path = "some/path", + Output = ValidOutputDirectory, + }; + + await FileDownloadHandler.FileDownloadAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + MockHttpClient!.Object, + CancellationToken.None + ); + + MockUnityEnvironment.Verify(ex => ex.FetchIdentifierAsync(CancellationToken.None), Times.Once); + } + + [Test] + public async Task FileDownloadAsync_CallsAuthorizeGameServerHostingService() + { + FileDownloadInput input = new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + ServerId = ValidServerId.ToString(), + Path = "some/path", + Output = ValidOutputDirectory, + }; + + await FileDownloadHandler.FileDownloadAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + MockHttpClient!.Object, + CancellationToken.None + ); + + MockUnityEnvironment.Verify(ex => ex.FetchIdentifierAsync(CancellationToken.None), Times.Once); + } + + [Test] + public async Task FileDownloadAsync_CallsGenerateDownloadURLAsync() + { + FileDownloadInput input = new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + ServerId = ValidServerId.ToString(), + Path = "some/path", + Output = ValidOutputDirectory, + }; + + await FileDownloadHandler.FileDownloadAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + MockHttpClient!.Object, + CancellationToken.None + ); + + FilesApi!.DefaultFilesClient.Verify( + api => api.GenerateDownloadURLAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + 0, + It.IsAny() + ), + Times.Once + ); + } + + [TestCase( + null, + "some/path", + ValidOutputDirectory, + typeof(ArgumentNullException), + TestName = "ServerId is null")] + [TestCase( + "1", + null, + ValidOutputDirectory, + typeof(ArgumentNullException), + TestName = "Path is null")] + [TestCase( + "1", + "some/path", + null, + typeof(MissingInputException), + TestName = "Output is null")] + public void FileDownloadAsync_InvalidInputThrowsException( + string? serverId, + string? path, + string? output, + Type exceptionType + ) + { + FileDownloadInput input = new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + ServerId = serverId, + Path = path, + Output = output, + }; + + Assert.ThrowsAsync( + exceptionType, + async () => + { + await FileDownloadHandler.FileDownloadAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + MockHttpClient!.Object, + CancellationToken.None + ); + } + ); + + TestsHelper.VerifyLoggerWasCalled( + MockLogger!, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Never + ); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileListHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileListHandlerTests.cs new file mode 100644 index 0000000..0c94fb3 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileListHandlerTests.cs @@ -0,0 +1,149 @@ +using System.Globalization; +using Microsoft.Extensions.Logging; +using Moq; +using Spectre.Console; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.GameServerHosting.Exceptions; +using Unity.Services.Cli.GameServerHosting.Handlers; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.TestUtils; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Handlers; + +[TestFixture] +class FileListHandlerTests : HandlerCommon +{ + [Test] + public async Task FilesListAsync_CallsLoadingIndicatorStartLoading() + { + var mockLoadingIndicator = new Mock(); + + await FileListHandler.FileListAsync( + null!, + MockUnityEnvironment.Object, + null!, + null!, + mockLoadingIndicator.Object, + CancellationToken.None); + + mockLoadingIndicator.Verify( + ex => ex.StartLoadingAsync( + It.IsAny(), + It.IsAny>() + ), + Times.Once + ); + } + + [Test] + public async Task FilesListAsync_CallsFetchIdentifierAsync() + { + FileListInput input = new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + ServerIds = new[] { ValidServerId }, + Limit = "100", + ModifiedFrom = "2017-07-21T17:32:25Z", + ModifiedTo = "2017-07-22T17:32:25Z", + PathFilter = "", + }; + + await FileListHandler.FileListAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ); + + MockUnityEnvironment.Verify(ex => ex.FetchIdentifierAsync(CancellationToken.None), Times.Once); + } + + [TestCase(1, "2017-07-21T17:32:25Z", "2017-07-21T17:32:25Z", "/games/tf2/", new[] { ValidServerId }, TestName = "Golden path")] + public async Task FilesListAsync_CallsListService( + long limit, + string modifiedFrom, + string modifiedTo, + string pathFilter, + long[] serverIds + ) + { + FileListInput input = new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + Limit = limit.ToString(), + ModifiedFrom = modifiedFrom, + ModifiedTo = modifiedTo, + PathFilter = pathFilter, + ServerIds = serverIds + }; + + await FileListHandler.FileListAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ); + + FilesApi!.DefaultFilesClient.Verify( + api => api.ListFilesAsync( + new Guid(ValidProjectId), + new Guid(ValidEnvironmentId), + new FilesListRequest( + limit, + DateTime.Parse(modifiedFrom).ToUniversalTime(), + DateTime.Parse(modifiedFrom).ToUniversalTime(), + pathFilter, + serverIds.ToList() + ), + 0, + CancellationToken.None + ), + Times.Once + ); + } + + [TestCase(null, 1, "2017-07-21T17:32:25Z", "2017-07-21T17:32:25Z", "/games/tf2/", new[] { ValidServerId }, typeof(ArgumentNullException), TestName = "Null Project Id throws ArgumentNullException")] + [TestCase(InvalidProjectId, 1, "2017-07-21T17:32:25Z", "2017-07-21T17:32:25Z", "/games/tf2/", new[] { ValidServerId }, typeof(HttpRequestException), TestName = "Invalid Project Id throws HttpRequestException")] + [TestCase(ValidProjectId, 1, "2017-07-21T17:32:25Z", "2017-07-21T17:32:25Z", "/games/tf2/", null, typeof(MissingInputException), TestName = "Null Server Ids throws ArgumentNullException")] + public void FilesListAsync_InvalidInputThrowsException( + string? projectId, + long limit, + DateTime? modifiedFrom, + DateTime? modifiedTo, + string? pathFilter, + long[]? serverIds, + Type exceptionType + ) + { + FileListInput input = new() + { + CloudProjectId = projectId, + TargetEnvironmentName = ValidEnvironmentName, + Limit = limit.ToString(), + ModifiedFrom = modifiedFrom == null ? null : modifiedFrom.ToString(), + ModifiedTo = modifiedTo == null ? null : modifiedTo.ToString(), + PathFilter = pathFilter, + ServerIds = serverIds + }; + + Assert.ThrowsAsync(exceptionType, () => + FileListHandler.FileListAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) + ); + + TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); + } +} + + diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetCreateHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetCreateHandlerTests.cs index 798e81d..0bd84c3 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetCreateHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetCreateHandlerTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Moq; +using Newtonsoft.Json; using Spectre.Console; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Logging; @@ -43,6 +44,10 @@ public async Task FleetCreateAsync_CallsFetchIdentifierAsync() Regions = new[] { ValidRegionId + }, + UsageSettings = new[] + { + ValidUsageSettingsJson } }; @@ -58,7 +63,8 @@ await FleetCreateHandler.FleetCreateAsync( MockUnityEnvironment.Verify(ex => ex.FetchIdentifierAsync(CancellationToken.None), Times.Once); } - [TestCase(ValidProjectId, ValidEnvironmentName, ValidFleetName, FleetCreateRequest.OsFamilyEnum.LINUX, + [TestCase(ValidProjectId, ValidEnvironmentName, ValidFleetName, + FleetCreateRequest.OsFamilyEnum.LINUX, new[] { ValidBuildConfigurationId @@ -66,10 +72,13 @@ await FleetCreateHandler.FleetCreateAsync( new[] { ValidRegionId + }, + new[]{ + ValidUsageSettingsJson } )] public async Task FleetCreateAsync_CallsCreateService(string projectId, string environmentName, string fleetName, - FleetCreateRequest.OsFamilyEnum osFamily, long[] buildConfigurations, string[] regions) + FleetCreateRequest.OsFamilyEnum osFamily, long[] buildConfigurations, string[] regions, string[] usageSettings) { FleetCreateInput input = new() { @@ -78,7 +87,8 @@ public async Task FleetCreateAsync_CallsCreateService(string projectId, string e FleetName = fleetName, OsFamily = osFamily, BuildConfigurations = buildConfigurations, - Regions = regions + Regions = regions, + UsageSettings = usageSettings }; await FleetCreateHandler.FleetCreateAsync(input, MockUnityEnvironment.Object, GameServerHostingService!, @@ -86,8 +96,10 @@ await FleetCreateHandler.FleetCreateAsync(input, MockUnityEnvironment.Object, Ga var regionList = regions.Select(r => new Region(regionID: new Guid(r))).ToList(); + var usageSetting = JsonConvert.DeserializeObject(ValidUsageSettingsJson); + var createRequest = new FleetCreateRequest(name: input.FleetName, osFamily: input.OsFamily, - buildConfigurations: buildConfigurations.ToList(), regions: regionList); + buildConfigurations: buildConfigurations.ToList(), regions: regionList, usageSettings: new List { usageSetting! }); FleetsApi!.DefaultFleetsClient.Verify(api => api.CreateFleetAsync( new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), @@ -147,7 +159,7 @@ await FleetCreateHandler.FleetCreateAsync(input, MockUnityEnvironment.Object, Ga ValidBuildConfigurationId }, new string[0], - TestName = "Emty build regions" + TestName = "Empty build regions" )] public Task FleetCreateAsync_MissingInputThrowsException(string? projectId, string? environmentName, string? fleetName, @@ -175,4 +187,46 @@ public Task FleetCreateAsync_MissingInputThrowsException(string? projectId, stri TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); return Task.CompletedTask; } + + [TestCase(ValidProjectId, ValidEnvironmentName, ValidFleetName, + FleetCreateRequest.OsFamilyEnum.LINUX, + new[] + { + ValidBuildConfigurationId + }, + new[] + { + ValidRegionId + }, + new[]{ + "badjson" + } + )] + public Task FleetCreateAsync_InvalidUsageSettingsJsonThrowsException(string? projectId, string? environmentName, + string? fleetName, + FleetCreateRequest.OsFamilyEnum? osFamily, long[] buildConfigurations, string[] regions, string[] usageSettings) + { + FleetCreateInput input = new() + { + CloudProjectId = projectId, + TargetEnvironmentName = environmentName, + FleetName = fleetName, + OsFamily = osFamily, + BuildConfigurations = buildConfigurations, + Regions = regions, + UsageSettings = usageSettings + }; + + Assert.ThrowsAsync(() => + FleetCreateHandler.FleetCreateAsync(input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) + ); + + TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); + return Task.CompletedTask; + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetUpdateHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetUpdateHandlerTests.cs index 580d4b6..5928078 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetUpdateHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetUpdateHandlerTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Moq; +using Newtonsoft.Json; using Spectre.Console; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Logging; @@ -38,7 +39,8 @@ public async Task FleetUpdateAsync_CallsFetchIdentifierAsync() BuildConfigs = new List(), DisabledDeleteTtl = 0, ShutdownTtl = 0, - DeleteTtl = 0 + DeleteTtl = 0, + UsageSettings = new List { ValidUsageSettingsJson } }; await FleetUpdateHandler.FleetUpdateAsync( @@ -101,7 +103,8 @@ string fleetId DeleteTtl = 0, BuildConfigs = new List() { 1 }, DisabledDeleteTtl = 0, - ShutdownTtl = 0 + ShutdownTtl = 0, + UsageSettings = new List { ValidUsageSettingsJson } }; await FleetUpdateHandler.FleetUpdateAsync( @@ -112,7 +115,9 @@ await FleetUpdateHandler.FleetUpdateAsync( CancellationToken.None ); - FleetUpdateRequest req = new FleetUpdateRequest(name: input.Name, buildConfigurations: input.BuildConfigs); + var usageSetting = JsonConvert.DeserializeObject(ValidUsageSettingsJson); + + FleetUpdateRequest req = new FleetUpdateRequest(name: input.Name, buildConfigurations: input.BuildConfigs, usageSettings: new List { usageSetting! }); FleetsApi!.DefaultFleetsClient.Verify(api => api.UpdateFleetAsync( new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), @@ -227,4 +232,40 @@ await FleetUpdateHandler.FleetUpdateAsync( new Guid(fleetId), expected, 0, CancellationToken.None ), Times.Once); } + + [TestCase(ValidProjectId, ValidEnvironmentName, ValidFleetId)] + public void FleetUpdateAsync_InvalidUsageSettingsJsonThrowsException( + string projectId, + string environmentName, + string fleetId + ) + { + FleetUpdateInput input = new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + FleetId = fleetId, + Name = ValidFleetName, + AllocTtl = 0, + DeleteTtl = 0, + BuildConfigs = new List(), + DisabledDeleteTtl = 0, + ShutdownTtl = 0, + UsageSettings = new List { "{badjson}" } + }; + + MockUnityEnvironment.Setup(ex => ex.FetchIdentifierAsync(CancellationToken.None)).ReturnsAsync(ValidEnvironmentId); + + Assert.ThrowsAsync(() => + FleetUpdateHandler.FleetUpdateAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) + ); + + TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/HandlerCommon.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/HandlerCommon.cs index b9a440d..c8fc972 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/HandlerCommon.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/HandlerCommon.cs @@ -120,6 +120,9 @@ public void SetUp() ); MockUnityEnvironment.Setup(ex => ex.FetchIdentifierAsync(CancellationToken.None)).ReturnsAsync(ValidEnvironmentId); + + // create tmp output directory for tests + Directory.CreateDirectory(ValidOutputDirectory); } [TearDown] @@ -127,5 +130,8 @@ public void TearDown() { // Clear invocations to Mock Environment MockUnityEnvironment.Invocations.Clear(); + + // delete tmp output directory for tests + Directory.Delete(ValidOutputDirectory, true); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FileDownloadInputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FileDownloadInputTests.cs new file mode 100644 index 0000000..2488325 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FileDownloadInputTests.cs @@ -0,0 +1,104 @@ +using System.CommandLine; +using Unity.Services.Cli.GameServerHosting.Input; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Input; + +public class FileDownloadInputTests +{ + [TestCase( + new[] + { + FileDownloadInput.OutputKey, + "dir" + }, + true)] + [TestCase( + new[] + { + FileDownloadInput.OutputKey, + "" + }, + false)] + [TestCase( + new[] + { + FileDownloadInput.OutputKey + }, + false)] + [TestCase( + new string[] + { }, + false)] + [TestCase(null, false)] + public void Validate_WithValidOutputInput_ReturnsTrue(string[] output, bool validates) + { + Assert.That(FileDownloadInput.OutputOption.Parse(output).Errors, validates ? Is.Empty : Is.Not.Empty); + } + + [TestCase( + new[] + { + FileDownloadInput.PathKey, + "error.log" + }, + true)] + [TestCase( + new[] + { + FileDownloadInput.PathKey, + "" + }, + false)] + [TestCase( + new[] + { + FileDownloadInput.PathKey + }, + false)] + [TestCase( + new string[] + { }, + false)] + [TestCase(null, false)] + public void Validate_WithValidPathInput_ReturnsTrue(string[] path, bool validates) + { + Assert.That(FileDownloadInput.PathOption.Parse(path).Errors, validates ? Is.Empty : Is.Not.Empty); + } + + [TestCase( + new[] + { + FileDownloadInput.ServerIdKey, + "666" + }, + true)] + [TestCase( + new[] + { + FileDownloadInput.ServerIdKey, + "nan" + }, + false)] + [TestCase( + new[] + { + FileDownloadInput.ServerIdKey, + "" + }, + false)] + [TestCase( + new[] + { + FileDownloadInput.ServerIdKey + }, + false)] + [TestCase( + new string[] + { }, + false)] + [TestCase(null, false)] + public void Validate_WithValidServerIdInput_ReturnsTrue(string[] serverId, bool validates) + { + Assert.That(FileDownloadInput.ServerIdOption.Parse(serverId).Errors, validates ? Is.Empty : Is.Not.Empty); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FileListInputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FileListInputTests.cs new file mode 100644 index 0000000..b370007 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FileListInputTests.cs @@ -0,0 +1,43 @@ +using System.CommandLine; +using Unity.Services.Cli.GameServerHosting.Input; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Input; + +[TestFixture] +public class FileListInputTests +{ + [TestCase("2018-07-22T18:32:25Z", true, TestName = "Golden path")] + [TestCase("3000-02-01T12:00:00Z", false, TestName = "Invalid ModifiedFrom, provided date is in the future")] + [TestCase("invalid", false, TestName = "Invalid date format provided for ModifiedFrom")] + public void Validate_WithValidModifiedFromInput_ReturnsTrue(string modifiedFrom, bool validates) + { + var arg = new[] + { + FileListInput.ModifiedFromKey, + modifiedFrom + }; + Assert.That(FileListInput.ModifiedFromOption.Parse(arg).Errors, validates ? Is.Empty : Is.Not.Empty); + } + + [TestCase("2018-07-22T18:32:25Z", true, TestName = "Golden path")] + [TestCase("3000-02-01T12:00:00Z", false, TestName = "Invalid ModifiedTo, provided date is in the future")] + [TestCase("invalid", false, TestName = "Invalid date format provided for ModifiedTo")] + public void Validate_WithValidModifiedToInput_ReturnsTrue(string modifiedTo, bool validates) + { + var arg = new[] + { + FileListInput.ModifiedToKey, + modifiedTo + }; + Assert.That(FileListInput.ModifiedToOption.Parse(arg).Errors, validates ? Is.Empty : Is.Not.Empty); + } + + [TestCase(new[] { "1", "2" }, true, TestName = "Golden path")] + [TestCase(null, false, TestName = "Server Ids as null")] + [TestCase(new string[] { }, false, TestName = "Server Ids as empty array")] + public void Validate_WithValidServerIdsInput_ReturnsTrue(string[]? serverIds, bool validates) + { + serverIds = (serverIds ?? Array.Empty()).Prepend(FileListInput.ServerIdKey).ToArray(); + Assert.That(FileListInput.ServerIdOption.Parse(serverIds).Errors, validates ? Is.Empty : Is.Not.Empty); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FleetCreateInputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FleetCreateInputTests.cs index 736cd43..d337a72 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FleetCreateInputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FleetCreateInputTests.cs @@ -36,4 +36,19 @@ public void Validate_WithValidOSFamily_ReturnsTrue(string[] osFamily, bool valid { Assert.That(FleetCreateInput.FleetOsFamilyOption.Parse(osFamily).Errors, validates ? Is.Empty : Is.Not.Empty); } + + [TestCase(new[] + { + FleetCreateInput.UsageSettingsKey, + "badjson" + }, false)] + [TestCase(new[] + { + FleetCreateInput.UsageSettingsKey, + ValidUsageSettingsJson + }, true)] + public void Validate_WithValidUsageSettings_ReturnsTrue(string[] usageSetting, bool validates) + { + Assert.That(FleetCreateInput.FleetUsageSettingsOption.Parse(usageSetting).Errors, validates ? Is.Empty : Is.Not.Empty); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FleetUpdateInputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FleetUpdateInputTests.cs new file mode 100644 index 0000000..70bf544 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/FleetUpdateInputTests.cs @@ -0,0 +1,23 @@ +using System.CommandLine; +using Unity.Services.Cli.GameServerHosting.Input; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Input; + +[TestFixture] +public class FleetUpdateInputTests +{ + [TestCase(new[] + { + FleetUpdateInput.UsageSettingsKey, + "badjson" + }, false)] + [TestCase(new[] + { + FleetUpdateInput.UsageSettingsKey, + ValidUsageSettingsJson + }, true)] + public void Validate_WithValidUsageSettings_ReturnsTrue(string[] usageSetting, bool validates) + { + Assert.That(FleetUpdateInput.UsageSettingsOption.Parse(usageSetting).Errors, validates ? Is.Empty : Is.Not.Empty); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingFilesApiV1Mock.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingFilesApiV1Mock.cs index 95ddc94..9beb5d2 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingFilesApiV1Mock.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingFilesApiV1Mock.cs @@ -49,12 +49,15 @@ public class GameServerHostingFilesApiV1Mock public List? ValidEnvironments; public List? ValidProjects; + public void SetUp() { DefaultFilesClient = new Mock(); DefaultFilesClient.Setup(a => a.Configuration) .Returns(new Configuration()); +#pragma warning disable CS8073 // The result of the expression is always the same since a value of this type is never equal to 'null' + // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract DefaultFilesClient.Setup( a => a.ListFilesAsync( It.IsAny(), // projectId @@ -89,14 +92,14 @@ CancellationToken _ if (filesListRequest.ModifiedFrom != null) { - results = results.Where(a => ValidateDateAfterFromString(a.LastModified, filesListRequest.ModifiedFrom)); - + results = results.Where( + a => ValidateDateAfterFromString(a.LastModified, filesListRequest.ModifiedFrom)); } if (filesListRequest.ModifiedTo != null) { - results = results.Where(a => ValidateDateBeforeFromString(a.LastModified, filesListRequest.ModifiedTo)); - + results = results.Where( + a => ValidateDateBeforeFromString(a.LastModified, filesListRequest.ModifiedTo)); } if (filesListRequest.Limit > 0) @@ -107,26 +110,48 @@ CancellationToken _ return Task.FromResult(results.ToList()); } ); + // ReSharper restore ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract +#pragma warning restore CS8073 // The result of the expression is always the same since a value of this type is never equal to 'null' + + DefaultFilesClient.Setup( + a => a.GenerateDownloadURLAsync( + It.IsAny(), // projectId + It.IsAny(), // environmentId + It.IsAny(), + 0, + CancellationToken.None + )) + .Returns( + ( + Guid _, + Guid _, + GenerateDownloadURLRequest _, + int _, + CancellationToken _ + ) => Task.FromResult( + new GenerateDownloadURLResponse( + url: "https://example.com" + ) + ) + ); } static bool ValidateDateBeforeFromString(DateTime lastModified, DateTime modifiedTo) { // providedDate is before targetDate - if (lastModified < modifiedTo || lastModified == modifiedTo) return true; - return false; + return lastModified < modifiedTo || lastModified == modifiedTo; } static bool ValidateDateAfterFromString(DateTime lastModified, DateTime modifiedFrom) { // providedDate is after targetDate - if (lastModified > modifiedFrom || lastModified == modifiedFrom) return true; - return false; + return lastModified > modifiedFrom || lastModified == modifiedFrom; } bool ValidateProjectEnvironment(Guid projectId, Guid environmentId) { - if (ValidProjects != null && !ValidProjects.Contains(projectId)) return false; - if (ValidEnvironments != null && !ValidEnvironments.Contains(environmentId)) return false; - return true; + var validaProject = ValidProjects != null && ValidProjects.Contains(projectId); + var validEnvironment = ValidEnvironments != null && ValidEnvironments.Contains(environmentId); + return validaProject && validEnvironment; } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemFleetOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemFleetOutputTests.cs new file mode 100644 index 0000000..4e39ca5 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemFleetOutputTests.cs @@ -0,0 +1,31 @@ +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Model; + +class FilesItemFleetOutputTests +{ + [SetUp] + public void SetUp() + { + m_FileFleetDetails = new FleetDetails( + id: new Guid(ValidFleetId), + name: ValidFleetName + ); + } + + FleetDetails? m_FileFleetDetails; + + [Test] + public void ConstructFilesFleetDetailsItemOutputWithValidInput() + { + FilesItemFleetOutput output = new(m_FileFleetDetails!); + Assert.Multiple( + () => + { + Assert.That(output.Id, Is.EqualTo(m_FileFleetDetails!.Id)); + Assert.That(output.Name, Is.EqualTo(m_FileFleetDetails!.Name)); + } + ); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemMachineOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemMachineOutputTests.cs new file mode 100644 index 0000000..22e728c --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemMachineOutputTests.cs @@ -0,0 +1,31 @@ +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Model; + +class FilesItemMachineOutputTests +{ + [SetUp] + public void SetUp() + { + m_FileMachine = new Machine( + id: ValidMachineId, + location: ValidFleetName + ); + } + + Machine? m_FileMachine; + + [Test] + public void ConstructFilesMachineItemOutputWithValidInput() + { + FilesItemMachineOutput output = new(m_FileMachine!); + Assert.Multiple( + () => + { + Assert.That(output.Id, Is.EqualTo(m_FileMachine!.Id)); + Assert.That(output.Location, Is.EqualTo(m_FileMachine!.Location)); + } + ); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemOutputTests.cs new file mode 100644 index 0000000..eecafb4 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesItemOutputTests.cs @@ -0,0 +1,52 @@ +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; +using File = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.File; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Model; + +class FilesItemOutputTests +{ + [SetUp] + public void SetUp() + { + m_File = new File( + filename: "error.log", + path: "logs/", + fileSize: 100, + createdAt: new DateTime(2022, 10, 11), + lastModified: new DateTime(2022, 10, 12), + fleet: new FleetDetails( + id: new Guid(ValidFleetId), + name: "Test Fleet" + ), + machine: new Machine( + id: ValidMachineId, + location: "europe-west1" + ), + serverID: ValidServerId + ); + } + + File? m_File; + + [Test] + public void ConstructFilesItemOutputWithValidInput() + { + FilesItemOutput output = new(m_File!); + Assert.Multiple( + () => + { + Assert.That(output.CreatedAt, Is.EqualTo(m_File!.CreatedAt)); + Assert.That(output.FileSize, Is.EqualTo(m_File!.FileSize)); + Assert.That(output.Filename, Is.EqualTo(m_File!.Filename)); + Assert.That(output.Fleet.Id, Is.EqualTo(m_File!.Fleet.Id)); + Assert.That(output.Fleet.Name, Is.EqualTo(m_File!.Fleet.Name)); + Assert.That(output.LastModified, Is.EqualTo(m_File!.LastModified)); + Assert.That(output.Machine.Id, Is.EqualTo(m_File!.Machine.Id)); + Assert.That(output.Machine.Location, Is.EqualTo(m_File!.Machine.Location)); + Assert.That(output.Path, Is.EqualTo(m_File!.Path)); + Assert.That(output.ServerId, Is.EqualTo(m_File!.ServerID)); + } + ); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesOutputTests.cs new file mode 100644 index 0000000..899b273 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FilesOutputTests.cs @@ -0,0 +1,61 @@ +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; +using File = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.File; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Model; + +[TestFixture] +class FilesOutputTests +{ + [SetUp] + public void SetUp() + { + m_Files = new List + { + new File( + createdAt: new DateTime(2022, 10, 11), + fileSize: 100, + filename: "path/error.log", + fleet: new FleetDetails( + id: new Guid(ValidFleetId), + name: ValidFleetName + ), + lastModified: new DateTime(2022, 10, 12), + machine: new Machine( + id: ValidMachineId, + location: "europe-west1" + ), + path: "/games/tf2/", + serverID: ValidServerId + ) + }; + } + + List? m_Files; + + [Test] + public void ConstructFilesOutputWithValidInput() + { + FilesOutput output = new(m_Files!); + for (var i = 0; i < output.Count; i++) + { + Assert.Multiple( + () => + { + Assert.That(output[i].CreatedAt, Is.EqualTo(m_Files![i].CreatedAt)); + Assert.That(output[i].FileSize, Is.EqualTo(m_Files![i].FileSize)); + Assert.That(output[i].Filename, Is.EqualTo(m_Files![i].Filename)); + Assert.That(output[i].Fleet.Id, Is.EqualTo(m_Files![i].Fleet.Id)); + Assert.That(output[i].Fleet.Name, Is.EqualTo(m_Files![i].Fleet.Name)); + Assert.That(output[i].LastModified, Is.EqualTo(m_Files![i].LastModified)); + Assert.That(output[i].Machine.Id, Is.EqualTo(m_Files![i].Machine.Id)); + Assert.That(output[i].Machine.Location, Is.EqualTo(m_Files![i].Machine.Location)); + Assert.That(output[i].Path, Is.EqualTo(m_Files![i].Path)); + Assert.That(output[i].ServerId, Is.EqualTo(m_Files![i].ServerID)); + } + ); + } + } + + +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetListItemOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetListItemOutputTests.cs index 6b71dc1..2f0db26 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetListItemOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetListItemOutputTests.cs @@ -20,7 +20,8 @@ public void SetUp() servers: new Servers(new FleetServerBreakdown(new ServerStatus()), new FleetServerBreakdown(new ServerStatus()), new FleetServerBreakdown(new ServerStatus())), - status: FleetListItem.StatusEnum.ONLINE + status: FleetListItem.StatusEnum.ONLINE, + usageSettings: new List() ); } @@ -39,6 +40,7 @@ public void ConstructFleetListItemOutputWithValidList() Assert.That(output.OsName, Is.EqualTo(m_Fleet!.OsName)); Assert.That(output.Servers, Is.EqualTo(m_Fleet!.Servers)); Assert.That(output.Status, Is.EqualTo(m_Fleet!.Status)); + Assert.That(output.UsageSettings, Is.EqualTo(m_Fleet!.UsageSettings)); }); } @@ -72,6 +74,7 @@ public void FleetListItemOutputToString() sb.AppendLine(" available: 0"); sb.AppendLine(" online: 0"); sb.AppendLine(" total: 0"); + sb.AppendLine("usageSettings: []"); Assert.That(output.ToString(), Is.EqualTo(sb.ToString())); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetListOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetListOutputTests.cs index f514b4f..ab210df 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetListOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetListOutputTests.cs @@ -22,7 +22,8 @@ public void SetUp() servers: new Servers(new FleetServerBreakdown(new ServerStatus()), new FleetServerBreakdown(new ServerStatus()), new FleetServerBreakdown(new ServerStatus())), - status: FleetListItem.StatusEnum.ONLINE + status: FleetListItem.StatusEnum.ONLINE, + usageSettings: new List() ), new( allocationType: FleetListItem.AllocationTypeEnum.ALLOCATION, @@ -34,7 +35,8 @@ public void SetUp() servers: new Servers(new FleetServerBreakdown(new ServerStatus()), new FleetServerBreakdown(new ServerStatus()), new FleetServerBreakdown(new ServerStatus())), - status: FleetListItem.StatusEnum.ONLINE + status: FleetListItem.StatusEnum.ONLINE, + usageSettings: new List() ) }; } @@ -56,6 +58,7 @@ public void ConstructFleetListOutputWithValidList() Assert.That(output[i].OsName, Is.EqualTo(m_Fleets[i].OsName)); Assert.That(output[i].Servers, Is.EqualTo(m_Fleets[i].Servers)); Assert.That(output[i].Status, Is.EqualTo(m_Fleets[i].Status)); + Assert.That(output[i].UsageSettings, Is.EqualTo(m_Fleets[i].UsageSettings)); }); } @@ -91,6 +94,7 @@ public void FleetListOutputToString() sb.AppendLine($" available: {fleet.Servers.Metal.Status.Available}"); sb.AppendLine($" online: {fleet.Servers.Metal.Status.Online}"); sb.AppendLine($" total: {fleet.Servers.Metal.Total}"); + sb.AppendLine(" usageSettings: []"); } Assert.That(output.ToString(), Is.EqualTo(sb.ToString())); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/CcdCloudStorageTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/CcdCloudStorageTests.cs index 34d40a8..f9d89a7 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/CcdCloudStorageTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/CcdCloudStorageTests.cs @@ -38,7 +38,7 @@ public async Task FindBucket_LoadsBucketByName() var bucketId = Guid.NewGuid(); var bucketName = "bucket"; m_MockBucketsApi!.Setup(api => api.ListBucketsByProjectEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new List { new CcdBucket(id: bucketId, name: bucketName) })); + .Returns(Task.FromResult(new List { new(id: bucketId, name: bucketName) })); var bucket = await m_CcdCloudStorage!.FindBucket("test"); @@ -50,8 +50,8 @@ public async Task CreateBucket_CreatesNewBucket() { var bucketId = Guid.NewGuid(); var bucketName = "bucket"; - m_MockBucketsApi!.Setup(api => api.CreateBucketByProjectEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new CcdBucket(id: bucketId, name: bucketName))); + m_MockBucketsApi!.Setup(api => api.CreateBucketByProjectEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new CcdGetBucket200Response(id: bucketId, name: bucketName))); var bucket = await m_CcdCloudStorage!.CreateBucket("test"); @@ -61,59 +61,59 @@ public async Task CreateBucket_CreatesNewBucket() [Test] public async Task UploadBuildEntries_UploadsNewEntries() { - SetupUpload(new List()); + SetupUpload(new List()); await m_CcdCloudStorage!.UploadBuildEntries( new CloudBucketId { Id = Guid.NewGuid() }, new List { - new BuildEntry("path", new MemoryStream(Encoding.UTF8.GetBytes("content"))) + new ("path", new MemoryStream(Encoding.UTF8.GetBytes("content"))) }); - m_MockEntriesApi!.Verify(api => api.CreateOrUpdateEntryByPathEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + m_MockEntriesApi!.Verify(api => api.CreateOrUpdateEntryByPathEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); } [Test] public async Task UploadBuildEntries_WhenExactFileExists_DoesNotUpload() { - SetupUpload(new List + SetupUpload(new List { // Uppercase hash to ensure that case differences are handled - new CcdEntry(path: "path", contentHash: "9a0364b9e99bb480dd25e1f0284c8555".ToUpperInvariant()) + new (path: "path", contentHash: "9a0364b9e99bb480dd25e1f0284c8555".ToUpperInvariant()) }); await m_CcdCloudStorage!.UploadBuildEntries( new CloudBucketId { Id = Guid.NewGuid() }, new List { - new BuildEntry("path", new MemoryStream(Encoding.UTF8.GetBytes("content"))) + new ("path", new MemoryStream(Encoding.UTF8.GetBytes("content"))) }); - m_MockEntriesApi!.Verify(api => api.CreateOrUpdateEntryByPathEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + m_MockEntriesApi!.Verify(api => api.CreateOrUpdateEntryByPathEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Test] public async Task UploadBuildEntries_WhenOrphanExists_DeletesOrphan() { - SetupUpload(new List + SetupUpload(new List { - new CcdEntry(path: "path", contentHash: "hash") + new (path: "path", contentHash: "hash") }); await m_CcdCloudStorage!.UploadBuildEntries( new CloudBucketId { Id = Guid.NewGuid() }, new List()); - m_MockEntriesApi!.Verify(api => api.DeleteEntryEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); + m_MockEntriesApi!.Verify(api => api.DeleteEntryEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); } - void SetupUpload(List ccdEntries) + void SetupUpload(List ccdEntries) { - m_MockEntriesApi!.Setup(api => api.GetEntriesEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + m_MockEntriesApi!.Setup(api => api.GetEntriesEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(ccdEntries)); - m_MockEntriesApi.Setup(api => api.CreateOrUpdateEntryByPathEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(new CcdEntry(signedUrl: "https://signed.url.example.com"))); - m_MockEntriesApi.Setup(api => api.DeleteEntryEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + m_MockEntriesApi.Setup(api => api.CreateOrUpdateEntryByPathEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new CcdGetEntries200ResponseInner(signedUrl: "https://signed.url.example.com"))); + m_MockEntriesApi.Setup(api => api.DeleteEntryEnvAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new List())); m_MockMessageHandler.Protected() .Setup>( diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/GameServerHostingModule.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/GameServerHostingModule.cs index 61e0efd..dcce1e8 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/GameServerHostingModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/GameServerHostingModule.cs @@ -49,7 +49,9 @@ public GameServerHostingModule() CancellationToken >(BuildCreateHandler.BuildCreateAsync); - BuildCreateVersionCommand = new Command("create-version", "Create a new version of a Game Server Hosting build.") + BuildCreateVersionCommand = new Command( + "create-version", + "Create a new version of a Game Server Hosting build.") { BuildCreateVersionInput.BuildIdArgument, CommonInput.EnvironmentNameOption, @@ -248,7 +250,9 @@ public GameServerHostingModule() CancellationToken >(BuildConfigurationUpdateHandler.BuildConfigurationUpdateAsync); - BuildConfigurationCommand = new Command("build-configuration", "Manage Game Server Hosting build configurations.") + BuildConfigurationCommand = new Command( + "build-configuration", + "Manage Game Server Hosting build configurations.") { BuildConfigurationGetCommand, BuildConfigurationCreateCommand, @@ -263,6 +267,7 @@ public GameServerHostingModule() FleetCreateInput.FleetOsFamilyOption, FleetCreateInput.FleetRegionsOption, FleetCreateInput.FleetBuildConfigurationsOption, + FleetCreateInput.FleetUsageSettingsOption, CommonInput.EnvironmentNameOption, CommonInput.CloudProjectIdOption }; @@ -329,6 +334,7 @@ public GameServerHostingModule() FleetUpdateInput.DisabledDeleteTtlOption, FleetUpdateInput.ShutdownTtlOption, FleetUpdateInput.BuildConfigsOption, + FleetUpdateInput.UsageSettingsOption, CommonInput.EnvironmentNameOption, CommonInput.CloudProjectIdOption }; @@ -351,7 +357,9 @@ public GameServerHostingModule() }; - FleetRegionTemplatesCommand = new Command("templates", "List Game Server Hosting templates for creating fleet regions.") + FleetRegionTemplatesCommand = new Command( + "templates", + "List Game Server Hosting templates for creating fleet regions.") { CommonInput.EnvironmentNameOption, CommonInput.CloudProjectIdOption @@ -365,7 +373,9 @@ public GameServerHostingModule() CancellationToken >(RegionTemplatesHandler.RegionTemplatesAsync); - FleetRegionAvailableCommand = new Command("available", "List Game Server Hosting available template regions for creating fleet regions.") + FleetRegionAvailableCommand = new Command( + "available", + "List Game Server Hosting available template regions for creating fleet regions.") { CommonInput.EnvironmentNameOption, CommonInput.CloudProjectIdOption, @@ -486,11 +496,56 @@ public GameServerHostingModule() CancellationToken >(ServerListHandler.ServerListAsync); + ServerFilesDownloadCommand = new Command( + "download", + "Download files for the provided Game Server Hosting server.") + { + CommonInput.EnvironmentNameOption, + CommonInput.CloudProjectIdOption, + FileDownloadInput.PathOption, + FileDownloadInput.ServerIdOption, + FileDownloadInput.OutputOption, + }; + ServerFilesDownloadCommand.SetHandler< + FileDownloadInput, + IUnityEnvironment, + IGameServerHostingService, + ILogger, + HttpClient, + ILoadingIndicator, + CancellationToken + >(FileDownloadHandler.FileDownloadAsync); + + ServerFilesListCommand = new Command("list", "List of files for the provided Game Server Hosting servers.") + { + CommonInput.EnvironmentNameOption, + CommonInput.CloudProjectIdOption, + FileListInput.LimitOption, + FileListInput.ModifiedFromOption, + FileListInput.ModifiedToOption, + FileListInput.PathFilterOption, + FileListInput.ServerIdOption, + }; + ServerFilesListCommand.SetHandler< + FileListInput, + IUnityEnvironment, + IGameServerHostingService, + ILogger, + ILoadingIndicator, + CancellationToken + >(FileListHandler.FileListAsync); + + ServerFilesCommand = new Command("files", "Manage Game Server Hosting server files.") + { + ServerFilesDownloadCommand, + ServerFilesListCommand, + }; ServerCommand = new Command("server", "Manage Game Server Hosting servers.") { ServerGetCommand, ServerListCommand, + ServerFilesCommand, }; ModuleRootCommand = new Command("game-server-hosting", "Manage Game Sever Hosting resources.") @@ -555,6 +610,9 @@ public GameServerHostingModule() internal Command ServerGetCommand { get; } internal Command ServerListCommand { get; } + internal Command ServerFilesCommand { get; } + internal Command ServerFilesListCommand { get; } + internal Command ServerFilesDownloadCommand { get; } internal static ExceptionFactory ExceptionFactory => (method, response) => diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationUpdateHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationUpdateHandler.cs index 56a989a..397226e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationUpdateHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationUpdateHandler.cs @@ -66,6 +66,7 @@ CancellationToken cancellationToken // API requires all these fields to be populated, to make it a nicer user experience we populate // Null input values with the existing values var req = new BuildConfigurationUpdateRequest( +#pragma warning disable CS0612 // Type or member is obsolete binaryPath: input.BinaryPath ?? currentConfig.BinaryPath, buildID: (input.BuildId is not null && input.BuildId != 0) ? input.BuildId.Value : currentConfig.BuildID, commandLine: input.CommandLine ?? currentConfig.CommandLine, @@ -75,6 +76,7 @@ CancellationToken cancellationToken name: input.Name ?? currentConfig.Name, queryType: input.QueryType ?? currentConfig.QueryType, speed: input.Speed ?? currentConfig.Speed +#pragma warning restore CS0612 // Type or member is obsolete ); var buildConfiguration = await service.BuildConfigurationsApi.UpdateBuildConfigurationAsync( diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileDownloadHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileDownloadHandler.cs new file mode 100644 index 0000000..e8db384 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileDownloadHandler.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.Logging; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Cli.GameServerHosting.Exceptions; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.GameServerHosting.Service; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.Handlers; + +static class FileDownloadHandler +{ + public static async Task FileDownloadAsync( + FileDownloadInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + HttpClient httpClient, + ILoadingIndicator loadingIndicator, + CancellationToken cancellationToken + ) + { + await loadingIndicator.StartLoadingAsync( + "Downloading file...", + _ => FileDownloadAsync( + input, + unityEnvironment, + service, + logger, + httpClient, + cancellationToken)); + } + + internal static async Task FileDownloadAsync( + FileDownloadInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + HttpClient httpClient, + CancellationToken cancellationToken + ) + { + var environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); + var output = input.Output ?? throw new MissingInputException(FileDownloadInput.OutputKey); + + await service.AuthorizeGameServerHostingService(cancellationToken); + + var request = new GenerateDownloadURLRequest( + path: input.Path!, + serverId: long.Parse(input.ServerId!) + ); + + var response = await service.FilesApi.GenerateDownloadURLAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + request, + cancellationToken: cancellationToken + ); + + // check if output is a directory or a file + if (Directory.Exists(output)) + { + // get filename from path + var filename = Path.GetFileName(input.Path!); + + // create local file + output = Path.Combine(output, filename); + } + + // create local file + var localFile = new FileStream(output, FileMode.Create); + + try + { + // stream file from signed url + var stream = await httpClient.GetStreamAsync(response.Url, cancellationToken); + + // write file to local file + await stream.CopyToAsync(localFile, cancellationToken); + } + catch (Exception exception) + { + logger.LogError("Failed to download file: {Message}", exception.Message); + return; + } + finally + { + // close file + localFile.Close(); + } + + + logger.LogInformation("File downloaded to {Output}", input.Output); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileListHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileListHandler.cs new file mode 100644 index 0000000..d4cc588 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileListHandler.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.Logging; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Cli.GameServerHosting.Exceptions; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Cli.GameServerHosting.Service; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.Handlers; + +static class FileListHandler +{ + public static async Task FileListAsync( + FileListInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + ILoadingIndicator loadingIndicator, + CancellationToken cancellationToken + ) + { + await loadingIndicator.StartLoadingAsync( + "Fetching files list...", + _ => FileListAsync( + input, + unityEnvironment, + service, + logger, + cancellationToken)); + } + + internal static async Task FileListAsync( + FileListInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + CancellationToken cancellationToken + ) + { + // FetchIdentifierAsync handles null checks for project-id and environment + var environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); + var serverIdValues = input.ServerIds ?? throw new MissingInputException(FileListInput.ServerIdKey); + + await service.AuthorizeGameServerHostingService(cancellationToken); + + var request = new FilesListRequest( + limit: long.Parse(input.Limit!), + pathFilter: input.PathFilter!, + serverIds: serverIdValues.ToList() + ); + + if (input.ModifiedFrom != null) + { + request.ModifiedFrom = DateTime.Parse(input.ModifiedFrom).ToUniversalTime(); + } + + if (input.ModifiedTo != null) + { + request.ModifiedTo = DateTime.Parse(input.ModifiedTo).ToUniversalTime(); + } + + var files = await service.FilesApi.ListFilesAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + request, + cancellationToken: cancellationToken + ); + + logger.LogResultValue(new FilesOutput(files)); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetCreateHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetCreateHandler.cs index 12ac624..08521e9 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetCreateHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetCreateHandler.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.Common.Utils; @@ -46,15 +47,23 @@ internal static async Task FleetCreateAsync(FleetCreateInput input, IUnityEnviro var regionIdList = regions.Select(Guid.Parse).ToList(); var regionCreateRequestList = regionIdList.Select(regionId => new Region(regionID: regionId)).ToList(); + var req = new FleetCreateRequest( + name: fleetName, + osFamily: osFamily, + regions: regionCreateRequestList, + buildConfigurations: buildConfigurations.ToList() + ); + + if (input.UsageSettings != null) + { + // Iterate through usage settings array, parse json and save as list. Only if usage settings specified. + req.UsageSettings = input.UsageSettings.Select(setting => JsonConvert.DeserializeObject(setting)!).ToList(); + } + var fleet = await service.FleetsApi.CreateFleetAsync( Guid.Parse(input.CloudProjectId!), Guid.Parse(environmentId), - new FleetCreateRequest( - name: fleetName, - osFamily: osFamily, - regions: regionCreateRequestList, - buildConfigurations: buildConfigurations.ToList() - ), + req, cancellationToken: cancellationToken ); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetUpdateHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetUpdateHandler.cs index 10e5b47..424bc6d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetUpdateHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetUpdateHandler.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.GameServerHosting.Exceptions; @@ -30,6 +31,7 @@ internal static async Task FleetUpdateAsync(FleetUpdateInput input, IUnityEnviro var disabledDeleteTtl = input.DisabledDeleteTtl ?? throw new InvalidCastException(); var shutdownTtl = input.ShutdownTtl ?? throw new InvalidCastException(); var buildConfigs = input.BuildConfigs; + var usageSettings = input.UsageSettings; var name = input.Name; await service.AuthorizeGameServerHostingService(cancellationToken); @@ -59,6 +61,12 @@ internal static async Task FleetUpdateAsync(FleetUpdateInput input, IUnityEnviro #pragma warning restore 612 }; + // If provided, include usage settings + if (usageSettings != null) + { + fleetUpdateReq.UsageSettings = usageSettings.Select(setting => JsonConvert.DeserializeObject(setting)!).ToList(); + } + await service.FleetsApi.UpdateFleetAsync(Guid.Parse(input.CloudProjectId!), Guid.Parse(environmentId), Guid.Parse(fleetId), fleetUpdateReq, cancellationToken: cancellationToken); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FileDownloadInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FileDownloadInput.cs new file mode 100644 index 0000000..8843287 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FileDownloadInput.cs @@ -0,0 +1,88 @@ +using System.CommandLine; +using System.CommandLine.Parsing; +using Unity.Services.Cli.Common.Input; + +namespace Unity.Services.Cli.GameServerHosting.Input; + +public class FileDownloadInput : CommonInput +{ + public const string OutputKey = "--output"; + public const string PathKey = "--path"; + public const string ServerIdKey = "--server-id"; + + public static readonly Option OutputOption = new( + OutputKey, + "The path to save the downloaded files to." + ) + { + IsRequired = true, + }; + + public static readonly Option PathOption = new( + PathKey, + "The path to the file to download." + ) + { + IsRequired = true, + }; + + public static readonly Option ServerIdOption = new( + ServerIdKey, + "The unique ID of the server" + ) + { + IsRequired = true, + }; + + static FileDownloadInput() + { + OutputOption.AddValidator(ValidateOutput); + PathOption.AddValidator(ValidatePath); + ServerIdOption.AddValidator(ValidateServerId); + } + + [InputBinding(nameof(OutputOption))] + public string? Output { get; init; } + + [InputBinding(nameof(PathOption))] + public string? Path { get; init; } + + [InputBinding(nameof(ServerIdOption))] + public string? ServerId { get; init; } + + static void ValidateOutput(OptionResult result) + { + var value = result.GetValueOrDefault(); + if (string.IsNullOrEmpty(value)) + { + result.ErrorMessage = "Output path cannot be empty."; + } + } + + static void ValidatePath(OptionResult result) + { + var value = result.GetValueOrDefault(); + if (string.IsNullOrEmpty(value)) + { + result.ErrorMessage = "Path cannot be empty."; + } + } + + static void ValidateServerId(OptionResult result) + { + var value = result.GetValueOrDefault(); + if (string.IsNullOrEmpty(value)) + { + result.ErrorMessage = "Server ID cannot be empty."; + } + try + { + _ = long.Parse(value!); + } + catch (Exception) + { + result.ErrorMessage = $"Server ID '{value}' not a valid ID."; + } + + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FileListInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FileListInput.cs new file mode 100644 index 0000000..c5658d1 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FileListInput.cs @@ -0,0 +1,124 @@ +using System.CommandLine; +using System.CommandLine.Parsing; +using Unity.Services.Cli.Common.Input; + +namespace Unity.Services.Cli.GameServerHosting.Input; + +public class FileListInput : CommonInput +{ + public const string LimitKey = "--limit"; + public const string ModifiedFromKey = "--modified-from"; + public const string ModifiedToKey = "--modified-to"; + public const string PathFilterKey = "--path-filter"; + public const string ServerIdKey = "--server-id"; + + public static readonly Option LimitOption = new( + LimitKey, + "Limit the number of items returned in the results. Default and max is 600." + ); + + public static readonly Option ModifiedFromOption = new( + ModifiedFromKey, + "The start date to filter files list by" + ); + + public static readonly Option ModifiedToOption = new( + ModifiedToKey, + "The end date to filter files list by." + ); + + public static readonly Option PathFilterOption = new( + PathFilterKey, + "The path to filter files list by." + ); + + public static readonly Option ServerIdOption = new( + ServerIdKey, + "The server Ids to retrieve files from." + ) + { + AllowMultipleArgumentsPerToken = true, + IsRequired = true, + }; + + static FileListInput() + { + // set default value to 600 + LimitOption.SetDefaultValue("600"); + PathFilterOption.SetDefaultValue(""); + + ModifiedFromOption.AddValidator(ValidateModifiedFrom); + ModifiedToOption.AddValidator(ValidateModifiedTo); + ServerIdOption.AddValidator(ValidateServerId); + } + + [InputBinding(nameof(LimitOption))] + public string? Limit { get; init; } + + [InputBinding(nameof(ModifiedFromOption))] + public string? ModifiedFrom { get; init; } + + [InputBinding(nameof(ModifiedToOption))] + public string? ModifiedTo { get; init; } + + [InputBinding(nameof(PathFilterOption))] + public string? PathFilter { get; init; } + + [InputBinding(nameof(ServerIdOption))] + public long[]? ServerIds { get; init; } + + static void ValidateModifiedFrom(OptionResult result) + { + var dateString = result.GetValueOrDefault(); + if (dateString == null) + { + return; + } + + var dateNow = DateTime.Now; + if (DateTime.TryParse(dateString, out var modifiedFrom)) + { + if (dateNow < modifiedFrom) + { + // provided modifiedFrom is in the future + result.ErrorMessage = $"Invalid option for --modified-from. {dateString} is in the future"; + } + } + else + { + result.ErrorMessage = $"Invalid option for --modified-from. {dateString} is not valid date"; + } + } + + static void ValidateModifiedTo(OptionResult result) + { + var dateString = result.GetValueOrDefault(); + if (dateString == null) + { + return; + } + + var dateNow = DateTime.Now; + if (DateTime.TryParse(dateString, out var modifiedTo)) + { + if (dateNow < modifiedTo) + { + // provided modifiedFrom is in the future + result.ErrorMessage = $"Invalid option for --modified-to. {dateString} is in the future"; + } + } + else + { + result.ErrorMessage = $"Invalid option for --modified-to. {dateString} is not valid date"; + } + } + + static void ValidateServerId(OptionResult result) + { + var servers = result.GetValueOrDefault(); + if (servers?.Length == 0) + { + result.ErrorMessage = $"Invalid option for --server-id. No server IDs provided"; + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FleetCreateInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FleetCreateInput.cs index 47a2f91..af33c5b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FleetCreateInput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FleetCreateInput.cs @@ -1,5 +1,6 @@ using System.CommandLine; using System.CommandLine.Parsing; +using Newtonsoft.Json; using Unity.Services.Cli.Common.Input; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; @@ -11,6 +12,7 @@ public class FleetCreateInput : CommonInput public const string OsFamilyKey = "--os-family"; public const string RegionsKey = "--region-id"; public const string BuildConfigurationsKey = "--build-configuration-id"; + public const string UsageSettingsKey = "--usage-setting"; public static readonly Option FleetNameOption = new( NameKey, @@ -46,10 +48,19 @@ public class FleetCreateInput : CommonInput IsRequired = true }; + public static readonly Option FleetUsageSettingsOption = new( + UsageSettingsKey, + "Usage settings JSON object to be added to the fleet. Can be supplied more than once." + ) + { + AllowMultipleArgumentsPerToken = true + }; + static FleetCreateInput() { FleetRegionsOption.AddValidator(ValidateRegionIds); FleetOsFamilyOption.AddValidator(ValidateOsFamilyEnum); + FleetUsageSettingsOption.AddValidator(ValidateUsageSetting); } [InputBinding(nameof(FleetNameOption))] @@ -64,6 +75,9 @@ static FleetCreateInput() [InputBinding(nameof(FleetBuildConfigurationsOption))] public long[]? BuildConfigurations { get; set; } + [InputBinding(nameof(FleetUsageSettingsOption))] + public string[]? UsageSettings { get; set; } + static void ValidateRegionIds(OptionResult result) { var value = result.GetValueOrDefault(); @@ -91,4 +105,20 @@ static void ValidateOsFamilyEnum(OptionResult result) result.ErrorMessage = $"Invalid option for --os-family. Did you mean one of the following? {string.Join(", ", Enum.GetNames())}"; } } + + static void ValidateUsageSetting(OptionResult result) + { + var values = result.GetValueOrDefault(); + foreach (var setting in values!) + { + try + { + JsonConvert.DeserializeObject(setting); + } + catch (Exception) + { + result.ErrorMessage = $"Invalid option for --usage-setting. '{setting}' is not a valid JSON usage setting object."; + } + } + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FleetUpdateInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FleetUpdateInput.cs index efd2451..43652c8 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FleetUpdateInput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/FleetUpdateInput.cs @@ -1,5 +1,8 @@ using System.CommandLine; +using System.CommandLine.Parsing; +using Newtonsoft.Json; using Unity.Services.Cli.Common.Input; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; namespace Unity.Services.Cli.GameServerHosting.Input; @@ -11,6 +14,7 @@ class FleetUpdateInput : FleetIdInput public const string DisabledDeleteTtlKey = "--disabled-delete-ttl"; public const string ShutdownTtlKey = "--shutdown-ttl"; public const string BuildConfigsKey = "--build-configurations"; + public const string UsageSettingsKey = "--usage-setting"; public static readonly Option FleetNameOption = new(NameKey, "The name of the fleet"); @@ -33,6 +37,18 @@ class FleetUpdateInput : FleetIdInput AllowMultipleArgumentsPerToken = true }; + public static readonly Option> UsageSettingsOption = new( + UsageSettingsKey, + "A list of usage settings in JSON format to associate with the fleet") + { + AllowMultipleArgumentsPerToken = true + }; + + static FleetUpdateInput() + { + UsageSettingsOption.AddValidator(ValidateUsageSetting); + } + [InputBinding(nameof(FleetNameOption))] public string? Name { get; set; } @@ -50,4 +66,23 @@ class FleetUpdateInput : FleetIdInput [InputBinding(nameof(BuildConfigsOption))] public List? BuildConfigs { get; set; } + + [InputBinding(nameof(UsageSettingsOption))] + public List? UsageSettings { get; set; } + + static void ValidateUsageSetting(OptionResult result) + { + var values = result.GetValueOrDefault>(); + foreach (var setting in values!) + { + try + { + JsonConvert.DeserializeObject(setting); + } + catch (Exception) + { + result.ErrorMessage = $"Invalid option for --usage-setting. '{setting}' is not a valid JSON usage setting object."; + } + } + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildConfigurationOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildConfigurationOutput.cs index b149fbe..c2a4285 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildConfigurationOutput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildConfigurationOutput.cs @@ -18,9 +18,11 @@ public BuildConfigurationOutput(BuildConfiguration buildConfiguration) CommandLine = buildConfiguration.CommandLine; QueryType = buildConfiguration.QueryType; Configuration = buildConfiguration._Configuration; +#pragma warning disable CS0612 // Type or member is obsolete Cores = buildConfiguration.Cores; Speed = buildConfiguration.Speed; Memory = buildConfiguration.Memory; +#pragma warning restore CS0612 // Type or member is obsolete Version = buildConfiguration._Version; CreatedAt = buildConfiguration.CreatedAt; UpdatedAt = buildConfiguration.UpdatedAt; diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesItemMachineOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesItemMachineOutput.cs new file mode 100644 index 0000000..622c157 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesItemMachineOutput.cs @@ -0,0 +1,27 @@ +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using Machine = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.Machine; + +namespace Unity.Services.Cli.GameServerHosting.Model; + +public class FilesItemMachineOutput +{ + public FilesItemMachineOutput(Machine machine) + { + Id = machine.Id; + Location = machine.Location; + } + + public long Id { get; } + public string Location { get; } + + + public override string ToString() + { + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .DisableAliases() + .Build(); + return serializer.Serialize(this); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesItemOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesItemOutput.cs new file mode 100644 index 0000000..ca29d6e --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesItemOutput.cs @@ -0,0 +1,38 @@ +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using File = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.File; + +namespace Unity.Services.Cli.GameServerHosting.Model; + +public class FilesItemOutput +{ + public FilesItemOutput(File file) + { + CreatedAt = file.CreatedAt; + FileSize = file.FileSize; + Filename = file.Filename; + Fleet = new FilesItemFleetOutput(file.Fleet); + LastModified = file.LastModified; + Machine = new FilesItemMachineOutput(file.Machine); + Path = file.Path; + ServerId = file.ServerID; + } + + public string Filename { get; } + public string Path { get; } + public DateTime CreatedAt { get; } + public DateTime LastModified { get; } + public long FileSize { get; } + public FilesItemFleetOutput Fleet { get; } + public FilesItemMachineOutput Machine { get; } + public long ServerId { get; } + + public override string ToString() + { + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .DisableAliases() + .Build(); + return serializer.Serialize(this); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesOutput.cs new file mode 100644 index 0000000..87083d5 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FilesOutput.cs @@ -0,0 +1,22 @@ +using File = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.File; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Unity.Services.Cli.GameServerHosting.Model; + +public class FilesOutput : List +{ + public FilesOutput(IReadOnlyCollection? files) + { + if (files != null) AddRange(files.Select(f => new FilesItemOutput(f))); + } + + public override string ToString() + { + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .DisableAliases() + .Build(); + return serializer.Serialize(this); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FleetGetOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FleetGetOutput.cs index b7bf8ae..a697cc2 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FleetGetOutput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FleetGetOutput.cs @@ -20,6 +20,7 @@ public FleetGetOutput(Fleet fleet) DeleteTtl = fleet.DeleteTTL; DisabledDeleteTtl = fleet.DisabledDeleteTTL; ShutdownTtl = fleet.ShutdownTTL; + UsageSettings = fleet.UsageSettings; } public string Name { get; } @@ -51,6 +52,9 @@ public FleetGetOutput(Fleet fleet) public long ShutdownTtl { get; } + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public List UsageSettings { get; } + public override string ToString() { var serializer = new SerializerBuilder() diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FleetListItemOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FleetListItemOutput.cs index 01fbd58..b2dc88d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FleetListItemOutput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/FleetListItemOutput.cs @@ -15,6 +15,7 @@ public FleetListItemOutput(FleetListItem fleet) BuildConfigurations = fleet.BuildConfigurations; Regions = fleet.Regions; Servers = fleet.Servers; + UsageSettings = fleet.UsageSettings; } public string Name { get; } @@ -31,6 +32,8 @@ public FleetListItemOutput(FleetListItem fleet) public Servers Servers { get; } + public List UsageSettings { get; } + public override string ToString() { var serializer = new SerializerBuilder() diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/CcdCloudStorageClient.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/CcdCloudStorageClient.cs index 7c6423e..0979aaa 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/CcdCloudStorageClient.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/CcdCloudStorageClient.cs @@ -35,7 +35,7 @@ public async Task FindBucket(string name, CancellationToken cance public async Task CreateBucket(string name, CancellationToken cancellationToken = default) { - var res = await m_BucketsApiClient.CreateBucketByProjectEnvAsync(m_ApiConfig.ProjectId.ToString(), m_ApiConfig.EnvironmentId.ToString(), new CcdBucketCreate(name: name, projectguid: m_ApiConfig.ProjectId), cancellationToken: cancellationToken); + var res = await m_BucketsApiClient.CreateBucketByProjectEnvAsync(m_ApiConfig.ProjectId.ToString(), m_ApiConfig.EnvironmentId.ToString(), new CcdCreateBucketByProjectRequest(name: name, projectguid: m_ApiConfig.ProjectId), cancellationToken: cancellationToken); return new CloudBucketId { Id = res.Id }; } @@ -74,19 +74,19 @@ await orphans.BatchAsync(k_BatchSize, async orphan => return changes; } - async Task DeleteEntry(CloudBucketId bucket, CcdEntry entry, CancellationToken cancellationToken = default) + async Task DeleteEntry(CloudBucketId bucket, CcdGetEntries200ResponseInner entry, CancellationToken cancellationToken = default) { - await m_EntriesApiClient.DeleteEntryEnvAsync(m_ApiConfig.EnvironmentId.ToString(), bucket.ToString(), entry.Entryid.ToString(), cancellationToken: cancellationToken); + await m_EntriesApiClient.DeleteEntryEnvAsync(m_ApiConfig.EnvironmentId.ToString(), bucket.ToString(), entry.Entryid.ToString(), m_ApiConfig.ProjectId.ToString(), cancellationToken: cancellationToken); } - async Task CreateOrUpdateEntry(CloudBucketId bucket, string path, string hash, int length, CancellationToken cancellationToken = default) + async Task CreateOrUpdateEntry(CloudBucketId bucket, string path, string hash, int length, CancellationToken cancellationToken = default) { - var create = new CcdEntryCreateByPath(hash, length, signedUrl: true); - var res = await m_EntriesApiClient.CreateOrUpdateEntryByPathEnvAsync(m_ApiConfig.EnvironmentId.ToString(), bucket.ToString(), path, create, updateIfExists: true, cancellationToken: cancellationToken); + var create = new CcdCreateOrUpdateEntryByPathRequest(hash, length, signedUrl: true); + var res = await m_EntriesApiClient.CreateOrUpdateEntryByPathEnvAsync(m_ApiConfig.EnvironmentId.ToString(), bucket.ToString(), path, m_ApiConfig.ProjectId.ToString(), create, updateIfExists: true, cancellationToken: cancellationToken); return res; } - async Task UploadSignedContent(CcdEntry entry, Stream content, CancellationToken cancellationToken = default) + async Task UploadSignedContent(CcdGetEntries200ResponseInner entry, Stream content, CancellationToken cancellationToken = default) { // Signed uploads need to be done using HTTP Client // Unity generated client does not support sending application/offset+octet-stream @@ -97,16 +97,16 @@ async Task UploadSignedContent(CcdEntry entry, Stream content, CancellationToken res.EnsureSuccessStatusCode(); } - async Task> ListAllRemoteEntries(CloudBucketId bucket, CancellationToken cancellationToken = default) + async Task> ListAllRemoteEntries(CloudBucketId bucket, CancellationToken cancellationToken = default) { const int entriesPerPage = 100; - var entries = new Dictionary(); + var entries = new Dictionary(); - List res; + List res; var page = 1; do { - res = await m_EntriesApiClient.GetEntriesEnvAsync(m_ApiConfig.EnvironmentId.ToString(), bucket.ToString(), page: page, perPage: entriesPerPage, cancellationToken: cancellationToken); + res = await m_EntriesApiClient.GetEntriesEnvAsync(m_ApiConfig.EnvironmentId.ToString(), bucket.ToString(), m_ApiConfig.ProjectId.ToString(), page: page, perPage: entriesPerPage, cancellationToken: cancellationToken); foreach (var entry in res) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Unity.Services.Cli.GameServerHosting.csproj b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Unity.Services.Cli.GameServerHosting.csproj index edc7b53..03a36ee 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Unity.Services.Cli.GameServerHosting.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Unity.Services.Cli.GameServerHosting.csproj @@ -7,7 +7,7 @@ true - + FEATURE_GAME_SERVER_HOSTING_SERVER_FILE_LIST @@ -23,8 +23,8 @@ - - + + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/Keys.cs b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/Keys.cs index 23120bd..86dee2f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/Keys.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/Keys.cs @@ -1,4 +1,5 @@ using Unity.Services.Cli.MockServer.Common; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; namespace Unity.Services.Cli.MockServer.ServiceMocks.GameServerHosting; @@ -20,6 +21,7 @@ public static class Keys public const long ValidBuildIdFileUpload = 103; public const long ValidMachineId = 654321L; public const string ValidServerId = "123"; + public const string ValidUsageSettingsJson = "{\"hardwareType\":\"CLOUD\", \"machineType\":\"GCP-N2\", \"maxServersPerMachine\":\"5\"}"; public const string ProjectPathPart = $"projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}"; diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingFleetTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingFleetTests.cs index a9154d5..345b00e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingFleetTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingFleetTests.cs @@ -1,4 +1,6 @@ +using System; using System.Threading.Tasks; +using Newtonsoft.Json; using NUnit.Framework; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.IntegrationTest.Common; @@ -9,8 +11,9 @@ namespace Unity.Services.Cli.IntegrationTest.GameServerHostingTests; public partial class GameServerHostingTests { + static readonly string k_ValidUsageSettingJson = JsonConvert.SerializeObject(Keys.ValidUsageSettingsJson); static readonly string k_FleetCreateCommand = - $"gsh fleet create --name test --os-family linux --region-id {Keys.ValidTemplateRegionId} --build-configuration-id {Keys.ValidBuildConfigurationId}"; + $"gsh fleet create --name test --os-family linux --region-id {Keys.ValidTemplateRegionId} --build-configuration-id {Keys.ValidBuildConfigurationId} --usage-setting {k_ValidUsageSettingJson}"; [Test] [Category("gsh")] @@ -159,6 +162,20 @@ await GetFullySetCli() .ExecuteAsync(); } + [Test] + [Category("gsh")] + [Category("gsh fleet")] + [Category("gsh fleet create")] + public async Task FleetCreate_ThrowsInvalidUsageSettingsJsonException() + { + await GetFullySetCli() + .Command( + $"gsh fleet create --name test --os-family linux --region-id {Keys.ValidTemplateRegionId} --build-configuration-id {Keys.ValidBuildConfigurationId} --usage-setting invalid_json") + .AssertExitCode(ExitCode.HandledError) + .AssertStandardErrorContains("Invalid option for --usage-setting") + .ExecuteAsync(); + } + [Test] [Category("gsh")] [Category("gsh fleet")] @@ -398,6 +415,7 @@ await GetLoggedInCli() [Category("gsh")] [Category("gsh fleet")] [Category("gsh fleet list")] + [Ignore("Breaks on windows - Task to fix: https://jira.unity3d.com/browse/GID-2370")] public async Task FleetList_SucceedsWithValidInput() { await GetFullySetCli() diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFileDownloadTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFileDownloadTests.cs new file mode 100644 index 0000000..ddf0ae1 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFileDownloadTests.cs @@ -0,0 +1,74 @@ +using System.Threading.Tasks; +using NUnit.Framework; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.IntegrationTest.Common; +using Unity.Services.Cli.MockServer.Common; + +namespace Unity.Services.Cli.IntegrationTest.GameServerHostingTests; + +public partial class GameServerHostingTests +{ + static readonly string k_ServerFilesDownloadCommand = "gsh server files download --server-id 1212 --path /logs/error.log --output server.log"; + + [Test] + [Category("gsh")] + [Category("gsh server")] + [Category("gsh server files download")] + public async Task ServerFilesDownload_Succeeds() + { + await GetFullySetCli() + .Command(k_ServerFilesDownloadCommand) + .AssertStandardOutput( + str => + { + Assert.IsTrue(str.Contains("Downloading file...")); + }) + .ExecuteAsync(); + } + + [Test] + [Category("gsh")] + [Category("gsh server")] + [Category("gsh server files download")] + public async Task ServerFilesDownload_ThrowsNotLoggedInException() + { + SetConfigValue("project-id", CommonKeys.ValidProjectId); + SetConfigValue("environment-id", CommonKeys.ValidEnvironmentId); + SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); + + await new UgsCliTestCase() + .Command(k_ServerFilesDownloadCommand) + .AssertExitCode(ExitCode.HandledError) + .AssertStandardErrorContains(k_NotLoggedIn) + .ExecuteAsync(); + } + + [Test] + [Category("gsh")] + [Category("gsh server")] + [Category("gsh server files download")] + public async Task ServerFilesDownload_ThrowsProjectIdNotSetException() + { + SetConfigValue("environment-id", CommonKeys.ValidEnvironmentId); + SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); + await GetLoggedInCli() + .Command(k_ServerFilesDownloadCommand) + .AssertExitCode(ExitCode.HandledError) + .AssertStandardErrorContains(k_ProjectIdIsNotSet) + .ExecuteAsync(); + } + + [Test] + [Category("gsh")] + [Category("gsh server")] + [Category("gsh server files download")] + public async Task ServerFilesDownload_ThrowsEnvironmentIdNotSetException() + { + SetConfigValue("project-id", CommonKeys.ValidProjectId); + await GetLoggedInCli() + .Command(k_ServerFilesDownloadCommand) + .AssertExitCode(ExitCode.HandledError) + .AssertStandardErrorContains(k_EnvironmentNameIsNotSet) + .ExecuteAsync(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFilesTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFilesTests.cs index 4f05ca6..cdb8ed8 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFilesTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFilesTests.cs @@ -12,16 +12,15 @@ public partial class GameServerHostingTests [Category("gsh")] [Category("gsh server")] [Category("gsh server files")] - [Ignore("Failing with feature flag")] public async Task ServerFiles_Succeeds() { await GetFullySetCli() - .Command("gsh server files 123") + .Command("gsh server files list --server-id 123") .AssertStandardOutput( str => { - Assert.IsTrue(str.Contains("Fetching server files...")); - Assert.IsTrue(str.Contains("fileName: server.log")); + Assert.IsTrue(str.Contains("Fetching files list...")); + Assert.IsTrue(str.Contains("filename: error.log")); }) .AssertNoErrors() .ExecuteAsync(); @@ -31,7 +30,6 @@ await GetFullySetCli() [Category("gsh")] [Category("gsh server")] [Category("gsh server files")] - [Ignore("Failing with feature flag")] public async Task ServerFiles_ThrowsNotLoggedInException() { SetConfigValue("project-id", CommonKeys.ValidProjectId); @@ -39,7 +37,7 @@ public async Task ServerFiles_ThrowsNotLoggedInException() SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); await new UgsCliTestCase() - .Command("gsh server files") + .Command("gsh server files list --server-id 123") .AssertExitCode(ExitCode.HandledError) .AssertStandardErrorContains(k_NotLoggedIn) .ExecuteAsync(); @@ -49,13 +47,12 @@ public async Task ServerFiles_ThrowsNotLoggedInException() [Category("gsh")] [Category("gsh server")] [Category("gsh server files")] - [Ignore("Failing with feature flag")] public async Task ServerFiles_ThrowsProjectIdNotSetException() { SetConfigValue("environment-id", CommonKeys.ValidEnvironmentId); SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); await GetLoggedInCli() - .Command("gsh server files") + .Command("gsh server files list --server-id 123") .AssertExitCode(ExitCode.HandledError) .AssertStandardErrorContains(k_ProjectIdIsNotSet) .ExecuteAsync(); @@ -65,13 +62,11 @@ await GetLoggedInCli() [Category("gsh")] [Category("gsh server")] [Category("gsh server files")] - [Ignore("Failing with feature flag")] public async Task ServerFiles_ThrowsEnvironmentIdNotSetException() { SetConfigValue("project-id", CommonKeys.ValidProjectId); - SetConfigValue("environment-name", CommonKeys.ValidEnvironmentName); await GetLoggedInCli() - .Command("gsh server files") + .Command("gsh server files list --server-id 123") .AssertExitCode(ExitCode.HandledError) .AssertStandardErrorContains(k_EnvironmentNameIsNotSet) .ExecuteAsync(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs index 6999d86..e4703c6 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs @@ -179,6 +179,7 @@ public async Task LeaderboardResetSucceed() await AssertSuccess("leaderboards reset lb1", expectedMessage); } [Test] + [Ignore("Flaky Test: Temporarily ignored, will be tackled in GID-2310")] public async Task LeaderboardImportSucceed() { ZipArchiver zipArchiver = new ZipArchiver(); @@ -193,6 +194,7 @@ await zipArchiver.ZipAsync(Path.Join(k_TestDirectory, k_DefaultFileName), "test" } [Test] + [Ignore("Flaky Test: Temporarily ignored, will be tackled in GID-2310")] public async Task LeaderboardImportWithNameSucceed() { ZipArchiver zipArchiver = new ZipArchiver(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentHandlerTests.cs deleted file mode 100644 index ade6301..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardDeploymentHandlerTests.cs +++ /dev/null @@ -1,267 +0,0 @@ -using Moq; -using NUnit.Framework; -using Unity.Services.Leaderboards.Authoring.Core.Deploy; -using Unity.Services.Leaderboards.Authoring.Core.Model; -using Unity.Services.Leaderboards.Authoring.Core.Service; - -namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; - -[TestFixture] -class LeaderboardDeploymentHandlerTests -{ - [Test] - public async Task DeployAsync_CorrectResult() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.DeployAsync( - localLeaderboards - ); - - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Updated); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Deployed); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Created); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Deployed); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Created); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Deployed); - } - - [Test] - public async Task DeployAsync_CreateCallsMade() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.DeployAsync( - localLeaderboards - ); - - mockLeaderboardsClient - .Verify( - c => c.Create( - It.Is(l => l.Id == "bar"), - It.IsAny()), - Times.Once); - mockLeaderboardsClient - .Verify( - c => c.Create( - It.Is(l => l.Id == "dup-id"), - It.IsAny()), - Times.Once); - } - - [Test] - public async Task DeployAsync_UpdateCallsMade() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.DeployAsync( - localLeaderboards - ); - - mockLeaderboardsClient - .Verify( - c => c.Update( - It.Is(l => l.Id == "foo"), - It.IsAny()), - Times.Once); - } - - [Test] - public async Task DeployAsync_NoReconcileNoDeleteCalls() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.DeployAsync( - localLeaderboards - ); - - mockLeaderboardsClient - .Verify( - c => c.Delete( - It.Is(l => l.Id == "echo"), - It.IsAny()), - Times.Never); - } - - [Test] - public async Task DeployAsync_ReconcileDeleteCalls() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.DeployAsync( - localLeaderboards, - reconcile: true - ); - - mockLeaderboardsClient - .Verify( - c => c.Delete( - It.Is(l => l.Id == "echo"), - It.IsAny()), - Times.Once); - } - - - [Test] - public async Task DeployAsync_DryRunNoCalls() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.DeployAsync( - localLeaderboards, - true - ); - - mockLeaderboardsClient - .Verify( - c => c.Create( - It.IsAny(), - It.IsAny()), - Times.Never); - - mockLeaderboardsClient - .Verify( - c => c.Update( - It.IsAny(), - It.IsAny()), - Times.Never); - - mockLeaderboardsClient - .Verify( - c => c.Delete( - It.IsAny(), - It.IsAny()), - Times.Never); - } - - [Test] - public async Task DeployAsync_DryRunCorrectResult() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.DeployAsync( - localLeaderboards, - dryRun: true - ); - - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Updated); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Created); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Created); - Assert.AreEqual(0, actualRes.Deployed.Count); - } - - [Test] - public async Task FetchAsync_DuplicateIdNotDeleted() - { - var localLeaderboards = GetLocalConfigs(); - localLeaderboards.Add(new LeaderboardConfig("dup-id", "other name") { Path = "otherpath.lb"}); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - var handler = new LeaderboardsDeploymentHandler(mockLeaderboardsClient.Object); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.DeployAsync( - localLeaderboards, - dryRun: true - ); - - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Name == "other name"), actualRes.Failed); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Name == "dup-id"), actualRes.Failed); - } - - static List GetLocalConfigs() - { - var leaderboards = new List() - { - new LeaderboardConfig("foo", "foo") - { - Path = "path1" - }, - new LeaderboardConfig("bar", "bar") - { - Path = "path2" - }, - new LeaderboardConfig("dup-id", "dup-id") - { - Path = "path3" - } - }; - return leaderboards; - } - - static IReadOnlyList GetRemoteConfigs() - { - var leaderboards = new List() - { - new LeaderboardConfig("foo", "foo") - { - Path = "Remote" - }, - new LeaderboardConfig("echo", "echo") - { - Path = "Remote" - } - }; - return leaderboards; - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchHandlerTests.cs deleted file mode 100644 index 8a17620..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardFetchHandlerTests.cs +++ /dev/null @@ -1,243 +0,0 @@ -using Moq; -using NUnit.Framework; -using Unity.Services.Cli.Leaderboards.Deploy; -using Unity.Services.Leaderboards.Authoring.Core.Deploy; -using Unity.Services.Leaderboards.Authoring.Core.Fetch; -using Unity.Services.Leaderboards.Authoring.Core.IO; -using Unity.Services.Leaderboards.Authoring.Core.Model; -using Unity.Services.Leaderboards.Authoring.Core.Service; - -namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; - -[TestFixture] -class LeaderboardFetchHandlerTests -{ - [Test] - public async Task FetchAsync_CorrectResult() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - Mock mockFileSystem = new(); - var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.FetchAsync( - "dir", - localLeaderboards - ); - - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Updated); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "foo"), actualRes.Fetched); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Deleted); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "bar"), actualRes.Fetched); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Deleted); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Fetched); - Assert.IsEmpty(actualRes.Created); - } - - [Test] - public async Task FetchAsync_WriteCallsMade() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - Mock mockFileSystem = new(); - var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.FetchAsync( - "dir", - localLeaderboards - ); - - mockFileSystem - .Verify(f => f.WriteAllText( - "path1", - It.IsAny(), - It.IsAny()), - Times.Once); - - mockFileSystem - .Verify(f => f.WriteAllText( - "echo", - It.IsAny(), - It.IsAny()), - Times.Never); //Should not happen unless reconcile - } - - [Test] - public async Task FetchAsync_DeleteCallsMade() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - Mock mockFileSystem = new(); - var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.FetchAsync( - "dir", - localLeaderboards - ); - - mockFileSystem - .Verify(f => f.Delete( - "path2", - It.IsAny()), - Times.Once); - mockFileSystem - .Verify(f => f.Delete( - "path3", - It.IsAny()), - Times.Once); - } - - [Test] - public async Task FetchAsync_WriteNewOnReconcile() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - Mock mockFileSystem = new(); - var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.FetchAsync( - "dir", - localLeaderboards, - reconcile: true - ); - - mockFileSystem - .Verify(f => f.WriteAllText( - "path1", - It.IsAny(), - It.IsAny()), - Times.Once); - - mockFileSystem - .Verify(f => f.WriteAllText( - Path.Combine("dir","echo.lb"), - It.IsAny(), - It.IsAny()), - Times.Once); //Should happen on reconcile - } - - [Test] - public async Task FetchAsync_DryRunNoCalls() - { - var localLeaderboards = GetLocalConfigs(); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - Mock mockFileSystem = new(); - var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.FetchAsync( - "dir", - localLeaderboards, - dryRun: true - ); - - mockFileSystem - .Verify(f => f.Delete( - It.IsAny(), - It.IsAny()), - Times.Never); - - mockFileSystem - .Verify(f => f.WriteAllText( - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Never); - } - - [Test] - public async Task FetchAsync_DuplicateIdNotDeleted() - { - var localLeaderboards = GetLocalConfigs(); - localLeaderboards.Add(new LeaderboardConfig("dup-id", "other name") { Path = "otherpath.lb"}); - var remoteLeaderboards = GetRemoteConfigs(); - - Mock mockLeaderboardsClient = new(); - Mock mockFileSystem = new(); - var handler = new LeaderboardsFetchHandler(mockLeaderboardsClient.Object, mockFileSystem.Object, new LeaderboardsSerializer()); - - mockLeaderboardsClient - .Setup(c => c.List(It.IsAny())) - .ReturnsAsync(remoteLeaderboards.ToList()); - - var actualRes = await handler.FetchAsync( - "dir", - localLeaderboards, - dryRun: true - ); - - mockFileSystem - .Verify(f => f.Delete( - It.Is(s => s == "path3"), - It.IsAny()), - Times.Never); - - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Name == "other name"), actualRes.Failed); - Assert.Contains(localLeaderboards.FirstOrDefault(l => l.Name == "dup-id"), actualRes.Failed); - } - - static List GetLocalConfigs() - { - var leaderboards = new List() - { - new LeaderboardConfig("foo", "foo") - { - Path = "path1" - }, - new LeaderboardConfig("bar", "bar") - { - Path = "path2" - }, - new LeaderboardConfig("dup-id", "dup-id") - { - Path = "path3" - } - }; - return leaderboards; - } - - static List GetRemoteConfigs() - { - var leaderboards = new List() - { - new LeaderboardConfig("foo", "foo") - { - Path = "Remote" - }, - new LeaderboardConfig("echo", "echo") - { - Path = "Remote" - } - }; - return leaderboards; - } -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardPatchConverterTest.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardPatchConverterTest.cs new file mode 100644 index 0000000..536d89d --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Deploy/LeaderboardPatchConverterTest.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Unity.Services.Cli.Leaderboards.Deploy; +using Unity.Services.Gateway.LeaderboardApiV1.Generated.Model; +using TieringConfig = Unity.Services.Gateway.LeaderboardApiV1.Generated.Model.TieringConfig; + +namespace Unity.Services.Cli.Leaderboards.UnitTest.Deploy; + +[TestFixture] +public class LeaderboardPatchConverterTest +{ + + [Test] + public void Converter_SerializesNullPropsToEmptyObj() + { + var obj = new LeaderboardPatchConfig("name"); + + var str = JsonConvert.SerializeObject(obj, new LeaderboardPatchConverter()); + + var jObj = JsonConvert.DeserializeObject(str)!; + + Assert.AreEqual(jObj[nameof(LeaderboardConfig.TieringConfig)]!.ToString(), (new JObject()).ToString()); + } + + [Test] + public void Converter_SerializesNonNullPropsToRealObj() + { + var originalObj = new TieringConfig( + TieringConfig.StrategyEnum.Rank, + new List + { + new ("one", 1) + }); + + var obj = new LeaderboardPatchConfig() + { + TieringConfig = originalObj, + // ResetConfig is required bcs of the weird patch behavior by the service, causes the + // class to be effectively asymmetrical. We can serialize it, but not deserialize it..... + ResetConfig = new ResetConfig(DateTime.UnixEpoch) + }; + + var str = JsonConvert.SerializeObject(obj, new LeaderboardPatchConverter()); + + var deserializeObject = JsonConvert.DeserializeObject(str)!.TieringConfig; + + Assert.AreEqual(originalObj, deserializeObject); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Unity.Services.Cli.Leaderboards.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Unity.Services.Cli.Leaderboards.UnitTest.csproj index f245f57..0bf54db 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Unity.Services.Cli.Leaderboards.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards.UnitTest/Unity.Services.Cli.Leaderboards.UnitTest.csproj @@ -16,6 +16,7 @@ + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/ILeaderboardsConfigLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/ILeaderboardsConfigLoader.cs index d7ac790..d77efb8 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/ILeaderboardsConfigLoader.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/ILeaderboardsConfigLoader.cs @@ -2,7 +2,7 @@ namespace Unity.Services.Cli.Leaderboards.Deploy; -public interface ILeaderboardsConfigLoader +interface ILeaderboardsConfigLoader { Task<(IReadOnlyList Loaded,IReadOnlyList Failed)> LoadConfigsAsync( IReadOnlyCollection paths, diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardConfigFile.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardConfigFile.cs index d14913f..8bee642 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardConfigFile.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardConfigFile.cs @@ -7,7 +7,7 @@ namespace Unity.Services.Cli.Leaderboards.Deploy; [Serializable] -public class LeaderboardConfigFile : IFileTemplate +class LeaderboardConfigFile : IFileTemplate { [JsonConstructor] public LeaderboardConfigFile(string name) : this(null, name, SortOrder.Asc, UpdateType.KeepBest) diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardPatchConverter.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardPatchConverter.cs new file mode 100644 index 0000000..8603d6d --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardPatchConverter.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Unity.Services.Gateway.LeaderboardApiV1.Generated.Model; + +namespace Unity.Services.Cli.Leaderboards.Deploy; + + +public class LeaderboardPatchConverter : JsonConverter +{ + public override bool CanRead => false; + + public override bool CanConvert(Type objectType) + { + return objectType.IsAssignableTo(typeof(LeaderboardPatchConfig)); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + JObject jsonObject = new JObject(); + var properties = value!.GetType().GetProperties(); + var clearableValues = new [] { nameof(LeaderboardPatchConfig.TieringConfig), nameof(LeaderboardPatchConfig.ResetConfig) }; + + // The patch will only clear an nested object if the object is serialized to `{}`. + // This makes any generated client fail to actually clear the nested object impossible + // without customizing json serialization + foreach (var property in properties) + { + object? propertyValue = property.GetValue(value); + if (propertyValue == null && clearableValues.Contains(property.Name)) + { + jsonObject.Add(property.Name, new JObject()); + } + else if (propertyValue == null) + { + jsonObject.Add(property.Name, JValue.CreateNull()); + } + else + { + jsonObject.Add(property.Name, JToken.FromObject(propertyValue!, serializer)); + } + } + + jsonObject.WriteTo(writer); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + throw new NotImplementedException("Deserialization is not needed"); + } +} + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsClient.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsClient.cs index cddbb7f..dcbd764 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsClient.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsClient.cs @@ -1,4 +1,4 @@ -using Unity.Services.Cli.Authoring.Service; +using Newtonsoft.Json; using Unity.Services.Cli.Leaderboards.Service; using Unity.Services.Gateway.LeaderboardApiV1.Generated.Client; using Unity.Services.Gateway.LeaderboardApiV1.Generated.Model; @@ -18,7 +18,7 @@ namespace Unity.Services.Cli.Leaderboards.Deploy; -public class LeaderboardsClient : ILeaderboardsClient +class LeaderboardsClient : ILeaderboardsClient { readonly ILeaderboardsService m_LeaderboardsService; internal string ProjectId { get; set; } @@ -175,7 +175,7 @@ static LeaderboardIdConfig CreateFromConfig(ILeaderboardConfig leaderboardConfig static LeaderboardPatchConfig PatchFromConfig(ILeaderboardConfig leaderboardConfig) { - var res = new LeaderboardPatchConfig() + var res = new LeaderboardPatchSpecializedConfig() { Name = leaderboardConfig.Name, SortOrder = (ApiSortOrder)(int)leaderboardConfig.SortOrder, @@ -218,4 +218,7 @@ static List FromConfig(List tiers) Start = config.Start }; } + + [JsonConverter(typeof(LeaderboardPatchConverter))] + class LeaderboardPatchSpecializedConfig : LeaderboardPatchConfig { } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsDeploymentService.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsDeploymentService.cs index 9d8c512..86e1e18 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsDeploymentService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsDeploymentService.cs @@ -9,7 +9,7 @@ namespace Unity.Services.Cli.Leaderboards.Deploy; -public class LeaderboardDeploymentService : IDeploymentService +class LeaderboardDeploymentService : IDeploymentService { readonly ILeaderboardsClient m_Client; readonly ILeaderboardsDeploymentHandler m_DeploymentHandler; diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsFetchService.cs index 56d0d54..8ee9d2e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsFetchService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsFetchService.cs @@ -9,7 +9,7 @@ namespace Unity.Services.Cli.Leaderboards.Deploy; -public class LeaderboardFetchService : IFetchService +class LeaderboardFetchService : IFetchService { readonly ILeaderboardsClient m_Client; readonly ILeaderboardsFetchHandler m_FetchHandler; diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsSerializer.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsSerializer.cs index 5de19d3..502ebc4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsSerializer.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Deploy/LeaderboardsSerializer.cs @@ -3,7 +3,7 @@ namespace Unity.Services.Cli.Leaderboards.Deploy; -public class LeaderboardsSerializer : ILeaderboardsSerializer +class LeaderboardsSerializer : ILeaderboardsSerializer { public string Serialize(ILeaderboardConfig config) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/LeaderboardsModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/LeaderboardsModule.cs index 0d30064..58b94ea 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/LeaderboardsModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/LeaderboardsModule.cs @@ -192,3 +192,4 @@ public static void RegisterServices(HostBuilderContext hostBuilderContext, IServ serviceCollection.AddTransient(); } } + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Unity.Services.Cli.Leaderboards.csproj b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Unity.Services.Cli.Leaderboards.csproj index 128da2d..7427d12 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Unity.Services.Cli.Leaderboards.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Leaderboards/Unity.Services.Cli.Leaderboards.csproj @@ -15,6 +15,9 @@ <_Parameter1>$(AssemblyName).UnitTest + + <_Parameter1>Unity.Services.Cli.IntegrationTest + <_Parameter1>DynamicProxyGenAssembly2 @@ -23,10 +26,10 @@ - + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Cli.sln b/Unity.Services.Cli/Unity.Services.Cli.sln index 234e2b6..29500d7 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.sln +++ b/Unity.Services.Cli/Unity.Services.Cli.sln @@ -69,8 +69,6 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Cli.Integration.MockServerApp", "Unity.Services.Cli.Integration.MockServerApp\Unity.Services.Cli.Integration.MockServerApp.csproj", "{27BBE31C-43CE-45AA-9DE4-BF3D097DC1A3}" EndProject EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Leaderboards.Authoring.Core", "Unity.Services.Leaderboards.Authoring.Core\Unity.Services.Leaderboards.Authoring.Core.csproj", "{C1E40F5F-F47D-451F-B91D-75CB630402CC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{ECED14B5-7FD3-4C82-8417-3C6E4A0FC054}" ProjectSection(SolutionItems) = preProject features-definition.json = features-definition.json @@ -209,10 +207,6 @@ Global {27BBE31C-43CE-45AA-9DE4-BF3D097DC1A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {27BBE31C-43CE-45AA-9DE4-BF3D097DC1A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {27BBE31C-43CE-45AA-9DE4-BF3D097DC1A3}.Release|Any CPU.Build.0 = Release|Any CPU - {C1E40F5F-F47D-451F-B91D-75CB630402CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C1E40F5F-F47D-451F-B91D-75CB630402CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C1E40F5F-F47D-451F-B91D-75CB630402CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C1E40F5F-F47D-451F-B91D-75CB630402CC}.Release|Any CPU.Build.0 = Release|Any CPU {E37321BA-9520-46D7-A775-E3C834468B55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E37321BA-9520-46D7-A775-E3C834468B55}.Debug|Any CPU.Build.0 = Debug|Any CPU {E37321BA-9520-46D7-A775-E3C834468B55}.Release|Any CPU.ActiveCfg = Release|Any CPU 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 db23d96..8be62e2 100644 --- a/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli/Unity.Services.Cli.csproj @@ -4,7 +4,7 @@ net6.0 10 ugs - 1.1.0 + 1.2.0 true true diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Batching/Batching.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Batching/Batching.cs deleted file mode 100644 index cf0a769..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Batching/Batching.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Unity.Services.Leaderboards.Authoring.Core.Batching -{ - /// An utility class for executing delegates in batches with a time interval between them - /// Currently only supports async delegates (with or without return values) - public static class Batching - { - const int k_BatchSize = 10; - const double k_SecondsDelay = 1; - - const string k_BatchingExceptionMessage = - "One or more exceptions were thrown during the batching execution. See inner exceptions."; - - public static async Task ExecuteInBatchesAsync( - IEnumerable tasks, - CancellationToken cancellationToken, - int batchSize = k_BatchSize, - double secondsDelay = k_SecondsDelay) - { - var exceptions = new List(); - var iterator = tasks.GetEnumerator(); - - while (true) - { - var chunk = new List(); - for (int i = 0; i < batchSize; ++i) - { - if (!iterator.MoveNext()) - break; - chunk.Add(iterator.Current); - } - - if (chunk.Count == 0) - break; - - var innerExceptions = await ExecuteBatchAsync(chunk); - exceptions.AddRange(innerExceptions); - - if (cancellationToken.IsCancellationRequested) - break; - - await Task.Delay(TimeSpan.FromSeconds(secondsDelay), cancellationToken); - - if (cancellationToken.IsCancellationRequested) - break; - } - - iterator.Dispose(); - - if (exceptions.Count != 0) - { - throw new AggregateException(k_BatchingExceptionMessage, exceptions.ToList()); - } - } - - static async Task> ExecuteBatchAsync(IEnumerable insideTasks) - { - var tasks = new ConcurrentBag(); - var exceptions = new ConcurrentQueue(); - - Parallel.ForEach( - insideTasks, - async del => - { - tasks.Add(del); - try - { - await del; - } - catch (Exception e) - { - exceptions.Enqueue(e); - } - }); - - await Task.WhenAll(tasks); - - return exceptions.ToList(); - } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/DeployResult.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/DeployResult.cs deleted file mode 100644 index 831b5ef..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/DeployResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Unity.Services.Leaderboards.Authoring.Core.Model; - -namespace Unity.Services.Leaderboards.Authoring.Core.Deploy -{ - public class DeployResult - { - public List Created { get; set; } - public List Updated { get; set; } - public List Deleted { get; set; } - public List Deployed { get; set; } - public List Failed { get; set; } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/ILeaderboardsDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/ILeaderboardsDeploymentHandler.cs deleted file mode 100644 index fa5b058..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/ILeaderboardsDeploymentHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Unity.Services.Leaderboards.Authoring.Core.Model; - -namespace Unity.Services.Leaderboards.Authoring.Core.Deploy -{ - public interface ILeaderboardsDeploymentHandler - { - Task DeployAsync(IReadOnlyList localResources, - bool dryRun = false, - bool reconcile = false, - CancellationToken token = default); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/LeaderboardsDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/LeaderboardsDeploymentHandler.cs deleted file mode 100644 index 25dd640..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Deploy/LeaderboardsDeploymentHandler.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Unity.Services.DeploymentApi.Editor; -using Unity.Services.Leaderboards.Authoring.Core.Model; -using Unity.Services.Leaderboards.Authoring.Core.Service; -using Unity.Services.Leaderboards.Authoring.Core.Validations; - -namespace Unity.Services.Leaderboards.Authoring.Core.Deploy -{ - public class LeaderboardsDeploymentHandler : ILeaderboardsDeploymentHandler - { - readonly ILeaderboardsClient m_Client; - readonly object m_ResultLock = new(); - - public LeaderboardsDeploymentHandler(ILeaderboardsClient client) - { - m_Client = client; - } - - public async Task DeployAsync( - IReadOnlyList localResources, - bool dryRun = false, - bool reconcile = false, - CancellationToken token = default) - { - var res = new DeployResult(); - - localResources = DuplicateResourceValidation.FilterDuplicateResources( - localResources, out var duplicateGroups); - - var remoteResources = await m_Client.List(token); - - var toCreate = localResources - .Except(remoteResources, new LeaderboardComparer()) - .ToList(); - - var toUpdate = localResources - .Except(toCreate, new LeaderboardComparer()) - .ToList(); - - var toDelete = new List(); - if (reconcile) - { - toDelete = remoteResources - .Except(localResources, new LeaderboardComparer()) - .ToList(); - } - - res.Created = toCreate; - res.Deleted = toDelete; - res.Updated = toUpdate; - res.Deployed = new List(); - res.Failed = new List(); - - UpdateDuplicateResourceStatus(res, duplicateGroups); - - if (dryRun) - { - return res; - } - - var createTasks = GetTasks(toCreate, m_Client.Create, res, token); - var updateTasks = GetTasks(toUpdate, m_Client.Update, res, token); - var deleteTasks = reconcile - ? GetTasks(toDelete, m_Client.Delete, res, token) - : new List(); - - var allTasks = createTasks.Concat(updateTasks).Concat(deleteTasks); - - await Batching.Batching.ExecuteInBatchesAsync(allTasks, token); - - return res; - } - - IEnumerable GetTasks( - List resources, - Func func, - DeployResult res, - CancellationToken token) - { - return resources.Select(i => DeployResource(func, i, res, token)); - } - - protected virtual void UpdateStatus( - ILeaderboardConfig leaderboardConfig, - DeploymentStatus status) - { - // clients can override this to provide user feedback on progress - leaderboardConfig.Status = status; - } - - protected virtual void UpdateProgress( - ILeaderboardConfig leaderboardConfig, - float progress) - { - // clients can override this to provide user feedback on progress - leaderboardConfig.Progress = progress; - } - - void UpdateDuplicateResourceStatus( - DeployResult result, - IReadOnlyList> duplicateGroups) - { - foreach (var group in duplicateGroups) - { - foreach (var res in group) - { - result.Failed.Add(res); - var (message, shortMessage) = DuplicateResourceValidation.GetDuplicateResourceErrorMessages(res, group.ToList()); - UpdateStatus(res, Statuses.GetFailedToDeploy(shortMessage)); - } - } - } - - async Task DeployResource( - Func task, - ILeaderboardConfig leaderboardConfig, - DeployResult res, - CancellationToken token) - { - try - { - await task(leaderboardConfig, token); - lock (m_ResultLock) - res.Deployed.Add(leaderboardConfig); - UpdateStatus(leaderboardConfig, Statuses.Deployed); - UpdateProgress(leaderboardConfig, 100); - } - catch (Exception e) - { - lock (m_ResultLock) - res.Failed.Add(leaderboardConfig); - UpdateStatus(leaderboardConfig, Statuses.GetFailedToDeploy(e.Message)); - } - } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/FetchResult.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/FetchResult.cs deleted file mode 100644 index 74ef115..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/FetchResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Unity.Services.Leaderboards.Authoring.Core.Model; - -namespace Unity.Services.Leaderboards.Authoring.Core.Fetch -{ - public class FetchResult - { - public List Created { get; set; } - public List Updated { get; set; } - public List Deleted { get; set; } - public List Fetched { get; set; } - public List Failed { get; set; } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/ILeaderboardsFetchHandler.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/ILeaderboardsFetchHandler.cs deleted file mode 100644 index 12f0d58..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/ILeaderboardsFetchHandler.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Unity.Services.Leaderboards.Authoring.Core.Model; - -namespace Unity.Services.Leaderboards.Authoring.Core.Fetch -{ - public interface ILeaderboardsFetchHandler - { - public Task FetchAsync( - string rootDirectory, - IReadOnlyList localResources, - bool dryRun = false, - bool reconcile = false, - CancellationToken token = default); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/LeaderboardsFetchHandler.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/LeaderboardsFetchHandler.cs deleted file mode 100644 index 0d94cad..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Fetch/LeaderboardsFetchHandler.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Unity.Services.DeploymentApi.Editor; -using Unity.Services.Leaderboards.Authoring.Core.Deploy; -using Unity.Services.Leaderboards.Authoring.Core.IO; -using Unity.Services.Leaderboards.Authoring.Core.Model; -using Unity.Services.Leaderboards.Authoring.Core.Serialization; -using Unity.Services.Leaderboards.Authoring.Core.Service; -using Unity.Services.Leaderboards.Authoring.Core.Validations; - -namespace Unity.Services.Leaderboards.Authoring.Core.Fetch -{ - public class LeaderboardsFetchHandler : ILeaderboardsFetchHandler - { - readonly ILeaderboardsClient m_Client; - readonly IFileSystem m_FileSystem; - readonly ILeaderboardsSerializer m_LeaderboardsSerializer; - - public LeaderboardsFetchHandler( - ILeaderboardsClient client, - IFileSystem fileSystem, - ILeaderboardsSerializer leaderboardsSerializer) - { - m_Client = client; - m_FileSystem = fileSystem; - m_LeaderboardsSerializer = leaderboardsSerializer; - } - - public async Task FetchAsync( - string rootDirectory, - IReadOnlyList localResources, - bool dryRun = false, - bool reconcile = false, - CancellationToken token = default) - { - var res = new FetchResult(); - - localResources = DuplicateResourceValidation.FilterDuplicateResources( - localResources, out var duplicateGroups); - - var remoteResources = await m_Client.List(token); - - var toUpdate = localResources - .Intersect(remoteResources, new LeaderboardComparer()) - .ToList(); - - var toDelete = localResources - .Except(remoteResources, new LeaderboardComparer()) - .ToList(); - - var toCreate = new List(); - if (reconcile) - { - toCreate = remoteResources - .Except(localResources, new LeaderboardComparer()) - .ToList(); - toCreate.ForEach(r => ((LeaderboardConfig)r).Path = Path.Combine(rootDirectory, r.Id) + ".lb" ); - } - - res.Created = toCreate; - res.Deleted = toDelete; - res.Updated = toUpdate; - res.Fetched = new List(); - res.Failed = new List(); - - UpdateDuplicateResourceStatus(res, duplicateGroups); - - if (dryRun) - { - return res; - } - - var updateTasks = new List<(ILeaderboardConfig, Task)>(); - var deleteTasks = new List<(ILeaderboardConfig, Task)>(); - var createTasks = new List<(ILeaderboardConfig, Task)>(); - - foreach (var resource in toUpdate) - { - var task = m_FileSystem.WriteAllText( - resource.Path, - m_LeaderboardsSerializer.Serialize(resource), - token); - updateTasks.Add((resource, task)); - } - - foreach (var resource in toDelete) - { - var task = m_FileSystem.Delete( - resource.Path, - token); - deleteTasks.Add((resource, task)); - } - - if (reconcile) - { - foreach (var resource in toCreate) - { - var task = m_FileSystem.WriteAllText( - resource.Path, - m_LeaderboardsSerializer.Serialize(resource), - token); - createTasks.Add((resource, task)); - } - } - - await UpdateResult(updateTasks, res); - await UpdateResult(deleteTasks, res); - await UpdateResult(createTasks, res); - - return res; - } - - protected virtual void UpdateStatus( - ILeaderboardConfig leaderboardConfig, - DeploymentStatus status) - { - // clients can override this to provide user feedback on progress - leaderboardConfig.Status = status; - } - - protected virtual void UpdateProgress( - ILeaderboardConfig leaderboardConfig, - float progress) - { - // clients can override this to provide user feedback on progress - leaderboardConfig.Progress = progress; - } - - void UpdateDuplicateResourceStatus( - FetchResult result, - IReadOnlyList> duplicateGroups) - { - foreach (var group in duplicateGroups) - { - foreach (var res in group) - { - result.Failed.Add(res); - var (message, shortMessage) = DuplicateResourceValidation.GetDuplicateResourceErrorMessages(res, group.ToList()); - UpdateStatus(res, Statuses.GetFailedToFetch(shortMessage)); - } - } - } - - async Task UpdateResult( - List<(ILeaderboardConfig, Task)> tasks, - FetchResult res) - { - foreach (var (resource, task) in tasks) - { - try - { - await task; - res.Fetched.Add(resource); - UpdateStatus(resource, Statuses.Fetched); - UpdateProgress(resource, 100); - } - catch (Exception e) - { - res.Failed.Add(resource); - UpdateStatus(resource, Statuses.GetFailedToFetch(e.Message)); - } - } - } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/IO/IFileSystem.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/IO/IFileSystem.cs deleted file mode 100644 index 2418661..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/IO/IFileSystem.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Unity.Services.Leaderboards.Authoring.Core.IO -{ - public interface IFileSystem - { - Task ReadAllText( - string path, - CancellationToken token = default(CancellationToken)); - - Task WriteAllText( - string path, - string contents, - CancellationToken token = default(CancellationToken)); - - Task Delete(string path, CancellationToken token = default(CancellationToken)); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ILeaderboardConfig.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ILeaderboardConfig.cs deleted file mode 100644 index 50a2f76..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ILeaderboardConfig.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Unity.Services.DeploymentApi.Editor; - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - public interface ILeaderboardConfig : IDeploymentItem, ITypedItem - { - string Id { get; } - new float Progress { get; set; } - - SortOrder SortOrder { get; set; } - UpdateType UpdateType { get; set; } - Decimal BucketSize { get; set; } - ResetConfig ResetConfig { get; set; } - TieringConfig TieringConfig { get; set; } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardComparer.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardComparer.cs deleted file mode 100644 index f8fc433..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardComparer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - public class LeaderboardComparer : IEqualityComparer - { - public bool Equals(ILeaderboardConfig x, ILeaderboardConfig y) - { - if (ReferenceEquals(x, y)) return true; - if (ReferenceEquals(x, null)) return false; - if (ReferenceEquals(y, null)) return false; - if (x.GetType() != y.GetType()) return false; - return x.Id == y.Id; - } - - public int GetHashCode(ILeaderboardConfig obj) - { - return (obj.Id != null ? obj.Id.GetHashCode() : 0); - } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardConfig.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardConfig.cs deleted file mode 100644 index ce877d2..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/LeaderboardConfig.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Unity.Services.DeploymentApi.Editor; - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - [Serializable] - public class LeaderboardConfig : ILeaderboardConfig - { - float m_Progress; - DeploymentStatus m_Status; - internal const string ConfigType = "Leaderboard"; - - public LeaderboardConfig() : this( - "myLeaderboard", - "My Leaderboard") - { - } - - public LeaderboardConfig( - string id, - string name, - SortOrder sortOrder = SortOrder.Asc, - UpdateType updateType = UpdateType.KeepBest) - { - Id = id; - Name = name; - Path = string.Empty; - States = new ObservableCollection(); - SortOrder = sortOrder; - UpdateType = updateType; - } - - public SortOrder SortOrder { get; set; } - public UpdateType UpdateType { get; set; } - public string Id { get; } - public string Name { get; set; } - public string Path { get; set; } - public string Type => ConfigType; - - /// - public float Progress - { - get => m_Progress; - set => SetField(ref m_Progress, value); - } - - public DeploymentStatus Status - { - get => m_Status; - set => SetField(ref m_Status, value); - } - - public ObservableCollection States { get; } - public decimal BucketSize { get; set; } - public ResetConfig ResetConfig { get; set; } - public TieringConfig TieringConfig { get; set; } - - public override string ToString() - { - if (Path == "Remote") - return Id; - return $"'{Path}'"; - } - - /// - /// Event will be raised when a property of the instance is changed - /// - public event PropertyChangedEventHandler PropertyChanged; - - /// - /// Sets the field and raises an OnPropertyChanged event. - /// - /// The field to set. - /// The value to set. - /// The callback. - /// Name of the property to set. - /// Type of the parameter. - protected void SetField( - ref T field, - T value, - Action onFieldChanged = null, - [CallerMemberName] string propertyName = null) - { - if (EqualityComparer.Default.Equals(field, value)) - return; - field = value; - OnPropertyChanged(propertyName!); - onFieldChanged?.Invoke(field); - } - - void OnPropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ResetConfig.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ResetConfig.cs deleted file mode 100644 index d19dbda..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/ResetConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - [Serializable] - public class ResetConfig - { - public DateTime Start { get; set; } - public string Schedule { get; set; } - public bool Archive { get; set; } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/SortOrder.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/SortOrder.cs deleted file mode 100644 index b01b796..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/SortOrder.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Runtime.Serialization; - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - public enum SortOrder - { - [EnumMember(Value = "asc")] - Asc = 1, - - [EnumMember(Value = "desc")] - Desc = 2, - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Statuses.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Statuses.cs deleted file mode 100644 index 3c55af5..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Statuses.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Unity.Services.DeploymentApi.Editor; - - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - static class Statuses - { - public static readonly DeploymentStatus FailedToLoad = new ("Failed to load", string.Empty, SeverityLevel.Error); - - public static DeploymentStatus GetFailedToFetch(string details) - => new ("Failed to fetch", details, SeverityLevel.Error); - public static readonly DeploymentStatus Fetching = new ("Fetching", string.Empty, SeverityLevel.Info); - public static readonly DeploymentStatus Fetched = new ("Fetched", string.Empty, SeverityLevel.Success); - - public static DeploymentStatus GetFailedToDeploy(string details) - => new ("Failed to deploy", details, SeverityLevel.Error); - public static readonly DeploymentStatus Deploying = new ( "Deploying", string.Empty, SeverityLevel.Info); - public static readonly DeploymentStatus Deployed = new ("Deployed", string.Empty, SeverityLevel.Success); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Strategy.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Strategy.cs deleted file mode 100644 index 1afc0d9..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Strategy.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.Serialization; - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - public enum Strategy - { - [EnumMember(Value = "score")] - Score = 1, - - [EnumMember(Value = "rank")] - Rank = 2, - - [EnumMember(Value = "percent")] - Percent = 3, - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Tier.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Tier.cs deleted file mode 100644 index 8a445d7..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/Tier.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - public class Tier - { - public string Id { get; set; } - public double? Cutoff { get; set; } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/TieringConfig.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/TieringConfig.cs deleted file mode 100644 index f608d3c..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/TieringConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - [Serializable] - public class TieringConfig - { - public Strategy Strategy { get; set; } - public List Tiers { get; set; } - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/UpdateType.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/UpdateType.cs deleted file mode 100644 index 821c046..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Model/UpdateType.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.Serialization; - -namespace Unity.Services.Leaderboards.Authoring.Core.Model -{ - public enum UpdateType - { - [EnumMember(Value = "keepBest")] - KeepBest = 1, - - [EnumMember(Value = "keepLatest")] - KeepLatest = 2, - - [EnumMember(Value = "aggregate")] - Aggregate = 3, - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Serialization/ILeaderboardSerializer.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Serialization/ILeaderboardSerializer.cs deleted file mode 100644 index d8b5f17..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Serialization/ILeaderboardSerializer.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Unity.Services.Leaderboards.Authoring.Core.Model; - -namespace Unity.Services.Leaderboards.Authoring.Core.Serialization -{ - public interface ILeaderboardsSerializer - { - string Serialize(ILeaderboardConfig config); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Service/ILeaderboardsClient.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Service/ILeaderboardsClient.cs deleted file mode 100644 index 60f6096..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Service/ILeaderboardsClient.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Unity.Services.Leaderboards.Authoring.Core.Model; - -namespace Unity.Services.Leaderboards.Authoring.Core.Service -{ - public interface ILeaderboardsClient - { - void Initialize(string environmentId, string projectId, CancellationToken cancellationToken); - - Task Get(string id, CancellationToken token); - Task Update(ILeaderboardConfig leaderboardConfig, CancellationToken token); - Task Create(ILeaderboardConfig leaderboardConfig, CancellationToken token); - Task Delete(ILeaderboardConfig leaderboardConfig, CancellationToken token); - Task> List(CancellationToken token); - } -} diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Unity.Services.Leaderboards.Authoring.Core.csproj b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Unity.Services.Leaderboards.Authoring.Core.csproj deleted file mode 100644 index 746aef6..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Unity.Services.Leaderboards.Authoring.Core.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - net5.0 - disable - 0.0.1 - disable - Library - true - true - - 9 - - - - <_Parameter1>$(AssemblyName).UnitTest - - - <_Parameter1>DynamicProxyGenAssembly2 - - - - - - \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Validations/DuplicateResourceValidation.cs b/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Validations/DuplicateResourceValidation.cs deleted file mode 100644 index 4b9af9a..0000000 --- a/Unity.Services.Cli/Unity.Services.Leaderboards.Authoring.Core/Validations/DuplicateResourceValidation.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Unity.Services.Leaderboards.Authoring.Core.Model; - -namespace Unity.Services.Leaderboards.Authoring.Core.Validations -{ - public static class DuplicateResourceValidation - { - public static IReadOnlyList FilterDuplicateResources( - IReadOnlyList resources, - out IReadOnlyList> duplicateGroups) - { - duplicateGroups = resources - .GroupBy(r => r.Id) - .Where(g => g.Count() > 1) - .ToList(); - - var hashset = new HashSet(duplicateGroups.Select(g => g.Key)); - - return resources - .Where(r => !hashset.Contains(r.Id)) - .ToList(); - } - - public static (string, string) GetDuplicateResourceErrorMessages( - ILeaderboardConfig targetLeaderboardConfig, - IReadOnlyList group) - { - var duplicates = group - .Except(new[] { targetLeaderboardConfig }) - .ToList(); - - var duplicatesStr = string.Join(", ", duplicates.Select(d => $"'{d.Path}'")); - var shortMessage = $"'{targetLeaderboardConfig.Path}' was found duplicated in other files: {duplicatesStr}"; - var message = $"Multiple resources with the same identifier '{targetLeaderboardConfig.Id}' were found. " - + "Only a single resource for a given identifier may be deployed/fetched at the same time. " - + "Give all resources unique identifiers or deploy/fetch them separately to proceed.\n" - + shortMessage; - return (shortMessage, message); - } - } -}