Skip to content

Commit 4d85bc3

Browse files
authored
Merge pull request #3 from umbraco/feature/add-hubspot-integration
Hubspot Forms integration.
2 parents f5018c9 + 12a10be commit 4d85bc3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2493
-252
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,8 @@ src/Umbraco.Cms.Integrations.Testsite.V9/App_Plugins
324324
src/Umbraco.Cms.Integrations.Testsite.V9/umbraco
325325
src/Umbraco.Cms.Integrations.Testsite.V9/wwwroot/media
326326

327+
src/Umbraco.Cms.Integrations.Crm.Hubspot.Testsite.V8/App_Data/
328+
src/Umbraco.Cms.Integrations.Crm.Hubspot.Testsite.V8/App_Plugins/
329+
src/Umbraco.Cms.Integrations.Crm.Hubspot.Testsite.V8/Media/
330+
src/Umbraco.Cms.Integrations.SEO.Semrush.Testsite.V8/App_Data/
331+
src/Umbraco.Cms.Integrations.SEO.Semrush.Testsite.V8/App_Plugins/

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ This repository houses open-source extensions, created for Umbraco CMS, that int
88

99
[CommerceTools](./src/Umbraco.Cms.Integrations.Commerce.CommerceTools/) - a product and category picker that can be added as a property editor for content, with a value converter providing a strongly typed model for rendering.
1010

11+
### SEO
12+
1113
[Semrush](./src/Umbraco.Cms.Integrations.SEO.Semrush/) - a search tool available as a content app, helping editors research and use appropriate keywords for their content, to help with website search engine optimisation.
14+
15+
### CRM
16+
17+
[HubSpot](./src/Umbraco.Cms.Integrations.Crm.HubSpot/) - a form picker and rendering component for Hubspot forms.

src/Umbraco.Cms.Integrations.Commerce.CommerceTools/readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This integration provides a product and category picker, with data sourced from
44

55
## Prerequisites
66

7-
Requires minimum versions of Umbraco CMS: 8.5.0
7+
Requires minimum version of Umbraco CMS: 8.5.4.
88

