Skip to content

Commit ea8c800

Browse files
committed
Added tests for HubSpot service
1 parent c7b5cc0 commit ea8c800

File tree

6 files changed

+332
-21
lines changed

6 files changed

+332
-21
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
using Moq;
2+
using Moq.Protected;
3+
using NUnit.Framework;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Umbraco.Core.Logging;
12+
using Umbraco.Forms.Core;
13+
using Umbraco.Forms.Core.Persistence.Dtos;
14+
using Umbraco.Forms.Core.Providers.Models;
15+
using Umbraco.Forms.Integrations.Crm.Hubspot.Models;
16+
using Umbraco.Forms.Integrations.Crm.Hubspot.Services;
17+
18+
namespace Umbraco.Forms.Integrations.Crm.Hubspot.Tests
19+
{
20+
public class HubspotContactServiceTests
21+
{
22+
private const string ApiKey = "test-api-key";
23+
private readonly string s_contactPropertiesResponse = @"{
24+
""results"":[
25+
{
26+
""updatedAt"":""2019-10-14T20:45:41.715Z"",
27+
""createdAt"":""2019-08-06T02:41:08.029Z"",
28+
""name"":""firstname"",
29+
""label"":""First Name"",
30+
""type"":""string"",
31+
""fieldType"":""text"",
32+
""description"":""A contact's first name"",
33+
""groupName"":""contactinformation"",
34+
""options"":[
35+
],
36+
""displayOrder"":0,
37+
""calculated"":false,
38+
""externalOptions"":false,
39+
""hasUniqueValue"":false,
40+
""hidden"":false,
41+
""hubspotDefined"":true,
42+
""modificationMetadata"":{
43+
""archivable"":true,
44+
""readOnlyDefinition"":true,
45+
""readOnlyValue"":false
46+
},
47+
""formField"":true
48+
},
49+
{
50+
""updatedAt"":""2019-10-14T20:45:41.796Z"",
51+
""createdAt"":""2019-08-06T02:41:08.109Z"",
52+
""name"":""lastname"",
53+
""label"":""Last Name"",
54+
""type"":""string"",
55+
""fieldType"":""text"",
56+
""description"":""A contact's last name"",
57+
""groupName"":""contactinformation"",
58+
""options"":[
59+
],
60+
""displayOrder"":1,
61+
""calculated"":false,
62+
""externalOptions"":false,
63+
""hasUniqueValue"":false,
64+
""hidden"":false,
65+
""hubspotDefined"":true,
66+
""modificationMetadata"":{
67+
""archivable"":true,
68+
""readOnlyDefinition"":true,
69+
""readOnlyValue"":false
70+
},
71+
""formField"":true
72+
},
73+
{
74+
""updatedAt"":""2019-10-14T20:45:42.027Z"",
75+
""createdAt"":""2019-08-06T02:41:08.204Z"",
76+
""name"":""email"",
77+
""label"":""Email"",
78+
""type"":""string"",
79+
""fieldType"":""text"",
80+
""description"":""A contact's email address"",
81+
""groupName"":""contactinformation"",
82+
""options"":[
83+
],
84+
""displayOrder"":3,
85+
""calculated"":false,
86+
""externalOptions"":false,
87+
""hasUniqueValue"":false,
88+
""hidden"":false,
89+
""hubspotDefined"":true,
90+
""modificationMetadata"":{
91+
""archivable"":true,
92+
""readOnlyDefinition"":true,
93+
""readOnlyValue"":false
94+
},
95+
""formField"":true
96+
}
97+
]
98+
}";
99+
100+
[Test]
101+
public async Task GetContactProperties_WithoutApiKeyConfigured_ReturnsEmptyCollectionWithLoggedWarning()
102+
{
103+
Mock<IFacadeConfiguration> mockedConfig = CreateMockedConfiguration(withApiKey: false);
104+
var mockedLogger = new Mock<ILogger>();
105+
var sut = new HubspotContactService(mockedConfig.Object, mockedLogger.Object);
106+
107+
var result = await sut.GetContactProperties();
108+
109+
mockedLogger
110+
.Verify(x => x.Warn(It.Is<Type>(y => y == typeof(HubspotContactService)), It.IsAny<string>()), Times.Once);
111+
Assert.IsEmpty(result);
112+
}
113+
114+
[Test]
115+
public async Task GetContactProperties_WithFailedRequest_ReturnsEmptyCollectionWithLoggedError()
116+
{
117+
Mock<IFacadeConfiguration> mockedConfig = CreateMockedConfiguration();
118+
var mockedLogger = new Mock<ILogger>();
119+
var sut = new HubspotContactService(mockedConfig.Object, mockedLogger.Object);
120+
121+
var httpClient = CreateMockedHttpClient(HttpStatusCode.InternalServerError);
122+
HubspotContactService.ClientFactory = () => httpClient;
123+
124+
var result = await sut.GetContactProperties();
125+
126+
mockedLogger
127+
.Verify(x => x.Error(It.Is<Type>(y => y == typeof(HubspotContactService)), It.IsAny<string>(), It.IsAny<object[]>()), Times.Once);
128+
Assert.IsEmpty(result);
129+
}
130+
131+
[Test]
132+
public async Task GetContactProperties_WithSuccessfulRequest_ReturnsMappedAndOrderedPropertyCollection()
133+
{
134+
Mock<IFacadeConfiguration> mockedConfig = CreateMockedConfiguration();
135+
var mockedLogger = new Mock<ILogger>();
136+
var sut = new HubspotContactService(mockedConfig.Object, mockedLogger.Object);
137+
138+
var httpClient = CreateMockedHttpClient(HttpStatusCode.OK, s_contactPropertiesResponse);
139+
HubspotContactService.ClientFactory = () => httpClient;
140+
141+
var result = await sut.GetContactProperties();
142+
143+
Assert.AreEqual(3, result.Count());
144+
Assert.AreEqual("Email,First Name,Last Name", string.Join(",", result.Select(x => x.Label)));
145+
Assert.AreEqual("email,firstname,lastname", string.Join(",", result.Select(x => x.Name)));
146+
}
147+
148+
[Test]
149+
public async Task PostContact_WithoutApiKeyConfigured_ReturnsNotConfiguredWithLoggedWarning()
150+
{
151+
Mock<IFacadeConfiguration> mockedConfig = CreateMockedConfiguration(withApiKey: false);
152+
var mockedLogger = new Mock<ILogger>();
153+
var sut = new HubspotContactService(mockedConfig.Object, mockedLogger.Object);
154+
155+
var record = new Record();
156+
var fieldMappings = new List<MappedProperty>();
157+
var result = await sut.PostContact(record, fieldMappings);
158+
159+
mockedLogger
160+
.Verify(x => x.Warn(It.Is<Type>(y => y == typeof(HubspotContactService)), It.IsAny<string>()), Times.Once);
161+
Assert.AreEqual(CommandResult.NotConfigured, result);
162+
}
163+
164+
[Test]
165+
public async Task PostContact_WithFailedRequest_ReturnsFailedWithLoggedError()
166+
{
167+
Mock<IFacadeConfiguration> mockedConfig = CreateMockedConfiguration();
168+
var mockedLogger = new Mock<ILogger>();
169+
var sut = new HubspotContactService(mockedConfig.Object, mockedLogger.Object);
170+
171+
var httpClient = CreateMockedHttpClient(HttpStatusCode.InternalServerError);
172+
HubspotContactService.ClientFactory = () => httpClient;
173+
174+
var record = new Record();
175+
var fieldMappings = new List<MappedProperty>();
176+
var result = await sut.PostContact(record, fieldMappings);
177+
178+
mockedLogger
179+
.Verify(x => x.Error(It.Is<Type>(y => y == typeof(HubspotContactService)), It.IsAny<string>()), Times.Once);
180+
181+
Assert.AreEqual(CommandResult.Failed, result);
182+
}
183+
184+
[Test]
185+
public async Task PostContact_WithSuccessfulRequest_ReturnSuccess()
186+
{
187+
Mock<IFacadeConfiguration> mockedConfig = CreateMockedConfiguration();
188+
var mockedLogger = new Mock<ILogger>();
189+
var sut = new HubspotContactService(mockedConfig.Object, mockedLogger.Object);
190+
191+
var httpClient = CreateMockedHttpClient(HttpStatusCode.OK);
192+
HubspotContactService.ClientFactory = () => httpClient;
193+
194+
var formFieldId = Guid.NewGuid();
195+
var record = new Record();
196+
record.RecordFields.Add(formFieldId, new RecordField
197+
{
198+
FieldId = formFieldId,
199+
Values = new List<object> { "Fred" }
200+
});
201+
var fieldMappings = new List<MappedProperty>()
202+
{
203+
new MappedProperty
204+
{
205+
FormField = formFieldId.ToString(),
206+
HubspotField = "firstname"
207+
}
208+
};
209+
var result = await sut.PostContact(record, fieldMappings);
210+
211+
mockedLogger
212+
.Verify(x => x.Warn(It.Is<Type>(y => y == typeof(HubspotContactService)), It.IsAny<string>(), It.IsAny<object[]>()), Times.Never);
213+
214+
Assert.AreEqual(CommandResult.Success, result);
215+
}
216+
217+
[Test]
218+
public async Task PostContact_WithSuccessfulRequestAndUnmappedField_ReturnSuccessWithLoggedWarning()
219+
{
220+
Mock<IFacadeConfiguration> mockedConfig = CreateMockedConfiguration();
221+
var mockedLogger = new Mock<ILogger>();
222+
var sut = new HubspotContactService(mockedConfig.Object, mockedLogger.Object);
223+
224+
var httpClient = CreateMockedHttpClient(HttpStatusCode.OK);
225+
HubspotContactService.ClientFactory = () => httpClient;
226+
227+
var formFieldId = Guid.NewGuid();
228+
var record = new Record();
229+
var fieldMappings = new List<MappedProperty>()
230+
{
231+
new MappedProperty
232+
{
233+
FormField = formFieldId.ToString(),
234+
HubspotField = "firstname"
235+
}
236+
};
237+
var result = await sut.PostContact(record, fieldMappings);
238+
239+
mockedLogger
240+
.Verify(x => x.Warn(It.Is<Type>(y => y == typeof(HubspotContactService)), It.IsAny<string>(), It.IsAny<object[]>()), Times.Once);
241+
242+
Assert.AreEqual(CommandResult.Success, result);
243+
}
244+
245+
private static Mock<IFacadeConfiguration> CreateMockedConfiguration(bool withApiKey = true)
246+
{
247+
var mockedConfiguration = new Mock<IFacadeConfiguration>();
248+
if (withApiKey)
249+
{
250+
mockedConfiguration
251+
.Setup(x => x.GetSetting(It.Is<string>(y => y == "HubSpotApiKey")))
252+
.Returns(ApiKey);
253+
}
254+
255+
return mockedConfiguration;
256+
}
257+
258+
private static HttpClient CreateMockedHttpClient(HttpStatusCode statusCode, string responseContent = "")
259+
{
260+
var handlerMock = new Mock<HttpMessageHandler>();
261+
var response = new HttpResponseMessage
262+
{
263+
StatusCode = statusCode,
264+
Content = new StringContent(responseContent),
265+
};
266+
267+
handlerMock
268+
.Protected()
269+
.Setup<Task<HttpResponseMessage>>(
270+
"SendAsync",
271+
ItExpr.IsAny<HttpRequestMessage>(),
272+
ItExpr.IsAny<CancellationToken>())
273+
.ReturnsAsync(response);
274+
var httpClient = new HttpClient(handlerMock.Object);
275+
return httpClient;
276+
}
277+
}
278+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net472</TargetFramework>
5+
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Moq" Version="4.16.1" />
11+
<PackageReference Include="NUnit" Version="3.12.0" />
12+
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="UmbracoCms.Core" Version="8.14.0" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<ProjectReference Include="..\Umbraco.Forms.Integrations.Crm.Hubspot\Umbraco.Forms.Integrations.Crm.Hubspot.csproj" />
22+
</ItemGroup>
23+
24+
</Project>

