diff --git a/CHANGELOG.md b/CHANGELOG.md index 400dc64..4a2835a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ 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.3.0] - 2024-02-29 +### Added +- Added new service module Scheduler + - `new-file` for deployment + - `list` to see live schedules +- Added support for support [Scheduler service](https://docs.unity.com/ugs/en-us/manual/cloud-code/manual/triggers#Scheduler) to Deploy and Fetch +- Added fetch for [Triggers](https://docs.unity.com/ugs/en-us/manual/cloud-code/manual/triggers) +- Added `--readiness` option to gsh build configuration create command +- Added `--readiness` option to gsh build configuration update command +- Added Game Server Hosting `core-dump` command to configure an external storage location for core dumps (GCS only) +- Added `--build-version-name` option to gsh build create/create-version commands + +### Fixed +- Fixed New-file command error for directory that is not exist. +- Deploy no longer requires permissions for services not being deployed, unless reconcile is specified +- Fixed Economy fetch issue making it not idempotent. +- Fixed issue where issues after loading were not reported when deploying CloudCode modules +- Fixed issue where deploying a solution as Cloud Code Module will be logged with the solution path and not the generated ccm + +### Changed +- Improved the error description when failing to deploy a solution with no clear main entry project, for Cloud Code Modules deployment. + ## [1.2.0] - 2023-11-14 ### Added diff --git a/Unity.Services.Cli/Unity.Services.Cli.Access.UnitTest/Deploy/AuthoringResultTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Access.UnitTest/Deploy/AuthoringResultTests.cs index c33a003..eba8ba7 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Access.UnitTest/Deploy/AuthoringResultTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Access.UnitTest/Deploy/AuthoringResultTests.cs @@ -54,7 +54,7 @@ public void Test_Orders_ofComplexitx() var table = ar.ToTable(); foreach (var (actual,expected) in table.Result.Zip(expectedTable)) { - Assert.That(actual.Name, Is.EqualTo(expected.Name));; + Assert.That(actual.Name, Is.EqualTo(expected.Name)); } } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Access.UnitTest/Service/AccessServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Access.UnitTest/Service/AccessServiceTests.cs index 74ddf10..f82e6d9 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Access.UnitTest/Service/AccessServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Access.UnitTest/Service/AccessServiceTests.cs @@ -7,6 +7,7 @@ using Unity.Services.Cli.ServiceAccountAuthentication; using Unity.Services.Cli.ServiceAccountAuthentication.Token; using Unity.Services.Gateway.AccessApiV1.Generated.Api; +using Unity.Services.Gateway.AccessApiV1.Generated.Client; using Unity.Services.Gateway.AccessApiV1.Generated.Model; namespace Unity.Services.Cli.Access.UnitTest.Service; @@ -88,6 +89,17 @@ public async Task GetPolicy_Valid() Times.Once); } + [Test] + public void GetPolicy_Invalid_ApiThrowsError() + { + m_ProjectPolicyApi.Setup(a => a.GetPolicyAsync(It.IsAny(), It.IsAny(), It.IsAny(), CancellationToken.None)) + .Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.GetPolicyAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, + CancellationToken.None)); + } + [Test] public async Task GetPlayerPolicy_Valid() { @@ -104,6 +116,17 @@ public async Task GetPlayerPolicy_Valid() Times.Once); } + [Test] + public void GetPlayerPolicy_Invalid_ApiThrowsError() + { + m_PlayerPolicyApi.Setup(a => a.GetPlayerPolicyAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), CancellationToken.None)) + .Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.GetPlayerPolicyAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, TestValues.ValidPlayerId, + CancellationToken.None)); + } + [Test] public async Task GetAllPlayerPolicies_Valid() { @@ -125,6 +148,17 @@ public async Task GetAllPlayerPolicies_Valid() Times.Once); } + [Test] + public void GetAllPlayerPolicies_Invalid_ApiThrowsError() + { + m_PlayerPolicyApi.Setup(a => a.GetAllPlayerPoliciesAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), CancellationToken.None)).Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.GetAllPlayerPoliciesAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, + CancellationToken.None)); + } + [Test] public async Task UpsertPolicyAsync_Valid() { @@ -147,6 +181,17 @@ public async Task UpsertPolicyAsync_Valid() Times.Once); } + [Test] + public void UpsertPolicyAsync_Invalid_ApiThrowsError() + { + m_ProjectPolicyApi.Setup(a => a.UpsertPolicyAsync(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), CancellationToken.None)).Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.UpsertPolicyAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, m_PolicyFile!, + CancellationToken.None)); + } + [Test] public void UpsertPolicyAsync_InvalidInput() { @@ -182,6 +227,17 @@ public async Task UpsertPlayerPolicyAsync_Valid() Times.Once); } + [Test] + public void UpsertPlayerPolicyAsync_Invalid_ApiThrowsError() + { + m_PlayerPolicyApi.Setup(a => a.UpsertPlayerPolicyAsync(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), CancellationToken.None)).Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.UpsertPlayerPolicyAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, TestValues.ValidPlayerId, + m_PolicyFile!, CancellationToken.None)); + } + [Test] public void UpsertPlayerPolicyAsync_InvalidInput() { @@ -215,6 +271,17 @@ public async Task DeletePolicyStatementsAsync_Valid() Times.Once); } + [Test] + public void DeletePolicyStatementsAsync_Invalid_ApiThrowsError() + { + m_ProjectPolicyApi.Setup(a => a.DeletePolicyStatementsAsync(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), CancellationToken.None)).Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.DeletePolicyStatementsAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, + m_PolicyFile!, CancellationToken.None)); + } + [Test] public void DeletePolicyStatementsAsync_InvalidInput() { @@ -229,7 +296,8 @@ public void DeletePolicyStatementsAsync_InvalidInput() [Test] public async Task DeletePlayerPolicyStatementsAsync_Valid() { - m_PlayerPolicyApi.Setup(a => a.DeletePlayerPolicyStatementsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), CancellationToken.None)); + m_PlayerPolicyApi.Setup(a => a.DeletePlayerPolicyStatementsAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), CancellationToken.None)); await m_AccessService!.DeletePlayerPolicyStatementsAsync( TestValues.ValidProjectId, @@ -249,6 +317,17 @@ public async Task DeletePlayerPolicyStatementsAsync_Valid() Times.Once); } + [Test] + public void DeletePlayerPolicyStatementsAsync_Invalid_ApiThrowsError() + { + m_PlayerPolicyApi.Setup(a => a.DeletePlayerPolicyStatementsAsync(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), CancellationToken.None)).Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.DeletePlayerPolicyStatementsAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, TestValues.ValidPlayerId, + m_PolicyFile!, CancellationToken.None)); + } + [Test] public void DeletePlayerPolicyStatementsAsync_InvalidInput() { @@ -284,6 +363,17 @@ public async Task UpsertProjectAccessCaCAsync_Valid() Times.Once); } + [Test] + public void UpsertProjectAccessCaCAsync_Invalid_ApiThrowsError() + { + m_ProjectPolicyApi.Setup(a => a.UpsertPolicyAsync(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), CancellationToken.None)).Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.UpsertProjectAccessCaCAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, It.IsAny(), + CancellationToken.None)); + } + [Test] public async Task DeleteProjectAccessCaCAsync_Valid() { @@ -302,4 +392,15 @@ public async Task DeleteProjectAccessCaCAsync_Valid() It.IsAny()), Times.Once); } + + [Test] + public void DeleteProjectAccessCaCAsync_Invalid_ApiThrowsError() + { + m_ProjectPolicyApi.Setup(a => a.DeletePolicyStatementsAsync(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), CancellationToken.None)).Throws(); + + Assert.ThrowsAsync( + () => m_AccessService!.DeleteProjectAccessCaCAsync(TestValues.ValidProjectId, TestValues.ValidEnvironmentId, It.IsAny(), + CancellationToken.None)); + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Access/Deploy/AccessAuthoringResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Access/Deploy/AccessAuthoringResult.cs index 79dedf7..4e0d21c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Access/Deploy/AccessAuthoringResult.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Access/Deploy/AccessAuthoringResult.cs @@ -24,7 +24,7 @@ public AccessDeploymentResult( { } - public override TableContent ToTable() + public override TableContent ToTable(string service = "") { return AccessControlResToTable(this); } @@ -79,7 +79,7 @@ public AccessFetchResult( { } - public override TableContent ToTable() + public override TableContent ToTable(string service) { return AccessDeploymentResult.AccessControlResToTable(this); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Access/Service/AccessService.cs b/Unity.Services.Cli/Unity.Services.Cli.Access/Service/AccessService.cs index 57961e0..ea40b8f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Access/Service/AccessService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Access/Service/AccessService.cs @@ -1,9 +1,9 @@ -using Microsoft.Extensions.FileProviders; using Newtonsoft.Json; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.ServiceAccountAuthentication; using Unity.Services.Cli.ServiceAccountAuthentication.Token; using Unity.Services.Gateway.AccessApiV1.Generated.Api; +using Unity.Services.Gateway.AccessApiV1.Generated.Client; using Unity.Services.Gateway.AccessApiV1.Generated.Model; namespace Unity.Services.Cli.Access.Service; @@ -48,9 +48,16 @@ public async Task GetPolicyAsync(string projectId, string environmentId, CancellationToken cancellationToken = default) { await AuthorizeServiceAsync(cancellationToken); - var response = - await m_ProjectPolicyApi.GetPolicyAsync(projectId, environmentId, cancellationToken: cancellationToken); - return response; + try + { + var response = + await m_ProjectPolicyApi.GetPolicyAsync(projectId, environmentId, cancellationToken: cancellationToken); + return response; + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); + } } public async Task GetPlayerPolicyAsync(string projectId, string environmentId, string playerId, @@ -58,9 +65,16 @@ public async Task GetPlayerPolicyAsync(string projectId, string en { await AuthorizeServiceAsync(cancellationToken); - var response = - await m_PlayerPolicyApi.GetPlayerPolicyAsync(projectId, environmentId, playerId, cancellationToken: cancellationToken); - return response; + try + { + var response = + await m_PlayerPolicyApi.GetPlayerPolicyAsync(projectId, environmentId, playerId, cancellationToken: cancellationToken); + return response; + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); + } } public async Task> GetAllPlayerPoliciesAsync(string projectId, string environmentId, @@ -68,17 +82,32 @@ public async Task> GetAllPlayerPoliciesAsync(string projectId { await AuthorizeServiceAsync(cancellationToken); - var response = - await m_PlayerPolicyApi.GetAllPlayerPoliciesAsync(projectId, environmentId, 100, cancellationToken: cancellationToken); - List results = response!.Results; - - while (response.Next != null) + try { - response = await m_PlayerPolicyApi.GetAllPlayerPoliciesAsync(projectId, environmentId, next: response.Next, cancellationToken: cancellationToken); - results = results.Concat(response.Results).ToList(); + var response = + await m_PlayerPolicyApi.GetAllPlayerPoliciesAsync( + projectId, + environmentId, + 100, + cancellationToken: cancellationToken); + List results = response!.Results; + + while (response.Next != null) + { + response = await m_PlayerPolicyApi.GetAllPlayerPoliciesAsync( + projectId, + environmentId, + next: response.Next, + cancellationToken: cancellationToken); + results = results.Concat(response.Results).ToList(); + } + + return results; + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); } - - return results; } public async Task UpsertPolicyAsync(string projectId, string environmentId, FileInfo file, @@ -100,7 +129,15 @@ public async Task UpsertPolicyAsync(string projectId, string environmentId, File { throw new CliException(k_JsonIncorrectFormatExceptionMessage, ExitCode.HandledError); } - await m_ProjectPolicyApi.UpsertPolicyAsync(projectId, environmentId, policy, cancellationToken: cancellationToken); + + try + { + await m_ProjectPolicyApi.UpsertPolicyAsync(projectId, environmentId, policy, cancellationToken: cancellationToken); + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); + } } public async Task UpsertPlayerPolicyAsync(string projectId, string environmentId, string playerId, FileInfo file, @@ -123,7 +160,14 @@ public async Task UpsertPlayerPolicyAsync(string projectId, string environmentId throw new CliException(k_JsonIncorrectFormatExceptionMessage, ExitCode.HandledError); } - await m_PlayerPolicyApi.UpsertPlayerPolicyAsync(projectId, environmentId, playerId, policy, cancellationToken: cancellationToken); + try + { + await m_PlayerPolicyApi.UpsertPlayerPolicyAsync(projectId, environmentId, playerId, policy, cancellationToken: cancellationToken); + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); + } } public async Task DeletePolicyStatementsAsync(string projectId, string environmentId, FileInfo file, @@ -146,7 +190,14 @@ public async Task DeletePolicyStatementsAsync(string projectId, string environme throw new CliException(k_JsonIncorrectFormatExceptionMessage, ExitCode.HandledError); } - await m_ProjectPolicyApi.DeletePolicyStatementsAsync(projectId, environmentId, deleteOptions, cancellationToken: cancellationToken); + try + { + await m_ProjectPolicyApi.DeletePolicyStatementsAsync(projectId, environmentId, deleteOptions, cancellationToken: cancellationToken); + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); + } } public async Task DeletePlayerPolicyStatementsAsync(string projectId, string environmentId, string playerId, FileInfo file, @@ -169,7 +220,14 @@ public async Task DeletePlayerPolicyStatementsAsync(string projectId, string env throw new CliException(k_JsonIncorrectFormatExceptionMessage, ExitCode.HandledError); } - await m_PlayerPolicyApi.DeletePlayerPolicyStatementsAsync(projectId, environmentId, playerId, deleteOptions, cancellationToken: cancellationToken); + try + { + await m_PlayerPolicyApi.DeletePlayerPolicyStatementsAsync(projectId, environmentId, playerId, deleteOptions, cancellationToken: cancellationToken); + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); + } } public async Task UpsertProjectAccessCaCAsync( @@ -179,7 +237,15 @@ public async Task UpsertProjectAccessCaCAsync( CancellationToken cancellationToken = default) { await AuthorizeServiceAsync(cancellationToken); - await m_ProjectPolicyApi.UpsertPolicyAsync(projectId, environmentId, policy, cancellationToken: cancellationToken); + + try + { + await m_ProjectPolicyApi.UpsertPolicyAsync(projectId, environmentId, policy, cancellationToken: cancellationToken); + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); + } } public async Task DeleteProjectAccessCaCAsync( @@ -189,6 +255,13 @@ public async Task DeleteProjectAccessCaCAsync( CancellationToken cancellationToken = default) { await AuthorizeServiceAsync(cancellationToken); - await m_ProjectPolicyApi.DeletePolicyStatementsAsync(projectId, environmentId, options, cancellationToken: cancellationToken); + try + { + await m_ProjectPolicyApi.DeletePolicyStatementsAsync(projectId, environmentId, options, cancellationToken: cancellationToken); + } + catch (ApiException e) + { + throw new CliException(e.Message, ExitCode.HandledError); + } } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/AuthoringCommandHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/AuthoringCommandHandlerTests.cs new file mode 100644 index 0000000..702dd72 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/AuthoringCommandHandlerTests.cs @@ -0,0 +1,337 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Spectre.Console; +using Unity.Services.Cli.Authoring.DeploymentDefinition; +using Unity.Services.Cli.Authoring.Handlers; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Common.Services; +using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.Authoring.UnitTest.Handlers; + +public class AuthoringCommandHandlerTests +{ + readonly Mock m_Host = new(); + readonly Mock m_Logger = new(); + readonly Mock m_ServiceProvider = new(); + readonly Mock m_UnityEnvironment = new(); + readonly Mock m_AnalyticsEventBuilder = new(); + readonly ServiceTypesBridge m_Bridge = new(); + + const string k_ValidEnvironmentId = "00000000-0000-0000-0000-000000000000"; + + public interface ITest1Service : IDeploymentService, IFetchService { } + public interface ITest2Service : IDeploymentService, IFetchService { } + + [SetUp] + public void SetUp() + { + m_Host.Reset(); + m_ServiceProvider.Reset(); + m_Logger.Reset(); + m_UnityEnvironment.Reset(); + m_UnityEnvironment + .Setup(x => x.FetchIdentifierAsync(CancellationToken.None)) + .Returns(Task.FromResult(k_ValidEnvironmentId)); + } + + [Test] + public async Task DeployDoesNotCallService_NotReconcile() + { + var mockService1 = + (Mock)CreateMockDeploymentService(".test1", "Test1"); + var mockService2 = + (Mock)CreateMockDeploymentService(".test2", "Test2"); + m_Host.Reset(); + var collection = m_Bridge.CreateBuilder(new ServiceCollection()); + collection.AddScoped(_ => mockService1.Object); + collection.AddScoped(_ => mockService2.Object); + var provider = m_Bridge.CreateServiceProvider(collection); + m_Host.Setup(x => x.Services) + .Returns(provider); + + var ddefServiceMock = GetDdefServiceMock(new Dictionary> + { + { ".test1", new[] {"path1.test1"}}, + { ".test2", Array.Empty() } + }); + + await DeployCommandHandler.DeployAsync( + m_Host.Object, new DeployInput { Reconcile = false , Services = new [] { "Test1", "Test2" } }, + m_UnityEnvironment.Object, + m_Logger.Object, + (StatusContext)null!, + ddefServiceMock.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + // Verify services are called / not + mockService1.Verify(serv1 => serv1 + .Deploy(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + + mockService2.Verify(serv2 => serv2 + .Deploy(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); + } + + [Test] + public async Task DeployDoesCallAllServices_OnReconcile() + { + var mockService1 = (Mock)CreateMockDeploymentService(".test1", "Test1"); + var mockService2 = (Mock)CreateMockDeploymentService(".test2", "Test2"); + m_Host.Reset(); + var collection = m_Bridge.CreateBuilder(new ServiceCollection()); + collection.AddScoped(_ => mockService1.Object); + collection.AddScoped(_ => mockService2.Object); + var provider = m_Bridge.CreateServiceProvider(collection); + m_Host.Setup(x => x.Services) + .Returns(provider); + + var ddefServiceMock = GetDdefServiceMock(new Dictionary> + { + { ".test1", new[] {"path1.test1"}}, + { ".test2", Array.Empty() } + }); + + await DeployCommandHandler.DeployAsync( + m_Host.Object, new DeployInput { Reconcile = true , Services = new [] { "Test1", "Test2" } }, + m_UnityEnvironment.Object, + m_Logger.Object, + (StatusContext)null!, + ddefServiceMock.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + // Verify services are called / not + mockService1.Verify(serv1 => serv1 + .Deploy(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + + mockService2.Verify(serv2 => serv2 + .Deploy(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + } + + [Test] + public async Task FetchDoesNotCallServiceNotReconcile() + { + var mockService1 = + (Mock)CreateMockFetchService(".test1", "Test1"); + var mockService2 = + (Mock)CreateMockFetchService(".test2", "Test2"); + m_Host.Reset(); + var collection = m_Bridge.CreateBuilder(new ServiceCollection()); + collection.AddScoped(_ => mockService1.Object); + collection.AddScoped(_ => mockService2.Object); + var provider = m_Bridge.CreateServiceProvider(collection); + m_Host.Setup(x => x.Services) + .Returns(provider); + + var ddefServiceMock = GetDdefServiceMock(new Dictionary> + { + { ".test1", new[] {"path1.test1"}}, + { ".test2", Array.Empty()} + }); + + await FetchCommandHandler.FetchAsync( + m_Host.Object, + new FetchInput() { Reconcile = false , Services = new [] { "Test1", "Test2" } }, + m_UnityEnvironment.Object, + m_Logger.Object, + (StatusContext)null!, + ddefServiceMock.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + // Verify services are called / not + mockService1.Verify(serv1 => serv1 + .FetchAsync(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + + mockService2.Verify(serv2 => serv2 + .FetchAsync(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Never + ); + } + + [Test] + public async Task FetchDoesCallAllServicesOnReconcile() + { + var mockService1 = (Mock)CreateMockFetchService(".test1", "Test1"); + var mockService2 = (Mock)CreateMockFetchService(".test2", "Test2"); + m_Host.Reset(); + var collection = m_Bridge.CreateBuilder(new ServiceCollection()); + collection.AddScoped(_ => mockService1.Object); + collection.AddScoped(_ => mockService2.Object); + var provider = m_Bridge.CreateServiceProvider(collection); + m_Host.Setup(x => x.Services) + .Returns(provider); + + var ddefServiceMock = GetDdefServiceMock(new Dictionary> + { + { ".test1", new[] {"path1.test1"}}, + { ".test2", Array.Empty()} + }); + + await FetchCommandHandler.FetchAsync( + m_Host.Object, + new FetchInput { Reconcile = true , Services = new [] { "Test1", "Test2" } }, + m_UnityEnvironment.Object, + m_Logger.Object, + (StatusContext)null!, + ddefServiceMock.Object, + m_AnalyticsEventBuilder.Object, + CancellationToken.None); + + // Verify services are called / not + mockService1.Verify(serv1 => serv1 + .FetchAsync(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + + mockService2.Verify(serv2 => serv2 + .FetchAsync(It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Once + ); + } + + static IMock CreateMockDeploymentService(string fileExtension, string serviceName) + where T : class, IDeploymentService + { + var deploymentService = new Mock(); + deploymentService.Setup(s => s.ServiceName) + .Returns(serviceName); + deploymentService.Setup(s => s.FileExtensions) + .Returns( + new[] + { + fileExtension + }); + + deploymentService.Setup( + s => s.Deploy( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + Task.FromResult( + new DeploymentResult( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty()))); + return deploymentService; + } + + static IMock CreateMockFetchService(string fileExtension, string serviceName) + where T : class, IFetchService + { + var fetchService = new Mock(); + fetchService.Setup(s => s.ServiceName) + .Returns(serviceName); + fetchService.Setup(s => s.FileExtensions) + .Returns( + new[] + { + fileExtension + }); + + fetchService.Setup( + s => s.FetchAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + Task.FromResult( + new FetchResult( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty()))); + return fetchService; + } + + static Mock GetDdefServiceMock(Dictionary> filesByExtension) + { + var ddefServiceMock = new Mock(); + ddefServiceMock + .Setup( + x => x.GetFilesFromInput( + It.IsAny>(), + It.IsAny>())) + .Returns( + new DeploymentDefinitionFilteringResult( + new DeploymentDefinitionFiles(), + filesByExtension)); + return ddefServiceMock; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/DeployHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/DeployHandlerTests.cs index 6322c92..24f5f66 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/DeployHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/DeployHandlerTests.cs @@ -57,8 +57,7 @@ public Task Deploy(DeployInput deployInput, IReadOnlyList(), new List(), new List(), - new List(), - false)); + new List())); } } @@ -90,8 +89,7 @@ public Task Deploy(DeployInput deployInput, IReadOnlyList() { new ("failure", "type", "path_2") - }, - false)); + })); } } @@ -99,7 +97,7 @@ public class TestDeploymentUnhandledExceptionService : IDeploymentService { string m_ServiceType = "Test"; string m_ServiceName = "test"; - string m_DeployFileExtension = ".test"; + string m_DeployFileExtension = ".test1"; public string ServiceType => m_ServiceType; public string ServiceName => m_ServiceName; @@ -113,7 +111,6 @@ public Task Deploy(DeployInput deployInput, IReadOnlyList(new NotImplementedException()); - } } @@ -172,7 +169,8 @@ public void SetUp() new DeploymentDefinitionFiles(), new Dictionary> { - { ".test", new List() } + { ".test", new List { "path.test"} }, + { ".test1", new List { "path1.test1"} } })); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/FetchHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/FetchHandlerTests.cs index d787478..d6f9c4c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/FetchHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Handlers/FetchHandlerTests.cs @@ -89,7 +89,8 @@ public void SetUp() new DeploymentDefinitionFiles(), new Dictionary> { - { ".test", new List() } + { ".test", new List { "path.test"} }, + { ".test1", new List { "path1.test1"} } })); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/DeploymentResultTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/DeploymentResultTests.cs index aad8e8f..8d7d6b1 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/DeploymentResultTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/DeploymentResultTests.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using Unity.Services.Cli.Authoring.Model; using Unity.Services.Cli.Authoring.Model.TableOutput; @@ -67,12 +65,10 @@ public void ToTableFormat() k_DeployedContents, k_FailedContents); var result = m_DeploymentResult.ToTable(); - var expected = TableContent.ToTable(k_DeployedContents[0]); + var expected = new TableContent(); - foreach (var failed in k_FailedContents) - { - expected.AddRow(RowContent.ToRow(failed)); - } + expected.AddRows(k_DeployedContents.Select(RowContent.ToRow).ToList()); + expected.AddRows(k_FailedContents.Select(RowContent.ToRow).ToList()); Assert.IsTrue(result.Result.Count == expected.Result.Count); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/FetchResultTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/FetchResultTests.cs index 24e37d8..6dc963f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/FetchResultTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/FetchResultTests.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; using NUnit.Framework; using Unity.Services.Cli.Authoring.Model; diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/TableOutput/TableTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/TableOutput/TableTests.cs index dd16945..0b5cf40 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/TableOutput/TableTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring.UnitTest/Model/TableOutput/TableTests.cs @@ -7,12 +7,8 @@ public class TableTests { TableContent m_Table = new TableContent(); - const string k_StartEntryResource = "Test"; - const string k_UpdatedEntryLocation = "NewDataValue"; - - - readonly RowContent m_StartTableRow = new RowContent(k_StartEntryResource); - readonly RowContent m_UpdatedTableRow = new RowContent(k_StartEntryResource, k_UpdatedEntryLocation); + readonly RowContent m_StartTableRow = new RowContent(); + readonly RowContent m_UpdatedTableRow = new RowContent(); [Test] public void UpdateRowsWorksCorrectly() diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/AuthoringHandlerCommon.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/AuthoringHandlerCommon.cs index af64c8f..c70e697 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/AuthoringHandlerCommon.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/AuthoringHandlerCommon.cs @@ -118,7 +118,7 @@ public static bool AreAllServicesSupported(AuthoringInput input, IReadOnlyList( AuthoringInput input, ILogger logger, - Task[] tasks, + AuthoringResultServiceTask[] tasks, T totalResult, IDeploymentDefinitionFilteringResult ddefResult) where T : AuthorResult { @@ -131,7 +131,7 @@ public static void PrintResult( foreach (var task in tasks) { - tableResult.AddRows(task.Result.ToTable()); + tableResult.AddRows(task.AuthorResultTask.Result.ToTable(task.ServiceType)); } logger.LogResultValue(tableResult); @@ -148,6 +148,7 @@ public static void PrintResult( // Get Exceptions from faulted deployments var exceptions = tasks + .Select(t => t.AuthorResultTask) .Where(t => t.IsFaulted) .Select(t => t.Exception?.InnerException) .ToList(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/DeployCommandHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/DeployCommandHandler.cs index 71e76f9..1a4977b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/DeployCommandHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/DeployCommandHandler.cs @@ -75,27 +75,39 @@ internal static async Task DeployAsync( var projectId = input.CloudProjectId!; var environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); - var tasks = deploymentServices - .Select( + var authoringResultServiceTask = deploymentServices + .Select>( service => { var filePaths = service.FileExtensions .SelectMany(extension => ddefResult.AllFilesByExtension[extension]) .ToArray(); - return service.Deploy( - input, - filePaths, - projectId, - environmentId, - loadingContext, - cancellationToken); + if (!input.Reconcile && !filePaths.Any()) + { + // nothing to do for this service + return new AuthoringResultServiceTask( + Task.FromResult(new DeploymentResult(Array.Empty())), + service.ServiceType); + } + + return new AuthoringResultServiceTask( + service.Deploy( + input, + filePaths, + projectId, + environmentId, + loadingContext, + cancellationToken), + service.ServiceType); }) .ToArray(); try { - await Task.WhenAll(tasks); + await Task.WhenAll( + authoringResultServiceTask + .Select(t => t.AuthorResultTask)); } catch { @@ -105,7 +117,8 @@ internal static async Task DeployAsync( } // Get Results from successfully ran deployments - var deploymentResults = tasks + var deploymentResults = authoringResultServiceTask + .Select(t => t.AuthorResultTask) .Where(t => t.IsCompletedSuccessfully) .Select(t => t.Result) .ToArray(); @@ -115,7 +128,7 @@ internal static async Task DeployAsync( AuthoringHandlerCommon.PrintResult( input, logger, - tasks, + authoringResultServiceTask, totalResult, ddefResult); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/FetchCommandHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/FetchCommandHandler.cs index b13310d..d95067d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/FetchCommandHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/FetchCommandHandler.cs @@ -54,7 +54,11 @@ internal static async Task FetchAsync( var services = host.Services.GetServices().ToList(); - if (!AuthoringHandlerCommon.PreActionValidation(input, logger, services, inputPaths)) + if (!AuthoringHandlerCommon.PreActionValidation( + input, + logger, + services, + inputPaths)) { return; } @@ -81,27 +85,39 @@ internal static async Task FetchAsync( var projectId = input.CloudProjectId!; var environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); - var tasks = fetchServices - .Select( + var authoringResultServiceTask = fetchServices + .Select>( service => { var filePaths = service.FileExtensions .SelectMany(extension => ddefResult.AllFilesByExtension[extension]) .ToArray(); - return service.FetchAsync( - input, - filePaths, - projectId, - environmentId, - loadingContext, - cancellationToken); + if (!input.Reconcile && !filePaths.Any()) + { + // nothing to do for this service + return new AuthoringResultServiceTask( + Task.FromResult(new FetchResult(Array.Empty())), + service.ServiceType); + } + + return new AuthoringResultServiceTask( + service.FetchAsync( + input, + filePaths, + projectId, + environmentId, + loadingContext, + cancellationToken), + service.ServiceType); }) .ToArray(); try { - fetchResult = await Task.WhenAll(tasks); + fetchResult = await Task.WhenAll( + authoringResultServiceTask + .Select(t => t.AuthorResultTask)); } catch { @@ -115,7 +131,7 @@ internal static async Task FetchAsync( AuthoringHandlerCommon.PrintResult( input, logger, - tasks, + authoringResultServiceTask, totalResult, ddefResult); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/NewFileHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/NewFileHandler.cs index 71ee4ff..24cfa30 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/NewFileHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Handlers/NewFileHandler.cs @@ -36,6 +36,13 @@ public static async Task NewFileAsync( { input.File = Path.ChangeExtension(input.File ?? defaultFileName, template.Extension); + var directoryPath = Path.GetDirectoryName(input.File); + if (!Directory.Exists(directoryPath) && !string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + logger.LogInformation("Directory {directoryPath} created successfully!", directoryPath); + } + if (file.Exists(input.File) && !input.UseForce) { logger.LogError($"A file with the name '{input.File}' already exists." + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthorResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthorResult.cs index 31004f9..790e346 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthorResult.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthorResult.cs @@ -1,6 +1,5 @@ using System.Text; using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using Unity.Services.Cli.Authoring.Model.TableOutput; using Unity.Services.DeploymentApi.Editor; @@ -24,17 +23,6 @@ public abstract class AuthorResult internal abstract string Operation { get; } - static AuthorResult() - { - JsonConvert.DefaultSettings = () => - { - return new JsonSerializerSettings() - { - ContractResolver = new DeployContentContractResolver() - }; - }; - } - /// /// All local resources modified by the fetch command (created, updated, and deleted). /// @@ -120,17 +108,17 @@ public override string ToString() return result.ToString(); } - public virtual TableContent ToTable() + public virtual TableContent ToTable(string service = "") { var table = new TableContent { IsDryRun = DryRun }; - table.AddRows(Updated.Select(TableContent.ToTable).ToList()); - table.AddRows(Deleted.Select(TableContent.ToTable).ToList()); - table.AddRows(Created.Select(TableContent.ToTable).ToList()); - table.AddRows(Failed.Select(TableContent.ToTable).ToList()); + table.AddRows(Updated.Select(i=> new RowContent(i, service)).ToList()); + table.AddRows(Deleted.Select(i=> new RowContent(i, service)).ToList()); + table.AddRows(Created.Select(i=> new RowContent(i, service)).ToList()); + table.AddRows(Failed.Select(i=> new RowContent(i, service)).ToList()); return table; } @@ -168,21 +156,4 @@ internal static void AppendResult(StringBuilder builder, IEnumerable CreateProperties(Type type, MemberSerialization memberSerialization) - { - if (!type.IsAssignableTo(typeof(IDeploymentItem))) - return base.CreateProperties(type, memberSerialization); - - IList properties = base.CreateProperties(type, memberSerialization); - var propertiesToInclude = typeof(DeployContent).GetProperties().Select(p => p.Name).ToList(); - - properties = - properties.Where(p => propertiesToInclude.Contains( p.PropertyName!) ).ToList(); - - return properties; - } - } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthoringResultServiceTask.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthoringResultServiceTask.cs new file mode 100644 index 0000000..1773ea7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/AuthoringResultServiceTask.cs @@ -0,0 +1,14 @@ +namespace Unity.Services.Cli.Authoring.Model; + +public class AuthoringResultServiceTask + where T : AuthorResult +{ + public Task AuthorResultTask { get; } + public string ServiceType { get; } + + public AuthoringResultServiceTask(Task authorResultTask, string serviceType) + { + AuthorResultTask = authorResultTask; + ServiceType = serviceType; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/DeploymentResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/DeploymentResult.cs index b436bd0..28d38f4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/DeploymentResult.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/DeploymentResult.cs @@ -14,9 +14,7 @@ public DeploymentResult( IReadOnlyList authored, IReadOnlyList failed, bool dryRun = false) - : base(updated, deleted, created, authored, failed, dryRun) - { - } + : base(updated, deleted, created, authored, failed, dryRun) { } public DeploymentResult(IReadOnlyList results, bool dryRun = false) : base(results, dryRun) { } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/FetchResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/FetchResult.cs index 93ffbaf..07248ea 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/FetchResult.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/FetchResult.cs @@ -16,13 +16,9 @@ public FetchResult( IReadOnlyList authored, IReadOnlyList failed, bool dryRun = false) - : base(updated, deleted, created, authored, failed, dryRun) - { - } + : base(updated, deleted, created, authored, failed, dryRun) { } - public FetchResult(IReadOnlyList results, bool dryRun = false) : base(results, dryRun) - { - } + public FetchResult(IReadOnlyList results, bool dryRun = false) : base(results, dryRun) { } internal override string Operation => "fetch"; } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/RowContent.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/RowContent.cs index 474f7f7..f81bd7d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/RowContent.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/RowContent.cs @@ -5,28 +5,37 @@ namespace Unity.Services.Cli.Authoring.Model.TableOutput; [Serializable] public class RowContent { - public string Name { get; protected set; } - public string Type { get; protected set; } - public string Status{ get; protected set; } - public string Details{ get; protected set; } - public string Path{ get; protected set; } + public string Name { get; protected set; } = ""; + public string Service { get; protected set; } = ""; + public string Type { get; protected set; } = ""; + public string Status { get; protected set; } = ""; + public string Details { get; protected set; } = ""; + public string Severity { get; protected set; } = ""; + public string Path { get; protected set; } = ""; - public RowContent(string name = "", string type = "", string status = "", string details = "", string path = "") + public RowContent() { } + + public RowContent(IDeploymentItem item, string service) { - Name = name; - Type = type; - Status = status; - Details = details; - Path = path; + Name = item.Name; + Service = service; + Type = ((ITypedItem)item).Type; + Status = item.Status.Message; + Details = item.Status.MessageDetail; + Severity = item.Status.MessageSeverity.ToString(); + Path = item.Path; } public static RowContent ToRow(IDeploymentItem item) { - return new RowContent( - item.Name, - ((ITypedItem)item).Type, - item.Status.Message, - item.Status.MessageDetail, - item.Path); + return new RowContent + { + Name = item.Name, + Type = ((ITypedItem)item).Type, + Status = item.Status.Message, + Details = item.Status.MessageDetail, + Path = item.Path, + Severity = item.Status.MessageSeverity.ToString() + }; } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/TableContent.cs b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/TableContent.cs index 5f01e8a..475bd46 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/TableContent.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Authoring/Model/TableOutput/TableContent.cs @@ -1,4 +1,5 @@ using Unity.Services.DeploymentApi.Editor; +using Newtonsoft.Json; namespace Unity.Services.Cli.Authoring.Model.TableOutput; @@ -62,13 +63,4 @@ void UpdateOrAddRow(RowContent item) AddRow(item); } } - - public static TableContent ToTable(IDeploymentItem item) - { - return new TableContent( - new[] - { - RowContent.ToRow(item) - }); - } } 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 22158ca..596fcc5 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 @@ -6,6 +6,7 @@ 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.Authoring; using Unity.Services.Cli.CloudCode.Deploy; @@ -289,4 +290,87 @@ public void DeployAsync_DoesNotThrowOnApiException() null!, CancellationToken.None)); } + + [Test] + public async Task DeployAsync_ErrorsBubbledUp() + { + CloudCodeInput input = new() + { + CloudProjectId = TestValues.ValidProjectId, + Paths = k_ValidCcmFilePaths, + }; + + IScript myModule = new Unity.Services.Cli.CloudCode.Deploy.CloudCodeModule( + new ScriptName("module.ccm"), + Language.CS, + "modules"); + + m_MockCloudCodeModulesLoader.Reset(); + + m_MockDeployFileService.Setup( + c => c.ListFilesToDeploy( + k_ValidCcmFilePaths, + CloudCodeConstants.FileExtensionModulesCcm, + false)) + .Returns(k_ValidCcmFilePaths); + + m_MockDeployFileService.Setup( + c => c.ListFilesToDeploy( + It.IsAny>(), + CloudCodeConstants.FileExtensionModulesSln, + false)) + .Returns(new Collection()); + + var loadedResult = new List + { + myModule + }; + + m_MockCloudCodeModulesLoader + .Setup( + c => c.LoadModulesAsync( + k_ValidCcmFilePaths, + It.IsAny>(), + It.IsAny())) + .ReturnsAsync( + () => + { + return (loadedResult, new List()); + }); + + var expectedResult = new DeployResult( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + new [] { new CloudCodeModuleScript("a.ccm") {Status = DeploymentStatus.FailedToDeploy } } + ); + + m_DeploymentHandler.Setup( + d => d.DeployAsync( + It.IsAny>(), + It.IsAny(), + false)) + .ThrowsAsync( + new DeploymentException( + new[] { new Exception() }, + expectedResult)); + + var result = await m_DeploymentService!.Deploy( + input, + k_ValidCcmFilePaths, + TestValues.ValidProjectId, + TestValues.ValidEnvironmentId, + null!, + CancellationToken.None); + + m_DeploymentHandler.Verify(d => d.DeployAsync( + It.IsAny>(), + It.IsAny(), + false), + Times.Once); + + Assert.AreEqual( 1, result.Failed.Count); + Assert.AreEqual( SeverityLevel.Error, result.Failed[0].Status.MessageSeverity); + } } 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 bc3e611..aad833f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModuleDeploymentService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.CloudCode/Deploy/CloudCodeModuleDeploymentService.cs @@ -1,5 +1,3 @@ -using System.Collections.ObjectModel; -using System.ComponentModel; using Spectre.Console; using Unity.Services.Cli.Authoring.Input; using Unity.Services.Cli.Authoring.Model; @@ -8,9 +6,6 @@ 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; using Unity.Services.Gateway.CloudCodeApiV1.Generated.Client; @@ -72,7 +67,7 @@ public async Task Deploy( loadingContext?.Status($"Loading {m_ServiceName} modules..."); - var (loadedModules, failedModules) = + var (loadedModules, failedToLoadModules) = await CloudCodeModulesLoader.LoadModulesAsync(ccmFilePaths, slnFilePaths, cancellationToken); loadingContext?.Status($"Deploying {m_ServiceType}..."); @@ -84,7 +79,6 @@ public async Task Deploy( try { result = await CloudCodeDeploymentHandler.DeployAsync(loadedModules, reconcile, dryrun); - failedModules.AddRange(result.Failed); } catch (ApiException) { @@ -98,7 +92,7 @@ public async Task Deploy( result = ex.Result; } - return ConstructResult(loadedModules, result, deployInput, failedModules); + return ConstructResult(loadedModules, result, deployInput, failedToLoadModules); } (List, List) ListFilesToDeploy(List filePaths) @@ -120,16 +114,17 @@ public async Task Deploy( return (ccmFilePaths, slnFilePaths); } - static IDeploymentItem SetPathAsSolutionWhenAvailable(IScript item) + static void SetPathAsSolutionWhenAvailable(IReadOnlyList items) { - return new CloudCodeModule( - item.Name.ToString(), - string.IsNullOrEmpty(((CloudCodeModule)item).SolutionPath) ? ((CloudCodeModule)item).Path : ((CloudCodeModule)item).SolutionPath, - ((CloudCodeModule)item).Progress, - ((CloudCodeModule)item).Status); + foreach (var item in items) + { + var ccm = (CloudCodeModule)item; + if (!string.IsNullOrEmpty(ccm.SolutionPath)) + ccm.Path = ccm.SolutionPath; + } } - static DeploymentResult ConstructResult(List loadResult, DeployResult? result, DeployInput deployInput, List failedModules) + static DeploymentResult ConstructResult(List loadResult, DeployResult? result, DeployInput deployInput, List failedToLoad) { DeploymentResult deployResult; if (result == null) @@ -138,12 +133,18 @@ static DeploymentResult ConstructResult(List loadResult, DeployResult? } else { + var failed = result.Failed.Concat(failedToLoad).ToList(); + SetPathAsSolutionWhenAvailable(result.Failed); + SetPathAsSolutionWhenAvailable(result.Created); + SetPathAsSolutionWhenAvailable(result.Updated); + SetPathAsSolutionWhenAvailable(failed); + deployResult = new DeploymentResult( - result.Updated.Select(SetPathAsSolutionWhenAvailable).ToList() as IReadOnlyList, + result.Updated.Cast().ToList(), ToDeleteDeploymentItems(result.Deleted, deployInput.DryRun), - result.Created.Select(SetPathAsSolutionWhenAvailable).ToList() as IReadOnlyList, - result.Deployed.Select(SetPathAsSolutionWhenAvailable).ToList() as IReadOnlyList, - failedModules.Select(SetPathAsSolutionWhenAvailable).ToList() as IReadOnlyList, + result.Created.Cast().ToList(), + result.Deployed.Cast().ToList(), + failed.Cast().ToList(), deployInput.DryRun); } 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 7d21f79..a85ecfe 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/Batching/Batching.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Batching/Batching.cs new file mode 100644 index 0000000..8942f1a --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Batching/Batching.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Unity.Services.ModuleTemplate.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."; + + /// + /// Asynchronously execute a collection of delegates in batches with delay between them + /// + /// IEnumerable of the delegates you want to run in batches + /// Callback that will be invoked when a delegate has finished executing + /// + /// Size of the batches + /// Delay in seconds between batches + /// Exception thrown when a delegate throws an exception + /// You need to handle the AggregateException's innerExceptions (that's where you'll get + /// the exceptions related to the individual batch items executed) + public static async Task ExecuteInBatchesAsync( + IEnumerable> delegates, + CancellationToken cancellationToken, + int batchSize = k_BatchSize, + double secondsDelay = k_SecondsDelay) + { + var newDelegates = new List>>(); + + foreach (var del in delegates) + { + newDelegates.Add( + async () => + { + await Task.Run(del, cancellationToken); + return 0; + }); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + } + + await ExecuteInBatchesAsync( + newDelegates, + cancellationToken, + batchSize, + secondsDelay); + } + + /// + /// Asynchronously execute a collection of delegates in batches with delay between them + /// + /// IEnumerable of the delegates you want to run in batches + /// Callback that will be invoked when a delegate has finished executing + /// + /// Size of the batches + /// Delay in seconds between batches + /// The return value type of your delegates + /// A collection of results + /// Exception thrown when a delegate throws an exception + /// You need to handle the AggregateException's innerExceptions (that's where you'll get + /// the exceptions related to the individual batch items executed) + public static async Task> ExecuteInBatchesAsync( + IReadOnlyList>> delegates, + CancellationToken cancellationToken, + int batchSize = k_BatchSize, + double secondsDelay = k_SecondsDelay) + { + var exceptions = new ConcurrentQueue(); + var batchesResult = new ConcurrentBag(); + var chunks = delegates.Chunk(batchSize).ToList(); + + for (int i = 0; i < chunks.Count; i++) + { + try + { + var batchResult = await ExecuteBatchAsync(chunks[i], cancellationToken); + foreach (var result in batchResult) + { + batchesResult.Add(result); + } + } + catch (AggregateException e) + { + foreach (var innerException in e.InnerExceptions) + { + exceptions.Enqueue(innerException); + } + } + + if (i + 1 != chunks.Count) + { + await Task.Delay(TimeSpan.FromSeconds(secondsDelay), cancellationToken); + } + + if (cancellationToken.IsCancellationRequested) + { + break; + } + } + + if (!exceptions.IsEmpty) + { + throw new AggregateException(k_BatchingExceptionMessage, exceptions.ToList()); + } + + return batchesResult.ToList(); + } + + static async Task> ExecuteBatchAsync( + IEnumerable>> delegates, + CancellationToken cancellationToken) + { + var exceptions = new ConcurrentQueue(); + var batchesResult = new ConcurrentBag(); + var tasks = new ConcurrentBag(); + + Parallel.ForEach( + delegates, + del => + { + var task = Task.Run( + async () => + { + try + { + var result = await Task.Run(del, cancellationToken); + batchesResult.Add(result); + } + catch (Exception e) + { + exceptions.Enqueue(e); + } + }, + cancellationToken); + + tasks.Add(task); + }); + + await Task.WhenAll(tasks); + + if (!exceptions.IsEmpty) + { + throw new AggregateException(k_BatchingExceptionMessage, exceptions.ToList()); + } + + return batchesResult.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(); + } + + // Copy/pasted utility code from the Enumerable.Chunk method available in dotnet 5 + static IEnumerable ChunkIterator(IEnumerable source, int size) + { + using IEnumerator e = source.GetEnumerator(); + while (e.MoveNext()) + { + TSource[] chunk = new TSource[size]; + chunk[0] = e.Current; + + int i = 1; + for (; i < chunk.Length && e.MoveNext(); i++) + { + chunk[i] = e.Current; + } + + if (i == chunk.Length) + { + yield return chunk; + } + else + { + Array.Resize(ref chunk, i); + yield return chunk; + yield break; + } + } + } + + static IEnumerable Chunk(this IEnumerable source, int size) + => ChunkIterator(source, size); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Models/Configuration.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Models/Configuration.cs index 67e4415..16014bd 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Models/Configuration.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Models/Configuration.cs @@ -12,6 +12,9 @@ public class Configuration [JsonProperty(Keys.ConfigKeys.ProjectId)] public string? CloudProjectId { get; set; } + [JsonProperty(Keys.ConfigKeys.BucketId)] + public string? CloudBucketId { get; set; } + public string? GetValue(string key) { return GetType() diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Models/Keys.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Models/Keys.cs index aab0dc8..ce2b24d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Models/Keys.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Models/Keys.cs @@ -26,8 +26,8 @@ public static class ConfigKeys public const string EnvironmentName = "environment-name"; // Environment Id is currently not stored/retrieved but the key is used for validation purposes public const string EnvironmentId = "environment-id"; - - public static readonly IReadOnlyList Keys = new List { ProjectId, EnvironmentName }; + public const string BucketId = "bucket-id"; + public static readonly IReadOnlyList Keys = new List { ProjectId, EnvironmentName, BucketId }; } /// @@ -37,6 +37,7 @@ public static class EnvironmentKeys { public const string ProjectId = "UGS_CLI_PROJECT_ID"; public const string EnvironmentName = "UGS_CLI_ENVIRONMENT_NAME"; + public const string BucketId = "UGS_CLI_BUCKET_ID"; public const string TelemetryDisabled = "UGS_CLI_TELEMETRY_DISABLED"; // Env variables used for identifying the cicd platform being used public const string RunningOnDocker = "DOTNET_RUNNING_IN_CONTAINER"; diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Validator/ConfigurationValidator.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Validator/ConfigurationValidator.cs index 9a1496f..ec9de29 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Validator/ConfigurationValidator.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Configuration/Validator/ConfigurationValidator.cs @@ -39,6 +39,8 @@ public bool IsConfigValid(string key, string? value, out string errorMessage) return IsEnvironmentNameValid(value, out errorMessage); case Keys.ConfigKeys.ProjectId: return IsProjectIdValid(value, out errorMessage); + case Keys.ConfigKeys.BucketId: + return IsProjectIdValid(value, out errorMessage); default: errorMessage = InvalidKeyMsg; return false; diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs index 96e4d14..2b10b7c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Exceptions/ExceptionHelper.cs @@ -7,13 +7,14 @@ using Unity.Services.Cli.Common.Telemetry.AnalyticEvent; using IdentityApiException = Unity.Services.Gateway.IdentityApiV1.Generated.Client.ApiException; using CloudCodeApiException = Unity.Services.Gateway.CloudCodeApiV1.Generated.Client.ApiException; +using SchedulerApiException = Unity.Services.Gateway.SchedulerApiV1.Generated.Client.ApiException; +using CloudContentDeliveryApiException = Unity.Services.Gateway.ContentDeliveryManagementApiV1.Generated.Client.ApiException; using EconomyApiException = Unity.Services.Gateway.EconomyApiV2.Generated.Client.ApiException; using LobbyApiException = Unity.Services.MpsLobby.LobbyApiV1.Generated.Client.ApiException; using LeaderboardApiException = Unity.Services.Gateway.LeaderboardApiV1.Generated.Client.ApiException; using PlayerAdminApiException = Unity.Services.Gateway.PlayerAdminApiV3.Generated.Client.ApiException; using PlayerAuthException = Unity.Services.Gateway.PlayerAuthApiV1.Generated.Client.ApiException; using HostingApiException = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client.ApiException; -using SentisApiException = Unity.Services.Gateway.SentisApiV1.Generated.Client.ApiException; namespace Unity.Services.Cli.Common.Exceptions; @@ -68,6 +69,12 @@ public int HandleException(Exception exception, ILogger logger, InvocationContex case CloudCodeApiException cloudCodeApiException: HandleApiException(exception, logger, cloudCodeApiException.ErrorCode); break; + case SchedulerApiException schedulerApiException: + HandleApiException(exception, logger, schedulerApiException.ErrorCode); + break; + case CloudContentDeliveryApiException cloudContentDeliveryApiException: + HandleApiException(exception, logger, cloudContentDeliveryApiException.ErrorCode); + break; case EconomyApiException economyApiException: HandleApiException(exception, logger, economyApiException.ErrorCode); break; @@ -83,9 +90,6 @@ public int HandleException(Exception exception, ILogger logger, InvocationContex case PlayerAuthException playerAuthApiException: HandleApiException(exception, logger, playerAuthApiException.ErrorCode); break; - case SentisApiException sentiApiException: - HandleApiException(exception, logger, sentiApiException.ErrorCode); - break; case AggregateException aggregateException: foreach (var ex in aggregateException.InnerExceptions) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/CloudContentDeliveryEndpoints.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/CloudContentDeliveryEndpoints.cs new file mode 100644 index 0000000..c7ab44b --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/CloudContentDeliveryEndpoints.cs @@ -0,0 +1,8 @@ +namespace Unity.Services.Cli.Common.Networking; + +public class CloudContentDeliveryApiEndpoints : NetworkTargetEndpoints +{ + protected override string Prod { get; } = "https://services.api.unity.com"; + + protected override string Staging { get; } = "https://staging.services.api.unity.com"; +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/CloudSaveEndpoints.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/CloudSaveEndpoints.cs new file mode 100644 index 0000000..c9d15c8 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/CloudSaveEndpoints.cs @@ -0,0 +1,8 @@ +namespace Unity.Services.Cli.Common.Networking; + +public class CloudSaveEndpoints : NetworkTargetEndpoints +{ + protected override string Prod { get; } = "https://services.api.unity.com"; + + protected override string Staging { get; } = "https://staging.services.api.unity.com"; +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/SchedulerEndpoints.cs b/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/SchedulerEndpoints.cs new file mode 100644 index 0000000..dbf1c85 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Common/Networking/Endpoints/SchedulerEndpoints.cs @@ -0,0 +1,8 @@ +namespace Unity.Services.Cli.Common.Networking; + +public class SchedulerEndpoints : NetworkTargetEndpoints +{ + protected override string Prod { get; } = "https://services.api.unity.com"; + + protected override string Staging { get; } = "https://staging.services.api.unity.com"; +} 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 d19c76d..837e43e 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 @@ -16,15 +16,16 @@ + - + - + @@ -35,11 +36,6 @@ <_Parameter1>DynamicProxyGenAssembly2 - - - Batching\Batching.cs - - $(DefineConstants);$(ExtraDefineConstants) diff --git a/Unity.Services.Cli/Unity.Services.Cli.Economy.UnitTest/Authoring/EconomyResourcesLoaderTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Economy.UnitTest/Authoring/EconomyResourcesLoaderTests.cs index eaa56f1..94c3dcf 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Economy.UnitTest/Authoring/EconomyResourcesLoaderTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Economy.UnitTest/Authoring/EconomyResourcesLoaderTests.cs @@ -207,17 +207,16 @@ public void ConstructResourceFile_ReturnsCorrectResourceFile() { m_MockEconomyJsonConverter!.Setup( x => x.SerializeObject( - It.IsAny(), - It.IsAny() + It.IsAny() )) .Returns(""); Assert.DoesNotThrow( - () => m_EconomyResourcesLoader!.ConstructResourceFile( + () => m_EconomyResourcesLoader!.CreateAndSerialize( resource)); m_MockEconomyJsonConverter.Verify( - x => x.SerializeObject(It.IsAny(), It.IsAny()), + x => x.SerializeObject(It.IsAny()), Times.Once); m_MockEconomyJsonConverter.Reset(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/EconomyResourcesLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/EconomyResourcesLoader.cs index d9ce08c..8ccd0c4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/EconomyResourcesLoader.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/EconomyResourcesLoader.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; using Unity.Services.Cli.Common.Exceptions; -using Unity.Services.Cli.Economy.Authoring.IO; using Unity.Services.Cli.Economy.Templates; using Unity.Services.DeploymentApi.Editor; using Unity.Services.Economy.Editor.Authoring.Core.IO; @@ -22,7 +21,7 @@ public EconomyResourcesLoader( m_FileSystem = fileSystem; } - public string ConstructResourceFile(IEconomyResource resource) + public string CreateAndSerialize(IEconomyResource resource) { EconomyResourceFile? resourceFile = null; @@ -79,7 +78,7 @@ public string ConstructResourceFile(IEconomyResource resource) throw new JsonSerializationException($"Error - {resource.Id} is not a valid resource."); } - return m_EconomyJsonConverter.SerializeObject(resourceFile, EconomyResourceFile.GetSerializationSettings()); + return m_EconomyJsonConverter.SerializeObject(resourceFile); } public async Task LoadResourceAsync( @@ -126,7 +125,6 @@ public async Task LoadResourceAsync( return resource; } - IEconomyResource LoadEconomyRealMoneyPurchaseResource(string path, string fileId, string resourceFileText) { var economyFile = m_EconomyJsonConverter.DeserializeObject(resourceFileText); diff --git a/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/IO/EconomyJsonConverter.cs b/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/IO/EconomyJsonConverter.cs index c8b1cb5..150a9dc 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/IO/EconomyJsonConverter.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/IO/EconomyJsonConverter.cs @@ -1,4 +1,6 @@ using Newtonsoft.Json; +using Unity.Services.Cli.Economy.Templates; +using Unity.Services.Economy.Editor.Authoring.Core.IO; namespace Unity.Services.Cli.Economy.Authoring.IO; @@ -9,8 +11,8 @@ class EconomyJsonConverter : IEconomyJsonConverter return JsonConvert.DeserializeObject(value); } - public string SerializeObject(object? value, JsonSerializerSettings? settings) + public string SerializeObject(object? value) { - return JsonConvert.SerializeObject(value, settings); + return JsonConvert.SerializeObject(value, EconomyResourceFile.GetSerializationSettings()); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/IO/IEconomyJsonConverter.cs b/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/IO/IEconomyJsonConverter.cs deleted file mode 100644 index 2ea767f..0000000 --- a/Unity.Services.Cli/Unity.Services.Cli.Economy/Authoring/IO/IEconomyJsonConverter.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace Unity.Services.Cli.Economy.Authoring.IO; - -interface IEconomyJsonConverter -{ - public T? DeserializeObject(string value); - - public string SerializeObject(object? value, JsonSerializerSettings? settings); -} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Economy/Unity.Services.Cli.Economy.csproj b/Unity.Services.Cli/Unity.Services.Cli.Economy/Unity.Services.Cli.Economy.csproj index 306576c..8b45cf4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Economy/Unity.Services.Cli.Economy.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Economy/Unity.Services.Cli.Economy.csproj @@ -25,7 +25,7 @@ - + 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 4f79455..61e5fa3 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingModuleTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingModuleTests.cs @@ -178,6 +178,7 @@ public void ExceptionFactory_CreateException(HttpStatusCode statusCode, string e [TestCase(typeof(IMultiplayBuildAuthoring))] [TestCase(typeof(IBinaryBuilder))] [TestCase(typeof(IBuildFileManagement))] + [TestCase(typeof(ICoreDumpApiAsync))] public void GameServerHostingModule_RegistersServices(Type serviceType) { var types = new List 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 39b4ce6..3d531ac 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingUnitTestsConstants.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/GameServerHostingUnitTestsConstants.cs @@ -13,6 +13,8 @@ public static class GameServerHostingUnitTestsConstants public const string InvalidBuildId = "1234"; public const string ValidBucketId = "11111000-0000-0000-0000-000000000000"; public const string ValidReleaseId = "00000000-1100-0000-0000-000000000000"; + public const string ValidBuildVersionName = "11.23.45-beta.4"; + public const string InValidBuildVersionName = "invalid"; public const string ValidContainerTag = "v1"; // Build File constants @@ -70,7 +72,8 @@ 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 ValidUsageSettingsJson = + "{\"hardwareType\":\"CLOUD\", \"machineType\":\"GCP-N2\", \"maxServersPerMachine\":5}"; public const string ValidOutputDirectory = "test/output"; @@ -81,4 +84,13 @@ public static class GameServerHostingUnitTestsConstants public const string ValidMachineCpuSeriesShortname = "U1.Standard.3"; public const string ValidMachineCpuType = "Cloud Intel 2nd Gen Scalable"; + + // Core Dump specific constants + public const string CoreDumpMockFleetIdWithoutConfig = "00000000-0000-0000-000c-000000000000"; + public const string CoreDumpMockFleetIdWithEnabledConfig = "00000000-0000-0000-000c-000000000001"; + public const string CoreDumpMockFleetIdWithDisabledConfig = "00000000-0000-0000-000c-000000000002"; + public const string CoreDumpMockValidBucketName = "testBucket"; + public const string CoreDumpMockValidAccessId = "testAccessId"; + public const string CoreDumpMockValidPrivateKey = "testPrivateKey"; + public const string CoreDumpMockValidCredentialsFilePath = "testFile.json"; } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildConfigurationCreateHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildConfigurationCreateHandlerTests.cs index 578bb97..aecb1e3 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildConfigurationCreateHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildConfigurationCreateHandlerTests.cs @@ -47,7 +47,8 @@ public async Task BuildConfigurationCreateAsync_CallsFetchIdentifierAsync() Memory = 100, Name = "test-build-config", QueryType = "none", - Speed = 100 + Speed = 100, + Readiness = true }; await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( @@ -71,6 +72,7 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( ValidBuildConfigurationName, ValidBuildConfigurationQueryType, 100, + true, TestName = "Missing BinaryPath")] [TestCase( ValidBuildConfigurationBinaryPath, @@ -82,6 +84,7 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( ValidBuildConfigurationName, ValidBuildConfigurationQueryType, 100, + true, TestName = "Missing BuildId")] [TestCase( ValidBuildConfigurationBinaryPath, @@ -93,6 +96,7 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( ValidBuildConfigurationName, ValidBuildConfigurationQueryType, 100, + true, TestName = "Missing CommandLine")] [TestCase( ValidBuildConfigurationBinaryPath, @@ -104,6 +108,7 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( ValidBuildConfigurationName, ValidBuildConfigurationQueryType, 100, + true, TestName = "Missing Configuration")] [TestCase( ValidBuildConfigurationBinaryPath, @@ -115,6 +120,7 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( ValidBuildConfigurationName, ValidBuildConfigurationQueryType, 100, + true, TestName = "Missing Cores")] [TestCase( ValidBuildConfigurationBinaryPath, @@ -126,6 +132,7 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( ValidBuildConfigurationName, ValidBuildConfigurationQueryType, 100, + true, TestName = "Missing Memory")] [TestCase( ValidBuildConfigurationBinaryPath, @@ -137,6 +144,7 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( null, ValidBuildConfigurationQueryType, 100, + true, TestName = "Missing Name")] [TestCase( ValidBuildConfigurationBinaryPath, @@ -148,6 +156,7 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( ValidBuildConfigurationName, null, 100, + true, TestName = "Missing QueryType")] [TestCase( ValidBuildConfigurationBinaryPath, @@ -159,7 +168,9 @@ await BuildConfigurationCreateHandler.BuildConfigurationCreateAsync( ValidBuildConfigurationName, ValidBuildConfigurationQueryType, null, - TestName = "Missing Speed")] + true, + TestName = "Missing Speed"), + ] public Task BuildConfigurationCreateAsync_NullInputThrowsException( string? binaryPath, long? buildId, @@ -169,7 +180,8 @@ public Task BuildConfigurationCreateAsync_NullInputThrowsException( long? memory, string? name, string? queryType, - long? speed) + long? speed, + bool? readiness) { BuildConfigurationCreateInput input = new() { @@ -188,7 +200,8 @@ public Task BuildConfigurationCreateAsync_NullInputThrowsException( Memory = memory, Name = name, QueryType = queryType, - Speed = speed + Speed = speed, + Readiness = readiness }; Assert.ThrowsAsync( @@ -227,7 +240,8 @@ public Task BuildConfigurationCreateAsync_InvalidConfigurationInputThrowsExcepti Memory = 100, Name = ValidBuildConfigurationName, QueryType = ValidBuildConfigurationQueryType, - Speed = 100 + Speed = 100, + Readiness = true }; Assert.ThrowsAsync( diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateHandlerTests.cs index 2823bf0..ef7a018 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateHandlerTests.cs @@ -2,6 +2,7 @@ using Moq; using Spectre.Console; using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.Common.Logging; using Unity.Services.Cli.GameServerHosting.Exceptions; using Unity.Services.Cli.GameServerHosting.Handlers; @@ -21,11 +22,18 @@ public async Task BuildCreateAsync_CallsLoadingIndicatorStartLoading() { var mockLoadingIndicator = new Mock(); - await BuildCreateHandler.BuildCreateAsync(null!, MockUnityEnvironment.Object, null!, null!, - mockLoadingIndicator.Object, CancellationToken.None); + await BuildCreateHandler.BuildCreateAsync( + null!, + MockUnityEnvironment.Object, + null!, + null!, + mockLoadingIndicator.Object, + CancellationToken.None); - mockLoadingIndicator.Verify(ex => ex - .StartLoadingAsync(It.IsAny(), It.IsAny>()), Times.Once); + mockLoadingIndicator.Verify( + ex => ex + .StartLoadingAsync(It.IsAny(), It.IsAny>()), + Times.Once); } [Test] @@ -51,9 +59,24 @@ await BuildCreateHandler.BuildCreateAsync( MockUnityEnvironment.Verify(ex => ex.FetchIdentifierAsync(CancellationToken.None), Times.Once); } - [TestCase(ValidProjectId, ValidEnvironmentName, null, OsFamilyEnum.LINUX, BuildTypeEnum.FILEUPLOAD)] - [TestCase(ValidProjectId, ValidEnvironmentName, null, OsFamilyEnum.LINUX, BuildTypeEnum.CONTAINER)] - [TestCase(ValidProjectId, ValidEnvironmentName, null, OsFamilyEnum.LINUX, BuildTypeEnum.S3)] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + null, + OsFamilyEnum.LINUX, + BuildTypeEnum.FILEUPLOAD)] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + null, + OsFamilyEnum.LINUX, + BuildTypeEnum.CONTAINER)] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + null, + OsFamilyEnum.LINUX, + BuildTypeEnum.S3)] public void BuildCreateAsync_NullBuildNameThrowsException( string projectId, string environmentName, @@ -71,31 +94,60 @@ BuildTypeEnum buildType BuildType = buildType }; - Assert.ThrowsAsync(() => - BuildCreateHandler.BuildCreateAsync( - input, - MockUnityEnvironment.Object, - GameServerHostingService!, - MockLogger!.Object, - CancellationToken.None - ) + Assert.ThrowsAsync( + () => + BuildCreateHandler.BuildCreateAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) ); - BuildsApi!.DefaultBuildsClient.Verify(api => api.CreateBuildAsync( - It.IsAny(), It.IsAny(), - It.IsAny(), 0, CancellationToken.None - ), Times.Never); + BuildsApi!.DefaultBuildsClient.Verify( + api => api.CreateBuildAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + 0, + CancellationToken.None + ), + Times.Never); - TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); + TestsHelper.VerifyLoggerWasCalled( + MockLogger!, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Never); } - [TestCase(ValidProjectId, ValidEnvironmentName, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.FILEUPLOAD)] - [TestCase(ValidProjectId, ValidEnvironmentName, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.CONTAINER)] - [TestCase(ValidProjectId, ValidEnvironmentName, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.S3)] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + ValidBuildName, + ValidBuildVersionName, + OsFamilyEnum.LINUX, + BuildTypeEnum.FILEUPLOAD)] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + ValidBuildName, + ValidBuildVersionName, + OsFamilyEnum.LINUX, + BuildTypeEnum.CONTAINER)] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + ValidBuildName, + ValidBuildVersionName, + OsFamilyEnum.LINUX, + BuildTypeEnum.S3)] public async Task BuildCreateAsync_CallsGetService( string projectId, string environmentName, string buildName, + string buildVersionName, OsFamilyEnum osFamily, BuildTypeEnum buildType ) @@ -106,7 +158,8 @@ BuildTypeEnum buildType TargetEnvironmentName = environmentName, BuildName = buildName, BuildOsFamily = osFamily, - BuildType = buildType + BuildType = buildType, + BuildVersionName = buildVersionName }; await BuildCreateHandler.BuildCreateAsync( @@ -117,24 +170,94 @@ await BuildCreateHandler.BuildCreateAsync( CancellationToken.None ); - BuildsApi!.DefaultBuildsClient.Verify(api => api.CreateBuildAsync( - new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), - new CreateBuildRequest(buildName, buildType, null!, osFamily), 0, CancellationToken.None - ), Times.Once); + BuildsApi!.DefaultBuildsClient.Verify( + api => api.CreateBuildAsync( + new Guid(input.CloudProjectId), + new Guid(ValidEnvironmentId), + new CreateBuildRequest( + buildName, + buildType, + buildVersionName, + null!, + osFamily), + 0, + CancellationToken.None + ), + Times.Once); } - [TestCase(InvalidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.FILEUPLOAD)] - [TestCase(ValidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.FILEUPLOAD)] - [TestCase(InvalidProjectId, ValidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.FILEUPLOAD)] - [TestCase(InvalidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.FILEUPLOAD)] - [TestCase(InvalidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.CONTAINER)] - [TestCase(ValidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.CONTAINER)] - [TestCase(InvalidProjectId, ValidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.CONTAINER)] - [TestCase(InvalidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.CONTAINER)] - [TestCase(InvalidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.S3)] - [TestCase(ValidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.S3)] - [TestCase(InvalidProjectId, ValidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.S3)] - [TestCase(InvalidProjectId, InvalidEnvironmentId, ValidBuildName, OsFamilyEnum.LINUX, BuildTypeEnum.S3)] + [TestCase( + InvalidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.FILEUPLOAD)] + [TestCase( + ValidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.FILEUPLOAD)] + [TestCase( + InvalidProjectId, + ValidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.FILEUPLOAD)] + [TestCase( + InvalidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.FILEUPLOAD)] + [TestCase( + InvalidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.CONTAINER)] + [TestCase( + ValidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.CONTAINER)] + [TestCase( + InvalidProjectId, + ValidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.CONTAINER)] + [TestCase( + InvalidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.CONTAINER)] + [TestCase( + InvalidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.S3)] + [TestCase( + ValidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.S3)] + [TestCase( + InvalidProjectId, + ValidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.S3)] + [TestCase( + InvalidProjectId, + InvalidEnvironmentId, + ValidBuildName, + OsFamilyEnum.LINUX, + BuildTypeEnum.S3)] public void BuildCreateAsync_InvalidInputThrowsException( string projectId, string environmentId, @@ -153,21 +276,36 @@ BuildTypeEnum buildType }; MockUnityEnvironment.Setup(ex => ex.FetchIdentifierAsync(CancellationToken.None)).ReturnsAsync(environmentId); - Assert.ThrowsAsync(() => - BuildCreateHandler.BuildCreateAsync( - input, - MockUnityEnvironment.Object, - GameServerHostingService!, - MockLogger!.Object, - CancellationToken.None - ) + Assert.ThrowsAsync( + () => + BuildCreateHandler.BuildCreateAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) ); - TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); + TestsHelper.VerifyLoggerWasCalled( + MockLogger!, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Never); } - [TestCase(ValidProjectId, ValidEnvironmentId, "Build1", OsFamilyEnum.LINUX, BuildTypeEnum.FILEUPLOAD)] - [TestCase(ValidProjectId, ValidEnvironmentId, "Build1", OsFamilyEnum.LINUX, BuildTypeEnum.CONTAINER)] + [TestCase( + ValidProjectId, + ValidEnvironmentId, + "Build1", + OsFamilyEnum.LINUX, + BuildTypeEnum.FILEUPLOAD)] + [TestCase( + ValidProjectId, + ValidEnvironmentId, + "Build1", + OsFamilyEnum.LINUX, + BuildTypeEnum.CONTAINER)] public void BuildCreateAsync_DuplicateNameThrowsException( string projectId, string environmentId, @@ -186,16 +324,58 @@ BuildTypeEnum buildType }; MockUnityEnvironment.Setup(ex => ex.FetchIdentifierAsync(CancellationToken.None)).ReturnsAsync(environmentId); - Assert.ThrowsAsync(() => - BuildCreateHandler.BuildCreateAsync( - input, - MockUnityEnvironment.Object, - GameServerHostingService!, - MockLogger!.Object, - CancellationToken.None - ) + Assert.ThrowsAsync( + () => + BuildCreateHandler.BuildCreateAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) + ); + + TestsHelper.VerifyLoggerWasCalled( + MockLogger!, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Never); + } + + [TestCase(BuildTypeEnum.FILEUPLOAD)] + [TestCase(BuildTypeEnum.CONTAINER)] + [TestCase(BuildTypeEnum.S3)] + public void BuildCreateAsync_InvalidBuildVersionName( + BuildTypeEnum buildType + ) + { + BuildCreateInput input = new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentId, + BuildName = ValidBuildName, + BuildOsFamily = OsFamilyEnum.LINUX, + BuildType = buildType, + BuildVersionName = InValidBuildVersionName + }; + MockUnityEnvironment.Setup(ex => ex.FetchIdentifierAsync(CancellationToken.None)) + .ReturnsAsync(ValidEnvironmentId); + + Assert.ThrowsAsync( + () => + BuildCreateHandler.BuildCreateAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) ); - TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); + TestsHelper.VerifyLoggerWasCalled( + MockLogger!, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Never); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateVersionFileUploadHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateVersionFileUploadHandlerTests.cs index 3479e8b..2628848 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateVersionFileUploadHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateVersionFileUploadHandlerTests.cs @@ -1,5 +1,4 @@ using System.Net; -using SystemFile = System.IO.File; using Microsoft.Extensions.Logging; using Moq; using Moq.Protected; @@ -10,6 +9,7 @@ using Unity.Services.Cli.GameServerHosting.Input; using Unity.Services.Cli.TestUtils; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; +using SystemFile = System.IO.File; namespace Unity.Services.Cli.GameServerHosting.UnitTest.Handlers; @@ -49,7 +49,8 @@ public void BuildCreateVersionAsync_FileUpload_MissingInputThrowsException( TargetEnvironmentName = ValidEnvironmentName, BuildId = buildId.HasValue ? buildId.ToString() : null, FileDirectory = directory, - RemoveOldFiles = removeOldFiles + RemoveOldFiles = removeOldFiles, + BuildVersionName = ValidBuildVersionName }; Assert.ThrowsAsync( diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateVersionHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateVersionHandlerTests.cs index de3bd9b..cffc7f9 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateVersionHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildCreateVersionHandlerTests.cs @@ -1,15 +1,10 @@ -using System.Net; -using Microsoft.Extensions.Logging; +using System.ComponentModel; using Moq; -using Moq.Protected; using Spectre.Console; using Unity.Services.Cli.Common.Console; using Unity.Services.Cli.Common.Exceptions; -using Unity.Services.Cli.Common.Logging; -using Unity.Services.Cli.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; @@ -67,4 +62,52 @@ await BuildCreateVersionHandler.BuildCreateVersionAsync( MockUnityEnvironment.Verify(ex => ex.FetchIdentifierAsync(CancellationToken.None), Times.Once); } + + [TestCase(CreateBuildRequest.BuildTypeEnum.CONTAINER)] + [TestCase(CreateBuildRequest.BuildTypeEnum.S3)] + [TestCase(CreateBuildRequest.BuildTypeEnum.FILEUPLOAD)] + public Task BuildCreateVersionAsync_InvalidBuildVersionName(CreateBuildRequest.BuildTypeEnum buildType) + { + BuildCreateVersionInput input = buildType switch + { + CreateBuildRequest.BuildTypeEnum.CONTAINER => new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + BuildId = ValidBuildIdContainer.ToString(), + ContainerTag = ValidContainerTag, + BuildVersionName = InValidBuildVersionName + }, + CreateBuildRequest.BuildTypeEnum.S3 => new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + BuildId = ValidBuildIdBucket.ToString(), + BuildVersionName = InValidBuildVersionName, + AccessKey = "accessKey", + BucketUrl = "bucketUrl", + SecretKey = "secretKey" + }, + CreateBuildRequest.BuildTypeEnum.FILEUPLOAD => new() + { + CloudProjectId = ValidProjectId, + TargetEnvironmentName = ValidEnvironmentName, + BuildId = BuildWithOneFileId.ToString(), + BuildVersionName = InValidBuildVersionName, + FileDirectory = m_TempDirectory + }, + _ => throw new InvalidEnumArgumentException() + }; + + Assert.ThrowsAsync( + async () => await BuildCreateVersionHandler.BuildCreateVersionAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + MockHttpClient!.Object, + CancellationToken.None)); + + return Task.CompletedTask; + } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildGetHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildGetHandlerTests.cs index 36ed7ff..0c972de 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildGetHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/BuildGetHandlerTests.cs @@ -18,11 +18,18 @@ public async Task BuildGetAsync_CallsLoadingIndicatorStartLoading() { var mockLoadingIndicator = new Mock(); - await BuildGetHandler.BuildGetAsync(null!, MockUnityEnvironment.Object, null!, null!, - mockLoadingIndicator.Object, CancellationToken.None); - - mockLoadingIndicator.Verify(ex => ex - .StartLoadingAsync(It.IsAny(), It.IsAny>()), Times.Once); + await BuildGetHandler.BuildGetAsync( + null!, + MockUnityEnvironment.Object, + null!, + null!, + mockLoadingIndicator.Object, + CancellationToken.None); + + mockLoadingIndicator.Verify( + ex => ex + .StartLoadingAsync(It.IsAny(), It.IsAny>()), + Times.Once); } [Test] @@ -56,22 +63,32 @@ public void BuildGetAsync_NullBuildIdThrowsException(string projectId, string en BuildId = buildId }; - Assert.ThrowsAsync(() => - BuildGetHandler.BuildGetAsync( - input, - MockUnityEnvironment.Object, - GameServerHostingService!, - MockLogger!.Object, - CancellationToken.None - ) + Assert.ThrowsAsync( + () => + BuildGetHandler.BuildGetAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) ); - BuildsApi!.DefaultBuildsClient.Verify(api => api.GetBuildAsync( - It.IsAny(), It.IsAny(), - It.IsAny(), 0, CancellationToken.None - ), Times.Never); - - TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); + BuildsApi!.DefaultBuildsClient.Verify( + api => api.GetBuildAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + 0, + CancellationToken.None + ), + Times.Never); + + TestsHelper.VerifyLoggerWasCalled( + MockLogger!, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Never); } [Test] @@ -81,7 +98,7 @@ public async Task BuildGetAsync_CallsGetService() { CloudProjectId = ValidProjectId, TargetEnvironmentName = ValidEnvironmentName, - BuildId = ValidBuildIdContainer.ToString() + BuildId = ValidBuildIdContainer.ToString(), }; await BuildGetHandler.BuildGetAsync( @@ -92,10 +109,15 @@ await BuildGetHandler.BuildGetAsync( CancellationToken.None ); - BuildsApi!.DefaultBuildsClient.Verify(api => api.GetBuildAsync( - new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), - long.Parse(input.BuildId), 0, CancellationToken.None - ), Times.Once); + BuildsApi!.DefaultBuildsClient.Verify( + api => api.GetBuildAsync( + new Guid(input.CloudProjectId), + new Guid(ValidEnvironmentId), + long.Parse(input.BuildId), + 0, + CancellationToken.None + ), + Times.Once); } [TestCase(InvalidProjectId, InvalidEnvironmentId, InvalidBuildId)] @@ -112,16 +134,21 @@ public void BuildGetAsync_InvalidInputThrowsException(string projectId, string e }; MockUnityEnvironment.Setup(ex => ex.FetchIdentifierAsync(CancellationToken.None)).ReturnsAsync(environmentId); - Assert.ThrowsAsync(() => - BuildGetHandler.BuildGetAsync( - input, - MockUnityEnvironment.Object, - GameServerHostingService!, - MockLogger!.Object, - CancellationToken.None - ) + Assert.ThrowsAsync( + () => + BuildGetHandler.BuildGetAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ) ); - TestsHelper.VerifyLoggerWasCalled(MockLogger!, LogLevel.Critical, LoggerExtension.ResultEventId, Times.Never); + TestsHelper.VerifyLoggerWasCalled( + MockLogger!, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Never); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpCreateHandlerTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpCreateHandlerTest.cs new file mode 100644 index 0000000..84b3086 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpCreateHandlerTest.cs @@ -0,0 +1,140 @@ +using System.IO.Abstractions; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.GameServerHosting.Handlers; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Cli.TestUtils; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Handlers; + +[TestFixture] +[TestOf(typeof(CoreDumpCreateHandler))] +class CoreDumpCreateHandlerTest : HandlerCommon +{ + static IEnumerable TestCases() + { + var validGcsCredentials = new GcsCredentials(CoreDumpMockValidAccessId, CoreDumpMockValidPrivateKey); + var invalidGcsCredentials = new GcsCredentials("invalid access id", CoreDumpMockValidPrivateKey); + + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithoutConfig, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + validGcsCredentials, + $"fleetId: {CoreDumpMockFleetIdWithoutConfig}", + false, + "" + ).SetName("Core Dump Config should be created"); + + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithEnabledConfig, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + validGcsCredentials, + "", + true, + "already exists" + ).SetName("Core Dump Config already exists"); + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithoutConfig, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + invalidGcsCredentials, + "", + true, + "Invalid credentials" + ).SetName("Core Dump Config invalid credentials"); + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithoutConfig, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + validGcsCredentials, + "", + true, + "Invalid GCS credentials file format" + ).SetName("Invalid file format"); + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + null, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + validGcsCredentials, + $"fleetId: {CoreDumpMockFleetIdWithoutConfig}", + true, + "Missing value for input: 'fleet-id'" + ).SetName("Missing input"); + } + + [TestCaseSource(nameof(TestCases))] + public async Task CoreDumpCreateAsync( + string projectId, + string environmentName, + string fleetId, + string state, + string credentialsFile, + string buketName, + GcsCredentials gcsFileContent, + string outputContains, + bool expectedException = false, + string exceptionContains = "") + { + var fileMoq = new Mock(); + fileMoq.Setup(f => f.Exists(It.IsAny())) + .Returns(true); + fileMoq.Setup(f => f.ReadAllText(It.IsAny())) + .Returns(JsonConvert.SerializeObject(gcsFileContent)); + + CoreDumpCreateInput input = new() + { + CloudProjectId = projectId, + TargetEnvironmentName = environmentName, + FleetId = fleetId, + CredentialsFile = credentialsFile, + State = state, + GcsBucket = buketName, + }; + + try + { + await CoreDumpCreateHandler.CoreDumpCreateAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + new GcsCredentialParser(fileMoq.Object), + CancellationToken.None + ); + } + catch (CliException e) when (expectedException) + { + Assert.That(e.Message, Does.Contain(exceptionContains)); + return; + } + + TestsHelper.VerifyLoggerWasCalled( + MockLogger, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Once, + outputContains); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpDeleteHandlerTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpDeleteHandlerTest.cs new file mode 100644 index 0000000..a844ef0 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpDeleteHandlerTest.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.GameServerHosting.Handlers; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.TestUtils; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Handlers; + +[TestFixture] +[TestOf(typeof(CoreDumpDeleteHandler))] +class CoreDumpDeleteHandlerTest : HandlerCommon +{ + [TestCase( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithEnabledConfig, + "Core Dump config deleted successfully", + TestName = "Core Dump Config should be deleted")] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithoutConfig, + "", + true, + "Core Dump Storage is not configured", + TestName = "Core Dump Config is not configured")] + [TestCase( + InvalidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithoutConfig, + "", + true, + TestName = "Invalid Project Id, unexpected exception")] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + null, + "Core Dump config deleted successfully", + true, + "Missing value for input: 'fleet-id'", + TestName = "Missing input")] + public async Task CoreDumpDeleteAsync( + string projectId, + string environmentName, + string fleetId, + string outputContains, + bool expectedException = false, + string exceptionContains = "") + { + FleetIdInput input = new() + { + CloudProjectId = projectId, + TargetEnvironmentName = environmentName, + FleetId = fleetId + }; + + try + { + await CoreDumpDeleteHandler.CoreDumpDeleteAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ); + } + catch (CliException e) when (expectedException) + { + Assert.That(e.Message, Does.Contain(exceptionContains)); + return; + } + + TestsHelper.VerifyLoggerWasCalled( + MockLogger, + LogLevel.Information, + 0, + Times.Once, + outputContains); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpGetHandlerTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpGetHandlerTest.cs new file mode 100644 index 0000000..bd7d066 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpGetHandlerTest.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.Logging; +using Moq; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.GameServerHosting.Handlers; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.TestUtils; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Handlers; + +[TestFixture] +[TestOf(typeof(CoreDumpGetHandler))] +class CoreDumpGetHandlerTest : HandlerCommon +{ + [TestCase( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithEnabledConfig, + "state: enabled", + TestName = "Core Dump Config should be enabled")] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithDisabledConfig, + "state: disabled", + TestName = "Core Dump Config Should be disabled")] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithoutConfig, + "", + true, + "Core Dump Storage is not configured", + TestName = "Core Dump Config is not configured")] + [TestCase( + InvalidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithoutConfig, + "", + true, + "validation error", + TestName = "Invalid Project Id, unexpected exception")] + [TestCase( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithEnabledConfig, + "state: enabled", + true, + "Missing value for input: 'fleet-id'", + TestName = "Missing input")] + public async Task CoreDumpGetAsync( + string projectId, + string environmentName, + string fleetId, + string outputContains, + bool expectedException = false, + string exceptionContains = "") + { + FleetIdInput input = new() + { + CloudProjectId = projectId, + TargetEnvironmentName = environmentName, + FleetId = fleetId + }; + + try + { + await CoreDumpGetHandler.CoreDumpGetAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + CancellationToken.None + ); + } + catch (CliException e) when (expectedException) + { + Assert.That(e.Message, Does.Contain(exceptionContains)); + return; + } + + TestsHelper.VerifyLoggerWasCalled( + MockLogger, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Once, + outputContains); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpUpdateHandlerTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpUpdateHandlerTest.cs new file mode 100644 index 0000000..d016457 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/CoreDumpUpdateHandlerTest.cs @@ -0,0 +1,138 @@ +using System.IO.Abstractions; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.GameServerHosting.Handlers; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Cli.TestUtils; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Handlers; + +[TestFixture] +[TestOf(typeof(CoreDumpUpdateHandler))] +class CoreDumpUpdateHandlerTest : HandlerCommon +{ + static IEnumerable TestCases() + { + var validGcsCredentials = new GcsCredentials(CoreDumpMockValidAccessId, CoreDumpMockValidPrivateKey); + var invalidGcsCredentials = new GcsCredentials("invalid access id", CoreDumpMockValidPrivateKey); + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithEnabledConfig, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + validGcsCredentials, + $"fleetId: {CoreDumpMockFleetIdWithEnabledConfig}", + false, + "" + ).SetName("Core Dump Config should be created"); + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithoutConfig, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + validGcsCredentials, + "", + true, + "Core dump config does not exists" + ).SetName("Core Dump Config already does not exists"); + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithEnabledConfig, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + invalidGcsCredentials, + "", + true, + "Invalid credentials" + ).SetName("Core Dump Config invalid credentials"); + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + CoreDumpMockFleetIdWithEnabledConfig, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + validGcsCredentials, + "", + true, + "Invalid GCS credentials file format" + ).SetName("Invalid file format"); + yield return new TestCaseData( + ValidProjectId, + ValidEnvironmentName, + null, + CoreDumpStateConverter.StateEnum.Enabled.ToString(), + CoreDumpMockValidCredentialsFilePath, + CoreDumpMockValidBucketName, + invalidGcsCredentials, + $"fleetId: {CoreDumpMockFleetIdWithEnabledConfig}", + true, + "Missing value for input: 'fleet-id'" + ).SetName("Core Dump Config should be created"); + } + + [TestCaseSource(nameof(TestCases))] + public async Task CoreDumpCreateAsync( + string projectId, + string environmentName, + string fleetId, + string state, + string credentialsFile, + string buketName, + GcsCredentials gcsCredentials, + string outputContains, + bool expectedException = false, + string exceptionContains = "") + { + var fileMoq = new Mock(); + fileMoq.Setup(f => f.Exists(It.IsAny())) + .Returns(true); + fileMoq.Setup(f => f.ReadAllText(It.IsAny())) + .Returns(JsonConvert.SerializeObject(gcsCredentials)); + + CoreDumpUpdateInput input = new() + { + CloudProjectId = projectId, + TargetEnvironmentName = environmentName, + FleetId = fleetId, + CredentialsFile = credentialsFile, + State = state, + GcsBucket = buketName, + }; + + try + { + await CoreDumpUpdateHandler.CoreDumpUpdateAsync( + input, + MockUnityEnvironment.Object, + GameServerHostingService!, + MockLogger!.Object, + new GcsCredentialParser(fileMoq.Object), + CancellationToken.None + ); + } + catch (CliException e) when (expectedException) + { + Assert.That(e.Message, Does.Contain(exceptionContains)); + return; + } + + TestsHelper.VerifyLoggerWasCalled( + MockLogger, + LogLevel.Critical, + LoggerExtension.ResultEventId, + Times.Once, + outputContains); + } +} 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 index 59e2509..db1c77d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileDownloadHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FileDownloadHandlerTests.cs @@ -110,7 +110,7 @@ await FileDownloadHandler.FileDownloadAsync( api => api.GenerateDownloadURLAsync( It.IsAny(), It.IsAny(), - It.IsAny(), + It.IsAny(), 0, It.IsAny() ), 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 0bd84c3..2af9fd9 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 @@ -102,7 +102,7 @@ await FleetCreateHandler.FleetCreateAsync(input, MockUnityEnvironment.Object, Ga buildConfigurations: buildConfigurations.ToList(), regions: regionList, usageSettings: new List { usageSetting! }); FleetsApi!.DefaultFleetsClient.Verify(api => api.CreateFleetAsync( - new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), + new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), null, createRequest, 0, CancellationToken.None ), Times.Once); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetRegionCreateHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetRegionCreateHandlerTests.cs index ec52577..88539b9 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetRegionCreateHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/FleetRegionCreateHandlerTests.cs @@ -73,7 +73,7 @@ await FleetRegionCreateHandler.FleetRegionCreateAsync(input, MockUnityEnvironmen FleetsApi!.DefaultFleetsClient.Verify(api => api.AddFleetRegionAsync( new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), - new Guid(fleetId), createRequest, 0, CancellationToken.None + new Guid(fleetId), null, createRequest, 0, CancellationToken.None ), Times.Once); } 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 c8fc972..0e3ff3d 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 @@ -19,6 +19,7 @@ class HandlerCommon internal static GameServerHostingServersApiV1Mock? ServersApi; internal static GameServerHostingMachinesApiV1Mock? MachinesApi; internal static GameServerHostingBuildConfigurationsApiV1Mock? BuildConfigurationsApi; + internal static GameServerHostingCoreDumpApiV1Mock? CoreDumpApi; protected static GameServerHostingService? GameServerHostingService; [SetUp] @@ -109,6 +110,9 @@ public void SetUp() }; BuildConfigurationsApi.SetUp(); + CoreDumpApi = new GameServerHostingCoreDumpApiV1Mock(); + CoreDumpApi.SetUp(); + GameServerHostingService = new GameServerHostingService( s_AuthenticationServiceObject.Object, BuildsApi.DefaultBuildsClient.Object, @@ -116,10 +120,12 @@ public void SetUp() FilesApi.DefaultFilesClient.Object, FleetsApi.DefaultFleetsClient.Object, MachinesApi.DefaultMachinesClient.Object, + CoreDumpApi.DefaultCoreDumpClient.Object, ServersApi.DefaultServersClient.Object ); - MockUnityEnvironment.Setup(ex => ex.FetchIdentifierAsync(CancellationToken.None)).ReturnsAsync(ValidEnvironmentId); + MockUnityEnvironment.Setup(ex => ex.FetchIdentifierAsync(CancellationToken.None)) + .ReturnsAsync(ValidEnvironmentId); // create tmp output directory for tests Directory.CreateDirectory(ValidOutputDirectory); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/RegionAvailableHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/RegionAvailableHandlerTests.cs index 5160d7a..8efc208 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/RegionAvailableHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/RegionAvailableHandlerTests.cs @@ -65,7 +65,7 @@ await RegionAvailableHandler.RegionAvailableAsync( FleetsApi!.DefaultFleetsClient.Verify(api => api.GetAvailableFleetRegionsAsync( new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), - new Guid(ValidFleetId), 0, CancellationToken.None + new Guid(ValidFleetId), null, 0, CancellationToken.None ), Times.Once); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/RegionTemplatesHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/RegionTemplatesHandlerTests.cs index 65ecef5..aee5288 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/RegionTemplatesHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Handlers/RegionTemplatesHandlerTests.cs @@ -62,7 +62,7 @@ await RegionTemplatesHandler.RegionTemplatesAsync( ); FleetsApi!.DefaultFleetsClient.Verify(api => api.ListTemplateFleetRegionsAsync( - new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), + new Guid(input.CloudProjectId), new Guid(ValidEnvironmentId), null, 0, CancellationToken.None ), Times.Once); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/CoreDumpCreateInputTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/CoreDumpCreateInputTest.cs new file mode 100644 index 0000000..faed464 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Input/CoreDumpCreateInputTest.cs @@ -0,0 +1,22 @@ +using System.CommandLine; +using Unity.Services.Cli.GameServerHosting.Input; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Input; + +[TestFixture] +[TestOf(typeof(CoreDumpCreateInput))] +public class CoreDumpCreateInputTest +{ + [TestCase("disabled", true)] + [TestCase("enabled", true)] + [TestCase("unavailable", false)] + public void ValidateStateOption(string state, bool valid) + { + var args = new[] + { + CoreDumpCreateInput.StateOption.Aliases.First(), + state + }; + Assert.That(CoreDumpCreateInput.StateOption.Parse(args).Errors, valid ? Is.Empty : Is.Not.Empty); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingBuildApiV1AsyncMock.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingBuildApiV1AsyncMock.cs index 7f82133..83421c9 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingBuildApiV1AsyncMock.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingBuildApiV1AsyncMock.cs @@ -1,3 +1,4 @@ +using System.Net; using Moq; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Api; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; @@ -82,6 +83,7 @@ class GameServerHostingBuildsApiV1Mock readonly List m_TestBuildInstalls = new() { new BuildListInner1( + ValidBuildVersionName, new CCDDetails(Guid.Parse(ValidBucketId), Guid.Parse(ValidReleaseId)), completedMachines: 1, container: new ContainerImage("tag"), @@ -93,9 +95,14 @@ class GameServerHostingBuildsApiV1Mock pendingMachines: 1, regions: new List { - new(1, 1, 1, "region name") + new( + 1, + 1, + 1, + "region name") }), new BuildListInner1( + ValidBuildVersionName, new CCDDetails(Guid.Parse(ValidBucketId), Guid.Parse(ValidReleaseId)), completedMachines: 3, container: new ContainerImage("tag"), @@ -107,7 +114,11 @@ class GameServerHostingBuildsApiV1Mock pendingMachines: 2, regions: new List { - new(3, 1, 2, "another region name") + new( + 3, + 1, + 2, + "another region name") }) }; @@ -117,6 +128,7 @@ class GameServerHostingBuildsApiV1Mock ValidBuildIdBucket, "build2-bucket-build", CreateBuild200Response.BuildTypeEnum.S3, + buildVersionName: ValidBuildVersionName, s3: new AmazonS3Details("s3://bucket-name"), osFamily: CreateBuild200Response.OsFamilyEnum.LINUX, syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCED, @@ -125,6 +137,7 @@ class GameServerHostingBuildsApiV1Mock ValidBuildIdContainer, "build1-container-build", CreateBuild200Response.BuildTypeEnum.CONTAINER, + buildVersionName: ValidBuildVersionName, container: new ContainerImage(ValidContainerTag), osFamily: CreateBuild200Response.OsFamilyEnum.LINUX, syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCED, @@ -133,6 +146,7 @@ class GameServerHostingBuildsApiV1Mock ValidBuildIdFileUpload, "build3-file-upload-build", CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, + buildVersionName: ValidBuildVersionName, ccd: new CCDDetails(Guid.Parse(ValidBucketId), Guid.Parse(ValidReleaseId)), osFamily: CreateBuild200Response.OsFamilyEnum.LINUX, syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCED, @@ -141,6 +155,7 @@ class GameServerHostingBuildsApiV1Mock BuildWithOneFileId, "Build3 (Build with one file test)", CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, + ValidBuildVersionName, new CCDDetails( new Guid(ValidBucketId), new Guid(ValidReleaseId)), @@ -151,6 +166,7 @@ class GameServerHostingBuildsApiV1Mock BuildWithTwoFilesId, "Build3 (Build with one file test)", CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, + ValidBuildVersionName, new CCDDetails( new Guid(ValidBucketId), new Guid(ValidReleaseId)), @@ -161,6 +177,7 @@ class GameServerHostingBuildsApiV1Mock SyncingBuildId, "Syncing Build", CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, + buildVersionName: ValidBuildVersionName, ccd: new CCDDetails(), osFamily: CreateBuild200Response.OsFamilyEnum.LINUX, syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCING, @@ -173,6 +190,7 @@ class GameServerHostingBuildsApiV1Mock 1, 11, "Build1", + buildVersionName: ValidBuildVersionName, ccd: new CCDDetails( new Guid(ValidBucketId), new Guid(ValidReleaseId)), @@ -183,6 +201,7 @@ class GameServerHostingBuildsApiV1Mock 2, 22, "Build2", + buildVersionName: ValidBuildVersionName, container: new ContainerImage("v1"), osFamily: BuildListInner.OsFamilyEnum.LINUX, syncStatus: BuildListInner.SyncStatusEnum.SYNCED, @@ -206,137 +225,180 @@ public void SetUp() DefaultBuildsClient.Setup(a => a.Configuration) .Returns(new Configuration()); - DefaultBuildsClient.Setup(a => - a.CreateBuildAsync( - It.IsAny(), // projectId - It.IsAny(), // environmentId - It.IsAny(), // build - 0, - CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, CreateBuildRequest req, int _, CancellationToken _) => - { - var validated = ValidateProjectEnvironment(projectId, environmentId); - if (!validated) throw new HttpRequestException(); + DefaultBuildsClient.Setup( + a => + a.CreateBuildAsync( + It.IsAny(), // projectId + It.IsAny(), // environmentId + It.IsAny(), // build + 0, + CancellationToken.None + )) + .Returns( + (Guid projectId, Guid environmentId, CreateBuildRequest req, int _, CancellationToken _) => + { + var validated = ValidateProjectEnvironment(projectId, environmentId); + if (!validated) throw new HttpRequestException(); - var buildExists = m_TestListBuilds.Find(b => b.BuildName == req.BuildName) != null; - if (buildExists) throw new ApiException(); + var buildExists = m_TestListBuilds.Find(b => b.BuildName == req.BuildName) != null; + if (buildExists) throw new ApiException(); - var osFamily = req.OsFamily switch - { - CreateBuildRequest.OsFamilyEnum.LINUX => CreateBuild200Response.OsFamilyEnum.LINUX, - _ => throw new ApiException() - }; + if (req.BuildVersionName is InValidBuildVersionName) + { + throw new ApiException( + (int)HttpStatusCode.BadRequest, + "Bad request", + "{\"Detail\":\"Invalid build version name\"}" + ); + } - var build = req.BuildType switch - { - CreateBuildRequest.BuildTypeEnum.CONTAINER => new CreateBuild200Response( - 1, req.BuildName, CreateBuild200Response.BuildTypeEnum.CONTAINER, - cfv: 5, osFamily: osFamily, updated: DateTime.Now, container: new ContainerImage("v1")), - CreateBuildRequest.BuildTypeEnum.FILEUPLOAD => new CreateBuild200Response( - 1, req.BuildName, CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, - cfv: 5, osFamily: osFamily, updated: DateTime.Now, - ccd: new CCDDetails(new Guid(ValidBucketId), new Guid(ValidReleaseId))), - CreateBuildRequest.BuildTypeEnum.S3 => new CreateBuild200Response( - 1, req.BuildName, CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, - cfv: 5, osFamily: osFamily, updated: DateTime.Now, - ccd: new CCDDetails(new Guid(ValidBucketId), new Guid(ValidReleaseId))), - _ => throw new ApiException() - }; - - return Task.FromResult(build); - }); - - DefaultBuildsClient.Setup(a => - a.ListBuildsAsync( - It.IsAny(), // projectId - It.IsAny(), // environmentId - It.IsAny(), // limit - It.IsAny(), // lastVal - It.IsAny(), // lastId - It.IsAny(), // sortBy - It.IsAny(), // sortDir - It.IsAny(), // partialFilter - 0, - CancellationToken.None - )).Returns(( - Guid projectId, - Guid environmentId, - string _, - Guid? _, - Guid? _, - string _, - string _, - string _, - int _, - CancellationToken _ - ) => - { - var validated = ValidateProjectEnvironment(projectId, environmentId); - if (!validated) throw new HttpRequestException(); - return Task.FromResult(m_TestListBuilds); - }); - - DefaultBuildsClient.Setup(a => - a.GetBuildAsync( - It.IsAny(), // projectId - It.IsAny(), // environmentId - It.IsAny(), // buildId - 0, - CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, long buildId, int _, CancellationToken _) => - { - var validated = ValidateProjectEnvironment(projectId, environmentId); - if (!validated) throw new HttpRequestException(); - - var build = m_TestBuilds.Find(b => b.BuildID == buildId); - if (build == null) throw new ApiException(); - - return Task.FromResult(build); - }); - DefaultBuildsClient.Setup(a => - a.DeleteBuildAsync( - It.IsAny(), // projectId - It.IsAny(), // environmentId - It.IsAny(), // buildId - null, // Dry Run - 0, - CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, long buildId, bool _, int _, CancellationToken _) => - { - var validated = ValidateProjectEnvironment(projectId, environmentId); - if (!validated) throw new HttpRequestException(); - - var build = GetBuildById(buildId); - if (build is null) throw new HttpRequestException(); - - return Task.CompletedTask; - }); - DefaultBuildsClient.Setup(a => - a.UpdateBuildAsync( - It.IsAny(), // projectId - It.IsAny(), // environmentId - It.IsAny(), // buildId - It.IsAny(), // update build request - 0, - CancellationToken.None - )).Returns(( - Guid projectId, - Guid environmentId, - long buildId, - UpdateBuildRequest _, - int _, - CancellationToken _ - ) => - { - var validated = ValidateProjectEnvironment(projectId, environmentId); - if (!validated) throw new HttpRequestException(); + var osFamily = req.OsFamily switch + { + CreateBuildRequest.OsFamilyEnum.LINUX => CreateBuild200Response.OsFamilyEnum.LINUX, + _ => throw new ApiException() + }; + + var build = req.BuildType switch + { + CreateBuildRequest.BuildTypeEnum.CONTAINER => new CreateBuild200Response( + 1, + req.BuildName, + CreateBuild200Response.BuildTypeEnum.CONTAINER, + buildVersionName: ValidBuildVersionName, + cfv: 5, + osFamily: osFamily, + updated: DateTime.Now, + container: new ContainerImage("v1") + ), + CreateBuildRequest.BuildTypeEnum.FILEUPLOAD => new CreateBuild200Response( + 1, + req.BuildName, + CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, + buildVersionName: ValidBuildVersionName, + cfv: 5, + osFamily: + osFamily, + updated: DateTime.Now, + ccd: new CCDDetails(new Guid(ValidBucketId), new Guid(ValidReleaseId)) + ), + CreateBuildRequest.BuildTypeEnum.S3 => new CreateBuild200Response( + 1, + req.BuildName, + CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, + buildVersionName: ValidBuildVersionName, + cfv: 5, + osFamily: osFamily, + updated: DateTime.Now, + ccd: new CCDDetails(new Guid(ValidBucketId), new Guid(ValidReleaseId))), + _ => throw new ApiException() + }; + + return Task.FromResult(build); + }); + + DefaultBuildsClient.Setup( + a => + a.ListBuildsAsync( + It.IsAny(), // projectId + It.IsAny(), // environmentId + It.IsAny(), // limit + It.IsAny(), // lastVal + It.IsAny(), // lastId + It.IsAny(), // sortBy + It.IsAny(), // sortDir + It.IsAny(), // partialFilter + 0, + CancellationToken.None + )) + .Returns( + ( + Guid projectId, + Guid environmentId, + string _, + Guid? _, + Guid? _, + string _, + string _, + string _, + int _, + CancellationToken _ + ) => + { + var validated = ValidateProjectEnvironment(projectId, environmentId); + if (!validated) throw new HttpRequestException(); + return Task.FromResult(m_TestListBuilds); + }); + + DefaultBuildsClient.Setup( + a => + a.GetBuildAsync( + It.IsAny(), // projectId + It.IsAny(), // environmentId + It.IsAny(), // buildId + 0, + CancellationToken.None + )) + .Returns( + (Guid projectId, Guid environmentId, long buildId, int _, CancellationToken _) => + { + var validated = ValidateProjectEnvironment(projectId, environmentId); + if (!validated) throw new HttpRequestException(); + + var build = m_TestBuilds.Find(b => b.BuildID == buildId); + if (build == null) throw new ApiException(); + + return Task.FromResult(build); + }); + DefaultBuildsClient.Setup( + a => + a.DeleteBuildAsync( + It.IsAny(), // projectId + It.IsAny(), // environmentId + It.IsAny(), // buildId + null, // Dry Run + 0, + CancellationToken.None + )) + .Returns( + (Guid projectId, Guid environmentId, long buildId, bool _, int _, CancellationToken _) => + { + var validated = ValidateProjectEnvironment(projectId, environmentId); + if (!validated) throw new HttpRequestException(); + + var build = GetBuildById(buildId); + if (build is null) throw new HttpRequestException(); + + return Task.CompletedTask; + }); + DefaultBuildsClient.Setup( + a => + a.UpdateBuildAsync( + It.IsAny(), // projectId + It.IsAny(), // environmentId + It.IsAny(), // buildId + It.IsAny(), // update build request + 0, + CancellationToken.None + )) + .Returns( + ( + Guid projectId, + Guid environmentId, + long buildId, + UpdateBuildRequest _, + int _, + CancellationToken _ + ) => + { + var validated = ValidateProjectEnvironment(projectId, environmentId); + if (!validated) throw new HttpRequestException(); - var build = m_TestBuilds.Find(b => b.BuildID == buildId); - if (build == null) throw new ApiException(); + var build = m_TestBuilds.Find(b => b.BuildID == buildId); + if (build == null) throw new ApiException(); - return Task.FromResult(build); - }); + return Task.FromResult(build); + }); DefaultBuildsClient.Setup( a => @@ -353,13 +415,22 @@ CancellationToken _ Guid projectId, Guid environmentId, long buildId, - CreateNewBuildVersionRequest _, + CreateNewBuildVersionRequest request, int _, CancellationToken _) => { var validated = ValidateProjectEnvironment(projectId, environmentId); if (!validated) throw new HttpRequestException(); + if (request.BuildVersionName is InValidBuildVersionName) + { + throw new ApiException( + (int)HttpStatusCode.BadRequest, + "Bad request", + "{\"Detail\":\"Invalid build version name\"}" + ); + } + var build = m_TestBuilds.Find(b => b.BuildID == buildId); if (build == null) throw new ApiException(); @@ -452,23 +523,26 @@ CancellationToken _ } ); - DefaultBuildsClient.Setup(a => - a.GetBuildInstallsAsync( - It.IsAny(), // projectId - It.IsAny(), // environmentId - It.IsAny(), // BuildId - 0, - CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, long buildId, int _, CancellationToken _) => - { - var validated = ValidateProjectEnvironment(projectId, environmentId); - if (!validated) throw new HttpRequestException(); + DefaultBuildsClient.Setup( + a => + a.GetBuildInstallsAsync( + It.IsAny(), // projectId + It.IsAny(), // environmentId + It.IsAny(), // BuildId + 0, + CancellationToken.None + )) + .Returns( + (Guid projectId, Guid environmentId, long buildId, int _, CancellationToken _) => + { + var validated = ValidateProjectEnvironment(projectId, environmentId); + if (!validated) throw new HttpRequestException(); - var buildExists = m_TestListBuilds.Find(b => b.BuildID == buildId) != null; - if (!buildExists) throw new HttpRequestException(); + var buildExists = m_TestListBuilds.Find(b => b.BuildID == buildId) != null; + if (!buildExists) throw new HttpRequestException(); - return Task.FromResult(m_TestBuildInstalls); - }); + return Task.FromResult(m_TestBuildInstalls); + }); } bool ValidateProjectEnvironment(Guid projectId, Guid environmentId) diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingBuildConfigurationsApiV1Mock.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingBuildConfigurationsApiV1Mock.cs index 0a093a6..03e61f5 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingBuildConfigurationsApiV1Mock.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingBuildConfigurationsApiV1Mock.cs @@ -39,7 +39,7 @@ class GameServerHostingBuildConfigurationsApiV1Mock name: ValidBuildConfigurationName, queryType: "sqp", speed: 1200L, - updatedAt:new DateTime(2022, 10, 11), + updatedAt: new DateTime(2022, 10, 11), version: 1L ); @@ -121,22 +121,22 @@ public void SetUp() ); return Task.FromResult(buildConfig); }); - - DefaultBuildConfigurationsClient.Setup(a => - a.DeleteBuildConfigurationAsync( - It.IsAny(), // projectId - It.IsAny(), // environmentId - It.IsAny(), // buildConfigurationId - null, - 0, - CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, long buildConfigurationId, bool _, int _, CancellationToken _) => - { - var validated = ValidateProjectEnvironment(projectId, environmentId); - if (!validated) throw new HttpRequestException(); - return Task.CompletedTask; - }); + DefaultBuildConfigurationsClient.Setup(a => + a.DeleteBuildConfigurationAsync( + It.IsAny(), // projectId + It.IsAny(), // environmentId + It.IsAny(), // buildConfigurationId + null, + 0, + CancellationToken.None + )).Returns((Guid projectId, Guid environmentId, long buildConfigurationId, bool _, int _, CancellationToken _) => + { + var validated = ValidateProjectEnvironment(projectId, environmentId); + if (!validated) throw new HttpRequestException(); + + return Task.CompletedTask; + }); DefaultBuildConfigurationsClient.Setup( a => diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingCoreDumpApiV1Mock.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingCoreDumpApiV1Mock.cs new file mode 100644 index 0000000..4a46871 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingCoreDumpApiV1Mock.cs @@ -0,0 +1,222 @@ +using Moq; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Api; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Mocks; + +public class GameServerHostingCoreDumpApiV1Mock +{ + public Mock DefaultCoreDumpClient = new(); + + List ValidProjects { get; } = new() + { + Guid.Parse(ValidProjectId) + }; + + List ValidEnvironments { get; } = new() + { + Guid.Parse(ValidEnvironmentId) + }; + + List ValidFleets { get; } = new() + { + Guid.Parse(CoreDumpMockFleetIdWithoutConfig), + Guid.Parse(CoreDumpMockFleetIdWithDisabledConfig), + Guid.Parse(CoreDumpMockFleetIdWithEnabledConfig), + }; + + public void SetUp() + { + DefaultCoreDumpClient = new Mock(); + DefaultCoreDumpClient.Setup(a => a.Configuration) + .Returns(new Configuration()); + + SetUpGetMethod(); + SetUpDeleteMethod(); + SetUpCreateMethod(); + SetUpUpdateMethod(); + } + + void SetUpGetMethod() + { + DefaultCoreDumpClient.Setup( + a => a.GetCoreDumpConfigAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + 0, + CancellationToken.None)) + .Returns( + ( + Guid projectId, + Guid environmentId, + Guid fleetId, + int timeout, + CancellationToken cancellationToken) => + { + if (!ValidateProjectEnvironmentFleet(projectId, environmentId, fleetId)) + { + throw new ApiException(400, "Bad Request", $"{{\"detail\": \"validation error\"}}"); + } + + return fleetId.ToString() switch + { + CoreDumpMockFleetIdWithDisabledConfig => Task.FromResult( + new GetCoreDumpConfig200Response( + credentials: new CredentialsForTheBucket(storageBucket: "testBucket"), + fleetId: fleetId, + state: GetCoreDumpConfig200Response.StateEnum.NUMBER_0, + storageType: GetCoreDumpConfig200Response.StorageTypeEnum.Gcs, + updatedAt: DateTime.UtcNow)), + CoreDumpMockFleetIdWithEnabledConfig => Task.FromResult( + new GetCoreDumpConfig200Response( + credentials: new CredentialsForTheBucket(storageBucket: "testBucket"), + fleetId: fleetId, + state: GetCoreDumpConfig200Response.StateEnum.NUMBER_1, + storageType: GetCoreDumpConfig200Response.StorageTypeEnum.Gcs, + updatedAt: DateTime.UtcNow)), + CoreDumpMockFleetIdWithoutConfig => throw new ApiException(404, "Not found"), + _ => throw new ApiException(400, "Bad Request", $"{{\"detail\": \"something went wrong\"}}") + }; + }); + } + + void SetUpDeleteMethod() + { + DefaultCoreDumpClient.Setup( + a => a.DeleteCoreDumpConfigAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + 0, + CancellationToken.None)) + .Returns( + ( + Guid projectId, + Guid environmentId, + Guid fleetId, + int timeout, + CancellationToken cancellationToken) => + { + if (!ValidateProjectEnvironmentFleet(projectId, environmentId, fleetId)) + { + throw new ApiException(400, "Bad Request", $"{{\"detail\": \"validation error\"}}"); + } + + return fleetId.ToString() switch + { + CoreDumpMockFleetIdWithDisabledConfig => Task.FromResult(""), + CoreDumpMockFleetIdWithEnabledConfig => Task.FromResult(""), + CoreDumpMockFleetIdWithoutConfig => throw new ApiException(404, "Not found"), + _ => throw new ApiException(400, "Bad Request", $"{{\"detail\": \"something went wrong\"}}") + }; + }); + } + + void SetUpCreateMethod() + { + DefaultCoreDumpClient.Setup( + a => a.PostCoreDumpConfigAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + 0, + CancellationToken.None)) + .Returns( + ( + Guid projectId, + Guid environmentId, + Guid fleetId, + CreateCoreDumpConfigRequest request, + int timeout, + CancellationToken cancellationToken) => + { + if (!ValidateProjectEnvironmentFleet(projectId, environmentId, fleetId)) + { + throw new ApiException(400, "Bad Request"); + } + + if (fleetId.ToString() != CoreDumpMockFleetIdWithoutConfig) + { + throw new ApiException( + 400, + "Bad request", + $"{{\"detail\": \"Core dump config already exists for fleet {fleetId}\"}}"); + } + + if (request.Credentials.StorageBucket != CoreDumpMockValidBucketName || + request.Credentials.ServiceAccountAccessId != CoreDumpMockValidAccessId || + request.Credentials.ServiceAccountPrivateKey != CoreDumpMockValidPrivateKey) + { + throw new ApiException(400, "Bad request", $"{{\"detail\": \"Invalid credentials\"}}"); + } + + return Task.FromResult( + new GetCoreDumpConfig200Response( + credentials: new CredentialsForTheBucket(storageBucket: request.Credentials.StorageBucket), + fleetId: fleetId, + state: (GetCoreDumpConfig200Response.StateEnum)request.State!, + storageType: (GetCoreDumpConfig200Response.StorageTypeEnum)request.StorageType, + updatedAt: DateTime.UtcNow)); + }); + } + + void SetUpUpdateMethod() + { + DefaultCoreDumpClient.Setup( + a => a.PutCoreDumpConfigAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + 0, + CancellationToken.None)) + .Returns( + ( + Guid projectId, + Guid environmentId, + Guid fleetId, + UpdateCoreDumpConfigRequest request, + int timeout, + CancellationToken cancellationToken) => + { + if (!ValidateProjectEnvironmentFleet(projectId, environmentId, fleetId)) + { + throw new ApiException(400, "Bad Request"); + } + + if (fleetId.ToString() == CoreDumpMockFleetIdWithoutConfig) + { + throw new ApiException( + 400, + "Bad request", + $"{{\"detail\": \"Core dump config does not exists for fleet {fleetId}\"}}"); + } + + if (request.Credentials.StorageBucket != CoreDumpMockValidBucketName || + request.Credentials.ServiceAccountAccessId != CoreDumpMockValidAccessId || + request.Credentials.ServiceAccountPrivateKey != CoreDumpMockValidPrivateKey) + { + throw new ApiException(400, "Bad request", $"{{\"detail\": \"Invalid credentials\"}}"); + } + + return Task.FromResult( + new GetCoreDumpConfig200Response( + credentials: new CredentialsForTheBucket(storageBucket: request.Credentials.StorageBucket), + fleetId: fleetId, + state: (GetCoreDumpConfig200Response.StateEnum)request.State!, + storageType: (GetCoreDumpConfig200Response.StorageTypeEnum)request.StorageType!, + updatedAt: DateTime.UtcNow)); + }); + } + + + bool ValidateProjectEnvironmentFleet(Guid projectId, Guid environmentId, Guid fleetId) + { + return ValidProjects.Contains(projectId) && + ValidEnvironments.Contains(environmentId) && + ValidFleets.Contains(fleetId); + } +} 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 9beb5d2..0bcada6 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 @@ -117,7 +117,7 @@ CancellationToken _ a => a.GenerateDownloadURLAsync( It.IsAny(), // projectId It.IsAny(), // environmentId - It.IsAny(), + It.IsAny(), 0, CancellationToken.None )) @@ -125,11 +125,11 @@ CancellationToken _ ( Guid _, Guid _, - GenerateDownloadURLRequest _, + GenerateContentURLRequest _, int _, CancellationToken _ ) => Task.FromResult( - new GenerateDownloadURLResponse( + new GenerateContentURLResponse( url: "https://example.com" ) ) diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingFleetsApiV1Mock.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingFleetsApiV1Mock.cs index f374471..cc810d5 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingFleetsApiV1Mock.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Mocks/GameServerHostingFleetsApiV1Mock.cs @@ -12,6 +12,7 @@ class GameServerHostingFleetsApiV1Mock new FleetListItem( FleetListItem.AllocationTypeEnum.ALLOCATION, new List(), + graceful: false, regions: new List(), id: new Guid(ValidFleetId), name: ValidFleetName, @@ -23,6 +24,7 @@ class GameServerHostingFleetsApiV1Mock new FleetListItem( FleetListItem.AllocationTypeEnum.ALLOCATION, new List(), + graceful: false, regions: new List(), id: new Guid(ValidFleetId2), name: ValidFleetName2, @@ -66,6 +68,7 @@ class GameServerHostingFleetsApiV1Mock { new(id: 1, name: "build config 1", buildName: "build 1", buildID: 1) }, + graceful: false, fleetRegions: k_TestFleetRegions, id: new Guid(ValidFleetId), name: ValidFleetName, @@ -81,6 +84,7 @@ class GameServerHostingFleetsApiV1Mock ), new Fleet( buildConfigurations: new List(), + graceful: false, fleetRegions: new List(), id: new Guid(ValidFleetId2), name: ValidFleetName2, @@ -185,11 +189,12 @@ public void SetUp() DefaultFleetsClient.Setup(a => a.CreateFleetAsync( It.IsAny(), // projectId - It.IsAny(), // environmentId + It.IsAny(), // environmentId, + It.IsAny(), // template fleet id It.IsAny(), 0, CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, FleetCreateRequest createReq, int _, CancellationToken _) => + )).Returns((Guid projectId, Guid environmentId, Guid? _, FleetCreateRequest createReq, int _, CancellationToken _) => { var validated = ValidateProjectEnvironment(projectId, environmentId); if (!validated) throw new HttpRequestException(); @@ -197,6 +202,7 @@ public void SetUp() var fleet = new Fleet( buildConfigurations: new List(), fleetRegions: new List(), + graceful: false, id: new Guid(ValidFleetId), name: createReq.Name, osFamily: (Fleet.OsFamilyEnum)createReq.OsFamily!, @@ -217,10 +223,11 @@ public void SetUp() a.CreateFleetAsync( It.IsAny(), // projectId It.IsAny(), // environmentId + It.IsAny(), // template Fleet id It.IsAny(), 0, CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, FleetCreateRequest createReq, int _, CancellationToken _) => + )).Returns((Guid projectId, Guid environmentId, Guid? _, FleetCreateRequest createReq, int _, CancellationToken _) => { var validated = ValidateProjectEnvironment(projectId, environmentId); if (!validated) throw new HttpRequestException(); @@ -229,6 +236,7 @@ public void SetUp() buildConfigurations: new List(), fleetRegions: new List(), id: new Guid(ValidFleetId), + graceful: false, name: createReq.Name, osFamily: (Fleet.OsFamilyEnum)createReq.OsFamily!, osName: OsNameLinux, @@ -290,9 +298,10 @@ CancellationToken _ DefaultFleetsClient.Setup(a => a.ListTemplateFleetRegionsAsync( It.IsAny(), // projectId It.IsAny(), // environmentId + It.IsAny(), // template Fleet id 0, CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, int _, CancellationToken _) => + )).Returns((Guid projectId, Guid environmentId, Guid? _, int _, CancellationToken _) => { var validated = ValidateProjectEnvironment(projectId, environmentId); if (!validated) throw new HttpRequestException(); @@ -303,9 +312,10 @@ CancellationToken _ It.IsAny(), // projectId It.IsAny(), // environmentId It.IsAny(), // fleetId + It.IsAny(), // template Fleet id 0, CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, Guid fleetId, int _, CancellationToken _) => + )).Returns((Guid projectId, Guid environmentId, Guid fleetId, Guid? _, int _, CancellationToken _) => { var validated = ValidateProjectEnvironment(projectId, environmentId); if (!validated) throw new HttpRequestException(); @@ -320,10 +330,11 @@ CancellationToken _ It.IsAny(), // projectId It.IsAny(), // environmentId It.IsAny(), // fleetId + It.IsAny(), // template Fleet id It.IsAny(), 0, CancellationToken.None - )).Returns((Guid projectId, Guid environmentId, Guid fleetId, AddRegionRequest _, int _, CancellationToken _) => + )).Returns((Guid projectId, Guid environmentId, Guid fleetId, Guid? _, AddRegionRequest _, int _, CancellationToken _) => { var validated = ValidateProjectEnvironment(projectId, environmentId); if (!validated) throw new HttpRequestException(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildConfigurationOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildConfigurationOutputTests.cs index fb49a07..560bed3 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildConfigurationOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildConfigurationOutputTests.cs @@ -51,17 +51,19 @@ public void ConstructBuildConfigurationOutputWithValidBuildConfiguration() Assert.That(output.BuildId, Is.EqualTo(m_BuildConfiguration!.BuildID)); Assert.That(output.BuildName, Is.EqualTo(m_BuildConfiguration!.BuildName)); Assert.That(output.CommandLine, Is.EqualTo(m_BuildConfiguration!.CommandLine)); - Assert.That(output.Cores, Is.EqualTo(m_BuildConfiguration!.Cores)); Assert.That(output.CreatedAt, Is.EqualTo(m_BuildConfiguration!.CreatedAt)); Assert.That(output.FleetId, Is.EqualTo(m_BuildConfiguration!.FleetID)); Assert.That(output.FleetName, Is.EqualTo(m_BuildConfiguration!.FleetName)); Assert.That(output.Id, Is.EqualTo(m_BuildConfiguration!.Id)); - Assert.That(output.Memory, Is.EqualTo(m_BuildConfiguration!.Memory)); Assert.That(output.Name, Is.EqualTo(m_BuildConfiguration!.Name)); Assert.That(output.QueryType, Is.EqualTo(m_BuildConfiguration!.QueryType)); - Assert.That(output.Speed, Is.EqualTo(m_BuildConfiguration!.Speed)); Assert.That(output.UpdatedAt, Is.EqualTo(m_BuildConfiguration!.UpdatedAt)); Assert.That(output.Version, Is.EqualTo(m_BuildConfiguration!._Version)); +#pragma warning disable CS0612 // Type or member is obsolete + Assert.That(output.Cores, Is.EqualTo(m_BuildConfiguration!.Cores)); + Assert.That(output.Memory, Is.EqualTo(m_BuildConfiguration!.Memory)); + Assert.That(output.Speed, Is.EqualTo(m_BuildConfiguration!.Speed)); +#pragma warning disable CS0612 // Type or member is obsolete for (var i = 0; i < m_BuildConfiguration!._Configuration.Count; i++) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildInstallsItemOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildInstallsItemOutputTests.cs index 015eb63..2e5369f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildInstallsItemOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildInstallsItemOutputTests.cs @@ -11,6 +11,7 @@ class BuildInstallsItemOutputTests public void SetUp() { m_Install = new BuildListInner1( + ValidBuildVersionName, new CCDDetails( Guid.Parse(ValidBucketId), Guid.Parse(ValidReleaseId) @@ -47,30 +48,33 @@ public void SetUp() public void ConstructBuildInstallsItemOutputWithValidInstalls() { BuildInstallsItemOutput output = new(m_Install!); - Assert.Multiple(() => - { - Assert.That(output.FleetName, Is.EqualTo(m_Install!.FleetName)); - Assert.That(output.Ccd?.BucketId, Is.EqualTo(m_Install!.Ccd.BucketID)); - Assert.That(output.Ccd?.ReleaseId, Is.EqualTo(m_Install!.Ccd.ReleaseID)); - Assert.That(output.Container, Is.EqualTo(m_Install!.Container)); - Assert.That(output.PendingMachines, Is.EqualTo(m_Install!.PendingMachines)); - Assert.That(output.CompletedMachines, Is.EqualTo(m_Install!.CompletedMachines)); - - for (var i = 0; i < m_Install!.Failures.Count; i++) + Assert.Multiple( + () => { - Assert.That(output.Failures[i].MachineId, Is.EqualTo(m_Install!.Failures[i].MachineID)); - Assert.That(output.Failures[i].Reason, Is.EqualTo(m_Install!.Failures[i].Reason)); - Assert.That(output.Failures[i].Updated, Is.EqualTo(m_Install!.Failures[i].Updated)); - } + Assert.That(output.FleetName, Is.EqualTo(m_Install!.FleetName)); + Assert.That(output.Ccd?.BucketId, Is.EqualTo(m_Install!.Ccd.BucketID)); + Assert.That(output.Ccd?.ReleaseId, Is.EqualTo(m_Install!.Ccd.ReleaseID)); + Assert.That(output.Container, Is.EqualTo(m_Install!.Container)); + Assert.That(output.PendingMachines, Is.EqualTo(m_Install!.PendingMachines)); + Assert.That(output.CompletedMachines, Is.EqualTo(m_Install!.CompletedMachines)); - for (var i = 0; i < m_Install!.Regions.Count; i++) - { - Assert.That(output.Regions[i].RegionName, Is.EqualTo(m_Install!.Regions[i].RegionName)); - Assert.That(output.Regions[i].PendingMachines, Is.EqualTo(m_Install!.Regions[i].PendingMachines)); - Assert.That(output.Regions[i].CompletedMachines, Is.EqualTo(m_Install!.Regions[i].CompletedMachines)); - Assert.That(output.Regions[i].Failures, Is.EqualTo(m_Install!.Regions[i].Failures)); - } - }); + for (var i = 0; i < m_Install!.Failures.Count; i++) + { + Assert.That(output.Failures[i].MachineId, Is.EqualTo(m_Install!.Failures[i].MachineID)); + Assert.That(output.Failures[i].Reason, Is.EqualTo(m_Install!.Failures[i].Reason)); + Assert.That(output.Failures[i].Updated, Is.EqualTo(m_Install!.Failures[i].Updated)); + } + + for (var i = 0; i < m_Install!.Regions.Count; i++) + { + Assert.That(output.Regions[i].RegionName, Is.EqualTo(m_Install!.Regions[i].RegionName)); + Assert.That(output.Regions[i].PendingMachines, Is.EqualTo(m_Install!.Regions[i].PendingMachines)); + Assert.That( + output.Regions[i].CompletedMachines, + Is.EqualTo(m_Install!.Regions[i].CompletedMachines)); + Assert.That(output.Regions[i].Failures, Is.EqualTo(m_Install!.Regions[i].Failures)); + } + }); } [Test] diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildInstallsOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildInstallsOutputTests.cs index 300b452..b4422aa 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildInstallsOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildInstallsOutputTests.cs @@ -12,7 +12,9 @@ public void SetUp() { m_Installs = new List(); - m_Installs.Add(new BuildListInner1( + m_Installs.Add( + new BuildListInner1( + ValidBuildVersionName, new CCDDetails( Guid.Parse(ValidBucketId), Guid.Parse(ValidReleaseId) @@ -43,7 +45,9 @@ public void SetUp() ) ); - m_Installs.Add(new BuildListInner1( + m_Installs.Add( + new BuildListInner1( + ValidBuildVersionName, new CCDDetails( Guid.Parse(ValidBucketId), Guid.Parse(ValidReleaseId) @@ -83,37 +87,45 @@ public void ConstructBuildInstallsOutputWithValidInstalls() BuildInstallsOutput output = new(m_Installs!); Assert.That(output, Has.Count.EqualTo(m_Installs!.Count)); for (var i = 0; i < output.Count; i++) - Assert.Multiple(() => - { - Assert.That(output[i].FleetName, Is.EqualTo(m_Installs[i].FleetName)); - Assert.That(output[i].Ccd?.BucketId, Is.EqualTo(m_Installs[i].Ccd.BucketID)); - Assert.That(output[i].Ccd?.ReleaseId, Is.EqualTo(m_Installs[i].Ccd.ReleaseID)); - Assert.That(output[i].Container, Is.EqualTo(m_Installs[i].Container)); - Assert.That(output[i].PendingMachines, Is.EqualTo(m_Installs[i].PendingMachines)); - Assert.That(output[i].CompletedMachines, Is.EqualTo(m_Installs[i].CompletedMachines)); - - for (var j = 0; j < output[i].Failures.Count; j++) + Assert.Multiple( + () => { - Assert.That(output[i].Failures[j].MachineId, - Is.EqualTo(m_Installs[i].Failures[j].MachineID)); - Assert.That(output[i].Failures[j].Reason, - Is.EqualTo(m_Installs[i].Failures[j].Reason)); - Assert.That(output[i].Failures[j].Updated, - Is.EqualTo(m_Installs[i].Failures[j].Updated)); - } + Assert.That(output[i].FleetName, Is.EqualTo(m_Installs[i].FleetName)); + Assert.That(output[i].Ccd?.BucketId, Is.EqualTo(m_Installs[i].Ccd.BucketID)); + Assert.That(output[i].Ccd?.ReleaseId, Is.EqualTo(m_Installs[i].Ccd.ReleaseID)); + Assert.That(output[i].Container, Is.EqualTo(m_Installs[i].Container)); + Assert.That(output[i].PendingMachines, Is.EqualTo(m_Installs[i].PendingMachines)); + Assert.That(output[i].CompletedMachines, Is.EqualTo(m_Installs[i].CompletedMachines)); - for (var j = 0; j < output[i].Regions.Count; j++) - { - Assert.That(output[i].Regions[j].RegionName, - Is.EqualTo(m_Installs[i].Regions[j].RegionName)); - Assert.That(output[i].Regions[j].PendingMachines, - Is.EqualTo(m_Installs[i].Regions[j].PendingMachines)); - Assert.That(output[i].Regions[j].CompletedMachines, - Is.EqualTo(m_Installs[i].Regions[j].CompletedMachines)); - Assert.That(output[i].Regions[j].Failures, - Is.EqualTo(m_Installs[i].Regions[j].Failures)); - } - }); + for (var j = 0; j < output[i].Failures.Count; j++) + { + Assert.That( + output[i].Failures[j].MachineId, + Is.EqualTo(m_Installs[i].Failures[j].MachineID)); + Assert.That( + output[i].Failures[j].Reason, + Is.EqualTo(m_Installs[i].Failures[j].Reason)); + Assert.That( + output[i].Failures[j].Updated, + Is.EqualTo(m_Installs[i].Failures[j].Updated)); + } + + for (var j = 0; j < output[i].Regions.Count; j++) + { + Assert.That( + output[i].Regions[j].RegionName, + Is.EqualTo(m_Installs[i].Regions[j].RegionName)); + Assert.That( + output[i].Regions[j].PendingMachines, + Is.EqualTo(m_Installs[i].Regions[j].PendingMachines)); + Assert.That( + output[i].Regions[j].CompletedMachines, + Is.EqualTo(m_Installs[i].Regions[j].CompletedMachines)); + Assert.That( + output[i].Regions[j].Failures, + Is.EqualTo(m_Installs[i].Regions[j].Failures)); + } + }); } [Test] diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildListOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildListOutputTests.cs index b60b0f2..d142f02 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildListOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildListOutputTests.cs @@ -16,6 +16,7 @@ public void SetUp() buildID: 1, buildName: "Test Build 1", buildConfigurations: 1, + buildVersionName: ValidBuildVersionName, syncStatus: BuildListInner.SyncStatusEnum.SYNCED, ccd: new CCDDetails( Guid.Parse(ValidBucketId), @@ -28,6 +29,7 @@ public void SetUp() buildID: 2, buildName: "Test Build 2", buildConfigurations: 2, + buildVersionName: ValidBuildVersionName, syncStatus: BuildListInner.SyncStatusEnum.PENDING, container: new ContainerImage("2"), osFamily: BuildListInner.OsFamilyEnum.LINUX, @@ -44,20 +46,21 @@ public void ConstructBuildListOutputWithValidList() BuildListOutput output = new(m_Builds!); Assert.That(output, Has.Count.EqualTo(m_Builds!.Count)); for (var i = 0; i < output.Count; i++) - Assert.Multiple(() => - { - Assert.That(output[i].BuildId, Is.EqualTo(m_Builds[i].BuildID)); - Assert.That(output[i].BuildName, Is.EqualTo(m_Builds[i].BuildName)); - Assert.That(output[i].BuildConfigurations, Is.EqualTo(m_Builds[i].BuildConfigurations)); - Assert.That(output[i].SyncStatus, Is.EqualTo(m_Builds[i].SyncStatus)); - // ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Assert.That(output[i].Ccd?.BucketId, Is.EqualTo(m_Builds[i].Ccd?.BucketID)); - Assert.That(output[i].Ccd?.ReleaseId, Is.EqualTo(m_Builds[i].Ccd?.ReleaseID)); - // ReSharper restore ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Assert.That(output[i].Container, Is.EqualTo(m_Builds[i].Container)); - Assert.That(output[i].OsFamily, Is.EqualTo(m_Builds[i].OsFamily)); - Assert.That(output[i].Updated, Is.EqualTo(m_Builds[i].Updated)); - }); + Assert.Multiple( + () => + { + Assert.That(output[i].BuildId, Is.EqualTo(m_Builds[i].BuildID)); + Assert.That(output[i].BuildName, Is.EqualTo(m_Builds[i].BuildName)); + Assert.That(output[i].BuildConfigurations, Is.EqualTo(m_Builds[i].BuildConfigurations)); + Assert.That(output[i].SyncStatus, Is.EqualTo(m_Builds[i].SyncStatus)); + // ReSharper disable ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + Assert.That(output[i].Ccd?.BucketId, Is.EqualTo(m_Builds[i].Ccd?.BucketID)); + Assert.That(output[i].Ccd?.ReleaseId, Is.EqualTo(m_Builds[i].Ccd?.ReleaseID)); + // ReSharper restore ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + Assert.That(output[i].Container, Is.EqualTo(m_Builds[i].Container)); + Assert.That(output[i].OsFamily, Is.EqualTo(m_Builds[i].OsFamily)); + Assert.That(output[i].Updated, Is.EqualTo(m_Builds[i].Updated)); + }); } [Test] @@ -67,7 +70,8 @@ public void BuildListOutputToString() var sb = new StringBuilder(); foreach (var build in output) { - sb.AppendLine($"- buildName: {build.BuildName}"); + sb.AppendLine($"- buildVersionName: {build.BuildVersionName}"); + sb.AppendLine($" buildName: {build.BuildName}"); sb.AppendLine($" buildId: {build.BuildId}"); sb.AppendLine($" osFamily: {build.OsFamily}"); sb.AppendLine($" updated: {build.Updated}"); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildOutputTests.cs index 5b4f45a..e236316 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/BuildOutputTests.cs @@ -14,6 +14,7 @@ public void SetUp() buildID: 1, buildName: "Test Build 1", buildConfigurations: 1, + buildVersionName: ValidBuildVersionName, syncStatus: BuildListInner.SyncStatusEnum.SYNCED, ccd: new CCDDetails( Guid.Parse(ValidBucketId), @@ -26,6 +27,7 @@ public void SetUp() 1, "Test Build 1", syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCED, + buildVersionName: ValidBuildVersionName, ccd: new CCDDetails( Guid.Parse(ValidBucketId), Guid.Parse(ValidReleaseId) @@ -42,31 +44,35 @@ public void SetUp() public void ConstructBuildOutputWithValidBuild() { BuildOutput output = new(m_BuildListItem!); - Assert.Multiple(() => - { - Assert.That(output.BuildId, Is.EqualTo(m_BuildListItem!.BuildID)); - Assert.That(output.BuildName, Is.EqualTo(m_BuildListItem!.BuildName)); - Assert.That(output.BuildConfigurations, Is.EqualTo(m_BuildListItem!.BuildConfigurations)); - Assert.That(output.SyncStatus, Is.EqualTo(m_BuildListItem!.SyncStatus)); - Assert.That(output.Ccd?.BucketId, Is.EqualTo(m_BuildListItem!.Ccd.BucketID)); - Assert.That(output.Ccd?.ReleaseId, Is.EqualTo(m_BuildListItem!.Ccd.ReleaseID)); - Assert.That(output.Container, Is.EqualTo(m_BuildListItem!.Container)); - Assert.That(output.OsFamily, Is.EqualTo(m_BuildListItem!.OsFamily)); - Assert.That(output.Updated, Is.EqualTo(m_BuildListItem!.Updated)); - }); + Assert.Multiple( + () => + { + Assert.That(output.BuildId, Is.EqualTo(m_BuildListItem!.BuildID)); + Assert.That(output.BuildName, Is.EqualTo(m_BuildListItem!.BuildName)); + Assert.That(output.BuildConfigurations, Is.EqualTo(m_BuildListItem!.BuildConfigurations)); + Assert.That(output.SyncStatus, Is.EqualTo(m_BuildListItem!.SyncStatus)); + Assert.That(output.Ccd?.BucketId, Is.EqualTo(m_BuildListItem!.Ccd.BucketID)); + Assert.That(output.Ccd?.ReleaseId, Is.EqualTo(m_BuildListItem!.Ccd.ReleaseID)); + Assert.That(output.Container, Is.EqualTo(m_BuildListItem!.Container)); + Assert.That(output.OsFamily, Is.EqualTo(m_BuildListItem!.OsFamily)); + Assert.That(output.Updated, Is.EqualTo(m_BuildListItem!.Updated)); + }); output = new BuildOutput(m_BuildCreateResponse!); - Assert.Multiple(() => - { - Assert.That(output.BuildId, Is.EqualTo(m_BuildCreateResponse!.BuildID)); - Assert.That(output.BuildName, Is.EqualTo(m_BuildCreateResponse!.BuildName)); - Assert.That(output.BuildConfigurations, Is.Null); - Assert.That(output.SyncStatus, Is.EqualTo((BuildListInner.SyncStatusEnum)m_BuildCreateResponse!.SyncStatus)); - Assert.That(output.Ccd?.BucketId, Is.EqualTo(m_BuildCreateResponse!.Ccd.BucketID)); - Assert.That(output.Ccd?.ReleaseId, Is.EqualTo(m_BuildCreateResponse!.Ccd.ReleaseID)); - Assert.That(output.Container, Is.EqualTo(m_BuildCreateResponse!.Container)); - Assert.That(output.OsFamily, Is.EqualTo((BuildListInner.OsFamilyEnum?)m_BuildCreateResponse!.OsFamily)); - Assert.That(output.Updated, Is.EqualTo(m_BuildCreateResponse!.Updated)); - }); + Assert.Multiple( + () => + { + Assert.That(output.BuildId, Is.EqualTo(m_BuildCreateResponse!.BuildID)); + Assert.That(output.BuildName, Is.EqualTo(m_BuildCreateResponse!.BuildName)); + Assert.That(output.BuildConfigurations, Is.Null); + Assert.That( + output.SyncStatus, + Is.EqualTo((BuildListInner.SyncStatusEnum)m_BuildCreateResponse!.SyncStatus)); + Assert.That(output.Ccd?.BucketId, Is.EqualTo(m_BuildCreateResponse!.Ccd.BucketID)); + Assert.That(output.Ccd?.ReleaseId, Is.EqualTo(m_BuildCreateResponse!.Ccd.ReleaseID)); + Assert.That(output.Container, Is.EqualTo(m_BuildCreateResponse!.Container)); + Assert.That(output.OsFamily, Is.EqualTo((BuildListInner.OsFamilyEnum?)m_BuildCreateResponse!.OsFamily)); + Assert.That(output.Updated, Is.EqualTo(m_BuildCreateResponse!.Updated)); + }); } [Test] @@ -74,6 +80,7 @@ public void BuildOutputToString() { var sb = new StringBuilder(); BuildOutput output = new(m_BuildListItem!); + sb.AppendLine($"buildVersionName: {ValidBuildVersionName}"); sb.AppendLine("buildName: Test Build 1"); sb.AppendLine("buildId: 1"); sb.AppendLine("osFamily: LINUX"); @@ -86,6 +93,7 @@ public void BuildOutputToString() Assert.That(output.ToString(), Is.EqualTo(sb.ToString())); sb.Clear(); output = new BuildOutput(m_BuildCreateResponse!); + sb.AppendLine($"buildVersionName: {ValidBuildVersionName}"); sb.AppendLine("buildName: Test Build 1"); sb.AppendLine("buildId: 1"); sb.AppendLine("osFamily: LINUX"); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/CoreDumpOutputTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/CoreDumpOutputTest.cs new file mode 100644 index 0000000..e91e724 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/CoreDumpOutputTest.cs @@ -0,0 +1,70 @@ +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Model; + +[TestFixture] +[TestOf(typeof(CoreDumpOutput))] +public class CoreDumpOutputTest +{ + static IEnumerable TestCases() + { + var fleetId = Guid.NewGuid(); + var updatedAt = DateTime.UtcNow; + var credentials = new CredentialsForTheBucket + { + StorageBucket = "testBucket", + }; + yield return new TestCaseData( + new GetCoreDumpConfig200Response + { + StorageType = GetCoreDumpConfig200Response.StorageTypeEnum.Gcs, + Credentials = credentials, + FleetId = fleetId, + State = GetCoreDumpConfig200Response.StateEnum.NUMBER_1, + UpdatedAt = updatedAt + }, + "gcs", + new CoreDumpCredentialsOutput(credentials), + fleetId, + "enabled", + updatedAt + ).SetName("Core Dump Config should be created"); + yield return new TestCaseData( + new GetCoreDumpConfig200Response + { + Credentials = credentials, + FleetId = fleetId, + State = GetCoreDumpConfig200Response.StateEnum.NUMBER_1, + UpdatedAt = updatedAt + }, + "gcs", + new CoreDumpCredentialsOutput(credentials), + fleetId, + "enabled", + updatedAt + ).SetName("unknown storage"); + } + + [TestCaseSource(nameof(TestCases))] + public void CoreDumpOutput( + GetCoreDumpConfig200Response response, + string storageType, + CoreDumpCredentialsOutput credentials, + Guid fleetId, + string state, + DateTime updatedAt) + { + var actual = new CoreDumpOutput(response); + + Assert.Multiple( + () => + { + Assert.That(actual.StorageType, Is.EqualTo(storageType)); + Assert.That(actual.Credentials.StorageBucket, Is.EqualTo(credentials.StorageBucket)); + Assert.That(actual.FleetId, Is.EqualTo(fleetId)); + Assert.That(actual.State, Is.EqualTo(state)); + Assert.That(actual.UpdatedAt, Is.EqualTo(updatedAt)); + }); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetGetOutputTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetGetOutputTests.cs index 531dbb4..1fa6cc3 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetGetOutputTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Model/FleetGetOutputTests.cs @@ -13,6 +13,7 @@ public void SetUp() m_Fleet = new Fleet( buildConfigurations: new List(), fleetRegions: new List(), + graceful: false, id: new Guid(ValidFleetId), name: ValidFleetName, osFamily: Fleet.OsFamilyEnum.LINUX, 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 2f0db26..ba5e47f 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 @@ -13,6 +13,7 @@ public void SetUp() m_Fleet = new FleetListItem( allocationType: FleetListItem.AllocationTypeEnum.ALLOCATION, new List(), + graceful: false, regions: new List(), id: new Guid(ValidFleetId), name: ValidFleetName, diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Service/GameServerHostingServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Service/GameServerHostingServiceTests.cs index 321b0b2..2b2cb50 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Service/GameServerHostingServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Service/GameServerHostingServiceTests.cs @@ -33,6 +33,9 @@ public void SetUp() m_BuildConfigurationsApi = new GameServerHostingBuildConfigurationsApiV1Mock(); m_BuildConfigurationsApi.SetUp(); + m_CoreDumpApi = new GameServerHostingCoreDumpApiV1Mock(); + m_CoreDumpApi.SetUp(); + m_GameServerHostingService = new GameServerHostingService( m_AuthenticationService.Object, m_BuildsApi.DefaultBuildsClient.Object, @@ -40,6 +43,7 @@ public void SetUp() m_FilesApi.DefaultFilesClient.Object, m_FleetsApi.DefaultFleetsClient.Object, m_MachinesApi.DefaultMachinesClient.Object, + m_CoreDumpApi.DefaultCoreDumpClient.Object, m_ServersApi.DefaultServersClient.Object ); } @@ -52,6 +56,7 @@ public void SetUp() GameServerHostingServersApiV1Mock? m_ServersApi; GameServerHostingService? m_GameServerHostingService; GameServerHostingBuildConfigurationsApiV1Mock? m_BuildConfigurationsApi; + GameServerHostingCoreDumpApiV1Mock? m_CoreDumpApi; [Test] public async Task AuthorizeGameServerHostingService() @@ -74,11 +79,15 @@ public async Task AuthorizeGameServerHostingService() m_ServersApi!.DefaultServersClient.Object.Configuration.DefaultHeaders["Authorization"], Is.EqualTo($"Basic {TestAccessToken}")); Assert.That( - m_BuildConfigurationsApi!.DefaultBuildConfigurationsClient.Object.Configuration.DefaultHeaders["Authorization"], + m_BuildConfigurationsApi!.DefaultBuildConfigurationsClient.Object.Configuration.DefaultHeaders[ + "Authorization"], Is.EqualTo($"Basic {TestAccessToken}")); Assert.That( m_MachinesApi!.DefaultMachinesClient.Object.Configuration.DefaultHeaders["Authorization"], Is.EqualTo($"Basic {TestAccessToken}")); + Assert.That( + m_CoreDumpApi!.DefaultCoreDumpClient.Object.Configuration.DefaultHeaders["Authorization"], + Is.EqualTo($"Basic {TestAccessToken}")); }); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/ApiExceptionConverterTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/ApiExceptionConverterTest.cs new file mode 100644 index 0000000..eab2eee --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/ApiExceptionConverterTest.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Services; + +[TestFixture] +[TestOf(typeof(ApiExceptionConverter))] +public class ApiExceptionConverterTest +{ + [TestCase("test", "Error parsing", typeof(JsonReaderException))] + [TestCase("{\"detail\":\"test\"}", "test", typeof(CliException))] + [TestCase("{\"detail\":\"\"}", "", typeof(CliException))] + [TestCase(null, "", typeof(ApiException))] + public void Convert(string payload, string message, Type exceptionType) + { + try + { + var e = new ApiException(400, "", payload); + ApiExceptionConverter.Convert(e); + } + catch (Exception e) + { + Assert.That(e, Is.TypeOf(exceptionType)); + Assert.That(e.Message, Does.Contain(message)); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/BuildClientTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/BuildClientTests.cs index af2da33..739726d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/BuildClientTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/BuildClientTests.cs @@ -11,8 +11,8 @@ namespace Unity.Services.Cli.GameServerHosting.UnitTest.Services; public class BuildClientTests { - Mock? m_MockApi; BuildClient? m_Client; + Mock? m_MockApi; [SetUp] public void SetUp() @@ -34,10 +34,16 @@ public async Task FindByName_WithNoResults_ReturnsNull() [Test] public async Task FindByName_WithOneResult_ReturnsId() { - SetupListResponse(new List - { - new (0, 0, "test", ccd: new CCDDetails()) - }); + SetupListResponse( + new List + { + new( + 0, + 0, + "test", + buildVersionName: ValidBuildVersionName, + ccd: new CCDDetails()) + }); var res = await m_Client!.FindByName("test"); @@ -47,11 +53,22 @@ public async Task FindByName_WithOneResult_ReturnsId() [Test] public void FindByName_WithMultipleResults_ThrowsDuplicateException() { - SetupListResponse(new List - { - new (0, 0, "test", ccd: new CCDDetails()), - new (0, 0, "test", ccd: new CCDDetails()) - }); + SetupListResponse( + new List + { + new( + 0, + 0, + "test", + buildVersionName: ValidBuildVersionName, + ccd: new CCDDetails()), + new( + 0, + 0, + "test", + buildVersionName: ValidBuildVersionName, + ccd: new CCDDetails()) + }); Assert.ThrowsAsync(async () => await m_Client!.FindByName("test")); } @@ -59,14 +76,30 @@ public void FindByName_WithMultipleResults_ThrowsDuplicateException() [Test] public async Task Create_CallsCreateBuildAsync() { - m_MockApi!.Setup(a => - a.CreateBuildAsync(Guid.Empty, Guid.Empty, It.IsAny(), default, default)) - .ReturnsAsync(new CreateBuild200Response(buildName: "test", ccd: new CCDDetails())); + m_MockApi!.Setup( + a => + a.CreateBuildAsync( + Guid.Empty, + Guid.Empty, + It.IsAny(), + default, + default)) + .ReturnsAsync( + new CreateBuild200Response( + buildName: "test", + ccd: new CCDDetails(), + buildVersionName: ValidBuildVersionName)); await m_Client!.Create("test", new MultiplayConfig.BuildDefinition()); - m_MockApi!.Verify(a => - a.CreateBuildAsync(Guid.Empty, Guid.Empty, It.IsAny(), default, default)); + m_MockApi!.Verify( + a => + a.CreateBuildAsync( + Guid.Empty, + Guid.Empty, + It.IsAny(), + default, + default)); } [Test] @@ -74,60 +107,105 @@ public async Task CreateVersion_CallsCreateNewBuildVersionAsync() { await m_Client!.CreateVersion(new BuildId(), new CloudBucketId()); - m_MockApi!.Verify(a => - a.CreateNewBuildVersionAsync(Guid.Empty, Guid.Empty, 0, It.IsAny(), default, default)); + m_MockApi!.Verify( + a => + a.CreateNewBuildVersionAsync( + Guid.Empty, + Guid.Empty, + 0, + It.IsAny(), + default, + default)); } [Test] public async Task IsSynced_SucceedsWhenSynced() { - m_MockApi!.Setup(c => c.GetBuildAsync(Guid.Empty, Guid.Empty, 0, default, default)) - .ReturnsAsync(new CreateBuild200Response( - buildName: string.Empty, - syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCED)); - - Assert.That(await m_Client!.IsSynced(new BuildId { Id = 0 })); + m_MockApi!.Setup( + c => c.GetBuildAsync( + Guid.Empty, + Guid.Empty, + 0, + default, + default)) + .ReturnsAsync( + new CreateBuild200Response( + buildName: string.Empty, + buildVersionName: ValidBuildVersionName, + syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCED)); + + Assert.That( + await m_Client!.IsSynced( + new BuildId + { + Id = 0 + })); } [Test] public async Task IsSynced_FailsWhenNotSynced() { - m_MockApi!.Setup(c => c.GetBuildAsync(Guid.Empty, Guid.Empty, 0, default, default)) - .ReturnsAsync(new CreateBuild200Response( - buildName: string.Empty, - syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCING)); - - Assert.That(await m_Client!.IsSynced(new BuildId { Id = 0 }), Is.Not.True); + m_MockApi!.Setup( + c => c.GetBuildAsync( + Guid.Empty, + Guid.Empty, + 0, + default, + default)) + .ReturnsAsync( + new CreateBuild200Response( + buildName: string.Empty, + buildVersionName: ValidBuildVersionName, + syncStatus: CreateBuild200Response.SyncStatusEnum.SYNCING)); + + Assert.That( + await m_Client!.IsSynced( + new BuildId + { + Id = 0 + }), + Is.Not.True); } [Test] public void IsSynced_ThrowsOnFailure() { - m_MockApi!.Setup(c => c.GetBuildAsync(Guid.Empty, Guid.Empty, 0, default, default)) - .ReturnsAsync(new CreateBuild200Response( - buildName: string.Empty, - syncStatus: CreateBuild200Response.SyncStatusEnum.FAILED)); - - Assert.ThrowsAsync(() => m_Client!.IsSynced(new BuildId - { - Id = 0 - })); - } - - void SetupListResponse(List results) - { - m_MockApi!.Setup(a => - a.ListBuildsAsync( + m_MockApi!.Setup( + c => c.GetBuildAsync( Guid.Empty, Guid.Empty, - null, - null, - null, - null, - null, - It.IsAny(), + 0, default, default)) + .ReturnsAsync( + new CreateBuild200Response( + buildName: string.Empty, + buildVersionName: ValidBuildVersionName, + syncStatus: CreateBuild200Response.SyncStatusEnum.FAILED)); + + Assert.ThrowsAsync( + () => m_Client!.IsSynced( + new BuildId + { + Id = 0 + })); + } + + void SetupListResponse(List results) + { + m_MockApi!.Setup( + a => + a.ListBuildsAsync( + Guid.Empty, + Guid.Empty, + null, + null, + null, + null, + null, + It.IsAny(), + default, + default)) .ReturnsAsync(results); } } 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 f9d89a7..751ef06 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 @@ -109,7 +109,7 @@ public async Task UploadBuildEntries_WhenOrphanExists_DeletesOrphan() 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(), 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(), It.IsAny())) .Returns(Task.FromResult(new CcdGetEntries200ResponseInner(signedUrl: "https://signed.url.example.com"))); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/CoreDumpStateConverterTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/CoreDumpStateConverterTest.cs new file mode 100644 index 0000000..372f692 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/CoreDumpStateConverterTest.cs @@ -0,0 +1,18 @@ +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Services; + +[TestFixture] +[TestOf(typeof(CoreDumpStateConverter))] +public class CoreDumpStateConverterTest +{ + [TestCase(GetCoreDumpConfig200Response.StateEnum.NUMBER_0, "disabled")] + [TestCase(GetCoreDumpConfig200Response.StateEnum.NUMBER_1, "enabled")] + [TestCase((GetCoreDumpConfig200Response.StateEnum)3, "unknown")] + [TestCase(null, "unknown")] + public void ConvertToString(GetCoreDumpConfig200Response.StateEnum? state, string expectedString) + { + Assert.That(CoreDumpStateConverter.ConvertToString(state), Is.EqualTo(expectedString)); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/FleetsClientTests.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/FleetsClientTests.cs index 280bdb4..d4c82b6 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/FleetsClientTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/FleetsClientTests.cs @@ -50,6 +50,7 @@ public async Task FindByName_WithOneResult_ReturnsId() new FleetListItem( allocationType: FleetListItem.AllocationTypeEnum.ALLOCATION, new List(), + graceful: false, Guid.Empty, name: "test", osName: string.Empty, @@ -81,6 +82,7 @@ public void FindByName_WithMultipleResults_ThrowsDuplicateException() new FleetListItem( allocationType: FleetListItem.AllocationTypeEnum.ALLOCATION, new List(), + graceful: false, Guid.Empty, name: "test", osName: string.Empty, @@ -94,6 +96,7 @@ public void FindByName_WithMultipleResults_ThrowsDuplicateException() new FleetListItem( allocationType: FleetListItem.AllocationTypeEnum.ALLOCATION, new List(), + graceful: false, Guid.Empty, name: "test", osName: string.Empty, @@ -113,11 +116,11 @@ public void FindByName_WithMultipleResults_ThrowsDuplicateException() public async Task Create_CallsCreateApi() { m_MockApi!.Setup(a => - a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, default, default)) + a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, null, default, default)) .ReturnsAsync(new List()); m_MockApi.Setup(a => - a.CreateFleetAsync(Guid.Empty, Guid.Empty, It.IsAny(), default, default)) + a.CreateFleetAsync(Guid.Empty, Guid.Empty, null, It.IsAny(), default, default)) .ReturnsAsync(new Fleet( buildConfigurations: new List(), fleetRegions: new List(), @@ -132,14 +135,14 @@ public async Task Create_CallsCreateApi() await m_Client!.Create("test", new List(), new MultiplayConfig.FleetDefinition()); m_MockApi.Verify(a => - a.CreateFleetAsync(Guid.Empty, Guid.Empty, It.IsAny(), default, default)); + a.CreateFleetAsync(Guid.Empty, Guid.Empty, null, It.IsAny(), default, default)); } [Test] public async Task Update_CallsTheUpdateApi() { m_MockApi!.Setup(a => - a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, default, default)) + a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, null, default, default)) .ReturnsAsync(new List()); m_MockApi.Setup(a => @@ -166,7 +169,7 @@ public async Task Update_RemovesOldFleets() { var regionId = Guid.NewGuid(); m_MockApi!.Setup(a => - a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, default, default)) + a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, null, default, default)) .ReturnsAsync(new List()); m_MockApi.Setup(a => @@ -195,7 +198,7 @@ public async Task Update_AddsNewFleets() { var regionId = Guid.NewGuid(); m_MockApi!.Setup(a => - a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, default, default)) + a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, null, default, default)) .ReturnsAsync(new List { new ("North-America", regionId) @@ -223,7 +226,7 @@ public async Task Update_AddsNewFleets() }); m_MockApi.Verify(f => - f.AddFleetRegionAsync(Guid.Empty, Guid.Empty, Guid.Empty, It.IsAny(), default, default)); + f.AddFleetRegionAsync(Guid.Empty, Guid.Empty, Guid.Empty, null, It.IsAny(), default, default)); } [Test] @@ -231,7 +234,7 @@ public async Task Update_UpdatesExistingFleets() { var regionId = Guid.NewGuid(); m_MockApi!.Setup(a => - a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, default, default)) + a.ListTemplateFleetRegionsAsync(Guid.Empty, Guid.Empty, null, default, default)) .ReturnsAsync(new List { new ("North-America", regionId) diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/GcsCredentialParserTest.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/GcsCredentialParserTest.cs new file mode 100644 index 0000000..a1c5e0a --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting.UnitTest/Services/GcsCredentialParserTest.cs @@ -0,0 +1,91 @@ +using System.IO.Abstractions; +using Moq; +using Unity.Services.Cli.GameServerHosting.Services; + +namespace Unity.Services.Cli.GameServerHosting.UnitTest.Services; + +[TestFixture] +[TestOf(typeof(GcsCredentialParser))] +public class GcsCredentialParserTest +{ + const string k_ValidFilePath = "validFilePath"; + + + [TestCase( + k_ValidFilePath, + "{\"client_email\":\"valid access id\",\"private_key\":\"valid private key\"}", + "valid access id", + "valid private key", + TestName = "happy path" + )] + [TestCase( + "invalid path", + "", + "valid access id", + "valid private key", + true, + "File not found", + TestName = "file doesn't exist" + )] + [TestCase( + k_ValidFilePath, + "qwdqw qwdqw", + "valid access id", + "valid private key", + true, + "Invalid JSON format", + TestName = "invalid json" + )] + [TestCase( + k_ValidFilePath, + "{}", + "valid access id", + "valid private key", + true, + "`private_key` or `client_email` are empty", + TestName = "empty credentials" + )] + public void Parse( + string path, + string content, + string expectedAccessId, + string expectedPrivateKey, + bool expectedException = false, + string expectedExceptionMessage = "") + { + var mockFileSystem = new Mock(); + + mockFileSystem.Setup(m => m.Exists(It.Is(s => s == k_ValidFilePath))).Returns(true); + mockFileSystem.Setup(m => m.ReadAllText(It.Is(s => s == k_ValidFilePath))).Returns(content); + + var parser = new GcsCredentialParser(mockFileSystem.Object); + + try + { + var credentials = parser.Parse(path); + + Assert.Multiple( + () => + { + Assert.That(credentials, Is.Not.Null); + Assert.That(credentials.ClientEmail, Is.EqualTo(expectedAccessId)); + Assert.That(credentials.PrivateKey, Is.EqualTo(expectedPrivateKey)); + }); + } + catch (Exception e) + { + if (!expectedException) + { + Assert.Fail($"Unexpected exception: {e}"); + } + + Assert.That(e.Message, Does.Contain(expectedExceptionMessage)); + return; + } + + if (expectedException) + { + Assert.Fail($"Expected exception: {expectedExceptionMessage}"); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/GameServerHostingModule.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/GameServerHostingModule.cs index dcce1e8..6d8de06 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/GameServerHostingModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/GameServerHostingModule.cs @@ -38,6 +38,7 @@ public GameServerHostingModule() BuildCreateInput.BuildNameOption, BuildCreateInput.BuildOsFamilyOption, BuildCreateInput.BuildTypeOption, + BuildCreateInput.BuildVersionNameOption, CommonInput.EnvironmentNameOption, CommonInput.CloudProjectIdOption }; @@ -61,7 +62,8 @@ public GameServerHostingModule() BuildCreateVersionInput.ContainerTagOption, BuildCreateVersionInput.FileDirectoryOption, BuildCreateVersionInput.SecretKeyOption, - BuildCreateVersionInput.RemoveOldFilesOption + BuildCreateVersionInput.RemoveOldFilesOption, + BuildCreateVersionInput.BuildVersionNameOption }; BuildCreateVersionCommand.SetHandler< BuildCreateVersionInput, @@ -185,6 +187,7 @@ public GameServerHostingModule() BuildConfigurationCreateInput.NameOption, BuildConfigurationCreateInput.QueryTypeOption, BuildConfigurationCreateInput.SpeedOption, + BuildConfigurationCreateInput.ReadinessOption, }; BuildConfigurationCreateCommand.SetHandler< BuildConfigurationCreateInput, @@ -239,6 +242,7 @@ public GameServerHostingModule() BuildConfigurationCreateInput.MemoryOption, BuildConfigurationCreateInput.NameOption, BuildConfigurationCreateInput.QueryTypeOption, + BuildConfigurationCreateInput.ReadinessOption, BuildConfigurationCreateInput.SpeedOption, }; BuildConfigurationUpdateCommand.SetHandler< @@ -548,6 +552,88 @@ public GameServerHostingModule() ServerFilesCommand, }; + CoreDumpGetCommand = new Command("get", "Get a Game Server Hosting core dump configuration.") + { + CommonInput.EnvironmentNameOption, + CommonInput.CloudProjectIdOption, + FleetIdInput.FleetIdArgument, + }; + + CoreDumpDeleteCommand = new Command("delete", "Delete a Game Server Hosting core dump configuration.") + { + CommonInput.EnvironmentNameOption, + CommonInput.CloudProjectIdOption, + FleetIdInput.FleetIdArgument, + }; + + CoreDumpCreateCommand = new Command("create", "Create a Game Server Hosting core dump configuration.") + { + CommonInput.EnvironmentNameOption, + CommonInput.CloudProjectIdOption, + FleetIdInput.FleetIdArgument, + CoreDumpCreateInput.StorageTypeOption, + CoreDumpCreateInput.GcsBucketOption, + CoreDumpCreateInput.GcsCredentialsFileOption, + CoreDumpCreateInput.StateOption, + }; + + CoreDumpUpdateCommand = new Command("update", "Update a Game Server Hosting core dump configuration.") + { + CommonInput.EnvironmentNameOption, + CommonInput.CloudProjectIdOption, + FleetIdInput.FleetIdArgument, + CoreDumpUpdateInput.StorageTypeOption, + CoreDumpUpdateInput.GcsBucketOption, + CoreDumpUpdateInput.GcsCredentialsFileOption, + CoreDumpUpdateInput.StateOption, + }; + + CoreDumpGetCommand.SetHandler< + FleetIdInput, + IUnityEnvironment, + IGameServerHostingService, + ILogger, + ILoadingIndicator, + CancellationToken + >(CoreDumpGetHandler.CoreDumpGetAsync); + + CoreDumpDeleteCommand.SetHandler< + FleetIdInput, + IUnityEnvironment, + IGameServerHostingService, + ILogger, + ILoadingIndicator, + CancellationToken + >(CoreDumpDeleteHandler.CoreDumpDeleteAsync); + + CoreDumpCreateCommand.SetHandler< + CoreDumpCreateInput, + IUnityEnvironment, + IGameServerHostingService, + ILogger, + ILoadingIndicator, + GcsCredentialParser, + CancellationToken + >(CoreDumpCreateHandler.CoreDumpCreateAsync); + + CoreDumpUpdateCommand.SetHandler< + CoreDumpUpdateInput, + IUnityEnvironment, + IGameServerHostingService, + ILogger, + ILoadingIndicator, + GcsCredentialParser, + CancellationToken + >(CoreDumpUpdateHandler.CoreDumpUpdateAsync); + + CoreDumpCommand = new Command("core-dump", "Manage Game Server Hosting core dump configurations.") + { + CoreDumpGetCommand, + CoreDumpDeleteCommand, + CoreDumpCreateCommand, + CoreDumpUpdateCommand + }; + ModuleRootCommand = new Command("game-server-hosting", "Manage Game Sever Hosting resources.") { BuildCommand, @@ -555,7 +641,8 @@ public GameServerHostingModule() FleetCommand, FleetRegionCommand, MachineCommand, - ServerCommand + CoreDumpCommand, + ServerCommand, }; ModuleRootCommand.AddAlias("gsh"); @@ -565,6 +652,7 @@ public GameServerHostingModule() FleetRegionCommand.AddAlias("fr"); MachineCommand.AddAlias("m"); ServerCommand.AddAlias("s"); + CoreDumpCommand.AddAlias("cd"); } internal Command BuildCommand { get; } @@ -574,6 +662,8 @@ public GameServerHostingModule() internal Command MachineCommand { get; } internal Command ServerCommand { get; } + internal Command CoreDumpCommand { get; } + // Build Commands internal Command BuildCreateCommand { get; } internal Command BuildCreateVersionCommand { get; } @@ -614,7 +704,6 @@ public GameServerHostingModule() internal Command ServerFilesListCommand { get; } internal Command ServerFilesDownloadCommand { get; } - internal static ExceptionFactory ExceptionFactory => (method, response) => { // Handle errors from the backend @@ -646,6 +735,12 @@ public GameServerHostingModule() string Message(string text) => $"Error calling {method}: {text}"; }; + // Core Dump Commands + internal Command CoreDumpGetCommand { get; } + internal Command CoreDumpCreateCommand { get; } + internal Command CoreDumpUpdateCommand { get; set; } + internal Command CoreDumpDeleteCommand { get; set; } + // GSH Module Command public Command ModuleRootCommand { get; } @@ -684,6 +779,10 @@ public static void RegisterServices(HostBuilderContext _, IServiceCollection ser { ExceptionFactory = ExceptionFactory }; + ICoreDumpApi coreDumpApi = new CoreDumpApi(gameServerHostingConfiguration) + { + ExceptionFactory = ExceptionFactory + }; GameServerHostingService service = new( authenticationService, @@ -692,6 +791,7 @@ public static void RegisterServices(HostBuilderContext _, IServiceCollection ser filesApi, fleetsApi, machinesApi, + coreDumpApi, serversApi ); @@ -699,7 +799,7 @@ public static void RegisterServices(HostBuilderContext _, IServiceCollection ser serviceCollection.AddTransient(_ => new FileSystem().File); serviceCollection.AddTransient(_ => new FileSystem().Directory); - + serviceCollection.AddTransient(); RegisterApiClients(serviceCollection); RegisterAuthoringServices(serviceCollection); } @@ -715,6 +815,7 @@ static void RegisterApiClients(IServiceCollection serviceCollection) serviceCollection.AddSingleton( new BuildConfigurationsApi(gameServerHostingConfig)); serviceCollection.AddSingleton(new FleetsApi(gameServerHostingConfig)); + serviceCollection.AddSingleton(new CoreDumpApi(gameServerHostingConfig)); var cloudContentDeliveryConfiguration = new CloudContentDeliveryConfiguration { @@ -745,4 +846,5 @@ static void RegisterAuthoringServices(IServiceCollection serviceCollection) serviceCollection.AddScoped(); serviceCollection.AddTransient(); } + } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationCreateHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationCreateHandler.cs index 7c6e96e..c7c8e9e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationCreateHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationCreateHandler.cs @@ -45,6 +45,7 @@ CancellationToken cancellationToken var name = input.Name ?? throw new MissingInputException(BuildConfigurationCreateInput.NameKey); var queryType = input.QueryType ?? throw new MissingInputException(BuildConfigurationCreateInput.QueryTypeKey); var speed = input.Speed ?? throw new MissingInputException(BuildConfigurationCreateInput.SpeedKey); + var readiness = input.Readiness ?? false; await service.AuthorizeGameServerHostingService(cancellationToken); @@ -62,7 +63,8 @@ CancellationToken cancellationToken memory: memory, name: name, queryType: queryType, - speed: speed + speed: speed, + readiness: readiness ), cancellationToken: cancellationToken); logger.LogResultValue(new BuildConfigurationOutput(buildConfiguration)); 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 397226e..c27aa5f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationUpdateHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildConfigurationUpdateHandler.cs @@ -75,7 +75,8 @@ CancellationToken cancellationToken memory: input.Memory ?? currentConfig.Memory, name: input.Name ?? currentConfig.Name, queryType: input.QueryType ?? currentConfig.QueryType, - speed: input.Speed ?? currentConfig.Speed + speed: input.Speed ?? currentConfig.Speed, + readiness: input.Readiness ?? currentConfig.Readiness #pragma warning restore CS0612 // Type or member is obsolete ); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateHandler.cs index 1eebb62..7839ee9 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateHandler.cs @@ -6,6 +6,8 @@ using Unity.Services.Cli.GameServerHosting.Input; using Unity.Services.Cli.GameServerHosting.Model; using Unity.Services.Cli.GameServerHosting.Service; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; namespace Unity.Services.Cli.GameServerHosting.Handlers; @@ -21,8 +23,15 @@ public static async Task BuildCreateAsync( CancellationToken cancellationToken ) { - await loadingIndicator.StartLoadingAsync("Creating build...", _ => - BuildCreateAsync(input, unityEnvironment, service, logger, cancellationToken)); + await loadingIndicator.StartLoadingAsync( + "Creating build...", + _ => + BuildCreateAsync( + input, + unityEnvironment, + service, + logger, + cancellationToken)); } internal static async Task BuildCreateAsync( @@ -41,12 +50,29 @@ CancellationToken cancellationToken await service.AuthorizeGameServerHostingService(cancellationToken); - var build = await service.BuildsApi.CreateBuildAsync( - Guid.Parse(input.CloudProjectId!), - Guid.Parse(environmentId), - new CreateBuildRequest(buildName, buildType, osFamily: buildOsFamily), - cancellationToken: cancellationToken); + try + { + var createBuildRequest = new CreateBuildRequest( + buildName, + buildType, + osFamily: buildOsFamily); - logger.LogResultValue(new BuildOutput(build)); + if (input.BuildVersionName != null) + { + createBuildRequest.BuildVersionName = input.BuildVersionName; + } + + var build = await service.BuildsApi.CreateBuildAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + createBuildRequest, + cancellationToken: cancellationToken); + + logger.LogResultValue(new BuildOutput(build)); + } + catch (ApiException e) + { + ApiExceptionConverter.Convert(e); + } } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionBucketHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionBucketHandler.cs index febfef6..5d48e73 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionBucketHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionBucketHandler.cs @@ -3,6 +3,8 @@ using Unity.Services.Cli.GameServerHosting.Exceptions; using Unity.Services.Cli.GameServerHosting.Input; using Unity.Services.Cli.GameServerHosting.Service; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; namespace Unity.Services.Cli.GameServerHosting.Handlers; @@ -19,16 +21,25 @@ CancellationToken cancellationToken ) { ValidateBucketInput(input); - await service.BuildsApi.CreateNewBuildVersionAsync( - Guid.Parse(input.CloudProjectId!), - Guid.Parse(environmentId), - build.BuildID, - new CreateNewBuildVersionRequest( - s3: new AmazonS3Request(input.AccessKey!, input.BucketUrl!, input.SecretKey!)), - cancellationToken: cancellationToken - ); + try + { + await service.BuildsApi.CreateNewBuildVersionAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + build.BuildID, + new CreateNewBuildVersionRequest( + buildVersionName: input.BuildVersionName!, + s3: new AmazonS3Request(input.AccessKey!, input.BucketUrl!, input.SecretKey!) + ), + cancellationToken: cancellationToken + ); - logger.LogInformation("Build version created successfully"); + logger.LogInformation("Build version created successfully"); + } + catch (ApiException e) + { + ApiExceptionConverter.Convert(e); + } } // We need to apply our own conditional validation based on the build type diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionContainerHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionContainerHandler.cs index c8ca314..5ed8e22 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionContainerHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionContainerHandler.cs @@ -3,6 +3,8 @@ using Unity.Services.Cli.GameServerHosting.Exceptions; using Unity.Services.Cli.GameServerHosting.Input; using Unity.Services.Cli.GameServerHosting.Service; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; namespace Unity.Services.Cli.GameServerHosting.Handlers; @@ -19,14 +21,24 @@ CancellationToken cancellationToken ) { ValidateContainerInput(input); - await service.BuildsApi.CreateNewBuildVersionAsync( - Guid.Parse(input.CloudProjectId!), - Guid.Parse(environmentId), - build.BuildID, - new CreateNewBuildVersionRequest(container: new ContainerImage(input.ContainerTag!)), - cancellationToken: cancellationToken - ); - logger.LogInformation("Build version created successfully"); + try + { + await service.BuildsApi.CreateNewBuildVersionAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + build.BuildID, + new CreateNewBuildVersionRequest( + buildVersionName: input.BuildVersionName!, + container: new ContainerImage(input.ContainerTag!) + ), + cancellationToken: cancellationToken + ); + logger.LogInformation("Build version created successfully"); + } + catch (ApiException e) + { + ApiExceptionConverter.Convert(e); + } } // We need to apply our own conditional validation based on the build type diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionFileUploadHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionFileUploadHandler.cs index f466aea..e8900d4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionFileUploadHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/BuildCreateVersionFileUploadHandler.cs @@ -1,6 +1,5 @@ using System.Net; using System.Text; -using SystemFile = System.IO.File; using Microsoft.Extensions.Logging; using Polly; using Unity.Services.Cli.Common.Exceptions; @@ -8,8 +7,10 @@ using Unity.Services.Cli.GameServerHosting.Input; using Unity.Services.Cli.GameServerHosting.Model; using Unity.Services.Cli.GameServerHosting.Service; +using Unity.Services.Cli.GameServerHosting.Services; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; +using SystemFile = System.IO.File; namespace Unity.Services.Cli.GameServerHosting.Handlers; @@ -69,17 +70,27 @@ CancellationToken cancellationToken cancellationToken); var policy = Policy - .Handle() + .Handle((exception => exception.ErrorCode.Equals(HttpStatusCode.BadRequest))) .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); - await policy.ExecuteAsync( - async () => await service.BuildsApi.CreateNewBuildVersionAsync( - Guid.Parse(input.CloudProjectId!), - Guid.Parse(environmentId), - build.BuildID, - new CreateNewBuildVersionRequest(new CCDDetails2(build.Ccd.BucketID)), - cancellationToken: cancellationToken - )); + try + { + await policy.ExecuteAsync( + async () => await service.BuildsApi.CreateNewBuildVersionAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + build.BuildID, + new CreateNewBuildVersionRequest( + buildVersionName: input.BuildVersionName!, + ccd: new CCDDetails2(build.Ccd.BucketID) + ), + cancellationToken: cancellationToken + )); + } + catch (ApiException e) when (e.ErrorCode == (int)HttpStatusCode.BadRequest) + { + ApiExceptionConverter.Convert(e); + } var details = new StringBuilder() .AppendLine($"Files to upload: {localFiles.Count}") diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpCreateHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpCreateHandler.cs new file mode 100644 index 0000000..852c6f9 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpCreateHandler.cs @@ -0,0 +1,86 @@ +using System.Net; +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.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.Handlers; + +static class CoreDumpCreateHandler +{ + public static async Task CoreDumpCreateAsync( + CoreDumpCreateInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + ILoadingIndicator loadingIndicator, + GcsCredentialParser gcsCredentialParser, + CancellationToken cancellationToken) + { + await loadingIndicator.StartLoadingAsync( + "Creating core dump config...", + _ => CoreDumpCreateAsync( + input, + unityEnvironment, + service, + logger, + gcsCredentialParser, + cancellationToken)); + } + + internal static async Task CoreDumpCreateAsync( + CoreDumpCreateInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + GcsCredentialParser gcsCredentialParser, + CancellationToken cancellationToken) + { + // FetchIdentifierAsync handles null checks for project-id and environment + var environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); + var fleetId = input.FleetId ?? throw new MissingInputException(FleetIdInput.FleetIdKey); + + await service.AuthorizeGameServerHostingService(cancellationToken); + + if (!Enum.TryParse(input.StorageType, true, out CreateCoreDumpConfigRequest.StorageTypeEnum storageType)) + { + throw new ArgumentException( + $"Invalid storage type {input.StorageType}", + nameof(CoreDumpUpdateInput.StorageType)); + } + + try + { + var credentials = gcsCredentialParser.Parse(input.CredentialsFile); + var coreDumpCreateResponse = await service.CoreDumpApi.PostCoreDumpConfigAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + Guid.Parse(fleetId), + new CreateCoreDumpConfigRequest( + new CredentialsForTheBucket1( + credentials.ClientEmail, + credentials.PrivateKey, + input.GcsBucket + ), + 0, + CoreDumpStateConverter.ConvertStringToCreateStateEnum(input.State), + storageType + ), + 0, + cancellationToken); + + logger.LogResultValue(new CoreDumpOutput(coreDumpCreateResponse)); + } + catch (ApiException e) when (e.ErrorCode == (int)HttpStatusCode.BadRequest) + { + ApiExceptionConverter.Convert(e); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpDeleteHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpDeleteHandler.cs new file mode 100644 index 0000000..77585f2 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpDeleteHandler.cs @@ -0,0 +1,73 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Exceptions; +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.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; + +namespace Unity.Services.Cli.GameServerHosting.Handlers; + +static class CoreDumpDeleteHandler +{ + public static async Task CoreDumpDeleteAsync( + FleetIdInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + ILoadingIndicator loadingIndicator, + CancellationToken cancellationToken + ) + { + await loadingIndicator.StartLoadingAsync( + "Deleting core dump config...", + _ => CoreDumpDeleteAsync( + input, + unityEnvironment, + service, + logger, + cancellationToken)); + } + + internal static async Task CoreDumpDeleteAsync( + FleetIdInput 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 fleetId = input.FleetId ?? throw new MissingInputException(FleetIdInput.FleetIdKey); + + await service.AuthorizeGameServerHostingService(cancellationToken); + + try + { + await service.CoreDumpApi.DeleteCoreDumpConfigAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + Guid.Parse(fleetId), + operationIndex: 0, + cancellationToken: cancellationToken + ); + + logger.LogInformation("Core Dump config deleted successfully"); + } + catch (ApiException ex) when (ex.ErrorCode == (int)HttpStatusCode.NotFound) + { + throw new CliException( + "Core Dump Storage is not configured for this fleet. Try `create` command to configure it.", + ex, + ExitCode.HandledError); + } + catch (ApiException e) when (e.ErrorCode == (int)HttpStatusCode.BadRequest) + { + ApiExceptionConverter.Convert(e); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpGetHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpGetHandler.cs new file mode 100644 index 0000000..59710b2 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpGetHandler.cs @@ -0,0 +1,75 @@ +using System.Net; +using Microsoft.Extensions.Logging; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Cli.GameServerHosting.Exceptions; +using Unity.Services.Cli.GameServerHosting.Input; +using Unity.Services.Cli.GameServerHosting.Model; +using Unity.Services.Cli.GameServerHosting.Service; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; + +namespace Unity.Services.Cli.GameServerHosting.Handlers; + +static class CoreDumpGetHandler +{ + public static async Task CoreDumpGetAsync( + FleetIdInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + ILoadingIndicator loadingIndicator, + CancellationToken cancellationToken + ) + { + await loadingIndicator.StartLoadingAsync( + "Fetching core dump configuration...", + _ => CoreDumpGetAsync( + input, + unityEnvironment, + service, + logger, + cancellationToken)); + } + + internal static async Task CoreDumpGetAsync( + FleetIdInput 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 fleetId = input.FleetId ?? throw new MissingInputException(FleetIdInput.FleetIdKey); + + await service.AuthorizeGameServerHostingService(cancellationToken); + + try + { + var coreDump = await service.CoreDumpApi.GetCoreDumpConfigAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + Guid.Parse(fleetId), + operationIndex: 0, + cancellationToken: cancellationToken + ); + + logger.LogResultValue(new CoreDumpOutput(coreDump)); + } + catch (ApiException e) when (e.ErrorCode == (int)HttpStatusCode.NotFound) + { + throw new CliException( + "Core Dump Storage is not configured for this fleet. Try `create` command to configure it.", + e, + ExitCode.HandledError); + } + catch (ApiException e) when (e.ErrorCode == (int)HttpStatusCode.BadRequest) + { + ApiExceptionConverter.Convert(e); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpUpdateHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpUpdateHandler.cs new file mode 100644 index 0000000..a138826 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/CoreDumpUpdateHandler.cs @@ -0,0 +1,106 @@ +using System.Net; +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.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.Handlers; + +static class CoreDumpUpdateHandler +{ + public static async Task CoreDumpUpdateAsync( + CoreDumpUpdateInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + ILoadingIndicator loadingIndicator, + GcsCredentialParser gcsCredentialParser, + CancellationToken cancellationToken) + { + await loadingIndicator.StartLoadingAsync( + "Updating core dump config...", + _ => CoreDumpUpdateAsync( + input, + unityEnvironment, + service, + logger, + gcsCredentialParser, + cancellationToken)); + } + + internal static async Task CoreDumpUpdateAsync( + CoreDumpUpdateInput input, + IUnityEnvironment unityEnvironment, + IGameServerHostingService service, + ILogger logger, + GcsCredentialParser gcsCredentialParser, + CancellationToken cancellationToken) + { + // FetchIdentifierAsync handles null checks for project-id and environment + var environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); + var fleetId = input.FleetId ?? throw new MissingInputException(FleetIdInput.FleetIdKey); + + var request = new UpdateCoreDumpConfigRequest(); + + if (input.StorageType != null) + { + if (!Enum.TryParse(input.StorageType, true, out UpdateCoreDumpConfigRequest.StorageTypeEnum storageType)) + { + throw new ArgumentException( + $"Invalid storage type {input.StorageType}", + nameof(CoreDumpUpdateInput.StorageType)); + } + + request.StorageType = storageType; + } + + if (input.State != null) + { + request.State = CoreDumpStateConverter.ConvertStringToUpdateStateEnum(input.State); + } + + if (input.CredentialsFile != null || input.GcsBucket != null) + { + var credentials = new CredentialsForTheBucket1(); + if (input.CredentialsFile != null) + { + var parsedCredentials = gcsCredentialParser.Parse(input.CredentialsFile); + credentials.ServiceAccountAccessId = parsedCredentials.ClientEmail; + credentials.ServiceAccountPrivateKey = parsedCredentials.PrivateKey; + } + + if (input.GcsBucket != null) + { + credentials.StorageBucket = input.GcsBucket; + } + + request.Credentials = credentials; + } + + await service.AuthorizeGameServerHostingService(cancellationToken); + + try + { + var coreDumpCreateResponse = await service.CoreDumpApi.PutCoreDumpConfigAsync( + Guid.Parse(input.CloudProjectId!), + Guid.Parse(environmentId), + Guid.Parse(fleetId), + request, + 0, + cancellationToken); + + logger.LogResultValue(new CoreDumpOutput(coreDumpCreateResponse)); + } + catch (ApiException e) when (e.ErrorCode == (int)HttpStatusCode.BadRequest) + { + ApiExceptionConverter.Convert(e); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileDownloadHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileDownloadHandler.cs index e8db384..9d36ee2 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileDownloadHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FileDownloadHandler.cs @@ -45,7 +45,7 @@ CancellationToken cancellationToken await service.AuthorizeGameServerHostingService(cancellationToken); - var request = new GenerateDownloadURLRequest( + var request = new GenerateContentURLRequest( path: input.Path!, serverId: long.Parse(input.ServerId!) ); 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 08521e9..632a036 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetCreateHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetCreateHandler.cs @@ -63,7 +63,8 @@ internal static async Task FleetCreateAsync(FleetCreateInput input, IUnityEnviro var fleet = await service.FleetsApi.CreateFleetAsync( Guid.Parse(input.CloudProjectId!), Guid.Parse(environmentId), - req, + null, + fleetCreateRequest: req, cancellationToken: cancellationToken ); diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetRegionCreateHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetRegionCreateHandler.cs index 4b53773..947befa 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetRegionCreateHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Handlers/FleetRegionCreateHandler.cs @@ -46,7 +46,7 @@ CancellationToken cancellationToken Guid.Parse(input.CloudProjectId!), Guid.Parse(environmentId), fleetId, - new AddRegionRequest( + addRegionRequest: new AddRegionRequest( maxServers, minAvailableServers, regionId diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildConfigurationCreateInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildConfigurationCreateInput.cs index 0b4f857..1505ea4 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildConfigurationCreateInput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildConfigurationCreateInput.cs @@ -15,6 +15,7 @@ public class BuildConfigurationCreateInput : CommonInput public const string NameKey = "--name"; public const string QueryTypeKey = "--query-type"; public const string SpeedKey = "--speed"; + public const string ReadinessKey = "--readiness"; public static readonly Option BinaryPathOption = new(BinaryPathKey, "Path to the game binary") @@ -64,6 +65,8 @@ public class BuildConfigurationCreateInput : CommonInput IsRequired = true }; + public static readonly Option ReadinessOption = new(ReadinessKey, "Readiness of the build configuration"); + [InputBinding(nameof(BinaryPathOption))] public string? BinaryPath { get; init; } @@ -91,4 +94,6 @@ public class BuildConfigurationCreateInput : CommonInput [InputBinding(nameof(SpeedOption))] public long? Speed { get; init; } + [InputBinding(nameof(ReadinessOption))] + public bool? Readiness { get; init; } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildCreateInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildCreateInput.cs index 06a8c77..8874c03 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildCreateInput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildCreateInput.cs @@ -10,6 +10,7 @@ class BuildCreateInput : CommonInput public const string NameKey = "--name"; public const string OsFamilyKey = "--os-family"; public const string TypeKey = "--type"; + public const string BuildVersionNameKey = "--build-version-name"; public static readonly Option BuildNameOption = new(NameKey, "The name of the build") { @@ -26,6 +27,10 @@ class BuildCreateInput : CommonInput IsRequired = true }; + public static readonly Option BuildVersionNameOption = new( + BuildVersionNameKey, + "The name of the build version to create"); + static BuildCreateInput() { BuildOsFamilyOption.AddValidator(ValidateOsFamilyEnum); @@ -41,6 +46,9 @@ static BuildCreateInput() [InputBinding(nameof(BuildTypeOption))] public BuildTypeEnum? BuildType { get; init; } + [InputBinding(nameof(BuildVersionNameOption))] + public string? BuildVersionName { get; init; } + static void ValidateOsFamilyEnum(OptionResult result) { try @@ -49,7 +57,8 @@ static void ValidateOsFamilyEnum(OptionResult result) } catch (Exception) { - result.ErrorMessage = $"Invalid option for --os-family. Did you mean one of the following? {string.Join(", ", Enum.GetNames())}"; + result.ErrorMessage = + $"Invalid option for --os-family. Did you mean one of the following? {string.Join(", ", Enum.GetNames())}"; } } @@ -61,7 +70,8 @@ static void ValidateBuildTypeEnum(OptionResult result) } catch (Exception) { - result.ErrorMessage = $"Invalid option for --type. Did you mean one of the following? {string.Join(", ", Enum.GetNames())}"; + result.ErrorMessage = + $"Invalid option for --type. Did you mean one of the following? {string.Join(", ", Enum.GetNames())}"; } } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildCreateVersionInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildCreateVersionInput.cs index 36dedb0..e6a8673 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildCreateVersionInput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/BuildCreateVersionInput.cs @@ -12,6 +12,7 @@ class BuildCreateVersionInput : CommonInput public const string FileDirectoryKey = "--directory"; public const string RemoveOldFilesKey = "--remove-old-files"; public const string SecretKeyKey = "--secret-key"; + public const string BuildVersionNameKey = "--build-version-name"; public static readonly Option AccessKeyOption = new( AccessKeyKey, @@ -41,6 +42,10 @@ class BuildCreateVersionInput : CommonInput SecretKeyKey, "The Amazon Web Services (AWS) secret key, for s3 bucket builds"); + public static readonly Option BuildVersionNameOption = new( + BuildVersionNameKey, + "The name of the build version to create"); + [InputBinding(nameof(AccessKeyOption))] public string? AccessKey { get; init; } @@ -61,4 +66,7 @@ class BuildCreateVersionInput : CommonInput [InputBinding(nameof(SecretKeyOption))] public string? SecretKey { get; init; } + + [InputBinding(nameof(BuildVersionNameOption))] + public string? BuildVersionName { get; init; } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/CoreDumpCreateInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/CoreDumpCreateInput.cs new file mode 100644 index 0000000..54c0aef --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/CoreDumpCreateInput.cs @@ -0,0 +1,72 @@ +using System.CommandLine; +using Unity.Services.Cli.Common.Input; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.Input; + +class CoreDumpCreateInput : FleetIdInput +{ + public static readonly Option StorageTypeOption = new( + new[] + { + "--storage-type", + "-t" + }, + "The storage type of the core dump"); + + public static readonly Option GcsCredentialsFileOption = new( + new[] + { + "--gcs-credentials-file", + "-c" + }, + "The path to the JSON file that contains the GCS credentials for the service account") + { + IsRequired = true + }; + + public static readonly Option GcsBucketOption = new( + new[] + { + "--gcs-bucket", + "-b" + }, + "The name of the GCS bucket to store the core dump") + { + IsRequired = true + }; + + public static readonly Option StateOption = new( + new[] + { + "--state", + "-s" + }, + "Enable or disable core dump collection"); + + + static CoreDumpCreateInput() + { + StateOption.SetDefaultValue(CoreDumpStateConverter.StateEnum.Enabled.ToString().ToLower()); + StateOption.FromAmong( + CoreDumpStateConverter.StateEnum.Enabled.ToString().ToLower(), + CoreDumpStateConverter.StateEnum.Disabled.ToString().ToLower() + ); + StorageTypeOption.SetDefaultValue(CreateCoreDumpConfigRequest.StorageTypeEnum.Gcs.ToString().ToLower()); + StorageTypeOption.FromAmong(CreateCoreDumpConfigRequest.StorageTypeEnum.Gcs.ToString().ToLower()); + } + + + [InputBinding(nameof(StorageTypeOption))] + public string StorageType { get; init; } = CreateCoreDumpConfigRequest.StorageTypeEnum.Gcs.ToString().ToLower(); + + [InputBinding(nameof(GcsCredentialsFileOption))] + public string CredentialsFile { get; set; } = null!; + + [InputBinding(nameof(GcsBucketOption))] + public string GcsBucket { get; init; } = null!; + + [InputBinding(nameof(StateOption))] + public string State { get; set; } = CoreDumpStateConverter.StateEnum.Enabled.ToString().ToLower(); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/CoreDumpUpdateInput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/CoreDumpUpdateInput.cs new file mode 100644 index 0000000..6506258 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Input/CoreDumpUpdateInput.cs @@ -0,0 +1,63 @@ +using System.CommandLine; +using Unity.Services.Cli.Common.Input; +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.Input; + +class CoreDumpUpdateInput : FleetIdInput +{ + public static readonly Option StorageTypeOption = new( + new[] + { + "--storage-type", + "-t" + }, + "The storage type of the core dump"); + + public static readonly Option GcsCredentialsFileOption = new( + new[] + { + "--gcs-credentials-file", + "-c" + }, + "The path to the JSON file that contains the GCS credentials for the service account"); + + public static readonly Option GcsBucketOption = new( + new[] + { + "--gcs-bucket", + "-b" + }, + "The name of the GCS bucket to store the core dump"); + + public static readonly Option StateOption = new( + new[] + { + "--state", + "-s" + }, + "Enable or disable core dump collection"); + + + static CoreDumpUpdateInput() + { + StateOption.FromAmong( + CoreDumpStateConverter.StateEnum.Enabled.ToString().ToLower(), + CoreDumpStateConverter.StateEnum.Disabled.ToString().ToLower() + ); + StorageTypeOption.FromAmong(UpdateCoreDumpConfigRequest.StorageTypeEnum.Gcs.ToString().ToLower()); + } + + [InputBinding(nameof(StorageTypeOption))] + public string? StorageType { get; init; } = null; + + [InputBinding(nameof(GcsCredentialsFileOption))] + public string? CredentialsFile { get; init; } = null!; + + [InputBinding(nameof(GcsBucketOption))] + public string? GcsBucket { get; init; } = null!; + + [InputBinding(nameof(StateOption))] + public string? State { get; init; } = null; +} 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 c2a4285..f3c463c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildConfigurationOutput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildConfigurationOutput.cs @@ -17,6 +17,7 @@ public BuildConfigurationOutput(BuildConfiguration buildConfiguration) BinaryPath = buildConfiguration.BinaryPath; CommandLine = buildConfiguration.CommandLine; QueryType = buildConfiguration.QueryType; + Readiness = buildConfiguration.Readiness; Configuration = buildConfiguration._Configuration; #pragma warning disable CS0612 // Type or member is obsolete Cores = buildConfiguration.Cores; @@ -39,6 +40,7 @@ public BuildConfigurationOutput(BuildConfiguration buildConfiguration) public string BinaryPath { get; } public string CommandLine { get; } public string QueryType { get; } + public bool Readiness { get; } public List Configuration { get; } public long Cores { get; } public long Speed { get; } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildOutput.cs index 44739be..4d2ff84 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildOutput.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/BuildOutput.cs @@ -11,6 +11,7 @@ public BuildOutput(BuildListInner build) BuildName = build.BuildName; BuildId = build.BuildID; OsFamily = build.OsFamily; + BuildVersionName = build.BuildVersionName; Updated = build.Updated; BuildConfigurations = build.BuildConfigurations; SyncStatus = build.SyncStatus; @@ -26,6 +27,7 @@ public BuildOutput(CreateBuild200Response build) BuildName = build.BuildName; BuildId = build.BuildID; OsFamily = (BuildListInner.OsFamilyEnum?)build.OsFamily; + BuildVersionName = build.BuildVersionName; Updated = build.Updated; SyncStatus = (BuildListInner.SyncStatusEnum)build.SyncStatus; // Ccd can be null, the codegen doesn't handle this case @@ -35,6 +37,8 @@ public BuildOutput(CreateBuild200Response build) S3 = build.S3; } + public string BuildVersionName { get; } + public string BuildName { get; } public long BuildId { get; } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/CoreDumpCredentialsOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/CoreDumpCredentialsOutput.cs new file mode 100644 index 0000000..7ecb230 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/CoreDumpCredentialsOutput.cs @@ -0,0 +1,23 @@ +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Unity.Services.Cli.GameServerHosting.Model; + +public class CoreDumpCredentialsOutput +{ + public CoreDumpCredentialsOutput(CredentialsForTheBucket credentials) + { + StorageBucket = credentials.StorageBucket; + } + + public string StorageBucket { get; set; } + + public override string ToString() + { + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + return serializer.Serialize(this); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/CoreDumpOutput.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/CoreDumpOutput.cs new file mode 100644 index 0000000..adbc619 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/CoreDumpOutput.cs @@ -0,0 +1,32 @@ +using Unity.Services.Cli.GameServerHosting.Services; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Unity.Services.Cli.GameServerHosting.Model; + +public class CoreDumpOutput +{ + public CoreDumpOutput(GetCoreDumpConfig200Response coreDump) + { + StorageType = coreDump.StorageType?.ToString().ToLower() ?? "unknown"; + Credentials = new CoreDumpCredentialsOutput(coreDump.Credentials); + FleetId = coreDump.FleetId; + State = CoreDumpStateConverter.ConvertToString(coreDump.State); + UpdatedAt = coreDump.UpdatedAt; + } + + public Guid FleetId { get; set; } + public string StorageType { get; set; } + public string State { get; set; } + public CoreDumpCredentialsOutput Credentials { get; set; } + public DateTime UpdatedAt { get; set; } + + public override string ToString() + { + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + return serializer.Serialize(this); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/GcsCredentials.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/GcsCredentials.cs new file mode 100644 index 0000000..c5675d3 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/GcsCredentials.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Unity.Services.Cli.GameServerHosting.Model; + +public class GcsCredentials +{ + public GcsCredentials(string clientEmail, string privateKey) + { + ClientEmail = clientEmail; + PrivateKey = privateKey; + } + + [JsonProperty("client_email")] + public string ClientEmail { get; set; } + + [JsonProperty("private_key")] + public string PrivateKey { get; set; } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/InvalidGcsCredentialsFileFormat.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/InvalidGcsCredentialsFileFormat.cs new file mode 100644 index 0000000..ca8fbb6 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Model/InvalidGcsCredentialsFileFormat.cs @@ -0,0 +1,20 @@ +namespace Unity.Services.Cli.GameServerHosting.Model; + +public class InvalidGcsCredentialsFileFormat +{ + public override string ToString() + { + return $@"Invalid GCS credentials file format. Please check the file and try again. + +The file should be in the following format: +{{ + ... + ""private_key"": ""..."", + ""client_email"": ""..."", + ... +}} + +See: https://developers.google.com/workspace/guides/create-credentials#create_credentials_for_a_service_account +"; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Service/GameServerHostingService.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Service/GameServerHostingService.cs index 4aa9469..edbcbac 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Service/GameServerHostingService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Service/GameServerHostingService.cs @@ -14,6 +14,7 @@ public GameServerHostingService( IFilesApi filesApi, IFleetsApi fleetsApi, IMachinesApi machinesApi, + ICoreDumpApi coreDumpApi, IServersApi serversApi ) { @@ -24,6 +25,7 @@ IServersApi serversApi FleetsApi = fleetsApi; MachinesApi = machinesApi; ServersApi = serversApi; + CoreDumpApi = coreDumpApi; } public IBuildsApi BuildsApi { get; } @@ -38,6 +40,8 @@ IServersApi serversApi public IServersApi ServersApi { get; } + public ICoreDumpApi CoreDumpApi { get; } + public async Task AuthorizeGameServerHostingService(CancellationToken cancellationToken = default) { @@ -48,5 +52,6 @@ public async Task AuthorizeGameServerHostingService(CancellationToken cancellati FleetsApi.Configuration.DefaultHeaders.Add("Authorization", $"Basic {token}"); MachinesApi.Configuration.DefaultHeaders.Add("Authorization", $"Basic {token}"); ServersApi.Configuration.DefaultHeaders.Add("Authorization", $"Basic {token}"); + CoreDumpApi.Configuration.DefaultHeaders.Add("Authorization", $"Basic {token}"); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Service/IGameServerHostingService.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Service/IGameServerHostingService.cs index 2e57f9e..967f683 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Service/IGameServerHostingService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Service/IGameServerHostingService.cs @@ -10,6 +10,7 @@ public interface IGameServerHostingService public IFleetsApi FleetsApi { get; } public IMachinesApi MachinesApi { get; } public IServersApi ServersApi { get; } + public ICoreDumpApi CoreDumpApi { get; } public Task AuthorizeGameServerHostingService(CancellationToken cancellationToken = default); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/ApiExceptionConverter.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/ApiExceptionConverter.cs new file mode 100644 index 0000000..2c3f78d --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/ApiExceptionConverter.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Gateway.AccessApiV1.Generated.Model; +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Client; + +namespace Unity.Services.Cli.GameServerHosting.Services; + +static class ApiExceptionConverter +{ + /// + /// It converts the ApiException to CliException and throws it, if .ErrorContent contains a valid JSON + /// + /// + /// + public static void Convert(ApiException e) + { + if (e.ErrorContent == null) + { + throw e; + } + + var validationError = JsonConvert.DeserializeObject((string)e.ErrorContent); + // if there is no detail, print out the whole error content + if (validationError!.Detail != null) + { + throw new CliException($"{validationError.Detail}", e, ExitCode.HandledError); + } + + throw new CliException($"{e.ErrorContent}", e, ExitCode.HandledError); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/CoreDumpStateConverter.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/CoreDumpStateConverter.cs new file mode 100644 index 0000000..40e0550 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/CoreDumpStateConverter.cs @@ -0,0 +1,62 @@ +using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; + +namespace Unity.Services.Cli.GameServerHosting.Services; + +public static class CoreDumpStateConverter +{ + public enum StateEnum + { + Disabled = 0, + Enabled = 1 + } + + public static string ConvertToString(GetCoreDumpConfig200Response.StateEnum? state) + { + return state switch + { + GetCoreDumpConfig200Response.StateEnum.NUMBER_0 => "disabled", + GetCoreDumpConfig200Response.StateEnum.NUMBER_1 => "enabled", + _ => "unknown" + }; + } + + public static CreateCoreDumpConfigRequest.StateEnum ConvertToCreateStateEnum(StateEnum? state) + { + return state switch + { + StateEnum.Disabled => CreateCoreDumpConfigRequest.StateEnum.NUMBER_0, + StateEnum.Enabled => CreateCoreDumpConfigRequest.StateEnum.NUMBER_1, + _ => throw new ArgumentException($"Invalid state: {state}") + }; + } + + public static UpdateCoreDumpConfigRequest.StateEnum ConvertToUpdateStateEnum(StateEnum? state) + { + return state switch + { + StateEnum.Disabled => UpdateCoreDumpConfigRequest.StateEnum.NUMBER_0, + StateEnum.Enabled => UpdateCoreDumpConfigRequest.StateEnum.NUMBER_1, + _ => throw new ArgumentException($"Invalid state: {state}") + }; + } + + public static UpdateCoreDumpConfigRequest.StateEnum ConvertStringToUpdateStateEnum(string state) + { + return state.ToLower() switch + { + "disabled" => UpdateCoreDumpConfigRequest.StateEnum.NUMBER_0, + "enabled" => UpdateCoreDumpConfigRequest.StateEnum.NUMBER_1, + _ => throw new ArgumentException($"Invalid state: {state}") + }; + } + + public static CreateCoreDumpConfigRequest.StateEnum ConvertStringToCreateStateEnum(string state) + { + return state.ToLower() switch + { + "disabled" => CreateCoreDumpConfigRequest.StateEnum.NUMBER_0, + "enabled" => CreateCoreDumpConfigRequest.StateEnum.NUMBER_1, + _ => throw new ArgumentException($"Invalid state: {state}") + }; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/FleetClient.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/FleetClient.cs index 5c1a4fe..4591435 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/FleetClient.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/FleetClient.cs @@ -45,7 +45,7 @@ public async Task Create(string name, IList build maxServers: r.Value.MaxServers)).ToList(), osID: Guid.Empty, // Must be set in order to avoid breaking the API osFamily: FleetCreateRequest.OsFamilyEnum.LINUX); - var res = await m_FleetsApiAsync.CreateFleetAsync(m_ApiConfig.ProjectId, m_ApiConfig.EnvironmentId, fleet, cancellationToken: cancellationToken); + var res = await m_FleetsApiAsync.CreateFleetAsync(m_ApiConfig.ProjectId, m_ApiConfig.EnvironmentId, fleetCreateRequest: fleet, cancellationToken: cancellationToken); return new FleetId { Id = res.Id }; } @@ -74,7 +74,7 @@ async Task UpdateRegions(FleetId id, Fleet fleet, MultiplayConfig.FleetDefinitio regionID: regions[regionName], minAvailableServers: region.MinAvailable, maxServers: region.MaxServers); - await m_FleetsApiAsync.AddFleetRegionAsync(m_ApiConfig.ProjectId, m_ApiConfig.EnvironmentId, id.ToGuid(), regionDefinition, cancellationToken: cancellationToken); + await m_FleetsApiAsync.AddFleetRegionAsync(m_ApiConfig.ProjectId, m_ApiConfig.EnvironmentId, id.ToGuid(), addRegionRequest: regionDefinition, cancellationToken: cancellationToken); } else { diff --git a/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/GcsCredentialParser.cs b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/GcsCredentialParser.cs new file mode 100644 index 0000000..2c00910 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.GameServerHosting/Services/GcsCredentialParser.cs @@ -0,0 +1,51 @@ +using System.IO.Abstractions; +using Newtonsoft.Json; +using Unity.Services.Cli.Common.Exceptions; +using Unity.Services.Cli.GameServerHosting.Model; + +namespace Unity.Services.Cli.GameServerHosting.Services; + +class GcsCredentialParser +{ + readonly IFile m_FileSystem; + + public GcsCredentialParser( + IFile fileSystem) + { + m_FileSystem = fileSystem; + } + + public GcsCredentials Parse(string path) + { + if (!m_FileSystem.Exists(path)) + { + throw new CliException( + "File not found", + ExitCode.HandledError); + } + + var content = m_FileSystem.ReadAllText(path); + + GcsCredentials? credentials; + try + { + credentials = JsonConvert.DeserializeObject(content); + } + catch (Exception e) + { + throw new CliException( + $"Invalid JSON format\n{new InvalidGcsCredentialsFileFormat()}", + e, + ExitCode.HandledError); + } + + if (credentials?.PrivateKey == null || credentials?.ClientEmail == null) + { + throw new CliException( + $"`private_key` or `client_email` are empty\n{new InvalidGcsCredentialsFileFormat()}", + ExitCode.HandledError); + } + + return credentials; + } +} 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 03a36ee..69f1639 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 @@ -23,8 +23,8 @@ - - + + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/CloudContentDeliveryApiMock.cs b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/CloudContentDeliveryApiMock.cs new file mode 100644 index 0000000..a7124ce --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/CloudContentDeliveryApiMock.cs @@ -0,0 +1,572 @@ +using System.Collections; +using Unity.Services.Cli.MockServer; +using Unity.Services.Cli.MockServer.Common; +using Unity.Services.Gateway.ContentDeliveryManagementApiV1.Generated.Model; +using WireMock.Admin.Mappings; +using WireMock.Matchers; +using WireMock.Server; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Types; + +namespace Unity.Services.Cli.MockServer.ServiceMocks; + +public class CloudContentDeliveryApiMock : IServiceApiMock +{ + + static readonly string k_Uuid = "00000000-0000-0000-0000-000000000000"; + + static readonly CcdGetAllByBucket200ResponseInner k_Permission = new( + "1", + "1", + "bucket/00000000-0000-0000-0000-000000000000", + "user" + ); + + static readonly CcdPromoteBucketAsync200Response k_Promote = new( + new Guid("00000000-0000-0000-0000-000000000000")); + + static readonly CcdGetPromotions200ResponseInner k_Promotion = new( + "", + new Guid("00000000-0000-0000-0000-000000000000"), + "my bucket name", + new Guid("00000000-0000-0000-0000-000000000000"), + "production", + new Guid("00000000-0000-0000-0000-000000000000"), + 1, + new Guid("00000000-0000-0000-0000-000000000000"), + CcdGetPromotions200ResponseInner.PromotionStatusEnum.Complete); + + static readonly CcdGetBucket200ResponseLastReleaseBadgesInner k_Badge = + new() + { + Name = "badge 1", + Releaseid = new Guid(k_Uuid), + Releasenum = 1 + }; + + static readonly List k_ListBadge = + new() + { + new CcdGetBucket200ResponseLastReleaseBadgesInner + { + Name = "badge 1", + Releaseid = new Guid(k_Uuid), + Releasenum = 1 + }, + new CcdGetBucket200ResponseLastReleaseBadgesInner + { + Name = "badge 2", + Releaseid = new Guid(k_Uuid), + Releasenum = 2 + } + }; + + static readonly CcdGetBucket200ResponseLastRelease k_Release = new() + { + Releaseid = new Guid(k_Uuid), + Releasenum = 1, + Badges = k_ListBadge, + Notes = "my note", + PromotedFromBucket = new Guid(k_Uuid), + PromotedFromRelease = new Guid(k_Uuid) + }; + + static readonly CcdGetEntries200ResponseInner k_Entry = new() + { + Complete = true, + ContentHash = "ac043a397e20f96d5ddffb8b16d5defd", + ContentLink = + "https://00000000-0000-0000-0000-000000000000.client-api.unity3dusercontent.com/client_api/v1/environments/00000000-0000-0000-0000-000000000000/buckets/00000000-0000-0000-0000-000000000000/entries/00000000-0000-0000-0000-000000000000/versions/00000000-0000-0000-0000-000000000000/content/", + ContentSize = 2692709, + ContentType = "image/jpeg", + CurrentVersionid = new Guid(k_Uuid), + Entryid = new Guid(k_Uuid), + Labels = new List() + { + "my label" + }, + Metadata = "{}", + Path = "image.jpg", + SignedUrl = "http://localhost:8080/ccd/upload" + }; + + static readonly CcdGetBucket200Response k_BucketResponse = new() + { + Id = new Guid("00000000-0000-0000-0000-000000000000"), + Name = "test abc", + Description = "my description", + EnvironmentName = "production", + Projectguid = new Guid(CommonKeys.ValidProjectId), + Private = true + }; + + static readonly List k_Entries = new() + { + k_Entry + }; + + public Task> CreateMappingModels() + { + IReadOnlyList models = new List(); + return Task.FromResult(models); + } + + public void CustomMock(WireMockServer mockServer) + { + // Define the response headers + var responseHeaders = new Dictionary> + { + { "Content-Type", new WireMockList("application/json") }, + { "Content-Range", new WireMockList("items 1-10/10") }, + { + "unity-ratelimit", + new WireMockList("limit=40,remaining=39,reset=1;limit=100000,remaining=99999,reset=1800") + } + }; + + MockGetAllBuckets(mockServer, responseHeaders); + MockGetBucket(mockServer, responseHeaders); + MockDeleteBucket(mockServer, responseHeaders); + MockPostBucket(mockServer, responseHeaders); + MockGetBucketPermission(mockServer, responseHeaders); + MockPutBucketPermission(mockServer, responseHeaders); + MockPostBucketPermission(mockServer, responseHeaders); + + MockListReleases(mockServer, responseHeaders); + MockGetRelease(mockServer, responseHeaders); + MockUpdateRelease(mockServer, responseHeaders); + MockCreateRelease(mockServer, responseHeaders); + + MockGetAllBadges(mockServer, responseHeaders); + MockCreateBadge(mockServer, responseHeaders); + MockDeleteBadge(mockServer, responseHeaders); + + MockPostPromoteBucket(mockServer, responseHeaders); + MockGetPromotionStatus(mockServer, responseHeaders); + + MockGetEntry(mockServer, responseHeaders); + MockGetAllEntries(mockServer, responseHeaders); + MockPutEntry(mockServer, responseHeaders); + MockCreateOrUpdateEntry(mockServer, responseHeaders); + MockDeleteEntry(mockServer, responseHeaders); + + MockUploadContent(mockServer, responseHeaders); + } + + static ArrayList MockGetAllBuckets( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + var listBucket = new ArrayList(); + + listBucket.Add( + new CcdGetBucket200Response() + { + Id = new Guid("00000000-0000-0000-0000-000000000000"), + Name = "test abc", + Description = "my description", + EnvironmentName = "production", + Projectguid = new Guid(CommonKeys.ValidProjectId), + Private = true + }); + + listBucket.Add( + new CcdGetBucket200Response() + { + Id = new Guid("00000000-0000-0000-0000-000000000000"), + Name = "test abc", + Description = "my description", + EnvironmentName = "production", + Projectguid = new Guid(CommonKeys.ValidProjectId), + Private = true + }); + + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(listBucket) + .WithStatusCode(200)); + + return listBucket; + } + + static void MockGetAllBadges( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/badges") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_ListBadge) + .WithStatusCode(200)); + } + + static void MockCreateBadge(WireMockServer mockServer, Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/badges") + .UsingPut()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Badge) + .WithStatusCode(200)); + } + + static void MockDeleteBadge(WireMockServer mockServer, Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/badges/*") + .UsingDelete()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithStatusCode(204)); + } + + static void MockListReleases( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + var list = new List(); + list.Add(k_Release); + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/releases") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(list) + .WithStatusCode(200)); + } + + static void MockGetRelease(WireMockServer mockServer, Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/releases/*") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Release) + .WithStatusCode(200)); + } + + static void MockCreateRelease( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/releases") + .UsingPost()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Release) + .WithStatusCode(200)); + } + + static void MockUpdateRelease( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/releases/*") + .UsingPut()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Release) + .WithStatusCode(200)); + } + + static void MockGetBucket( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_BucketResponse) + .WithStatusCode(200)); + } + + static void MockGetBucketPermission( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + var list = new List(); + + list.Add( + new CcdGetAllByBucket200ResponseInner( + "1", + "1", + "bucket/00000000-0000-0000-0000-000000000000", + "user" + )); + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/permissions") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(list) + .WithStatusCode(200)); + } + + static void MockPostBucketPermission( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/permissions") + .UsingPost()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Permission) + .WithStatusCode(200)); + } + + static void MockPutBucketPermission( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/permissions") + .UsingPut()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Permission) + .WithStatusCode(200)); + + } + + static void MockDeleteBucket( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*") + .UsingDelete()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithStatusCode(201)); + } + + static void MockPostBucket( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets") + .UsingPost()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_BucketResponse) + .WithStatusCode(200)); + } + + static void MockPostPromoteBucket( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/*/buckets/*/promoteasync") + .UsingPost()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Promote) + .WithStatusCode(200)); + } + + static void MockGetPromotionStatus( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/promote/*") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Promotion) + .WithStatusCode(200)); + } + + static void MockGetAllEntries( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithParam("starting_after", "00000000-0000-0000-0000-000000000000") + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/entries") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(new List()) + .WithStatusCode(200)); + + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/entries") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Entries) + .WithStatusCode(200)); + + } + + static void MockGetEntry( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/entries/*") + .UsingGet()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Entry) + .WithStatusCode(200)); + } + + static void MockCreateOrUpdateEntry( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/entry_by_path") + .UsingPost()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Entry) + .WithStatusCode(200)); + } + + static void MockPutEntry( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/entries/*") + .UsingPut()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithBodyAsJson(k_Entry) + .WithStatusCode(200)); + } + + static void MockDeleteEntry( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath( + $"/ccd/management/v1/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}/buckets/*/entries/*") + .UsingDelete()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithStatusCode(204)); + } + + static void MockUploadContent( + WireMockServer mockServer, + Dictionary> responseHeaders) + { + mockServer + .Given( + Request.Create() + .WithPath("/ccd/upload") + .UsingPut()) + .RespondWith( + Response.Create() + .WithHeaders(responseHeaders) + .WithStatusCode(200)); + } + +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/CloudSaveApiMock.cs b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/CloudSaveApiMock.cs new file mode 100644 index 0000000..8dcae7c --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/CloudSaveApiMock.cs @@ -0,0 +1,197 @@ +using System.Diagnostics; +using System.Net; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.MockServer.Common; +using Unity.Services.Gateway.CloudSaveApiV1.Generated.Model; +using WireMock.Admin.Mappings; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace Unity.Services.Cli.MockServer.ServiceMocks; + +public class CloudSaveApiMock : IServiceApiMock +{ + const string k_CloudSavePath = "/cloud-save/v1"; + const string k_CloudSaveDataPath = "data"; + const string k_BaseUrl = $"{k_CloudSavePath}/{k_CloudSaveDataPath}/projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}"; + + static readonly GetIndexIdsResponse k_GetIndexIdsResponse = new GetIndexIdsResponse( + new List + { + new LiveIndexConfigInner( + "testIndex1", + LiveIndexConfigInner.EntityTypeEnum.Player, + AccessClass.Default, + IndexStatus.READY, + new List() + { + new IndexField("testIndexKey1", true) + } + ), + new LiveIndexConfigInner( + "testIndex2", + LiveIndexConfigInner.EntityTypeEnum.Custom, + AccessClass.Private, + IndexStatus.BUILDING, + new List() + { + new IndexField("testIndexKey2", false) + } + ), + } + ); + + static readonly GetCustomIdsResponse k_GetCustomIdsResponse = new( + new List + { + new( + "testId1", + new AccessClassesWithMetadata( + null, new AccessClassMetadata(1, 100), null, new AccessClassMetadata(2, 200))), + new( + "testId1", + new AccessClassesWithMetadata( + null, new AccessClassMetadata(1, 100), null, new AccessClassMetadata(2, 200))), + }, + new GetPlayersWithDataResponseLinks("someLink") + ); + + static readonly List k_ValidQueryResponseList = new List() + { + new QueryIndexResponseResultsInner( + "id", + new List() + { + new Item( + "key1", + "value", + "writelock", + new ModifiedMetadata(DateTime.Now), + new ModifiedMetadata(DateTime.Today)) + } + ) + }; + + static readonly QueryIndexResponse k_ValidQueryResponse = new QueryIndexResponse + { + Results = k_ValidQueryResponseList + }; + + static readonly List k_ValidIndexFields = new List() + { + new IndexField("key1", true), + new IndexField("key2", false) + }; + + static readonly CreateIndexBody k_ValidCreatePlayerIndexBody = new CreateIndexBody ( + new CreateIndexBodyIndexConfig(k_ValidIndexFields)); + + static readonly CreateIndexResponse k_ValidCreateIndexResponse = new CreateIndexResponse("id", IndexStatus.READY); + + public async Task> CreateMappingModels() + { + var cloudsaveServiceModels = await MappingModelUtils.ParseMappingModelsFromGeneratorConfigAsync("cloud-save-api-v1-generator-config.yaml", new()); + return cloudsaveServiceModels.ToArray(); + } + + static void MockListIndexes(WireMockServer mockServer) + { + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/indexes") + .UsingGet()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_GetIndexIdsResponse) + .WithStatusCode(HttpStatusCode.OK)); + } + + static void MockListCustomIds(WireMockServer mockServer) + { + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/custom") + .UsingGet()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_GetCustomIdsResponse) + .WithStatusCode(HttpStatusCode.OK)); + } + + static void MockPlayerQueries(WireMockServer mockServer) + { + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/indexes/players") + .UsingPost()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_ValidQueryResponse) + .WithStatusCode(HttpStatusCode.OK)); + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/indexes/players/public") + .UsingPost()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_ValidQueryResponse) + .WithStatusCode(HttpStatusCode.OK)); + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/indexes/players/protected") + .UsingPost()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_ValidQueryResponse) + .WithStatusCode(HttpStatusCode.OK)); + } + + static void MockCustomDataQueries(WireMockServer mockServer) + { + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/custom/query") + .UsingPost()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_ValidQueryResponse) + .WithStatusCode(HttpStatusCode.OK)); + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/custom/private/query") + .UsingPost()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_ValidQueryResponse) + .WithStatusCode(HttpStatusCode.OK)); + } + + static void MockCreatePlayerIndexes(WireMockServer mockServer) + { + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/indexes/players") + .UsingPost()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_ValidCreateIndexResponse) + .WithStatusCode(HttpStatusCode.OK)); + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/indexes/players/protected") + .UsingPost()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_ValidCreateIndexResponse) + .WithStatusCode(HttpStatusCode.OK)); + mockServer.Given(Request.Create() + .WithPath($"{k_BaseUrl}/indexes/players/public") + .UsingPost()) + .RespondWith(Response.Create() + .WithHeaders(new Dictionary { { "Content-Type", "application/json" } }) + .WithBodyAsJson(k_ValidCreateIndexResponse) + .WithStatusCode(HttpStatusCode.OK)); + } + + public void CustomMock(WireMockServer mockServer) + { + MockListIndexes(mockServer); + MockListCustomIds(mockServer); + MockPlayerQueries(mockServer); + MockCustomDataQueries(mockServer); + MockCreatePlayerIndexes(mockServer); + mockServer.AllowPartialMapping(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/GameServerHostingApiMock.cs b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/GameServerHostingApiMock.cs index caab193..588b0cf 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/GameServerHostingApiMock.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/ServiceMocks/GameServerHosting/GameServerHostingApiMock.cs @@ -1,18 +1,21 @@ using System.Net; +using System.Text; using Unity.Services.Cli.MockServer.Common; using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; -using File = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.File; -using Machine = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.Machine1; using WireMock.Admin.Mappings; using WireMock.Net.OpenApiParser.Settings; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; +using File = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.File; +using Machine = Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model.Machine1; namespace Unity.Services.Cli.MockServer.ServiceMocks.GameServerHosting; public class GameServerHostingApiMock : IServiceApiMock { + public const string TempFileName = "temp-file.txt"; + public async Task> CreateMappingModels() { var models = await MappingModelUtils.ParseMappingModelsAsync( @@ -46,6 +49,7 @@ public void CustomMock(WireMockServer mockServer) MockBuildConfigurationUpdate(mockServer); MockBuildConfigurationList(mockServer); MockMachineList(mockServer); + MockFilesDownload(mockServer); } static void MockFleetGet(WireMockServer mockServer) @@ -97,8 +101,22 @@ static void MockServerGet(WireMockServer mockServer) machineID: 123, machineName: "test machine", machineSpec: new MachineSpec1( - contractEndDate: new DateTime(2020, 12, 31, 12, 0, 0, DateTimeKind.Utc), - contractStartDate: new DateTime(2020, 1, 1, 12, 0, 0, DateTimeKind.Utc), + contractEndDate: new DateTime( + 2020, + 12, + 31, + 12, + 0, + 0, + DateTimeKind.Utc), + contractStartDate: new DateTime( + 2020, + 1, + 1, + 12, + 0, + 0, + DateTimeKind.Utc), cpuName: "test-cpu", cpuShortname: "tc" ), @@ -134,8 +152,22 @@ static void MockServerList(WireMockServer mockServer) machineID: 123, machineName: "test machine", machineSpec: new MachineSpec1( - contractEndDate: new DateTime(2020, 12, 31, 12, 0,0, DateTimeKind.Utc), - contractStartDate: new DateTime(2020, 1, 1, 12, 0, 0, DateTimeKind.Utc), + contractEndDate: new DateTime( + 2020, + 12, + 31, + 12, + 0, + 0, + DateTimeKind.Utc), + contractStartDate: new DateTime( + 2020, + 1, + 1, + 12, + 0, + 0, + DateTimeKind.Utc), cpuName: "test-cpu", cpuShortname: "tc" ), @@ -196,6 +228,7 @@ static void MockBuildList(WireMockServer mockServer) 1, 11, "Build1", + buildVersionName: "v1", ccd: new CCDDetails( new Guid(Keys.ValidBucketId), new Guid(Keys.ValidRegionId)), @@ -206,6 +239,7 @@ static void MockBuildList(WireMockServer mockServer) 2, 22, "Build2", + buildVersionName: "v1", container: new ContainerImage("v1"), osFamily: BuildListInner.OsFamilyEnum.LINUX, syncStatus: BuildListInner.SyncStatusEnum.SYNCED, @@ -228,6 +262,7 @@ static void MockBuild(WireMockServer mockServer) 1, "name1", CreateBuild200Response.BuildTypeEnum.S3, + "v1", ccd: new CCDDetails() ); var request = Request.Create() @@ -251,6 +286,7 @@ static void MockBuildBucket(WireMockServer mockServer) Keys.ValidBuildIdBucket, "Bucket Build", CreateBuild200Response.BuildTypeEnum.S3, + "v1", s3: new AmazonS3Details("bucket-url") ); @@ -271,6 +307,7 @@ static void MockBuildContainer(WireMockServer mockServer) Keys.ValidBuildIdContainer, "Container Build", CreateBuild200Response.BuildTypeEnum.CONTAINER, + "v1", container: new ContainerImage("v1") ); @@ -291,6 +328,7 @@ static void MockBuildFileUpload(WireMockServer mockServer) Keys.ValidBuildIdFileUpload, "File Upload Build", CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, + "v1", ccd: new CCDDetails() ); @@ -337,6 +375,7 @@ static void MockBuildInstalls(WireMockServer mockServer) var buildInstalls = new List { new BuildListInner1( + Keys.ValidBuildVersionName, new CCDDetails( Guid.Parse(Keys.ValidBucketId), Guid.Parse(Keys.ValidReleaseId) @@ -381,6 +420,8 @@ static void MockBuildCreateResponse(WireMockServer mockServer) var build = new CreateBuild200Response( 1, "Build1", + CreateBuild200Response.BuildTypeEnum.FILEUPLOAD, + "v1", ccd: new CCDDetails( new Guid(Keys.ValidBucketId), new Guid(Keys.ValidReleaseId) @@ -576,8 +617,6 @@ static void MockFleetAvailableRegionsResponse(WireMockServer mockServer) mockServer.Given(request).RespondWith(response); } - public const string TempFileName = "temp-file.txt"; - static void MockMachineList(WireMockServer mockServer) { var machines = new List @@ -622,4 +661,39 @@ static void MockMachineList(WireMockServer mockServer) mockServer.Given(request).RespondWith(response); } + + static void MockFilesDownload(WireMockServer mockServer) + { + var contentUrl = new GenerateContentURLResponse( + url: $"{mockServer.Url}{Keys.MockedSignedUrlPath}" + ); + + var requestSignedUrlJson = new GenerateContentURLRequest( + path: Keys.ValidErrorLogPath, + serverId: long.Parse(Keys.ValidServerId) + ); + + var requestSignedUrl = Request.Create() + .WithPath(Keys.GenerateDownloadUrlPath) + .WithBodyAsJson(requestSignedUrlJson) + .UsingPost(); + + var responseSignedUrl = Response.Create() + .WithBodyAsJson(contentUrl) + .WithStatusCode(HttpStatusCode.OK); + + mockServer.Given(requestSignedUrl).RespondWith(responseSignedUrl); + + var fileContentBytes = Encoding.UTF8.GetBytes(Keys.MockFileContent); + + var request = Request.Create() + .WithPath(Keys.MockedSignedUrlPath) + .UsingGet(); + + var response = Response.Create() + .WithBody(fileContentBytes) + .WithStatusCode(HttpStatusCode.OK); + + mockServer.Given(request).RespondWith(response); + } } 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 86dee2f..2b9bb34 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,5 +1,4 @@ using Unity.Services.Cli.MockServer.Common; -using Unity.Services.Gateway.GameServerHostingApiV1.Generated.Model; namespace Unity.Services.Cli.MockServer.ServiceMocks.GameServerHosting; @@ -16,12 +15,15 @@ public static class Keys public const long ValidBuildId = 1; public const string ValidBucketId = "00000000-0000-0000-0000-000000000000"; public const string ValidReleaseId = "00000000-0000-0000-0000-000000000000"; + public const string ValidBuildVersionName = "11.11.11.alpha.2"; public const long ValidBuildIdBucket = 101; public const long ValidBuildIdContainer = 102; 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 ValidUsageSettingsJson = + "{\"hardwareType\":\"CLOUD\", \"machineType\":\"GCP-N2\", \"maxServersPerMachine\":\"5\"}"; public const string ProjectPathPart = $"projects/{CommonKeys.ValidProjectId}/environments/{CommonKeys.ValidEnvironmentId}"; @@ -37,8 +39,13 @@ public static class Keys public const string FleetRegionsPath = $"{ValidFleetPath}/regions"; public const string BuildsPath = $"/multiplay/builds/v1/{ProjectPathPart}/builds"; - public const string BuildConfigurationsPath = $"/multiplay/build-configurations/v1/{ProjectPathPart}/build-configurations"; - public static readonly string ValidBuildConfigurationPath = $"{BuildConfigurationsPath}/{ValidBuildConfigurationId}"; + + public const string BuildConfigurationsPath = + $"/multiplay/build-configurations/v1/{ProjectPathPart}/build-configurations"; + + public static readonly string ValidBuildConfigurationPath = + $"{BuildConfigurationsPath}/{ValidBuildConfigurationId}"; + public static readonly string ValidBuildPath = $"{BuildsPath}/{ValidBuildId}"; public static readonly string ValidBuildInstallsPath = $"{ValidBuildPath}/installs"; @@ -46,4 +53,9 @@ public static class Keys public static readonly string ValidBuildPathContainer = $"{BuildsPath}/{ValidBuildIdContainer}"; public static readonly string ValidBuildPathFileUpload = $"{BuildsPath}/{ValidBuildIdFileUpload}"; public static readonly string ValidBuildPathFileUploadFiles = $"{BuildsPath}/{ValidBuildIdFileUpload}/files"; + + public static readonly string MockedSignedUrlPath = "/multiplay/files/signed-url"; + public static readonly string GenerateDownloadUrlPath = $"{FilesPath}/generate-download-url"; + public static readonly string MockFileContent = "This is a mock file content"; + public static readonly string ValidErrorLogPath = "/logs/error.log"; } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/Unity.Services.Cli.Integration.MockServer.csproj b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/Unity.Services.Cli.Integration.MockServer.csproj index fe53310..5bd4f2e 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/Unity.Services.Cli.Integration.MockServer.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServer/Unity.Services.Cli.Integration.MockServer.csproj @@ -9,6 +9,7 @@ true + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServerApp/Program.cs b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServerApp/Program.cs index f80795f..6dce538 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServerApp/Program.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Integration.MockServerApp/Program.cs @@ -33,5 +33,6 @@ static async Task MockIntegrationTestAsync(MockApi mockApi, IntegrationConfig in //Replace these mock with your service mocks await mockApi.MockServiceAsync(new IdentityV1Mock()); await mockApi.MockServiceAsync(new CloudCodeV1Mock()); + await mockApi.MockServiceAsync(new CloudContentDeliveryApiMock()); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/CloudCode/CloudCodeDeployTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/CloudCode/CloudCodeDeployTests.cs index 394c81d..b6befc1 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/CloudCode/CloudCodeDeployTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/CloudCode/CloudCodeDeployTests.cs @@ -117,7 +117,7 @@ public async Task DeployValidConfigFromDirectorySucceedWithJsonOutput() Array.Empty(), m_DeployedContents, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable("Cloud Code Scripts"), Formatting.Indented); await GetLoggedInCli() .Command($"deploy {k_TestDirectory} -j -s cloud-code-scripts -s cloud-code-modules") .AssertStandardOutputContains(resultString) @@ -139,7 +139,8 @@ public async Task DeployConfig_DryRun() Array.Empty(), Array.Empty(), true); - var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); + + var resultString = JsonConvert.SerializeObject(logResult.ToTable("Cloud Code Scripts"), Formatting.Indented); await GetLoggedInCli() .Command($"deploy {k_TestDirectory} -j --dry-run -s cloud-code-scripts -s cloud-code-modules") .AssertStandardOutputContains(resultString) @@ -205,8 +206,8 @@ public async Task DeployWithReconcileWillDeleteRemoteFiles() Array.Empty()); var finalResult = new TableContent(); - finalResult.AddRows(logResultCc.ToTable()); - finalResult.AddRows(logResultCcm.ToTable()); + finalResult.AddRows(logResultCc.ToTable("Cloud Code Scripts")); + finalResult.AddRows(logResultCcm.ToTable("Cloud Code Modules")); var resultString = JsonConvert.SerializeObject(finalResult, Formatting.Indented); await GetLoggedInCli() @@ -229,7 +230,7 @@ public async Task DeployWithoutReconcileWillNotDeleteRemoteFiles() Array.Empty(), m_DeployedContents, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable("Cloud Code Scripts"), Formatting.Indented); await GetLoggedInCli() .Command($"deploy {k_TestDirectory} -j -s cloud-code-scripts -s cloud-code-modules") .AssertStandardOutputContains(resultString) diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/Economy/EconomyDeployTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/Economy/EconomyDeployTests.cs index 48ff7ea..92c2232 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/Economy/EconomyDeployTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Deploy/Economy/EconomyDeployTests.cs @@ -69,11 +69,12 @@ public async Task DeployWithReconcileWillDeleteRemoteFiles() "Remote", 100.0f, Statuses.Deployed, - "Deleted remotely"); + "Deleted remotely", + SeverityLevel.Success); var createdContentList = await CreateDeployTestFilesAsync(DeployedTestCases); //TODO: remove this after message details will be adjusted in other services or removed from Economy module - createdContentList[0].Status = new DeploymentStatus(Statuses.Deployed, "Created remotely"); + createdContentList[0].Status = new DeploymentStatus(Statuses.Deployed, "Created remotely", SeverityLevel.Success); // deployed content list has the same as the created + the content economy content deployed var deployedContentList = createdContentList.ToList(); deployedContentList.Add(content); @@ -88,7 +89,7 @@ public async Task DeployWithReconcileWillDeleteRemoteFiles() deployedContentList, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable("Economy"), Formatting.Indented); await GetFullySetCli() .Command($"deploy {TestDirectory} -j --reconcile -s economy") diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/DeployTestsFixture.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/DeployTestsFixture.cs index 4331e40..04ad1ce 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/DeployTestsFixture.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/DeployTestsFixture.cs @@ -133,7 +133,7 @@ public virtual async Task DeployValidConfigFromDirectorySucceedWithJsonOutput() { var deployedContents = await CreateDeployTestFilesAsync(DeployedTestCases); - deployedContents[0].Status = new DeploymentStatus(Statuses.Deployed, "Created remotely"); + deployedContents[0].Status = new DeploymentStatus(Statuses.Deployed, "Created remotely", SeverityLevel.Success); var logResult = CreateResult( deployedContents, @@ -142,7 +142,7 @@ public virtual async Task DeployValidConfigFromDirectorySucceedWithJsonOutput() deployedContents, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable("Economy"), Formatting.Indented); await GetFullySetCli() .Command($"deploy {TestDirectory} -j") .AssertStandardOutputContains(resultString) @@ -168,7 +168,7 @@ public virtual async Task DeployConfig_DryRun() Array.Empty(), Array.Empty(), true); - var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable("Economy"), Formatting.Indented); await GetFullySetCli() .Command($"deploy {TestDirectory} -j --dry-run") .AssertStandardOutputContains(resultString) @@ -193,7 +193,7 @@ public virtual async Task DeployWithoutReconcileWillNotDeleteRemoteFiles() Array.Empty(), contentList, Array.Empty()); - var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable("Economy"), Formatting.Indented); await GetFullySetCli() .Command($"deploy {TestDirectory} -j") @@ -204,7 +204,7 @@ await GetFullySetCli() #endregion - protected async Task> CreateDeployTestFilesAsync( + protected static async Task> CreateDeployTestFilesAsync( IReadOnlyList testCases) { List deployedContentList = new(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/CloudCodeFetchTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/CloudCodeFetchTests.cs index e8ac842..d0a817f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/CloudCodeFetchTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/CloudCodeFetchTests.cs @@ -148,7 +148,7 @@ public async Task FetchValidConfigFromDirectorySucceedWithJsonOutput(string dryR fetchedPaths, Array.Empty(), !string.IsNullOrEmpty(dryRunOption)); - var resultString = JsonConvert.SerializeObject(res.ToTable(), Formatting.Indented); + var resultString = JsonConvert.SerializeObject(res.ToTable("Cloud Code Scripts"), Formatting.Indented); await GetLoggedInCli() .Command($"fetch {k_TestDirectory} {dryRunOption} -j -s cloud-code-scripts") .AssertStandardOutputContains(resultString) @@ -183,7 +183,7 @@ public async Task FetchValidConfigReconcileSucceedWithJsonOutput(string dryRunOp fetchedPaths, Array.Empty(), !string.IsNullOrEmpty(dryRunOption)); - var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); + var resultString = JsonConvert.SerializeObject(logResult.ToTable("Cloud Code Scripts"), Formatting.Indented); await GetLoggedInCli() .Command($"fetch {k_TestDirectory} --reconcile -s cloud-code-scripts {dryRunOption} -j ") .AssertStandardOutputContains(resultString) diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/RemoteConfigFetchTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/RemoteConfigFetchTests.cs index ada799b..9dcc52c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/RemoteConfigFetchTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/Fetch/RemoteConfigFetchTests.cs @@ -128,7 +128,6 @@ await GetLoggedInCli() .AssertStandardOutput( output => { - Console.WriteLine(output); StringAssert.Contains($"Successfully fetched the following files:{Environment.NewLine}", output); foreach (var file in m_FetchedTestCases) { @@ -175,8 +174,7 @@ public async Task FetchValidConfigFromDirectorySucceedWithJsonOutput() m_FetchedKeysTestCases, Array.Empty(), fetchedPaths, - Array.Empty(), - false); + Array.Empty()); var resultString = JsonConvert.SerializeObject(logResult.ToTable(), Formatting.Indented); await GetLoggedInCli() .Command($"fetch {k_TestDirectory} -j -s remote-config") diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/FetchTestsFixture.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/FetchTestsFixture.cs index 4607474..5410d45 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/FetchTestsFixture.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Authoring/FetchTestsFixture.cs @@ -9,6 +9,7 @@ using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.MockServer.Common; using Unity.Services.Cli.MockServer.ServiceMocks; +using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.Cli.IntegrationTest.Authoring; @@ -51,7 +52,7 @@ public static AuthoringTestCase SetTestCase(AuthoringTestCase testCase, string s var type = testCase.DeployedContent.Type; testCase.DeployedContent = new DeployContent( - testCase.ConfigFileName, type, testCase.ConfigFilePath, 100, status, detail); + testCase.ConfigFileName, type, testCase.ConfigFilePath, 100, status, detail, SeverityLevel.Success); return testCase; } @@ -270,7 +271,7 @@ protected static string FormatDefaultOutput(List deployContentLis protected static string FormatJsonOutput(List deployContentList, bool isDryRun) { var fetchResult = GetFetchResult(deployContentList, isDryRun); - return JsonConvert.SerializeObject(fetchResult.ToTable(), Formatting.Indented); + return JsonConvert.SerializeObject(fetchResult.ToTable("Economy"), Formatting.Indented); } static FetchResult GetFetchResult(List deployContentList, bool isDryRun) diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliFixture.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliFixture.cs index 03d458d..ab2c015 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliFixture.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliFixture.cs @@ -1,3 +1,5 @@ +using System; +using System.Diagnostics; using System.IO; using System.Threading.Tasks; using NUnit.Framework; @@ -36,6 +38,7 @@ public abstract class UgsCliFixture protected readonly MockApi MockApi = new(NetworkTargetEndpoints.MockServer); readonly IntegrationConfig m_IntegrationConfig = new(); + Stopwatch? m_Stopwatch; [OneTimeTearDown] public void DisposeMockServer() @@ -48,6 +51,28 @@ public void DisposeIntegrationConfig() m_IntegrationConfig.Dispose(); } + [SetUp] + public void TestOutputTrackingSetup() + { + Console.WriteLine($"Running Test '{TestContext.CurrentContext.Test.Name}' ..."); + m_Stopwatch = Stopwatch.StartNew(); + } + + [TearDown] + public void TestOutputTrackingTeardown() + { + string? timeElapsedStr = null; + if (m_Stopwatch != null) + { + m_Stopwatch!.Stop(); + timeElapsedStr = $"in {m_Stopwatch.Elapsed.Milliseconds} ms"; + } + + var printLine = + $"Finished Test '{TestContext.CurrentContext.Test.Name}' {timeElapsedStr ?? string.Empty}"; + Console.WriteLine(printLine); + } + [OneTimeSetUp] public async Task BuildCliIfNeeded() { diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliTestCase.ExternalProcess.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliTestCase.ExternalProcess.cs index ff60fa6..2d2f340 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliTestCase.ExternalProcess.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Common/UgsCliTestCase.ExternalProcess.cs @@ -10,7 +10,7 @@ public partial class UgsCliTestCase { class ExternalProcess : IProcess { - const int k_Timeout = 30; + const int k_Timeout = 45; public ExternalProcess(Process innerProcess) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/ConfigTests/ConfigTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/ConfigTests/ConfigTests.cs index a5ff4b3..0259bba 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/ConfigTests/ConfigTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/ConfigTests/ConfigTests.cs @@ -50,7 +50,7 @@ public async Task ConfigGetReadsFromConfigFile() [Test] public async Task ConfigGetJsonReturnsJson() { - var expected = JsonConvert.SerializeObject( "some-value"); + var expected = JsonConvert.SerializeObject("some-value"); await new UgsCliTestCase() .Command("config set environment-name some-value") .Command("config get environment-name -j") @@ -84,7 +84,7 @@ public async Task ConfigSetProjectIdFails() [Test] public async Task ConfigSetWithInvalidKeyErrorsOut() { - const string expectedError = "key 'invalid-key' not allowed. Allowed values: environment-name,project-id"; + const string expectedError = "key 'invalid-key' not allowed. Allowed values: environment-name,project-id,bucket-id"; await new UgsCliTestCase() .Command("config set invalid-key random-value") 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 345b00e..37059df 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingFleetTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingFleetTests.cs @@ -415,7 +415,6 @@ 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 index ddf0ae1..23720c8 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFileDownloadTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingServerFileDownloadTests.cs @@ -1,14 +1,22 @@ +using System; +using System.IO; using System.Threading.Tasks; using NUnit.Framework; +using SharpYaml.Tokens; using Unity.Services.Cli.Common.Exceptions; using Unity.Services.Cli.IntegrationTest.Common; using Unity.Services.Cli.MockServer.Common; +using Unity.Services.Cli.MockServer.ServiceMocks.GameServerHosting; 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"; + // Get the operating system's temporary directory + static string tempDirectory = Path.GetTempPath(); + static string outputArgPath = $"{tempDirectory}/server.log"; + + static readonly string k_ServerFilesDownloadCommand = $"gsh server files download --server-id {Keys.ValidServerId} --path {Keys.ValidErrorLogPath} --output {outputArgPath}"; [Test] [Category("gsh")] @@ -23,6 +31,14 @@ await GetFullySetCli() { Assert.IsTrue(str.Contains("Downloading file...")); }) + .AssertStandardError( + str => + { + Assert.IsTrue(str.Contains($"File downloaded to {outputArgPath}")); + string fileContents = File.ReadAllText(outputArgPath); + Assert.AreEqual(Keys.MockFileContent, fileContents); + } + ) .ExecuteAsync(); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingTests.cs b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingTests.cs index 2a2fe9e..2a0691f 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/GameServerHostingTests/GameServerHostingTests.cs @@ -56,7 +56,8 @@ void DeleteTempFiles() const string k_ProjectIdIsNotSet = "'project-id' is not set in project configuration."; const string k_EnvironmentNameIsNotSet = "'environment-name' is not set in project configuration."; - const string k_BuildConfigurationCreateOrUpdateCommandComplete = "--binary-path simple-game-server-go --build 25289 --command-line \"--init game.init\" --cores 1 --memory 100 --name \"Testing BC\" --query-type sqp --speed 100"; + const string k_BuildConfigurationCreateOrUpdateCommandComplete = "--binary-path simple-game-server-go --build 25289 --command-line \"--init game.init\" --cores 1 --memory 100 --name \"Testing BC\" --query-type sqp --readiness true --speed 100"; + const string k_BuildConfigurationCreateOrUpdateCommandMissingReadiness = "--binary-path simple-game-server-go --build 25289 --command-line \"--init game.init\" --cores 1 --memory 100 --name \"Testing BC\" --query-type sqp --speed 100"; const string k_BuildConfigurationCreateOrUpdateCommandMissingBinaryPath = "--build 25289 --command-line \"--init game.init\" --cores 1 --memory 100 --name \"Testing BC\" --query-type sqp --speed 100"; const string k_BuildConfigurationCreateOrUpdateCommandMissingBuild = "--binary-path simple-game-server-go --command-line \"--init game.init\" --cores 1 --memory 100 --name \"Testing BC\" --query-type sqp --speed 100"; const string k_BuildConfigurationCreateOrUpdateCommandMissingCommandLine = "--binary-path simple-game-server-go --build 25289 --cores 1 --memory 100 --name \"Testing BC\" --query-type sqp --speed 100"; 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 e4703c6..b4fe5c0 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/LeaderboardTests/LeaderboardTests.cs @@ -178,31 +178,6 @@ public async Task LeaderboardResetSucceed() var expectedMessage = "leaderboard reset! Version Id: v10"; 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(); - await zipArchiver.ZipAsync(Path.Join(k_TestDirectory, k_DefaultFileName), "test", new[] - { LeaderboardApiMock.Leaderboard1, LeaderboardApiMock.Leaderboard2, LeaderboardApiMock.Leaderboard3, - LeaderboardApiMock.Leaderboard4, LeaderboardApiMock.Leaderboard5, LeaderboardApiMock.Leaderboard6, - LeaderboardApiMock.Leaderboard7, LeaderboardApiMock.Leaderboard8, LeaderboardApiMock.Leaderboard9, - LeaderboardApiMock.Leaderboard10, LeaderboardApiMock.Leaderboard11, LeaderboardApiMock.Leaderboard12 }); - - var expectedMessage = "Importing configs..."; - await AssertSuccess($"leaderboards import {k_TestDirectory}", expectedResult: expectedMessage); - } - - [Test] - [Ignore("Flaky Test: Temporarily ignored, will be tackled in GID-2310")] - public async Task LeaderboardImportWithNameSucceed() - { - ZipArchiver zipArchiver = new ZipArchiver(); - await zipArchiver.ZipAsync(Path.Join(k_TestDirectory, k_AlternateFileName), "test", new[] { LeaderboardApiMock.Leaderboard1, LeaderboardApiMock.Leaderboard2, LeaderboardApiMock.Leaderboard3, LeaderboardApiMock.Leaderboard4, LeaderboardApiMock.Leaderboard5, LeaderboardApiMock.Leaderboard6, LeaderboardApiMock.Leaderboard7, LeaderboardApiMock.Leaderboard8, LeaderboardApiMock.Leaderboard9, LeaderboardApiMock.Leaderboard10, LeaderboardApiMock.Leaderboard11, LeaderboardApiMock.Leaderboard12 }); - - var expectedMessage = "Importing configs..."; - await AssertSuccess($"leaderboards import {k_TestDirectory} {k_AlternateFileName}", expectedResult: expectedMessage); - } [Test] public async Task LeaderboardExportSucceed() diff --git a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Unity.Services.Cli.IntegrationTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Unity.Services.Cli.IntegrationTest.csproj index c35715f..10fb01b 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Unity.Services.Cli.IntegrationTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.IntegrationTest/Unity.Services.Cli.IntegrationTest.csproj @@ -18,6 +18,7 @@ + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Handlers/ExportHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Handlers/ExportHandlerTests.cs index b326db2..2c75184 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Handlers/ExportHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Handlers/ExportHandlerTests.cs @@ -29,7 +29,7 @@ class ExportHandlerTests LobbyConfig m_Config = new() { Id = "mock_id", - Config = JsonConvert.DeserializeObject("{ \"mock_key\": \"mock_value\" }") + Config = JsonConvert.DeserializeObject("{ \"mock_key\": \"mock_value\" }")! }; LobbyExporter m_LobbyExporter = null!; @@ -44,7 +44,7 @@ public void SetUp() m_MockRemoteConfigService.Reset(); var configJson = JsonConvert.SerializeObject(m_Config); - var configElement = JsonConvert.DeserializeObject(configJson); + var configElement = JsonConvert.DeserializeObject(configJson)!; m_MockRemoteConfigService.Setup( rc => rc.GetAllConfigsFromEnvironmentAsync(It.IsAny(), diff --git a/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Handlers/LobbyConfigTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Handlers/LobbyConfigTests.cs index 4ce091b..92f8ddc 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Handlers/LobbyConfigTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Handlers/LobbyConfigTests.cs @@ -28,14 +28,19 @@ public void TryParse_SucceedsWithValidConfig() var success = LobbyConfig.TryParse(json, out var lobbyConfig); Assert.True(success); Assert.NotNull(lobbyConfig); - Assert.AreEqual(lobbyConfig.Id, configResponse.Configs.First().Id); - Assert.AreEqual(2, lobbyConfig.Config.Count); - Assert.True(lobbyConfig.Config.ContainsKey(nameof(MockLobbyConfig.StringSetting))); - Assert.AreEqual(k_DefaultStringSetting, - lobbyConfig.Config.GetValue(nameof(MockLobbyConfig.StringSetting)).Value()); - Assert.True(lobbyConfig.Config.ContainsKey(nameof(MockLobbyConfig.IntSetting))); - Assert.AreEqual(k_DefaultIntSetting, - lobbyConfig.Config.GetValue(nameof(MockLobbyConfig.IntSetting)).Value()); + Assert.AreEqual(lobbyConfig?.Id, configResponse.Configs.First().Id); + if (lobbyConfig != null) + { + Assert.AreEqual(2, lobbyConfig.Config.Count); + Assert.True(lobbyConfig.Config.ContainsKey(nameof(MockLobbyConfig.StringSetting))); + Assert.AreEqual( + k_DefaultStringSetting, + lobbyConfig.Config.GetValue(nameof(MockLobbyConfig.StringSetting))!.Value()); + Assert.True(lobbyConfig.Config.ContainsKey(nameof(MockLobbyConfig.IntSetting))); + Assert.AreEqual( + k_DefaultIntSetting, + lobbyConfig.Config.GetValue(nameof(MockLobbyConfig.IntSetting))!.Value()); + } } [Test] @@ -73,11 +78,7 @@ public void TryParse_FailsWithNoConfigs() static RemoteConfigResponse NewDefaultConfig(bool includeMockConfig = true) { var mockConfigJson = includeMockConfig ? JsonConvert.SerializeObject( - new MockLobbyConfig() - { - StringSetting = k_DefaultStringSetting, - IntSetting = k_DefaultIntSetting, - }) : "{}"; + new MockLobbyConfig(k_DefaultStringSetting, k_DefaultIntSetting)) : "{}"; var now = DateTime.Now.ToString(k_TimestampFormat); var config = new RemoteConfigResponse.Config @@ -110,6 +111,12 @@ static RemoteConfigResponse NewDefaultConfig(bool includeMockConfig = true) class MockLobbyConfig { + public MockLobbyConfig(string stringSetting, int intSetting) + { + StringSetting = stringSetting; + IntSetting = intSetting; + } + public string StringSetting { get; set; } public int IntSetting { get; set; } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Unity.Services.Cli.Lobby.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Unity.Services.Cli.Lobby.UnitTest.csproj index 71806c3..d3425c1 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Unity.Services.Cli.Lobby.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Lobby.UnitTest/Unity.Services.Cli.Lobby.UnitTest.csproj @@ -3,6 +3,7 @@ net6.0 enable enable + true false diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentResultTests.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentResultTests.cs index 8f5065e..e27577d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentResultTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig.UnitTest/Deploy/RemoteConfigDeploymentResultTests.cs @@ -34,13 +34,17 @@ public class RemoteConfigDeploymentResultTests public void SetUp() { m_RemoteConfigDeploymentResult = new RemoteConfigDeploymentResult( - m_UpdatedItems, m_DeletedItems, m_CreatedItems, m_AuthoredItems, m_FailedItems); + m_UpdatedItems, + m_DeletedItems, + m_CreatedItems, + m_AuthoredItems, + m_FailedItems); } [Test] public void ToTableHasCorrectAmountRows() { - var tableResult = m_RemoteConfigDeploymentResult.ToTable(); + var tableResult = m_RemoteConfigDeploymentResult.ToTable("Remote Config"); var itemsCount = m_AuthoredItems.Count + m_UpdatedItems.Count + m_CreatedItems.Count + m_DeletedItems.Count + m_FailedItems.Count; diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentResult.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentResult.cs index c356ec9..111caad 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentResult.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigDeploymentResult.cs @@ -19,11 +19,11 @@ public RemoteConfigDeploymentResult( created, authored, failed, - dryRun) - { } + dryRun) { } + public RemoteConfigDeploymentResult(IReadOnlyList results) : base(results) { } - public override TableContent ToTable() + public override TableContent ToTable(string service = "") { var baseTable = new TableContent(); baseTable.IsDryRun = DryRun; diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchResult.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchResult.cs index 2134dd1..e5db1d7 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchResult.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchResult.cs @@ -6,7 +6,6 @@ namespace Unity.Services.Cli.RemoteConfig.Deploy; public class RemoteConfigFetchResult : FetchResult { - public RemoteConfigFetchResult( IReadOnlyList updated, IReadOnlyList deleted, @@ -22,12 +21,13 @@ public RemoteConfigFetchResult( failed, dryRun) { } - public RemoteConfigFetchResult(IReadOnlyList results) : base(results) { } - public override TableContent ToTable() + public override TableContent ToTable(string service = "") { - var baseTable = new TableContent(); - baseTable.IsDryRun = DryRun; + var baseTable = new TableContent() + { + IsDryRun = DryRun + }; foreach (var file in Authored) { diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs index 1133a7e..66a9564 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Deploy/RemoteConfigFetchService.cs @@ -73,7 +73,7 @@ public async Task FetchAsync( return new RemoteConfigFetchResult( fetchResult.Updated.Select(rce => GetDeployContent(rce, "Updated")).ToList(), fetchResult.Deleted.Select(rce => GetDeployContent(rce, "Deleted")).ToList(), - fetchResult.Created.Select(rce => GetDeployContent(rce, "Updated")).ToList(), + fetchResult.Created.Select(rce => GetDeployContent(rce, "Created")).ToList(), fetchResult.Fetched.Select(ToFetchedFile).ToList(), failed, input.DryRun); diff --git a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj index 1b5ed89..0d85824 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.RemoteConfig/Unity.Services.Cli.RemoteConfig.csproj @@ -20,7 +20,7 @@ - + $(DefineConstants);$(ExtraDefineConstants) diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/ScheduleConfigLoaderTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/ScheduleConfigLoaderTests.cs new file mode 100644 index 0000000..3ad9554 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/ScheduleConfigLoaderTests.cs @@ -0,0 +1,62 @@ +using Moq; +using Unity.Services.Cli.Scheduler.Deploy; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.IO; + +namespace Unity.Services.Cli.Scheduler.UnitTest.Deploy; + +[TestFixture] +public class ScheduleConfigLoaderTests +{ + ScheduleResourceLoader? m_SchedulesConfigLoader; + Mock m_FileSystem = null!; + + [SetUp] + public void Setup() + { + m_FileSystem = new Mock(); + m_SchedulesConfigLoader = new ScheduleResourceLoader( + m_FileSystem.Object); + } + + [Test] + public async Task ConfigLoader_Deserializes() + { + var content = @" + { + ""Configs"": { + ""Schedule1"": { + ""EventName"": ""EventType1"", + ""Type"": ""recurring"", + ""Schedule"": ""0 * * * *"", + ""PayloadVersion"": 1, + ""Payload"": ""{}"" + } + } + }"; + m_FileSystem.Setup(f => f.ReadAllText(It.IsAny(), It.IsAny())) + .ReturnsAsync(content); + + var configs = await m_SchedulesConfigLoader! + .LoadResource("path", CancellationToken.None); + + var config = configs.Content.Configs.First(); + + Assert.That(config.Value.Name, Is.EqualTo("Schedule1")); + Assert.That(config.Value.EventName, Is.EqualTo("EventType1")); + Assert.That(configs.Status.MessageSeverity, Is.EqualTo(SeverityLevel.None)); + } + + [Test] + public async Task ConfigLoader_ReportsFailures() + { + var content = @"{'"; + m_FileSystem.Setup(f => f.ReadAllText(It.IsAny(), It.IsAny())) + .ReturnsAsync(content); + + var configs = await m_SchedulesConfigLoader! + .LoadResource("path", CancellationToken.None); + + Assert.That(configs.Status.MessageSeverity, Is.EqualTo(SeverityLevel.Error)); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerClientTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerClientTests.cs new file mode 100644 index 0000000..45a57f0 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerClientTests.cs @@ -0,0 +1,230 @@ +using Moq; +using Unity.Services.Cli.Common.Validator; +using Unity.Services.Cli.Scheduler.Deploy; +using Unity.Services.Cli.Scheduler.UnitTest.Utils; +using Unity.Services.Cli.ServiceAccountAuthentication; +using Unity.Services.Gateway.SchedulerApiV1.Generated.Client; +using Unity.Services.Gateway.SchedulerApiV1.Generated.Api; +using Unity.Services.Gateway.SchedulerApiV1.Generated.Model; +using Configuration = Unity.Services.Gateway.SchedulerApiV1.Generated.Client.Configuration; +using ScheduleConfig = Unity.Services.Scheduler.Authoring.Core.Model.ScheduleConfig; + +namespace Unity.Services.Cli.Scheduler.UnitTest.Deploy; + +[TestFixture] +public class SchedulerClientTests +{ + readonly ScheduleConfig m_Schedule; + Mock m_MockApi; + Mock m_MockAuthService; + Mock m_MockValidator; + SchedulerClient m_SchedulerClient; + + public SchedulerClientTests() + { + m_Schedule = new("foo", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") { Path = "path" }; + m_Schedule.Id = "11111111-1111-1111-1111-111111111111"; + } + + [SetUp] + public void Setup() + { + m_MockApi = new Mock(); + m_MockApi.Setup(a => a.Configuration) + .Returns(new Configuration()); + m_MockAuthService = new Mock(); + m_MockValidator = new Mock(); + m_SchedulerClient = new SchedulerClient(m_MockApi.Object, m_MockAuthService.Object, m_MockValidator.Object); + } + + [Test] + public async Task Initialize_Succeed() + { + await m_SchedulerClient.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + Assert.That(m_SchedulerClient.EnvironmentId.ToString(), Is.EqualTo(TestValues.ValidEnvironmentId)); + Assert.That(m_SchedulerClient.ProjectId.ToString(), Is.EqualTo(TestValues.ValidProjectId)); + Assert.That(m_SchedulerClient.CancellationToken, Is.EqualTo(CancellationToken.None)); + } + + [Test] + public async Task ListMoreThanLimit() + { + await m_SchedulerClient.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + var schedules = Enumerable.Range(0, 75) + .Select( + i => new Gateway.SchedulerApiV1.Generated.Model.ScheduleConfig( + Guid.NewGuid(), + "name" + i, + "event" + i, + m_Schedule.ScheduleType, + m_Schedule.Schedule, + m_Schedule.PayloadVersion, + m_Schedule.Payload)).ToList(); + m_MockApi.Setup( + api => api.ListSchedulerConfigsAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + It.Is(i => i == 50), + It.Is(s => s == null), + It.IsAny(), + It.IsAny())) + .Returns( + Task.FromResult( + new SchedulerConfigPage( + null!, + schedules.Take(50).ToList()))); + m_MockApi.Setup( + api => api.ListSchedulerConfigsAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + It.Is(i => i == 50), + It.Is(s => s == schedules[49].Id.ToString()), + It.IsAny(), + It.IsAny())) + .Returns( + Task.FromResult( + new SchedulerConfigPage( + null!, + schedules.Skip(50).ToList()))); + + var list = await m_SchedulerClient.List(); + + Assert.That(list.Count, Is.EqualTo(75)); + m_MockApi.VerifyAll(); + } + + [Test] + public async Task ListWhenThereAreNone() + { + await m_SchedulerClient.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + m_MockApi.Setup( + api => api.ListSchedulerConfigsAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + It.Is(i => i == 50), + It.Is(s => s == null), + It.IsAny(), + It.IsAny())) + .Returns( + Task.FromResult( + new SchedulerConfigPage( + null!, + new List()))); + + var list = await m_SchedulerClient.List(); + + Assert.That(list.Count, Is.EqualTo(0)); + m_MockApi.VerifyAll(); + } + + [Test] + public async Task UpdateMapsToDeleteCreate() + { + await m_SchedulerClient.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + m_MockApi.Setup( + api => api.DeleteScheduleConfigAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + Guid.Parse(m_Schedule.Id), + 0, + It.IsAny())) + .Returns(Task.CompletedTask); + m_MockApi.Setup(a => a.CreateScheduleConfigAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + It.Is(sch => sch.Name == "foo"), + 0, + It.IsAny())).Returns(Task.FromResult(new ScheduleConfigId(Guid.Parse(m_Schedule.Id)))); + + await m_SchedulerClient.Update(m_Schedule!); + + m_MockApi.VerifyAll(); + } + + [Test] + public async Task UpdateExceptionPropagates() + { + await m_SchedulerClient.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + m_MockApi.Setup( + api => api.DeleteScheduleConfigAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + Guid.Parse(m_Schedule.Id), + 0, + It.IsAny())) + .ThrowsAsync(new ApiException()); + + Assert.ThrowsAsync( async () => await m_SchedulerClient.Update(m_Schedule!) ); + } + + [Test] + public async Task CreateMapsToCreate() + { + await m_SchedulerClient.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + m_MockApi.Setup(a => a.CreateScheduleConfigAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + It.Is(sch => sch.Name == "foo"), + 0, + It.IsAny())).Returns(Task.FromResult(new ScheduleConfigId(Guid.Parse(m_Schedule.Id)))); + + await m_SchedulerClient.Create(m_Schedule!); + + m_MockApi.VerifyAll(); + } + + [Test] + public async Task DeleteMapsToDelete() + { + await m_SchedulerClient.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + m_MockApi.Setup( + api => api.DeleteScheduleConfigAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + Guid.Parse(m_Schedule.Id), + 0, + It.IsAny())) + .Returns(Task.CompletedTask); + + await m_SchedulerClient.Delete(m_Schedule!); + + m_MockApi.VerifyAll(); + } + + [Test] + public async Task GetMapsToGet() + { + await m_SchedulerClient.Initialize(TestValues.ValidEnvironmentId, TestValues.ValidProjectId, CancellationToken.None); + m_MockApi.Setup( + api => api.GetScheduleConfigAsync( + It.Is(g => g.ToString() == TestValues.ValidProjectId), + It.Is(g => g.ToString() == TestValues.ValidEnvironmentId), + It.Is(g => g.ToString() == m_Schedule.Id), + 0, + It.IsAny())) + .Returns(Task.FromResult(new Gateway.SchedulerApiV1.Generated.Model.ScheduleConfig( + Guid.Parse(m_Schedule.Id), + m_Schedule.Name, + m_Schedule.EventName, + m_Schedule.ScheduleType, + m_Schedule.Schedule, + m_Schedule.PayloadVersion, + m_Schedule.Payload))); + + var res = await m_SchedulerClient.Get(m_Schedule.Id); + + Assert.That(m_Schedule.Id, Is.EqualTo(res.Id)); + Assert.That(m_Schedule.Name, Is.EqualTo(res.Name)); + Assert.That(m_Schedule.EventName, Is.EqualTo(res.EventName)); + Assert.That(m_Schedule.ScheduleType, Is.EqualTo(res.ScheduleType)); + Assert.That(m_Schedule.Schedule, Is.EqualTo(res.Schedule)); + Assert.That(m_Schedule.PayloadVersion, Is.EqualTo(res.PayloadVersion)); + Assert.That(m_Schedule.Payload, Is.EqualTo(res.Payload)); + m_MockApi.VerifyAll(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerDeploymentHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerDeploymentHandlerTests.cs new file mode 100644 index 0000000..dfe1293 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerDeploymentHandlerTests.cs @@ -0,0 +1,361 @@ +using Moq; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.Deploy; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Service; + +namespace Unity.Services.Cli.Scheduler.UnitTest.Deploy; + +[TestFixture] +class SchedulerDeploymentHandlerTests +{ + [Test] + public async Task DeployAsync_CorrectResult() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.DeployAsync( + localSchedules + ); + + Assert.Contains(localSchedules.FirstOrDefault(l => l.Id == "foo"), actualRes.Updated); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Id == "foo"), actualRes.Deployed); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Id == "bar"), actualRes.Created); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Id == "bar"), actualRes.Deployed); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Created); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Id == "dup-id"), actualRes.Deployed); + } + + [Test] + public async Task DeployAsync_CreateCallsMade() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + await handler.DeployAsync( + localSchedules + ); + + mockSchedulesClient + .Verify( + c => c.Create( + It.Is(l => l.Id == "bar")), + Times.Once); + mockSchedulesClient + .Verify( + c => c.Create( + It.Is(l => l.Id == "dup-id")), + Times.Once); + } + + [Test] + public async Task DeployAsync_UpdateCallsMade() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + await handler.DeployAsync( + localSchedules + ); + + mockSchedulesClient + .Verify( + c => c.Update( + It.Is(l => l.Id == "foo")), + Times.Once); + } + + [Test] + public async Task DeployAsync_StatusesSet() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.DeployAsync( + localSchedules, + reconcile: true + ); + + mockSchedulesClient + .Verify( + c => c.Update( + It.Is(l => l.Id == "foo")), + Times.Once); + var expectedCreatedSchedule = actualRes.Deployed.FirstOrDefault(l => l.Id == "bar"); + Assert.IsTrue(expectedCreatedSchedule.Status.Message == "Deployed"); + Assert.IsTrue(expectedCreatedSchedule.Status.MessageDetail == "Created"); + + var expectedUpdatedSchedule = actualRes.Deployed.FirstOrDefault(l => l.Id == "foo"); + Assert.IsTrue(expectedUpdatedSchedule.Status.Message == "Deployed"); + Assert.IsTrue(expectedUpdatedSchedule.Status.MessageDetail == "Updated"); + + var expectedDeletedSchedule = actualRes.Deployed.FirstOrDefault(l => l.Id == "echo"); + Assert.IsTrue(expectedDeletedSchedule.Status.Message == "Deployed"); + Assert.IsTrue(expectedDeletedSchedule.Status.MessageDetail == "Deleted"); + } + + [Test] + public async Task DeployAsync_NoReconcileNoDeleteCalls() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + await handler.DeployAsync( + localSchedules + ); + + mockSchedulesClient + .Verify( + c => c.Delete( + It.Is(l => l.Id == "echo")), + Times.Never); + } + + [Test] + public async Task DeployAsync_ReconcileDeleteCalls() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + await handler.DeployAsync( + localSchedules, + reconcile: true + ); + + mockSchedulesClient + .Verify( + c => c.Delete( + It.Is(l => l.Id == "echo")), + Times.Once); + } + + + [Test] + public async Task DeployAsync_DryRunNoCalls() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + await handler.DeployAsync( + localSchedules, + true + ); + + mockSchedulesClient + .Verify( + c => c.Create( + It.IsAny()), + Times.Never); + + mockSchedulesClient + .Verify( + c => c.Update( + It.IsAny()), + Times.Never); + + mockSchedulesClient + .Verify( + c => c.Delete( + It.IsAny()), + Times.Never); + } + + [Test] + public async Task DeployAsync_DryRunCorrectResult() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.DeployAsync( + localSchedules, + dryRun: true + ); + Assert.Multiple(() => + { + Assert.That(actualRes.Updated, Does.Contain(localSchedules.FirstOrDefault(l => l.Id == "foo"))); + Assert.That(actualRes.Created, Does.Contain(localSchedules.FirstOrDefault(l => l.Id == "bar"))); + Assert.That(actualRes.Created, Does.Contain(localSchedules.FirstOrDefault(l => l.Id == "dup-id"))); + Assert.That(actualRes.Deployed.Count, Is.EqualTo(0)); + }); + } + + [Test] + public async Task DeployAsync_DuplicateNames() + { + var localSchedules = GetLocalConfigs(); + localSchedules.Add(new ScheduleConfig("dup-name", + "EventType5", + "recurring", + "0 * * * *", + 1, + "{}") + { + Id = "dup-id", + Path = "otherpath.sched" + }); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.DeployAsync( + localSchedules, + dryRun: true + ); + Assert.Multiple(() => + { + Assert.Contains(localSchedules.FirstOrDefault(l => l.Name == "dup-name"), actualRes.Failed); + Assert.That(actualRes.Failed.Count, Is.EqualTo(2)); + }); + } + + [Test] + public async Task DeployAsync_ExceptionWhenDeployingResource() + { + var localSchedules = GetLocalConfigs(); + + Mock mockSchedulesClient = new(); + var handler = new SchedulerDeploymentHandler(mockSchedulesClient.Object); + + mockSchedulesClient + .Setup(c => c.List()) + .ReturnsAsync(new List()); + mockSchedulesClient + .Setup(c => c.Create(It.IsAny())) + .ThrowsAsync(new Exception()); + + var actualRes = await handler.DeployAsync( + localSchedules + ); + + Assert.That(actualRes.Failed.Count, Is.EqualTo(3)); + Assert.That(actualRes.Failed.First().Status.MessageSeverity, Is.EqualTo(SeverityLevel.Error)); + } + + static List GetLocalConfigs() + { + var schedules = new List() + { + new ScheduleConfig("foo", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + { + Id = "foo", + Path = "path1" + }, + new ScheduleConfig("bar", + "EventType2", + "recurring", + "0 * * * *", + 1, + "{}") + { + Id = "bar", + Path = "path2" + }, + new ScheduleConfig("dup-name", + "EventType4", + "recurring", + "0 * * * *", + 1, + "{}") + { + Id = "dup-id", + Path = "path3" + } + }; + return schedules; + } + + static IReadOnlyList GetRemoteConfigs() + { + var schedules = new List() + { + new ScheduleConfig("foo", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + { + Id = "foo", + Path = "Remote" + }, + new ScheduleConfig("echo", + "EventType3", + "recurring", + "0 * * * *", + 1, + "{}") + { + Id = "echo", + Path = "Remote" + } + }; + return schedules; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerDeploymentServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerDeploymentServiceTests.cs new file mode 100644 index 0000000..cbea9da --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerDeploymentServiceTests.cs @@ -0,0 +1,149 @@ +using Moq; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Scheduler.Deploy; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.Deploy; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Service; + +namespace Unity.Services.Cli.Scheduler.UnitTest.Deploy; + +[TestFixture] +public class SchedulerDeploymentServiceTests +{ + SchedulerDeploymentService? m_DeploymentService; + readonly Mock m_MockScheduleClient = new(); + readonly Mock m_MockScheduleDeploymentHandler = new(); + readonly Mock m_MockScheduleConfigLoader = new(); + + [SetUp] + public void SetUp() + { + m_MockScheduleClient.Reset(); + m_DeploymentService = new SchedulerDeploymentService( + m_MockScheduleDeploymentHandler.Object, + m_MockScheduleClient.Object, + m_MockScheduleConfigLoader.Object); + } + + [Test] + public async Task DeployAsync_MapsResult() + { + var schedule1 = new ScheduleConfig("foo", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}"); + var schedule2 = new ScheduleConfig("bar", + "EventType2", + "recurring", + "0 * * * *", + 1, + "{}"); + + var fileItem = new ScheduleFileItem( + new ScheduleConfigFile(new Dictionary() + { + { "schedule1", schedule1 }, + { "schedule2", schedule2 } + }), + "path"); + m_MockScheduleConfigLoader + .Setup( + m => + m.LoadResource( + It.IsAny(), + It.IsAny()) + ) + .ReturnsAsync(fileItem); + var deployResult = new DeployResult() + { + Created = new List { schedule2 }, + Updated = new List(), + Deleted = new List(), + Deployed = new List { schedule2 }, + Failed = new List() + }; + m_MockScheduleDeploymentHandler.Setup( + d => d.DeployAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Returns(Task.FromResult(deployResult)); + + var input = new DeployInput() + { + CloudProjectId = string.Empty + }; + var res = await m_DeploymentService!.Deploy( + input, + new[] { "path"}, + String.Empty, + string.Empty, + null, + CancellationToken.None); + Assert.Multiple(() => + { + Assert.That(res.Created.Count, Is.EqualTo(1)); + Assert.That(res.Updated.Count, Is.EqualTo(0)); + Assert.That(res.Deleted.Count, Is.EqualTo(0)); + Assert.That(res.Deployed.Count, Is.EqualTo(1)); + Assert.That(res.Failed.Count, Is.EqualTo(0)); + }); + } + + [Test] + public async Task DeployAsync_MapsFailed() + { + m_MockScheduleConfigLoader + .Setup( + m => + m.LoadResource( + It.IsAny(), + It.IsAny()) + ) + .ReturnsAsync(new ScheduleFileItem(new ScheduleConfigFile( + new Dictionary()), + "scheduleFile.sched", + status: new DeploymentStatus("failed", "failed", SeverityLevel.Error))); + var deployResult = new DeployResult() + { + Created = new List(), + Updated = new List(), + Deleted = new List(), + Deployed = new List(), + Failed = new List() + }; + m_MockScheduleDeploymentHandler.Setup( + d => d.DeployAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Returns(Task.FromResult(deployResult)); + + var input = new DeployInput() + { + CloudProjectId = string.Empty + }; + var res = await m_DeploymentService!.Deploy( + input, + new[] { "dir" }, + string.Empty, + string.Empty, + null, + CancellationToken.None); + Assert.Multiple(() => + { + Assert.That(res.Created.Count, Is.EqualTo(0)); + Assert.That(res.Updated.Count, Is.EqualTo(0)); + Assert.That(res.Deleted.Count, Is.EqualTo(0)); + Assert.That(res.Deployed.Count, Is.EqualTo(0)); + Assert.That(res.Failed.Count, Is.EqualTo(1)); + }); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerFetchHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerFetchHandlerTests.cs new file mode 100644 index 0000000..d37a439 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerFetchHandlerTests.cs @@ -0,0 +1,331 @@ +using Moq; +using Unity.Services.Cli.Scheduler.Deploy; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.Fetch; +using Unity.Services.Scheduler.Authoring.Core.IO; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Service; + +namespace Unity.Services.Cli.Scheduler.UnitTest.Deploy; + +[TestFixture] +class SchedulerFetchHandlerTests +{ + [Test] + public async Task FetchAsync_CorrectResult() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulerClient = new(); + Mock mockFileSystem = new(); + var handler = new SchedulerFetchHandler(mockSchedulerClient.Object, mockFileSystem.Object, new SchedulesSerializer()); + + mockSchedulerClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localSchedules + ); + + Assert.Contains(localSchedules.FirstOrDefault(l => l.Name == "schedule1"), actualRes.Updated); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Name == "schedule1"), actualRes.Fetched); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Name == "schedule2"), actualRes.Deleted); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Name == "schedule2"), actualRes.Fetched); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Name == "schedule3"), actualRes.Deleted); + Assert.Contains(localSchedules.FirstOrDefault(l => l.Name == "schedule3"), actualRes.Fetched); + Assert.IsEmpty(actualRes.Created); + } + + [Test] + public async Task FetchAsync_WriteCallsMade() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulerClient = new(); + Mock mockFileSystem = new(); + var handler = new SchedulerFetchHandler(mockSchedulerClient.Object, mockFileSystem.Object, new SchedulesSerializer()); + + mockSchedulerClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localSchedules + ); + + mockFileSystem + .Verify(f => f.WriteAllText( + "path1", + It.Is(s => s.Contains("1 * * * *")), + 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 localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulerClient = new(); + Mock mockFileSystem = new(); + var handler = new SchedulerFetchHandler(mockSchedulerClient.Object, mockFileSystem.Object, new SchedulesSerializer()); + + mockSchedulerClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localSchedules + ); + + 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 localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulerClient = new(); + Mock mockFileSystem = new(); + var handler = new SchedulerFetchHandler(mockSchedulerClient.Object, mockFileSystem.Object, new SchedulesSerializer()); + + mockSchedulerClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localSchedules, + reconcile: true + ); + + mockFileSystem + .Verify(f => f.WriteAllText( + Path.Combine("dir", "schedule4.sched"), + It.IsAny(), + It.IsAny()), + Times.Once); + + Assert.That(actualRes.Created.Count, Is.EqualTo(1)); + Assert.That(actualRes.Created.First().Name, Is.EqualTo("schedule4")); + } + + + [Test] + public async Task FetchAsync_StatusesAreCorrect() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulerClient = new(); + Mock mockFileSystem = new(); + var handler = new SchedulerFetchHandler(mockSchedulerClient.Object, mockFileSystem.Object, new SchedulesSerializer()); + + mockSchedulerClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localSchedules, + reconcile: true + ); + + var expectedCreatedSchedule = actualRes.Fetched.FirstOrDefault(l => l.Name == "schedule4"); + Assert.IsTrue(expectedCreatedSchedule.Status.Message == "Fetched"); + Assert.IsTrue(expectedCreatedSchedule.Status.MessageDetail == "Created"); + + var expectedUpdatedSchedule = actualRes.Fetched.FirstOrDefault(l => l.Name == "schedule1"); + Assert.IsTrue(expectedUpdatedSchedule.Status.Message == "Fetched"); + Assert.IsTrue(expectedUpdatedSchedule.Status.MessageDetail == "Updated"); + + var expectedDeletedSchedule = actualRes.Fetched.FirstOrDefault(l => l.Name == "schedule2"); + Assert.IsTrue(expectedDeletedSchedule.Status.Message == "Fetched"); + Assert.IsTrue(expectedDeletedSchedule.Status.MessageDetail == "Deleted"); + } + + + + [Test] + public async Task FetchAsync_DryRunNoCalls() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulerClient = new(); + Mock mockFileSystem = new(); + var handler = new SchedulerFetchHandler(mockSchedulerClient.Object, mockFileSystem.Object, new SchedulesSerializer()); + + mockSchedulerClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localSchedules, + 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_DuplicateNames() + { + var localSchedules = GetLocalConfigs(); + localSchedules.Add(new ScheduleConfig("schedule1", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") { Path = "otherpath"}); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulerClient = new(); + Mock mockFileSystem = new(); + var handler = new SchedulerFetchHandler(mockSchedulerClient.Object, mockFileSystem.Object, new SchedulesSerializer()); + + mockSchedulerClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + + var actualRes = await handler.FetchAsync( + "dir", + localSchedules, + dryRun: true + ); + + mockFileSystem + .Verify(f => f.Delete( + It.Is(s => s == "path3"), + It.IsAny()), + Times.Never); + Assert.Multiple(() => + { + Assert.That(actualRes.Failed[0].ToString(), Is.EqualTo("'schedule1' in 'path1'")); + Assert.That(actualRes.Failed[1].ToString(), Is.EqualTo("'schedule1' in 'otherpath'")); + }); + } + + [Test] + public async Task FetchAsync_ExceptionWhenFetchingResource() + { + var localSchedules = GetLocalConfigs(); + var remoteSchedules = GetRemoteConfigs(); + + Mock mockSchedulerClient = new(); + Mock mockFileSystem = new(); + var handler = new SchedulerFetchHandler(mockSchedulerClient.Object, mockFileSystem.Object, new SchedulesSerializer()); + + mockSchedulerClient + .Setup(c => c.List()) + .ReturnsAsync(remoteSchedules.ToList()); + mockFileSystem.Setup(c => c.WriteAllText(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception()); + + var actualRes = await handler.FetchAsync( + "dir", + localSchedules + ); + + Assert.That(actualRes.Failed.Count, Is.EqualTo(1)); + Assert.That(actualRes.Failed.First().Status.MessageSeverity, Is.EqualTo(SeverityLevel.Error)); + } + + static List GetLocalConfigs() + { + var schedules = new List() + { + new ScheduleConfig("schedule1", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + { + Path = "path1" + }, + new ScheduleConfig("schedule2", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + { + Path = "path2" + }, + new ScheduleConfig("schedule3", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + { + Path = "path3" + } + }; + return schedules; + } + + static List GetRemoteConfigs() + { + var schedules = new List() + { + new ScheduleConfig("schedule1", + "EventType1", + "recurring", + "1 * * * *", + 1, + "{}") + { + Path = "Remote" + }, + new ScheduleConfig("schedule4", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + { + Path = "Remote" + } + }; + return schedules; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerFetchServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerFetchServiceTests.cs new file mode 100644 index 0000000..b37e2a7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Deploy/SchedulerFetchServiceTests.cs @@ -0,0 +1,161 @@ +using Moq; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Scheduler.Deploy; +using Unity.Services.Cli.Scheduler.Fetch; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.Fetch; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Service; + +namespace Unity.Services.Cli.Scheduler.UnitTest.Deploy; + +[TestFixture] +public class SchedulerFetchServiceTests +{ + SchedulerFetchService? m_FetchService; + readonly Mock m_MockScheduleClient = new(); + readonly Mock m_MockScheduleFetchHandler = new(); + readonly Mock m_MockScheduleConfigLoader = new(); + + [SetUp] + public void SetUp() + { + m_MockScheduleClient.Reset(); + m_FetchService = new SchedulerFetchService( + m_MockScheduleFetchHandler.Object, + m_MockScheduleClient.Object, + m_MockScheduleConfigLoader.Object); + } + + [Test] + public async Task FetchAsync_MapsResult() + { + var schedule1 = new ScheduleConfig( + "schedule1", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + { + Id = "schedule1", + Path = "scheduleFile.sched", + }; + var schedule2 = new ScheduleConfig( + "schedule2", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + { + Id = "schedule2", + Path = "scheduleFile.sched", + }; + m_MockScheduleConfigLoader + .Setup( + m => + m.LoadResource( + It.IsAny(), + It.IsAny()) + ) + .ReturnsAsync(new ScheduleFileItem(new ScheduleConfigFile( + new Dictionary() + { + { "schedule1", schedule1 }, + { "schedule2", schedule2 } + }), "scheduleFile.sched")); + var deployResult = new FetchResult() + { + Created = new List { schedule2 }, + Updated = new List(), + Deleted = new List(), + Fetched = new List { schedule2 }, + Failed = new List() + }; + m_MockScheduleFetchHandler.Setup( + d => d.FetchAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Returns(Task.FromResult(deployResult)); + + var input = new FetchInput() + { + Path = "dir", + CloudProjectId = string.Empty + }; + var res = await m_FetchService!.FetchAsync( + input, + new[] { "dir" }, + string.Empty, + string.Empty, + null, + CancellationToken.None); + Assert.Multiple(() => + { + Assert.That(res.Created.Count, Is.EqualTo(1)); + Assert.That(res.Updated.Count, Is.EqualTo(0)); + Assert.That(res.Deleted.Count, Is.EqualTo(0)); + Assert.That(res.Fetched.Count, Is.EqualTo(2)); + Assert.That(res.Failed.Count, Is.EqualTo(0)); + }); + } + + [Test] + public async Task FetchAsync_MapsFailed() + { + m_MockScheduleConfigLoader + .Setup( + m => + m.LoadResource( + It.IsAny(), + It.IsAny()) + ) + .ReturnsAsync(new ScheduleFileItem(new ScheduleConfigFile( + new Dictionary()), + "scheduleFile.sched", + status: new DeploymentStatus("failed", "failed", SeverityLevel.Error))); + var deployResult = new FetchResult() + { + Created = new List(), + Updated = new List(), + Deleted = new List(), + Fetched = new List(), + Failed = new List() + }; + m_MockScheduleFetchHandler.Setup( + d => d.FetchAsync( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Returns(Task.FromResult(deployResult)); + + var input = new FetchInput() + { + Path = "dir", + CloudProjectId = string.Empty + }; + var res = await m_FetchService!.FetchAsync( + input, + new[] { "dir" }, + string.Empty, + string.Empty, + null, + CancellationToken.None); + Assert.Multiple(() => + { + Assert.That(res.Created.Count, Is.EqualTo(0)); + Assert.That(res.Updated.Count, Is.EqualTo(0)); + Assert.That(res.Deleted.Count, Is.EqualTo(0)); + Assert.That(res.Fetched.Count, Is.EqualTo(0)); + Assert.That(res.Failed.Count, Is.EqualTo(1)); + }); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Unity.Services.Cli.Scheduler.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Unity.Services.Cli.Scheduler.UnitTest.csproj new file mode 100644 index 0000000..0145693 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Unity.Services.Cli.Scheduler.UnitTest.csproj @@ -0,0 +1,24 @@ + + + net6.0 + enable + enable + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Usings.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Usings.cs new file mode 100644 index 0000000..3244567 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Utils/TestValues.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Utils/TestValues.cs new file mode 100644 index 0000000..e694021 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler.UnitTest/Utils/TestValues.cs @@ -0,0 +1,9 @@ +namespace Unity.Services.Cli.Scheduler.UnitTest.Utils; + +public class TestValues +{ + public const string ValidProjectId = "a912b1fd-541d-42e1-89f2-85436f27aabd"; + public const string ValidEnvironmentId = "00000000-0000-0000-0000-000000000000"; + public const string Cursor = "schedule_id_1"; + public const int Limit = 10; +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/IScheduleResourceLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/IScheduleResourceLoader.cs new file mode 100644 index 0000000..98930bf --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/IScheduleResourceLoader.cs @@ -0,0 +1,6 @@ +namespace Unity.Services.Cli.Scheduler.Deploy; + +interface IScheduleResourceLoader +{ + Task LoadResource(string filePath, CancellationToken cancellationToken); +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleConfigFile.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleConfigFile.cs new file mode 100644 index 0000000..5cffee0 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleConfigFile.cs @@ -0,0 +1,64 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Unity.Services.Cli.Authoring.Templates; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Cli.Scheduler.Deploy; + +public class ScheduleConfigFile : IFileTemplate +{ + [JsonProperty("$schema")] + public string Value { get; } = "https://ugs-config-schemas.unity3d.com/v1/schedules.schema.json"; + + public IDictionary Configs { get; set; } + + [JsonIgnore] + public string Extension => SchedulerConstants.DeployFileExtension; + + [JsonIgnore] + public string FileBodyText => JsonConvert.SerializeObject(this, GetSerializationSettings()); + + public ScheduleConfigFile() + { + Configs = new Dictionary() + { + { + "Schedule1", + new ("Schedule1", + "EventType1", + "recurring", + "0 * * * *", + 1, + "{}") + + }, + { + "Schedule2", + new ("Schedule2", + "EventType2", + "one-time", + DateTime.Now.AddHours(1).ToString("yyyy-MM-dd'T'HH:mm:ss.fffK"), + 1, + "{ \"message\": \"Hello, world!\"}") + }, + }; + } + + [JsonConstructor] + public ScheduleConfigFile(IDictionary configs) + { + Configs = configs; + } + + public static JsonSerializerSettings GetSerializationSettings() + { + var settings = new JsonSerializerSettings() + { + Converters = { new StringEnumConverter() }, + Formatting = Formatting.Indented, + DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, + + }; + return settings; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleDeploymentResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleDeploymentResult.cs new file mode 100644 index 0000000..bfc3faf --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleDeploymentResult.cs @@ -0,0 +1,81 @@ +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Model.TableOutput; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.Scheduler.Deploy; + +class ScheduleDeploymentResult : DeploymentResult +{ + public ScheduleDeploymentResult( + IReadOnlyList updated, + IReadOnlyList deleted, + IReadOnlyList created, + IReadOnlyList authored, + IReadOnlyList failed, + bool dryRun = false) + : base( + updated, + deleted, + created, + authored, + failed, + dryRun) { } + + public override TableContent ToTable(string service = "") + { + return AccessControlResToTable(this); + } + + public static TableContent AccessControlResToTable(AuthorResult res) + { + var table = new TableContent + { + IsDryRun = res.DryRun + }; + + foreach (var deploymentItem in res.Authored) + { + var file = (ScheduleFileItem)deploymentItem; + table.AddRow(RowContent.ToRow(file)); + foreach (var config in file.Content.Configs) + table.AddRow(RowContent.ToRow(config.Value)); + } + + foreach (var deleted in res.Deleted) + { + table.AddRow(RowContent.ToRow(deleted)); + } + + foreach (var deploymentItem in res.Failed) + { + var file = (ScheduleFileItem)deploymentItem; + table.AddRow(RowContent.ToRow(file)); + foreach (var config in file.Content.Configs) + table.AddRow(RowContent.ToRow(config.Value)); + } + + return table; + } +} + +public class SchedulesFetchResult : FetchResult +{ + public SchedulesFetchResult( + IReadOnlyList updated, + IReadOnlyList deleted, + IReadOnlyList created, + IReadOnlyList authored, + IReadOnlyList failed, + bool dryRun = false) : base( + updated, + deleted, + created, + authored, + failed, + dryRun) { } + + public override TableContent ToTable(string service = "") + { + return ScheduleDeploymentResult.AccessControlResToTable(this); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleFileItem.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleFileItem.cs new file mode 100644 index 0000000..33ed6fd --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleFileItem.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.Scheduler.Deploy; + +[DataContract] +public class ScheduleFileItem : DeployContent +{ + public ScheduleConfigFile Content { get; } + const string k_TriggersConfigFileType = "Schedule Config File"; + + public ScheduleFileItem(ScheduleConfigFile content, string path, float progress = 0, DeploymentStatus? status = null) + : base( + System.IO.Path.GetFileName(path), + k_TriggersConfigFileType, + path, + progress, + status) + { + Content = content; + Path = path; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleResourceLoader.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleResourceLoader.cs new file mode 100644 index 0000000..4b4e0e4 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleResourceLoader.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.IO; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Cli.Scheduler.Deploy; + +public class ScheduleResourceLoader : IScheduleResourceLoader +{ + readonly IFileSystem m_FileSystem; + + public ScheduleResourceLoader(IFileSystem fileSystem) + { + m_FileSystem = fileSystem; + } + + public async Task LoadResource(string filePath, CancellationToken cancellationToken) + { + try + { + var content = await m_FileSystem.ReadAllText(filePath, cancellationToken); + var triggersConfigFile = JsonConvert.DeserializeObject( + content, + ScheduleConfigFile.GetSerializationSettings())!; + + foreach (var config in triggersConfigFile.Configs) + { + config.Value.Path = filePath; + config.Value.Name = config.Key; + } + return new ScheduleFileItem(triggersConfigFile, filePath); + } + catch (Exception ex) + { + var res = new ScheduleFileItem(new ScheduleConfigFile(new Dictionary()), filePath) + { + Status = new DeploymentStatus( + "Failed to Load", + $"Error reading file: {ex.Message}", + SeverityLevel.Error) + }; + return res; + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleSerializer.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleSerializer.cs new file mode 100644 index 0000000..d4c5ccf --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/ScheduleSerializer.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Serialization; + +namespace Unity.Services.Cli.Scheduler.Deploy; + +public class SchedulesSerializer : ISchedulesSerializer +{ + public string Serialize(IList config) + { + var file = new ScheduleConfigFile() + { + Configs = config.Cast().ToDictionary(k => k.Name, v => v) + }; + return JsonConvert.SerializeObject(file, Formatting.Indented); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerClient.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerClient.cs new file mode 100644 index 0000000..79d2e09 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerClient.cs @@ -0,0 +1,156 @@ +using Unity.Services.Cli.Common.Models; +using Unity.Services.Cli.Common.Validator; +using Unity.Services.Cli.ServiceAccountAuthentication; +using Unity.Services.Cli.ServiceAccountAuthentication.Token; +using Unity.Services.Gateway.SchedulerApiV1.Generated.Api; +using Unity.Services.Gateway.SchedulerApiV1.Generated.Client; +using Unity.Services.Gateway.SchedulerApiV1.Generated.Model; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Service; +using ScheduleConfig = Unity.Services.Scheduler.Authoring.Core.Model.ScheduleConfig; + +namespace Unity.Services.Cli.Scheduler.Deploy; + +class SchedulerClient : ISchedulerClient +{ + readonly ISchedulerApiAsync m_SchedulerApi; + readonly IServiceAccountAuthenticationService m_AuthenticationService; + readonly IConfigurationValidator m_Validator; + internal Guid ProjectId { get; set; } + internal Guid EnvironmentId { get; set; } + internal CancellationToken CancellationToken { get; set; } + + public SchedulerClient(ISchedulerApiAsync schedulerApi, + IServiceAccountAuthenticationService authenticationService, + IConfigurationValidator validator) + { + m_SchedulerApi = schedulerApi; + m_AuthenticationService = authenticationService; + m_Validator = validator; + } + + public async Task Initialize( + string environmentId, + string projectId, + CancellationToken cancellationToken) + { + ProjectId = new Guid(projectId); + EnvironmentId = new Guid(environmentId); + CancellationToken = cancellationToken; + + await AuthorizeServiceAsync(cancellationToken); + ValidateProjectIdAndEnvironmentId(projectId, environmentId); + } + + public async Task AuthorizeServiceAsync(CancellationToken cancellationToken = default) + { + var token = await m_AuthenticationService.GetAccessTokenAsync(cancellationToken); + m_SchedulerApi.Configuration.DefaultHeaders.SetAccessTokenHeader(token); + } + + public void ValidateProjectIdAndEnvironmentId(string projectId, string environmentId) + { + m_Validator.ThrowExceptionIfConfigInvalid(Keys.ConfigKeys.ProjectId, projectId); + m_Validator.ThrowExceptionIfConfigInvalid(Keys.ConfigKeys.EnvironmentId, environmentId); + } + + public async Task Get(string id) + { + var schedule = await m_SchedulerApi.GetScheduleConfigAsync( + ProjectId, + EnvironmentId, + new Guid(id), + 0, + CancellationToken); + return FromResponse(schedule); + } + + public async Task Update(IScheduleConfig resource) + { + await m_SchedulerApi.DeleteScheduleConfigAsync( + ProjectId, + EnvironmentId, + new Guid(resource.Id), + 0, + CancellationToken); + await m_SchedulerApi.CreateScheduleConfigAsync( + ProjectId, + EnvironmentId, + new ScheduleConfigBody( + resource.Name, + resource.EventName, + resource.ScheduleType, + resource.Schedule, + resource.PayloadVersion, + resource.Payload), + 0, + CancellationToken); + } + + public async Task Create(IScheduleConfig resource) + { + await m_SchedulerApi.CreateScheduleConfigAsync( + ProjectId, + EnvironmentId, + new ScheduleConfigBody( + resource.Name, + resource.EventName, + resource.ScheduleType, + resource.Schedule, + resource.PayloadVersion, + resource.Payload), + 0, + CancellationToken); + } + + public async Task Delete(IScheduleConfig resource) + { + await m_SchedulerApi.DeleteScheduleConfigAsync( + ProjectId, + EnvironmentId, + new Guid(resource.Id), + 0, + CancellationToken); + } + + public async Task> List() + { + const int limit = 50; + var schedules = new List(); + string? cursor = null; + List newBatch; + do + { + var results = await m_SchedulerApi.ListSchedulerConfigsAsync( + ProjectId, + EnvironmentId, + limit, + cursor, + 0, + CancellationToken); + newBatch = results.Configs; + cursor = newBatch.LastOrDefault()?.Id.ToString(); + schedules.AddRange(newBatch); + + if (CancellationToken.IsCancellationRequested) + break; + } while (newBatch.Count >= limit); + + return schedules.Select(FromResponse).ToList(); + } + + static ScheduleConfig FromResponse(Gateway.SchedulerApiV1.Generated.Model.ScheduleConfig response) + { + return new ScheduleConfig( + response.Name, + response.EventName, + response.Type, + response.Schedule, + response.PayloadVersion, + response.Payload) + { + Id = response.Id.ToString(), + Path = "Remote" + }; + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerDeployFetchBase.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerDeployFetchBase.cs new file mode 100644 index 0000000..742dd81 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerDeployFetchBase.cs @@ -0,0 +1,62 @@ +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Cli.Scheduler.Deploy; + +abstract class SchedulerDeployFetchBase +{ + readonly IScheduleResourceLoader m_ResourceLoader; + + public string ServiceType => SchedulerConstants.ServiceType; + public string ServiceName => SchedulerConstants.ServiceName; + + public IReadOnlyList FileExtensions => new[] + { + SchedulerConstants.DeployFileExtension + }; + + protected SchedulerDeployFetchBase(IScheduleResourceLoader resourceLoader) + { + m_ResourceLoader = resourceLoader; + } + + protected static void SetFileStatus(IReadOnlyList deserializedFiles) + { + foreach (var deployedFile in deserializedFiles) + { + var failedItems = deployedFile.Content.Configs.Count(c => c.Value.Status.MessageSeverity == SeverityLevel.Error); + if (failedItems == deployedFile.Content.Configs.Count) + { + deployedFile.SetStatusSeverity(SeverityLevel.Error); + deployedFile.SetStatusDescription("Failed to deploy"); + deployedFile.SetStatusDetail("All items failed to deploy"); + } + else if (failedItems > 0) + { + deployedFile.SetStatusSeverity(SeverityLevel.Warning); + deployedFile.SetStatusDescription("Partial deployment"); + deployedFile.SetStatusDetail("Some items failed to deploy"); + } + else + { + deployedFile.SetStatusSeverity(SeverityLevel.Success); + deployedFile.SetStatusDescription("Deployed"); + deployedFile.SetStatusDetail("All items successfully deployed"); + } + } + } + + protected async Task<(IReadOnlyList,IReadOnlyList)> GetResourcesFromFiles( + IReadOnlyCollection filePaths, + CancellationToken token) + { + var resources = await Task.WhenAll( + filePaths.Select(f => m_ResourceLoader.LoadResource(f,token))); + var deserializedFiles = resources + .Where(r => r.Status.MessageSeverity != SeverityLevel.Error) + .ToList(); + var failedToDeserialize = resources + .Where(r => r.Status.MessageSeverity == SeverityLevel.Error) + .ToList(); + return (deserializedFiles, failedToDeserialize); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerDeploymentService.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerDeploymentService.cs new file mode 100644 index 0000000..194b5aa --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Deploy/SchedulerDeploymentService.cs @@ -0,0 +1,57 @@ +using Spectre.Console; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Authoring.Model; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.Deploy; +using Unity.Services.Scheduler.Authoring.Core.Service; + +namespace Unity.Services.Cli.Scheduler.Deploy; + +class SchedulerDeploymentService : SchedulerDeployFetchBase, IDeploymentService +{ + readonly IScheduleDeploymentHandler m_DeploymentHandler; + readonly ISchedulerClient m_Client; + + public SchedulerDeploymentService( + IScheduleDeploymentHandler deploymentHandler, + ISchedulerClient client, + IScheduleResourceLoader loader) : base(loader) + { + m_DeploymentHandler = deploymentHandler; + m_Client = client; + } + + public async Task Deploy( + DeployInput deployInput, + IReadOnlyList filePaths, + string projectId, + string environmentId, + StatusContext? loadingContext, + CancellationToken cancellationToken) + { + await m_Client.Initialize(environmentId, projectId, cancellationToken); + loadingContext?.Status($"Reading {ServiceType} files..."); + var (deserializedFiles, failedToDeserialize) = await GetResourcesFromFiles(filePaths, cancellationToken); + + loadingContext?.Status($"Deploying {ServiceType} files..."); + var res = await m_DeploymentHandler.DeployAsync( + deserializedFiles.ToList().SelectMany(f => f.Content.Configs.Values).ToList(), + dryRun: deployInput.DryRun, + reconcile: deployInput.Reconcile, token: cancellationToken); + + SetFileStatus(deserializedFiles); + + var failedToDeploy = deserializedFiles + .Where(f => f.Status.MessageSeverity == SeverityLevel.Error) + .ToList(); + + return new ScheduleDeploymentResult( + res.Updated, + res.Deleted, + res.Created, + deserializedFiles.Where(f => f.Status.MessageSeverity != SeverityLevel.Error).ToList(), + failedToDeserialize.Cast().Concat(failedToDeploy).ToList(), + deployInput.DryRun); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Exceptions/SchedulerException.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Exceptions/SchedulerException.cs new file mode 100644 index 0000000..977e499 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Exceptions/SchedulerException.cs @@ -0,0 +1,28 @@ +using System.Runtime.Serialization; +using Unity.Services.Cli.Common.Exceptions; + +namespace Unity.Services.Cli.Scheduler.Exceptions; + +/// +/// Example of custom exception for incorrect user operation. +/// +[Serializable] +public class SchedulerException : CliException +{ + protected SchedulerException(SerializationInfo info, StreamingContext context) : base(info, context) { } + + public SchedulerException(int exitCode = Common.Exceptions.ExitCode.HandledError) + : base(exitCode) { } + + /// + /// constructor. + /// + /// A message with instructions to guide user how to fix the operation. + /// Exit code when this exception triggered. Default value is HandledError. + public SchedulerException(string message, int exitCode = Common.Exceptions.ExitCode.HandledError) + : base(message, exitCode) { } + + public SchedulerException( + string message, Exception innerException, int exitCode = Common.Exceptions.ExitCode.HandledError) + : base(message, innerException, exitCode) { } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Fetch/SchedulerFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Fetch/SchedulerFetchService.cs new file mode 100644 index 0000000..2e18fcc --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Fetch/SchedulerFetchService.cs @@ -0,0 +1,72 @@ +using Spectre.Console; +using Unity.Services.Cli.Authoring.Input; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Scheduler.Deploy; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.Fetch; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Service; +using FetchResult = Unity.Services.Cli.Authoring.Model.FetchResult; +using Statuses = Unity.Services.Scheduler.Authoring.Core.Model.Statuses; + +namespace Unity.Services.Cli.Scheduler.Fetch; + +class SchedulerFetchService : SchedulerDeployFetchBase, IFetchService +{ + readonly IScheduleFetchHandler m_FetchHandler; + readonly ISchedulerClient m_Client; + + public SchedulerFetchService( + IScheduleFetchHandler fetchHandler, + ISchedulerClient client, + IScheduleResourceLoader loader) : base(loader) + { + m_FetchHandler = fetchHandler; + m_Client = client; + } + + public async Task FetchAsync( + FetchInput input, + IReadOnlyList filePaths, + string projectId, + string environmentId, + StatusContext? loadingContext, + CancellationToken cancellationToken) + { + await m_Client.Initialize(environmentId, projectId, cancellationToken); + loadingContext?.Status($"Reading {ServiceType} files..."); + var (deserializedFiles, failedToDeserialize) = await GetResourcesFromFiles(filePaths, cancellationToken); + + loadingContext?.Status($"Fetching {ServiceType} files..."); + var res = await m_FetchHandler.FetchAsync( + input.Path, + deserializedFiles.SelectMany(f => f.Content.Configs.Values).ToList(), + input.DryRun, + input.Reconcile, + cancellationToken); + + SetFileStatus(deserializedFiles); + + var failedToDeploy = deserializedFiles + .Where(f => f.Status.MessageSeverity == SeverityLevel.Error) + .ToList(); + + var createdFiles = res.Created.Select( + t => new ScheduleFileItem( + new ScheduleConfigFile( + new Dictionary() + { + { t.Name, (ScheduleConfig)t } + }), + t.Path, 100, Statuses.GetDeployed("Created"))); + + return new SchedulesFetchResult( + res.Updated, + res.Deleted, + res.Created, + deserializedFiles.Where(f => f.Status.MessageSeverity != SeverityLevel.Error) + .Concat(createdFiles).ToList(), + failedToDeserialize.Cast().Concat(failedToDeploy).ToList(), + input.DryRun); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Handlers/SchedulerListHandler.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Handlers/SchedulerListHandler.cs new file mode 100644 index 0000000..e7a7dce --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Handlers/SchedulerListHandler.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Logging; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Input; +using Unity.Services.Cli.Common.Logging; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Service; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Unity.Services.Cli.Scheduler.Handlers; + +static class SchedulerListHandler +{ + public static async Task SchedulerListHandlerHandlerAsync( + CommonInput input, + IUnityEnvironment unityEnvironment, + ISchedulerClient schedulerAdminClient, + ILogger logger, + ILoadingIndicator loadingIndicator, + CancellationToken cancellationToken) + { + await loadingIndicator.StartLoadingAsync( + "Fetching scheduler resource list...", + _ => SchedulerListAsync(input, unityEnvironment, schedulerAdminClient, logger, cancellationToken)); + } + + static async Task SchedulerListAsync( + CommonInput input, + IUnityEnvironment unityEnvironment, + ISchedulerClient schedulerAdminClient, + ILogger logger, + CancellationToken cancellationToken) + { + var environmentId = await unityEnvironment.FetchIdentifierAsync(cancellationToken); + var projectId = input.CloudProjectId!; + await schedulerAdminClient.Initialize(environmentId, projectId, cancellationToken); + var listResult = await schedulerAdminClient.List(); + + var cliFriendlyList = listResult.Select(i => new ScheduleItem(i)); + logger.LogResultValue(cliFriendlyList); + } + + class ScheduleItem + { + readonly IScheduleConfig m_ServerModel; + public string Name { get; } + public string EventName { get; } + public string ScheduleType { get; } + public string Schedule { get; } + public int PayloadVersion { get; } + public string Payload { get; } + + public ScheduleItem(IScheduleConfig serverModel) + { + m_ServerModel = serverModel; + Name = serverModel.Name; + Schedule = serverModel.Schedule; + EventName = serverModel.EventName; + ScheduleType = serverModel.ScheduleType; + PayloadVersion = serverModel.PayloadVersion; + Payload = serverModel.Payload; + } + + public override string ToString() + { + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .DisableAliases() + .Build(); + return serializer.Serialize(m_ServerModel); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/IO/FileSystem.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/IO/FileSystem.cs new file mode 100644 index 0000000..08755f0 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/IO/FileSystem.cs @@ -0,0 +1,5 @@ +using Unity.Services.Scheduler.Authoring.Core.IO; + +namespace Unity.Services.Cli.Scheduler.IO; + +class FileSystem : Common.IO.FileSystem, IFileSystem { } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/SchedulerConstants.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/SchedulerConstants.cs new file mode 100644 index 0000000..aef0053 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/SchedulerConstants.cs @@ -0,0 +1,8 @@ +namespace Unity.Services.Cli.Scheduler; + +public static class SchedulerConstants +{ + public static string DeployFileExtension => ".sched"; + public static string ServiceType => "Scheduler"; + public static string ServiceName => "scheduler"; +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/SchedulerModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/SchedulerModule.cs new file mode 100644 index 0000000..8fd444f --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/SchedulerModule.cs @@ -0,0 +1,118 @@ +using System.CommandLine; +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Polly; +using RestSharp; +using Unity.Services.Cli.Authoring.Handlers; +using Unity.Services.Cli.Common; +using Unity.Services.Cli.Common.Input; +using Unity.Services.Cli.Authoring.Service; +using Unity.Services.Cli.Common.Console; +using Unity.Services.Cli.Common.Networking; +using Unity.Services.Cli.Common.Utils; +using Unity.Services.Cli.Scheduler.Deploy; +using Unity.Services.Cli.Scheduler.Fetch; +using Unity.Services.Cli.Scheduler.Handlers; +using Unity.Services.Gateway.SchedulerApiV1.Generated.Api; +using Unity.Services.Scheduler.Authoring.Core.Deploy; +using Unity.Services.Scheduler.Authoring.Core.Fetch; +using Unity.Services.Scheduler.Authoring.Core.Serialization; +using Unity.Services.Scheduler.Authoring.Core.Service; +using FileSystem = Unity.Services.Cli.Scheduler.IO.FileSystem; +using IFileSystem = Unity.Services.Scheduler.Authoring.Core.IO.IFileSystem; + +namespace Unity.Services.Cli.Scheduler; + +/// +/// A Template module to achieve a get request command: ugs scheduler get `address` -o `file` +/// +public class SchedulerModule : ICommandModule +{ + class SchedulerInput : CommonInput + { + public static readonly Argument AddressArgument = new( + "address", + "The address to send GET request"); + + [InputBinding(nameof(AddressArgument))] + public string? Address { get; set; } + + public static readonly Option OutputFileOption = new(new[] + { + "-o", + "--output" + }, "Write output to file instead of stdout"); + + [InputBinding(nameof(OutputFileOption))] + public string? OutputFile { get; set; } + } + + public Command? ModuleRootCommand { get; } + + public SchedulerModule() + { + var schedulerListCommand = new Command("list", "List online schedules.") + { + CommonInput.CloudProjectIdOption, + CommonInput.EnvironmentNameOption, + }; + schedulerListCommand.SetHandler< + CommonInput, + IUnityEnvironment, + ISchedulerClient, + ILogger, + ILoadingIndicator, + CancellationToken>( + SchedulerListHandler.SchedulerListHandlerHandlerAsync); + + ModuleRootCommand = new("scheduler", "Scheduler module root command.") + { + ModuleRootCommand.AddNewFileCommand("Schedule"), + schedulerListCommand + }; + ModuleRootCommand.AddAlias("sched"); + } + + /// + /// Register service to UGS CLI host builder + /// + public static void RegisterServices(IServiceCollection serviceCollection) + { + var config = new Gateway.SchedulerApiV1.Generated.Client.Configuration + { + BasePath = EndpointHelper.GetCurrentEndpointFor() + }; + config.DefaultHeaders.SetXClientIdHeader(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(_ => new SchedulerApi(config)); + + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddSingleton(); + + var retryAfterPolicy = Policy + .HandleResult(r => r.StatusCode == HttpStatusCode.TooManyRequests && r.Headers != null) + .WaitAndRetryAsync( + retryCount: 3, + sleepDurationProvider: RetryAfterSleepDuration, + onRetryAsync: (_, _, _, _) => Task.CompletedTask); + Gateway.SchedulerApiV1.Generated.Client.RetryConfiguration.AsyncRetryPolicy = retryAfterPolicy; + } + + static TimeSpan RetryAfterSleepDuration(int retryCount, DelegateResult result, Context ctx) + { + const string retryAfter = "Retry-After"; + var header = result.Result.Headers!.First(x => x.Name!.Equals(retryAfter)); + var retryValue = header.Value?.ToString(); + var retryValueInt = int.Parse(retryValue!); + var time = 2 * retryValueInt; + return TimeSpan.FromSeconds(time); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Unity.Services.Cli.Scheduler.csproj b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Unity.Services.Cli.Scheduler.csproj new file mode 100644 index 0000000..2839dd2 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Cli.Scheduler/Unity.Services.Cli.Scheduler.csproj @@ -0,0 +1,31 @@ + + + net6.0 + 10 + enable + enable + true + + + + <_Parameter1>$(AssemblyName).UnitTest + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + + + + + + + + + + + + $(DefineConstants);$(ExtraDefineConstants) + + \ No newline at end of file diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Deploy/TriggerFetchHandlerTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Deploy/TriggerFetchHandlerTests.cs index 8b79cfa..480e1e0 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Deploy/TriggerFetchHandlerTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Deploy/TriggerFetchHandlerTests.cs @@ -1,4 +1,5 @@ using Moq; +using Newtonsoft.Json; using NUnit.Framework; using Unity.Services.Cli.Triggers.Deploy; using Unity.Services.Triggers.Authoring.Core.Deploy; @@ -10,7 +11,6 @@ namespace Unity.Services.Cli.Triggers.UnitTest.Deploy; [TestFixture] -[Ignore("Fetch not in scope")] class TriggerFetchHandlerTests { [Test] @@ -119,20 +119,23 @@ public async Task FetchAsync_WriteNewOnReconcile() reconcile: true ); - var expectedJson = "{\"Configs\":[{\"Id\":\"id1\",\"Name\":\"name1\",\"EventType\":\"eventType\",\"ActionType\":\"cloud-code\",\"ActionUrn\":\"actionUrn\"}]}"; + var expectedFile1 = new TriggersConfigFile( + new List(){(TriggerConfig)remoteTriggers[0]}); mockFileSystem .Verify(f => f.WriteAllText( "path1", - expectedJson, + JsonConvert.SerializeObject(expectedFile1, Formatting.Indented), It.IsAny()), Times.Once); - var expectedJson2 = "{\"Configs\":[{\"Id\":\"id2\",\"Name\":\"name2\",\"EventType\":\"eventType\",\"ActionType\":\"cloud-code\",\"ActionUrn\":\"actionUrn\"}]}"; + var expectedFile2 = new TriggersConfigFile( + new List(){(TriggerConfig)remoteTriggers[1]}); mockFileSystem .Verify(f => f.WriteAllText( Path.Combine("dir", "name2.tr"), - expectedJson2, + JsonConvert.SerializeObject(expectedFile2, Formatting.Indented), It.IsAny()), Times.Once); + mockFileSystem.Verify(f => f.Delete("path3", It.IsAny())); } [Test] @@ -170,10 +173,10 @@ public async Task FetchAsync_DryRunNoCalls() } [Test] - public async Task FetchAsync_DuplicateIdNotDeleted() + public async Task FetchAsync_DuplicateNameNotDeleted() { var localTriggers = GetLocalConfigs(); - var triggerConfig = new TriggerConfig("id1", "changed name", "eventType", "cloud-code", "actionUrn") { Path = ""}; + var triggerConfig = new TriggerConfig("otherId", "name1", "changedEventType", "cloud-code", "actionUrn") { Path = "" }; localTriggers.Add(triggerConfig); var remoteTriggers = GetRemoteConfigs(); @@ -198,8 +201,8 @@ public async Task FetchAsync_DuplicateIdNotDeleted() It.IsAny()), Times.Never); - Assert.Contains(localTriggers.FirstOrDefault(l => l.Name == "changed name"), actualRes.Failed); - Assert.Contains(localTriggers.FirstOrDefault(l => l.Name == "name1"), actualRes.Failed); + Assert.Contains(localTriggers.FirstOrDefault(l => l.Id == "otherId"), actualRes.Failed); + Assert.Contains(localTriggers.FirstOrDefault(l => l.Id == "id1"), actualRes.Failed); } static List GetLocalConfigs() diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Deploy/TriggerFetchServiceTests.cs b/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Deploy/TriggerFetchServiceTests.cs index 8d996d4..e8c02e5 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Deploy/TriggerFetchServiceTests.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Deploy/TriggerFetchServiceTests.cs @@ -2,16 +2,11 @@ using Moq; using Newtonsoft.Json; using Unity.Services.Cli.Authoring.Input; -using Unity.Services.Cli.Authoring.Model; -using Unity.Services.Cli.Authoring.Service; -using Unity.Services.Cli.Common.Utils; using Unity.Services.Cli.Triggers.Deploy; using Unity.Services.Cli.Triggers.Fetch; using Unity.Services.Cli.Triggers.IO; using Unity.Services.DeploymentApi.Editor; -using Unity.Services.Triggers.Authoring.Core.Deploy; using Unity.Services.Triggers.Authoring.Core.Fetch; -using Unity.Services.Triggers.Authoring.Core.IO; using Unity.Services.Triggers.Authoring.Core.Model; using Unity.Services.Triggers.Authoring.Core.Service; using FetchResult = Unity.Services.Triggers.Authoring.Core.Fetch.FetchResult; @@ -19,7 +14,6 @@ namespace Unity.Services.Cli.Triggers.UnitTest.Deploy; [TestFixture] -[Ignore("Fetch not in scope")] public class TriggerFetchServiceTests { TriggersFetchService? m_FetchService; @@ -36,11 +30,8 @@ public void SetUp() m_MockTriggerClient.Object, m_MockLoader.Object); - var tr1 = new TriggerConfig("tr1", "tr1", "eventType", "cloud-code", "actionUrn"); - var tr2 = new TriggerConfig("tr2", "tr2", "eventType", "cloud-code", "actionUrn"); - - var mockLoad = (IReadOnlyList) new[] { tr1 }; - var failedToLoad = (IReadOnlyList)Array.Empty(); + var tr1 = new TriggerConfig("tr1", "Trigger1", "EventType1", "cloud-code", "urn:ugs:cloud-code:MyTestScript"); + var tr2 = new TriggerConfig("tr2", "Trigger2", "EventType2", "cloud-code", "urn:ugs:cloud-code:MyTestScript"); m_MockLoader .Setup( @@ -49,21 +40,18 @@ public void SetUp() It.IsAny(), It.IsAny()) ) - .ReturnsAsync( - () => + .ReturnsAsync(new TriggersFileItem(new TriggersConfigFile(new List() { - var file = JsonConvert.DeserializeObject( - "{\"Configs\":[\n {\"Id\":\"tr1\",\"Name\":\"Trigger1\",\"EventType\":\"EventType1\",\"ActionType\":\"cloud-code\",\"ActionUrn\":\"urn:ugs:cloud-code:MyTestScript\"},\n {\"Id\":\"tr2\",\"Name\":\"Trigger2\",\"EventType\":\"EventType1\",\"ActionType\":\"cloud-code\",\"ActionUrn\":\"urn:ugs:cloud-code:MyTestScript\"},\n ]}" - ); - return new TriggersFileItem(file!, "samplePath"); - }); + new("Trigger1", "EventType1", "cloud-code", "urn:ugs:cloud-code:MyTestScript"), + new("Trigger2", "EventType2", "cloud-code", "urn:ugs:cloud-code:MyTestScript") + }), "samplePath")); var deployResult = new FetchResult() { Created = new List { tr2 }, Updated = new List(), Deleted = new List(), - Fetched = new List { tr2 }, + Fetched = new List { tr1, tr2 }, Failed = new List() }; var fromResult = Task.FromResult(deployResult); @@ -98,7 +86,7 @@ public async Task FetchAsync_MapsResult() Assert.AreEqual(1, res.Created.Count); Assert.AreEqual(0, res.Updated.Count); Assert.AreEqual(0, res.Deleted.Count); - Assert.AreEqual(1, res.Fetched.Count); + Assert.AreEqual(2, res.Fetched.Count); Assert.AreEqual(0, res.Failed.Count); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Unity.Services.Cli.Triggers.UnitTest.csproj b/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Unity.Services.Cli.Triggers.UnitTest.csproj index 5db10d3..85c9299 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Unity.Services.Cli.Triggers.UnitTest.csproj +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers.UnitTest/Unity.Services.Cli.Triggers.UnitTest.csproj @@ -6,9 +6,15 @@ Unity.Services.Cli.Triggers.UnitTest + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersAuthoringResult.cs b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersAuthoringResult.cs index 9b12a17..000fe8d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersAuthoringResult.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersAuthoringResult.cs @@ -19,11 +19,9 @@ public TriggersDeploymentResult( created, authored, failed, - dryRun) - { - } + dryRun) { } - public override TableContent ToTable() + public override TableContent ToTable(string service = "") { return AccessControlResToTable(this); } @@ -74,11 +72,9 @@ public TriggersFetchResult( created, authored, failed, - dryRun) - { - } + dryRun) { } - public override TableContent ToTable() + public override TableContent ToTable(string service = "") { return TriggersDeploymentResult.AccessControlResToTable(this); } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersClient.cs b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersClient.cs index 13c4dba..ae1294d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersClient.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersClient.cs @@ -98,7 +98,7 @@ static TriggerConfig FromResponse(Gateway.TriggersApiV1.Generated.Model.TriggerC { return new TriggerConfig() { - ActionType = JsonConvert.SerializeObject(responseConfig.ActionType), + ActionType = JsonConvert.SerializeObject(responseConfig.ActionType).Replace("\"", ""), ActionUrn = responseConfig.ActionUrn, EventType = responseConfig.EventType, Id = responseConfig.Id.ToString(), diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersConfigFile.cs b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersConfigFile.cs index 228972a..4be8b18 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersConfigFile.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersConfigFile.cs @@ -7,6 +7,9 @@ namespace Unity.Services.Cli.Triggers.Deploy; public class TriggersConfigFile : IFileTemplate { + [JsonProperty("$schema")] + public string Value => "https://ugs-config-schemas.unity3d.com/v1/triggers.schema.json"; + public IList Configs { get; set; } [JsonIgnore] diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersSerializer.cs b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersSerializer.cs index 5fb941a..1e80705 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersSerializer.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Deploy/TriggersSerializer.cs @@ -12,6 +12,6 @@ public string Serialize(IList config) { Configs = config.Cast().ToList() }; - return JsonConvert.SerializeObject(file); + return file.FileBodyText; } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Fetch/TriggersFetchService.cs b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Fetch/TriggersFetchService.cs index cff2f3f..fab0927 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers/Fetch/TriggersFetchService.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers/Fetch/TriggersFetchService.cs @@ -15,7 +15,6 @@ class TriggersFetchService : TriggerDeployFetchBase, IFetchService { readonly ITriggersFetchHandler m_FetchHandler; readonly ITriggersClient m_Client; - readonly ITriggersResourceLoader m_ResourceLoader; public TriggersFetchService( ITriggersFetchHandler fetchHandler, @@ -24,7 +23,6 @@ public TriggersFetchService( { m_FetchHandler = fetchHandler; m_Client = client; - m_ResourceLoader = resourceLoader; } public string ServiceType => TriggersConstants.ServiceType; @@ -74,9 +72,14 @@ public async Task FetchAsync( res.Updated, res.Deleted, res.Created, - deserializedFiles.Where(f => f.Status.MessageSeverity != SeverityLevel.Error) - .Concat(createdFiles).ToList(), - failedToDeserialize.Cast().Concat(failedToDeploy).ToList(), + deserializedFiles + .Where(f => f.Status.MessageSeverity != SeverityLevel.Error) + .Concat(createdFiles) + .ToList(), + failedToDeserialize + .Cast() + .Concat(failedToDeploy) + .ToList(), input.DryRun); } } diff --git a/Unity.Services.Cli/Unity.Services.Cli.Triggers/TriggersModule.cs b/Unity.Services.Cli/Unity.Services.Cli.Triggers/TriggersModule.cs index 421986f..656792d 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.Triggers/TriggersModule.cs +++ b/Unity.Services.Cli/Unity.Services.Cli.Triggers/TriggersModule.cs @@ -60,7 +60,7 @@ public static void RegisterServices(IServiceCollection serviceCollection) // Register the command handler serviceCollection.AddTransient(); serviceCollection.AddTransient(); - + serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); diff --git a/Unity.Services.Cli/Unity.Services.Cli.sln b/Unity.Services.Cli/Unity.Services.Cli.sln index 29500d7..defc8c2 100644 --- a/Unity.Services.Cli/Unity.Services.Cli.sln +++ b/Unity.Services.Cli/Unity.Services.Cli.sln @@ -83,6 +83,15 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Cli.Triggers.UnitTest", "Unity.Services.Cli.Triggers.UnitTest\Unity.Services.Cli.Triggers.UnitTest.csproj", "{BA3B69BA-FEF9-4F71-A73F-B4BE717CC392}" EndProject EndProject +EndProject +EndProject +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Cli.Scheduler", "Unity.Services.Cli.Scheduler\Unity.Services.Cli.Scheduler.csproj", "{7BA75DA5-852E-4B13-AA6A-C79B7E268418}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Scheduler.Authoring.Core", "Unity.Services.Scheduler.Authoring.Core\Unity.Services.Scheduler.Authoring.Core.csproj", "{9ECC7DF3-EDA2-4036-B175-52073E43335A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Services.Cli.Scheduler.UnitTest", "Unity.Services.Cli.Scheduler.UnitTest\Unity.Services.Cli.Scheduler.UnitTest.csproj", "{D511207A-820C-4AA9-AF46-D76F1027F105}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -231,5 +240,17 @@ Global {D8E319B8-E411-4A68-B2EA-39D3B65C745C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D8E319B8-E411-4A68-B2EA-39D3B65C745C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D8E319B8-E411-4A68-B2EA-39D3B65C745C}.Release|Any CPU.Build.0 = Release|Any CPU + {7BA75DA5-852E-4B13-AA6A-C79B7E268418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7BA75DA5-852E-4B13-AA6A-C79B7E268418}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7BA75DA5-852E-4B13-AA6A-C79B7E268418}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7BA75DA5-852E-4B13-AA6A-C79B7E268418}.Release|Any CPU.Build.0 = Release|Any CPU + {9ECC7DF3-EDA2-4036-B175-52073E43335A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9ECC7DF3-EDA2-4036-B175-52073E43335A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9ECC7DF3-EDA2-4036-B175-52073E43335A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9ECC7DF3-EDA2-4036-B175-52073E43335A}.Release|Any CPU.Build.0 = Release|Any CPU + {D511207A-820C-4AA9-AF46-D76F1027F105}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D511207A-820C-4AA9-AF46-D76F1027F105}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D511207A-820C-4AA9-AF46-D76F1027F105}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D511207A-820C-4AA9-AF46-D76F1027F105}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Unity.Services.Cli/Unity.Services.Cli/Program.cs b/Unity.Services.Cli/Unity.Services.Cli/Program.cs index 550a0ca..bf08a3c 100644 --- a/Unity.Services.Cli/Unity.Services.Cli/Program.cs +++ b/Unity.Services.Cli/Unity.Services.Cli/Program.cs @@ -37,6 +37,7 @@ #endif using Unity.Services.Cli.Player; using Unity.Services.Cli.Access; +using Unity.Services.Cli.Scheduler; namespace Unity.Services.Cli; @@ -70,10 +71,12 @@ public static async Task InternalMain(string[] args, Logger logger) telemetrySender = CommonModule.CreateTelemetrySender(systemEnvironmentProvider); host.ConfigureServices(ConfigurationModule.RegisterServices); + host.ConfigureServices(AuthenticationModule.RegisterServices); host.ConfigureServices(EnvironmentModule.RegisterServices); host.ConfigureServices(DeployModule.RegisterServices); host.ConfigureServices(CloudCodeModule.RegisterServices); + host.ConfigureServices(SchedulerModule.RegisterServices); host.ConfigureServices(RemoteConfigModule.RegisterServices); host.ConfigureServices(AccessModule.RegisterServices); host.ConfigureServices(GameServerHostingModule.RegisterServices); @@ -140,6 +143,7 @@ public static async Task InternalMain(string[] args, Logger logger) .AddCliServicesMiddleware(services) .AddModule(new AuthenticationModule()) .AddModule(new CloudCodeModule()) + .AddModule(new SchedulerModule()) .AddModule(new ConfigurationModule()) .AddModule(new DeployModule()) .AddModule(new FetchModule()) 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 8be62e2..a8096c1 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.2.0 + 1.3.0 true true @@ -30,6 +30,7 @@ + @@ -42,7 +43,7 @@ full - TRACE;FEATURE_ECONOMY;FEATURE_LEADERBOARDS;FEATURE_LEADERBOARDS_DEPLOY;FEATURE_LEADERBOARDS_IMPORT_EXPORT;FEATURE_TRIGGERS + TRACE;FEATURE_ECONOMY;FEATURE_LEADERBOARDS;FEATURE_LEADERBOARDS_DEPLOY;FEATURE_LEADERBOARDS_IMPORT_EXPORT;FEATURE_TRIGGERS; $(DefineConstants);$(ExtraDefineConstants) diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Batching/Batching.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Batching/Batching.cs index 3989792..2826249 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Batching/Batching.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Batching/Batching.cs @@ -20,104 +20,13 @@ public static class Batching /// /// Asynchronously execute a collection of delegates in batches with delay between them /// - /// IEnumerable of the delegates you want to run in batches - /// Callback that will be invoked when a delegate has finished executing + /// IEnumerable of the delegates you want to run in batches /// /// Size of the batches /// Delay in seconds between batches - /// Exception thrown when a delegate throws an exception + /// Exception thrown when a batch item throws an exception /// You need to handle the AggregateException's innerExceptions (that's where you'll get /// the exceptions related to the individual batch items executed) - public static async Task ExecuteInBatchesAsync( - IEnumerable> delegates, - CancellationToken cancellationToken, - int batchSize = k_BatchSize, - double secondsDelay = k_SecondsDelay) - { - var newDelegates = new List>>(); - - foreach (var del in delegates) - { - newDelegates.Add( - async () => - { - await Task.Run(del, cancellationToken); - return 0; - }); - - if (cancellationToken.IsCancellationRequested) - { - return; - } - } - - await ExecuteInBatchesAsync( - newDelegates, - cancellationToken, - batchSize, - secondsDelay); - } - - /// - /// Asynchronously execute a collection of delegates in batches with delay between them - /// - /// IEnumerable of the delegates you want to run in batches - /// Callback that will be invoked when a delegate has finished executing - /// - /// Size of the batches - /// Delay in seconds between batches - /// The return value type of your delegates - /// A collection of results - /// Exception thrown when a delegate throws an exception - /// You need to handle the AggregateException's innerExceptions (that's where you'll get - /// the exceptions related to the individual batch items executed) - public static async Task> ExecuteInBatchesAsync( - IReadOnlyList>> delegates, - CancellationToken cancellationToken, - int batchSize = k_BatchSize, - double secondsDelay = k_SecondsDelay) - { - var exceptions = new ConcurrentQueue(); - var batchesResult = new ConcurrentBag(); - var chunks = delegates.Chunk(batchSize).ToList(); - - for (int i = 0; i < chunks.Count; i++) - { - try - { - var batchResult = await ExecuteBatchAsync(chunks[i], cancellationToken); - foreach (var result in batchResult) - { - batchesResult.Add(result); - } - } - catch (AggregateException e) - { - foreach (var innerException in e.InnerExceptions) - { - exceptions.Enqueue(innerException); - } - } - - if (i + 1 != chunks.Count) - { - await Task.Delay(TimeSpan.FromSeconds(secondsDelay), cancellationToken); - } - - if (cancellationToken.IsCancellationRequested) - { - break; - } - } - - if (!exceptions.IsEmpty) - { - throw new AggregateException(k_BatchingExceptionMessage, exceptions.ToList()); - } - - return batchesResult.ToList(); - } - public static async Task ExecuteInBatchesAsync( IEnumerable tasks, CancellationToken cancellationToken, @@ -160,46 +69,6 @@ public static async Task ExecuteInBatchesAsync( } } - static async Task> ExecuteBatchAsync( - IEnumerable>> delegates, - CancellationToken cancellationToken) - { - var exceptions = new ConcurrentQueue(); - var batchesResult = new ConcurrentBag(); - var tasks = new ConcurrentBag(); - - Parallel.ForEach( - delegates, - del => - { - var task = Task.Run( - async () => - { - try - { - var result = await Task.Run(del, cancellationToken); - batchesResult.Add(result); - } - catch (Exception e) - { - exceptions.Enqueue(e); - } - }, - cancellationToken); - - tasks.Add(task); - }); - - await Task.WhenAll(tasks); - - if (!exceptions.IsEmpty) - { - throw new AggregateException(k_BatchingExceptionMessage, exceptions.ToList()); - } - - return batchesResult.ToList(); - } - static async Task> ExecuteBatchAsync(IEnumerable insideTasks) { var tasks = new ConcurrentBag(); @@ -224,36 +93,5 @@ static async Task> ExecuteBatchAsync(IEnumerable return exceptions.ToList(); } - - // Copy/pasted utility code from the Enumerable.Chunk method available in dotnet 5 - static IEnumerable ChunkIterator(IEnumerable source, int size) - { - using IEnumerator e = source.GetEnumerator(); - while (e.MoveNext()) - { - TSource[] chunk = new TSource[size]; - chunk[0] = e.Current; - - int i = 1; - for (; i < chunk.Length && e.MoveNext(); i++) - { - chunk[i] = e.Current; - } - - if (i == chunk.Length) - { - yield return chunk; - } - else - { - Array.Resize(ref chunk, i); - yield return chunk; - yield break; - } - } - } - - static IEnumerable Chunk(this IEnumerable source, int size) - => ChunkIterator(source, size); } } diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/CompoundModuleTemplateDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/CompoundModuleTemplateDeploymentHandler.cs new file mode 100644 index 0000000..fb6fdd1 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/CompoundModuleTemplateDeploymentHandler.cs @@ -0,0 +1,127 @@ +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.ModuleTemplate.Authoring.Core.Model; +using Unity.Services.ModuleTemplate.Authoring.Core.Service; +using Unity.Services.ModuleTemplate.Authoring.Core.Validations; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.Deploy +{ + public class CompoundModuleTemplateDeploymentHandler : ModuleTemplateFetchDeployBase, ICompoundModuleTemplateDeploymentHandler + { + public CompoundModuleTemplateDeploymentHandler(IModuleTemplateClient client) + : base(client) { } + + public async Task DeployAsync( + IReadOnlyList compoundLocalResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default) + { + var res = new DeployResult(); + + var localResources = compoundLocalResources + .SelectMany(c => c.Items) + .ToList(); + + var cmpdLocalResources = compoundLocalResources.ToList(); + + var filteredLocalResources = DuplicateResourceValidation.FilterDuplicateResources( + localResources, out var duplicateGroups); + + UpdateDuplicateResourceStatus(duplicateGroups); + + var remoteResources = await GetRemoteItems(cancellationToken: token); + + SetupMaps(filteredLocalResources, remoteResources); + + var toCreate = filteredLocalResources + .Where(DoesNotExistRemotely) + .ToList(); + + var toUpdate = filteredLocalResources + .Where(ExistsRemotely) + .ToList(); + + var toDelete = new List(); + if (reconcile) + { + toDelete = remoteResources + .Where(DoesNotExistLocally) + .ToList(); + } + + res.Deployed = cmpdLocalResources.Cast().Concat(toDelete).ToList(); + + if (dryRun) + { + UpdateDryRunResult(toUpdate, toDelete, toCreate, compoundLocalResources); + return res; + } + + cmpdLocalResources.ForEach(i => i.Progress = 50); + filteredLocalResources.ForEach(l => l.Progress = 50); + + var createTasks = GetTasks(toCreate, Client.Create, Constants.Created, token); + var updateTasks = GetTasks(toUpdate, Client.Update, Constants.Updated, token); + var deleteTasks = reconcile + ? GetTasks(toDelete, Client.Delete, Constants.Deleted, token) + : new List(); + + var allTasks = createTasks.Concat(updateTasks).Concat(deleteTasks); + + await Batching.Batching.ExecuteInBatchesAsync(allTasks, token); + + UpdateCompoundItemStatus(compoundLocalResources); + + return res; + } + + static IEnumerable GetTasks( + IReadOnlyList resources, + Func func, + string taskAction, + CancellationToken token) + { + return resources.Select(i => DeployResource(func, i, taskAction, token)); + } + + static async Task DeployResource( + Func task, + IResourceDeploymentItem resource, + string taskAction, + CancellationToken token) + { + try + { + resource.Status = Statuses.GetDeploying(); + await task(resource.Resource, token); + resource.Status = Statuses.GetDeployed(taskAction); + resource.Progress = 100f; + } + catch (Exception e) + { + resource.Status = Statuses.GetFailedToDeploy(e.Message); + } + } + + protected override DeploymentStatus GetSuccessStatus(string message) + { + return Statuses.GetDeployed(message); + } + + protected override DeploymentStatus GetFailedStatus(string message) + { + return Statuses.GetFailedToDeploy(message); + } + + protected override IResourceDeploymentItem CreateItem(string rootDirectory, IResource resource) + { + return new NestedResourceDeploymentItem("Remote", resource); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/DeployResult.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/DeployResult.cs index 36b7084..9bf3892 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/DeployResult.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/DeployResult.cs @@ -1,14 +1,10 @@ using System.Collections.Generic; -using Unity.Services.ModuleTemplate.Authoring.Core.Model; +using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.ModuleTemplate.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; } + public IReadOnlyList Deployed { get; set; } } } diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ICompoundModuleTemplateDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ICompoundModuleTemplateDeploymentHandler.cs new file mode 100644 index 0000000..be846f5 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ICompoundModuleTemplateDeploymentHandler.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.ModuleTemplate.Authoring.Core.Model; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.Deploy +{ + public interface ICompoundModuleTemplateDeploymentHandler + { + Task DeployAsync( + IReadOnlyList compoundLocalResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default); + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/IModuleTemplateDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/IModuleTemplateDeploymentHandler.cs index 46f0757..6646b16 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/IModuleTemplateDeploymentHandler.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/IModuleTemplateDeploymentHandler.cs @@ -7,7 +7,8 @@ namespace Unity.Services.ModuleTemplate.Authoring.Core.Deploy { public interface IModuleTemplateDeploymentHandler { - Task DeployAsync(IReadOnlyList localResources, + Task DeployAsync( + IReadOnlyList localResources, bool dryRun = false, bool reconcile = false, CancellationToken token = default); diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateDeploymentHandler.cs index 39237a9..71db0f2 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateDeploymentHandler.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateDeploymentHandler.cs @@ -10,62 +10,58 @@ namespace Unity.Services.ModuleTemplate.Authoring.Core.Deploy { - public class ModuleTemplateDeploymentHandler : IModuleTemplateDeploymentHandler + public class ModuleTemplateDeploymentHandler : ModuleTemplateFetchDeployBase, IModuleTemplateDeploymentHandler { - readonly IModuleTemplateClient m_Client; - readonly object m_ResultLock = new(); - public ModuleTemplateDeploymentHandler(IModuleTemplateClient client) - { - m_Client = client; - } + : base(client) { } public async Task DeployAsync( - IReadOnlyList localResources, + IReadOnlyList localResources, bool dryRun = false, bool reconcile = false, CancellationToken token = default) { var res = new DeployResult(); - localResources = DuplicateResourceValidation.FilterDuplicateResources( + var filteredLocalResources = DuplicateResourceValidation.FilterDuplicateResources( localResources, out var duplicateGroups); - var remoteResources = await m_Client.List(); + UpdateDuplicateResourceStatus(duplicateGroups); - var toCreate = localResources - .Except(remoteResources) + var remoteResources = await GetRemoteItems(cancellationToken: token); + + SetupMaps(filteredLocalResources, remoteResources); + + var toCreate = filteredLocalResources + .Where(DoesNotExistRemotely) .ToList(); - var toUpdate = localResources - .Except(toCreate) + var toUpdate = filteredLocalResources + .Where(ExistsRemotely) .ToList(); - var toDelete = new List(); + var toDelete = new List(); if (reconcile) { toDelete = remoteResources - .Except(localResources) + .Where(DoesNotExistLocally) .ToList(); } - res.Created = toCreate; - res.Deleted = toDelete; - res.Updated = toUpdate; - res.Deployed = new List(); - res.Failed = new List(); - - UpdateDuplicateResourceStatus(res, duplicateGroups); + res.Deployed = localResources.Concat(toDelete).ToList(); if (dryRun) { + UpdateDryRunResult(toUpdate, toDelete, toCreate); return res; } - var createTasks = GetTasks(toCreate, m_Client.Create, res); - var updateTasks = GetTasks(toUpdate, m_Client.Update, res); + filteredLocalResources.ForEach(l => l.Progress = 50); + + var createTasks = GetTasks(toCreate, Client.Create, Constants.Created, token); + var updateTasks = GetTasks(toUpdate, Client.Update, Constants.Updated, token); var deleteTasks = reconcile - ? GetTasks(toDelete, m_Client.Delete, res) + ? GetTasks(toDelete, Client.Delete, Constants.Deleted, token) : new List(); var allTasks = createTasks.Concat(updateTasks).Concat(deleteTasks); @@ -75,60 +71,42 @@ public async Task DeployAsync( return res; } - // TODO: Add support for CancellationToken in m_Client.Create, m_Client.Update, m_Client.Delete - IEnumerable GetTasks(List resources, Func func, DeployResult res) - => resources.Select(i => DeployResource(func, i, res)); - - protected virtual void UpdateStatus( - IResource resource, - DeploymentStatus status) - { - // clients can override this to provide user feedback on progress - resource.Status = status; - } - - protected virtual void UpdateProgress( - IResource resource, - float progress) + static IEnumerable GetTasks( + List resources, + Func func, + string taskAction, + CancellationToken token) { - // clients can override this to provide user feedback on progress - resource.Progress = progress; + return resources.Select(i => DeployResource(func, i, taskAction, token)); } - 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, - IResource resource, - DeployResult res) + static async Task DeployResource( + Func task, + IResourceDeploymentItem resource, + string taskAction, + CancellationToken token) { try { - await task(resource); - lock (m_ResultLock) - res.Deployed.Add(resource); - UpdateStatus(resource, Statuses.Deployed); - UpdateProgress(resource, 100); + resource.Status = Statuses.GetDeploying(); + await task(resource.Resource, token); + resource.Status = Statuses.GetDeployed(taskAction); + resource.Progress = 100f; } catch (Exception e) { - lock (m_ResultLock) - res.Failed.Add(resource); - UpdateStatus(resource, Statuses.GetFailedToDeploy(e.Message)); + resource.Status = Statuses.GetFailedToDeploy(e.Message); } } + + protected override DeploymentStatus GetSuccessStatus(string message) + { + return Statuses.GetDeployed(message); + } + + protected override DeploymentStatus GetFailedStatus(string message) + { + return Statuses.GetFailedToDeploy(message); + } } } diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateFetchDeployBase.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateFetchDeployBase.cs new file mode 100644 index 0000000..7d5cb48 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Deploy/ModuleTemplateFetchDeployBase.cs @@ -0,0 +1,154 @@ +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.ModuleTemplate.Authoring.Core.Model; +using Unity.Services.ModuleTemplate.Authoring.Core.Service; +using Unity.Services.ModuleTemplate.Authoring.Core.Validations; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.Deploy +{ + public abstract class ModuleTemplateFetchDeployBase + { + IReadOnlyDictionary m_LocalMap; + IReadOnlyDictionary m_RemoteMap; + protected IModuleTemplateClient Client { get; } + + protected ModuleTemplateFetchDeployBase(IModuleTemplateClient client) + { + Client = client; + } + + protected void SetupMaps(IReadOnlyList filteredLocalResources, IReadOnlyList remoteResources) + { + //TODO: Verify the right nomenclature for your ID here, or use `Name` + m_LocalMap = filteredLocalResources.ToDictionary(l => l.Resource.Id, l => l); + m_RemoteMap = remoteResources.ToDictionary(l => l.Resource.Id, l => l); + } + + protected async Task> GetRemoteItems( + string rootDirectory = null, + CancellationToken cancellationToken = default) + { + // TODO: if you fail to get a remote resource during the list, + // you must set the status accordingly. + // We're operating under the assumption that List will either completely succeed or not + // if you have to make multiple GET calls, update this method accordingly + var remoteResources = await Client.List(cancellationToken); + var remoteItems = remoteResources + .Select( + resource => + { + var deploymentItem = CreateItem(rootDirectory, resource); + return deploymentItem; + }) + .ToList(); + return remoteItems; + } + + protected bool ExistsRemotely(IResourceDeploymentItem resource) + { + return m_RemoteMap.ContainsKey(resource.Resource.Id); + } + + protected bool DoesNotExistRemotely(IResourceDeploymentItem resource) + { + return !m_RemoteMap.ContainsKey(resource.Resource.Id); + } + + protected bool DoesNotExistLocally(IResourceDeploymentItem resource) + { + return !m_LocalMap.ContainsKey(resource.Resource.Id); + } + + protected IResourceDeploymentItem GetRemoteResourceItem(string id) + { + return m_RemoteMap[id]; + } + + protected void UpdateDuplicateResourceStatus( + IReadOnlyList> duplicateGroups) + { + foreach (var group in duplicateGroups) + { + foreach (var resourceItem in group) + { + var (message, shortMessage) = DuplicateResourceValidation.GetDuplicateResourceErrorMessages(resourceItem, group.ToList()); + resourceItem.Status = GetFailedStatus(shortMessage); + } + } + } + + protected virtual IResourceDeploymentItem CreateItem(string rootDirectory, IResource resource) + { + var path = rootDirectory != null + ? Path.Combine(rootDirectory, resource.Id + Constants.SimpleFileExtension) + : "Remote"; + return new SimpleResourceDeploymentItem(path) + { + Resource = resource + }; + } + + protected virtual void UpdateDryRunResult( + IReadOnlyList toUpdate, + IReadOnlyList toDelete, + IReadOnlyList toCreate, + IReadOnlyList localCompoundItems = null) + { + foreach (var i in toUpdate) + { + i.Status = GetSuccessStatus(Constants.Updated); + } + + foreach (var i in toDelete) + { + i.Status = GetSuccessStatus(Constants.Deleted); + } + + foreach (var i in toCreate) + { + i.Status = GetSuccessStatus(Constants.Created); + } + + if (localCompoundItems != null) + { + UpdateCompoundItemStatus(localCompoundItems); + } + } + + protected void UpdateCompoundItemStatus(IReadOnlyList localCompoundItems) + { + foreach (var item in localCompoundItems) + { + var failedItemCount = + item.Items.Count(nested => nested.Status.MessageSeverity != SeverityLevel.Success); + + if (failedItemCount == 0) + { + item.Status = GetSuccessStatus("All items were successfully deployed"); + item.Progress = 100f; + } + else if (failedItemCount != item.Items.Count) + { + item.Status = GetPartialStatus(); + } + else + { + item.Status = GetFailedStatus("No items were deployed"); + } + } + } + + protected abstract DeploymentStatus GetSuccessStatus(string message); + + protected abstract DeploymentStatus GetFailedStatus(string message); + + protected virtual DeploymentStatus GetPartialStatus(string message = null) + { + return Statuses.GetPartialDeploy(message); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/CompoundModuleTemplateFetchHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/CompoundModuleTemplateFetchHandler.cs new file mode 100644 index 0000000..7b8b688 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/CompoundModuleTemplateFetchHandler.cs @@ -0,0 +1,349 @@ +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.ModuleTemplate.Authoring.Core.Deploy; +using Unity.Services.ModuleTemplate.Authoring.Core.IO; +using Unity.Services.ModuleTemplate.Authoring.Core.Model; +using Unity.Services.ModuleTemplate.Authoring.Core.Service; +using Unity.Services.ModuleTemplate.Authoring.Core.Validations; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.Fetch +{ + public class CompoundModuleTemplateFetchHandler : ModuleTemplateFetchDeployBase, ICompoundModuleTemplateFetchHandler + { + internal const string FetchResultName = "fetched_resources" + Constants.CompoundFileExtension; + readonly IModuleTemplateCompoundResourceLoader m_ResourceLoader; + + public CompoundModuleTemplateFetchHandler( + IModuleTemplateClient client, + IModuleTemplateCompoundResourceLoader resourceLoader) + : base(client) + { + m_ResourceLoader = resourceLoader; + } + + public async Task FetchAsync( + string rootDirectory, + IReadOnlyList compoundLocalResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default) + { + compoundLocalResources.ToList().ForEach(l => l.Progress = 0f); + + var localResources = compoundLocalResources + .SelectMany(c => c.Items) + .ToList(); + + var filteredLocalResources = DuplicateResourceValidation.FilterDuplicateResources( + localResources, out var duplicateGroups); + + UpdateDuplicateResourceStatus(duplicateGroups); + + var remoteResources = (await GetRemoteItems(rootDirectory, token)).Cast().ToList(); + + SetupMaps(filteredLocalResources, remoteResources); + + var toUpdate = filteredLocalResources + .Where(ExistsRemotely) + .ToList(); + + var toDelete = filteredLocalResources + .Where(DoesNotExistRemotely) + .ToList(); + + var toCreate = new List(); + if (reconcile) + { + toCreate = remoteResources + .Where(DoesNotExistLocally) + .ToList(); + } + + var (defaultFile, defaultFileCreated) = GetDefaultFile( + rootDirectory, + compoundLocalResources, + toCreate); + + if (defaultFileCreated && toCreate.Count > 0) + { + compoundLocalResources = compoundLocalResources.Append(defaultFile).ToList(); + } + + var res = new FetchResult + { + Fetched = compoundLocalResources + }; + + if (dryRun) + { + UpdateCompoundDryRunResult(toUpdate, toDelete, toCreate, compoundLocalResources, defaultFileCreated); + return res; + } + + // Modify the state in-memory. if the file was just created, it has been populated. + if (!defaultFileCreated) + CreateLocal(toCreate); + UpdateLocal(toUpdate); + DeleteLocal(toDelete); + + var (toUpdateFiles, toDeleteFiles, toCreateFiles) + = GetAffectedResources(compoundLocalResources, toCreate, toDelete); + + filteredLocalResources.ForEach(l => l.Progress = 50); + + var createTasks = UpdateOrCreateResources(toCreateFiles, token); + var updateTasks = UpdateOrCreateResources(toUpdateFiles, token); + var deleteTasks = DeleteResources(toDeleteFiles, token); + + await WaitForTasks(createTasks, Constants.Created); + await WaitForTasks(updateTasks, Constants.Updated); + await WaitForTasks(deleteTasks, Constants.Deleted); + + UpdateCompoundItemsStatus(compoundLocalResources, toCreate, toUpdate, toDelete, defaultFileCreated); + return res; + } + + static (ICompoundResourceDeploymentItem, bool) GetDefaultFile( + string rootDir, + IReadOnlyList compoundLocalResources, + IReadOnlyList toCreate) + { + var defaultFile = compoundLocalResources + .FirstOrDefault(f => f.Name == FetchResultName); + var created = defaultFile == null; + if (created) + { + // If it does not exist, there are no updates/deletes to be done + defaultFile = new CompoundResourceDeploymentItem(Path.Combine(rootDir, FetchResultName)) + { + Items = toCreate.ToList() + }; + } + + foreach (var nestedResourceDeploymentItem in toCreate) + { + nestedResourceDeploymentItem.Parent = defaultFile; + } + + return (defaultFile, created); + } + + static (List, List, List) + GetAffectedResources( + IReadOnlyList files, + IReadOnlyList toCreate, + IReadOnlyList toDelete) + { + var toCreateFiles = toCreate.Select(i => i.Parent).Distinct().ToList(); + var otherFiles = files.Except(toCreateFiles).ToList(); + var toUpdateFiles = new List(); + var toDeleteFiles = new List(); + foreach (var file in otherFiles) + { + //Avoid re-writting files where all resources failed to be written + var allFailed = file.Items.All(r => r.Status.MessageSeverity == SeverityLevel.Error); + if (allFailed) + continue; + + var deletedResourceCount = file.Items.Count(toDelete.Contains); + if (deletedResourceCount == file.Items.Count) + toDeleteFiles.Add(file); + else + toUpdateFiles.Add(file); + } + + return (toUpdateFiles, toDeleteFiles, toCreateFiles); + } + + void CreateLocal( + IReadOnlyList toCreate) + { + foreach (var entry in toCreate) + { + entry.Parent.Items.Add( + (INestedResourceDeploymentItem) GetRemoteResourceItem(entry.Resource.Id)); + } + } + + void UpdateLocal( + IReadOnlyList toUpdate) + { + foreach (var entry in toUpdate) + { + var entryIx = entry.Parent.Items.IndexOf(entry); + entry.Parent.Items[entryIx].Resource = + GetRemoteResourceItem(entry.Resource.Id).Resource; + } + } + + static void DeleteLocal( + IReadOnlyList toDelete) + { + foreach (var entry in toDelete) + { + var entryIx = entry.Parent.Items.IndexOf(entry); + //Marking null is marking for deletion. + //This allows the output to still be comprehensive (tell the user the item was deleted) + entry.Parent.Items[entryIx].Resource = null; + } + } + + List<(ICompoundResourceDeploymentItem, Task)> UpdateOrCreateResources( + List filesToWrite, + CancellationToken token) + { + List<(ICompoundResourceDeploymentItem, Task)> updateTasks = new List<(ICompoundResourceDeploymentItem, Task)>(); + foreach (var item in filesToWrite) + { + var task = m_ResourceLoader.CreateOrUpdateResource(item, token); + updateTasks.Add((item, task)); + } + + return updateTasks; + } + + List<(ICompoundResourceDeploymentItem, Task)> DeleteResources(List toDelete, CancellationToken token) + { + List<(ICompoundResourceDeploymentItem, Task)> deleteTasks = new List<(ICompoundResourceDeploymentItem, Task)>(); + foreach (var resource in toDelete) + { + var task = m_ResourceLoader.DeleteResource( + resource, + token); + deleteTasks.Add((resource, task)); + } + + return deleteTasks; + } + + protected async Task WaitForTasks( + List<(ICompoundResourceDeploymentItem, Task)> tasks, + string taskAction) + { + foreach (var (resource, task) in tasks) + { + try + { + await task; + resource.Progress = 100f; + resource.Status = GetSuccessStatus(taskAction); + } + catch (Exception e) + { + resource.Status = GetFailedStatus(e.Message); + } + } + } + + protected void UpdateCompoundDryRunResult( + IReadOnlyList toUpdate, + IReadOnlyList toDelete, + IReadOnlyList toCreate, + IReadOnlyList localCompoundItems, + bool defaultFileCreated) + { + base.UpdateDryRunResult( + toUpdate, + toDelete, + toCreate, + null); + + //Make a copy of the resource list to not modify the internal state during a "dry-run" + var fileToList = localCompoundItems.ToDictionary( + k => k, + k => k.Items.ToList()); + + foreach (var deletedItem in toDelete) + fileToList[deletedItem.Parent].Remove(deletedItem); + foreach (var createdItem in toCreate) + fileToList[createdItem.Parent].Add(createdItem); + + var createdFiles = defaultFileCreated + ? toCreate + .Select(i => i.Parent) + .Distinct() + .ToList() + : new List(); + + // Need to update the default file for added items, there's no better alternative + if (!defaultFileCreated) + { + CreateLocal(toCreate); + } + + foreach (var item in localCompoundItems) + { + var fileFutureItems = fileToList[item]; + var itemCount = fileFutureItems.Count; + UpdateCompoundItemStatus(item, itemCount, createdFiles); + } + } + + protected void UpdateCompoundItemsStatus( + IReadOnlyList localCompoundItems, + IReadOnlyList toCreate, + IReadOnlyList toUpdate, + IReadOnlyList toDelete, + bool defaultFileWasCreated) + { + base.UpdateDryRunResult(toUpdate, toDelete, toCreate); + var createdFiles = defaultFileWasCreated + ? toCreate + .Select(i => i.Parent) + .Distinct() + .ToList() + : new List(); + + foreach (var item in localCompoundItems) + { + var itemCount = item.Items.Except(toDelete).Count(); + UpdateCompoundItemStatus(item, itemCount, createdFiles); + } + } + + void UpdateCompoundItemStatus( + ICompoundResourceDeploymentItem item, + int itemCount, + List createdFiles) + { + var message = createdFiles.Contains(item) ? Constants.Created : Constants.Updated; + var failedItemCount = + item.Items.Count(nested => nested.Status.MessageSeverity != SeverityLevel.Success); + + if (itemCount == 0) + item.Status = GetSuccessStatus( + Constants.Deleted + "; All items were deleted, as they no longer exist remotely."); + else if (failedItemCount == 0) + item.Status = GetSuccessStatus(message + "; All items were successfully fetched"); + else if (failedItemCount != item.Items.Count) + item.Status = GetPartialStatus(); + else //futureItemCount > 0 && failedItem == futureItemCount + item.Status = GetFailedStatus("No items were fetched"); + } + + protected override IResourceDeploymentItem CreateItem(string rootDirectory, IResource resource) + { + return new NestedResourceDeploymentItem(Path.Combine(rootDirectory,FetchResultName), resource); + } + + protected override DeploymentStatus GetSuccessStatus(string message) + { + return Statuses.GetFetched(message); + } + + protected override DeploymentStatus GetFailedStatus(string message) + { + return Statuses.GetFailedToFetch(message); + } + + protected override DeploymentStatus GetPartialStatus(string message = null) + { + return Statuses.GetPartialFetch(message); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/FetchResult.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/FetchResult.cs index b1b51ce..de90532 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/FetchResult.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/FetchResult.cs @@ -1,14 +1,10 @@ using System.Collections.Generic; -using Unity.Services.ModuleTemplate.Authoring.Core.Model; +using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.ModuleTemplate.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; } + public IReadOnlyList Fetched { get; set; } } } diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ICompoundModuleTemplateFetchHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ICompoundModuleTemplateFetchHandler.cs new file mode 100644 index 0000000..e02c082 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ICompoundModuleTemplateFetchHandler.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.ModuleTemplate.Authoring.Core.Model; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.Fetch +{ + public interface ICompoundModuleTemplateFetchHandler + { + public Task FetchAsync( + string rootDirectory, + IReadOnlyList compoundLocalResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default); + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/IModuleTemplateFetchHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/IModuleTemplateFetchHandler.cs index 82fd18d..e4dedb5 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/IModuleTemplateFetchHandler.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/IModuleTemplateFetchHandler.cs @@ -9,7 +9,7 @@ public interface IModuleTemplateFetchHandler { public Task FetchAsync( string rootDirectory, - IReadOnlyList localResources, + IReadOnlyList localResources, bool dryRun = false, bool reconcile = false, CancellationToken token = default); diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ModuleTemplateFetchHandler.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ModuleTemplateFetchHandler.cs index 5398e72..bd4005b 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ModuleTemplateFetchHandler.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Fetch/ModuleTemplateFetchHandler.cs @@ -12,151 +12,136 @@ namespace Unity.Services.ModuleTemplate.Authoring.Core.Fetch { - public class ModuleTemplateFetchHandler : IModuleTemplateFetchHandler + public class ModuleTemplateFetchHandler : ModuleTemplateFetchDeployBase, IModuleTemplateFetchHandler { - readonly IModuleTemplateClient m_Client; - readonly IFileSystem m_FileSystem; + readonly IModuleTemplateSimpleResourceLoader m_ResourceLoader; public ModuleTemplateFetchHandler( IModuleTemplateClient client, - IFileSystem fileSystem) + IModuleTemplateSimpleResourceLoader resourceLoader) + : base(client) { - m_Client = client; - m_FileSystem = fileSystem; + m_ResourceLoader = resourceLoader; } - public async Task FetchAsync(string rootDirectory, - IReadOnlyList localResources, + public async Task FetchAsync( + string rootDirectory, + IReadOnlyList localResources, bool dryRun = false, bool reconcile = false, CancellationToken token = default) { - var res = new FetchResult(); + localResources.ToList().ForEach(l => l.Progress = 0f); - localResources = DuplicateResourceValidation.FilterDuplicateResources( + var filteredLocalResources = DuplicateResourceValidation.FilterDuplicateResources( localResources, out var duplicateGroups); - var remoteResources = await m_Client.List(); + UpdateDuplicateResourceStatus(duplicateGroups); - var toUpdate = localResources - .Intersect(remoteResources) + var remoteResources = await GetRemoteItems(rootDirectory, token); + + SetupMaps(filteredLocalResources, remoteResources); + + var toUpdate = filteredLocalResources + .Where(ExistsRemotely) .ToList(); - var toDelete = localResources - .Except(remoteResources) + var toDelete = filteredLocalResources + .Where(DoesNotExistRemotely) .ToList(); - var toCreate = new List(); + var toCreate = new List(); if (reconcile) { toCreate = remoteResources - .Except(localResources) + .Where(DoesNotExistLocally) .ToList(); } - res.Created = toCreate; - res.Deleted = toDelete; - res.Updated = toUpdate; - res.Fetched = new List(); - res.Failed = new List(); - - UpdateDuplicateResourceStatus(res, duplicateGroups); + var res = new FetchResult + { + Fetched = localResources.Concat(toCreate).ToList() + }; if (dryRun) { + UpdateDryRunResult(toUpdate, toDelete, toCreate); return res; } - var updateTasks = new List<(IResource, Task)>(); - var deleteTasks = new List<(IResource, Task)>(); - var createTasks = new List<(IResource, Task)>(); + filteredLocalResources.ForEach(l => l.Progress = 50); - foreach (var resource in toUpdate) - { - var task = m_FileSystem.WriteAllText( - resource.Path, - res.ToString(), - token); - updateTasks.Add((resource, task)); - } + var updateTasks = CreateOrUpdateResources(toUpdate, token); - foreach (var resource in toDelete) - { - var task = m_FileSystem.Delete( - resource.Path, - token); - deleteTasks.Add((resource, task)); - } + var deleteTasks = DeleteResources(toDelete, token); + var createTasks = new List<(IResourceDeploymentItem, Task)>(); if (reconcile) { - foreach (var resource in toCreate) - { - var task = m_FileSystem.WriteAllText( - resource.Path, - resource.Name, - token); - createTasks.Add((resource, task)); - } + createTasks = CreateOrUpdateResources(toCreate, token); } - await UpdateResult(updateTasks, res); - await UpdateResult(deleteTasks, res); - await UpdateResult(createTasks, res); + await WaitForTasks(updateTasks, Constants.Updated); + await WaitForTasks(deleteTasks, Constants.Deleted); + await WaitForTasks(createTasks, Constants.Created); return res; } - protected virtual void UpdateStatus( - IResource resource, - DeploymentStatus status) + List<(IResourceDeploymentItem, Task)> CreateOrUpdateResources(List toUpdate, CancellationToken token) { - // clients can override this to provide user feedback on progress - resource.Status = status; - } + List<(IResourceDeploymentItem, Task)> updateTasks = new List<(IResourceDeploymentItem, Task)>(); + foreach (var item in toUpdate) + { + item.Resource = GetRemoteResourceItem(item.Resource.Id).Resource; + var task = m_ResourceLoader.CreateOrUpdateResource(item, token); + updateTasks.Add((item, task)); + } - protected virtual void UpdateProgress( - IResource resource, - float progress) - { - // clients can override this to provide user feedback on progress - resource.Progress = progress; + return updateTasks; } - void UpdateDuplicateResourceStatus( - FetchResult result, - IReadOnlyList> duplicateGroups) + List<(IResourceDeploymentItem, Task)> DeleteResources(List toDelete, CancellationToken token) { - foreach (var group in duplicateGroups) + List<(IResourceDeploymentItem, Task)> deleteTasks = new List<(IResourceDeploymentItem, Task)>(); + foreach (var resource in toDelete) { - foreach (var res in group) - { - result.Failed.Add(res); - var (message, shortMessage) = DuplicateResourceValidation.GetDuplicateResourceErrorMessages(res, group.ToList()); - UpdateStatus(res, Statuses.GetFailedToFetch(shortMessage)); - } + var task = m_ResourceLoader.DeleteResource( + resource, + token); + deleteTasks.Add((resource, task)); } + + return deleteTasks; } - async Task UpdateResult( - List<(IResource, Task)> tasks, - FetchResult res) + static async Task WaitForTasks( + List<(IResourceDeploymentItem, Task)> tasks, + string taskAction) { foreach (var (resource, task) in tasks) { try { await task; - res.Fetched.Add(resource); - UpdateStatus(resource, Statuses.Fetched); - UpdateProgress(resource, 100); + resource.Progress = 100f; + resource.Status = Statuses.GetFetched(taskAction); } catch (Exception e) { - res.Failed.Add(resource); - UpdateStatus(resource, Statuses.GetFailedToFetch(e.Message)); + resource.Status = Statuses.GetFailedToFetch(e.Message); } } } + + protected override DeploymentStatus GetSuccessStatus(string message) + { + return Statuses.GetFetched(message); + } + + protected override DeploymentStatus GetFailedStatus(string message) + { + return Statuses.GetFailedToFetch(message); + } } } diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/CompoundResourceConfigFile.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/CompoundResourceConfigFile.cs new file mode 100644 index 0000000..30b8df3 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/CompoundResourceConfigFile.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Unity.Services.ModuleTemplate.Authoring.Core.Model; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.IO +{ + [Serializable] + public class CompoundResourceConfigFile + { + public CompoundResourceConfigFile() + { + Resources = new Dictionary(); + } + + public Dictionary Resources { get; set; } + } + + [DataContract] + public class ModuleTemplateResourceEntry + { + [DataMember] + public string Name { get; set; } + [DataMember] + public string UserFriendlyStringName { get; set; } + [DataMember] + public NestedObject NestedObj { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IFileSystem.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IFileSystem.cs index 4ddbb0b..c6cf3ce 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IFileSystem.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IFileSystem.cs @@ -3,7 +3,7 @@ namespace Unity.Services.ModuleTemplate.Authoring.Core.IO { - public interface IFileSystem + public interface IFileSystem //Abstracted away - delete { Task ReadAllText( string path, diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IModuleTemplateCompoundResourceLoader.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IModuleTemplateCompoundResourceLoader.cs new file mode 100644 index 0000000..ee583ec --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IModuleTemplateCompoundResourceLoader.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.ModuleTemplate.Authoring.Core.Model; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.IO +{ + public interface IModuleTemplateCompoundResourceLoader + { + Task ReadResource(string path, CancellationToken token); + Task CreateOrUpdateResource(ICompoundResourceDeploymentItem deployableItem, CancellationToken token); + Task DeleteResource(ICompoundResourceDeploymentItem deploymentItem, CancellationToken token); + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IModuleTemplateSimpleResourceLoader.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IModuleTemplateSimpleResourceLoader.cs new file mode 100644 index 0000000..7e17911 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/IO/IModuleTemplateSimpleResourceLoader.cs @@ -0,0 +1,13 @@ +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.ModuleTemplate.Authoring.Core.Model; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.IO +{ + public interface IModuleTemplateSimpleResourceLoader + { + Task ReadResource(string path, CancellationToken token); + Task CreateOrUpdateResource(IResourceDeploymentItem deployableItem, CancellationToken token); + Task DeleteResource(IResourceDeploymentItem deploymentItem, CancellationToken token); + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/ClientException.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/ClientException.cs new file mode 100644 index 0000000..6cfa88c --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/ClientException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.Model +{ + public class ClientException : Exception + { + public ClientException(string message, Exception innerExcception) : base(message, innerExcception) + { + + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/Constants.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/Constants.cs new file mode 100644 index 0000000..7da01b1 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/Constants.cs @@ -0,0 +1,12 @@ +namespace Unity.Services.ModuleTemplate.Authoring.Core.Model +{ + public class Constants + { + //TODO: Modify this extension as appropriate + public const string SimpleFileExtension = ".serv"; + public const string CompoundFileExtension = ".cerv"; + public const string Updated = "Updated"; + public const string Created = "Created"; + public const string Deleted = "Deleted"; + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/ICompoundResourceDeploymentItem.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/ICompoundResourceDeploymentItem.cs new file mode 100644 index 0000000..c4fcd9f --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/ICompoundResourceDeploymentItem.cs @@ -0,0 +1,130 @@ +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.ModuleTemplate.Authoring.Core.Model +{ + public interface ICompoundResourceDeploymentItem : IDeploymentItem, ITypedItem + { + new float Progress { get; set; } + + //TODO: Rename to match your model (e.g. script, entry, pool, etc) + List Items { get; set; } + } + + public class CompoundResourceDeploymentItem : ICompoundResourceDeploymentItem + { + internal const string CompoundResourceTypeName = "ModuleTemplate Compound Resource"; + float m_Progress; + DeploymentStatus m_Status; + string m_Path; + + public CompoundResourceDeploymentItem(string path) + { + Name = System.IO.Path.GetFileName(path); + Path = path; + Type = CompoundResourceTypeName; + Items = new List(); + } + + /// + /// Name of the item as shown for user feedback, normally file_name.ext + /// + public string Type { get; } + + public string Name { get; } + public string Path + { + get => m_Path; + set => SetField(ref m_Path, value); + } + + public float Progress + { + get => m_Progress; + set => SetField(ref m_Progress, value); + } + + public List Items { get; set; } + + public DeploymentStatus Status + { + get => m_Status; + set => SetField(ref m_Status, value); + } + + public ObservableCollection States { get; } = new(); + + 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; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + onFieldChanged?.Invoke(field); + } + } + + public interface INestedResourceDeploymentItem : IResourceDeploymentItem + { + public ICompoundResourceDeploymentItem Parent { get; set; } + } + + public class NestedResourceDeploymentItem : SimpleResourceDeploymentItem, INestedResourceDeploymentItem + { + internal const string CompoundResourceTypeName = "ModuleTemplate Resource Entry"; + readonly string m_Id; + public NestedResourceDeploymentItem(string path, IResource resource) : base(path) + { + Resource = resource; + m_Id = resource.Id; + } + + public NestedResourceDeploymentItem(ICompoundResourceDeploymentItem parent, IResource resource) : base(parent.Path) + { + Parent = parent; + Resource = resource; + m_Id = resource.Id; + } + + public override string Name => Resource?.Id ?? m_Id; + public ICompoundResourceDeploymentItem Parent { get; set; } + + /// + /// Name of the item as shown for user feedback, normally file_name.ext + /// + public override string Type => CompoundResourceTypeName; + + public override string ToString() + { + if (Path == "Remote") + return Name; + return $"{Name} in '{Path}'"; + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/IResource.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/IResource.cs index 36b93ce..b23bde5 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/IResource.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/IResource.cs @@ -1,10 +1,34 @@ +using System.Runtime.Serialization; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.ModuleTemplate.Authoring.Core.Model { - public interface IResource : IDeploymentItem, ITypedItem + public interface IResource { + //TODO: Try not to leak non-human readable IDs to users (non-human readable like GUIDS, UUIDs) + // Default to using the file as the ID, and allow overrides string Id { get; } + string Name { get; } + + string AStrValue { get; set; } + + NestedObject NestedObj{ get; set; } + } + + public interface IResourceDeploymentItem : IDeploymentItem, ITypedItem + { new float Progress { get; set; } + + //TODO: Rename to match your model (e.g. script, entry, pool, etc) + IResource Resource { get; set; } + } + + [DataContract] + public class NestedObject + { + [DataMember] + public bool NestedObjectBoolean { get; set; } + [DataMember] + public string NestedObjectString { get; set; } } } diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/SimpleResource.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/SimpleResource.cs new file mode 100644 index 0000000..4d31545 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/SimpleResource.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.Model +{ + [DataContract] + public class SimpleResource : IResource + { + [DataMember] + public string Id { get; set; } + [DataMember] + public string Name { get; set; } + [DataMember] + public string AStrValue { get; set; } + [DataMember] + public NestedObject NestedObj { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/SimpleResourceDeploymentItem.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/SimpleResourceDeploymentItem.cs new file mode 100644 index 0000000..57a4d80 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/SimpleResourceDeploymentItem.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.ModuleTemplate.Authoring.Core.Model +{ + [DataContract] + public class SimpleResourceDeploymentItem : IResourceDeploymentItem + { + internal const string SimpleResourceTypeName = "ModuleTemplate Simple Resource"; + float m_Progress; + DeploymentStatus m_Status; + string m_Path; + + public SimpleResourceDeploymentItem(string path) + { + Name = System.IO.Path.GetFileName(path); + Path = path; + } + + /// + /// Name of the item as shown for user feedback, normally file_name.ext + /// + public virtual string Type => SimpleResourceTypeName; + + public virtual string Name { get; } + + public string Path + { + get => m_Path; + set => SetField(ref m_Path, value); + } + + public float Progress + { + get => m_Progress; + set => SetField(ref m_Progress, value); + } + + public IResource Resource { get; set; } + + public DeploymentStatus Status + { + get => m_Status; + set => SetField(ref m_Status, value); + } + + public ObservableCollection States { get; } = new(); + + public override string ToString() + { + if (Path == "Remote") + return Resource.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; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + onFieldChanged?.Invoke(field); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/Statuses.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/Statuses.cs index 11d03e7..4fb7272 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/Statuses.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Model/Statuses.cs @@ -1,20 +1,50 @@ +using System; using Unity.Services.DeploymentApi.Editor; namespace Unity.Services.ModuleTemplate.Authoring.Core.Model { - static class Statuses + public 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 GetFetched(string detail) => new ("Fetched", detail, 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); + public static DeploymentStatus GetDeploying(string details = null) + => new ( "Deploying", details ?? string.Empty, SeverityLevel.Info); + public static DeploymentStatus GetDeployed(string details) + => new ("Deployed", details, SeverityLevel.Success); + + public static DeploymentStatus GetFailedToLoad(Exception e, string path) + => new ("Failed to load", $"Failed to load '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetFailedToRead(Exception e, string path) + => new ("Failed to read", $"Failed to read '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetFailedToWrite(Exception e, string path) + => new ("Failed to write", $"Failed to write '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetFailedToSerialize(Exception e, string path) + => new ("Failed to serialize", $"Failed to serialize '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetFailedToDelete(Exception e, string path) + => new ("Failed to serialize", $"Failed to delete '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetPartialDeploy(string details) + => new DeploymentStatus( + "Partially deployed", + details ?? "Some items were not successfully deployed, see sub-items for details", + SeverityLevel.Warning); + + public static DeploymentStatus GetPartialFetch(string details) + => new DeploymentStatus( + "Partially deployed", + details ?? "Some items were not successfully fetched, see sub-items for details", + SeverityLevel.Warning); } } diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Service/IModuleTemplateClient.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Service/IModuleTemplateClient.cs index 8f4e0c5..6891e50 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Service/IModuleTemplateClient.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Service/IModuleTemplateClient.cs @@ -8,12 +8,13 @@ namespace Unity.Services.ModuleTemplate.Authoring.Core.Service //This is a sample IServiceClient and might not map to your existing admin APIs public interface IModuleTemplateClient { - void Initialize(string environmentId, string projectId, CancellationToken cancellationToken); + Task Initialize(string environmentId, string projectId, CancellationToken cancellationToken); - Task Get(string name); - Task Update(IResource resource); - Task Create(IResource resource); - Task Delete(IResource resource); - Task> List(); + Task Get(string id, CancellationToken cancellationToken); + Task Update(IResource resource, CancellationToken cancellationToken); + Task Create(IResource resource, CancellationToken cancellationToken); + Task Delete(IResource resource, CancellationToken cancellationToken); + Task> List(CancellationToken cancellationToken); + Task RawGetRequest(string address, CancellationToken cancellationToken = default); } } diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Unity.Services.ModuleTemplate.Authoring.Core.csproj b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Unity.Services.ModuleTemplate.Authoring.Core.csproj index 746aef6..35b7f71 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Unity.Services.ModuleTemplate.Authoring.Core.csproj +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Unity.Services.ModuleTemplate.Authoring.Core.csproj @@ -1,6 +1,6 @@ - net5.0 + net6.0 disable 0.0.1 disable @@ -13,6 +13,8 @@ <_Parameter1>$(AssemblyName).UnitTest + <_Parameter1>Unity.Services.Cli.ModuleTemplate + <_Parameter1>Unity.Services.Cli.ModuleTemplate.UnitTest <_Parameter1>DynamicProxyGenAssembly2 diff --git a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Validations/DuplicateResourceValidation.cs b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Validations/DuplicateResourceValidation.cs index a64171a..fd46a66 100644 --- a/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Validations/DuplicateResourceValidation.cs +++ b/Unity.Services.Cli/Unity.Services.ModuleTemplate.Authoring.Core/Validations/DuplicateResourceValidation.cs @@ -7,25 +7,27 @@ namespace Unity.Services.ModuleTemplate.Authoring.Core.Validations { static class DuplicateResourceValidation { - public static IReadOnlyList FilterDuplicateResources( - IReadOnlyList resources, - out IReadOnlyList> duplicateGroups) + public static List FilterDuplicateResources( + IReadOnlyList resources, + out IReadOnlyList> duplicateGroups) + where T: IResourceDeploymentItem { + //TODO: Revisit this to use name, or whatever ID is appropriate for your implementation duplicateGroups = resources - .GroupBy(r => r.Id) + .GroupBy(r => r.Resource.Id) .Where(g => g.Count() > 1) .ToList(); var hashset = new HashSet(duplicateGroups.Select(g => g.Key)); return resources - .Where(r => !hashset.Contains(r.Id)) + .Where(r => !hashset.Contains(r.Resource.Id)) .ToList(); } public static (string, string) GetDuplicateResourceErrorMessages( - IResource targetResource, - IReadOnlyList group) + IResourceDeploymentItem targetResource, + IReadOnlyList group) { var duplicates = group .Except(new[] { targetResource }) @@ -33,7 +35,7 @@ public static (string, string) GetDuplicateResourceErrorMessages( var duplicatesStr = string.Join(", ", duplicates.Select(d => $"'{d.Path}'")); var shortMessage = $"'{targetResource.Path}' was found duplicated in other files: {duplicatesStr}"; - var message = $"Multiple resources with the same identifier '{targetResource.Id}' were found. " + var message = $"Multiple resources with the same identifier '{targetResource.Resource.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; diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Batching/Batching.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Batching/Batching.cs new file mode 100644 index 0000000..9068292 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Batching/Batching.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Unity.Services.Scheduler.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."; + + /// + /// Asynchronously execute a collection of delegates in batches with delay between them + /// + /// IEnumerable of the delegates you want to run in batches + /// Callback that will be invoked when a delegate has finished executing + /// + /// Size of the batches + /// Delay in seconds between batches + /// Exception thrown when a delegate throws an exception + /// You need to handle the AggregateException's innerExceptions (that's where you'll get + /// the exceptions related to the individual batch items executed) + public static async Task ExecuteInBatchesAsync( + IEnumerable> delegates, + CancellationToken cancellationToken, + int batchSize = k_BatchSize, + double secondsDelay = k_SecondsDelay) + { + var newDelegates = new List>>(); + + foreach (var del in delegates) + { + newDelegates.Add( + async () => + { + await Task.Run(del, cancellationToken); + return 0; + }); + + if (cancellationToken.IsCancellationRequested) + { + return; + } + } + + await ExecuteInBatchesAsync( + newDelegates, + cancellationToken, + batchSize, + secondsDelay); + } + + /// + /// Asynchronously execute a collection of delegates in batches with delay between them + /// + /// IEnumerable of the delegates you want to run in batches + /// Callback that will be invoked when a delegate has finished executing + /// + /// Size of the batches + /// Delay in seconds between batches + /// The return value type of your delegates + /// A collection of results + /// Exception thrown when a delegate throws an exception + /// You need to handle the AggregateException's innerExceptions (that's where you'll get + /// the exceptions related to the individual batch items executed) + public static async Task> ExecuteInBatchesAsync( + IReadOnlyList>> delegates, + CancellationToken cancellationToken, + int batchSize = k_BatchSize, + double secondsDelay = k_SecondsDelay) + { + var exceptions = new ConcurrentQueue(); + var batchesResult = new ConcurrentBag(); + var chunks = delegates.Chunk(batchSize).ToList(); + + for (int i = 0; i < chunks.Count; i++) + { + try + { + var batchResult = await ExecuteBatchAsync(chunks[i], cancellationToken); + foreach (var result in batchResult) + { + batchesResult.Add(result); + } + } + catch (AggregateException e) + { + foreach (var innerException in e.InnerExceptions) + { + exceptions.Enqueue(innerException); + } + } + + if (i + 1 != chunks.Count) + { + await Task.Delay(TimeSpan.FromSeconds(secondsDelay), cancellationToken); + } + + if (cancellationToken.IsCancellationRequested) + { + break; + } + } + + if (!exceptions.IsEmpty) + { + throw new AggregateException(k_BatchingExceptionMessage, exceptions.ToList()); + } + + return batchesResult.ToList(); + } + + 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>> delegates, + CancellationToken cancellationToken) + { + var exceptions = new ConcurrentQueue(); + var batchesResult = new ConcurrentBag(); + var tasks = new ConcurrentBag(); + + Parallel.ForEach( + delegates, + del => + { + var task = Task.Run( + async () => + { + try + { + var result = await Task.Run(del, cancellationToken); + batchesResult.Add(result); + } + catch (Exception e) + { + exceptions.Enqueue(e); + } + }, + cancellationToken); + + tasks.Add(task); + }); + + await Task.WhenAll(tasks); + + if (!exceptions.IsEmpty) + { + throw new AggregateException(k_BatchingExceptionMessage, exceptions.ToList()); + } + + return batchesResult.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(); + } + + // Copy/pasted utility code from the Enumerable.Chunk method available in dotnet 5 + static IEnumerable ChunkIterator(IEnumerable source, int size) + { + using IEnumerator e = source.GetEnumerator(); + while (e.MoveNext()) + { + TSource[] chunk = new TSource[size]; + chunk[0] = e.Current; + + int i = 1; + for (; i < chunk.Length && e.MoveNext(); i++) + { + chunk[i] = e.Current; + } + + if (i == chunk.Length) + { + yield return chunk; + } + else + { + Array.Resize(ref chunk, i); + yield return chunk; + yield break; + } + } + } + + static IEnumerable Chunk(this IEnumerable source, int size) + => ChunkIterator(source, size); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Deploy/DeployResult.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Deploy/DeployResult.cs new file mode 100644 index 0000000..2759fe7 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Deploy/DeployResult.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Scheduler.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.Scheduler.Authoring.Core/Deploy/IScheduleDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Deploy/IScheduleDeploymentHandler.cs new file mode 100644 index 0000000..3413ac0 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Deploy/IScheduleDeploymentHandler.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Scheduler.Authoring.Core.Deploy +{ + public interface IScheduleDeploymentHandler + { + Task DeployAsync(IReadOnlyList localResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Deploy/SchedulerDeploymentHandler.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Deploy/SchedulerDeploymentHandler.cs new file mode 100644 index 0000000..f29cbb9 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Deploy/SchedulerDeploymentHandler.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.DeploymentApi.Editor; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Service; +using Unity.Services.Scheduler.Authoring.Core.Validations; + +namespace Unity.Services.Scheduler.Authoring.Core.Deploy +{ + public class SchedulerDeploymentHandler : IScheduleDeploymentHandler + { + readonly ISchedulerClient m_Client; + readonly object m_ResultLock = new(); + + public SchedulerDeploymentHandler(ISchedulerClient 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(); + + var toCreate = localResources + .Except(remoteResources, new ScheduleComparer()) + .ToList(); + + var toUpdate = localResources + .Except(toCreate, new ScheduleComparer()) + .ToList(); + + var toDelete = new List(); + if (reconcile) + { + toDelete = remoteResources + .Except(localResources, new ScheduleComparer()) + .ToList(); + } + + res.Created = toCreate; + res.Deleted = toDelete; + res.Updated = toUpdate; + res.Deployed = new List(); + res.Failed = new List(); + + UpdateDuplicateResourceStatus(res, duplicateGroups); + FindIdsForSchedules(toUpdate, remoteResources, toDelete); + + if (dryRun) + { + return res; + } + + var createTasks = GetTasks(toCreate, m_Client.Create, res, "Created"); + var updateTasks = GetTasks(toUpdate, m_Client.Update, res, "Updated"); + var deleteTasks = reconcile + ? GetTasks(toDelete, m_Client.Delete, res, "Deleted") + : new List(); + + var allTasks = createTasks.Concat(updateTasks).Concat(deleteTasks); + + await Batching.Batching.ExecuteInBatchesAsync(allTasks, token); + + return res; + } + + static void FindIdsForSchedules( + List toUpdate, + IReadOnlyList remoteResources, + List toDelete) + { + foreach (var trigger in toUpdate) + { + trigger.Id = remoteResources.First(t => t.Name == trigger.Name).Id; + } + + foreach (var trigger in toDelete) + { + trigger.Id = remoteResources.First(t => t.Name == trigger.Name).Id; + } + } + + // TODO: Add support for CancellationToken in m_Client.Create, m_Client.Update, m_Client.Delete + IEnumerable GetTasks(List resources, Func func, DeployResult res, string detail) + => resources.Select(i => DeployResource(func, i, res, detail)); + + protected virtual void UpdateStatus( + IScheduleConfig triggerConfig, + DeploymentStatus status) + { + // clients can override this to provide user feedback on progress + triggerConfig.Status = status; + } + + protected virtual void UpdateProgress( + IScheduleConfig triggerConfig, + float progress) + { + // clients can override this to provide user feedback on progress + triggerConfig.Progress = progress; + } + + void UpdateDuplicateResourceStatus( + DeployResult result, + IReadOnlyList> duplicateGroups) + { + foreach (var group in duplicateGroups) + { + foreach (var res in group) + { + result.Failed.Add(res); + result.Created.Remove(res); + result.Updated.Remove(res); + var (message, shortMessage) = DuplicateResourceValidation.GetDuplicateResourceErrorMessages(res, group.ToList()); + UpdateStatus(res, Statuses.GetFailedToDeploy(shortMessage)); + } + } + } + + async Task DeployResource( + Func task, + IScheduleConfig triggerConfig, + DeployResult res, + string detail) + { + try + { + await task(triggerConfig); + lock (m_ResultLock) + res.Deployed.Add(triggerConfig); + UpdateStatus(triggerConfig, Statuses.GetDeployed(detail)); + UpdateProgress(triggerConfig, 100); + } + catch (Exception e) + { + lock (m_ResultLock) + res.Failed.Add(triggerConfig); + UpdateStatus(triggerConfig, Statuses.GetFailedToDeploy(e.Message)); + } + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Fetch/FetchResult.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Fetch/FetchResult.cs new file mode 100644 index 0000000..1ad818b --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Fetch/FetchResult.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Scheduler.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.Scheduler.Authoring.Core/Fetch/IScheduleFetchHandler.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Fetch/IScheduleFetchHandler.cs new file mode 100644 index 0000000..4687135 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Fetch/IScheduleFetchHandler.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Scheduler.Authoring.Core.Fetch +{ + public interface IScheduleFetchHandler + { + public Task FetchAsync( + string rootDirectory, + IReadOnlyList localResources, + bool dryRun = false, + bool reconcile = false, + CancellationToken token = default); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Fetch/SchedulerFetchHandler.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Fetch/SchedulerFetchHandler.cs new file mode 100644 index 0000000..bacd930 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Fetch/SchedulerFetchHandler.cs @@ -0,0 +1,172 @@ +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.Scheduler.Authoring.Core.IO; +using Unity.Services.Scheduler.Authoring.Core.Model; +using Unity.Services.Scheduler.Authoring.Core.Serialization; +using Unity.Services.Scheduler.Authoring.Core.Service; +using Unity.Services.Scheduler.Authoring.Core.Validations; + +namespace Unity.Services.Scheduler.Authoring.Core.Fetch +{ + public class SchedulerFetchHandler : IScheduleFetchHandler + { + readonly ISchedulerClient m_Client; + readonly IFileSystem m_FileSystem; + readonly ISchedulesSerializer m_ScheduleSerializer; + + public SchedulerFetchHandler( + ISchedulerClient client, + IFileSystem fileSystem, + ISchedulesSerializer scheduleSerializer) + { + m_Client = client; + m_FileSystem = fileSystem; + m_ScheduleSerializer = scheduleSerializer; + } + + 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(); + + var toUpdate = localResources + .Intersect(remoteResources, new ScheduleComparer()) + .ToList(); + + var toDelete = localResources + .Except(remoteResources, new ScheduleComparer()) + .ToList(); + + var toCreate = new List(); + if (reconcile) + { + toCreate = remoteResources + .Except(localResources, new ScheduleComparer()) + .ToList(); + toCreate.ForEach(r => r.Path = Path.Combine(rootDirectory, r.Name) + ".sched"); + } + + 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<(IScheduleConfig, Task)>(); + var deleteTasks = new List<(IScheduleConfig, Task)>(); + var createTasks = new List<(IScheduleConfig, Task)>(); + + var updatedFiles = toUpdate.GroupBy(r => r.Path); + foreach (var file in updatedFiles) + { + var task = m_FileSystem.WriteAllText( + file.Key, + m_ScheduleSerializer.Serialize(remoteResources.Intersect(file.ToList(), new ScheduleComparer()).ToList()), + token); + file.ToList().ForEach(f => updateTasks.Add((f, task))); + } + + var filesToDelete = toDelete.GroupBy(t => t.Path) + .ExceptBy(updatedFiles.Select(f => f.Key), + g => g.Key); + foreach (var file in filesToDelete) + { + var task = m_FileSystem.Delete( + file.Key, + token); + file.ToList().ForEach(f => deleteTasks.Add((f, task))); + } + + if (reconcile) + { + foreach (var resource in toCreate) + { + var task = m_FileSystem.WriteAllText( + resource.Path, + m_ScheduleSerializer.Serialize(new List(){resource}), + token); + createTasks.Add((resource, task)); + } + } + + await UpdateResult(updateTasks, res, "Updated"); + await UpdateResult(deleteTasks, res, "Deleted"); + await UpdateResult(createTasks, res, "Created"); + + return res; + } + + protected virtual void UpdateStatus( + IScheduleConfig triggerConfig, + DeploymentStatus status) + { + // clients can override this to provide user feedback on progress + triggerConfig.Status = status; + } + + protected virtual void UpdateProgress( + IScheduleConfig triggerConfig, + float progress) + { + // clients can override this to provide user feedback on progress + triggerConfig.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<(IScheduleConfig, Task)> tasks, + FetchResult res, + string detail) + { + foreach (var (resource, task) in tasks) + { + try + { + await task; + res.Fetched.Add(resource); + UpdateStatus(resource, Statuses.GetFetched(detail)); + UpdateProgress(resource, 100); + } + catch (Exception e) + { + res.Failed.Add(resource); + UpdateStatus(resource, Statuses.GetFailedToFetch(e.Message)); + } + } + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/IO/IFileSystem.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/IO/IFileSystem.cs new file mode 100644 index 0000000..a7e4840 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/IO/IFileSystem.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Unity.Services.Scheduler.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.Scheduler.Authoring.Core/Model/IScheduleConfig.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/IScheduleConfig.cs new file mode 100644 index 0000000..5548640 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/IScheduleConfig.cs @@ -0,0 +1,17 @@ +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Scheduler.Authoring.Core.Model +{ + public interface IScheduleConfig : IDeploymentItem, ITypedItem + { + string Id { get; set; } + string EventName { get; } + string ScheduleType { get; } + string Schedule { get; } + int PayloadVersion { get; } + string Payload { get; } + + new float Progress { get; set; } + new string Path { get; set; } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/ScheduleComparer.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/ScheduleComparer.cs new file mode 100644 index 0000000..8db46d4 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/ScheduleComparer.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace Unity.Services.Scheduler.Authoring.Core.Model +{ + public class ScheduleComparer : IEqualityComparer + { + public bool Equals(IScheduleConfig x, IScheduleConfig 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.Name == y.Name; + } + + public int GetHashCode(IScheduleConfig obj) + { + return (obj.Name != null ? obj.Name.GetHashCode() : 0); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/ScheduleConfig.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/ScheduleConfig.cs new file mode 100644 index 0000000..477c231 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/ScheduleConfig.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using Unity.Services.DeploymentApi.Editor; + +namespace Unity.Services.Scheduler.Authoring.Core.Model +{ + [DataContract] + public class ScheduleConfig : IScheduleConfig + { + public string Id { get; set; } + public string Name { get; set; } + + [DataMember] + public string EventName { get; set; } + + [DataMember(Name = "Type")] + public string ScheduleType { get; set; } + + [DataMember] + public string Schedule { get; set; } + + [DataMember] + public int PayloadVersion { get; set; } + + [DataMember] + public string Payload { get; set; } + + public event PropertyChangedEventHandler PropertyChanged; + + float IScheduleConfig.Progress + { + get => Progress; + set => Progress = value; + } + + public string Path { get; set; } + public float Progress { get; set; } + public DeploymentStatus Status { get; set; } + public ObservableCollection States { get; set; } + + // Explicit interface implementation is needed to avoid conflicting with serialization of ScheduleType property + string ITypedItem.Type => "Schedule"; + + public ScheduleConfig() + { + + } + + public ScheduleConfig( + string name, + string eventName, + string scheduleType, + string schedule, + int payloadVersion, + string payload) + { + EventName = eventName; + ScheduleType = scheduleType; + Schedule = schedule; + PayloadVersion = payloadVersion; + Payload = payload; + Name = name; + } + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } + + public override string ToString() + { + if (Path == "Remote") + return Name; + return $"'{Name}' in '{Path}'"; + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/Statuses.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/Statuses.cs new file mode 100644 index 0000000..6d4c7aa --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Model/Statuses.cs @@ -0,0 +1,38 @@ +using System; +using Unity.Services.DeploymentApi.Editor; + + +namespace Unity.Services.Scheduler.Authoring.Core.Model +{ + public 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 DeploymentStatus GetFetched(string detail) => new ("Fetched", detail, SeverityLevel.Success); + + public static DeploymentStatus GetFailedToDeploy(string details) + => new ("Failed to deploy", details, SeverityLevel.Error); + public static DeploymentStatus GetDeploying(string details = null) + => new ( "Deploying", details ?? string.Empty, SeverityLevel.Info); + public static DeploymentStatus GetDeployed(string details) + => new ("Deployed", details, SeverityLevel.Success); + + public static DeploymentStatus GetFailedToLoad(Exception e, string path) + => new ("Failed to load", $"Failed to load '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetFailedToRead(Exception e, string path) + => new ("Failed to read", $"Failed to read '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetFailedToWrite(Exception e, string path) + => new ("Failed to write", $"Failed to write '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetFailedToSerialize(Exception e, string path) + => new ("Failed to serialize", $"Failed to serialize '{path}'. Reason: {e.Message}", SeverityLevel.Error); + + public static DeploymentStatus GetFailedToDelete(Exception e, string path) + => new ("Failed to serialize", $"Failed to delete '{path}'. Reason: {e.Message}", SeverityLevel.Error); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Serialization/ISchedulesSerializer.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Serialization/ISchedulesSerializer.cs new file mode 100644 index 0000000..643ca1b --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Serialization/ISchedulesSerializer.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Scheduler.Authoring.Core.Serialization +{ + public interface ISchedulesSerializer + { + string Serialize(IList config); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Service/ISchedulerClient.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Service/ISchedulerClient.cs new file mode 100644 index 0000000..62bae77 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Service/ISchedulerClient.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Scheduler.Authoring.Core.Service +{ + //This is a sample IServiceClient and might not map to your existing admin APIs + public interface ISchedulerClient + { + Task Initialize(string environmentId, string projectId, CancellationToken cancellationToken); + + Task Get(string id); + Task Update(IScheduleConfig resource); + Task Create(IScheduleConfig resource); + Task Delete(IScheduleConfig resource); + Task> List(); + } +} diff --git a/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Unity.Services.Scheduler.Authoring.Core.csproj b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Unity.Services.Scheduler.Authoring.Core.csproj new file mode 100644 index 0000000..9087581 --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Unity.Services.Scheduler.Authoring.Core.csproj @@ -0,0 +1,24 @@ + + + net6.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.Scheduler.Authoring.Core/Validations/DuplicateResourceValidation.cs b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Validations/DuplicateResourceValidation.cs new file mode 100644 index 0000000..d598e8c --- /dev/null +++ b/Unity.Services.Cli/Unity.Services.Scheduler.Authoring.Core/Validations/DuplicateResourceValidation.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Services.Scheduler.Authoring.Core.Model; + +namespace Unity.Services.Scheduler.Authoring.Core.Validations +{ + static class DuplicateResourceValidation + { + public static IReadOnlyList FilterDuplicateResources( + IReadOnlyList resources, + out IReadOnlyList> duplicateGroups) + { + duplicateGroups = resources + .GroupBy(r => r.Name) + .Where(g => g.Count() > 1) + .ToList(); + + var hashset = new HashSet(duplicateGroups.Select(g => g.Key)); + + return resources + .Where(r => !hashset.Contains(r.Name)) + .ToList(); + } + + public static (string, string) GetDuplicateResourceErrorMessages( + IScheduleConfig targetResource, + IReadOnlyList group) + { + var duplicates = group + .Except(new[] { targetResource }) + .ToList(); + + var duplicatesStr = string.Join(", ", duplicates.Select(d => $"'{d.Path}'")); + var shortMessage = $"'{targetResource.Path}' was found duplicated in other files: {duplicatesStr}"; + var message = $"Multiple resources with the same name '{targetResource.Name}' 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); + } + } +} diff --git a/Unity.Services.Cli/Unity.Services.Triggers.Authoring.Core/Fetch/TriggersFetchHandler.cs b/Unity.Services.Cli/Unity.Services.Triggers.Authoring.Core/Fetch/TriggersFetchHandler.cs index 001fc24..7d1102b 100644 --- a/Unity.Services.Cli/Unity.Services.Triggers.Authoring.Core/Fetch/TriggersFetchHandler.cs +++ b/Unity.Services.Cli/Unity.Services.Triggers.Authoring.Core/Fetch/TriggersFetchHandler.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Threading.Tasks; using Unity.Services.DeploymentApi.Editor; -using Unity.Services.Triggers.Authoring.Core.Deploy; using Unity.Services.Triggers.Authoring.Core.IO; using Unity.Services.Triggers.Authoring.Core.Model; using Unity.Services.Triggers.Authoring.Core.Serialization; @@ -18,16 +17,16 @@ public class TriggersFetchHandler : ITriggersFetchHandler { readonly ITriggersClient m_Client; readonly IFileSystem m_FileSystem; - readonly ITriggersSerializer m_LeaderboardsSerializer; + readonly ITriggersSerializer m_TriggersSerializer; public TriggersFetchHandler( ITriggersClient client, IFileSystem fileSystem, - ITriggersSerializer leaderboardsSerializer) + ITriggersSerializer triggersSerializer) { m_Client = client; m_FileSystem = fileSystem; - m_LeaderboardsSerializer = leaderboardsSerializer; + m_TriggersSerializer = triggersSerializer; } public async Task FetchAsync(string rootDirectory, @@ -77,12 +76,12 @@ public async Task FetchAsync(string rootDirectory, var deleteTasks = new List<(ITriggerConfig, Task)>(); var createTasks = new List<(ITriggerConfig, Task)>(); - var updatedFiles = toUpdate.GroupBy(r => r.Path); + var updatedFiles = toUpdate.GroupBy(r => r.Path).ToList(); foreach (var file in updatedFiles) { var task = m_FileSystem.WriteAllText( file.Key, - m_LeaderboardsSerializer.Serialize(file.ToList()), + m_TriggersSerializer.Serialize(remoteResources.Intersect(file.ToList(), new TriggerComparer()).ToList()), token); file.ToList().ForEach(f => updateTasks.Add((f, task))); } @@ -104,7 +103,7 @@ public async Task FetchAsync(string rootDirectory, { var task = m_FileSystem.WriteAllText( resource.Path, - m_LeaderboardsSerializer.Serialize(new List(){resource}), + m_TriggersSerializer.Serialize(new List(){ resource }), token); createTasks.Add((resource, task)); }