From a86cc8138ed463f9da646f2fa9d70219a8f82863 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Fri, 19 Nov 2021 13:42:04 +0000 Subject: [PATCH 01/26] Initial Development commit more work to follow --- .editorconfig | 165 ++++++++++++++++++ .gitignore | 2 + Directory.Build.props | 16 ++ Enclave.Sdk.Api.sln | 47 +++++ src/Enclave.Sdk/Clients/AuthorityClient.cs | 5 + src/Enclave.Sdk/Clients/ClientBase.cs | 85 +++++++++ src/Enclave.Sdk/Clients/DNSClient.cs | 5 + .../Clients/EnrolmentKeysClient.cs | 6 + src/Enclave.Sdk/Clients/LogsClient.cs | 6 + src/Enclave.Sdk/Clients/OrganisationClient.cs | 120 +++++++++++++ src/Enclave.Sdk/Clients/PoliciesClient.cs | 5 + src/Enclave.Sdk/Clients/SystemsClient.cs | 6 + src/Enclave.Sdk/Clients/TagsClient.cs | 6 + .../Clients/UnapprovedSystemsClient.cs | 6 + src/Enclave.Sdk/Data/Builder.cs | 48 +++++ src/Enclave.Sdk/Data/IDataModel.cs | 5 + .../Data/Organisations/AccountOrganisation.cs | 27 +++ .../Data/Organisations/Organisation.cs | 31 ++++ .../Organisations/OrganisationBillingEvent.cs | 10 ++ .../Organisations/OrganisationPlanPricing.cs | 10 ++ .../Data/Organisations/OrganisationPricing.cs | 18 ++ .../Data/Organisations/OrganisationUser.cs | 29 +++ .../Data/Organisations/Quantity.cs | 10 ++ src/Enclave.Sdk/Enclave.Sdk.Api.csproj | 10 ++ src/Enclave.Sdk/EnclaveClient.cs | 52 ++++++ src/Enclave.Sdk/Exceptions/ApiException.cs | 26 +++ .../Enclave.Sdk.Api.Tests.csproj | 22 +++ 27 files changed, 778 insertions(+) create mode 100644 .editorconfig create mode 100644 Directory.Build.props create mode 100644 Enclave.Sdk.Api.sln create mode 100644 src/Enclave.Sdk/Clients/AuthorityClient.cs create mode 100644 src/Enclave.Sdk/Clients/ClientBase.cs create mode 100644 src/Enclave.Sdk/Clients/DNSClient.cs create mode 100644 src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs create mode 100644 src/Enclave.Sdk/Clients/LogsClient.cs create mode 100644 src/Enclave.Sdk/Clients/OrganisationClient.cs create mode 100644 src/Enclave.Sdk/Clients/PoliciesClient.cs create mode 100644 src/Enclave.Sdk/Clients/SystemsClient.cs create mode 100644 src/Enclave.Sdk/Clients/TagsClient.cs create mode 100644 src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs create mode 100644 src/Enclave.Sdk/Data/Builder.cs create mode 100644 src/Enclave.Sdk/Data/IDataModel.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/AccountOrganisation.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/Organisation.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/Quantity.cs create mode 100644 src/Enclave.Sdk/Enclave.Sdk.Api.csproj create mode 100644 src/Enclave.Sdk/EnclaveClient.cs create mode 100644 src/Enclave.Sdk/Exceptions/ApiException.cs create mode 100644 tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0c2b152 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,165 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs + +# All files +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +; Force VS to recommend underscore at the start of created private fields. +[*.cs] +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + +# name all constant fields using UpperCase +dotnet_naming_rule.constant_fields_should_be_upper_case.severity = warning +dotnet_naming_rule.constant_fields_should_be_upper_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_upper_case.style = upper_case_style + +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.upper_case_style.capitalization = pascal_case + +; Avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# IDE0024: Use block body for operators +csharp_style_expression_bodied_operators = when_on_single_line + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = none + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = silent + +# CA1003: Use generic event handler instances +dotnet_diagnostic.CA1003.severity = suggestion + +# CA1056: URI-like properties should not be strings +dotnet_diagnostic.CA1056.severity = suggestion + +# CA1819: Properties should not return arrays +dotnet_diagnostic.CA1819.severity = suggestion + +# CA1724: Type names should not match namespaces +dotnet_diagnostic.CA1724.severity = suggestion + +# CA1030: Use events where appropriate +dotnet_diagnostic.CA1030.severity = silent + +# CA1002: Do not expose generic lists +dotnet_diagnostic.CA1002.severity = suggestion + +# CA2227: Collection properties should be read only +dotnet_diagnostic.CA2227.severity = suggestion + +# CA1014: Mark assemblies with CLSCompliant +dotnet_diagnostic.CA1014.severity = none + +# CA1711: Identifiers should not have incorrect suffix +dotnet_diagnostic.CA1711.severity = suggestion + +# CA1028: Enum Storage should be Int32 +dotnet_diagnostic.CA1028.severity = none + +# CA1826: Don't apply to ordefault methods +dotnet_code_quality.CA1826.exclude_ordefault_methods = true + +# CA1308: Normalize strings to uppercase +dotnet_diagnostic.CA1308.severity = none + +# CA1032: Implement standard exception constructors +dotnet_diagnostic.CA1032.severity = none + +# C0162 exclude extension methods from needing null contract assertions +dotnet_code_quality.CA1062.exclude_extension_method_this_parameter = true + +# CA2225: Operator overloads have named alternates +dotnet_diagnostic.CA2225.severity = none + +# CA2229: Implement serialization constructors +dotnet_diagnostic.CA2229.severity = none + +# CA1508: Avoid dead conditional code - Too many false positives for this to be useful. +dotnet_diagnostic.CA1508.severity = none + +# SA1309: Field names should not begin with underscore +dotnet_diagnostic.SA1309.severity = none + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none + +# SA1623: Property summary documentation should match accessors +dotnet_diagnostic.SA1623.severity = none + +# SA1200: Using directives should be placed correctly +dotnet_diagnostic.SA1200.severity = none + +# SA1633: File should have header +dotnet_diagnostic.SA1633.severity = none + +# SA1116: Split parameters should start on line after declaration +dotnet_diagnostic.SA1116.severity = none + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = none + +# SA1204: Static elements should appear before instance elements +dotnet_diagnostic.SA1204.severity = none + +# CA1721: Property names should not match get methods +dotnet_diagnostic.CA1721.severity = suggestion + +# CA1720: Identifier contains type name +dotnet_diagnostic.CA1720.severity = none + +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = suggestion + +# SA1642: Constructor summary documentation should begin with standard text +dotnet_diagnostic.SA1642.severity = none + +# SA1611: Element parameters should be documented +dotnet_diagnostic.SA1611.severity = none + +# SA1615: Element return value should be documented +dotnet_diagnostic.SA1615.severity = none + +# SA1201: Elements should appear in the correct order +dotnet_diagnostic.SA1201.severity = none + +# CA1054: URI-like parameters should not be strings +dotnet_diagnostic.CA1054.severity = none + +# SA1515: Single-line comment should be preceded by blank line +dotnet_diagnostic.SA1515.severity = none + +# CA1716: Identifiers should not match keywords +dotnet_diagnostic.CA1716.severity = none + +# CA1812: Internal types should be instantiated (suppressing because analyser doesn't understand serialisers) +dotnet_diagnostic.CA1812.severity = suggestion + +# CA1055: URI-like return values should not be strings +dotnet_diagnostic.CA1055.severity = none + +# CA1062: Exclude our guard methods from null-check warnings. +dotnet_code_quality.CA1062.null_check_validation_methods = NotNull + +dotnet_diagnostic.SA0001.severity=silent + +# CA2234: Pass system uri objects instead of strings +dotnet_diagnostic.CA2234.severity = silent diff --git a/.gitignore b/.gitignore index dfcfd56..b6113d3 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,5 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +tests/Enclave.Sdk.Tests/IntegrationTests.cs +src/Enclave.Sdk/test.txt diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..76fcced --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,16 @@ + + + + 9.0 + enable + AllEnabledByDefault + + + + + + + All + + + \ No newline at end of file diff --git a/Enclave.Sdk.Api.sln b/Enclave.Sdk.Api.sln new file mode 100644 index 0000000..d969f82 --- /dev/null +++ b/Enclave.Sdk.Api.sln @@ -0,0 +1,47 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api", "src\Enclave.Sdk\Enclave.Sdk.Api.csproj", "{AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api.Tests", "tests\Enclave.Sdk.Tests\Enclave.Sdk.Api.Tests.csproj", "{AC896965-74E9-405F-BA4E-E9D7336B906B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CA499537-7F6B-4E85-934A-04368AF78E9E}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{822BFA5E-EC93-45BF-B36E-1CAD1E7D88B1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BD898880-875C-43EA-B22C-A7C801CFA607}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}.Release|Any CPU.Build.0 = Release|Any CPU + {AC896965-74E9-405F-BA4E-E9D7336B906B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC896965-74E9-405F-BA4E-E9D7336B906B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC896965-74E9-405F-BA4E-E9D7336B906B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC896965-74E9-405F-BA4E-E9D7336B906B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4} = {CA499537-7F6B-4E85-934A-04368AF78E9E} + {AC896965-74E9-405F-BA4E-E9D7336B906B} = {822BFA5E-EC93-45BF-B36E-1CAD1E7D88B1} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {33E7CCF5-8133-4138-BDDC-6B2785C7DDC6} + EndGlobalSection +EndGlobal diff --git a/src/Enclave.Sdk/Clients/AuthorityClient.cs b/src/Enclave.Sdk/Clients/AuthorityClient.cs new file mode 100644 index 0000000..2452be0 --- /dev/null +++ b/src/Enclave.Sdk/Clients/AuthorityClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients; + +public class AuthorityClient +{ +} diff --git a/src/Enclave.Sdk/Clients/ClientBase.cs b/src/Enclave.Sdk/Clients/ClientBase.cs new file mode 100644 index 0000000..4c24f6e --- /dev/null +++ b/src/Enclave.Sdk/Clients/ClientBase.cs @@ -0,0 +1,85 @@ +using Enclave.Sdk.Api.Exceptions; +using System.Net; +using System.Net.Mime; +using System.Text; +using System.Text.Json; + +namespace Enclave.Sdk.Api.Clients; +public class ClientBase +{ + protected HttpClient HttpClient { get; private set; } + + protected JsonSerializerOptions JsonSerializerOptions { get; private set; } + + public ClientBase(HttpClient httpClient) + { + HttpClient = httpClient; + JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + } + + public StringContent Encode(TModel data) + { + if (data is null) + { + throw new ArgumentNullException(nameof(data), "Data should not be null"); + } + + var json = JsonSerializer.Serialize(data, JsonSerializerOptions); + var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); // use MediaTypeNames.Application.Json in Core 3.0+ and Standard 2.1+ + + return stringContent; + } + + public async Task DeserializeAsync(HttpContent httpContent) + { + if (httpContent is null) + { + return default; + } + + var contentStream = await httpContent.ReadAsStreamAsync(); + return await JsonSerializer.DeserializeAsync(contentStream, JsonSerializerOptions); + } + + public async Task CheckStatusCodes(HttpResponseMessage httpResponse) + { + if (httpResponse is null) + { + throw new ArgumentNullException(nameof(httpResponse), "httpResponse should not be null are you sure a call has been made"); + } + + var responseText = await httpResponse.Content.ReadAsStringAsync(); + if (httpResponse.StatusCode == HttpStatusCode.BadRequest) + { + throw new ApiException("Bad request; ensure you have provided the correct data to the Api", httpResponse.StatusCode, responseText, httpResponse.Headers); + } + else if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) + { + throw new ApiException("Unauthorized request; ensure you have provided a valid Access Token with \'Authorization: Bearer {token}\'.", httpResponse.StatusCode, responseText, httpResponse.Headers); + } + else if (httpResponse.StatusCode == HttpStatusCode.Forbidden) + { + throw new ApiException("The provided token does not grant rights to this request.", httpResponse.StatusCode, responseText, httpResponse.Headers); + } + else if (!httpResponse.IsSuccessStatusCode) + { + throw new ApiException($"The HTTP status code of the response was not expected ({httpResponse.StatusCode}).", httpResponse.StatusCode, responseText, httpResponse.Headers); + } + } + + protected void CheckModel(TModel model) + { + if (model is null) + { + throw new InvalidOperationException("Return from API is null please ensure you've entered the correct data or raise an issue"); + } + } + + protected virtual string PrepareUrl(string url) + { + return url; + } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/DNSClient.cs b/src/Enclave.Sdk/Clients/DNSClient.cs new file mode 100644 index 0000000..0a08b1a --- /dev/null +++ b/src/Enclave.Sdk/Clients/DNSClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients; +public class DNSClient +{ + +} diff --git a/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs b/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs new file mode 100644 index 0000000..0f739b2 --- /dev/null +++ b/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs @@ -0,0 +1,6 @@ +namespace Enclave.Sdk.Api.Clients; + +public class EnrolmentKeysClient +{ + +} diff --git a/src/Enclave.Sdk/Clients/LogsClient.cs b/src/Enclave.Sdk/Clients/LogsClient.cs new file mode 100644 index 0000000..7b08179 --- /dev/null +++ b/src/Enclave.Sdk/Clients/LogsClient.cs @@ -0,0 +1,6 @@ +namespace Enclave.Sdk.Api.Clients; + +public class LogsClient +{ + +} diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs new file mode 100644 index 0000000..b4c4678 --- /dev/null +++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs @@ -0,0 +1,120 @@ +using Enclave.Sdk.Api.Data.Organisations; + +namespace Enclave.Sdk.Api.Clients; + +public class OrganisationClient : ClientBase +{ + public AccountOrganisation CurrentOrganisation { get; private set; } + + public DNSClient DNSClient { get; private set; } + + private string _orgRoute; + + public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrganisation) + : base(httpClient) + { + CurrentOrganisation = currentOrganisation; + _orgRoute = $"org/{CurrentOrganisation.OrgId}"; + } + + public async Task GetAsync() + { + var result = await HttpClient.GetAsync(_orgRoute); + await CheckStatusCodes(result); + + var model = await DeserializeAsync(result.Content); + + CheckModel(model); + + return model; + } + + public async Task UpdateAsync(Dictionary updatedModel) + { + var encoded = Encode(updatedModel); + var result = await HttpClient.PatchAsync(_orgRoute, encoded); + await CheckStatusCodes(result); + + var model = await DeserializeAsync(result.Content); + + CheckModel(model); + + return model; + } + + public async Task?> GetOrganisationUsersAsync() + { + var result = await HttpClient.GetAsync($"{_orgRoute}/users"); + await CheckStatusCodes(result); + + var model = await DeserializeAsync(result.Content); + + CheckModel(model); + + return model.Users; + } + + public async Task RemoveUserAsync(string accountId) + { + var result = await HttpClient.DeleteAsync($"{_orgRoute}/users/{accountId}"); + await CheckStatusCodes(result); + } + + public async Task GetPendingInvitesAsync() + { + var result = await HttpClient.GetAsync($"{_orgRoute}/invites"); + await CheckStatusCodes(result); + + var model = DeserializeAsync(result.Content); + + CheckModel(model); + + return model; + } + + public async Task InviteUserAsync(string emailAddress) + { + var encoded = Encode(new + { + emailAddress, + }); + + var result = await HttpClient.PostAsync($"{_orgRoute}/invites", encoded); + await CheckStatusCodes(result); + + var model = DeserializeAsync(result.Content); + + CheckModel(model); + + return model; + } + + public async Task CancelInviteAync(string emailAddress) + { + var encoded = Encode(new + { + emailAddress, + }); + + var request = new HttpRequestMessage + { + Content = encoded, + Method = HttpMethod.Delete, + RequestUri = new Uri("{_orgRoute}/invites"), + }; + + var result = await HttpClient.SendAsync(request); + await CheckStatusCodes(result); + } + + public async Task GetOrganisationPricing() + { + throw new NotImplementedException(); + } + + protected override string PrepareUrl(string url) + { + var newUrl = $"{_orgRoute}{url}"; + return base.PrepareUrl(newUrl); + } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/PoliciesClient.cs b/src/Enclave.Sdk/Clients/PoliciesClient.cs new file mode 100644 index 0000000..3351200 --- /dev/null +++ b/src/Enclave.Sdk/Clients/PoliciesClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients; + +public class PoliciesClient +{ +} diff --git a/src/Enclave.Sdk/Clients/SystemsClient.cs b/src/Enclave.Sdk/Clients/SystemsClient.cs new file mode 100644 index 0000000..ad1d154 --- /dev/null +++ b/src/Enclave.Sdk/Clients/SystemsClient.cs @@ -0,0 +1,6 @@ +namespace Enclave.Sdk.Api.Clients; + +public class SystemsClient +{ + +} diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs new file mode 100644 index 0000000..dc6fea5 --- /dev/null +++ b/src/Enclave.Sdk/Clients/TagsClient.cs @@ -0,0 +1,6 @@ +namespace Enclave.Sdk.Api.Clients; + +public class TagsClient +{ + +} diff --git a/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs new file mode 100644 index 0000000..3210e2a --- /dev/null +++ b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs @@ -0,0 +1,6 @@ +namespace Enclave.Sdk.Api.Clients; + +public class UnapprovedSystemsClient +{ + +} diff --git a/src/Enclave.Sdk/Data/Builder.cs b/src/Enclave.Sdk/Data/Builder.cs new file mode 100644 index 0000000..c703269 --- /dev/null +++ b/src/Enclave.Sdk/Data/Builder.cs @@ -0,0 +1,48 @@ +using System.Linq.Expressions; + +namespace Enclave.Sdk.Api.Data; + +public class Builder + where TModel : IDataModel +{ + private Dictionary _patchDictionary = new Dictionary(); + + public Builder Set(Expression> propExpr, TValue newValue) + { + if (newValue is null) + { + throw new ArgumentNullException(nameof(newValue), "please specificy a valid new value."); + } + + if (propExpr is null) + { + throw new ArgumentNullException(nameof(propExpr), "please specificy a valid property expression."); + } + + // Get the property expression. + var property = propExpr.Body as MemberExpression; + + if (property is null) + { + throw new ArgumentException("Specified property expression is not valid."); + } + + var propName = property.Member.Name; + + _patchDictionary.Add(propName, newValue); + + return this; + } + + public Dictionary Send() + { + try + { + return _patchDictionary; + } + finally + { + _patchDictionary = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/IDataModel.cs b/src/Enclave.Sdk/Data/IDataModel.cs new file mode 100644 index 0000000..1d3b153 --- /dev/null +++ b/src/Enclave.Sdk/Data/IDataModel.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Data; + +public interface IDataModel +{ +} diff --git a/src/Enclave.Sdk/Data/Organisations/AccountOrganisation.cs b/src/Enclave.Sdk/Data/Organisations/AccountOrganisation.cs new file mode 100644 index 0000000..8e916a7 --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/AccountOrganisation.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Organisations; + +public class AccountOrganisation +{ + // TODO: Set as Organisation Data Type + [JsonPropertyName("orgId")] + public string OrgId { get; init; } + + [JsonPropertyName("orgName")] + public string OrgName { get; init; } + + [JsonPropertyName("role")] + public string Role { get; init; } +} + +public class AccountOrganisationTopLevel +{ + [JsonPropertyName("orgs")] + public List Orgs { get; init; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/Organisation.cs b/src/Enclave.Sdk/Data/Organisations/Organisation.cs new file mode 100644 index 0000000..4d6e2ff --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/Organisation.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Organisations; + +public class Organisation : IDataModel +{ + // TODO shall we use a organisationGuid? + public string Id { get; init; } + + public DateTime Created { get; init; } + + public string Name { get; init; } + + // TODO DO we want an enum here? + public string Plan { get; init; } + + public string? Website { get; init; } + + public string? Phone { get; init; } + + public int MaxSystems { get; init; } + + public long EnrolledSystems { get; init; } + + public long UnapprovedSystems { get; init; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs new file mode 100644 index 0000000..464d381 --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs @@ -0,0 +1,10 @@ +namespace Enclave.Sdk.Api.Data.Organisations; + +public class OrganisationBillingEvent +{ + public string Code { get; init; } + + public string Message { get; init; } + + public string Level { get; init; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs new file mode 100644 index 0000000..b76782f --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs @@ -0,0 +1,10 @@ +namespace Enclave.Sdk.Api.Data.Organisations; + +public class OrganisationPlanPricing +{ + public string CurrencySymbol { get; init; } + + public bool Enabled { get; init; } + + public IReadOnlyList Quantities { get; init; } +} diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs new file mode 100644 index 0000000..ead9c9e --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Organisations; + +public class OrganisationPricing +{ + public OrganisationPlanPricing Starter { get; init; } + + public OrganisationPlanPricing Pro { get; init; } + + public OrganisationPlanPricing Business { get; init; } + + public OrganisationBillingEvent LastBillingEvent { get; init; } +} diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs new file mode 100644 index 0000000..8375796 --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Organisations; + +public class OrganisationUser +{ + // TODO Do we want to use account GUID? + public string Id { get; init; } + + public string EmailAddress { get; init; } + + public string FirstName { get; init; } + + public string LastName { get; init; } + + public DateTime JoinDate { get; init; } + + // TODO Impliment ENUM? + public string Role { get; init; } +} + +public class OrganisationUsersTopLevel +{ + public IReadOnlyList Users { get; init; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/Quantity.cs b/src/Enclave.Sdk/Data/Organisations/Quantity.cs new file mode 100644 index 0000000..2336a0d --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/Quantity.cs @@ -0,0 +1,10 @@ +namespace Enclave.Sdk.Api.Data.Organisations; + +public class Quantity +{ + public int Capacity { get; init; } + + public int Price { get; init; } + + public bool IsDefault { get; init; } +} diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj new file mode 100644 index 0000000..944ca57 --- /dev/null +++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj @@ -0,0 +1,10 @@ + + + + 10.0 + net6.0 + enable + enable + + + diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs new file mode 100644 index 0000000..04238bf --- /dev/null +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -0,0 +1,52 @@ +using Enclave.Sdk.Api.Clients; +using Enclave.Sdk.Api.Data.Organisations; +using System.Net; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text.Json; + +namespace Enclave.Sdk.Api; + +public class EnclaveClient +{ + public OrganisationClient? Organisation { get; private set; } + + private readonly HttpClient _httpClient; + + // TODO Make HttpClient and BaseUrl optional and for token pull it from .enclave folder if it's not supplied + public EnclaveClient(HttpClient httpClient, string baseUrl, string token) + { + _httpClient = httpClient; + _httpClient.BaseAddress = new Uri(baseUrl); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var clientHeader = new ProductInfoHeaderValue("SDK", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); + _httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); + } + + public async Task> GetOrganisationsAsync() + { + var result = await _httpClient.GetAsync("/account/orgs"); + + if (result is null) + { + throw new InvalidOperationException("Could not find any organisation associated to this token"); + } + + result.EnsureSuccessStatusCode(); + + var contentStream = await result.Content.ReadAsStreamAsync(); + var organisations = await JsonSerializer.DeserializeAsync(contentStream); + + if (organisations is null) + { + throw new InvalidOperationException("Could not deserialize orgs associated to this token"); + } + + return organisations.Orgs; + } + + public OrganisationClient CreateOrganisationClient(AccountOrganisation organisation) + { + return new OrganisationClient(_httpClient, organisation); + } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Exceptions/ApiException.cs b/src/Enclave.Sdk/Exceptions/ApiException.cs new file mode 100644 index 0000000..db7bb7d --- /dev/null +++ b/src/Enclave.Sdk/Exceptions/ApiException.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Net.Http.Headers; + +namespace Enclave.Sdk.Api.Exceptions; + +public class ApiException : Exception +{ + public HttpStatusCode StatusCode { get; private set; } + + public string Response { get; private set; } + + public HttpResponseHeaders Headers { get; private set; } + + public ApiException(string message, HttpStatusCode statusCode, string response, HttpResponseHeaders headers) + : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + (response == null ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length))) + { + StatusCode = statusCode; + Response = response; + Headers = headers; + } + + public override string ToString() + { + return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + } +} \ No newline at end of file diff --git a/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj b/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj new file mode 100644 index 0000000..0aa084d --- /dev/null +++ b/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj @@ -0,0 +1,22 @@ + + + + 10.0 + net6.0 + enable + enable + 0c261991-ae7a-4598-9650-7f05d2939f98 + + + + + + + + + + + + + + From 8ecdd75ad175de15c6780e086c6e6af4f2d8f5e0 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Fri, 19 Nov 2021 17:46:15 +0000 Subject: [PATCH 02/26] more changes to associated clients and started work on unit testing before continuing with other clients --- src/Enclave.Sdk/Clients/AuthorityClient.cs | 9 ++- src/Enclave.Sdk/Clients/ClientBase.cs | 6 +- src/Enclave.Sdk/Clients/DNSClient.cs | 11 ++- .../Clients/EnrolmentKeysClient.cs | 8 +- .../Clients/Interfaces/IOrganisationClient.cs | 28 +++++++ src/Enclave.Sdk/Clients/LogsClient.cs | 8 +- src/Enclave.Sdk/Clients/OrganisationClient.cs | 39 +++++----- src/Enclave.Sdk/Clients/PoliciesClient.cs | 9 ++- src/Enclave.Sdk/Clients/SystemsClient.cs | 8 +- src/Enclave.Sdk/Clients/TagsClient.cs | 46 ++++++++++- .../Clients/UnapprovedSystemsClient.cs | 8 +- .../AccountOrganisation.cs | 16 ++-- .../Data/Account/UserOrganisationRole.cs | 12 +++ .../Organisations/Enum/BillingEventLevel.cs | 22 ++++++ .../Organisations/Enum/OrganisationPlan.cs | 8 ++ .../Data/Organisations/Organisation.cs | 37 ++++++++- .../Organisations/OrganisationBillingEvent.cs | 18 ++++- .../Data/Organisations/OrganisationInvite.cs | 19 +++++ .../OrganisationPendingInvites.cs | 18 +++++ .../Organisations/OrganisationPlanPricing.cs | 15 +++- .../Data/Organisations/OrganisationPricing.cs | 12 +++ .../Data/Organisations/OrganisationUser.cs | 28 ++++++- .../Organisations/PlanPricingQuanitity.cs | 22 ++++++ .../Data/Organisations/Quantity.cs | 10 --- .../Data/Pagination/PaginationLinks.cs | 33 ++++++++ .../Data/Pagination/PaginationMetadata.cs | 38 ++++++++++ .../Pagination/PagninatedResponseModel.cs | 29 +++++++ src/Enclave.Sdk/Data/Tags/TagItem.cs | 37 +++++++++ .../Data/Tags/TagQuerySortOrder.cs | 8 ++ src/Enclave.Sdk/EnclaveClient.cs | 4 +- .../Clients/OrganisationClientTests.cs | 57 ++++++++++++++ .../Enclave.Sdk.Api.Tests.csproj | 3 +- tests/Enclave.Sdk.Tests/EnclaveClientTests.cs | 76 +++++++++++++++++++ 33 files changed, 644 insertions(+), 58 deletions(-) create mode 100644 src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs rename src/Enclave.Sdk/Data/{Organisations => Account}/AccountOrganisation.cs (59%) create mode 100644 src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/Enum/BillingEventLevel.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/Enum/OrganisationPlan.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs create mode 100644 src/Enclave.Sdk/Data/Organisations/PlanPricingQuanitity.cs delete mode 100644 src/Enclave.Sdk/Data/Organisations/Quantity.cs create mode 100644 src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs create mode 100644 src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs create mode 100644 src/Enclave.Sdk/Data/Pagination/PagninatedResponseModel.cs create mode 100644 src/Enclave.Sdk/Data/Tags/TagItem.cs create mode 100644 src/Enclave.Sdk/Data/Tags/TagQuerySortOrder.cs create mode 100644 tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs create mode 100644 tests/Enclave.Sdk.Tests/EnclaveClientTests.cs diff --git a/src/Enclave.Sdk/Clients/AuthorityClient.cs b/src/Enclave.Sdk/Clients/AuthorityClient.cs index 2452be0..c767a33 100644 --- a/src/Enclave.Sdk/Clients/AuthorityClient.cs +++ b/src/Enclave.Sdk/Clients/AuthorityClient.cs @@ -1,5 +1,12 @@ namespace Enclave.Sdk.Api.Clients; -public class AuthorityClient +public class AuthorityClient : ClientBase { + private string _orgRoute; + + public AuthorityClient(HttpClient httpClient, string orgRoute) + : base(httpClient) + { + _orgRoute = orgRoute; + } } diff --git a/src/Enclave.Sdk/Clients/ClientBase.cs b/src/Enclave.Sdk/Clients/ClientBase.cs index 4c24f6e..8d954d9 100644 --- a/src/Enclave.Sdk/Clients/ClientBase.cs +++ b/src/Enclave.Sdk/Clients/ClientBase.cs @@ -20,7 +20,7 @@ public ClientBase(HttpClient httpClient) }; } - public StringContent Encode(TModel data) + protected StringContent Encode(TModel data) { if (data is null) { @@ -33,7 +33,7 @@ public StringContent Encode(TModel data) return stringContent; } - public async Task DeserializeAsync(HttpContent httpContent) + protected async Task DeserializeAsync(HttpContent httpContent) { if (httpContent is null) { @@ -44,7 +44,7 @@ public StringContent Encode(TModel data) return await JsonSerializer.DeserializeAsync(contentStream, JsonSerializerOptions); } - public async Task CheckStatusCodes(HttpResponseMessage httpResponse) + protected async Task CheckStatusCodes(HttpResponseMessage httpResponse) { if (httpResponse is null) { diff --git a/src/Enclave.Sdk/Clients/DNSClient.cs b/src/Enclave.Sdk/Clients/DNSClient.cs index 0a08b1a..403c98d 100644 --- a/src/Enclave.Sdk/Clients/DNSClient.cs +++ b/src/Enclave.Sdk/Clients/DNSClient.cs @@ -1,5 +1,14 @@ +using Enclave.Sdk.Api.Data.Organisations; + namespace Enclave.Sdk.Api.Clients; -public class DNSClient + +public class DnsClient : ClientBase { + private string _orgRoute; + public DnsClient(HttpClient httpClient, string orgRoute) + : base(httpClient) + { + _orgRoute = orgRoute; + } } diff --git a/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs b/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs index 0f739b2..39d9875 100644 --- a/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs +++ b/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs @@ -1,6 +1,12 @@ namespace Enclave.Sdk.Api.Clients; -public class EnrolmentKeysClient +public class EnrolmentKeysClient : ClientBase { + private string _orgRoute; + public EnrolmentKeysClient(HttpClient httpClient, string orgRoute) + : base(httpClient) + { + _orgRoute = orgRoute; + } } diff --git a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs new file mode 100644 index 0000000..0ed6548 --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs @@ -0,0 +1,28 @@ +using Enclave.Sdk.Api.Data.Account; +using Enclave.Sdk.Api.Data.Organisations; + +namespace Enclave.Sdk.Api.Clients.Interfaces +{ + public interface IOrganisationClient + { + AccountOrganisation CurrentOrganisation { get; } + + DnsClient DNSClient { get; } + + Task CancelInviteAync(string emailAddress); + + Task GetAsync(); + + Task GetOrganisationPricing(); + + Task?> GetOrganisationUsersAsync(); + + Task GetPendingInvitesAsync(); + + Task InviteUserAsync(string emailAddress); + + Task RemoveUserAsync(string accountId); + + Task UpdateAsync(Dictionary updatedModel); + } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/LogsClient.cs b/src/Enclave.Sdk/Clients/LogsClient.cs index 7b08179..ce91cef 100644 --- a/src/Enclave.Sdk/Clients/LogsClient.cs +++ b/src/Enclave.Sdk/Clients/LogsClient.cs @@ -1,6 +1,12 @@ namespace Enclave.Sdk.Api.Clients; -public class LogsClient +public class LogsClient : ClientBase { + private string _orgRoute; + public LogsClient(HttpClient httpClient, string orgRoute) + : base(httpClient) + { + _orgRoute = orgRoute; + } } diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs index b4c4678..d2612f4 100644 --- a/src/Enclave.Sdk/Clients/OrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs @@ -1,12 +1,14 @@ -using Enclave.Sdk.Api.Data.Organisations; +using Enclave.Sdk.Api.Clients.Interfaces; +using Enclave.Sdk.Api.Data.Account; +using Enclave.Sdk.Api.Data.Organisations; namespace Enclave.Sdk.Api.Clients; -public class OrganisationClient : ClientBase +public class OrganisationClient : ClientBase, IOrganisationClient { public AccountOrganisation CurrentOrganisation { get; private set; } - public DNSClient DNSClient { get; private set; } + public DnsClient DNSClient { get; private set; } private string _orgRoute; @@ -60,47 +62,41 @@ public async Task RemoveUserAsync(string accountId) await CheckStatusCodes(result); } - public async Task GetPendingInvitesAsync() + public async Task GetPendingInvitesAsync() { var result = await HttpClient.GetAsync($"{_orgRoute}/invites"); await CheckStatusCodes(result); - var model = DeserializeAsync(result.Content); + var model = await DeserializeAsync(result.Content); CheckModel(model); return model; } - public async Task InviteUserAsync(string emailAddress) + public async Task InviteUserAsync(string emailAddress) { - var encoded = Encode(new + var encoded = Encode(new OrganisationInvite { - emailAddress, + EmailAddress = emailAddress, }); var result = await HttpClient.PostAsync($"{_orgRoute}/invites", encoded); await CheckStatusCodes(result); - - var model = DeserializeAsync(result.Content); - - CheckModel(model); - - return model; } public async Task CancelInviteAync(string emailAddress) { - var encoded = Encode(new + var encoded = Encode(new OrganisationInvite { - emailAddress, + EmailAddress = emailAddress, }); var request = new HttpRequestMessage { Content = encoded, Method = HttpMethod.Delete, - RequestUri = new Uri("{_orgRoute}/invites"), + RequestUri = new Uri($"{HttpClient.BaseAddress}{_orgRoute}/invites"), }; var result = await HttpClient.SendAsync(request); @@ -109,7 +105,14 @@ public async Task CancelInviteAync(string emailAddress) public async Task GetOrganisationPricing() { - throw new NotImplementedException(); + var result = await HttpClient.GetAsync($"{_orgRoute}/pricing"); + await CheckStatusCodes(result); + + var model = await DeserializeAsync(result.Content); + + CheckModel(model); + + return model; } protected override string PrepareUrl(string url) diff --git a/src/Enclave.Sdk/Clients/PoliciesClient.cs b/src/Enclave.Sdk/Clients/PoliciesClient.cs index 3351200..4bd8ecb 100644 --- a/src/Enclave.Sdk/Clients/PoliciesClient.cs +++ b/src/Enclave.Sdk/Clients/PoliciesClient.cs @@ -1,5 +1,12 @@ namespace Enclave.Sdk.Api.Clients; -public class PoliciesClient +public class PoliciesClient : ClientBase { + private string _orgRoute; + + public PoliciesClient(HttpClient httpClient, string orgRoute) + : base(httpClient) + { + _orgRoute = orgRoute; + } } diff --git a/src/Enclave.Sdk/Clients/SystemsClient.cs b/src/Enclave.Sdk/Clients/SystemsClient.cs index ad1d154..28bcc4a 100644 --- a/src/Enclave.Sdk/Clients/SystemsClient.cs +++ b/src/Enclave.Sdk/Clients/SystemsClient.cs @@ -1,6 +1,12 @@ namespace Enclave.Sdk.Api.Clients; -public class SystemsClient +public class SystemsClient : ClientBase { + private string _orgRoute; + public SystemsClient(HttpClient httpClient, string orgRoute) + : base(httpClient) + { + _orgRoute = orgRoute; + } } diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs index dc6fea5..abde3a7 100644 --- a/src/Enclave.Sdk/Clients/TagsClient.cs +++ b/src/Enclave.Sdk/Clients/TagsClient.cs @@ -1,6 +1,50 @@ +using Enclave.Sdk.Api.Data.Pagination; +using Enclave.Sdk.Api.Data.Tags; +using System.Text; + namespace Enclave.Sdk.Api.Clients; -public class TagsClient +public class TagsClient : ClientBase { + private string _orgRoute; + + public TagsClient(HttpClient httpClient, string orgRoute) + : base(httpClient) + { + _orgRoute = orgRoute; + } + + public async Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null) + { + string? queryString = default; + if (searchTerm is not null) + { + queryString += $"search={searchTerm}"; + } + + if (sortOrder is not null) + { + queryString += $"sort={sortOrder}"; + } + + if (pageNumber is not null) + { + queryString += $"page={pageNumber}"; + } + + if (perPage is not null) + { + queryString += $"per_page={perPage}"; + } + + var result = await HttpClient.GetAsync($"{_orgRoute}/tags?{queryString}"); + + await CheckStatusCodes(result); + + var model = await DeserializeAsync>(result.Content); + + CheckModel(model); + return model; + } } diff --git a/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs index 3210e2a..850defa 100644 --- a/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs +++ b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs @@ -1,6 +1,12 @@ namespace Enclave.Sdk.Api.Clients; -public class UnapprovedSystemsClient +public class UnapprovedSystemsClient : ClientBase { + private string _orgRoute; + public UnapprovedSystemsClient(HttpClient httpClient, string orgRoute) + : base(httpClient) + { + _orgRoute = orgRoute; + } } diff --git a/src/Enclave.Sdk/Data/Organisations/AccountOrganisation.cs b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs similarity index 59% rename from src/Enclave.Sdk/Data/Organisations/AccountOrganisation.cs rename to src/Enclave.Sdk/Data/Account/AccountOrganisation.cs index 8e916a7..ad3f55e 100644 --- a/src/Enclave.Sdk/Data/Organisations/AccountOrganisation.cs +++ b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs @@ -5,19 +5,25 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; -namespace Enclave.Sdk.Api.Data.Organisations; +namespace Enclave.Sdk.Api.Data.Account; public class AccountOrganisation { // TODO: Set as Organisation Data Type - [JsonPropertyName("orgId")] + /// + /// The organisation ID. + /// public string OrgId { get; init; } - [JsonPropertyName("orgName")] + /// + /// The organisation name. + /// public string OrgName { get; init; } - [JsonPropertyName("role")] - public string Role { get; init; } + /// + /// The user's role within the organisation. + /// + public UserOrganisationRole Role { get; init; } } public class AccountOrganisationTopLevel diff --git a/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs b/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs new file mode 100644 index 0000000..70603d1 --- /dev/null +++ b/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Account; +public enum UserOrganisationRole +{ + Owner, + Admin, +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/Enum/BillingEventLevel.cs b/src/Enclave.Sdk/Data/Organisations/Enum/BillingEventLevel.cs new file mode 100644 index 0000000..7b92bd8 --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/Enum/BillingEventLevel.cs @@ -0,0 +1,22 @@ +namespace Enclave.Sdk.Api.Data.Organisations.Enum; + +/// +/// Defines levels for billing events. +/// +public enum BillingEventLevel +{ + /// + /// Info. + /// + Information, + + /// + /// Warning. + /// + Warning, + + /// + /// Error. + /// + Error, +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/Enum/OrganisationPlan.cs b/src/Enclave.Sdk/Data/Organisations/Enum/OrganisationPlan.cs new file mode 100644 index 0000000..8ed8025 --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/Enum/OrganisationPlan.cs @@ -0,0 +1,8 @@ +namespace Enclave.Sdk.Api.Data.Organisations.Enum; + +public enum OrganisationPlan +{ + Starter = 0, + Pro = 1, + Business = 2, +} diff --git a/src/Enclave.Sdk/Data/Organisations/Organisation.cs b/src/Enclave.Sdk/Data/Organisations/Organisation.cs index 4d6e2ff..70f9eee 100644 --- a/src/Enclave.Sdk/Data/Organisations/Organisation.cs +++ b/src/Enclave.Sdk/Data/Organisations/Organisation.cs @@ -1,4 +1,5 @@ -using System; +using Enclave.Sdk.Api.Data.Organisations.Enum; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -7,25 +8,53 @@ namespace Enclave.Sdk.Api.Data.Organisations; +/// +/// Organisation properties model. +/// public class Organisation : IDataModel { - // TODO shall we use a organisationGuid? + /// + /// The organisation ID. + /// public string Id { get; init; } + /// + /// The UTC timestamp at which the organisation was created. + /// public DateTime Created { get; init; } + /// + /// The name of the organisation. + /// public string Name { get; init; } - // TODO DO we want an enum here? - public string Plan { get; init; } + /// + /// The current plan the organisation is on. + /// + public OrganisationPlan Plan { get; init; } + /// + /// The provided website info for the org (if known). + /// public string? Website { get; init; } + /// + /// The provided phone info for the org (if known). + /// public string? Phone { get; init; } + /// + /// The maximum number of systems this organisation can have enrolled. + /// public int MaxSystems { get; init; } + /// + /// The number of enrolled systems. + /// public long EnrolledSystems { get; init; } + /// + /// The number of systems waiting to be approved. + /// public long UnapprovedSystems { get; init; } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs index 464d381..aed0788 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs @@ -1,10 +1,24 @@ -namespace Enclave.Sdk.Api.Data.Organisations; +using Enclave.Sdk.Api.Data.Organisations.Enum; +namespace Enclave.Sdk.Api.Data.Organisations; + +/// +/// Defines the properties available for a billing event on an organisation. +/// public class OrganisationBillingEvent { + /// + /// The code indicating the billing event. + /// public string Code { get; init; } + /// + /// A human-readable message describing the event. + /// public string Message { get; init; } - public string Level { get; init; } + /// + /// The event 'level'. + /// + public BillingEventLevel Level { get; init; } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs new file mode 100644 index 0000000..839190a --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Organisations +{ + /// + /// Invite model. + /// + public class OrganisationInvite + { + /// + /// The email address of the user to invite. + /// + public string EmailAddress { get; init; } + } +} diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs new file mode 100644 index 0000000..4c33de9 --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Organisations; + +/// +/// Model for the pending list of org invites. +/// +public class OrganisationPendingInvites +{ + /// + /// The set of outstanding invites that have been sent for this organisation. + /// + public IReadOnlyList Invites { get; init; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs index b76782f..9a50f03 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs @@ -1,10 +1,23 @@ namespace Enclave.Sdk.Api.Data.Organisations; + +/// +/// A model defining the pricing for a given plan. +/// public class OrganisationPlanPricing { + /// + /// The appropriate currency symbol. + /// public string CurrencySymbol { get; init; } + /// + /// Whether or not this plan is enabled. + /// public bool Enabled { get; init; } - public IReadOnlyList Quantities { get; init; } + /// + /// The quantities of systems available in this plan. + /// + public IReadOnlyList Quantities { get; init; } } diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs index ead9c9e..cd38b66 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs @@ -8,11 +8,23 @@ namespace Enclave.Sdk.Api.Data.Organisations; public class OrganisationPricing { + /// + /// The starter tier pricing info. + /// public OrganisationPlanPricing Starter { get; init; } + /// + /// Pro tier pricing info. + /// public OrganisationPlanPricing Pro { get; init; } + /// + /// Business tier pricing info. + /// public OrganisationPlanPricing Business { get; init; } + /// + /// The last billing event for the organisation (if any). + /// public OrganisationBillingEvent LastBillingEvent { get; init; } } diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs index 8375796..dbd77b0 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs @@ -1,4 +1,5 @@ -using System; +using Enclave.Sdk.Api.Data.Account; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,21 +7,40 @@ namespace Enclave.Sdk.Api.Data.Organisations; +/// +/// Defines the properties of a user's membership of an organisation. +/// public class OrganisationUser { - // TODO Do we want to use account GUID? + /// + /// The account ID. + /// public string Id { get; init; } + /// + /// The user email address. + /// public string EmailAddress { get; init; } + /// + /// The user first name. + /// public string FirstName { get; init; } + /// + /// The user last name. + /// public string LastName { get; init; } + /// + /// The UTC timestamp for when the user joined the organisation. + /// public DateTime JoinDate { get; init; } - // TODO Impliment ENUM? - public string Role { get; init; } + /// + /// The user's role in the organisation. + /// + public UserOrganisationRole Role { get; init; } } public class OrganisationUsersTopLevel diff --git a/src/Enclave.Sdk/Data/Organisations/PlanPricingQuanitity.cs b/src/Enclave.Sdk/Data/Organisations/PlanPricingQuanitity.cs new file mode 100644 index 0000000..9f811f2 --- /dev/null +++ b/src/Enclave.Sdk/Data/Organisations/PlanPricingQuanitity.cs @@ -0,0 +1,22 @@ +namespace Enclave.Sdk.Api.Data.Organisations; + +/// +/// The price of a certain quantity of systems. +/// +public class PlanPricingQuanitity +{ + /// + /// The max system capacity of this quantity. + /// + public int Capacity { get; init; } + + /// + /// The price of this quantity. + /// + public decimal Price { get; init; } + + /// + /// Whether or not this is the default quantity to display in the plan. + /// + public bool IsDefault { get; init; } +} diff --git a/src/Enclave.Sdk/Data/Organisations/Quantity.cs b/src/Enclave.Sdk/Data/Organisations/Quantity.cs deleted file mode 100644 index 2336a0d..0000000 --- a/src/Enclave.Sdk/Data/Organisations/Quantity.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Enclave.Sdk.Api.Data.Organisations; - -public class Quantity -{ - public int Capacity { get; init; } - - public int Price { get; init; } - - public bool IsDefault { get; init; } -} diff --git a/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs b/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs new file mode 100644 index 0000000..cbb1201 --- /dev/null +++ b/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Pagination; + +/// +/// Defines the available pagination links. +/// +public class PaginationLinks +{ + /// + /// The first page of data. + /// + public Uri First { get; init; } + + /// + /// The previous page of data (or null if this is the first page). + /// + public Uri? Prev { get; init; } + + /// + /// The next page of data (or null if this is the last page). + /// + public Uri? Next { get; init; } + + /// + /// The last page of data. + /// + public Uri Last { get; init; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs b/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs new file mode 100644 index 0000000..77e9102 --- /dev/null +++ b/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Pagination; + +/// +/// Defines the metadata attached to a paginated response. +/// +public class PaginationMetadata +{ + /// + /// The total number of items (across all pages). + /// + public int Total { get; init; } + + /// + /// The first page number. + /// + public int FirstPage { get; init; } + + /// + /// The previous page number (null if this is the first page). + /// + public int? PrevPage { get; init; } + + /// + /// The last page number. + /// + public int LastPage { get; init; } + + /// + /// The next page number (null if this is the last page). + /// + public int? NextPage { get; init; } +} diff --git a/src/Enclave.Sdk/Data/Pagination/PagninatedResponseModel.cs b/src/Enclave.Sdk/Data/Pagination/PagninatedResponseModel.cs new file mode 100644 index 0000000..944f5ef --- /dev/null +++ b/src/Enclave.Sdk/Data/Pagination/PagninatedResponseModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data.Pagination; + +/// +/// Response model for paginated data. +/// +/// The item type. +public class PagninatedResponseModel +{ + /// + /// Metadata for the paginated data. + /// + public PaginationMetadata Metadata { get; init; } + + /// + /// The related links for the current page of data. + /// + public PaginationLinks Links { get; init; } + + /// + /// The requested page of items. + /// + public IEnumerable Items { get; init; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Tags/TagItem.cs b/src/Enclave.Sdk/Data/Tags/TagItem.cs new file mode 100644 index 0000000..6a943d8 --- /dev/null +++ b/src/Enclave.Sdk/Data/Tags/TagItem.cs @@ -0,0 +1,37 @@ +namespace Enclave.Sdk.Api.Data.Tags; + +/// +/// Represents a single tag. +/// +public class TagItem +{ + /// + /// The tag name. + /// + public string Tag { get; init; } + + /// + /// The last time this tag was last referenced. + /// + public DateTime LastReferenced { get; init; } + + /// + /// The number of systems that reference this tag. + /// + public int Systems { get; init; } + + /// + /// The number of enrolment keys that reference this tag. + /// + public int Keys { get; init; } + + /// + /// The number of policies that reference this tag. + /// + public int Policies { get; init; } + + /// + /// The number of DNS records that reference this tag. + /// + public int DnsRecords { get; init; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Tags/TagQuerySortOrder.cs b/src/Enclave.Sdk/Data/Tags/TagQuerySortOrder.cs new file mode 100644 index 0000000..d44057f --- /dev/null +++ b/src/Enclave.Sdk/Data/Tags/TagQuerySortOrder.cs @@ -0,0 +1,8 @@ +namespace Enclave.Sdk.Api.Data.Tags; + +public enum TagQuerySortOrder +{ + Alphabetical, + RecentlyUsed, + ReferencedSystems, +} \ No newline at end of file diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index 04238bf..c178ca5 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -1,5 +1,5 @@ using Enclave.Sdk.Api.Clients; -using Enclave.Sdk.Api.Data.Organisations; +using Enclave.Sdk.Api.Data.Account; using System.Net; using System.Net.Http.Headers; using System.Reflection; @@ -29,7 +29,7 @@ public async Task> GetOrganisationsAsync() if (result is null) { - throw new InvalidOperationException("Could not find any organisation associated to this token"); + throw new InvalidOperationException("Did not get any response"); } result.EnsureSuccessStatusCode(); diff --git a/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs b/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs new file mode 100644 index 0000000..2e1cda5 --- /dev/null +++ b/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs @@ -0,0 +1,57 @@ +using Enclave.Sdk.Api.Clients; +using Enclave.Sdk.Api.Data.Account; +using Enclave.Sdk.Api.Data.Organisations; +using NUnit.Framework; +using System.Text.Json; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace Enclave.Sdk.Api.Tests.Clients +{ + public class OrganisationClientTests + { + private OrganisationClient _organisationClient; + private WireMockServer _server; + private string _orgRoute; + + [SetUp] + public void Setup() + { + _server = WireMockServer.Start(); + + var httpClient = new HttpClient + { + BaseAddress = new Uri(_server.Urls[0]) + }; + + var currentOrganisation = new AccountOrganisation + { + OrgId = "testId", + OrgName = "TestName", + Role = UserOrganisationRole.Admin, + }; + + _orgRoute = $"/org/{currentOrganisation.OrgId}"; + + _organisationClient = new OrganisationClient(httpClient, currentOrganisation); + } + + public async Task should_return_a_detailed_organisation_model_when_calling_GetAsync() + { + // Arrange + var org = new Organisation(); + + _server + .Given(Request.Create().WithPath(_orgRoute).UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize(org))); + + // Act + + // Assert + } + } +} diff --git a/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj b/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj index 0aa084d..2062fe0 100644 --- a/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj +++ b/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj @@ -4,7 +4,7 @@ 10.0 net6.0 enable - enable + disable 0c261991-ae7a-4598-9650-7f05d2939f98 @@ -13,6 +13,7 @@ + diff --git a/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs b/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs new file mode 100644 index 0000000..bb58082 --- /dev/null +++ b/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs @@ -0,0 +1,76 @@ +using Enclave.Sdk.Api.Data.Account; +using NUnit.Framework; +using System.Text.Json; +using System.Text.Json.Serialization; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace Enclave.Sdk.Api.Tests; + +public class EnclaveClientTests +{ + private EnclaveClient _client; + private WireMockServer _server; + + [SetUp] + public void Setup() + { + var httpClient = new HttpClient(); + _server = WireMockServer.Start(); + _client = new EnclaveClient(httpClient, _server.Urls[0], string.Empty); + } + + [Test] + public async Task should_return_list_of_orgs_when_calling_GetOrganisationsAsync() + { + // Arrange + var accountOrg = new AccountOrganisationTopLevel + { + Orgs = new List + { + new AccountOrganisation + { + OrgId = "testId", + OrgName = "TestName", + Role = UserOrganisationRole.Admin, + }, + }, + }; + + _server + .Given(Request.Create().WithPath("/account/orgs").UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize(accountOrg))); + + // Act + var result = await _client.GetOrganisationsAsync(); + + // Assert + Assert.That(result.FirstOrDefault().OrgId, + Is.EqualTo(accountOrg.Orgs.FirstOrDefault().OrgId)); + } + + [Test] + public async Task should_throw_aggregate_exception_if_server_is_unreachable() + { + // Assert + } + + [Test] + public async Task should_throw_invalid_operation_exception_if_response_does_not_contain_an_organisation_model() + { + // Arrange + _server + .Given(Request.Create().WithPath("/account/orgs").UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize("{}"))); + + // Assert + Assert.Throws(() => _client.GetOrganisationsAsync().Wait()); + } +} \ No newline at end of file From ed4fe54e856f8a6403a4c227377e121185ba4b28 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Mon, 22 Nov 2021 17:19:17 +0000 Subject: [PATCH 03/26] added interface for all clients, added new organisationId guid and updated editor config --- .editorconfig | 1 + src/Enclave.Sdk/Clients/AuthorityClient.cs | 6 +- src/Enclave.Sdk/Clients/ClientBase.cs | 9 +- src/Enclave.Sdk/Clients/DNSClient.cs | 4 +- .../Clients/EnrolmentKeysClient.cs | 4 +- .../Clients/Interfaces/IAuthorityClient.cs | 5 + .../Clients/Interfaces/IDnsClient.cs | 5 + .../Interfaces/IEnrolmentKeysClient.cs | 5 + .../Clients/Interfaces/ILogsClient.cs | 5 + .../Clients/Interfaces/IOrganisationClient.cs | 43 +++++--- .../Clients/Interfaces/IPoliciesClient.cs | 5 + .../Clients/Interfaces/ISystemsClient.cs | 5 + .../Clients/Interfaces/ITagsClient.cs | 5 + .../Interfaces/IUnapprovedSystemsClient.cs | 5 + src/Enclave.Sdk/Clients/LogsClient.cs | 4 +- src/Enclave.Sdk/Clients/OrganisationClient.cs | 24 ++++- src/Enclave.Sdk/Clients/PoliciesClient.cs | 6 +- src/Enclave.Sdk/Clients/SystemsClient.cs | 4 +- src/Enclave.Sdk/Clients/TagsClient.cs | 7 +- .../Clients/UnapprovedSystemsClient.cs | 6 +- .../Data/Account/AccountOrganisation.cs | 12 +-- .../Data/Account/OrganisationId.cs | 8 ++ ...onseModel.cs => PaginatedResponseModel.cs} | 2 +- src/Enclave.Sdk/Enclave.Sdk.Api.csproj | 5 + src/Enclave.Sdk/EnclaveClient.cs | 11 ++- .../Clients/OrganisationClientTests.cs | 79 ++++++++------- .../Clients/TagClientTests.cs | 98 +++++++++++++++++++ .../Enclave.Sdk.Api.Tests.csproj | 1 + tests/Enclave.Sdk.Tests/EnclaveClientTests.cs | 30 +++--- tests/Enclave.Sdk.Tests/GlobalSuppressions.cs | 9 ++ 30 files changed, 315 insertions(+), 98 deletions(-) create mode 100644 src/Enclave.Sdk/Clients/Interfaces/IAuthorityClient.cs create mode 100644 src/Enclave.Sdk/Clients/Interfaces/IDnsClient.cs create mode 100644 src/Enclave.Sdk/Clients/Interfaces/IEnrolmentKeysClient.cs create mode 100644 src/Enclave.Sdk/Clients/Interfaces/ILogsClient.cs create mode 100644 src/Enclave.Sdk/Clients/Interfaces/IPoliciesClient.cs create mode 100644 src/Enclave.Sdk/Clients/Interfaces/ISystemsClient.cs create mode 100644 src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs create mode 100644 src/Enclave.Sdk/Clients/Interfaces/IUnapprovedSystemsClient.cs create mode 100644 src/Enclave.Sdk/Data/Account/OrganisationId.cs rename src/Enclave.Sdk/Data/Pagination/{PagninatedResponseModel.cs => PaginatedResponseModel.cs} (93%) create mode 100644 tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs create mode 100644 tests/Enclave.Sdk.Tests/GlobalSuppressions.cs diff --git a/.editorconfig b/.editorconfig index 0c2b152..81d9de8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -163,3 +163,4 @@ dotnet_diagnostic.SA0001.severity=silent # CA2234: Pass system uri objects instead of strings dotnet_diagnostic.CA2234.severity = silent +csharp_style_namespace_declarations=file_scoped:warning diff --git a/src/Enclave.Sdk/Clients/AuthorityClient.cs b/src/Enclave.Sdk/Clients/AuthorityClient.cs index c767a33..c413012 100644 --- a/src/Enclave.Sdk/Clients/AuthorityClient.cs +++ b/src/Enclave.Sdk/Clients/AuthorityClient.cs @@ -1,6 +1,8 @@ -namespace Enclave.Sdk.Api.Clients; +using Enclave.Sdk.Api.Clients.Interfaces; -public class AuthorityClient : ClientBase +namespace Enclave.Sdk.Api.Clients; + +public class AuthorityClient : ClientBase, IAuthorityClient { private string _orgRoute; diff --git a/src/Enclave.Sdk/Clients/ClientBase.cs b/src/Enclave.Sdk/Clients/ClientBase.cs index 8d954d9..b49a0d5 100644 --- a/src/Enclave.Sdk/Clients/ClientBase.cs +++ b/src/Enclave.Sdk/Clients/ClientBase.cs @@ -1,4 +1,5 @@ using Enclave.Sdk.Api.Exceptions; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Mime; using System.Text; @@ -70,14 +71,18 @@ protected async Task CheckStatusCodes(HttpResponseMessage httpResponse) } } - protected void CheckModel(TModel model) + protected void CheckModel([NotNull] TModel? model) { if (model is null) { - throw new InvalidOperationException("Return from API is null please ensure you've entered the correct data or raise an issue"); + Throw(); } } + [DoesNotReturn] + private void Throw() => + throw new InvalidOperationException("Return from API is null please ensure you've entered the correct data or raise an issue"); + protected virtual string PrepareUrl(string url) { return url; diff --git a/src/Enclave.Sdk/Clients/DNSClient.cs b/src/Enclave.Sdk/Clients/DNSClient.cs index 403c98d..73c8e42 100644 --- a/src/Enclave.Sdk/Clients/DNSClient.cs +++ b/src/Enclave.Sdk/Clients/DNSClient.cs @@ -1,8 +1,8 @@ -using Enclave.Sdk.Api.Data.Organisations; +using Enclave.Sdk.Api.Clients.Interfaces; namespace Enclave.Sdk.Api.Clients; -public class DnsClient : ClientBase +public class DnsClient : ClientBase, IDnsClient { private string _orgRoute; diff --git a/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs b/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs index 39d9875..bc98e38 100644 --- a/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs +++ b/src/Enclave.Sdk/Clients/EnrolmentKeysClient.cs @@ -1,6 +1,8 @@ +using Enclave.Sdk.Api.Clients.Interfaces; + namespace Enclave.Sdk.Api.Clients; -public class EnrolmentKeysClient : ClientBase +public class EnrolmentKeysClient : ClientBase, IEnrolmentKeysClient { private string _orgRoute; diff --git a/src/Enclave.Sdk/Clients/Interfaces/IAuthorityClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IAuthorityClient.cs new file mode 100644 index 0000000..49b89d6 --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/IAuthorityClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface IAuthorityClient +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/IDnsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IDnsClient.cs new file mode 100644 index 0000000..c409dd2 --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/IDnsClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface IDnsClient +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/IEnrolmentKeysClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IEnrolmentKeysClient.cs new file mode 100644 index 0000000..a11d9cd --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/IEnrolmentKeysClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface IEnrolmentKeysClient +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/ILogsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/ILogsClient.cs new file mode 100644 index 0000000..11595fd --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/ILogsClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface ILogsClient +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs index 0ed6548..34f9708 100644 --- a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs @@ -1,28 +1,41 @@ using Enclave.Sdk.Api.Data.Account; using Enclave.Sdk.Api.Data.Organisations; -namespace Enclave.Sdk.Api.Clients.Interfaces +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface IOrganisationClient { - public interface IOrganisationClient - { - AccountOrganisation CurrentOrganisation { get; } + AccountOrganisation CurrentOrganisation { get; } + + IAuthorityClient Authority { get; } + + IDnsClient Dns { get; } + + IEnrolmentKeysClient EnrolmentKeys { get; } + + ILogsClient Logs { get; } + + IPoliciesClient Policies { get; } + + ISystemsClient Systems { get; } + + ITagsClient Tags { get; } - DnsClient DNSClient { get; } + IUnapprovedSystemsClient UnapprovedSystems { get; } - Task CancelInviteAync(string emailAddress); + Task CancelInviteAync(string emailAddress); - Task GetAsync(); + Task GetAsync(); - Task GetOrganisationPricing(); + Task GetOrganisationPricing(); - Task?> GetOrganisationUsersAsync(); + Task?> GetOrganisationUsersAsync(); - Task GetPendingInvitesAsync(); + Task GetPendingInvitesAsync(); - Task InviteUserAsync(string emailAddress); + Task InviteUserAsync(string emailAddress); - Task RemoveUserAsync(string accountId); + Task RemoveUserAsync(string accountId); - Task UpdateAsync(Dictionary updatedModel); - } -} \ No newline at end of file + Task UpdateAsync(Dictionary updatedModel); +} diff --git a/src/Enclave.Sdk/Clients/Interfaces/IPoliciesClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IPoliciesClient.cs new file mode 100644 index 0000000..b3283e4 --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/IPoliciesClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface IPoliciesClient +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/ISystemsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/ISystemsClient.cs new file mode 100644 index 0000000..b0cf022 --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/ISystemsClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface ISystemsClient +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs new file mode 100644 index 0000000..0d7d53d --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface ITagsClient +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/IUnapprovedSystemsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IUnapprovedSystemsClient.cs new file mode 100644 index 0000000..aa34fdc --- /dev/null +++ b/src/Enclave.Sdk/Clients/Interfaces/IUnapprovedSystemsClient.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Clients.Interfaces; + +public interface IUnapprovedSystemsClient +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/LogsClient.cs b/src/Enclave.Sdk/Clients/LogsClient.cs index ce91cef..53e7df1 100644 --- a/src/Enclave.Sdk/Clients/LogsClient.cs +++ b/src/Enclave.Sdk/Clients/LogsClient.cs @@ -1,6 +1,8 @@ +using Enclave.Sdk.Api.Clients.Interfaces; + namespace Enclave.Sdk.Api.Clients; -public class LogsClient : ClientBase +public class LogsClient : ClientBase, ILogsClient { private string _orgRoute; diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs index d2612f4..e8715a8 100644 --- a/src/Enclave.Sdk/Clients/OrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs @@ -8,7 +8,21 @@ public class OrganisationClient : ClientBase, IOrganisationClient { public AccountOrganisation CurrentOrganisation { get; private set; } - public DnsClient DNSClient { get; private set; } + public IAuthorityClient Authority => throw new NotImplementedException(); + + public IDnsClient Dns => throw new NotImplementedException(); + + public IEnrolmentKeysClient EnrolmentKeys => throw new NotImplementedException(); + + public ILogsClient Logs => throw new NotImplementedException(); + + public IPoliciesClient Policies => throw new NotImplementedException(); + + public ISystemsClient Systems => throw new NotImplementedException(); + + public ITagsClient Tags => throw new NotImplementedException(); + + public IUnapprovedSystemsClient UnapprovedSystems => throw new NotImplementedException(); private string _orgRoute; @@ -33,7 +47,7 @@ public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrga public async Task UpdateAsync(Dictionary updatedModel) { - var encoded = Encode(updatedModel); + using var encoded = Encode(updatedModel); var result = await HttpClient.PatchAsync(_orgRoute, encoded); await CheckStatusCodes(result); @@ -76,7 +90,7 @@ public async Task GetPendingInvitesAsync() public async Task InviteUserAsync(string emailAddress) { - var encoded = Encode(new OrganisationInvite + using var encoded = Encode(new OrganisationInvite { EmailAddress = emailAddress, }); @@ -87,12 +101,12 @@ public async Task InviteUserAsync(string emailAddress) public async Task CancelInviteAync(string emailAddress) { - var encoded = Encode(new OrganisationInvite + using var encoded = Encode(new OrganisationInvite { EmailAddress = emailAddress, }); - var request = new HttpRequestMessage + using var request = new HttpRequestMessage { Content = encoded, Method = HttpMethod.Delete, diff --git a/src/Enclave.Sdk/Clients/PoliciesClient.cs b/src/Enclave.Sdk/Clients/PoliciesClient.cs index 4bd8ecb..0f6aafd 100644 --- a/src/Enclave.Sdk/Clients/PoliciesClient.cs +++ b/src/Enclave.Sdk/Clients/PoliciesClient.cs @@ -1,6 +1,8 @@ -namespace Enclave.Sdk.Api.Clients; +using Enclave.Sdk.Api.Clients.Interfaces; -public class PoliciesClient : ClientBase +namespace Enclave.Sdk.Api.Clients; + +public class PoliciesClient : ClientBase, IPoliciesClient { private string _orgRoute; diff --git a/src/Enclave.Sdk/Clients/SystemsClient.cs b/src/Enclave.Sdk/Clients/SystemsClient.cs index 28bcc4a..fb4bc02 100644 --- a/src/Enclave.Sdk/Clients/SystemsClient.cs +++ b/src/Enclave.Sdk/Clients/SystemsClient.cs @@ -1,6 +1,8 @@ +using Enclave.Sdk.Api.Clients.Interfaces; + namespace Enclave.Sdk.Api.Clients; -public class SystemsClient : ClientBase +public class SystemsClient : ClientBase, ISystemsClient { private string _orgRoute; diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs index abde3a7..68d9674 100644 --- a/src/Enclave.Sdk/Clients/TagsClient.cs +++ b/src/Enclave.Sdk/Clients/TagsClient.cs @@ -1,10 +1,11 @@ +using Enclave.Sdk.Api.Clients.Interfaces; using Enclave.Sdk.Api.Data.Pagination; using Enclave.Sdk.Api.Data.Tags; using System.Text; namespace Enclave.Sdk.Api.Clients; -public class TagsClient : ClientBase +public class TagsClient : ClientBase, ITagsClient { private string _orgRoute; @@ -14,7 +15,7 @@ public TagsClient(HttpClient httpClient, string orgRoute) _orgRoute = orgRoute; } - public async Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null) + public async Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null) { string? queryString = default; if (searchTerm is not null) @@ -41,7 +42,7 @@ public async Task> GetAsync(string? searchTerm await CheckStatusCodes(result); - var model = await DeserializeAsync>(result.Content); + var model = await DeserializeAsync>(result.Content); CheckModel(model); diff --git a/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs index 850defa..4075970 100644 --- a/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs +++ b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs @@ -1,6 +1,8 @@ +using Enclave.Sdk.Api.Clients.Interfaces; + namespace Enclave.Sdk.Api.Clients; -public class UnapprovedSystemsClient : ClientBase +public class UnapprovedSystemsClient : ClientBase, IUnapprovedSystemsClient { private string _orgRoute; @@ -9,4 +11,6 @@ public UnapprovedSystemsClient(HttpClient httpClient, string orgRoute) { _orgRoute = orgRoute; } + + } diff --git a/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs index ad3f55e..3891175 100644 --- a/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs +++ b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -namespace Enclave.Sdk.Api.Data.Account; +namespace Enclave.Sdk.Api.Data.Account; public class AccountOrganisation { @@ -13,7 +6,7 @@ public class AccountOrganisation /// /// The organisation ID. /// - public string OrgId { get; init; } + public OrganisationId OrgId { get; init; } /// /// The organisation name. @@ -28,6 +21,5 @@ public class AccountOrganisation public class AccountOrganisationTopLevel { - [JsonPropertyName("orgs")] public List Orgs { get; init; } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Account/OrganisationId.cs b/src/Enclave.Sdk/Data/Account/OrganisationId.cs new file mode 100644 index 0000000..e8185d8 --- /dev/null +++ b/src/Enclave.Sdk/Data/Account/OrganisationId.cs @@ -0,0 +1,8 @@ +using TypedIds; + +namespace Enclave.Sdk.Api.Data.Account; + +[TypedId] +public readonly partial struct OrganisationId +{ +} diff --git a/src/Enclave.Sdk/Data/Pagination/PagninatedResponseModel.cs b/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs similarity index 93% rename from src/Enclave.Sdk/Data/Pagination/PagninatedResponseModel.cs rename to src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs index 944f5ef..f687b56 100644 --- a/src/Enclave.Sdk/Data/Pagination/PagninatedResponseModel.cs +++ b/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs @@ -10,7 +10,7 @@ namespace Enclave.Sdk.Api.Data.Pagination; /// Response model for paginated data. /// /// The item type. -public class PagninatedResponseModel +public class PaginatedResponseModel { /// /// Metadata for the paginated data. diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj index 944ca57..b72e505 100644 --- a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj +++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj @@ -5,6 +5,11 @@ net6.0 enable enable + True + + + + diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index c178ca5..107064c 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -1,4 +1,5 @@ using Enclave.Sdk.Api.Clients; +using Enclave.Sdk.Api.Clients.Interfaces; using Enclave.Sdk.Api.Data.Account; using System.Net; using System.Net.Http.Headers; @@ -12,6 +13,7 @@ public class EnclaveClient public OrganisationClient? Organisation { get; private set; } private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonSerializerOptions; // TODO Make HttpClient and BaseUrl optional and for token pull it from .enclave folder if it's not supplied public EnclaveClient(HttpClient httpClient, string baseUrl, string token) @@ -21,6 +23,11 @@ public EnclaveClient(HttpClient httpClient, string baseUrl, string token) _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var clientHeader = new ProductInfoHeaderValue("SDK", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); _httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); + + _jsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; } public async Task> GetOrganisationsAsync() @@ -35,7 +42,7 @@ public async Task> GetOrganisationsAsync() result.EnsureSuccessStatusCode(); var contentStream = await result.Content.ReadAsStreamAsync(); - var organisations = await JsonSerializer.DeserializeAsync(contentStream); + var organisations = await JsonSerializer.DeserializeAsync(contentStream, _jsonSerializerOptions); if (organisations is null) { @@ -45,7 +52,7 @@ public async Task> GetOrganisationsAsync() return organisations.Orgs; } - public OrganisationClient CreateOrganisationClient(AccountOrganisation organisation) + public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisation) { return new OrganisationClient(_httpClient, organisation); } diff --git a/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs b/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs index 2e1cda5..614d589 100644 --- a/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs +++ b/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs @@ -7,51 +7,62 @@ using WireMock.ResponseBuilders; using WireMock.Server; -namespace Enclave.Sdk.Api.Tests.Clients +namespace Enclave.Sdk.Api.Tests.Clients; + +public class OrganisationClientTests { - public class OrganisationClientTests + private OrganisationClient _organisationClient; + private WireMockServer _server; + private string _orgRoute; + private JsonSerializerOptions _serializerOptions = new JsonSerializerOptions { - private OrganisationClient _organisationClient; - private WireMockServer _server; - private string _orgRoute; + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; - [SetUp] - public void Setup() - { - _server = WireMockServer.Start(); + [SetUp] + public void Setup() + { + _server = WireMockServer.Start(); - var httpClient = new HttpClient - { - BaseAddress = new Uri(_server.Urls[0]) - }; + var httpClient = new HttpClient + { + BaseAddress = new Uri(_server.Urls[0]), + }; - var currentOrganisation = new AccountOrganisation - { - OrgId = "testId", - OrgName = "TestName", - Role = UserOrganisationRole.Admin, - }; + var currentOrganisation = new AccountOrganisation + { + OrgId = OrganisationId.New(), + OrgName = "TestName", + Role = UserOrganisationRole.Admin, + }; - _orgRoute = $"/org/{currentOrganisation.OrgId}"; + _orgRoute = $"/org/{currentOrganisation.OrgId}"; - _organisationClient = new OrganisationClient(httpClient, currentOrganisation); - } + _organisationClient = new OrganisationClient(httpClient, currentOrganisation); + } - public async Task should_return_a_detailed_organisation_model_when_calling_GetAsync() + [Test] + public async Task Should_return_a_detailed_organisation_model_when_calling_GetAsync() + { + // Arrange + var org = new Organisation { - // Arrange - var org = new Organisation(); + Id = "testId", + }; - _server - .Given(Request.Create().WithPath(_orgRoute).UsingGet()) - .RespondWith( - Response.Create() - .WithStatusCode(200) - .WithBody(JsonSerializer.Serialize(org))); + _server + .Given(Request.Create().WithPath(_orgRoute).UsingGet()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(org, _serializerOptions))); - // Act + // Act + var result = await _organisationClient.GetAsync(); - // Assert - } + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Id, Is.EqualTo(org.Id)); } } diff --git a/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs b/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs new file mode 100644 index 0000000..7c4329f --- /dev/null +++ b/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs @@ -0,0 +1,98 @@ +using System.Text.Json; +using Enclave.Sdk.Api.Clients; +using Enclave.Sdk.Api.Data.Account; +using Enclave.Sdk.Api.Data.Pagination; +using Enclave.Sdk.Api.Data.Tags; +using NUnit.Framework; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace Enclave.Sdk.Api.Tests.Clients; + +public class TagClientTests +{ + private TagsClient _tagClient; + private WireMockServer _server; + private string _orgRoute; + private JsonSerializerOptions _serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + [SetUp] + public void Setup() + { + _server = WireMockServer.Start(); + + var httpClient = new HttpClient + { + BaseAddress = new Uri(_server.Urls[0]), + }; + + var orgId = "testId"; + + _orgRoute = $"/org/{orgId}"; + + _tagClient = new TagsClient(httpClient, _orgRoute); + } + + [Test] + public async Task Should_return_a_list_of_tags_in_pagination_format() + { + // Arrange + var responseModel = new PaginatedResponseModel + { + Items = new List + { + new TagItem { Tag = "tag1", Keys = 12, DnsRecords = 1, Policies = 0, Systems = 3 }, + new TagItem { Tag = "tag2", Keys = 13, DnsRecords = 0, Policies = 43, Systems = 0 }, + }, + Links = new PaginationLinks(), + Metadata = new PaginationMetadata(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions))); + + // Act + var result = await _tagClient.GetAsync(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Items, Is.Not.Null); + } + + [Test] + public async Task Should_make_call_to_api_with_search_queryString() + { + // Arrange + var responseModel = new PaginatedResponseModel + { + Items = new List + { + new TagItem { Tag = "tag1", Keys = 12, DnsRecords = 1, Policies = 0, Systems = 3 }, + new TagItem { Tag = "tag2", Keys = 13, DnsRecords = 0, Policies = 43, Systems = 0 }, + }, + Links = new PaginationLinks(), + Metadata = new PaginationMetadata(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/tags").WithParam("search").UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions))); + + // Act + var result = await _tagClient.GetAsync(searchTerm: "test"); + + // Assert + Assert.That(result, Is.Not.Null); + } +} \ No newline at end of file diff --git a/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj b/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj index 2062fe0..71e7e81 100644 --- a/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj +++ b/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs b/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs index bb58082..e4aa306 100644 --- a/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs +++ b/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs @@ -1,18 +1,23 @@ -using Enclave.Sdk.Api.Data.Account; +using System.Text.Json; +using Enclave.Sdk.Api.Data.Account; +using FluentAssertions; using NUnit.Framework; -using System.Text.Json; -using System.Text.Json.Serialization; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; -namespace Enclave.Sdk.Api.Tests; +namespace Enclave.Sdk.Api.Tests.Clients; public class EnclaveClientTests { private EnclaveClient _client; private WireMockServer _server; + private JsonSerializerOptions _serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + [SetUp] public void Setup() { @@ -31,7 +36,7 @@ public async Task should_return_list_of_orgs_when_calling_GetOrganisationsAsync( { new AccountOrganisation { - OrgId = "testId", + OrgId = OrganisationId.New(), OrgName = "TestName", Role = UserOrganisationRole.Admin, }, @@ -43,20 +48,13 @@ public async Task should_return_list_of_orgs_when_calling_GetOrganisationsAsync( .RespondWith( Response.Create() .WithStatusCode(200) - .WithBody(JsonSerializer.Serialize(accountOrg))); + .WithBody(JsonSerializer.Serialize(accountOrg, _serializerOptions))); // Act var result = await _client.GetOrganisationsAsync(); // Assert - Assert.That(result.FirstOrDefault().OrgId, - Is.EqualTo(accountOrg.Orgs.FirstOrDefault().OrgId)); - } - - [Test] - public async Task should_throw_aggregate_exception_if_server_is_unreachable() - { - // Assert + result.FirstOrDefault().OrgId.Should().Be(accountOrg.Orgs.FirstOrDefault().OrgId); } [Test] @@ -68,9 +66,9 @@ public async Task should_throw_invalid_operation_exception_if_response_does_not_ .RespondWith( Response.Create() .WithStatusCode(200) - .WithBody(JsonSerializer.Serialize("{}"))); + .WithBody("null")); // Assert - Assert.Throws(() => _client.GetOrganisationsAsync().Wait()); + await _client.Invoking(c => c.GetOrganisationsAsync()).Should().ThrowAsync(); } } \ No newline at end of file diff --git a/tests/Enclave.Sdk.Tests/GlobalSuppressions.cs b/tests/Enclave.Sdk.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..450989e --- /dev/null +++ b/tests/Enclave.Sdk.Tests/GlobalSuppressions.cs @@ -0,0 +1,9 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "We are using variables for tests they do not need to be disposed at end of scope", Scope = "member", Target = "~M:Enclave.Sdk.Api.Tests.Clients.OrganisationClientTests.Setup")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "unit test methods use underscores for readability", Scope = "member", Target = "~M:Enclave.Sdk.Api.Tests.Clients.OrganisationClientTests.Should_return_a_detailed_organisation_model_when_calling_GetAsync~System.Threading.Tasks.Task")] From e34c016cf81495b3b66c5eb3d93a428204e37278 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 11:46:40 +0000 Subject: [PATCH 04/26] added documentation to existing clients and implimenmted fallback settings to enclave folder --- src/Enclave.Sdk/Clients/ClientBase.cs | 53 +++++++++++--- .../Clients/Interfaces/IOrganisationClient.cs | 71 ++++++++++++++++++- .../Clients/Interfaces/ITagsClient.cs | 17 ++++- src/Enclave.Sdk/Clients/OrganisationClient.cs | 44 ++++++++---- src/Enclave.Sdk/Clients/TagsClient.cs | 9 ++- src/Enclave.Sdk/Data/Builder.cs | 18 ++++- src/Enclave.Sdk/Data/EnclaveSettings.cs | 23 ++++++ src/Enclave.Sdk/Data/IDataModel.cs | 5 -- .../Data/Organisations/Organisation.cs | 15 ++-- .../Data/Organisations/OrganisationInvite.cs | 17 +++-- src/Enclave.Sdk/EnclaveClient.cs | 64 ++++++++++++++--- src/Enclave.Sdk/sdkSettings-example.json | 4 ++ .../Clients/OrganisationClientTests.cs | 2 +- 13 files changed, 280 insertions(+), 62 deletions(-) create mode 100644 src/Enclave.Sdk/Data/EnclaveSettings.cs delete mode 100644 src/Enclave.Sdk/Data/IDataModel.cs create mode 100644 src/Enclave.Sdk/sdkSettings-example.json diff --git a/src/Enclave.Sdk/Clients/ClientBase.cs b/src/Enclave.Sdk/Clients/ClientBase.cs index b49a0d5..cd53113 100644 --- a/src/Enclave.Sdk/Clients/ClientBase.cs +++ b/src/Enclave.Sdk/Clients/ClientBase.cs @@ -1,17 +1,31 @@ -using Enclave.Sdk.Api.Exceptions; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Mime; using System.Text; using System.Text.Json; +using Enclave.Sdk.Api.Exceptions; namespace Enclave.Sdk.Api.Clients; + +/// +/// Base class used for commonly accessed methods and properties for all clients. +/// public class ClientBase { + /// + /// HttpClient used for all clients API calls. + /// protected HttpClient HttpClient { get; private set; } + /// + /// Options required for desrializing an serializing JSON to the API. + /// protected JsonSerializerOptions JsonSerializerOptions { get; private set; } + /// + /// Constructor to setup all required fields this is called by all child classes. + /// + /// HttpClient with baseUrl of the API used for all calls. public ClientBase(HttpClient httpClient) { HttpClient = httpClient; @@ -21,6 +35,13 @@ public ClientBase(HttpClient httpClient) }; } + /// + /// Get a string content for use with HttpClient. + /// + /// the object type to encode. + /// the object to encode. + /// String content of object. + /// throws if data provided is null. protected StringContent Encode(TModel data) { if (data is null) @@ -29,12 +50,18 @@ protected StringContent Encode(TModel data) } var json = JsonSerializer.Serialize(data, JsonSerializerOptions); - var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); // use MediaTypeNames.Application.Json in Core 3.0+ and Standard 2.1+ + var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); return stringContent; } - protected async Task DeserializeAsync(HttpContent httpContent) + /// + /// Desreialise the httpContent. + /// + /// the object type to deserialise to. + /// httpContent from the api call. + /// the object of type specified. + protected async Task DeserialiseAsync(HttpContent httpContent) { if (httpContent is null) { @@ -45,6 +72,12 @@ protected StringContent Encode(TModel data) return await JsonSerializer.DeserializeAsync(contentStream, JsonSerializerOptions); } + /// + /// Check respoonse status codes for errors. + /// + /// response from an http call. + /// Throws if httpResponse is null. + /// throws if error codes are detected. protected async Task CheckStatusCodes(HttpResponseMessage httpResponse) { if (httpResponse is null) @@ -71,6 +104,11 @@ protected async Task CheckStatusCodes(HttpResponseMessage httpResponse) } } + /// + /// Checks model is not null. + /// + /// Type being checked. + /// object being checked. protected void CheckModel([NotNull] TModel? model) { if (model is null) @@ -79,12 +117,11 @@ protected void CheckModel([NotNull] TModel? model) } } + /// + /// Throws an error every time it's called. + /// + /// Throws every time this is called. [DoesNotReturn] private void Throw() => throw new InvalidOperationException("Return from API is null please ensure you've entered the correct data or raise an issue"); - - protected virtual string PrepareUrl(string url) - { - return url; - } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs index 34f9708..2b00701 100644 --- a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs @@ -3,39 +3,104 @@ namespace Enclave.Sdk.Api.Clients.Interfaces; +/// +/// Provides access to organisation level API calls and organisation related clients. +/// For more information please refer to our API docs at https://api.enclave.io/. +/// public interface IOrganisationClient { + /// + /// The organisation selected and the one used to create this client. + /// AccountOrganisation CurrentOrganisation { get; } + /// + /// An instance of AuthorityClient associated with the current organisaiton. + /// IAuthorityClient Authority { get; } + /// + /// An instance of DnsClient associated with the current organisaiton. + /// IDnsClient Dns { get; } + /// + /// An instance of EnrolmentKeysClient associated with the current organisaiton. + /// IEnrolmentKeysClient EnrolmentKeys { get; } + /// + /// An instance of LogsClient associated with the current organisaiton. + /// ILogsClient Logs { get; } + /// + /// An instance of PoliciesClient associated with the current organisaiton. + /// IPoliciesClient Policies { get; } + /// + /// An instance of SystemsClient associated with the current organisaiton. + /// ISystemsClient Systems { get; } + /// + /// An instance of TagsClient associated with the current organisaiton. + /// ITagsClient Tags { get; } + /// + /// An instance of UnapprovedSystemsClient associated with the current organisaiton. + /// IUnapprovedSystemsClient UnapprovedSystems { get; } - Task CancelInviteAync(string emailAddress); - + /// + /// Get more detail on your current organisaiton. + /// + /// A more detailed version of CurrentOrganisaiton. Task GetAsync(); + /// + /// Gets pricing options for the current organisaiton. + /// + /// A representation of the pricing options. Task GetOrganisationPricing(); + /// + /// Gets the users that have access to the current organisaiton. + /// + /// List of users associated with the current organisation. Task?> GetOrganisationUsersAsync(); - Task GetPendingInvitesAsync(); + /// + /// Get a list of invites that haven't been accepted. + /// + /// ReadOnlyList of pending invites. + Task> GetPendingInvitesAsync(); + /// + /// Invite a user provided they haven't already been invited. + /// + /// Email address of the user you want to invite. Task InviteUserAsync(string emailAddress); + /// + /// Cancel and invite before it's accepted. + /// + /// Email address of the user who's invite you want to revoke. + Task CancelInviteAync(string emailAddress); + + /// + /// Removes a user from the organisation. + /// + /// The id of the users you want to remove. Task RemoveUserAsync(string accountId); + /// + /// Patch request to update the organisation. + /// Use Builder.cs to help you generate the dictionary. + /// + /// Use Builder.cs to properly generate. + /// The updated organisation. Task UpdateAsync(Dictionary updatedModel); } diff --git a/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs index 0d7d53d..2e4194c 100644 --- a/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs +++ b/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs @@ -1,5 +1,20 @@ -namespace Enclave.Sdk.Api.Clients.Interfaces; +using Enclave.Sdk.Api.Data.Pagination; +using Enclave.Sdk.Api.Data.Tags; +namespace Enclave.Sdk.Api.Clients.Interfaces; + +/// +/// Access and search for tags. +/// public interface ITagsClient { + /// + /// Gets a paginated list of tags which can be searched and interated upon. + /// + /// a partial matching search term. + /// Sort order for the pagination. + /// which page number do you want to return. + /// How many tags per page. + /// a paginated response model with links to get the previous, next, first and last pages. + Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null); } \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs index e8715a8..4676d61 100644 --- a/src/Enclave.Sdk/Clients/OrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs @@ -4,28 +4,44 @@ namespace Enclave.Sdk.Api.Clients; +/// public class OrganisationClient : ClientBase, IOrganisationClient { + /// public AccountOrganisation CurrentOrganisation { get; private set; } + /// public IAuthorityClient Authority => throw new NotImplementedException(); + /// public IDnsClient Dns => throw new NotImplementedException(); + /// public IEnrolmentKeysClient EnrolmentKeys => throw new NotImplementedException(); + /// public ILogsClient Logs => throw new NotImplementedException(); + /// public IPoliciesClient Policies => throw new NotImplementedException(); + /// public ISystemsClient Systems => throw new NotImplementedException(); + /// public ITagsClient Tags => throw new NotImplementedException(); + /// public IUnapprovedSystemsClient UnapprovedSystems => throw new NotImplementedException(); private string _orgRoute; + /// + /// This constructor is called by EnclaveClient when setting up the OrganisationClient. + /// It also calls the ClientBase constructor. + /// + /// an instance of httpClient with a baseURL referencing the API. + /// The current organisaiton used for routing the API calls. public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrganisation) : base(httpClient) { @@ -33,61 +49,67 @@ public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrga _orgRoute = $"org/{CurrentOrganisation.OrgId}"; } + /// public async Task GetAsync() { var result = await HttpClient.GetAsync(_orgRoute); await CheckStatusCodes(result); - var model = await DeserializeAsync(result.Content); + var model = await DeserialiseAsync(result.Content); CheckModel(model); return model; } + /// public async Task UpdateAsync(Dictionary updatedModel) { using var encoded = Encode(updatedModel); var result = await HttpClient.PatchAsync(_orgRoute, encoded); await CheckStatusCodes(result); - var model = await DeserializeAsync(result.Content); + var model = await DeserialiseAsync(result.Content); CheckModel(model); return model; } + /// public async Task?> GetOrganisationUsersAsync() { var result = await HttpClient.GetAsync($"{_orgRoute}/users"); await CheckStatusCodes(result); - var model = await DeserializeAsync(result.Content); + var model = await DeserialiseAsync(result.Content); CheckModel(model); return model.Users; } + /// public async Task RemoveUserAsync(string accountId) { var result = await HttpClient.DeleteAsync($"{_orgRoute}/users/{accountId}"); await CheckStatusCodes(result); } - public async Task GetPendingInvitesAsync() + /// + public async Task> GetPendingInvitesAsync() { var result = await HttpClient.GetAsync($"{_orgRoute}/invites"); await CheckStatusCodes(result); - var model = await DeserializeAsync(result.Content); + var model = await DeserialiseAsync(result.Content); CheckModel(model); - return model; + return model.Invites; } + /// public async Task InviteUserAsync(string emailAddress) { using var encoded = Encode(new OrganisationInvite @@ -99,6 +121,7 @@ public async Task InviteUserAsync(string emailAddress) await CheckStatusCodes(result); } + /// public async Task CancelInviteAync(string emailAddress) { using var encoded = Encode(new OrganisationInvite @@ -117,21 +140,16 @@ public async Task CancelInviteAync(string emailAddress) await CheckStatusCodes(result); } + /// public async Task GetOrganisationPricing() { var result = await HttpClient.GetAsync($"{_orgRoute}/pricing"); await CheckStatusCodes(result); - var model = await DeserializeAsync(result.Content); + var model = await DeserialiseAsync(result.Content); CheckModel(model); return model; } - - protected override string PrepareUrl(string url) - { - var newUrl = $"{_orgRoute}{url}"; - return base.PrepareUrl(newUrl); - } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs index 68d9674..2689dfc 100644 --- a/src/Enclave.Sdk/Clients/TagsClient.cs +++ b/src/Enclave.Sdk/Clients/TagsClient.cs @@ -5,16 +5,23 @@ namespace Enclave.Sdk.Api.Clients; +/// public class TagsClient : ClientBase, ITagsClient { private string _orgRoute; + /// + /// Consutructor which will be called by organisationClient when it's created. + /// + /// an instance of httpClient with a baseURL referencing the API. + /// the orgRoute which specifies the orgId. public TagsClient(HttpClient httpClient, string orgRoute) : base(httpClient) { _orgRoute = orgRoute; } + /// public async Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null) { string? queryString = default; @@ -42,7 +49,7 @@ public async Task> GetAsync(string? searchTerm = await CheckStatusCodes(result); - var model = await DeserializeAsync>(result.Content); + var model = await DeserialiseAsync>(result.Content); CheckModel(model); diff --git a/src/Enclave.Sdk/Data/Builder.cs b/src/Enclave.Sdk/Data/Builder.cs index c703269..889f59d 100644 --- a/src/Enclave.Sdk/Data/Builder.cs +++ b/src/Enclave.Sdk/Data/Builder.cs @@ -2,11 +2,23 @@ namespace Enclave.Sdk.Api.Data; +/// +/// Class used to construct patch models. +/// +/// The Type we're updating. public class Builder - where TModel : IDataModel { private Dictionary _patchDictionary = new Dictionary(); + /// + /// Set a value in the global dictionary to the updated value. + /// + /// The type of the value you're updating. + /// Expression tree witht he property you want to update. + /// the new value. + /// Builder for fluent building./ + /// throws if either propExpr or newValue are null. + /// if the selected propExpr body is null. public Builder Set(Expression> propExpr, TValue newValue) { if (newValue is null) @@ -34,6 +46,10 @@ public Builder Set(Expression> propExpr, T return this; } + /// + /// Build the Dictionary and return it. then reset the dictionary so the builder can be reused. + /// + /// Dictionary built using the Set model in this class. public Dictionary Send() { try diff --git a/src/Enclave.Sdk/Data/EnclaveSettings.cs b/src/Enclave.Sdk/Data/EnclaveSettings.cs new file mode 100644 index 0000000..80fb34c --- /dev/null +++ b/src/Enclave.Sdk/Data/EnclaveSettings.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Data; + +/// +/// A representation of the enclave settings json file. +/// +public class EnclaveSettings +{ + /// + /// the bearer token from encalve. + /// + public string? BearerToken { get; set; } + + /// + /// The Api base url. + /// + public string? BaseUrl { get; set; } +} diff --git a/src/Enclave.Sdk/Data/IDataModel.cs b/src/Enclave.Sdk/Data/IDataModel.cs deleted file mode 100644 index 1d3b153..0000000 --- a/src/Enclave.Sdk/Data/IDataModel.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Enclave.Sdk.Api.Data; - -public interface IDataModel -{ -} diff --git a/src/Enclave.Sdk/Data/Organisations/Organisation.cs b/src/Enclave.Sdk/Data/Organisations/Organisation.cs index 70f9eee..e5d7c30 100644 --- a/src/Enclave.Sdk/Data/Organisations/Organisation.cs +++ b/src/Enclave.Sdk/Data/Organisations/Organisation.cs @@ -1,22 +1,17 @@ -using Enclave.Sdk.Api.Data.Organisations.Enum; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading.Tasks; +using Enclave.Sdk.Api.Data.Account; +using Enclave.Sdk.Api.Data.Organisations.Enum; namespace Enclave.Sdk.Api.Data.Organisations; /// /// Organisation properties model. /// -public class Organisation : IDataModel +public class Organisation { /// /// The organisation ID. /// - public string Id { get; init; } + public OrganisationId? Id { get; init; } /// /// The UTC timestamp at which the organisation was created. @@ -26,7 +21,7 @@ public class Organisation : IDataModel /// /// The name of the organisation. /// - public string Name { get; init; } + public string? Name { get; init; } /// /// The current plan the organisation is on. diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs index 839190a..ca066b3 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs @@ -4,16 +4,15 @@ using System.Text; using System.Threading.Tasks; -namespace Enclave.Sdk.Api.Data.Organisations +namespace Enclave.Sdk.Api.Data.Organisations; + +/// +/// Invite model. +/// +public class OrganisationInvite { /// - /// Invite model. + /// The email address of the user to invite. /// - public class OrganisationInvite - { - /// - /// The email address of the user to invite. - /// - public string EmailAddress { get; init; } - } + public string? EmailAddress { get; init; } } diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index 107064c..e360972 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -1,25 +1,39 @@ -using Enclave.Sdk.Api.Clients; -using Enclave.Sdk.Api.Clients.Interfaces; -using Enclave.Sdk.Api.Data.Account; -using System.Net; -using System.Net.Http.Headers; +using System.Net.Http.Headers; using System.Reflection; using System.Text.Json; +using Enclave.Sdk.Api.Clients; +using Enclave.Sdk.Api.Clients.Interfaces; +using Enclave.Sdk.Api.Data; +using Enclave.Sdk.Api.Data.Account; namespace Enclave.Sdk.Api; +/// +/// Our main entry point for all API work. +/// public class EnclaveClient { - public OrganisationClient? Organisation { get; private set; } - private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonSerializerOptions; - // TODO Make HttpClient and BaseUrl optional and for token pull it from .enclave folder if it's not supplied - public EnclaveClient(HttpClient httpClient, string baseUrl, string token) + /// + /// Setup all requirments for making api calls. + /// + /// an optional instance of httpClient. + /// optional instance of baseUrl if it's not provided it defaults to the enclave settings file. + /// optional instance of token if it's not provided it defaults to the enclave settings file. + public EnclaveClient(HttpClient? httpClient = default, string? baseUrl = default, string? token = default) { - _httpClient = httpClient; + if (token is null || baseUrl is null) + { + var settings = GetSettingsFile(); + token ??= settings?.BearerToken; + baseUrl ??= settings?.BaseUrl; + } + + _httpClient = httpClient ?? new HttpClient(); _httpClient.BaseAddress = new Uri(baseUrl); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var clientHeader = new ProductInfoHeaderValue("SDK", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); _httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); @@ -30,6 +44,11 @@ public EnclaveClient(HttpClient httpClient, string baseUrl, string token) }; } + /// + /// Gets a list of organisation associated to the authorised user. + /// + /// List of organisation containing the OrgId and Name and the users role in that organisation. + /// throws when the Api returns a null response. public async Task> GetOrganisationsAsync() { var result = await _httpClient.GetAsync("/account/orgs"); @@ -52,8 +71,33 @@ public async Task> GetOrganisationsAsync() return organisations.Orgs; } + /// + /// Create an organisationClient from an AccountOrganisation. + /// + /// the AccountOrganisation from GetOrganisationsAsync. + /// OrganisationClient that can be used for all following Api Calls related to an organisation. public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisation) { return new OrganisationClient(_httpClient, organisation); } + + private EnclaveSettings? GetSettingsFile() + { + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var location = $"{userProfile}\\.enclave"; + + try + { + using var streamReader = new StreamReader($"{location}\\sdkSettings.json"); + var json = streamReader.ReadToEnd(); + var settings = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + return settings; + } + catch + { + throw new ArgumentException("Can't find user settings file please refer to documentaiton for more information " + + "or provide a bearer token in the constructor"); + } + } } \ No newline at end of file diff --git a/src/Enclave.Sdk/sdkSettings-example.json b/src/Enclave.Sdk/sdkSettings-example.json new file mode 100644 index 0000000..52081ca --- /dev/null +++ b/src/Enclave.Sdk/sdkSettings-example.json @@ -0,0 +1,4 @@ +{ + "bearerToken": "", + "baseUrl": "" +} \ No newline at end of file diff --git a/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs b/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs index 614d589..b9253d4 100644 --- a/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs +++ b/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs @@ -47,7 +47,7 @@ public async Task Should_return_a_detailed_organisation_model_when_calling_GetAs // Arrange var org = new Organisation { - Id = "testId", + Id = OrganisationId.New(), }; _server From e04d37b26919718bea1dc8ebf3a534be03aafcec Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 13:31:07 +0000 Subject: [PATCH 05/26] happy path tests added along with using enclavesettings for enclaveclient --- src/Enclave.Sdk/Data/Account/AccountId.cs | 8 + src/Enclave.Sdk/Data/EnclaveSettings.cs | 7 + .../Data/Organisations/Organisation.cs | 2 +- .../Data/Organisations/OrganisationUser.cs | 2 +- src/Enclave.Sdk/EnclaveClient.cs | 22 +- ...-example.json => credentials-example.json} | 0 .../Clients/OrganisationClientTests.cs | 303 +++++++++++++++++- .../Clients/TagClientTests.cs | 7 +- .../Enclave.Sdk.Api.Tests.csproj | 1 + tests/Enclave.Sdk.Tests/EnclaveClientTests.cs | 19 +- tests/Enclave.Sdk.Tests/GlobalSuppressions.cs | 7 +- 11 files changed, 349 insertions(+), 29 deletions(-) create mode 100644 src/Enclave.Sdk/Data/Account/AccountId.cs rename src/Enclave.Sdk/{sdkSettings-example.json => credentials-example.json} (100%) diff --git a/src/Enclave.Sdk/Data/Account/AccountId.cs b/src/Enclave.Sdk/Data/Account/AccountId.cs new file mode 100644 index 0000000..319b21b --- /dev/null +++ b/src/Enclave.Sdk/Data/Account/AccountId.cs @@ -0,0 +1,8 @@ +using TypedIds; + +namespace Enclave.Sdk.Api.Data.Account; + +[TypedId] +public readonly partial struct AccountId +{ +} diff --git a/src/Enclave.Sdk/Data/EnclaveSettings.cs b/src/Enclave.Sdk/Data/EnclaveSettings.cs index 80fb34c..e492729 100644 --- a/src/Enclave.Sdk/Data/EnclaveSettings.cs +++ b/src/Enclave.Sdk/Data/EnclaveSettings.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Enclave.Sdk.Api.Data; @@ -20,4 +21,10 @@ public class EnclaveSettings /// The Api base url. /// public string? BaseUrl { get; set; } + + /// + /// Custom httpClient for use with all Client classes. + /// + [JsonIgnore] + public HttpClient? HttpClient { get; set; } } diff --git a/src/Enclave.Sdk/Data/Organisations/Organisation.cs b/src/Enclave.Sdk/Data/Organisations/Organisation.cs index e5d7c30..a234db9 100644 --- a/src/Enclave.Sdk/Data/Organisations/Organisation.cs +++ b/src/Enclave.Sdk/Data/Organisations/Organisation.cs @@ -11,7 +11,7 @@ public class Organisation /// /// The organisation ID. /// - public OrganisationId? Id { get; init; } + public OrganisationId Id { get; init; } /// /// The UTC timestamp at which the organisation was created. diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs index dbd77b0..5255524 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs @@ -15,7 +15,7 @@ public class OrganisationUser /// /// The account ID. /// - public string Id { get; init; } + public AccountId Id { get; init; } /// /// The user email address. diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index e360972..9cb121e 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -13,28 +13,26 @@ namespace Enclave.Sdk.Api; /// public class EnclaveClient { + private const string FallbackUrl = "https://api.enclave.io/"; + private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonSerializerOptions; /// /// Setup all requirments for making api calls. /// - /// an optional instance of httpClient. - /// optional instance of baseUrl if it's not provided it defaults to the enclave settings file. - /// optional instance of token if it's not provided it defaults to the enclave settings file. - public EnclaveClient(HttpClient? httpClient = default, string? baseUrl = default, string? token = default) + /// optional set of settings should you need to configure the client further such as your own httpClient. + public EnclaveClient(EnclaveSettings? settings = default) { - if (token is null || baseUrl is null) + if (settings is null) { - var settings = GetSettingsFile(); - token ??= settings?.BearerToken; - baseUrl ??= settings?.BaseUrl; + settings = GetSettingsFile(); } - _httpClient = httpClient ?? new HttpClient(); - _httpClient.BaseAddress = new Uri(baseUrl); + _httpClient = settings?.HttpClient ?? new HttpClient(); + _httpClient.BaseAddress = new Uri(settings?.BaseUrl ?? FallbackUrl); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", settings?.BearerToken); var clientHeader = new ProductInfoHeaderValue("SDK", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); _httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); @@ -88,7 +86,7 @@ public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisa try { - using var streamReader = new StreamReader($"{location}\\sdkSettings.json"); + using var streamReader = new StreamReader($"{location}\\credentials.json"); var json = streamReader.ReadToEnd(); var settings = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); diff --git a/src/Enclave.Sdk/sdkSettings-example.json b/src/Enclave.Sdk/credentials-example.json similarity index 100% rename from src/Enclave.Sdk/sdkSettings-example.json rename to src/Enclave.Sdk/credentials-example.json diff --git a/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs b/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs index b9253d4..2ea9947 100644 --- a/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs +++ b/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs @@ -1,8 +1,11 @@ -using Enclave.Sdk.Api.Clients; +using System.Text.Json; +using Enclave.Sdk.Api.Clients; +using Enclave.Sdk.Api.Data; using Enclave.Sdk.Api.Data.Account; using Enclave.Sdk.Api.Data.Organisations; +using FluentAssertions; using NUnit.Framework; -using System.Text.Json; +using WireMock.FluentAssertions; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; @@ -62,7 +65,299 @@ public async Task Should_return_a_detailed_organisation_model_when_calling_GetAs var result = await _organisationClient.GetAsync(); // Assert - Assert.That(result, Is.Not.Null); - Assert.That(result.Id, Is.EqualTo(org.Id)); + result.Should().NotBeNull(); + result.Id.Should().Be(org.Id); + } + + [Test] + public async Task Should_return_a_detailed_organisation_model_when_updating_with_UpdateAsync() + { + // Arrange + var org = new Organisation + { + Id = OrganisationId.New(), + Website = "newWebsite", + }; + + _server + .Given(Request.Create().WithPath(_orgRoute).UsingPatch()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(org, _serializerOptions))); + + var patchModel = new Builder().Set(x => x.Website, "newWebsite").Send(); + + // Act + var result = await _organisationClient.UpdateAsync(patchModel); + + // Assert + result.Should().NotBeNull(); + result.Website.Should().Be(org.Website); + } + + [Test] + public async Task Should_make_a_call_to_api_when_updating_with_UpdateAsync() + { + // Arrange + var org = new Organisation + { + Id = OrganisationId.New(), + Website = "newWebsite", + }; + + _server + .Given(Request.Create().WithPath(_orgRoute).UsingPatch()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(org, _serializerOptions))); + + var patchModel = new Builder().Set(x => x.Website, "newWebsite").Send(); + + // Act + var result = await _organisationClient.UpdateAsync(patchModel); + + // Assert + _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}"); + } + + [Test] + public async Task Should_return_a_list_of_organisation_users_when_calling_GetOrganisationUsersAsync() + { + // Arrange + var orgUsers = new OrganisationUsersTopLevel + { + Users = new List() + { + new OrganisationUser + { + Id = AccountId.New(), + FirstName = "testUser1", + LastName = "lastName1", + }, + new OrganisationUser + { + Id = AccountId.New(), + FirstName = "testUser2", + LastName = "lastName2", + }, + }, + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/users").UsingGet()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(orgUsers, _serializerOptions))); + + // Act + var result = await _organisationClient.GetOrganisationUsersAsync(); + + // Assert + result.Count.Should().Be(2); + } + + [Test] + public async Task Should_make_a_call_to_api_when_calling_GetOrganisationUsersAsync() + { + // Arrange + var orgUsers = new OrganisationUsersTopLevel + { + Users = new List(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/users").UsingGet()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(orgUsers, _serializerOptions))); + + // Act + var result = await _organisationClient.GetOrganisationUsersAsync(); + + // Assert + _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/users"); + } + + [Test] + public async Task Should_make_a_call_to_api_when_calling_RemoveUserAsync() + { + // Arrange + var accountId = "test"; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/users/{accountId}").UsingDelete()) + .RespondWith( + Response.Create() + .WithSuccess()); + // Act + await _organisationClient.RemoveUserAsync(accountId); + + // Assert + _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/users/{accountId}"); + } + + [Test] + public async Task Should_return_list_of_pending_invites_when_calling_GetPendingInvitesAsync() + { + // Arrange + var invites = new OrganisationPendingInvites() + { + Invites = new List + { + new OrganisationInvite { EmailAddress = "testEmail1" }, + new OrganisationInvite { EmailAddress = "testEmail2" }, + }, + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/invites").UsingGet()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(invites, _serializerOptions))); + + // Act + var result = await _organisationClient.GetPendingInvitesAsync(); + + // Assert + result.Should().NotBeNull(); + result.Count.Should().Be(2); + } + + [Test] + public async Task Should_make_a_call_to_api_when_calling_GetPendingInvitesAsync() + { + // Arrange + var invites = new OrganisationPendingInvites() + { + Invites = new List(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/invites").UsingGet()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(invites, _serializerOptions))); + + // Act + var result = await _organisationClient.GetPendingInvitesAsync(); + + // Assert + _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/invites"); + } + + [Test] + public async Task Should_make_a_call_to_api_when_calling_InviteUserAsync() + { + // Arrange + _server + .Given(Request.Create().WithPath($"{_orgRoute}/invites").UsingPost()) + .RespondWith( + Response.Create() + .WithSuccess()); + + // Act + await _organisationClient.InviteUserAsync("testEmailAddress"); + + // Assert + _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/invites"); + } + + [Test] + public async Task Should_make_a_call_to_api_when_calling_CancelInviteAync() + { + // Arrange + _server + .Given(Request.Create().WithPath($"{_orgRoute}/invites").UsingDelete()) + .RespondWith( + Response.Create() + .WithSuccess()); + + // Act + await _organisationClient.CancelInviteAync("testEmailAddress"); + + // Assert + _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/invites"); + } + + [Test] + public async Task Should_return_organisation_pricing_when_calling_GetOrganisationPricing() + { + // Arrange + var organisationPricing = new OrganisationPricing + { + LastBillingEvent = new OrganisationBillingEvent(), + Starter = new OrganisationPlanPricing + { + CurrencySymbol = "£", + Enabled = true, + Quantities = new List(), + }, + Pro = new OrganisationPlanPricing + { + CurrencySymbol = "£", + Enabled = true, + Quantities = new List(), + }, + Business = new OrganisationPlanPricing + { + CurrencySymbol = "£", + Enabled = true, + Quantities = new List(), + }, + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/pricing").UsingGet()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(organisationPricing, _serializerOptions))); + + // Act + var result = await _organisationClient.GetOrganisationPricing(); + + // Assert + result.Should().NotBeNull(); + result.Starter.CurrencySymbol.Should().Be("£"); + } + + [Test] + public async Task Should_make_a_call_to_api_when_calling_GetOrganisationPricing() + { + // Arrange + var organisationPricing = new OrganisationPricing + { + LastBillingEvent = new OrganisationBillingEvent(), + Starter = new OrganisationPlanPricing(), + Pro = new OrganisationPlanPricing(), + Business = new OrganisationPlanPricing(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/pricing").UsingGet()) + .RespondWith( + Response.Create() + .WithSuccess() + .WithHeader("Content-Type", "application/json") + .WithBody(JsonSerializer.Serialize(organisationPricing, _serializerOptions))); + + // Act + await _organisationClient.GetOrganisationPricing(); + + // Assert + _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/pricing"); } } diff --git a/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs b/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs index 7c4329f..0c6307d 100644 --- a/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs +++ b/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs @@ -3,6 +3,7 @@ using Enclave.Sdk.Api.Data.Account; using Enclave.Sdk.Api.Data.Pagination; using Enclave.Sdk.Api.Data.Tags; +using FluentAssertions; using NUnit.Framework; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; @@ -63,8 +64,8 @@ public async Task Should_return_a_list_of_tags_in_pagination_format() var result = await _tagClient.GetAsync(); // Assert - Assert.That(result, Is.Not.Null); - Assert.That(result.Items, Is.Not.Null); + result.Should().NotBeNull(); + result.Items.Should().NotBeNull(); } [Test] @@ -93,6 +94,6 @@ public async Task Should_make_call_to_api_with_search_queryString() var result = await _tagClient.GetAsync(searchTerm: "test"); // Assert - Assert.That(result, Is.Not.Null); + result.Should().NotBeNull(); } } \ No newline at end of file diff --git a/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj b/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj index 71e7e81..149c637 100644 --- a/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj +++ b/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs b/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs index e4aa306..e2655ff 100644 --- a/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs +++ b/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Enclave.Sdk.Api.Data; using Enclave.Sdk.Api.Data.Account; using FluentAssertions; using NUnit.Framework; @@ -6,14 +7,14 @@ using WireMock.ResponseBuilders; using WireMock.Server; -namespace Enclave.Sdk.Api.Tests.Clients; +namespace Enclave.Sdk.Api.Tests; public class EnclaveClientTests { private EnclaveClient _client; private WireMockServer _server; - private JsonSerializerOptions _serializerOptions = new JsonSerializerOptions + private readonly JsonSerializerOptions _serializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; @@ -21,13 +22,19 @@ public class EnclaveClientTests [SetUp] public void Setup() { - var httpClient = new HttpClient(); _server = WireMockServer.Start(); - _client = new EnclaveClient(httpClient, _server.Urls[0], string.Empty); + + var enclaveSettings = new EnclaveSettings + { + BaseUrl = _server.Urls[0], + HttpClient = new HttpClient(), + }; + + _client = new EnclaveClient(enclaveSettings); } [Test] - public async Task should_return_list_of_orgs_when_calling_GetOrganisationsAsync() + public async Task Should_return_list_of_orgs_when_calling_GetOrganisationsAsync() { // Arrange var accountOrg = new AccountOrganisationTopLevel @@ -58,7 +65,7 @@ public async Task should_return_list_of_orgs_when_calling_GetOrganisationsAsync( } [Test] - public async Task should_throw_invalid_operation_exception_if_response_does_not_contain_an_organisation_model() + public async Task Should_throw_invalid_operation_exception_if_response_does_not_contain_an_organisation_model() { // Arrange _server diff --git a/tests/Enclave.Sdk.Tests/GlobalSuppressions.cs b/tests/Enclave.Sdk.Tests/GlobalSuppressions.cs index 450989e..d3d3820 100644 --- a/tests/Enclave.Sdk.Tests/GlobalSuppressions.cs +++ b/tests/Enclave.Sdk.Tests/GlobalSuppressions.cs @@ -5,5 +5,8 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "We are using variables for tests they do not need to be disposed at end of scope", Scope = "member", Target = "~M:Enclave.Sdk.Api.Tests.Clients.OrganisationClientTests.Setup")] -[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "unit test methods use underscores for readability", Scope = "member", Target = "~M:Enclave.Sdk.Api.Tests.Clients.OrganisationClientTests.Should_return_a_detailed_organisation_model_when_calling_GetAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "We are using variables for tests they do not need to be disposed at end of scope", Scope = "namespace", Target = "Enclave.Sdk.Api.Tests")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "unit test methods use underscores for readability", Scope = "namespace", Target = "Enclave.Sdk.Api.Tests")] + +[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "We are using variables for tests they do not need to be disposed at end of scope", Scope = "namespace", Target = "Enclave.Sdk.Api.Tests.Clients")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "unit test methods use underscores for readability", Scope = "namespace", Target = "Enclave.Sdk.Api.Tests.Clients")] \ No newline at end of file From c02efdc445f52b69be43de177b9ffa2dad06bef5 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 14:58:19 +0000 Subject: [PATCH 06/26] refactor of tags method and added unit tests happy path again --- src/Enclave.Sdk/Clients/TagsClient.cs | 39 ++++-- .../Clients/TagClientTests.cs | 128 ++++++++++++++++-- 2 files changed, 145 insertions(+), 22 deletions(-) diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs index 2689dfc..944ddd0 100644 --- a/src/Enclave.Sdk/Clients/TagsClient.cs +++ b/src/Enclave.Sdk/Clients/TagsClient.cs @@ -1,7 +1,6 @@ using Enclave.Sdk.Api.Clients.Interfaces; using Enclave.Sdk.Api.Data.Pagination; using Enclave.Sdk.Api.Data.Tags; -using System.Text; namespace Enclave.Sdk.Api.Clients; @@ -24,35 +23,49 @@ public TagsClient(HttpClient httpClient, string orgRoute) /// public async Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null) { + var queryString = BuildQueryString(searchTerm, sortOrder, pageNumber, perPage); + + var result = await HttpClient.GetAsync($"{_orgRoute}/tags?{queryString}"); + + await CheckStatusCodes(result); + + var model = await DeserialiseAsync>(result.Content); + + CheckModel(model); + + return model; + } + + private string? BuildQueryString(string? searchTerm, TagQuerySortOrder? sortOrder, int? pageNumber, int? perPage) + { + var queryStringSet = false; string? queryString = default; if (searchTerm is not null) { queryString += $"search={searchTerm}"; + queryStringSet = true; } if (sortOrder is not null) { - queryString += $"sort={sortOrder}"; + var delimiter = queryStringSet ? "&" : string.Empty; + queryString += $"{delimiter}sort={sortOrder}"; + queryStringSet = true; } if (pageNumber is not null) { - queryString += $"page={pageNumber}"; + var delimiter = queryStringSet ? "&" : string.Empty; + queryString += $"{delimiter}page={pageNumber}"; + queryStringSet = true; } if (perPage is not null) { - queryString += $"per_page={perPage}"; + var delimiter = queryStringSet ? "&" : string.Empty; + queryString += $"{delimiter}per_page={perPage}"; } - var result = await HttpClient.GetAsync($"{_orgRoute}/tags?{queryString}"); - - await CheckStatusCodes(result); - - var model = await DeserialiseAsync>(result.Content); - - CheckModel(model); - - return model; + return queryString; } } diff --git a/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs b/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs index 0c6307d..5675c01 100644 --- a/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs +++ b/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs @@ -1,10 +1,10 @@ using System.Text.Json; using Enclave.Sdk.Api.Clients; -using Enclave.Sdk.Api.Data.Account; using Enclave.Sdk.Api.Data.Pagination; using Enclave.Sdk.Api.Data.Tags; using FluentAssertions; using NUnit.Framework; +using WireMock.FluentAssertions; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; @@ -72,28 +72,138 @@ public async Task Should_return_a_list_of_tags_in_pagination_format() public async Task Should_make_call_to_api_with_search_queryString() { // Arrange + var searchTerm = "test"; + var responseModel = new PaginatedResponseModel { - Items = new List - { - new TagItem { Tag = "tag1", Keys = 12, DnsRecords = 1, Policies = 0, Systems = 3 }, - new TagItem { Tag = "tag2", Keys = 13, DnsRecords = 0, Policies = 43, Systems = 0 }, - }, + Items = new List(), Links = new PaginationLinks(), Metadata = new PaginationMetadata(), }; _server - .Given(Request.Create().WithPath($"{_orgRoute}/tags").WithParam("search").UsingGet()) + .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet()) .RespondWith( Response.Create() .WithStatusCode(200) .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions))); // Act - var result = await _tagClient.GetAsync(searchTerm: "test"); + var result = await _tagClient.GetAsync(searchTerm: searchTerm); // Assert - result.Should().NotBeNull(); + _server.Should().HaveReceivedACall().AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?search={searchTerm}"); + } + + [Test] + public async Task Should_make_call_to_api_with_sort_queryString() + { + // Arrange + var sortEnum = TagQuerySortOrder.Alphabetical; + + var responseModel = new PaginatedResponseModel + { + Items = new List(), + Links = new PaginationLinks(), + Metadata = new PaginationMetadata(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions))); + + // Act + var result = await _tagClient.GetAsync(sortOrder: sortEnum); + + // Assert + _server.Should().HaveReceivedACall().AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?sort={sortEnum}"); + } + + [Test] + public async Task Should_make_call_to_api_with_page_queryString() + { + // Arrange + var pageNumber = 1; + + var responseModel = new PaginatedResponseModel + { + Items = new List(), + Links = new PaginationLinks(), + Metadata = new PaginationMetadata(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions))); + + // Act + var result = await _tagClient.GetAsync(pageNumber: pageNumber); + + // Assert + _server.Should().HaveReceivedACall().AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?page={pageNumber}"); + } + + [Test] + public async Task Should_make_call_to_api_with_per_page_queryString() + { + // Arrange + var perPage = 1; + + var responseModel = new PaginatedResponseModel + { + Items = new List(), + Links = new PaginationLinks(), + Metadata = new PaginationMetadata(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions))); + + // Act + var result = await _tagClient.GetAsync(perPage: perPage); + + // Assert + _server.Should().HaveReceivedACall().AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?per_page={perPage}"); + } + + [Test] + public async Task Should_make_call_to_api_with_all_queryStrings() + { + // Arrange + var searchTerm = "test"; + var sortEnum = TagQuerySortOrder.Alphabetical; + var perPage = 1; + var pageNumber = 1; + + var responseModel = new PaginatedResponseModel + { + Items = new List(), + Links = new PaginationLinks(), + Metadata = new PaginationMetadata(), + }; + + _server + .Given(Request.Create().WithPath($"{_orgRoute}/tags").UsingGet()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonSerializer.Serialize(responseModel, _serializerOptions))); + + // Act + var result = await _tagClient.GetAsync(searchTerm: searchTerm, sortOrder: sortEnum, pageNumber: pageNumber, perPage: perPage); + + // Assert + _server.Should().HaveReceivedACall() + .AtAbsoluteUrl($"{_server.Urls[0]}{_orgRoute}/tags?search={searchTerm}&sort={sortEnum}&page={pageNumber}&per_page={perPage}"); } } \ No newline at end of file From 12dd8b5bf652cc69cb9ca3f6c468eecccf56ecd2 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 15:12:11 +0000 Subject: [PATCH 07/26] added some more documentation --- src/Enclave.Sdk/Data/Account/AccountId.cs | 3 +++ .../Data/Account/AccountOrganisation.cs | 14 +++++++++++--- src/Enclave.Sdk/Data/Account/OrganisationId.cs | 3 +++ .../Data/Account/UserOrganisationRole.cs | 1 + 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Enclave.Sdk/Data/Account/AccountId.cs b/src/Enclave.Sdk/Data/Account/AccountId.cs index 319b21b..8cf6769 100644 --- a/src/Enclave.Sdk/Data/Account/AccountId.cs +++ b/src/Enclave.Sdk/Data/Account/AccountId.cs @@ -2,6 +2,9 @@ namespace Enclave.Sdk.Api.Data.Account; +/// +/// AccountId struct to distinguish between Id types. +/// [TypedId] public readonly partial struct AccountId { diff --git a/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs index 3891175..20fee8a 100644 --- a/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs +++ b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs @@ -1,8 +1,10 @@ namespace Enclave.Sdk.Api.Data.Account; +/// +/// Contains the role an account has within an organisation. +/// public class AccountOrganisation { - // TODO: Set as Organisation Data Type /// /// The organisation ID. /// @@ -11,7 +13,7 @@ public class AccountOrganisation /// /// The organisation name. /// - public string OrgName { get; init; } + public string? OrgName { get; init; } /// /// The user's role within the organisation. @@ -19,7 +21,13 @@ public class AccountOrganisation public UserOrganisationRole Role { get; init; } } +/// +/// Account orgs response model. +/// public class AccountOrganisationTopLevel { - public List Orgs { get; init; } + /// + /// The set of organisations. + /// + public List? Orgs { get; init; } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Account/OrganisationId.cs b/src/Enclave.Sdk/Data/Account/OrganisationId.cs index e8185d8..5907c18 100644 --- a/src/Enclave.Sdk/Data/Account/OrganisationId.cs +++ b/src/Enclave.Sdk/Data/Account/OrganisationId.cs @@ -2,6 +2,9 @@ namespace Enclave.Sdk.Api.Data.Account; +/// +/// OrganisaitonId struct to distinguish between Id types. +/// [TypedId] public readonly partial struct OrganisationId { diff --git a/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs b/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs index 70603d1..daf79bd 100644 --- a/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs +++ b/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; namespace Enclave.Sdk.Api.Data.Account; + public enum UserOrganisationRole { Owner, From 06ccf7a323c0fed3fe21e0d2919cd718aba108e2 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 15:23:02 +0000 Subject: [PATCH 08/26] more documentaiton and code cleanup --- .../Clients/UnapprovedSystemsClient.cs | 4 +-- .../Data/Account/UserOrganisationRole.cs | 8 +----- src/Enclave.Sdk/Data/Builder.cs | 2 +- src/Enclave.Sdk/Data/EnclaveSettings.cs | 7 +----- .../Data/Organisations/OrganisationInvite.cs | 8 +----- .../OrganisationPendingInvites.cs | 8 +----- .../Organisations/OrganisationPlanPricing.cs | 4 +-- .../Data/Organisations/OrganisationPricing.cs | 8 +----- .../Data/Organisations/OrganisationUser.cs | 11 +++----- .../Data/Pagination/PaginatedResponseModel.cs | 8 +----- .../Data/Pagination/PaginationLinks.cs | 8 +----- .../Data/Pagination/PaginationMetadata.cs | 8 +----- src/Enclave.Sdk/Enclave.Sdk.Api.csproj | 2 +- src/Enclave.Sdk/Exceptions/ApiException.cs | 25 ++++++++++++++++++- 14 files changed, 40 insertions(+), 71 deletions(-) diff --git a/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs index 4075970..5eebb5f 100644 --- a/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs +++ b/src/Enclave.Sdk/Clients/UnapprovedSystemsClient.cs @@ -11,6 +11,4 @@ public UnapprovedSystemsClient(HttpClient httpClient, string orgRoute) { _orgRoute = orgRoute; } - - -} +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs b/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs index daf79bd..d5af46d 100644 --- a/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs +++ b/src/Enclave.Sdk/Data/Account/UserOrganisationRole.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Enclave.Sdk.Api.Data.Account; +namespace Enclave.Sdk.Api.Data.Account; public enum UserOrganisationRole { diff --git a/src/Enclave.Sdk/Data/Builder.cs b/src/Enclave.Sdk/Data/Builder.cs index 889f59d..dd12659 100644 --- a/src/Enclave.Sdk/Data/Builder.cs +++ b/src/Enclave.Sdk/Data/Builder.cs @@ -16,7 +16,7 @@ public class Builder /// The type of the value you're updating. /// Expression tree witht he property you want to update. /// the new value. - /// Builder for fluent building./ + /// Builder for fluent building. /// throws if either propExpr or newValue are null. /// if the selected propExpr body is null. public Builder Set(Expression> propExpr, TValue newValue) diff --git a/src/Enclave.Sdk/Data/EnclaveSettings.cs b/src/Enclave.Sdk/Data/EnclaveSettings.cs index e492729..a862f86 100644 --- a/src/Enclave.Sdk/Data/EnclaveSettings.cs +++ b/src/Enclave.Sdk/Data/EnclaveSettings.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading.Tasks; +using System.Text.Json.Serialization; namespace Enclave.Sdk.Api.Data; diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs index ca066b3..1129ce2 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Enclave.Sdk.Api.Data.Organisations; +namespace Enclave.Sdk.Api.Data.Organisations; /// /// Invite model. diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs index 4c33de9..7370c21 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Enclave.Sdk.Api.Data.Organisations; +namespace Enclave.Sdk.Api.Data.Organisations; /// /// Model for the pending list of org invites. diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs index 9a50f03..23971a3 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs @@ -9,7 +9,7 @@ public class OrganisationPlanPricing /// /// The appropriate currency symbol. /// - public string CurrencySymbol { get; init; } + public string? CurrencySymbol { get; init; } /// /// Whether or not this plan is enabled. @@ -19,5 +19,5 @@ public class OrganisationPlanPricing /// /// The quantities of systems available in this plan. /// - public IReadOnlyList Quantities { get; init; } + public IReadOnlyList? Quantities { get; init; } } diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs index cd38b66..0dae807 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Enclave.Sdk.Api.Data.Organisations; +namespace Enclave.Sdk.Api.Data.Organisations; public class OrganisationPricing { diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs index 5255524..9b847fc 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs @@ -1,9 +1,4 @@ using Enclave.Sdk.Api.Data.Account; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Enclave.Sdk.Api.Data.Organisations; @@ -20,17 +15,17 @@ public class OrganisationUser /// /// The user email address. /// - public string EmailAddress { get; init; } + public string? EmailAddress { get; init; } /// /// The user first name. /// - public string FirstName { get; init; } + public string? FirstName { get; init; } /// /// The user last name. /// - public string LastName { get; init; } + public string? LastName { get; init; } /// /// The UTC timestamp for when the user joined the organisation. diff --git a/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs b/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs index f687b56..6018bb9 100644 --- a/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs +++ b/src/Enclave.Sdk/Data/Pagination/PaginatedResponseModel.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Enclave.Sdk.Api.Data.Pagination; +namespace Enclave.Sdk.Api.Data.Pagination; /// /// Response model for paginated data. diff --git a/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs b/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs index cbb1201..d72c346 100644 --- a/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs +++ b/src/Enclave.Sdk/Data/Pagination/PaginationLinks.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Enclave.Sdk.Api.Data.Pagination; +namespace Enclave.Sdk.Api.Data.Pagination; /// /// Defines the available pagination links. diff --git a/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs b/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs index 77e9102..e66fee8 100644 --- a/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs +++ b/src/Enclave.Sdk/Data/Pagination/PaginationMetadata.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Enclave.Sdk.Api.Data.Pagination; +namespace Enclave.Sdk.Api.Data.Pagination; /// /// Defines the metadata attached to a paginated response. diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj index b72e505..fdfa424 100644 --- a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj +++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Enclave.Sdk/Exceptions/ApiException.cs b/src/Enclave.Sdk/Exceptions/ApiException.cs index db7bb7d..2d0f949 100644 --- a/src/Enclave.Sdk/Exceptions/ApiException.cs +++ b/src/Enclave.Sdk/Exceptions/ApiException.cs @@ -3,22 +3,45 @@ namespace Enclave.Sdk.Api.Exceptions; +/// +/// Exception used for Api specific errors. +/// public class ApiException : Exception { + /// + /// Http Status Code. + /// public HttpStatusCode StatusCode { get; private set; } + /// + /// Response represented as a string. + /// public string Response { get; private set; } + /// + /// Http Response Headers. + /// public HttpResponseHeaders Headers { get; private set; } + /// + /// Constructor for generating an ApiException. + /// + /// user defined error message. + /// http status code. + /// http response as string. + /// HttpResponseHeaders. public ApiException(string message, HttpStatusCode statusCode, string response, HttpResponseHeaders headers) : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + (response == null ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length))) { StatusCode = statusCode; - Response = response; + Response = response ?? string.Empty; Headers = headers; } + /// + /// Exception as string. + /// + /// Exception as a string public override string ToString() { return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); From fb08853d94f66e41997be92706ab2d2a3ac9c135 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 15:43:21 +0000 Subject: [PATCH 09/26] updated to latest TypeIds version --- src/Enclave.Sdk/Enclave.Sdk.Api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj index fdfa424..74a1e55 100644 --- a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj +++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj @@ -9,7 +9,7 @@ - + From 105c5f5aff8abd0fdec05ca412c00e23ca53a8e8 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 15:56:23 +0000 Subject: [PATCH 10/26] Added build.yml and gitversion.yml --- .github/workflows/sdk-api-build.yml | 60 +++++++++++++++++++++++++++++ Enclave.Sdk.Api.sln | 2 + GitVersion.yml | 11 ++++++ 3 files changed, 73 insertions(+) create mode 100644 .github/workflows/sdk-api-build.yml create mode 100644 GitVersion.yml diff --git a/.github/workflows/sdk-api-build.yml b/.github/workflows/sdk-api-build.yml new file mode 100644 index 0000000..fda9c00 --- /dev/null +++ b/.github/workflows/sdk-api-build.yml @@ -0,0 +1,60 @@ +name: SDK API Build + +on: + push: + branches: [ develop, main ] + pull_request: + branches: [ develop ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@v0.9.7 + with: + versionSpec: '5.x' + + - name: Determine Version + id: gitversion + uses: gittools/actions/gitversion/execute@v0.9.7 + + - name: Setup .NET 5 (SDK) + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + source-url: https://nuget.pkg.github.com/enclave-networks/index.json + env: + NUGET_AUTH_TOKEN: ${{github.token}} + + - name: Build + run: dotnet build Enclave.Sdk.Api.sln -c Release /p:Version=${{ steps.gitversion.outputs.SemVer }} + + - name: Unit Tests + working-directory: tests/Enclave.Sdk.Api.Tests + run: dotnet test -c Release + + - name: Push Github Source Packages + if: github.event_name == 'push' + run: dotnet nuget push src/**/*${{ steps.gitversion.outputs.SemVer }}.nupkg --api-key ${{github.token}} -s https://nuget.pkg.github.com/enclave-networks/index.json --skip-duplicate --no-symbols true + + - name: Push Github Test Tools Package + if: github.event_name == 'push' + run: dotnet nuget push test/**/*${{ steps.gitversion.outputs.SemVer }}.nupkg --api-key ${{github.token}} -s https://nuget.pkg.github.com/enclave-networks/index.json --skip-duplicate --no-symbols true + + - name: Create Release + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.gitversion.outputs.SemVer }} + release_name: Release v${{ steps.gitversion.outputs.SemVer }} + body: Latest SDK API Release \ No newline at end of file diff --git a/Enclave.Sdk.Api.sln b/Enclave.Sdk.Api.sln index d969f82..864e66f 100644 --- a/Enclave.Sdk.Api.sln +++ b/Enclave.Sdk.Api.sln @@ -17,6 +17,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BD898880-875C-43EA-B22C-A7C801CFA607}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + GitVersion.yml = GitVersion.yml + .github\workflows\sdk-api-build.yml = .github\workflows\sdk-api-build.yml EndProjectSection EndProject Global diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..fc2356f --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,11 @@ +assembly-versioning-scheme: None +mode: ContinuousDelivery +next-version: 0.0.1 +branches: + main: + mode: ContinuousDelivery + develop: + increment: Patch +ignore: + sha: [] +merge-message-formats: {} From eebd84b7f5126a6ce7e5f1804cf578fa19cdd9b2 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 15:58:27 +0000 Subject: [PATCH 11/26] version step up --- .github/workflows/sdk-api-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sdk-api-build.yml b/.github/workflows/sdk-api-build.yml index fda9c00..dbd128e 100644 --- a/.github/workflows/sdk-api-build.yml +++ b/.github/workflows/sdk-api-build.yml @@ -26,10 +26,10 @@ jobs: id: gitversion uses: gittools/actions/gitversion/execute@v0.9.7 - - name: Setup .NET 5 (SDK) + - name: Setup .NET 6 (SDK) uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x source-url: https://nuget.pkg.github.com/enclave-networks/index.json env: NUGET_AUTH_TOKEN: ${{github.token}} From 0fb6e8590fe03801638b88e2c706305ff24a26a0 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 16:02:55 +0000 Subject: [PATCH 12/26] Moved tests to correct folder matching csproj --- Enclave.Sdk.Api.sln | 2 +- .../Clients/OrganisationClientTests.cs | 0 .../Clients/TagClientTests.cs | 0 .../Enclave.Sdk.Api.Tests.csproj | 0 .../EnclaveClientTests.cs | 0 .../GlobalSuppressions.cs | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename tests/{Enclave.Sdk.Tests => Enclave.Sdk.Api.Tests}/Clients/OrganisationClientTests.cs (100%) rename tests/{Enclave.Sdk.Tests => Enclave.Sdk.Api.Tests}/Clients/TagClientTests.cs (100%) rename tests/{Enclave.Sdk.Tests => Enclave.Sdk.Api.Tests}/Enclave.Sdk.Api.Tests.csproj (100%) rename tests/{Enclave.Sdk.Tests => Enclave.Sdk.Api.Tests}/EnclaveClientTests.cs (100%) rename tests/{Enclave.Sdk.Tests => Enclave.Sdk.Api.Tests}/GlobalSuppressions.cs (100%) diff --git a/Enclave.Sdk.Api.sln b/Enclave.Sdk.Api.sln index 864e66f..d5378fa 100644 --- a/Enclave.Sdk.Api.sln +++ b/Enclave.Sdk.Api.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api", "src\Enclave.Sdk\Enclave.Sdk.Api.csproj", "{AE9A2DA2-DBA1-4A0F-AE39-26E777C2CDF4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api.Tests", "tests\Enclave.Sdk.Tests\Enclave.Sdk.Api.Tests.csproj", "{AC896965-74E9-405F-BA4E-E9D7336B906B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api.Tests", "tests\Enclave.Sdk.Api.Tests\Enclave.Sdk.Api.Tests.csproj", "{AC896965-74E9-405F-BA4E-E9D7336B906B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CA499537-7F6B-4E85-934A-04368AF78E9E}" ProjectSection(SolutionItems) = preProject diff --git a/tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs b/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs similarity index 100% rename from tests/Enclave.Sdk.Tests/Clients/OrganisationClientTests.cs rename to tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs diff --git a/tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs b/tests/Enclave.Sdk.Api.Tests/Clients/TagClientTests.cs similarity index 100% rename from tests/Enclave.Sdk.Tests/Clients/TagClientTests.cs rename to tests/Enclave.Sdk.Api.Tests/Clients/TagClientTests.cs diff --git a/tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj b/tests/Enclave.Sdk.Api.Tests/Enclave.Sdk.Api.Tests.csproj similarity index 100% rename from tests/Enclave.Sdk.Tests/Enclave.Sdk.Api.Tests.csproj rename to tests/Enclave.Sdk.Api.Tests/Enclave.Sdk.Api.Tests.csproj diff --git a/tests/Enclave.Sdk.Tests/EnclaveClientTests.cs b/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs similarity index 100% rename from tests/Enclave.Sdk.Tests/EnclaveClientTests.cs rename to tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs diff --git a/tests/Enclave.Sdk.Tests/GlobalSuppressions.cs b/tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs similarity index 100% rename from tests/Enclave.Sdk.Tests/GlobalSuppressions.cs rename to tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs From 59e57496096880b32dcfa72a13d31c98d1d5e4b4 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 16:11:42 +0000 Subject: [PATCH 13/26] Updated gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b6113d3..4e3ee73 100644 --- a/.gitignore +++ b/.gitignore @@ -348,5 +348,4 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ -tests/Enclave.Sdk.Tests/IntegrationTests.cs -src/Enclave.Sdk/test.txt +tests/Enclave.Sdk.Api.Tests/IntegrationTests.cs From d5bf0e33220605217f75ead129d4c314f2cf36cd Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 16:16:39 +0000 Subject: [PATCH 14/26] removed unneeded step and now building nuget package --- .github/workflows/sdk-api-build.yml | 4 ---- src/Enclave.Sdk/Enclave.Sdk.Api.csproj | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/sdk-api-build.yml b/.github/workflows/sdk-api-build.yml index dbd128e..0af8a75 100644 --- a/.github/workflows/sdk-api-build.yml +++ b/.github/workflows/sdk-api-build.yml @@ -44,10 +44,6 @@ jobs: - name: Push Github Source Packages if: github.event_name == 'push' run: dotnet nuget push src/**/*${{ steps.gitversion.outputs.SemVer }}.nupkg --api-key ${{github.token}} -s https://nuget.pkg.github.com/enclave-networks/index.json --skip-duplicate --no-symbols true - - - name: Push Github Test Tools Package - if: github.event_name == 'push' - run: dotnet nuget push test/**/*${{ steps.gitversion.outputs.SemVer }}.nupkg --api-key ${{github.token}} -s https://nuget.pkg.github.com/enclave-networks/index.json --skip-duplicate --no-symbols true - name: Create Release if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj index 74a1e55..2b20772 100644 --- a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj +++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj @@ -6,6 +6,7 @@ enable enable True + True From ce1139cf077831b080fa4a6d80217edb8ae48800 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 16:21:17 +0000 Subject: [PATCH 15/26] slight change to csproj should hopefully now build --- src/Enclave.Sdk/Enclave.Sdk.Api.csproj | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj index 2b20772..0493ae9 100644 --- a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj +++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj @@ -1,16 +1,18 @@  - - 10.0 - net6.0 - enable - enable - True - True - + + 10.0 + net6.0 + Library + enable + enable + True + True + false + - - - + + + From f3e7c5e300e9601a5ceedaf7a904197849193ffd Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 16:26:02 +0000 Subject: [PATCH 16/26] added repository url --- Directory.Build.props | 1 + Enclave.Sdk.Api.sln | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 76fcced..74fd518 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,7 @@ 9.0 + https://github.com/enclave-networks/sdk enable AllEnabledByDefault diff --git a/Enclave.Sdk.Api.sln b/Enclave.Sdk.Api.sln index d5378fa..4476e76 100644 --- a/Enclave.Sdk.Api.sln +++ b/Enclave.Sdk.Api.sln @@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api.Tests", "te EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CA499537-7F6B-4E85-934A-04368AF78E9E}" ProjectSection(SolutionItems) = preProject - src\Directory.Build.props = src\Directory.Build.props + Directory.Build.props = Directory.Build.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{822BFA5E-EC93-45BF-B36E-1CAD1E7D88B1}" From 73e2275cba16f1d9b5f41700d78993cb92d0981a Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Tue, 23 Nov 2021 16:35:53 +0000 Subject: [PATCH 17/26] Fixed repo url --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 74fd518..ad6badf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ 9.0 - https://github.com/enclave-networks/sdk + https://github.com/enclave-networks/Enclave.Sdk.Api enable AllEnabledByDefault From c690a1faec17a8f6bd64edab5cea84807d0d56bc Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Wed, 24 Nov 2021 16:11:42 +0000 Subject: [PATCH 18/26] Lots of changes pased on PR #1 --- Enclave.Sdk.Api.sln | 2 +- .../Directory.Build.props | 0 src/Enclave.Sdk/Clients/ClientBase.cs | 34 ++++++------- .../Clients/Interfaces/IOrganisationClient.cs | 20 ++++---- .../Clients/Interfaces/ITagsClient.cs | 6 +-- src/Enclave.Sdk/Clients/OrganisationClient.cs | 48 +++++++++---------- src/Enclave.Sdk/Clients/TagsClient.cs | 4 +- src/Enclave.Sdk/Data/Builder.cs | 6 +-- src/Enclave.Sdk/Data/EnclaveSettings.cs | 25 ---------- src/Enclave.Sdk/EnclaveClient.cs | 47 +++++++++--------- src/Enclave.Sdk/EnclaveClientOptions.cs | 33 +++++++++++++ ...ApiException.cs => EnclaveApiException.cs} | 9 ++-- src/Enclave.Sdk/credentials-example.json | 2 +- .../EnclaveClientTests.cs | 2 +- .../GlobalSuppressions.cs | 7 +-- 15 files changed, 125 insertions(+), 120 deletions(-) rename Directory.Build.props => src/Directory.Build.props (100%) delete mode 100644 src/Enclave.Sdk/Data/EnclaveSettings.cs create mode 100644 src/Enclave.Sdk/EnclaveClientOptions.cs rename src/Enclave.Sdk/Exceptions/{ApiException.cs => EnclaveApiException.cs} (73%) diff --git a/Enclave.Sdk.Api.sln b/Enclave.Sdk.Api.sln index 4476e76..d5378fa 100644 --- a/Enclave.Sdk.Api.sln +++ b/Enclave.Sdk.Api.sln @@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Enclave.Sdk.Api.Tests", "te EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CA499537-7F6B-4E85-934A-04368AF78E9E}" ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props + src\Directory.Build.props = src\Directory.Build.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{822BFA5E-EC93-45BF-B36E-1CAD1E7D88B1}" diff --git a/Directory.Build.props b/src/Directory.Build.props similarity index 100% rename from Directory.Build.props rename to src/Directory.Build.props diff --git a/src/Enclave.Sdk/Clients/ClientBase.cs b/src/Enclave.Sdk/Clients/ClientBase.cs index cd53113..23993f7 100644 --- a/src/Enclave.Sdk/Clients/ClientBase.cs +++ b/src/Enclave.Sdk/Clients/ClientBase.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using System.Net.Http.Json; using System.Net.Mime; using System.Text; using System.Text.Json; @@ -10,23 +11,23 @@ namespace Enclave.Sdk.Api.Clients; /// /// Base class used for commonly accessed methods and properties for all clients. /// -public class ClientBase +public abstract class ClientBase { /// /// HttpClient used for all clients API calls. /// - protected HttpClient HttpClient { get; private set; } + protected HttpClient HttpClient { get; } /// /// Options required for desrializing an serializing JSON to the API. /// - protected JsonSerializerOptions JsonSerializerOptions { get; private set; } + protected JsonSerializerOptions JsonSerializerOptions { get; } /// /// Constructor to setup all required fields this is called by all child classes. /// /// HttpClient with baseUrl of the API used for all calls. - public ClientBase(HttpClient httpClient) + protected ClientBase(HttpClient httpClient) { HttpClient = httpClient; JsonSerializerOptions = new JsonSerializerOptions @@ -42,7 +43,7 @@ public ClientBase(HttpClient httpClient) /// the object to encode. /// String content of object. /// throws if data provided is null. - protected StringContent Encode(TModel data) + protected StringContent CreateJsonContent(TModel data) { if (data is null) { @@ -56,10 +57,10 @@ protected StringContent Encode(TModel data) } /// - /// Desreialise the httpContent. + /// Desreialize the httpContent. /// /// the object type to deserialise to. - /// httpContent from the api call. + /// httpContent from the API call. /// the object of type specified. protected async Task DeserialiseAsync(HttpContent httpContent) { @@ -68,8 +69,7 @@ protected StringContent Encode(TModel data) return default; } - var contentStream = await httpContent.ReadAsStreamAsync(); - return await JsonSerializer.DeserializeAsync(contentStream, JsonSerializerOptions); + return await httpContent.ReadFromJsonAsync(JsonSerializerOptions); } /// @@ -77,8 +77,8 @@ protected StringContent Encode(TModel data) /// /// response from an http call. /// Throws if httpResponse is null. - /// throws if error codes are detected. - protected async Task CheckStatusCodes(HttpResponseMessage httpResponse) + /// throws if error codes are detected. + protected static async Task CheckStatusCodes(HttpResponseMessage httpResponse) { if (httpResponse is null) { @@ -88,19 +88,19 @@ protected async Task CheckStatusCodes(HttpResponseMessage httpResponse) var responseText = await httpResponse.Content.ReadAsStringAsync(); if (httpResponse.StatusCode == HttpStatusCode.BadRequest) { - throw new ApiException("Bad request; ensure you have provided the correct data to the Api", httpResponse.StatusCode, responseText, httpResponse.Headers); + throw new EnclaveApiException("Bad request; ensure you have provided the correct data to the Api", httpResponse.StatusCode, responseText, httpResponse.Headers); } else if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) { - throw new ApiException("Unauthorized request; ensure you have provided a valid Access Token with \'Authorization: Bearer {token}\'.", httpResponse.StatusCode, responseText, httpResponse.Headers); + throw new EnclaveApiException("Unauthorized request; ensure you have provided a valid Access Token with \'Authorization: Bearer {token}\'.", httpResponse.StatusCode, responseText, httpResponse.Headers); } else if (httpResponse.StatusCode == HttpStatusCode.Forbidden) { - throw new ApiException("The provided token does not grant rights to this request.", httpResponse.StatusCode, responseText, httpResponse.Headers); + throw new EnclaveApiException("The provided token does not grant rights to this request.", httpResponse.StatusCode, responseText, httpResponse.Headers); } else if (!httpResponse.IsSuccessStatusCode) { - throw new ApiException($"The HTTP status code of the response was not expected ({httpResponse.StatusCode}).", httpResponse.StatusCode, responseText, httpResponse.Headers); + throw new EnclaveApiException($"The HTTP status code of the response was not expected ({httpResponse.StatusCode}).", httpResponse.StatusCode, responseText, httpResponse.Headers); } } @@ -109,7 +109,7 @@ protected async Task CheckStatusCodes(HttpResponseMessage httpResponse) /// /// Type being checked. /// object being checked. - protected void CheckModel([NotNull] TModel? model) + protected static void EnsureNotNull([NotNull] TModel? model) { if (model is null) { @@ -122,6 +122,6 @@ protected void CheckModel([NotNull] TModel? model) /// /// Throws every time this is called. [DoesNotReturn] - private void Throw() => + private static void Throw() => throw new InvalidOperationException("Return from API is null please ensure you've entered the correct data or raise an issue"); } \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs index 2b00701..127e69a 100644 --- a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs @@ -12,52 +12,52 @@ public interface IOrganisationClient /// /// The organisation selected and the one used to create this client. /// - AccountOrganisation CurrentOrganisation { get; } + AccountOrganisation Organisation { get; } /// - /// An instance of AuthorityClient associated with the current organisaiton. + /// An instance of associated with the current organisaiton. /// IAuthorityClient Authority { get; } /// - /// An instance of DnsClient associated with the current organisaiton. + /// An instance of associated with the current organisaiton. /// IDnsClient Dns { get; } /// - /// An instance of EnrolmentKeysClient associated with the current organisaiton. + /// An instance of associated with the current organisaiton. /// IEnrolmentKeysClient EnrolmentKeys { get; } /// - /// An instance of LogsClient associated with the current organisaiton. + /// An instance of associated with the current organisaiton. /// ILogsClient Logs { get; } /// - /// An instance of PoliciesClient associated with the current organisaiton. + /// An instance of associated with the current organisaiton. /// IPoliciesClient Policies { get; } /// - /// An instance of SystemsClient associated with the current organisaiton. + /// An instance of associated with the current organisaiton. /// ISystemsClient Systems { get; } /// - /// An instance of TagsClient associated with the current organisaiton. + /// An instance of associated with the current organisaiton. /// ITagsClient Tags { get; } /// - /// An instance of UnapprovedSystemsClient associated with the current organisaiton. + /// An instance of associated with the current organisaiton. /// IUnapprovedSystemsClient UnapprovedSystems { get; } /// /// Get more detail on your current organisaiton. /// - /// A more detailed version of CurrentOrganisaiton. + /// A detailed organisation model. Task GetAsync(); /// diff --git a/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs b/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs index 2e4194c..39647d1 100644 --- a/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs +++ b/src/Enclave.Sdk/Clients/Interfaces/ITagsClient.cs @@ -11,10 +11,10 @@ public interface ITagsClient /// /// Gets a paginated list of tags which can be searched and interated upon. /// - /// a partial matching search term. + /// A partial matching search term. /// Sort order for the pagination. - /// which page number do you want to return. + /// Which page number do you want to return. /// How many tags per page. - /// a paginated response model with links to get the previous, next, first and last pages. + /// A paginated response model with links to get the previous, next, first and last pages. Task> GetAsync(string? searchTerm = null, TagQuerySortOrder? sortOrder = null, int? pageNumber = null, int? perPage = null); } \ No newline at end of file diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs index 4676d61..5bfb3af 100644 --- a/src/Enclave.Sdk/Clients/OrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs @@ -7,8 +7,23 @@ namespace Enclave.Sdk.Api.Clients; /// public class OrganisationClient : ClientBase, IOrganisationClient { + private string _orgRoute; + + /// + /// This constructor is called by when setting up the . + /// It also calls the constructor. + /// + /// an instance of httpClient with a baseURL referencing the API. + /// The current organisaiton used for routing the API calls. + public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrganisation) + : base(httpClient) + { + Organisation = currentOrganisation; + _orgRoute = $"org/{Organisation.OrgId}"; + } + /// - public AccountOrganisation CurrentOrganisation { get; private set; } + public AccountOrganisation Organisation { get; } /// public IAuthorityClient Authority => throw new NotImplementedException(); @@ -34,21 +49,6 @@ public class OrganisationClient : ClientBase, IOrganisationClient /// public IUnapprovedSystemsClient UnapprovedSystems => throw new NotImplementedException(); - private string _orgRoute; - - /// - /// This constructor is called by EnclaveClient when setting up the OrganisationClient. - /// It also calls the ClientBase constructor. - /// - /// an instance of httpClient with a baseURL referencing the API. - /// The current organisaiton used for routing the API calls. - public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrganisation) - : base(httpClient) - { - CurrentOrganisation = currentOrganisation; - _orgRoute = $"org/{CurrentOrganisation.OrgId}"; - } - /// public async Task GetAsync() { @@ -57,7 +57,7 @@ public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrga var model = await DeserialiseAsync(result.Content); - CheckModel(model); + EnsureNotNull(model); return model; } @@ -65,13 +65,13 @@ public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrga /// public async Task UpdateAsync(Dictionary updatedModel) { - using var encoded = Encode(updatedModel); + using var encoded = CreateJsonContent(updatedModel); var result = await HttpClient.PatchAsync(_orgRoute, encoded); await CheckStatusCodes(result); var model = await DeserialiseAsync(result.Content); - CheckModel(model); + EnsureNotNull(model); return model; } @@ -84,7 +84,7 @@ public async Task UpdateAsync(Dictionary updatedMo var model = await DeserialiseAsync(result.Content); - CheckModel(model); + EnsureNotNull(model); return model.Users; } @@ -104,7 +104,7 @@ public async Task> GetPendingInvitesAsync() var model = await DeserialiseAsync(result.Content); - CheckModel(model); + EnsureNotNull(model); return model.Invites; } @@ -112,7 +112,7 @@ public async Task> GetPendingInvitesAsync() /// public async Task InviteUserAsync(string emailAddress) { - using var encoded = Encode(new OrganisationInvite + using var encoded = CreateJsonContent(new OrganisationInvite { EmailAddress = emailAddress, }); @@ -124,7 +124,7 @@ public async Task InviteUserAsync(string emailAddress) /// public async Task CancelInviteAync(string emailAddress) { - using var encoded = Encode(new OrganisationInvite + using var encoded = CreateJsonContent(new OrganisationInvite { EmailAddress = emailAddress, }); @@ -148,7 +148,7 @@ public async Task GetOrganisationPricing() var model = await DeserialiseAsync(result.Content); - CheckModel(model); + EnsureNotNull(model); return model; } diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs index 944ddd0..84f8a3a 100644 --- a/src/Enclave.Sdk/Clients/TagsClient.cs +++ b/src/Enclave.Sdk/Clients/TagsClient.cs @@ -10,7 +10,7 @@ public class TagsClient : ClientBase, ITagsClient private string _orgRoute; /// - /// Consutructor which will be called by organisationClient when it's created. + /// Consutructor which will be called by when it's created. /// /// an instance of httpClient with a baseURL referencing the API. /// the orgRoute which specifies the orgId. @@ -31,7 +31,7 @@ public async Task> GetAsync(string? searchTerm = var model = await DeserialiseAsync>(result.Content); - CheckModel(model); + EnsureNotNull(model); return model; } diff --git a/src/Enclave.Sdk/Data/Builder.cs b/src/Enclave.Sdk/Data/Builder.cs index dd12659..c1e70d7 100644 --- a/src/Enclave.Sdk/Data/Builder.cs +++ b/src/Enclave.Sdk/Data/Builder.cs @@ -15,10 +15,10 @@ public class Builder /// /// The type of the value you're updating. /// Expression tree witht he property you want to update. - /// the new value. + /// The new value. /// Builder for fluent building. - /// throws if either propExpr or newValue are null. - /// if the selected propExpr body is null. + /// Throws if either propExpr or newValue are null. + /// If the selected propExpr body is null. public Builder Set(Expression> propExpr, TValue newValue) { if (newValue is null) diff --git a/src/Enclave.Sdk/Data/EnclaveSettings.cs b/src/Enclave.Sdk/Data/EnclaveSettings.cs deleted file mode 100644 index a862f86..0000000 --- a/src/Enclave.Sdk/Data/EnclaveSettings.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Enclave.Sdk.Api.Data; - -/// -/// A representation of the enclave settings json file. -/// -public class EnclaveSettings -{ - /// - /// the bearer token from encalve. - /// - public string? BearerToken { get; set; } - - /// - /// The Api base url. - /// - public string? BaseUrl { get; set; } - - /// - /// Custom httpClient for use with all Client classes. - /// - [JsonIgnore] - public HttpClient? HttpClient { get; set; } -} diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index 9cb121e..49c8580 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -1,4 +1,5 @@ using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Reflection; using System.Text.Json; using Enclave.Sdk.Api.Clients; @@ -19,10 +20,10 @@ public class EnclaveClient private readonly JsonSerializerOptions _jsonSerializerOptions; /// - /// Setup all requirments for making api calls. + /// Setup all requirements for making API calls. /// - /// optional set of settings should you need to configure the client further such as your own httpClient. - public EnclaveClient(EnclaveSettings? settings = default) + /// optional set of settings should you need to configure the client further such as your own . + public EnclaveClient(EnclaveClientOptions? settings = default) { if (settings is null) { @@ -32,8 +33,12 @@ public EnclaveClient(EnclaveSettings? settings = default) _httpClient = settings?.HttpClient ?? new HttpClient(); _httpClient.BaseAddress = new Uri(settings?.BaseUrl ?? FallbackUrl); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", settings?.BearerToken); - var clientHeader = new ProductInfoHeaderValue("SDK", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); + if (!string.IsNullOrWhiteSpace(settings?.PersonalAccessToken)) + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", settings?.PersonalAccessToken); + } + + var clientHeader = new ProductInfoHeaderValue("Enclave.Sdk.Api", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); _httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); _jsonSerializerOptions = new JsonSerializerOptions @@ -43,23 +48,13 @@ public EnclaveClient(EnclaveSettings? settings = default) } /// - /// Gets a list of organisation associated to the authorised user. + /// Gets a list of associated to the authorised user. /// /// List of organisation containing the OrgId and Name and the users role in that organisation. /// throws when the Api returns a null response. public async Task> GetOrganisationsAsync() { - var result = await _httpClient.GetAsync("/account/orgs"); - - if (result is null) - { - throw new InvalidOperationException("Did not get any response"); - } - - result.EnsureSuccessStatusCode(); - - var contentStream = await result.Content.ReadAsStreamAsync(); - var organisations = await JsonSerializer.DeserializeAsync(contentStream, _jsonSerializerOptions); + var organisations = await _httpClient.GetFromJsonAsync("/account/orgs", _jsonSerializerOptions); if (organisations is null) { @@ -70,28 +65,32 @@ public async Task> GetOrganisationsAsync() } /// - /// Create an organisationClient from an AccountOrganisation. + /// Create an from an . /// - /// the AccountOrganisation from GetOrganisationsAsync. + /// the from . /// OrganisationClient that can be used for all following Api Calls related to an organisation. public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisation) { return new OrganisationClient(_httpClient, organisation); } - private EnclaveSettings? GetSettingsFile() + private EnclaveClientOptions? GetSettingsFile() { var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var location = $"{userProfile}\\.enclave"; + + var location = Path.Combine(userProfile, ".encalve", "credentials.json"); try { - using var streamReader = new StreamReader($"{location}\\credentials.json"); - var json = streamReader.ReadToEnd(); - var settings = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + var json = File.ReadAllText(location); + var settings = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return settings; } + catch (IOException) + { + return null; + } catch { throw new ArgumentException("Can't find user settings file please refer to documentaiton for more information " + diff --git a/src/Enclave.Sdk/EnclaveClientOptions.cs b/src/Enclave.Sdk/EnclaveClientOptions.cs new file mode 100644 index 0000000..c6c7e6f --- /dev/null +++ b/src/Enclave.Sdk/EnclaveClientOptions.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace Enclave.Sdk.Api; + +/// +/// A representation of options used when creating . +/// +public class EnclaveClientOptions +{ + /// + /// Default constructor which sets the BaseURL to the production version of the Enclave API. + /// + public EnclaveClientOptions() + { + BaseUrl = "https://api.enclave.io"; + } + + /// + /// the Personal access token from the Enclave portal. + /// + public string? PersonalAccessToken { get; set; } + + /// + /// The base URL of the Enclave API endpoint. + /// + public string BaseUrl { get; set; } + + /// + /// Custom HttpClient for use with all Client classes. + /// + [JsonIgnore] + public HttpClient? HttpClient { get; set; } +} diff --git a/src/Enclave.Sdk/Exceptions/ApiException.cs b/src/Enclave.Sdk/Exceptions/EnclaveApiException.cs similarity index 73% rename from src/Enclave.Sdk/Exceptions/ApiException.cs rename to src/Enclave.Sdk/Exceptions/EnclaveApiException.cs index 2d0f949..03fcd40 100644 --- a/src/Enclave.Sdk/Exceptions/ApiException.cs +++ b/src/Enclave.Sdk/Exceptions/EnclaveApiException.cs @@ -6,7 +6,7 @@ namespace Enclave.Sdk.Api.Exceptions; /// /// Exception used for Api specific errors. /// -public class ApiException : Exception +public class EnclaveApiException : Exception { /// /// Http Status Code. @@ -30,8 +30,8 @@ public class ApiException : Exception /// http status code. /// http response as string. /// HttpResponseHeaders. - public ApiException(string message, HttpStatusCode statusCode, string response, HttpResponseHeaders headers) - : base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + (response == null ? "(null)" : response.Substring(0, response.Length >= 512 ? 512 : response.Length))) + public EnclaveApiException(string message, HttpStatusCode statusCode, string response, HttpResponseHeaders headers) + : base(message) { StatusCode = statusCode; Response = response ?? string.Empty; @@ -44,6 +44,7 @@ public ApiException(string message, HttpStatusCode statusCode, string response, /// Exception as a string public override string ToString() { - return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString()); + var responseDetails = Response == null ? "(null)" : Response.Substring(0, Response.Length >= 512 ? 512 : Response.Length); + return $"HTTP Exception: {Message}\n\nStatus: {StatusCode} \nResponse: \n{responseDetails}"; } } \ No newline at end of file diff --git a/src/Enclave.Sdk/credentials-example.json b/src/Enclave.Sdk/credentials-example.json index 52081ca..0497e09 100644 --- a/src/Enclave.Sdk/credentials-example.json +++ b/src/Enclave.Sdk/credentials-example.json @@ -1,4 +1,4 @@ { - "bearerToken": "", + "personalAccessToken": "", "baseUrl": "" } \ No newline at end of file diff --git a/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs b/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs index e2655ff..bba8dff 100644 --- a/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs +++ b/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs @@ -24,7 +24,7 @@ public void Setup() { _server = WireMockServer.Start(); - var enclaveSettings = new EnclaveSettings + var enclaveSettings = new EnclaveClientOptions { BaseUrl = _server.Urls[0], HttpClient = new HttpClient(), diff --git a/tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs b/tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs index d3d3820..aa8ddbd 100644 --- a/tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs +++ b/tests/Enclave.Sdk.Api.Tests/GlobalSuppressions.cs @@ -5,8 +5,5 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "We are using variables for tests they do not need to be disposed at end of scope", Scope = "namespace", Target = "Enclave.Sdk.Api.Tests")] -[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "unit test methods use underscores for readability", Scope = "namespace", Target = "Enclave.Sdk.Api.Tests")] - -[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "We are using variables for tests they do not need to be disposed at end of scope", Scope = "namespace", Target = "Enclave.Sdk.Api.Tests.Clients")] -[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "unit test methods use underscores for readability", Scope = "namespace", Target = "Enclave.Sdk.Api.Tests.Clients")] \ No newline at end of file +[assembly: SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "We are using variables for tests they do not need to be disposed at end of scope", Scope = "module")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "unit test methods use underscores for readability", Scope = "module")] From 34a895780a90045854dee4504c38061d3464adb9 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Wed, 24 Nov 2021 16:38:01 +0000 Subject: [PATCH 19/26] Added new EnclaveClient constructors as suggest in PR #1 --- src/Enclave.Sdk/EnclaveClient.cs | 70 ++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index 49c8580..fc5f416 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -17,34 +17,57 @@ public class EnclaveClient private const string FallbackUrl = "https://api.enclave.io/"; private readonly HttpClient _httpClient; - private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; /// - /// Setup all requirements for making API calls. + /// Create an using settings found in the .enclave/credentials.json file in your user directory. /// - /// optional set of settings should you need to configure the client further such as your own . - public EnclaveClient(EnclaveClientOptions? settings = default) + /// Throws if options in file are null. + public EnclaveClient() { - if (settings is null) + var options = GetSettingsFile(); + + if (options is null) { - settings = GetSettingsFile(); + throw new ArgumentNullException(nameof(options)); } - _httpClient = settings?.HttpClient ?? new HttpClient(); - _httpClient.BaseAddress = new Uri(settings?.BaseUrl ?? FallbackUrl); + _httpClient = new HttpClient(); - if (!string.IsNullOrWhiteSpace(settings?.PersonalAccessToken)) - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", settings?.PersonalAccessToken); - } + SetupHttpClient(options); + } - var clientHeader = new ProductInfoHeaderValue("Enclave.Sdk.Api", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); - _httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); + /// + /// Simple Setup using just a PersonalAccessToken. + /// + /// The token created on the Enclave Portal. + public EnclaveClient(string personalAccessToken) + { + var options = new EnclaveClientOptions { PersonalAccessToken = personalAccessToken }; + + _httpClient = new HttpClient(); + + SetupHttpClient(options); + } + + /// + /// Setup all requirements for making API calls. + /// + /// Options for setting up the . + /// Throws if options are null. + public EnclaveClient(EnclaveClientOptions options) + { + _httpClient = options?.HttpClient ?? new HttpClient(); - _jsonSerializerOptions = new JsonSerializerOptions + if (options is null) { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; + throw new ArgumentNullException(nameof(options)); + } + + SetupHttpClient(options); } /// @@ -97,4 +120,17 @@ public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisa "or provide a bearer token in the constructor"); } } + + private void SetupHttpClient(EnclaveClientOptions options) + { + _httpClient.BaseAddress = new Uri(options.BaseUrl ?? FallbackUrl); + + if (!string.IsNullOrWhiteSpace(options.PersonalAccessToken)) + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.PersonalAccessToken); + } + + var clientHeader = new ProductInfoHeaderValue("Enclave.Sdk.Api", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); + _httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); + } } \ No newline at end of file From 5a0ca4f89426a2a9f9f2ea1d689f7741aeb1b1d9 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Wed, 24 Nov 2021 16:54:30 +0000 Subject: [PATCH 20/26] fixed typo --- src/Enclave.Sdk/EnclaveClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index fc5f416..c3490cd 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -101,7 +101,7 @@ public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisa { var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var location = Path.Combine(userProfile, ".encalve", "credentials.json"); + var location = Path.Combine(userProfile, ".enclave", "credentials.json"); try { From 25b944fcb5c063a3a277ec00e8df9581effae4d8 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Thu, 25 Nov 2021 11:39:14 +0000 Subject: [PATCH 21/26] Changes to http error handling and added patch models to builder spec as per PR #1 --- src/Enclave.Sdk/Clients/ClientBase.cs | 45 +----- .../Clients/Interfaces/IOrganisationClient.cs | 9 +- src/Enclave.Sdk/Clients/OrganisationClient.cs | 39 ++--- src/Enclave.Sdk/Clients/TagsClient.cs | 8 +- src/Enclave.Sdk/Constants.cs | 18 +++ .../Data/{Builder.cs => PatchBuilder.cs} | 10 +- .../Data/PatchModel/AccountPatch.cs | 22 +++ .../Data/PatchModel/DnsRecordPatch.cs | 27 ++++ .../Data/PatchModel/DnsZonePatch.cs | 17 +++ .../Data/PatchModel/IPatchModel.cs | 5 + .../Data/PatchModel/OrganisationPatch.cs | 22 +++ .../Data/PatchModel/PolicyPatch.cs | 39 +++++ .../Data/PatchModel/SystemPatch.cs | 27 ++++ src/Enclave.Sdk/Data/Policy/PolicyAclModel.cs | 22 +++ .../Data/Policy/PolicyAclProtocol.cs | 9 ++ src/Enclave.Sdk/EnclaveClient.cs | 33 ++--- src/Enclave.Sdk/EnclaveClientOptions.cs | 8 +- .../Exceptions/EnclaveApiException.cs | 50 ------- src/Enclave.Sdk/Exceptions/ProblemDetails.cs | 64 +++++++++ .../Exceptions/ProblemDetailsException.cs | 15 ++ .../Exceptions/ProblemDetailsJsonConverter.cs | 133 ++++++++++++++++++ .../ProblemDetailsHttpMessageHandler.cs | 26 ++++ .../Clients/OrganisationClientTests.cs | 5 +- .../EnclaveClientTests.cs | 1 - 24 files changed, 495 insertions(+), 159 deletions(-) create mode 100644 src/Enclave.Sdk/Constants.cs rename src/Enclave.Sdk/Data/{Builder.cs => PatchBuilder.cs} (87%) create mode 100644 src/Enclave.Sdk/Data/PatchModel/AccountPatch.cs create mode 100644 src/Enclave.Sdk/Data/PatchModel/DnsRecordPatch.cs create mode 100644 src/Enclave.Sdk/Data/PatchModel/DnsZonePatch.cs create mode 100644 src/Enclave.Sdk/Data/PatchModel/IPatchModel.cs create mode 100644 src/Enclave.Sdk/Data/PatchModel/OrganisationPatch.cs create mode 100644 src/Enclave.Sdk/Data/PatchModel/PolicyPatch.cs create mode 100644 src/Enclave.Sdk/Data/PatchModel/SystemPatch.cs create mode 100644 src/Enclave.Sdk/Data/Policy/PolicyAclModel.cs create mode 100644 src/Enclave.Sdk/Data/Policy/PolicyAclProtocol.cs delete mode 100644 src/Enclave.Sdk/Exceptions/EnclaveApiException.cs create mode 100644 src/Enclave.Sdk/Exceptions/ProblemDetails.cs create mode 100644 src/Enclave.Sdk/Exceptions/ProblemDetailsException.cs create mode 100644 src/Enclave.Sdk/Exceptions/ProblemDetailsJsonConverter.cs create mode 100644 src/Enclave.Sdk/Handlers/ProblemDetailsHttpMessageHandler.cs diff --git a/src/Enclave.Sdk/Clients/ClientBase.cs b/src/Enclave.Sdk/Clients/ClientBase.cs index 23993f7..ea1b936 100644 --- a/src/Enclave.Sdk/Clients/ClientBase.cs +++ b/src/Enclave.Sdk/Clients/ClientBase.cs @@ -18,11 +18,6 @@ public abstract class ClientBase /// protected HttpClient HttpClient { get; } - /// - /// Options required for desrializing an serializing JSON to the API. - /// - protected JsonSerializerOptions JsonSerializerOptions { get; } - /// /// Constructor to setup all required fields this is called by all child classes. /// @@ -30,10 +25,6 @@ public abstract class ClientBase protected ClientBase(HttpClient httpClient) { HttpClient = httpClient; - JsonSerializerOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; } /// @@ -50,7 +41,7 @@ protected StringContent CreateJsonContent(TModel data) throw new ArgumentNullException(nameof(data), "Data should not be null"); } - var json = JsonSerializer.Serialize(data, JsonSerializerOptions); + var json = JsonSerializer.Serialize(data, Constants.JsonSerializerOptions); var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json); return stringContent; @@ -69,39 +60,7 @@ protected StringContent CreateJsonContent(TModel data) return default; } - return await httpContent.ReadFromJsonAsync(JsonSerializerOptions); - } - - /// - /// Check respoonse status codes for errors. - /// - /// response from an http call. - /// Throws if httpResponse is null. - /// throws if error codes are detected. - protected static async Task CheckStatusCodes(HttpResponseMessage httpResponse) - { - if (httpResponse is null) - { - throw new ArgumentNullException(nameof(httpResponse), "httpResponse should not be null are you sure a call has been made"); - } - - var responseText = await httpResponse.Content.ReadAsStringAsync(); - if (httpResponse.StatusCode == HttpStatusCode.BadRequest) - { - throw new EnclaveApiException("Bad request; ensure you have provided the correct data to the Api", httpResponse.StatusCode, responseText, httpResponse.Headers); - } - else if (httpResponse.StatusCode == HttpStatusCode.Unauthorized) - { - throw new EnclaveApiException("Unauthorized request; ensure you have provided a valid Access Token with \'Authorization: Bearer {token}\'.", httpResponse.StatusCode, responseText, httpResponse.Headers); - } - else if (httpResponse.StatusCode == HttpStatusCode.Forbidden) - { - throw new EnclaveApiException("The provided token does not grant rights to this request.", httpResponse.StatusCode, responseText, httpResponse.Headers); - } - else if (!httpResponse.IsSuccessStatusCode) - { - throw new EnclaveApiException($"The HTTP status code of the response was not expected ({httpResponse.StatusCode}).", httpResponse.StatusCode, responseText, httpResponse.Headers); - } + return await httpContent.ReadFromJsonAsync(Constants.JsonSerializerOptions); } /// diff --git a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs index 127e69a..845163e 100644 --- a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs @@ -1,5 +1,7 @@ -using Enclave.Sdk.Api.Data.Account; +using Enclave.Sdk.Api.Data; +using Enclave.Sdk.Api.Data.Account; using Enclave.Sdk.Api.Data.Organisations; +using Enclave.Sdk.Api.Data.PatchModel; namespace Enclave.Sdk.Api.Clients.Interfaces; @@ -100,7 +102,8 @@ public interface IOrganisationClient /// Patch request to update the organisation. /// Use Builder.cs to help you generate the dictionary. /// - /// Use Builder.cs to properly generate. + /// An instance of used to setup our patch request. /// The updated organisation. - Task UpdateAsync(Dictionary updatedModel); + /// Throws if builder is null. + Task UpdateAsync(PatchBuilder builder); } diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs index 5bfb3af..1978fef 100644 --- a/src/Enclave.Sdk/Clients/OrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs @@ -1,6 +1,9 @@ using Enclave.Sdk.Api.Clients.Interfaces; +using Enclave.Sdk.Api.Data; using Enclave.Sdk.Api.Data.Account; using Enclave.Sdk.Api.Data.Organisations; +using Enclave.Sdk.Api.Data.PatchModel; +using System.Net.Http.Json; namespace Enclave.Sdk.Api.Clients; @@ -52,10 +55,7 @@ public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrga /// public async Task GetAsync() { - var result = await HttpClient.GetAsync(_orgRoute); - await CheckStatusCodes(result); - - var model = await DeserialiseAsync(result.Content); + var model = await HttpClient.GetFromJsonAsync(_orgRoute); EnsureNotNull(model); @@ -63,11 +63,14 @@ public OrganisationClient(HttpClient httpClient, AccountOrganisation currentOrga } /// - public async Task UpdateAsync(Dictionary updatedModel) + public async Task UpdateAsync(PatchBuilder builder) { - using var encoded = CreateJsonContent(updatedModel); + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + using var encoded = CreateJsonContent(builder.Send()); var result = await HttpClient.PatchAsync(_orgRoute, encoded); - await CheckStatusCodes(result); var model = await DeserialiseAsync(result.Content); @@ -79,10 +82,7 @@ public async Task UpdateAsync(Dictionary updatedMo /// public async Task?> GetOrganisationUsersAsync() { - var result = await HttpClient.GetAsync($"{_orgRoute}/users"); - await CheckStatusCodes(result); - - var model = await DeserialiseAsync(result.Content); + var model = await HttpClient.GetFromJsonAsync($"{_orgRoute}/users"); EnsureNotNull(model); @@ -92,17 +92,13 @@ public async Task UpdateAsync(Dictionary updatedMo /// public async Task RemoveUserAsync(string accountId) { - var result = await HttpClient.DeleteAsync($"{_orgRoute}/users/{accountId}"); - await CheckStatusCodes(result); + await HttpClient.DeleteAsync($"{_orgRoute}/users/{accountId}"); } /// public async Task> GetPendingInvitesAsync() { - var result = await HttpClient.GetAsync($"{_orgRoute}/invites"); - await CheckStatusCodes(result); - - var model = await DeserialiseAsync(result.Content); + var model = await HttpClient.GetFromJsonAsync($"{_orgRoute}/invites"); EnsureNotNull(model); @@ -118,7 +114,6 @@ public async Task InviteUserAsync(string emailAddress) }); var result = await HttpClient.PostAsync($"{_orgRoute}/invites", encoded); - await CheckStatusCodes(result); } /// @@ -136,17 +131,13 @@ public async Task CancelInviteAync(string emailAddress) RequestUri = new Uri($"{HttpClient.BaseAddress}{_orgRoute}/invites"), }; - var result = await HttpClient.SendAsync(request); - await CheckStatusCodes(result); + await HttpClient.SendAsync(request); } /// public async Task GetOrganisationPricing() { - var result = await HttpClient.GetAsync($"{_orgRoute}/pricing"); - await CheckStatusCodes(result); - - var model = await DeserialiseAsync(result.Content); + var model = await HttpClient.GetFromJsonAsync($"{_orgRoute}/pricing"); EnsureNotNull(model); diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs index 84f8a3a..723e0ff 100644 --- a/src/Enclave.Sdk/Clients/TagsClient.cs +++ b/src/Enclave.Sdk/Clients/TagsClient.cs @@ -1,6 +1,7 @@ using Enclave.Sdk.Api.Clients.Interfaces; using Enclave.Sdk.Api.Data.Pagination; using Enclave.Sdk.Api.Data.Tags; +using System.Net.Http.Json; namespace Enclave.Sdk.Api.Clients; @@ -25,11 +26,7 @@ public async Task> GetAsync(string? searchTerm = { var queryString = BuildQueryString(searchTerm, sortOrder, pageNumber, perPage); - var result = await HttpClient.GetAsync($"{_orgRoute}/tags?{queryString}"); - - await CheckStatusCodes(result); - - var model = await DeserialiseAsync>(result.Content); + var model = await HttpClient.GetFromJsonAsync>($"{_orgRoute}/tags?{queryString}"); EnsureNotNull(model); @@ -38,6 +35,7 @@ public async Task> GetAsync(string? searchTerm = private string? BuildQueryString(string? searchTerm, TagQuerySortOrder? sortOrder, int? pageNumber, int? perPage) { + // TODO: encode values. var queryStringSet = false; string? queryString = default; if (searchTerm is not null) diff --git a/src/Enclave.Sdk/Constants.cs b/src/Enclave.Sdk/Constants.cs new file mode 100644 index 0000000..b516ecd --- /dev/null +++ b/src/Enclave.Sdk/Constants.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api; + +internal static class Constants +{ + public static JsonSerializerOptions JsonSerializerOptions { get; } = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + public const string ApiUrl = "https://api.enclave.io"; +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Builder.cs b/src/Enclave.Sdk/Data/PatchBuilder.cs similarity index 87% rename from src/Enclave.Sdk/Data/Builder.cs rename to src/Enclave.Sdk/Data/PatchBuilder.cs index c1e70d7..531a67e 100644 --- a/src/Enclave.Sdk/Data/Builder.cs +++ b/src/Enclave.Sdk/Data/PatchBuilder.cs @@ -1,4 +1,5 @@ -using System.Linq.Expressions; +using Enclave.Sdk.Api.Data.PatchModel; +using System.Linq.Expressions; namespace Enclave.Sdk.Api.Data; @@ -6,7 +7,8 @@ namespace Enclave.Sdk.Api.Data; /// Class used to construct patch models. /// /// The Type we're updating. -public class Builder +public class PatchBuilder + where TModel : IPatchModel { private Dictionary _patchDictionary = new Dictionary(); @@ -19,7 +21,7 @@ public class Builder /// Builder for fluent building. /// Throws if either propExpr or newValue are null. /// If the selected propExpr body is null. - public Builder Set(Expression> propExpr, TValue newValue) + public PatchBuilder Set(Expression> propExpr, TValue newValue) { if (newValue is null) { @@ -50,7 +52,7 @@ public Builder Set(Expression> propExpr, T /// Build the Dictionary and return it. then reset the dictionary so the builder can be reused. /// /// Dictionary built using the Set model in this class. - public Dictionary Send() + internal Dictionary Send() { try { diff --git a/src/Enclave.Sdk/Data/PatchModel/AccountPatch.cs b/src/Enclave.Sdk/Data/PatchModel/AccountPatch.cs new file mode 100644 index 0000000..c77b186 --- /dev/null +++ b/src/Enclave.Sdk/Data/PatchModel/AccountPatch.cs @@ -0,0 +1,22 @@ +namespace Enclave.Sdk.Api.Data.PatchModel; + +/// +/// Defines the modifiable properties of an account. +/// +public class AccountPatch : IPatchModel +{ + /// + /// The user's declared first name. + /// + public string? FirstName { get; set; } + + /// + /// The user's declared last name. + /// + public string? LastName { get; set; } + + /// + /// Whether the user has indicated they would like to recieve marketing emails. + /// + public bool? EmailNotificationsEnabled { get; set; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/PatchModel/DnsRecordPatch.cs b/src/Enclave.Sdk/Data/PatchModel/DnsRecordPatch.cs new file mode 100644 index 0000000..8412310 --- /dev/null +++ b/src/Enclave.Sdk/Data/PatchModel/DnsRecordPatch.cs @@ -0,0 +1,27 @@ +namespace Enclave.Sdk.Api.Data.PatchModel; + +/// +/// The patch model for a DNS record. Any values not provided will not be updated. +/// +public class DnsRecordPatch : IPatchModel +{ + /// + /// The name of the record (without the zone). + /// + public string Name { get; set; } = default!; + + /// + /// The set of tags to which this DNS name is applied. + /// + public IReadOnlyList? Tags { get; set; } + + /// + /// The set of systems to which this DNS name is applied. + /// + public IReadOnlyList? Systems { get; set; } + + /// + /// Any notes or additional info for this record. + /// + public string? Notes { get; set; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/PatchModel/DnsZonePatch.cs b/src/Enclave.Sdk/Data/PatchModel/DnsZonePatch.cs new file mode 100644 index 0000000..32b9aeb --- /dev/null +++ b/src/Enclave.Sdk/Data/PatchModel/DnsZonePatch.cs @@ -0,0 +1,17 @@ +namespace Enclave.Sdk.Api.Data.PatchModel; + +/// +/// Patch model for updating a zone. +/// +public class DnsZonePatch : IPatchModel +{ + /// + /// The name of the zone; changing the name of a zone will change the fully-qualified name of every DNS record. + /// + public string? Name { get; set; } + + /// + /// Update the notes for the zone. + /// + public string? Notes { get; set; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/PatchModel/IPatchModel.cs b/src/Enclave.Sdk/Data/PatchModel/IPatchModel.cs new file mode 100644 index 0000000..96e0059 --- /dev/null +++ b/src/Enclave.Sdk/Data/PatchModel/IPatchModel.cs @@ -0,0 +1,5 @@ +namespace Enclave.Sdk.Api.Data.PatchModel; + +public interface IPatchModel +{ +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/PatchModel/OrganisationPatch.cs b/src/Enclave.Sdk/Data/PatchModel/OrganisationPatch.cs new file mode 100644 index 0000000..ac3d8e5 --- /dev/null +++ b/src/Enclave.Sdk/Data/PatchModel/OrganisationPatch.cs @@ -0,0 +1,22 @@ +namespace Enclave.Sdk.Api.Data.PatchModel; + +/// +/// Model defining the modifiable properties for an organisation. +/// +public class OrganisationPatch : IPatchModel +{ + /// + /// The org name. + /// + public string? Name { get; set; } + + /// + /// The org website. + /// + public string? Website { get; set; } + + /// + /// A contact number for the organisation. + /// + public string? Phone { get; set; } +} diff --git a/src/Enclave.Sdk/Data/PatchModel/PolicyPatch.cs b/src/Enclave.Sdk/Data/PatchModel/PolicyPatch.cs new file mode 100644 index 0000000..4cb526a --- /dev/null +++ b/src/Enclave.Sdk/Data/PatchModel/PolicyPatch.cs @@ -0,0 +1,39 @@ +using Enclave.Sdk.Api.Data.Policy; + +namespace Enclave.Sdk.Api.Data.PatchModel; + +/// +/// Defines the modifiable properties of a policy. +/// +public class PolicyPatch : IPatchModel +{ + /// + /// The policy name/description. + /// + public string? Description { get; set; } + + /// + /// Whether or not the policy is enabled. + /// + public bool? IsEnabled { get; set; } + + /// + /// A set of sender tags. + /// + public string[]? SenderTags { get; set; } + + /// + /// A set of receiver tags. + /// + public string[]? ReceiverTags { get; set; } + + /// + /// The set of ACLs for the policy. + /// + public PolicyAclModel[]? Acls { get; set; } + + /// + /// Notes for the policy. + /// + public string? Notes { get; set; } +} diff --git a/src/Enclave.Sdk/Data/PatchModel/SystemPatch.cs b/src/Enclave.Sdk/Data/PatchModel/SystemPatch.cs new file mode 100644 index 0000000..e309b1a --- /dev/null +++ b/src/Enclave.Sdk/Data/PatchModel/SystemPatch.cs @@ -0,0 +1,27 @@ +namespace Enclave.Sdk.Api.Data.PatchModel; + +/// +/// Defines the modifiable properties of a system. +/// +public class SystemPatch : IPatchModel +{ + /// + /// The system description. + /// + public string? Description { get; set; } + + /// + /// Whether or not the system is enabled (and available for use). + /// + public bool? IsEnabled { get; set; } + + /// + /// The set of tags applied to the system. + /// + public string[]? Tags { get; set; } + + /// + /// Any notes or additional info for this system. + /// + public string? Notes { get; set; } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Policy/PolicyAclModel.cs b/src/Enclave.Sdk/Data/Policy/PolicyAclModel.cs new file mode 100644 index 0000000..95ec7e5 --- /dev/null +++ b/src/Enclave.Sdk/Data/Policy/PolicyAclModel.cs @@ -0,0 +1,22 @@ +namespace Enclave.Sdk.Api.Data.Policy; + +/// +/// Model representing a single ACL entry for a policy. +/// +public class PolicyAclModel +{ + /// + /// The protocol. + /// + public PolicyAclProtocol Protocol { get; set; } + + /// + /// The port range (or single port) for the ACL. + /// + public string? Ports { get; set; } + + /// + /// An optional description. + /// + public string? Description { get; set; } +} diff --git a/src/Enclave.Sdk/Data/Policy/PolicyAclProtocol.cs b/src/Enclave.Sdk/Data/Policy/PolicyAclProtocol.cs new file mode 100644 index 0000000..aeba45d --- /dev/null +++ b/src/Enclave.Sdk/Data/Policy/PolicyAclProtocol.cs @@ -0,0 +1,9 @@ +namespace Enclave.Sdk.Api.Data.Policy; + +public enum PolicyAclProtocol +{ + Any, + Tcp, + Udp, + Icmp +} diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index c3490cd..d0d25b2 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -6,6 +6,7 @@ using Enclave.Sdk.Api.Clients.Interfaces; using Enclave.Sdk.Api.Data; using Enclave.Sdk.Api.Data.Account; +using Enclave.Sdk.Api.Handlers; namespace Enclave.Sdk.Api; @@ -14,13 +15,7 @@ namespace Enclave.Sdk.Api; /// public class EnclaveClient { - private const string FallbackUrl = "https://api.enclave.io/"; - private readonly HttpClient _httpClient; - private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; /// /// Create an using settings found in the .enclave/credentials.json file in your user directory. @@ -35,9 +30,7 @@ public EnclaveClient() throw new ArgumentNullException(nameof(options)); } - _httpClient = new HttpClient(); - - SetupHttpClient(options); + _httpClient = SetupHttpClient(options); } /// @@ -48,9 +41,7 @@ public EnclaveClient(string personalAccessToken) { var options = new EnclaveClientOptions { PersonalAccessToken = personalAccessToken }; - _httpClient = new HttpClient(); - - SetupHttpClient(options); + _httpClient = SetupHttpClient(options); } /// @@ -60,14 +51,12 @@ public EnclaveClient(string personalAccessToken) /// Throws if options are null. public EnclaveClient(EnclaveClientOptions options) { - _httpClient = options?.HttpClient ?? new HttpClient(); - if (options is null) { throw new ArgumentNullException(nameof(options)); } - SetupHttpClient(options); + _httpClient = SetupHttpClient(options); } /// @@ -77,7 +66,7 @@ public EnclaveClient(EnclaveClientOptions options) /// throws when the Api returns a null response. public async Task> GetOrganisationsAsync() { - var organisations = await _httpClient.GetFromJsonAsync("/account/orgs", _jsonSerializerOptions); + var organisations = await _httpClient.GetFromJsonAsync("/account/orgs", Constants.JsonSerializerOptions); if (organisations is null) { @@ -121,16 +110,20 @@ public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisa } } - private void SetupHttpClient(EnclaveClientOptions options) + private static HttpClient SetupHttpClient(EnclaveClientOptions options) { - _httpClient.BaseAddress = new Uri(options.BaseUrl ?? FallbackUrl); + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()) + { + BaseAddress = new Uri(options.BaseUrl), + }; if (!string.IsNullOrWhiteSpace(options.PersonalAccessToken)) { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.PersonalAccessToken); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.PersonalAccessToken); } var clientHeader = new ProductInfoHeaderValue("Enclave.Sdk.Api", Assembly.GetExecutingAssembly().GetName().Version?.ToString()); - _httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); + httpClient.DefaultRequestHeaders.UserAgent.Add(clientHeader); + return httpClient; } } \ No newline at end of file diff --git a/src/Enclave.Sdk/EnclaveClientOptions.cs b/src/Enclave.Sdk/EnclaveClientOptions.cs index c6c7e6f..54c7d55 100644 --- a/src/Enclave.Sdk/EnclaveClientOptions.cs +++ b/src/Enclave.Sdk/EnclaveClientOptions.cs @@ -12,7 +12,7 @@ public class EnclaveClientOptions /// public EnclaveClientOptions() { - BaseUrl = "https://api.enclave.io"; + BaseUrl = Constants.ApiUrl; } /// @@ -24,10 +24,4 @@ public EnclaveClientOptions() /// The base URL of the Enclave API endpoint. /// public string BaseUrl { get; set; } - - /// - /// Custom HttpClient for use with all Client classes. - /// - [JsonIgnore] - public HttpClient? HttpClient { get; set; } } diff --git a/src/Enclave.Sdk/Exceptions/EnclaveApiException.cs b/src/Enclave.Sdk/Exceptions/EnclaveApiException.cs deleted file mode 100644 index 03fcd40..0000000 --- a/src/Enclave.Sdk/Exceptions/EnclaveApiException.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Net; -using System.Net.Http.Headers; - -namespace Enclave.Sdk.Api.Exceptions; - -/// -/// Exception used for Api specific errors. -/// -public class EnclaveApiException : Exception -{ - /// - /// Http Status Code. - /// - public HttpStatusCode StatusCode { get; private set; } - - /// - /// Response represented as a string. - /// - public string Response { get; private set; } - - /// - /// Http Response Headers. - /// - public HttpResponseHeaders Headers { get; private set; } - - /// - /// Constructor for generating an ApiException. - /// - /// user defined error message. - /// http status code. - /// http response as string. - /// HttpResponseHeaders. - public EnclaveApiException(string message, HttpStatusCode statusCode, string response, HttpResponseHeaders headers) - : base(message) - { - StatusCode = statusCode; - Response = response ?? string.Empty; - Headers = headers; - } - - /// - /// Exception as string. - /// - /// Exception as a string - public override string ToString() - { - var responseDetails = Response == null ? "(null)" : Response.Substring(0, Response.Length >= 512 ? 512 : Response.Length); - return $"HTTP Exception: {Message}\n\nStatus: {StatusCode} \nResponse: \n{responseDetails}"; - } -} \ No newline at end of file diff --git a/src/Enclave.Sdk/Exceptions/ProblemDetails.cs b/src/Enclave.Sdk/Exceptions/ProblemDetails.cs new file mode 100644 index 0000000..26f044a --- /dev/null +++ b/src/Enclave.Sdk/Exceptions/ProblemDetails.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Exceptions; + +/// +/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. +/// +[JsonConverter(typeof(ProblemDetailsJsonConverter))] +public class ProblemDetails +{ + /// + /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when + /// dereferenced, it provide human-readable documentation for the problem type + /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be + /// "about:blank". + /// + [JsonPropertyName("type")] + public string? Type { get; set; } + + /// + /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence + /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; + /// see[RFC7231], Section 3.4). + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. + /// + [JsonPropertyName("status")] + public int? Status { get; set; } + + /// + /// A human-readable explanation specific to this occurrence of the problem. + /// + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// + /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced. + /// + [JsonPropertyName("instance")] + public string? Instance { get; set; } + + /// + /// Gets the for extension members. + /// + /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as + /// other members of a problem type. + /// + /// + /// + /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. + /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. + /// + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Exceptions/ProblemDetailsException.cs b/src/Enclave.Sdk/Exceptions/ProblemDetailsException.cs new file mode 100644 index 0000000..af74bfc --- /dev/null +++ b/src/Enclave.Sdk/Exceptions/ProblemDetailsException.cs @@ -0,0 +1,15 @@ +namespace Enclave.Sdk.Api.Exceptions; + +internal class ProblemDetailsException : Exception +{ + public ProblemDetails ProblemDetails { get; } + + public HttpResponseMessage Response { get; } + + public ProblemDetailsException(ProblemDetails problemDetails, HttpResponseMessage response) + : base(problemDetails.Title) + { + ProblemDetails = problemDetails; + Response = response; + } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Exceptions/ProblemDetailsJsonConverter.cs b/src/Enclave.Sdk/Exceptions/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..380c8ae --- /dev/null +++ b/src/Enclave.Sdk/Exceptions/ProblemDetailsJsonConverter.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Enclave.Sdk.Api.Exceptions; + +internal sealed class ProblemDetailsJsonConverter : JsonConverter +{ + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexcepted end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexcepted end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + internal static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + if (TryReadStringProperty(ref reader, Type, out var propertyValue)) + { + value.Type = propertyValue; + } + else if (TryReadStringProperty(ref reader, Title, out propertyValue)) + { + value.Title = propertyValue; + } + else if (TryReadStringProperty(ref reader, Detail, out propertyValue)) + { + value.Detail = propertyValue; + } + else if (TryReadStringProperty(ref reader, Instance, out propertyValue)) + { + value.Instance = propertyValue; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + // Nothing to do here. + } + else + { + value.Status = reader.GetInt32(); + } + } + else + { + var key = reader.GetString()!; + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof(object), options); + } + } + + internal static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, [NotNullWhen(true)] out string? value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = default; + return false; + } + + reader.Read(); + value = reader.GetString()!; + return true; + } + + internal static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + if (value.Status != null) + { + writer.WriteNumber(Status, value.Status.Value); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var kvp in value.Extensions) + { + writer.WritePropertyName(kvp.Key); + JsonSerializer.Serialize(writer, kvp.Value, kvp.Value?.GetType() ?? typeof(object), options); + } + } +} \ No newline at end of file diff --git a/src/Enclave.Sdk/Handlers/ProblemDetailsHttpMessageHandler.cs b/src/Enclave.Sdk/Handlers/ProblemDetailsHttpMessageHandler.cs new file mode 100644 index 0000000..5bbedfb --- /dev/null +++ b/src/Enclave.Sdk/Handlers/ProblemDetailsHttpMessageHandler.cs @@ -0,0 +1,26 @@ +using System.Net.Http.Json; +using Enclave.Sdk.Api.Exceptions; + +namespace Enclave.Sdk.Api.Handlers; + +internal sealed class ProblemDetailsHttpMessageHandler : DelegatingHandler +{ + public ProblemDetailsHttpMessageHandler() +#pragma warning disable CA2000 // Dispose objects before losing scope + : base(new HttpClientHandler()) { } +#pragma warning restore CA2000 // Dispose objects before losing scope + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.OrdinalIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(Constants.JsonSerializerOptions, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } +} \ No newline at end of file diff --git a/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs b/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs index 2ea9947..1fb7c0a 100644 --- a/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs +++ b/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs @@ -3,6 +3,7 @@ using Enclave.Sdk.Api.Data; using Enclave.Sdk.Api.Data.Account; using Enclave.Sdk.Api.Data.Organisations; +using Enclave.Sdk.Api.Data.PatchModel; using FluentAssertions; using NUnit.Framework; using WireMock.FluentAssertions; @@ -87,7 +88,7 @@ public async Task Should_return_a_detailed_organisation_model_when_updating_with .WithHeader("Content-Type", "application/json") .WithBody(JsonSerializer.Serialize(org, _serializerOptions))); - var patchModel = new Builder().Set(x => x.Website, "newWebsite").Send(); + var patchModel = new PatchBuilder().Set(x => x.Website, "newWebsite"); // Act var result = await _organisationClient.UpdateAsync(patchModel); @@ -115,7 +116,7 @@ public async Task Should_make_a_call_to_api_when_updating_with_UpdateAsync() .WithHeader("Content-Type", "application/json") .WithBody(JsonSerializer.Serialize(org, _serializerOptions))); - var patchModel = new Builder().Set(x => x.Website, "newWebsite").Send(); + var patchModel = new PatchBuilder().Set(x => x.Website, "newWebsite"); // Act var result = await _organisationClient.UpdateAsync(patchModel); diff --git a/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs b/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs index bba8dff..0091ac5 100644 --- a/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs +++ b/tests/Enclave.Sdk.Api.Tests/EnclaveClientTests.cs @@ -27,7 +27,6 @@ public void Setup() var enclaveSettings = new EnclaveClientOptions { BaseUrl = _server.Urls[0], - HttpClient = new HttpClient(), }; _client = new EnclaveClient(enclaveSettings); From b90a4a3de1ee6843a49db34702edf1b8b6629517 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Thu, 25 Nov 2021 11:52:28 +0000 Subject: [PATCH 22/26] added root file readme to csproj for use on nuget and added description and company details --- src/Enclave.Sdk/Data/Account/AccountOrganisation.cs | 4 ++-- src/Enclave.Sdk/Data/Organisations/Organisation.cs | 2 +- .../Data/Organisations/OrganisationBillingEvent.cs | 4 ++-- src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs | 2 +- .../Data/Organisations/OrganisationPendingInvites.cs | 2 +- .../Data/Organisations/OrganisationPlanPricing.cs | 3 +-- src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs | 2 +- src/Enclave.Sdk/Data/Tags/TagItem.cs | 2 +- src/Enclave.Sdk/Enclave.Sdk.Api.csproj | 6 ++++++ 9 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs index 20fee8a..a0dd9bc 100644 --- a/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs +++ b/src/Enclave.Sdk/Data/Account/AccountOrganisation.cs @@ -13,7 +13,7 @@ public class AccountOrganisation /// /// The organisation name. /// - public string? OrgName { get; init; } + public string OrgName { get; init; } = default!; /// /// The user's role within the organisation. @@ -29,5 +29,5 @@ public class AccountOrganisationTopLevel /// /// The set of organisations. /// - public List? Orgs { get; init; } + public List Orgs { get; init; } = default!; } \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/Organisation.cs b/src/Enclave.Sdk/Data/Organisations/Organisation.cs index a234db9..e329646 100644 --- a/src/Enclave.Sdk/Data/Organisations/Organisation.cs +++ b/src/Enclave.Sdk/Data/Organisations/Organisation.cs @@ -21,7 +21,7 @@ public class Organisation /// /// The name of the organisation. /// - public string? Name { get; init; } + public string Name { get; init; } = default!; /// /// The current plan the organisation is on. diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs index aed0788..e54d139 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationBillingEvent.cs @@ -10,12 +10,12 @@ public class OrganisationBillingEvent /// /// The code indicating the billing event. /// - public string Code { get; init; } + public string Code { get; init; } = default!; /// /// A human-readable message describing the event. /// - public string Message { get; init; } + public string Message { get; init; } = default!; /// /// The event 'level'. diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs index 1129ce2..aa61746 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationInvite.cs @@ -8,5 +8,5 @@ public class OrganisationInvite /// /// The email address of the user to invite. /// - public string? EmailAddress { get; init; } + public string EmailAddress { get; init; } = default!; } diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs index 7370c21..a11b1eb 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPendingInvites.cs @@ -8,5 +8,5 @@ public class OrganisationPendingInvites /// /// The set of outstanding invites that have been sent for this organisation. /// - public IReadOnlyList Invites { get; init; } + public IReadOnlyList? Invites { get; init; } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs index 23971a3..edfa1dc 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs @@ -1,6 +1,5 @@ namespace Enclave.Sdk.Api.Data.Organisations; - /// /// A model defining the pricing for a given plan. /// @@ -9,7 +8,7 @@ public class OrganisationPlanPricing /// /// The appropriate currency symbol. /// - public string? CurrencySymbol { get; init; } + public string CurrencySymbol { get; init; } = default!; /// /// Whether or not this plan is enabled. diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs index 9b847fc..59b7519 100644 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs +++ b/src/Enclave.Sdk/Data/Organisations/OrganisationUser.cs @@ -40,5 +40,5 @@ public class OrganisationUser public class OrganisationUsersTopLevel { - public IReadOnlyList Users { get; init; } + public IReadOnlyList? Users { get; init; } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Tags/TagItem.cs b/src/Enclave.Sdk/Data/Tags/TagItem.cs index 6a943d8..a9962d6 100644 --- a/src/Enclave.Sdk/Data/Tags/TagItem.cs +++ b/src/Enclave.Sdk/Data/Tags/TagItem.cs @@ -8,7 +8,7 @@ public class TagItem /// /// The tag name. /// - public string Tag { get; init; } + public string Tag { get; init; } = default!; /// /// The last time this tag was last referenced. diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj index 0493ae9..1640fed 100644 --- a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj +++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj @@ -6,13 +6,19 @@ Library enable enable + Enclave Networks Limited True True false + README.md + + Provides a NuGet package that makes it easier to consume the Enclave Management APIs. + + From 9d1d83e67e7249714157f1083475c25e2dff38bb Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Thu, 25 Nov 2021 13:54:24 +0000 Subject: [PATCH 23/26] removed unncesseray api endpoint --- .../Clients/Interfaces/IOrganisationClient.cs | 6 ----- src/Enclave.Sdk/Clients/OrganisationClient.cs | 14 ++--------- .../Organisations/OrganisationPlanPricing.cs | 22 ----------------- .../Data/Organisations/OrganisationPricing.cs | 24 ------------------- .../Organisations/PlanPricingQuanitity.cs | 22 ----------------- 5 files changed, 2 insertions(+), 86 deletions(-) delete mode 100644 src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs delete mode 100644 src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs delete mode 100644 src/Enclave.Sdk/Data/Organisations/PlanPricingQuanitity.cs diff --git a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs index 845163e..35a9bea 100644 --- a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs @@ -62,12 +62,6 @@ public interface IOrganisationClient /// A detailed organisation model. Task GetAsync(); - /// - /// Gets pricing options for the current organisaiton. - /// - /// A representation of the pricing options. - Task GetOrganisationPricing(); - /// /// Gets the users that have access to the current organisaiton. /// diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs index 1978fef..05d2b11 100644 --- a/src/Enclave.Sdk/Clients/OrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs @@ -1,9 +1,9 @@ -using Enclave.Sdk.Api.Clients.Interfaces; +using System.Net.Http.Json; +using Enclave.Sdk.Api.Clients.Interfaces; using Enclave.Sdk.Api.Data; using Enclave.Sdk.Api.Data.Account; using Enclave.Sdk.Api.Data.Organisations; using Enclave.Sdk.Api.Data.PatchModel; -using System.Net.Http.Json; namespace Enclave.Sdk.Api.Clients; @@ -133,14 +133,4 @@ public async Task CancelInviteAync(string emailAddress) await HttpClient.SendAsync(request); } - - /// - public async Task GetOrganisationPricing() - { - var model = await HttpClient.GetFromJsonAsync($"{_orgRoute}/pricing"); - - EnsureNotNull(model); - - return model; - } } \ No newline at end of file diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs deleted file mode 100644 index edfa1dc..0000000 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPlanPricing.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Enclave.Sdk.Api.Data.Organisations; - -/// -/// A model defining the pricing for a given plan. -/// -public class OrganisationPlanPricing -{ - /// - /// The appropriate currency symbol. - /// - public string CurrencySymbol { get; init; } = default!; - - /// - /// Whether or not this plan is enabled. - /// - public bool Enabled { get; init; } - - /// - /// The quantities of systems available in this plan. - /// - public IReadOnlyList? Quantities { get; init; } -} diff --git a/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs b/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs deleted file mode 100644 index 0dae807..0000000 --- a/src/Enclave.Sdk/Data/Organisations/OrganisationPricing.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Enclave.Sdk.Api.Data.Organisations; - -public class OrganisationPricing -{ - /// - /// The starter tier pricing info. - /// - public OrganisationPlanPricing Starter { get; init; } - - /// - /// Pro tier pricing info. - /// - public OrganisationPlanPricing Pro { get; init; } - - /// - /// Business tier pricing info. - /// - public OrganisationPlanPricing Business { get; init; } - - /// - /// The last billing event for the organisation (if any). - /// - public OrganisationBillingEvent LastBillingEvent { get; init; } -} diff --git a/src/Enclave.Sdk/Data/Organisations/PlanPricingQuanitity.cs b/src/Enclave.Sdk/Data/Organisations/PlanPricingQuanitity.cs deleted file mode 100644 index 9f811f2..0000000 --- a/src/Enclave.Sdk/Data/Organisations/PlanPricingQuanitity.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Enclave.Sdk.Api.Data.Organisations; - -/// -/// The price of a certain quantity of systems. -/// -public class PlanPricingQuanitity -{ - /// - /// The max system capacity of this quantity. - /// - public int Capacity { get; init; } - - /// - /// The price of this quantity. - /// - public decimal Price { get; init; } - - /// - /// Whether or not this is the default quantity to display in the plan. - /// - public bool IsDefault { get; init; } -} From 1c252595223ee62f9de387afd4ed22783907aabf Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Thu, 25 Nov 2021 13:55:11 +0000 Subject: [PATCH 24/26] Fixed tests --- .../Clients/OrganisationClientTests.cs | 70 ------------------- 1 file changed, 70 deletions(-) diff --git a/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs b/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs index 1fb7c0a..1fae9a2 100644 --- a/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs +++ b/tests/Enclave.Sdk.Api.Tests/Clients/OrganisationClientTests.cs @@ -291,74 +291,4 @@ public async Task Should_make_a_call_to_api_when_calling_CancelInviteAync() // Assert _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/invites"); } - - [Test] - public async Task Should_return_organisation_pricing_when_calling_GetOrganisationPricing() - { - // Arrange - var organisationPricing = new OrganisationPricing - { - LastBillingEvent = new OrganisationBillingEvent(), - Starter = new OrganisationPlanPricing - { - CurrencySymbol = "£", - Enabled = true, - Quantities = new List(), - }, - Pro = new OrganisationPlanPricing - { - CurrencySymbol = "£", - Enabled = true, - Quantities = new List(), - }, - Business = new OrganisationPlanPricing - { - CurrencySymbol = "£", - Enabled = true, - Quantities = new List(), - }, - }; - - _server - .Given(Request.Create().WithPath($"{_orgRoute}/pricing").UsingGet()) - .RespondWith( - Response.Create() - .WithSuccess() - .WithHeader("Content-Type", "application/json") - .WithBody(JsonSerializer.Serialize(organisationPricing, _serializerOptions))); - - // Act - var result = await _organisationClient.GetOrganisationPricing(); - - // Assert - result.Should().NotBeNull(); - result.Starter.CurrencySymbol.Should().Be("£"); - } - - [Test] - public async Task Should_make_a_call_to_api_when_calling_GetOrganisationPricing() - { - // Arrange - var organisationPricing = new OrganisationPricing - { - LastBillingEvent = new OrganisationBillingEvent(), - Starter = new OrganisationPlanPricing(), - Pro = new OrganisationPlanPricing(), - Business = new OrganisationPlanPricing(), - }; - - _server - .Given(Request.Create().WithPath($"{_orgRoute}/pricing").UsingGet()) - .RespondWith( - Response.Create() - .WithSuccess() - .WithHeader("Content-Type", "application/json") - .WithBody(JsonSerializer.Serialize(organisationPricing, _serializerOptions))); - - // Act - await _organisationClient.GetOrganisationPricing(); - - // Assert - _server.Should().HaveReceivedACall().AtUrl($"{_server.Urls[0]}{_orgRoute}/pricing"); - } } From 0c434c3ede2372a21b44bacb2d7e85a0af6ca1da Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Thu, 25 Nov 2021 14:25:30 +0000 Subject: [PATCH 25/26] handling query strings with build in parser --- src/Enclave.Sdk/Clients/ClientBase.cs | 1 - src/Enclave.Sdk/Clients/TagsClient.cs | 23 ++++++++--------------- src/Enclave.Sdk/Enclave.Sdk.Api.csproj | 2 +- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Enclave.Sdk/Clients/ClientBase.cs b/src/Enclave.Sdk/Clients/ClientBase.cs index ea1b936..3afbd75 100644 --- a/src/Enclave.Sdk/Clients/ClientBase.cs +++ b/src/Enclave.Sdk/Clients/ClientBase.cs @@ -4,7 +4,6 @@ using System.Net.Mime; using System.Text; using System.Text.Json; -using Enclave.Sdk.Api.Exceptions; namespace Enclave.Sdk.Api.Clients; diff --git a/src/Enclave.Sdk/Clients/TagsClient.cs b/src/Enclave.Sdk/Clients/TagsClient.cs index 723e0ff..43eea63 100644 --- a/src/Enclave.Sdk/Clients/TagsClient.cs +++ b/src/Enclave.Sdk/Clients/TagsClient.cs @@ -2,6 +2,7 @@ using Enclave.Sdk.Api.Data.Pagination; using Enclave.Sdk.Api.Data.Tags; using System.Net.Http.Json; +using System.Web; namespace Enclave.Sdk.Api.Clients; @@ -33,37 +34,29 @@ public async Task> GetAsync(string? searchTerm = return model; } - private string? BuildQueryString(string? searchTerm, TagQuerySortOrder? sortOrder, int? pageNumber, int? perPage) + private static string? BuildQueryString(string? searchTerm, TagQuerySortOrder? sortOrder, int? pageNumber, int? perPage) { - // TODO: encode values. - var queryStringSet = false; - string? queryString = default; + var queryString = HttpUtility.ParseQueryString(string.Empty); if (searchTerm is not null) { - queryString += $"search={searchTerm}"; - queryStringSet = true; + queryString.Add("search", searchTerm); } if (sortOrder is not null) { - var delimiter = queryStringSet ? "&" : string.Empty; - queryString += $"{delimiter}sort={sortOrder}"; - queryStringSet = true; + queryString.Add("sort", sortOrder.ToString()); } if (pageNumber is not null) { - var delimiter = queryStringSet ? "&" : string.Empty; - queryString += $"{delimiter}page={pageNumber}"; - queryStringSet = true; + queryString.Add("page", pageNumber.ToString()); } if (perPage is not null) { - var delimiter = queryStringSet ? "&" : string.Empty; - queryString += $"{delimiter}per_page={perPage}"; + queryString.Add("per_page", perPage.ToString()); } - return queryString; + return queryString.ToString(); } } diff --git a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj index 1640fed..3e9595c 100644 --- a/src/Enclave.Sdk/Enclave.Sdk.Api.csproj +++ b/src/Enclave.Sdk/Enclave.Sdk.Api.csproj @@ -18,7 +18,7 @@ - + From ce7a4f76b7310b7aefaa5b34c296f5f89f4f78f6 Mon Sep 17 00:00:00 2001 From: Thomas Soulard Date: Fri, 26 Nov 2021 11:08:49 +0000 Subject: [PATCH 26/26] slight changes as per PR #1 --- src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs | 2 +- src/Enclave.Sdk/Clients/OrganisationClient.cs | 6 +++--- src/Enclave.Sdk/EnclaveClient.cs | 5 ----- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs index 35a9bea..519151d 100644 --- a/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/Interfaces/IOrganisationClient.cs @@ -66,7 +66,7 @@ public interface IOrganisationClient /// Gets the users that have access to the current organisaiton. /// /// List of users associated with the current organisation. - Task?> GetOrganisationUsersAsync(); + Task> GetOrganisationUsersAsync(); /// /// Get a list of invites that haven't been accepted. diff --git a/src/Enclave.Sdk/Clients/OrganisationClient.cs b/src/Enclave.Sdk/Clients/OrganisationClient.cs index 05d2b11..d876310 100644 --- a/src/Enclave.Sdk/Clients/OrganisationClient.cs +++ b/src/Enclave.Sdk/Clients/OrganisationClient.cs @@ -80,13 +80,13 @@ public async Task UpdateAsync(PatchBuilder buil } /// - public async Task?> GetOrganisationUsersAsync() + public async Task> GetOrganisationUsersAsync() { var model = await HttpClient.GetFromJsonAsync($"{_orgRoute}/users"); EnsureNotNull(model); - return model.Users; + return model.Users ?? Array.Empty(); } /// @@ -102,7 +102,7 @@ public async Task> GetPendingInvitesAsync() EnsureNotNull(model); - return model.Invites; + return model.Invites ?? Array.Empty(); } /// diff --git a/src/Enclave.Sdk/EnclaveClient.cs b/src/Enclave.Sdk/EnclaveClient.cs index d0d25b2..f4972b0 100644 --- a/src/Enclave.Sdk/EnclaveClient.cs +++ b/src/Enclave.Sdk/EnclaveClient.cs @@ -103,11 +103,6 @@ public IOrganisationClient CreateOrganisationClient(AccountOrganisation organisa { return null; } - catch - { - throw new ArgumentException("Can't find user settings file please refer to documentaiton for more information " + - "or provide a bearer token in the constructor"); - } } private static HttpClient SetupHttpClient(EnclaveClientOptions options)