src/Umbraco.Forms.Integrations.Crm.Hubspot/Services/HubspotContactService.cs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ namespace Umbraco.Forms.Integrations.Crm.Hubspot.Services
1515
{
1616
public class HubspotContactService : IContactService
1717
{
18-
private static readonly HttpClient client = new HttpClient();
18+
// Using a static HttpClient (see: https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/).
19+
private readonly static HttpClient s_client = new HttpClient();
20+
21+
// Access to the client within the class is via ClientFactory(), allowing us to mock the responses in tests.
22+
internal static Func<HttpClient> ClientFactory = () => s_client;
1923

2024
private readonly IFacadeConfiguration _configuration;
2125
private readonly ILogger _logger;
@@ -35,27 +39,26 @@ public async Task<IEnumerable<Property>> GetContactProperties()
3539
}
3640

3741
var url = ConstructUrl("properties/contacts", apiKey);
38-
var response = await client.GetAsync(new Uri(url));
42+
var response = await ClientFactory().GetAsync(new Uri(url));
3943
if (response.IsSuccessStatusCode == false)
4044
{
4145
_logger.Error<HubspotContactService>("Failed to fetch contact properties from HubSpot API for mapping. {StatusCode} {ReasonPhrase}", response.StatusCode, response.ReasonPhrase);
4246
return Enumerable.Empty<Property>();
4347
}
4448

45-
// Map Properties back to our simpler object
46-
// Don't need all the fields in the response
49+
// Map the properties to our simpler object, as we don't need all the fields in the response.
4750
var properties = new List<Property>();
48-
var rawResult = await response.Content.ReadAsStringAsync();
49-
var json = JsonConvert.DeserializeObject<PropertiesResponse>(rawResult);
50-
properties.AddRange(json.Results);
51+
var responseContent = await response.Content.ReadAsStringAsync();
52+
var responseContentAsJson = JsonConvert.DeserializeObject<PropertiesResponse>(responseContent);
53+
properties.AddRange(responseContentAsJson.Results);
5154
return properties.OrderBy(x => x.Label);
5255
}
5356

