diff --git a/src/Crowdin.Api/Applications/Application.cs b/src/Crowdin.Api/Applications/Application.cs new file mode 100644 index 00000000..9e551e62 --- /dev/null +++ b/src/Crowdin.Api/Applications/Application.cs @@ -0,0 +1,49 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; + +namespace Crowdin.Api.Applications +{ + [PublicAPI] + public class Application + { + private Application() { } + + [JsonProperty("identifier")] + public string Identifier { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("logo")] + public string Logo { get; set; } + + [JsonProperty("baseUrl")] + public string BaseUrl { get; set; } + + [JsonProperty("manifestUrl")] + public string ManifestUrl { get; set; } + + [JsonProperty("scopes")] + public string[] Scopes { get; set; } + + [JsonProperty("modules")] + public ApplicationModule[] Modules { get; set; } + + [JsonProperty("createdAt")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonProperty("permissions")] + public ApplicationPermissions Permissions { get; set; } + + [JsonProperty("defaultPermissions")] + public JObject DefaultPermissions { get; set; } + + [JsonProperty("limitReached")] + public bool LimitReached { get; set; } + } +} diff --git a/src/Crowdin.Api/Applications/ApplicationModule.cs b/src/Crowdin.Api/Applications/ApplicationModule.cs new file mode 100644 index 00000000..3b868248 --- /dev/null +++ b/src/Crowdin.Api/Applications/ApplicationModule.cs @@ -0,0 +1,22 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Crowdin.Api.Applications +{ + [PublicAPI] + public class ApplicationModule + { + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("data")] + public JObject Data { get; set; } + + [JsonProperty("authenticationType")] + public string AuthenticationType { get; set; } + } +} diff --git a/src/Crowdin.Api/Applications/ApplicationPermission.cs b/src/Crowdin.Api/Applications/ApplicationPermission.cs new file mode 100644 index 00000000..777c53be --- /dev/null +++ b/src/Crowdin.Api/Applications/ApplicationPermission.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; +using System; + +namespace Crowdin.Api.Applications +{ + [PublicAPI] + public class ApplicationPermissions + { + [JsonProperty("user")] + public ApplicationUser User { get; set; } + + [JsonProperty("project")] + public ApplicationProject Project { get; set; } + + } +} diff --git a/src/Crowdin.Api/Applications/ApplicationProject.cs b/src/Crowdin.Api/Applications/ApplicationProject.cs new file mode 100644 index 00000000..2dd778f0 --- /dev/null +++ b/src/Crowdin.Api/Applications/ApplicationProject.cs @@ -0,0 +1,26 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Crowdin.Api.Applications +{ + [PublicAPI] + public class ApplicationProject + { + [JsonProperty("value")] + public ApplicationProjectValue Value { get; set; } + + [JsonProperty("ids")] + public ICollection Ids { get; set; } + } + + [PublicAPI] + public enum ApplicationProjectValue + { + [Description("own")] + Own, + [Description("restricted")] + Restricted + } +} diff --git a/src/Crowdin.Api/Applications/ApplicationUser.cs b/src/Crowdin.Api/Applications/ApplicationUser.cs new file mode 100644 index 00000000..a91cd8df --- /dev/null +++ b/src/Crowdin.Api/Applications/ApplicationUser.cs @@ -0,0 +1,36 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Crowdin.Api.Applications +{ + [PublicAPI] + public class ApplicationUser + { + [JsonProperty("value")] + public ApplicationUserValue Value { get; set; } + + [JsonProperty("ids")] + public ICollection Ids { get; set; } + } + + [PublicAPI] + public enum ApplicationUserValue + { + [Description("owner")] + Owner, + + [Description("managers")] + Managers, + + [Description("all")] + All, + + [Description("guests")] + Guests, + + [Description("restricted")] + Restricted + } +} diff --git a/src/Crowdin.Api/Applications/ApplicationsApiExecutor.cs b/src/Crowdin.Api/Applications/ApplicationsApiExecutor.cs index f39b55f3..62eb99d7 100644 --- a/src/Crowdin.Api/Applications/ApplicationsApiExecutor.cs +++ b/src/Crowdin.Api/Applications/ApplicationsApiExecutor.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; using Crowdin.Api.Core; using JetBrains.Annotations; using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; namespace Crowdin.Api.Applications { @@ -11,6 +11,7 @@ public class ApplicationsApiExecutor { private readonly ICrowdinApiClient _apiClient; private readonly IJsonParser _jsonParser; + private const string ApplicationsInstallationsUrl = "/applications/installations"; public ApplicationsApiExecutor(ICrowdinApiClient apiClient) { @@ -24,6 +25,72 @@ public ApplicationsApiExecutor(ICrowdinApiClient apiClient, IJsonParser jsonPars _jsonParser = jsonParser; } + /// + /// Get Application Installations List. Documentation: + /// Crowdin API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task> ListApplicationInstallations(int limit = 25, int offset = 0) + { + IDictionary queryParams = Utils.CreateQueryParamsFromPaging(limit, offset); + CrowdinApiResult result = await _apiClient.SendGetRequest(ApplicationsInstallationsUrl, queryParams); + return _jsonParser.ParseResponseList(result.JsonObject); + } + + /// + /// Get Application Installation. Documentation: + /// Crowdin API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task GetApplicationInstallation(string applicationIdentifier) + { + string url = FormUrl_ApplicationsInstallations(applicationIdentifier); + CrowdinApiResult result = await _apiClient.SendGetRequest(url); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + + /// + /// Install Application. Documentation: + /// Crowdin API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task InstallApplication(InstallApplicationRequest request) + { + CrowdinApiResult result = await _apiClient.SendPostRequest(ApplicationsInstallationsUrl, request); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + + /// + /// Delete Application Installation. Documentation: + /// Crowdin API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task DeleteApplicationInstallation(string applicationIdentifier, bool force = false) + { + string url = FormUrl_ApplicationsInstallations(applicationIdentifier); + + IDictionary queryParams = new Dictionary { { "force", force.ToString() } }; + HttpStatusCode statusCode = await _apiClient.SendDeleteRequest(url, queryParams); + Utils.ThrowIfStatusNot204(statusCode, $"Application {applicationIdentifier} installation removal failed"); + } + + /// + /// Edit Application Installation. Documentation: + /// Crowdin API + /// Crowdin Enterprise API + /// + [PublicAPI] + public async Task EditApplicationInstallation(string applicationIdentifier, IEnumerable patches) + { + string url = FormUrl_ApplicationsInstallations(applicationIdentifier); + CrowdinApiResult result = await _apiClient.SendPatchRequest(url, patches); + return _jsonParser.ParseResponseObject(result.JsonObject); + } + /// /// Get Application Data. Documentation: /// Crowdin API @@ -72,7 +139,7 @@ public async Task AddApplicationData(string applicationIdentifier, stri public async Task DeleteApplicationData(string applicationIdentifier, string path) { string url = FormUrl_Applications(applicationIdentifier, path); - HttpStatusCode statusCode = await _apiClient.SendDeleteRequest(url); + HttpStatusCode statusCode = await _apiClient.SendDeleteRequest(url); Utils.ThrowIfStatusNot204(statusCode, $"Application {applicationIdentifier} data removal failed"); } @@ -93,5 +160,9 @@ private string FormUrl_Applications(string applicationIdentifier, string path) { return $"/applications/{applicationIdentifier}/api/{path}"; } + private string FormUrl_ApplicationsInstallations(string applicationIdentifier) + { + return $"{ApplicationsInstallationsUrl}/{applicationIdentifier}"; + } } } \ No newline at end of file diff --git a/src/Crowdin.Api/Applications/InstallApplicationRequest.cs b/src/Crowdin.Api/Applications/InstallApplicationRequest.cs new file mode 100644 index 00000000..8cf13919 --- /dev/null +++ b/src/Crowdin.Api/Applications/InstallApplicationRequest.cs @@ -0,0 +1,19 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; + +#nullable enable + +namespace Crowdin.Api.Applications +{ + [PublicAPI] + public class InstallApplicationRequest + { + [JsonProperty("url")] +#pragma warning disable CS8618 + public string Url { get; set; } +#pragma warning restore CS8618 + + [JsonProperty("permissions")] + public ApplicationPermissions? Permissions { get; set; } + } +} diff --git a/src/Crowdin.Api/Applications/InstallationPatch.cs b/src/Crowdin.Api/Applications/InstallationPatch.cs new file mode 100644 index 00000000..60c5c777 --- /dev/null +++ b/src/Crowdin.Api/Applications/InstallationPatch.cs @@ -0,0 +1,20 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; +using System.ComponentModel; + +namespace Crowdin.Api.Applications +{ + [PublicAPI] + public class InstallationPatch: PatchEntry + { + [JsonProperty("path")] + public InstallationPatchPath Path { get; set; } + } + + [PublicAPI] + public enum InstallationPatchPath + { + [Description("/permissions")] + Permissions + } +} \ No newline at end of file diff --git a/tests/Crowdin.Api.Tests/Applications/ApplicationsInstallationsApiTests.cs b/tests/Crowdin.Api.Tests/Applications/ApplicationsInstallationsApiTests.cs new file mode 100644 index 00000000..7d54d917 --- /dev/null +++ b/tests/Crowdin.Api.Tests/Applications/ApplicationsInstallationsApiTests.cs @@ -0,0 +1,176 @@ +using Crowdin.Api.Applications; +using Crowdin.Api.Core; +using Crowdin.Api.Tests.Core; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Xunit; + +namespace Crowdin.Api.Tests.Applications +{ + public class ApplicationsInstallationsApiTests + { + private const string applicationIdentifier = "test-application"; + private static readonly Mock mockClient = TestUtils.CreateMockClientWithDefaultParser(); + + [Fact] + public async Task GetApplicationInstallation() + { + var url = $"/applications/installations/{applicationIdentifier}"; + mockClient.Setup(client => client.SendGetRequest(url, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(Core.Resources.Applications.GetApplicationInstallation_Response) + }); + var executor = new ApplicationsApiExecutor(mockClient.Object); + Application? response = await executor.GetApplicationInstallation(applicationIdentifier); + Assert_ApplicationInstallation(response); + + } + + [Fact] + public async Task GetApplicationInstallations() + { + var url = "/applications/installations"; + IDictionary queryParams = TestUtils.CreateQueryParamsFromPaging(); + mockClient.Setup(client => client.SendGetRequest(url, queryParams)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(Core.Resources.Applications.ListApplicationInstallation_Response) + }); + var executor = new ApplicationsApiExecutor(mockClient.Object); + var response = await executor.ListApplicationInstallations(); + Assert.NotNull(response); + Assert.Single(response.Data); + Assert_ApplicationInstallation(response.Data[0]); + } + + [Fact] + public async Task InstallApplication() + { + var url = "/applications/installations"; + var permissions = new ApplicationPermissions() + { + User = new() + { + Value = ApplicationUserValue.Restricted, + Ids = new int[] { 1 } + }, + Project = new() + { + Value = ApplicationProjectValue.Restricted, + Ids = new int[] { 1 } + + } + }; + var request = new InstallApplicationRequest { Url = "https://localhost.dev", Permissions = permissions }; + mockClient.Setup(client => client.SendPostRequest(url, request, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(Core.Resources.Applications.GetApplicationInstallation_Response) + }); + var executor = new ApplicationsApiExecutor(mockClient.Object); + var response = await executor.InstallApplication(request); + Assert.NotNull(response); + Assert_ApplicationInstallation(response); + + } + + [Fact] + public async Task EditApplicationInstallation() + { + var permissions = new ApplicationPermissions() + { + User = new() + { + Value = ApplicationUserValue.Restricted, + Ids = new int[] { 1 } + }, + Project = new() + { + Value = ApplicationProjectValue.Restricted, + Ids = new int[] { 1 } + + } + }; + + var patches = new[] + { + new InstallationPatch + { + Operation = PatchOperation.Replace, + Path = InstallationPatchPath.Permissions, + Value = permissions + } + }; + + string actualRequestJson = JsonConvert.SerializeObject(patches, TestUtils.CreateJsonSerializerOptions()); + string expectedRequestJson = TestUtils.CompactJson(Core.Resources.Applications.EditInstallation_Request); + Assert.Equal(expectedRequestJson, actualRequestJson); + + var url = $"/applications/installations/{applicationIdentifier}"; + + mockClient + .Setup(client => client.SendPatchRequest(url, patches, null)) + .ReturnsAsync(new CrowdinApiResult + { + StatusCode = HttpStatusCode.OK, + JsonObject = JObject.Parse(Core.Resources.Applications.GetApplicationInstallation_Response) + }); + + var executor = new ApplicationsApiExecutor(mockClient.Object); + Application? response = await executor.EditApplicationInstallation(applicationIdentifier, patches); + + Assert_ApplicationInstallation(response); + } + + [Fact] + public async Task DeleteApplicationInstallation() + { + var url = $"/applications/installations/{applicationIdentifier}"; + IDictionary queryParams = new Dictionary { { "force", "False" } }; + mockClient.Setup(client => client.SendDeleteRequest(url, queryParams)) + .ReturnsAsync(HttpStatusCode.NoContent); + var executor = new ApplicationsApiExecutor(mockClient.Object); + await executor.DeleteApplicationInstallation(applicationIdentifier); + } + + private void Assert_ApplicationInstallation(Application? application) + { + Assert.NotNull(application); + Assert.Equal("Test Application", application!.Name); + Assert.Equal(applicationIdentifier, application.Identifier); + Assert.Equal("Test Description", application.Description); + Assert.True(application.LimitReached); + Assert.Equal("/logo.png", application.Logo); + Assert.Equal("https://localhost.dev", application.BaseUrl); + Assert.Equal("https://localhost.dev", application.ManifestUrl); + + DateTimeOffset date = DateTimeOffset.Parse("2024-01-13T11:34:40+00:00"); + Assert.Equal(date, application.CreatedAt); + + Assert.NotNull(application.Scopes); + Assert.Single(application.Scopes); + Assert.Equal("project", application.Scopes[0]); + + Assert.NotNull(application.Modules); + Assert.Single(application.Modules); + Assert.Equal("test-application", application.Modules[0].Key); + Assert.Equal("module-type", application.Modules[0].Type); + Assert.Equal("none", application.Modules[0].AuthenticationType); + + Assert.NotNull(application.Permissions); + Assert.Equal(ApplicationUserValue.Restricted, application.Permissions.User.Value); + Assert.Equal(ApplicationProjectValue.Restricted, application.Permissions.Project.Value); + Assert.Single(application.Permissions.User.Ids); + Assert.Single(application.Permissions.User.Ids); + } + } +} diff --git a/tests/Crowdin.Api.Tests/Core/Resources/Applications.Designer.cs b/tests/Crowdin.Api.Tests/Core/Resources/Applications.Designer.cs index 10107cee..a5d9bad5 100644 --- a/tests/Crowdin.Api.Tests/Core/Resources/Applications.Designer.cs +++ b/tests/Crowdin.Api.Tests/Core/Resources/Applications.Designer.cs @@ -1,6 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. +// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -18,7 +19,7 @@ namespace Crowdin.Api.Tests.Core.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Applications { @@ -59,6 +60,34 @@ internal Applications() { } } + /// + /// Looks up a localized string similar to [ + /// { + /// "path": "/permissions", + /// "op": "replace", + /// "value": { + /// "user": { + /// "value": "restricted", + /// "ids": [ + /// 1 + /// ] + /// }, + /// "project": { + /// "value": "restricted", + /// "ids": [ + /// 1 + /// ] + /// } + /// } + /// } + ///]. + /// + internal static string EditInstallation_Request { + get { + return ResourceManager.GetString("EditInstallation_Request", resourceCulture); + } + } + /// /// Looks up a localized string similar to /// { @@ -79,5 +108,64 @@ internal static string GetApplicationData_Response { return ResourceManager.GetString("GetApplicationData_Response", resourceCulture); } } + + /// + /// Looks up a localized string similar to { + /// "data": { + /// "identifier": "test-application", + /// "name": "Test Application", + /// "description": "Test Description", + /// "logo": "/logo.png", + /// "baseUrl": "https://localhost.dev", + /// "manifestUrl": "https://localhost.dev", + /// "createdAt": "2024-01-13T11:34:40+00:00", + /// "modules": [ + /// { + /// "key": "test-application", + /// "type": "module-type", + /// "data": {}, + /// "authenticationType": "none" + /// } + /// ], + /// "scopes": [ + /// "project" + /// ], + /// "permiss [rest of string was truncated]";. + /// + internal static string GetApplicationInstallation_Response { + get { + return ResourceManager.GetString("GetApplicationInstallation_Response", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to { + /// "data": [ + /// { + /// "data": { + /// "identifier": "test-application", + /// "name": "Test Application", + /// "description": "Test Description", + /// "logo": "/logo.png", + /// "baseUrl": "https://localhost.dev", + /// "manifestUrl": "https://localhost.dev", + /// "createdAt": "2024-01-13T11:34:40+00:00", + /// "modules": [ + /// { + /// "key": "test-application", + /// "type": "module-type", + /// "data": {}, + /// "authenticationType": "none" + /// } + /// ], + /// "scopes": [ + /// "project" + /// [rest of string was truncated]";. + /// + internal static string ListApplicationInstallation_Response { + get { + return ResourceManager.GetString("ListApplicationInstallation_Response", resourceCulture); + } + } } } diff --git a/tests/Crowdin.Api.Tests/Core/Resources/Applications.resx b/tests/Crowdin.Api.Tests/Core/Resources/Applications.resx index 36d7360b..94521b1c 100644 --- a/tests/Crowdin.Api.Tests/Core/Resources/Applications.resx +++ b/tests/Crowdin.Api.Tests/Core/Resources/Applications.resx @@ -1,26 +1,146 @@  - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + [ + { + "path": "/permissions", + "op": "replace", + "value": { + "user": { + "value": "restricted", + "ids": [ + 1 + ] + }, + "project": { + "value": "restricted", + "ids": [ + 1 + ] + } + } + } +] + + + { "data": { "identifier": "some_application", @@ -33,6 +153,99 @@ } } - - + + + { + "data": { + "identifier": "test-application", + "name": "Test Application", + "description": "Test Description", + "logo": "/logo.png", + "baseUrl": "https://localhost.dev", + "manifestUrl": "https://localhost.dev", + "createdAt": "2024-01-13T11:34:40+00:00", + "modules": [ + { + "key": "test-application", + "type": "module-type", + "data": {}, + "authenticationType": "none" + } + ], + "scopes": [ + "project" + ], + "permissions": { + "user": { + "value": "restricted", + "ids": [ + 1 + ] + }, + "project": { + "value": "restricted", + "ids": [ + 1 + ] + } + }, + "defaultPermissions": { + "user": "owner", + "project": "own" + }, + "limitReached": true + } +} + + + { + "data": [ + { + "data": { + "identifier": "test-application", + "name": "Test Application", + "description": "Test Description", + "logo": "/logo.png", + "baseUrl": "https://localhost.dev", + "manifestUrl": "https://localhost.dev", + "createdAt": "2024-01-13T11:34:40+00:00", + "modules": [ + { + "key": "test-application", + "type": "module-type", + "data": {}, + "authenticationType": "none" + } + ], + "scopes": [ + "project" + ], + "permissions": { + "user": { + "value": "restricted", + "ids": [ + 1 + ] + }, + "project": { + "value": "restricted", + "ids": [ + 1 + ] + } + }, + "defaultPermissions": { + "user": "owner", + "project": "own" + }, + "limitReached": true + } +} + ], + "pagination": { + "offset": 0, + "limit": 25 + } +} + \ No newline at end of file