From 002b91488942156687e5929a678925d2b69229f8 Mon Sep 17 00:00:00 2001 From: Diaconu Ionut-Victor <36921680+diaconu96vi@users.noreply.github.com> Date: Wed, 17 Nov 2021 11:21:41 +0100 Subject: [PATCH] re #19767 - External orders - CRM bubble + hubspot sdk (#40) * re #19767 - External orders - CRM bubble + hubspot sdk Added http client to retrieve pipelines. Added unit tests for the module. Updated deal model to include pipeline. Retrieving a deal will also retrieve the pipeline associated. * re #19767 - External orders - CRM bubble + hubspot sdk Implemented PR feedback * re #19767 - External orders - CRM bubble + hubspot sdk Fix feedback. Pass NotFoundException as paramater instead of manually creating it. * Test commit --- .../HttpHubSpotClient.Pipelines.cs | 26 ++++ src/HubSpot.Client/HttpHubSpotClient.cs | 4 + src/HubSpot.Client/IHubSpotClient.cs | 3 + .../Model/Pipelines/IHubSpotPipelineClient.cs | 12 ++ .../Model/Pipelines/Pipeline.cs | 46 +++++++ src/HubSpot/Deals/Deal.cs | 8 +- src/HubSpot/Deals/HubSpotDealConnector.cs | 1 + .../CustomAutoDataAttribute.cs | 2 + .../Pipelines/HubSpotPipelineClientTests.cs | 102 ++++++++++++++ .../Deals/HubSpotDealConnectorTests.cs | 125 ++++++++++++++++++ 10 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/HubSpot.Client/HttpHubSpotClient.Pipelines.cs create mode 100644 src/HubSpot.Client/Model/Pipelines/IHubSpotPipelineClient.cs create mode 100644 src/HubSpot.Client/Model/Pipelines/Pipeline.cs create mode 100644 tests/Tests.HubSpot.Client/Pipelines/HubSpotPipelineClientTests.cs create mode 100644 tests/Tests.HubSpot/Deals/HubSpotDealConnectorTests.cs diff --git a/src/HubSpot.Client/HttpHubSpotClient.Pipelines.cs b/src/HubSpot.Client/HttpHubSpotClient.Pipelines.cs new file mode 100644 index 0000000..ab610e0 --- /dev/null +++ b/src/HubSpot.Client/HttpHubSpotClient.Pipelines.cs @@ -0,0 +1,26 @@ +using HubSpot.Model.Pipelines; +using Kralizek.Extensions.Http; +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace HubSpot +{ + public partial class HttpHubSpotClient : IHubSpotPipelineClient + { + async Task IHubSpotPipelineClient.GetByGuidAsync(string guid) + { + try + { + var result = await _client.GetAsync($"/deals/v1/pipelines/{guid}"); + return result; + } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + throw new NotFoundException("Pipeline not found", ex); + } + } + } +} diff --git a/src/HubSpot.Client/HttpHubSpotClient.cs b/src/HubSpot.Client/HttpHubSpotClient.cs index e971f67..f539a19 100644 --- a/src/HubSpot.Client/HttpHubSpotClient.cs +++ b/src/HubSpot.Client/HttpHubSpotClient.cs @@ -8,6 +8,7 @@ using HubSpot.Model.Deals; using HubSpot.Model.Lists; using HubSpot.Model.Owners; +using HubSpot.Model.Pipelines; using HubSpot.Utils; using Kralizek.Extensions.Http; using Microsoft.Extensions.Logging; @@ -51,5 +52,8 @@ public HttpHubSpotClient(IHttpRestClient client, ILogger logg public IHubSpotOwnerClient Owners => this; public IHubSpotCrmClient Crm => this; + + public IHubSpotPipelineClient Pipelines => this; + } } \ No newline at end of file diff --git a/src/HubSpot.Client/IHubSpotClient.cs b/src/HubSpot.Client/IHubSpotClient.cs index 45f6a42..cd53673 100644 --- a/src/HubSpot.Client/IHubSpotClient.cs +++ b/src/HubSpot.Client/IHubSpotClient.cs @@ -4,6 +4,7 @@ using HubSpot.Model.Deals; using HubSpot.Model.Lists; using HubSpot.Model.Owners; +using HubSpot.Model.Pipelines; namespace HubSpot { @@ -15,6 +16,8 @@ public interface IHubSpotClient IHubSpotDealClient Deals { get; } + IHubSpotPipelineClient Pipelines { get; } + IHubSpotListClient Lists { get; } IHubSpotOwnerClient Owners { get; } diff --git a/src/HubSpot.Client/Model/Pipelines/IHubSpotPipelineClient.cs b/src/HubSpot.Client/Model/Pipelines/IHubSpotPipelineClient.cs new file mode 100644 index 0000000..d531a02 --- /dev/null +++ b/src/HubSpot.Client/Model/Pipelines/IHubSpotPipelineClient.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace HubSpot.Model.Pipelines +{ + public interface IHubSpotPipelineClient + { + Task GetByGuidAsync(string guid); + } +} diff --git a/src/HubSpot.Client/Model/Pipelines/Pipeline.cs b/src/HubSpot.Client/Model/Pipelines/Pipeline.cs new file mode 100644 index 0000000..27a2638 --- /dev/null +++ b/src/HubSpot.Client/Model/Pipelines/Pipeline.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace HubSpot.Model.Pipelines +{ + public class Pipeline + { + [JsonProperty("active")] + public bool IsActive { get; set; } + + [JsonProperty("displayOrder")] + public int DisplayOrder { get; set; } + + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("pipelineid")] + public string Guid { get; set; } + + [JsonProperty("stages")] + public IReadOnlyList Stages { get; set; } + } + + public class StageProperty + { + [JsonProperty("active")] + public bool IsActive { get; set; } + + [JsonProperty("closedWon")] + public bool ClosedWon { get; set; } + + [JsonProperty("displayOrder")] + public int DisplayOrder { get; set; } + + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("probability")] + public decimal Probability { get; set; } + + [JsonProperty("stageId")] + public string StageID { get; set; } + } +} diff --git a/src/HubSpot/Deals/Deal.cs b/src/HubSpot/Deals/Deal.cs index b707a5a..79a006c 100644 --- a/src/HubSpot/Deals/Deal.cs +++ b/src/HubSpot/Deals/Deal.cs @@ -1,4 +1,5 @@ -using System; +using HubSpot.Model.Pipelines; +using System; using System.Collections.Generic; using System.Text; @@ -32,6 +33,11 @@ public class Deal : IHubSpotDealEntity [CustomProperty("num_associated_contacts", IsReadOnly = true)] public long NumberOfAssociatedContacts { get; set; } + [CustomProperty("pipeline")] + public string PipelineGuid { get; set; } + + public Pipeline Pipeline { get; set; } + public IReadOnlyList AssociatedCompanyIds { get; set; } = Array.Empty(); public IReadOnlyList AssociatedContactIds { get; set; } = Array.Empty(); diff --git a/src/HubSpot/Deals/HubSpotDealConnector.cs b/src/HubSpot/Deals/HubSpotDealConnector.cs index 98e9a67..de6175c 100644 --- a/src/HubSpot/Deals/HubSpotDealConnector.cs +++ b/src/HubSpot/Deals/HubSpotDealConnector.cs @@ -30,6 +30,7 @@ public HubSpotDealConnector(IHubSpotClient client, IDealTypeManager typeManager) { var hubspotDeal = await selector.GetDeal(_client).ConfigureAwait(false); var deal = _typeManager.ConvertTo(hubspotDeal); + deal.Pipeline = await _client.Pipelines.GetByGuidAsync(deal.PipelineGuid); return deal; } catch (NotFoundException) diff --git a/tests/Tests.HubSpot.Client/CustomAutoDataAttribute.cs b/tests/Tests.HubSpot.Client/CustomAutoDataAttribute.cs index b4d3729..1251255 100644 --- a/tests/Tests.HubSpot.Client/CustomAutoDataAttribute.cs +++ b/tests/Tests.HubSpot.Client/CustomAutoDataAttribute.cs @@ -50,6 +50,8 @@ public static IFixture CreateFixture() fixture.Register((HttpHubSpotClient client) => client.Owners); + fixture.Register((HttpHubSpotClient client) => client.Pipelines); + fixture.Register((HttpHubSpotClient client) => client.Contacts.Properties); fixture.Register((HttpHubSpotClient client) => client.Contacts.PropertyGroups); diff --git a/tests/Tests.HubSpot.Client/Pipelines/HubSpotPipelineClientTests.cs b/tests/Tests.HubSpot.Client/Pipelines/HubSpotPipelineClientTests.cs new file mode 100644 index 0000000..b1c52df --- /dev/null +++ b/tests/Tests.HubSpot.Client/Pipelines/HubSpotPipelineClientTests.cs @@ -0,0 +1,102 @@ +using AutoFixture; +using AutoFixture.Idioms; +using AutoFixture.NUnit3; +using HubSpot; +using HubSpot.Model.Pipelines; +using Kralizek.Extensions.Http; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Tests.Pipelines +{ + [TestFixture] + public class HubSpotPipelineClientTests + { + [Test, CustomAutoData] + public void Constructor_is_guarded_against_null_parameters(GuardClauseAssertion guardClauseAssertion) + { + //Arrange + + //Act + + //Assert + guardClauseAssertion.Verify(typeof(HttpHubSpotClient).GetConstructors()); + } + + [Test, CustomAutoData] + public void GetByGuidAsync_throws_NotFoundException_if_request_returns_NotFound_status([Frozen] IHttpRestClient httpRestClient, IHubSpotPipelineClient sut, string guid, IFixture fixture) + { + //Arrange + var httpException = fixture.Build().With(x => x.StatusCode, HttpStatusCode.NotFound).Create(); + Mock.Get(httpRestClient) + .Setup(p => p.SendAsync(HttpMethod.Get, $"/deals/v1/pipelines/{guid}", null)) + .Throws(httpException); + + //Act + + //Assert + Assert.That(() => sut.GetByGuidAsync(guid), Throws.InstanceOf()); + } + + [Test, CustomAutoData] + public void GetByGuidAsync_invokes_http_client_get_method_once_with_guid([Frozen] IHttpRestClient httpRestClient, IHubSpotPipelineClient sut, string guid) + { + //Arrange + + //Act + sut.GetByGuidAsync(guid); + + //Assert + Mock.Get(httpRestClient).Verify(p => p.SendAsync(HttpMethod.Get, $"/deals/v1/pipelines/{guid}", null), Times.Once); + } + + [Test, CustomAutoData] + public async Task GetByGuidAsync_returns_null_if_API_retrieves_null([Frozen] IHttpRestClient httpRestClient, IHubSpotPipelineClient sut, string guid) + { + //Arrange + Mock.Get(httpRestClient) + .Setup(p => p.SendAsync(HttpMethod.Get, $"/deals/v1/pipelines/{guid}", null)).Returns(Task.FromResult((Pipeline)null)); + + //Act + var result = await sut.GetByGuidAsync(guid); + + //Assert + Assert.That(result, Is.Null); + } + + [Test, CustomAutoData] + public async Task GetByGuidAsync_returns_pipeline_retrieved_by_http_client_request([Frozen] IHttpRestClient httpRestClient,[Frozen] Pipeline pipeline, IHubSpotPipelineClient sut, string guid) + { + //Arrange + Mock.Get(httpRestClient) + .Setup(p => p.SendAsync(HttpMethod.Get, $"/deals/v1/pipelines/{guid}", null)).Returns(Task.FromResult(pipeline)); + + //Act + var result = await sut.GetByGuidAsync(guid); + + //Assert + Assert.That(result, Is.EqualTo(pipeline)); + } + + [Test, CustomAutoData] + public async Task GetByGuidAsync_returns_pipeline_with_valid_guid([Frozen] IHttpRestClient httpRestClient, IHubSpotPipelineClient sut, string guid, IFixture fixture) + { + //Arrange + var pipeline = fixture.Build().With(x => x.Guid, guid).Create(); + Mock.Get(httpRestClient) + .Setup(p => p.SendAsync(HttpMethod.Get, $"/deals/v1/pipelines/{guid}", null)).Returns(Task.FromResult(pipeline)); + + //Act + var result = await sut.GetByGuidAsync(guid); + + //Assert + Assert.That(result.Guid, Is.EqualTo(guid)); + } + } +} diff --git a/tests/Tests.HubSpot/Deals/HubSpotDealConnectorTests.cs b/tests/Tests.HubSpot/Deals/HubSpotDealConnectorTests.cs new file mode 100644 index 0000000..8494519 --- /dev/null +++ b/tests/Tests.HubSpot/Deals/HubSpotDealConnectorTests.cs @@ -0,0 +1,125 @@ +using AutoFixture; +using AutoFixture.Idioms; +using AutoFixture.NUnit3; +using HubSpot; +using HubSpot.Deals; +using HubSpot.Model.Deals; +using HubSpot.Model.Pipelines; +using Kralizek.Extensions.Http; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Tests.Deals +{ + [TestFixture] + public class HubSpotDealConnectorTests + { + [Test, CustomAutoData] + public void Constructor_is_guarded_against_null_parameters(GuardClauseAssertion guardClauseAssertion) + { + //Arrange + + //Act + + //Assert + guardClauseAssertion.Verify(typeof(HubSpotDealConnector).GetConstructors()); + } + + [Test, CustomAutoData] + public void GetAsync_is_guarded_against_null_selector(HubSpotDealConnector sut) + { + //Arrange + + //Act + + //Assert + Assert.ThrowsAsync(async () => await sut.GetAsync(null)); + } + + [Test, CustomAutoData] + public async Task GetAsync_invokes_selector_from_parameters_to_retrieve_deal([Frozen] IHubSpotClient hubSpotClient, [Frozen] IDealSelector dealSelector, HubSpotDealConnector sut) + { + //Arrange + + //Act + await sut.GetAsync(dealSelector); + + //Assert + Mock.Get(dealSelector).Verify(x => x.GetDeal(hubSpotClient), Times.Once); + } + + [Test, CustomAutoData] + public async Task GetAsync_invokes_deal_type_manager_to_convert_selector_result([Frozen] IDealTypeManager dealTypeManager, [Frozen] IHubSpotClient hubSpotClient, [Frozen] IDealSelector dealSelector, HubSpotDealConnector sut,HubSpot.Model.Deals.Deal deal) + { + //Arrange + Mock.Get(dealSelector).Setup(x => x.GetDeal(hubSpotClient)).Returns(Task.FromResult(deal)); + + //Act + await sut.GetAsync(dealSelector); + + //Assert + Mock.Get(dealTypeManager).Verify(x => x.ConvertTo(deal), Times.Once); + } + + [Test, CustomAutoData] + public async Task GetAsync_returns_converted_deal([Frozen] IDealTypeManager dealTypeManager, [Frozen] IHubSpotClient hubSpotClient, [Frozen] IDealSelector dealSelector, HubSpotDealConnector sut, IFixture fixture) + { + //Arrange + var convertedDeal = fixture.Build().Without(x => x.Pipeline).Create(); + Mock.Get(dealTypeManager).Setup(x => x.ConvertTo(It.IsAny())).Returns(convertedDeal); + Mock.Get(hubSpotClient).Setup(x => x.Pipelines.GetByGuidAsync(It.IsAny())).Returns(Task.FromResult((Pipeline)null)); + + //Act + var result = await sut.GetAsync(dealSelector); + + //Assert + Assert.That(result, Is.EqualTo(convertedDeal)); + } + + [Test, CustomAutoData] + public async Task GetAsync_returns_deal_with_pipeline_retrieved_by_http_client([Frozen] IDealTypeManager dealTypeManager, [Frozen] IHubSpotClient hubSpotClient, [Frozen] IDealSelector dealSelector, HubSpotDealConnector sut, IFixture fixture, Pipeline pipeline) + { + //Arrange + var convertedDeal = fixture.Build().Without(x => x.Pipeline).Create(); + Mock.Get(dealTypeManager).Setup(x => x.ConvertTo(It.IsAny())).Returns(convertedDeal); + Mock.Get(hubSpotClient).Setup(x => x.Pipelines.GetByGuidAsync(It.IsAny())).Returns(Task.FromResult(pipeline)); + + //Act + var result = await sut.GetAsync(dealSelector); + + //Assert + Assert.That(result.Pipeline, Is.EqualTo(pipeline)); + } + + [Test, CustomAutoData] + public async Task GetAsync_returns_null_if_deal_retrieving_throws_NotFoundException([Frozen] IHubSpotClient hubSpotClient, [Frozen] IDealSelector dealSelector, HubSpotDealConnector sut, NotFoundException notFoundException) + { + //Arrange + Mock.Get(dealSelector).Setup(x => x.GetDeal(hubSpotClient)).Throws(notFoundException); + + //Act + var result = await sut.GetAsync(dealSelector); + + //Assert + Assert.That(result, Is.Null); + } + + [Test, CustomAutoData] + public async Task GetAsync_returns_null_if_pipeline_retrieving_throws_NotFoundeException([Frozen] IHubSpotClient hubSpotClient, [Frozen] IDealSelector dealSelector, HubSpotDealConnector sut, NotFoundException notFoundException) + { + //Arrange + Mock.Get(hubSpotClient).Setup(x => x.Pipelines.GetByGuidAsync(It.IsAny())).Throws(notFoundException); + + //Act + var result = await sut.GetAsync(dealSelector); + + //Assert + Assert.That(result, Is.Null); + } + } +}