5457
public async Task<CommandResult> PostContact(Record record, List<MappedProperty> fieldMappings)
5558
{
5659
if (!TryGetApiKey(out string apiKey))
5760
{
58-
_logger.Warn<HubspotContactService>("Failed to fetch contact properties from HubSpot API for mapping as no API Key has been configured.");
61+
_logger.Warn<HubspotContactService>("Failed to post contact details via the HubSpot API as no API Key has been configured.");
5962
return CommandResult.NotConfigured;
6063
}
6164

@@ -65,16 +68,15 @@ public async Task<CommandResult> PostContact(Record record, List<MappedProperty>
6568
foreach (var mapping in fieldMappings)
6669
{
6770
var fieldId = mapping.FormField;
68-
var recordField = record.GetRecordField(new Guid(fieldId));
71+
var recordField = record.GetRecordField(Guid.Parse(fieldId));
6972
if (recordField != null)
7073
{
71-
// TODO: What about different field types in forms & Hubspot that are not simple text ones ?
74+
// TODO: What about different field types in forms & Hubspot that are not simple text ones?
7275
postData.Properties.Add(mapping.HubspotField, recordField.ValuesAsString(false));
7376
}
7477
else
7578
{
76-
// There field mapping value could not be found.
77-
// Write a warning in the log
79+
// The field mapping value could not be found so write a warning in the log.
7880
_logger.Warn<HubspotContactService>("The field mapping with Id, {FieldMappingId}, did not match any record fields. This is probably caused by the record field being marked as sensitive and the workflow has been set not to include sensitive data", mapping.FormField);
7981
}
8082
}
@@ -86,7 +88,7 @@ public async Task<CommandResult> PostContact(Record record, List<MappedProperty>
8688
// POST data to hubspot
8789
// https://api.hubapi.com/crm/v3/objects/contacts?hapikey=YOUR_HUBSPOT_API_KEY
8890
var url = ConstructUrl("objects/contacts", apiKey);
89-
var response = await client.PostAsync(url, content).ConfigureAwait(false);
91+
var response = await ClientFactory().PostAsync(url, content).ConfigureAwait(false);
9092

9193
// Depending on POST status fail or mark workflow as completed
9294
if (response.IsSuccessStatusCode == false)
@@ -101,13 +103,7 @@ public async Task<CommandResult> PostContact(Record record, List<MappedProperty>
101103
private bool TryGetApiKey(out string apiKey)
102104
{
103105
apiKey = _configuration.GetSetting("HubSpotApiKey");
104-
if (string.IsNullOrEmpty(apiKey))
105-
{
106-
_logger.Warn<HubspotContactService>("Failed to fetch contact properties from HubSpot API for mapping as no API Key has been configured.");
107-
return false;
108-
}
109-
110-
return true;
106+
return !string.IsNullOrEmpty(apiKey);
111107
}
112108

113109
private string ConstructUrl(string path, string apiKey)

0 commit comments

Comments
 (0)