99
## How To Use
1010

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<%@ Page Language="C#" AutoEventWireup="True" Inherits="Umbraco.Web.UI.Config.Splashes.NoNodes" CodeBehind="NoNodes.aspx.cs" %>
2+
<%@ Import Namespace="Umbraco.Core.Configuration" %>
3+
<%@ Import Namespace="Umbraco.Core.IO" %>
4+
5+
<!doctype html>
6+
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
7+
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
8+
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]-->
9+
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
10+
<head>
11+
<meta charset="utf-8">
12+
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
13+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
14+
15+
<title></title>
16+
<meta name="description" content="">
17+
<meta name="author" content="">
18+
19+
<link rel="stylesheet" href="../../Umbraco/assets/css/nonodes.style.min.css" />
20+
21+
</head>
22+
<body>
23+
24+
<section>
25+
<article>
26+
<div>
27+
<div class="logo"></div>
28+
29+
<h1>Welcome to your Umbraco installation</h1>
30+
<h3>You're seeing this wonderful page because your website doesn't contain any published content yet.</h3>
31+
32+
<div class="cta">
33+
<a href="<%= IOHelper.ResolveUrl(SystemDirectories.Umbraco) %>" class="button">Open Umbraco</a>
34+
</div>
35+
36+
37+
<div class="row">
38+
<div class="col">
39+
<h2>Easy start with Umbraco.tv</h2>
40+
<p>We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.</p>
41+
42+
<a href="https://umbraco.tv?ref=tvFromInstaller" target="_blank">Umbraco.tv &rarr;</a>
43+
</div>
44+
45+
<div class="col">
46+
<h2>Be a part of the community</h2>
47+
<p>The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.</p>
48+
49+
<a href="https://our.umbraco.com/?ref=ourFromInstaller" target="_blank">our.Umbraco &rarr;</a>
50+
</div>
51+
</div>
52+
53+
</div>
54+
</article>
55+
56+
</section>
57+
58+
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
59+
60+
</body>
61+
</html>
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using System.Web;
11+
using Microsoft.AspNet.SignalR.Hubs;
12+
using Moq;
13+
using Moq.Protected;
14+
using Newtonsoft.Json;
15+
using NUnit.Framework;
16+
using NUnit.Framework.Internal;
17+
using Umbraco.Cms.Integrations.Crm.Hubspot.Configuration;
18+
using Umbraco.Cms.Integrations.Crm.Hubspot.Controllers;
19+
using Umbraco.Cms.Integrations.Crm.Hubspot.Models;
20+
using Umbraco.Cms.Integrations.Crm.Hubspot.Models.Dtos;
21+
using Umbraco.Cms.Integrations.Crm.Hubspot.Services;
22+
using Umbraco.Core.Composing;
23+
using Umbraco.Core.Configuration;
24+
using Umbraco.Core.Configuration.UmbracoSettings;
25+
using Umbraco.Core.Logging;
26+
using Umbraco.Web;
27+
using Umbraco.Web.Routing;
28+
using Umbraco.Web.Security;
29+
using ILogger = Umbraco.Core.Logging.ILogger;
30+
31+
namespace Umbraco.Cms.Integrations.Crm.Hubspot.Tests.Controllers
32+
{
33+
[TestFixture]
34+
public class FormsControllerTests
35+
{
36+
private readonly string InvalidApiKey = @"{
37+
""status"": ""error"",
38+
""message"": ""This hapikey doesn't exist."",
39+
""correlationId"": ""73f4a25b-7b0a-4537-a490-e5c226619d59""
40+
}";
41+
42+
private readonly string ExpiredOAuthToken = @"{
43+
""status"": ""error"",
44+
""message"": ""This oauth-token is expired! expiresAt: 1643194402611, now: 1643972072320"",
45+
""correlationId"": ""1ae46a55-f5b9-4f68-811a-695a12aaa4f5"",
46+
""category"": ""EXPIRED_AUTHENTICATION"",
47+
""errors"": [
48+
{
49+
""message"": ""The OAuth token used to make this call expired 9 day(s) ago.""
50+
}
51+
]
52+
}";
53+
54+
private Mock<IAppSettings> MockedAppSettingsApiSetup;
55+
56+
private Mock<IAppSettings> MockedAppSettingsOAuthSetup;
57+
58+
private Mock<IAppSettings> MockedAppSettingsNoSetup;
59+
60+
private Mock<ILogger> MockedLogger;
61+
62+
public FormsControllerTests()
63+
{
64+
Current.Factory = new Mock<IFactory>().Object;
65+
}
66+
67+
[SetUp]
68+
public void Init()
69+
{
70+
MockedAppSettingsApiSetup = CreateMockedAppSettings(true);
71+
72+
MockedAppSettingsOAuthSetup = CreateMockedAppSettings();
73+
74+
MockedAppSettingsNoSetup = CreateMockedAppSettings();
75+
76+
MockedLogger = new Mock<ILogger>();
77+
}
78+
79+
#region CheckConfiguration
80+
81+
[Test]
82+
public void CheckApiConfiguration_WithApiConfig_ShouldReturnValidConfigurationResponseObjectWithType()
83+
{
84+
var sut = new FormsController(MockedAppSettingsApiSetup.Object, Mock.Of<ITokenService>(), Mock.Of<ILogger>());
85+
86+
var result = sut.CheckConfiguration();
87+
88+
Assert.That(result.IsValid, Is.True);
89+
Assert.AreEqual(result.Type.Value, ConfigurationType.Api.Value);
90+
}
91+
92+
[Test]
93+
public void CheckOAuthConfiguration_WithOAuthConfigAndNoApiConfig_ShouldReturnValidConfigurationResponseObjectWithType()
94+
{
95+
var sut = new FormsController(MockedAppSettingsOAuthSetup.Object, Mock.Of<ITokenService>(), Mock.Of<ILogger>());
96+
97+
var result = sut.CheckConfiguration();
98+
99+
Assert.That(result.IsValid, Is.True);
100+
Assert.AreEqual(result.Type.Value, ConfigurationType.OAuth.Value);
101+
}
102+
103+
#endregion
104+
105+
#region GetAllUsingApiKey
106+
107+
[Test]
108+
public async Task GetAll_WithoutApiKey_ShouldReturnInvalidResponseObjectWithLoggedInfo()
109+
{
110+
var sut = new FormsController(Mock.Of<IAppSettings>(), Mock.Of<ITokenService>(), MockedLogger.Object);
111+
112+
var result = await sut.GetAll();
113+
114+
MockedLogger.Verify(x => x.Info(It.Is<Type>(y => y == typeof(FormsController)), It.IsAny<string>()), Times.Once);
115+
116+
Assert.That(result.IsValid, Is.False);
117+
Assert.That(result.IsExpired, Is.False);
118+
}
119+
120+
[Test]
121+
public async Task GetAll_WithUnauthorizedRequest_ShouldReturnExpiredResponseObjectWithLoggedError()
122+
{
123+
var sut = new FormsController(MockedAppSettingsApiSetup.Object, Mock.Of<ITokenService>(), MockedLogger.Object);
124+
125+
var httpClient = CreateMockedHttpClient(HttpStatusCode.Unauthorized, InvalidApiKey);
126+
FormsController.ClientFactory = () => httpClient;
127+
128+
var result = await sut.GetAll();
129+
130+
MockedLogger.Verify(x => x.Error(It.Is<Type>(y => y == typeof(FormsController)), It.IsAny<string>()), Times.Once);
131+
132+
Assert.That(result.IsValid, Is.False);
133+
Assert.That(result.IsExpired, Is.True);
134+
}
135+
136+
[Test]
137+
public async Task GetAll_WithSuccessfulRequest_ShouldReturnResponseObjectWithFormsCollection()
138+
{
139+
var sut = new FormsController(MockedAppSettingsApiSetup.Object, Mock.Of<ITokenService>(), MockedLogger.Object);
140+
141+
var response = File.ReadAllText(TestContext.CurrentContext.TestDirectory + "\\Data\\mockResponseApiSetup.json");
142+
143+
var httpClient = CreateMockedHttpClient(HttpStatusCode.OK, response);
144+
FormsController.ClientFactory = () => httpClient;
145+
146+
var result = await sut.GetAll();
147+
148+
Assert.That(result.IsValid, Is.True);
149+
Assert.That(result.IsExpired, Is.False);
150+
Assert.AreEqual(1, result.Forms.Count);
151+
}
152+
153+
[Test]
154+
public async Task GetAll_WithFailedRequest_ShouldReturnDefaultResponseObjectWithLoggedError()
155+
{
156+
var sut = new FormsController(MockedAppSettingsApiSetup.Object, Mock.Of<ITokenService>(), MockedLogger.Object);
157+
158+
var response = File.ReadAllText(TestContext.CurrentContext.TestDirectory + "\\Data\\mockResponseApiSetup.json");
159+
160+
var httpClient = CreateMockedHttpClient(HttpStatusCode.InternalServerError, response);
161+
FormsController.ClientFactory = () => httpClient;
162+
163+
var result = await sut.GetAll();
164+
165+
MockedLogger.Verify(x => x.Error(It.Is<Type>(y => y == typeof(FormsController)), It.IsAny<string>()), Times.Once);
166+
167+
Assert.That(result.IsExpired, Is.False);
168+
Assert.That(result.IsValid, Is.False);
169+
Assert.That(result.Forms, Is.Empty);
170+
}
171+
172+
#endregion
173+
174+
#region GetAllUsingOAuth
175+
176+
[Test]
177+
public async Task GetAllOAuth_WithoutAccessToken_ShouldReturnInvalidResponseObjectWithLoggedInfo()
178+
{
179+
var mockedTokenService = CreateMockedTokenService(false);
180+
181+
var sut = new FormsController(Mock.Of<IAppSettings>(), mockedTokenService.Object, MockedLogger.Object);
182+
183+
var result = await sut.GetAllOAuth();
184+
185+
MockedLogger.Verify(x => x.Info(It.Is<Type>(y => y == typeof(FormsController)), It.IsAny<string>()), Times.Once);
186+
187+
Assert.That(result.IsValid, Is.False);
188+
Assert.That(result.IsExpired, Is.False);
189+
}
190+
191+
[Test]
192+
public async Task GetAllOAuth_WithUnauthorizedRequest_ShouldReturnExpiredResponseObjectWithLoggedError()
193+
{
194+
var mockedTokenService = CreateMockedTokenService(true);
195+
196+
var sut = new FormsController(MockedAppSettingsApiSetup.Object, mockedTokenService.Object, MockedLogger.Object);
197+
198+
var httpClient = CreateMockedHttpClient(HttpStatusCode.Unauthorized);
199+
FormsController.ClientFactory = () => httpClient;
200+
201+
var result = await sut.GetAllOAuth();
202+
203+
MockedLogger.Verify(x => x.Error(It.Is<Type>(y => y == typeof(FormsController)), It.IsAny<string>()), Times.Once);
204+
205+
Assert.That(result.IsValid, Is.False);
206+
Assert.That(result.IsExpired, Is.True);
207+
}
208+
209+
[Test]
210+
public async Task GetAllOAuth_WithSuccessfulRequest_ShouldReturnResponseObjectWithFormsCollection()
211+
{
212+
var mockedTokenService = CreateMockedTokenService(true);
213+
214+
var sut = new FormsController(MockedAppSettingsApiSetup.Object, mockedTokenService.Object, MockedLogger.Object);
215+
216+
var response = File.ReadAllText(TestContext.CurrentContext.TestDirectory + "\\Data\\mockResponseOAuthSetup.json");
217+
218+
var httpClient = CreateMockedHttpClient(HttpStatusCode.OK, response);
219+
FormsController.ClientFactory = () => httpClient;
220+
221+
var result = await sut.GetAll();
222+
223+
Assert.That(result.IsValid, Is.True);
224+
Assert.That(result.IsExpired, Is.False);
225+
Assert.AreEqual(1, result.Forms.Count);
226+
}
227+
228+
[Test]
229+
public async Task GetAllOAuth_WithFailedRequest_ShouldReturnDefaultResponseObjectWithLoggedError()
230+
{
231+
var mockedTokenService = CreateMockedTokenService(true);
232+
233+
var sut = new FormsController(MockedAppSettingsApiSetup.Object, mockedTokenService.Object, MockedLogger.Object);
234+
235+
var response = File.ReadAllText(TestContext.CurrentContext.TestDirectory + "\\Data\\mockResponseApiSetup.json");
236+
237+
var httpClient = CreateMockedHttpClient(HttpStatusCode.InternalServerError, response);
238+
FormsController.ClientFactory = () => httpClient;
239+
240+
var result = await sut.GetAll();
241+
242+
MockedLogger.Verify(x => x.Error(It.Is<Type>(y => y == typeof(FormsController)), It.IsAny<string>()), Times.Once);
243+
244+
Assert.That(result.IsExpired, Is.False);
245+
Assert.That(result.IsValid, Is.False);
246+
Assert.That(result.Forms, Is.Empty);
247+
}
248+
249+
#endregion
250+
251+
private static Mock<IAppSettings> CreateMockedAppSettings(bool includeApiKeySettings = false)
252+
{
253+
var mockedAppSettings = new Mock<IAppSettings>();
254+
255+
if (includeApiKeySettings)
256+
{
257+
mockedAppSettings
258+
.Setup(c => c[AppSettingsConstants.UmbracoCmsIntegrationsCrmHubspotApiKey])
259+
.Returns("test-api-key");
260+
}
261+
262+
return mockedAppSettings;
263+
}
264+
265+
private static Mock<ITokenService> CreateMockedTokenService(bool includeAccessToken)
266+
{
267+
var mockedTokenService = new Mock<ITokenService>();
268+
269+
if (includeAccessToken)
270+
{
271+
string key = "Umbraco.Cms.Integrations.Hubspot.AccessTokenDbKey";
272+
string expectedValue = "CNzV5ansLxICBAMY7N6UDCDJ3LYNKNuEJjIUnFi8abi-btslTeL14maOJ3J7CK86KABAIEEAAAAAAAAwAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCFEZUslFv3AAjejJzrxfHYDNIvbcnSgNldTFSAFoA";
273+
274+
mockedTokenService.Setup(x => x.TryGetParameters(key, out expectedValue)).Returns(true);
275+
}
276+
277+
return mockedTokenService;
278+
}
279+
280+
281+
private static HttpClient CreateMockedHttpClient(HttpStatusCode statusCode, string responseContent = "")
282+
{
283+
var handlerMock = new Mock<HttpMessageHandler>();
284+
handlerMock
285+
.Protected()
286+
.Setup<Task<HttpResponseMessage>>(
287+
"SendAsync",
288+
ItExpr.IsAny<HttpRequestMessage>(),
289+
ItExpr.IsAny<CancellationToken>())
290+
.ReturnsAsync(new HttpResponseMessage()
291+
{
292+
StatusCode = statusCode,
293+
Content = new StringContent(responseContent)
294+
});
295+
296+
var httpClient = new HttpClient(handlerMock.Object);
297+
298+
return httpClient;
299+
}
300+
}
301+
}

0 commit comments

Comments
 (0)