Skip to content
This repository has been archived by the owner on Jan 19, 2021. It is now read-only.

Improvements and small bug fix in List Webhooks extension methods #1233

Merged
merged 3 commits into from Jun 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions Core/OfficeDevPnP.Core.Tests/App.config.sample
Expand Up @@ -78,6 +78,12 @@


<!--Testing for Webhook test endpoint-->
<!--
CAUTION:
If the Webhook endpoint is an Azure Function.
Sometimes (probably according to configuration), an Azure function gone idle can take more than 5 seconds to respond.
This could lead to unit test fail...
-->
<add key="WebHookTestUrl" value="https://[test-functions].azurewebsites.net" />
</appSettings>
<system.diagnostics>
Expand Down
57 changes: 57 additions & 0 deletions Core/OfficeDevPnP.Core.Tests/Extensions/ListExtensionsTests.cs
Expand Up @@ -350,6 +350,63 @@ public void AddWebhookTest()
}
}

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void AddWebhookWithInvalidExpirationDateTest()
{
using (var clientContext = TestCommon.CreateClientContext())
{
var testList = clientContext.Web.Lists.GetById(webHookListId);
clientContext.Load(testList);
clientContext.ExecuteQueryRetry();

testList.AddWebhookSubscription(TestCommon.TestWebhookUrl, 12);
}
}

[TestMethod]
public void AddWebhookWithVeryLastValidExpirationDateTest()
{
using (var clientContext = TestCommon.CreateClientContext())
{
var testList = clientContext.Web.Lists.GetById(webHookListId);
clientContext.Load(testList);
clientContext.ExecuteQueryRetry();

DateTime veryLastValidExpiration = DateTime.UtcNow.AddDays(180).AddMinutes(90);

WebhookSubscription expectedSubscription = new WebhookSubscription()
{
ExpirationDateTime = veryLastValidExpiration,
NotificationUrl = TestCommon.TestWebhookUrl,
Resource = TestCommon.DevSiteUrl + string.Format("/_api/lists('{0}')", webHookListId)
};
WebhookSubscription actualSubscription = testList.AddWebhookSubscription(TestCommon.TestWebhookUrl, veryLastValidExpiration);

// Compare properties of expected and actual
Assert.IsTrue(Equals(expectedSubscription.ClientState, actualSubscription.ClientState)
&& Equals(expectedSubscription.ExpirationDateTime.Date, actualSubscription.ExpirationDateTime.Date)
&& Equals(expectedSubscription.NotificationUrl, actualSubscription.NotificationUrl)
&& expectedSubscription.Resource.Contains(actualSubscription.Resource));
}
}

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void AddWebhookWithBarelyInvalidExpirationDateTest()
{
using (var clientContext = TestCommon.CreateClientContext())
{
var testList = clientContext.Web.Lists.GetById(webHookListId);
clientContext.Load(testList);
clientContext.ExecuteQueryRetry();

DateTime barelyInvalidExpiration = DateTime.UtcNow.AddDays(180).AddMinutes(95);

testList.AddWebhookSubscription(TestCommon.TestWebhookUrl, barelyInvalidExpiration);
}
}

[TestMethod]
public void UpdateWebhookTest()
{
Expand Down
103 changes: 79 additions & 24 deletions Core/OfficeDevPnP.Core/Extensions/ListExtensions.cs
Expand Up @@ -32,7 +32,7 @@ public static partial class ListExtensions
/// </summary>
private static readonly char[] UrlDelimiters = { '\\', '/' };

#region Event Receivers
#region Event Receivers

/// <summary>
/// Registers a remote event receiver
Expand Down Expand Up @@ -141,9 +141,9 @@ in list.EventReceivers
return null;
}

#endregion
#endregion

#region Webhooks
#region Webhooks
#if !ONPREMISES
/// <summary>
/// Add the a Webhook subscription to a list
Expand All @@ -158,16 +158,27 @@ in list.EventReceivers
public static WebhookSubscription AddWebhookSubscription(this List list, string notificationUrl, DateTime expirationDate, string clientState = null, string accessToken = null)
{
// Get the access from the client context if not specified.
accessToken = accessToken ?? list.Context.GetAccessToken();
accessToken = accessToken ?? list.Context.GetAccessToken();

return WebhookUtility.AddWebhookSubscriptionAsync(list.Context.Url,
WebHookResourceType.List, accessToken, list.Context as ClientContext, new WebhookSubscription()
{
Resource = list.Id.ToString(),
ExpirationDateTime = expirationDate,
NotificationUrl = notificationUrl,
ClientState = clientState
}).Result;
// Ensure the list Id is known
Guid listId = list.EnsureProperty(l => l.Id);

try
{
return WebhookUtility.AddWebhookSubscriptionAsync(list.Context.Url,
WebHookResourceType.List, accessToken, list.Context as ClientContext, new WebhookSubscription()
{
Resource = listId.ToString(),
ExpirationDateTime = expirationDate,
NotificationUrl = notificationUrl,
ClientState = clientState
}).Result;
}
catch (AggregateException ex)
{
// Rethrow the inner exception of the AggregateException thrown by the async method
throw ex.InnerException ?? ex;
}
}

/// <summary>
Expand All @@ -180,12 +191,23 @@ public static WebhookSubscription AddWebhookSubscription(this List list, string
/// <param name="clientState">The client state to use in the Webhook subscription</param>
/// <param name="accessToken">(optional) The access token to SharePoint</param>
/// <returns>The added subscription object</returns>
public static WebhookSubscription AddWebhookSubscription(this List list, string notificationUrl, int validityInMonths = 3, string clientState = null, string accessToken = null)
public static WebhookSubscription AddWebhookSubscription(this List list, string notificationUrl, int validityInMonths = 6, string clientState = null, string accessToken = null)
{
// Get the access from the client context if not specified.
accessToken = accessToken ?? list.Context.GetAccessToken();

return WebhookUtility.AddWebhookSubscriptionAsync(list.Context.Url, WebHookResourceType.List, accessToken, list.Context as ClientContext, list.Id.ToString(), notificationUrl, clientState, validityInMonths).Result;
// Ensure the list Id is known
Guid listId = list.EnsureProperty(l => l.Id);

try
{
return WebhookUtility.AddWebhookSubscriptionAsync(list.Context.Url, WebHookResourceType.List, accessToken, list.Context as ClientContext, listId.ToString(), notificationUrl, clientState, validityInMonths).Result;
}
catch (AggregateException ex)
{
// Rethrow the inner exception of the AggregateException thrown by the async method
throw ex.InnerException ?? ex;
}
}

/// <summary>
Expand All @@ -203,7 +225,18 @@ public static bool UpdateWebhookSubscription(this List list, string subscription
// Get the access from the client context if not specified.
accessToken = accessToken ?? list.Context.GetAccessToken();

return WebhookUtility.UpdateWebhookSubscriptionAsync(list.Context.Url, WebHookResourceType.List, list.Id.ToString(), subscriptionId, webHookEndPoint, expirationDateTime, accessToken, list.Context as ClientContext).Result;
// Ensure the list Id is known
Guid listId = list.EnsureProperty(l => l.Id);

try
{
return WebhookUtility.UpdateWebhookSubscriptionAsync(list.Context.Url, WebHookResourceType.List, listId.ToString(), subscriptionId, webHookEndPoint, expirationDateTime, accessToken, list.Context as ClientContext).Result;
}
catch (AggregateException ex)
{
// Rethrow the inner exception of the AggregateException thrown by the async method
throw ex.InnerException ?? ex;
}
}

/// <summary>
Expand Down Expand Up @@ -247,7 +280,18 @@ public static bool RemoveWebhookSubscription(this List list, string subscription
// Get the access from the client context if not specified.
accessToken = accessToken ?? list.Context.GetAccessToken();

return WebhookUtility.RemoveWebhookSubscriptionAsync(list.Context.Url, WebHookResourceType.List, list.Id.ToString(), subscriptionId, accessToken, list.Context as ClientContext).Result;
// Ensure the list Id is known
Guid listId = list.EnsureProperty(l => l.Id);

try
{
return WebhookUtility.RemoveWebhookSubscriptionAsync(list.Context.Url, WebHookResourceType.List, listId.ToString(), subscriptionId, accessToken, list.Context as ClientContext).Result;
}
catch (AggregateException ex)
{
// Rethrow the inner exception of the AggregateException thrown by the async method
throw ex.InnerException ?? ex;
}
}

/// <summary>
Expand Down Expand Up @@ -288,12 +332,23 @@ public static IList<WebhookSubscription> GetWebhookSubscriptions(this List list,
// Get the access from the client context if not specified.
accessToken = accessToken ?? list.Context.GetAccessToken();

return WebhookUtility.GetWebhooksSubscriptionsAsync(list.Context.Url, WebHookResourceType.List, list.Id.ToString(), accessToken, list.Context as ClientContext).Result.Value;
// Ensure the list Id is known
Guid listId = list.EnsureProperty(l => l.Id);

try
{
return WebhookUtility.GetWebhooksSubscriptionsAsync(list.Context.Url, WebHookResourceType.List, listId.ToString(), accessToken, list.Context as ClientContext).Result.Value;
}
catch (AggregateException ex)
{
// Rethrow the inner exception of the AggregateException thrown by the async method
throw ex.InnerException ?? ex;
}
}
#endif
#endregion
#endregion

#region List Properties
#region List Properties

/// <summary>
/// Sets a key/value pair in the web property bag
Expand Down Expand Up @@ -419,7 +474,7 @@ public static bool PropertyBagContainsKey(this List list, string key)
}
}

#endregion
#endregion

/// <summary>
/// Removes a content type from a list/library by name
Expand Down Expand Up @@ -1005,7 +1060,7 @@ private static string GetWebRelativeUrl(string listRootFolderServerRelativeUrl,
return listWebRelativeUrl.Trim(UrlDelimiters);
}

#region List Permissions
#region List Permissions

/// <summary>
/// Set custom permission to the list
Expand Down Expand Up @@ -1065,9 +1120,9 @@ public static void SetListPermission(this List list, Principal principal, RoleTy
list.Context.ExecuteQueryRetry();
}

#endregion
#endregion

#region List view
#region List view

/// <summary>
/// Creates list views based on specific xml structure from file
Expand Down Expand Up @@ -1305,7 +1360,7 @@ public static View GetViewByName(this List list, string name, params Expression<

}

#endregion
#endregion

private static void SetDefaultColumnValuesImplementation(this List list, IEnumerable<IDefaultColumnValue> columnValues)
{
Expand Down
66 changes: 54 additions & 12 deletions Core/OfficeDevPnP.Core/Utilities/Webhooks/WebhookUtility.cs
Expand Up @@ -28,9 +28,22 @@ public enum WebHookResourceType
/// </summary>
internal static class WebhookUtility
{

private const string SubscriptionsUrlPart = "subscriptions";
private const string ListIdentifierFormat = @"{0}/_api/web/lists('{1}')";
public const int MaximumValidityInMonths = 6;
public const int ExpirationDateTimeMaxDays = 180;
public const int ExpirationDateTimeGraceMinutes = 90;

/// <summary>
/// The effective maximum expiration datetime
/// </summary>
public static DateTime MaxExpirationDateTime
{
get
{
return DateTime.UtcNow.AddDays(ExpirationDateTimeMaxDays).AddMinutes(ExpirationDateTimeGraceMinutes);
}
}

/// <summary>
/// Add a Webhook subscription to a SharePoint resource
Expand All @@ -40,9 +53,13 @@ internal static class WebhookUtility
/// <param name="accessToken">Access token to authenticate against SharePoint</param>
/// <param name="context">ClientContext instance to use for authentication</param>
/// <param name="subscription">The Webhook subscription to add</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when expiration date is out of valid range.</exception>
/// <returns>The added subscription object</returns>
internal static async Task<WebhookSubscription> AddWebhookSubscriptionAsync(string webUrl, WebHookResourceType resourceType, string accessToken, ClientContext context, WebhookSubscription subscription)
{
if (!ValidateExpirationDateTime(subscription.ExpirationDateTime))
throw new ArgumentOutOfRangeException(nameof(subscription.ExpirationDateTime), "The specified expiration date is invalid. Should be greater than today and within 6 months");

string responseString = null;
using (var handler = new HttpClientHandler())
{
Expand Down Expand Up @@ -97,15 +114,21 @@ internal static async Task<WebhookSubscription> AddWebhookSubscriptionAsync(stri
/// <param name="notificationUrl">The Webhook endpoint URL</param>
/// <param name="clientState">The client state to use in the Webhook subscription</param>
/// <param name="validityInMonths">The validity of the subscriptions in months</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when expiration date is out of valid range.</exception>
/// <returns>The added subscription object</returns>
internal static async Task<WebhookSubscription> AddWebhookSubscriptionAsync(string webUrl, WebHookResourceType resourceType, string accessToken, ClientContext context, string resourceId, string notificationUrl,
string clientState = null, int validityInMonths = 3)
internal static async Task<WebhookSubscription> AddWebhookSubscriptionAsync(string webUrl, WebHookResourceType resourceType, string accessToken, ClientContext context, string resourceId, string notificationUrl,
string clientState = null, int validityInMonths = MaximumValidityInMonths)
{
// If validity in months is the Maximum, use the effective max allowed DateTime instead
DateTime expirationDateTime = validityInMonths == MaximumValidityInMonths
? MaxExpirationDateTime
: DateTime.UtcNow.AddMonths(validityInMonths);

var subscription = new WebhookSubscription()
{
Resource = resourceId,
NotificationUrl = notificationUrl,
ExpirationDateTime = DateTime.Now.AddMonths(validityInMonths).ToUniversalTime(),
ExpirationDateTime = expirationDateTime,
ClientState = clientState
};

Expand All @@ -123,9 +146,14 @@ internal static async Task<WebhookSubscription> AddWebhookSubscriptionAsync(stri
/// <param name="expirationDateTime">New web hook expiration date</param>
/// <param name="accessToken">Access token to authenticate against SharePoint</param>
/// <param name="context">ClientContext instance to use for authentication</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when expiration date is out of valid range.</exception>
/// <returns>true if succesful, exception in case something went wrong</returns>
internal static async Task<bool> UpdateWebhookSubscriptionAsync(string webUrl, WebHookResourceType resourceType, string resourceId, string subscriptionId, string webHookEndPoint, DateTime expirationDateTime, string accessToken, ClientContext context)
internal static async Task<bool> UpdateWebhookSubscriptionAsync(string webUrl, WebHookResourceType resourceType, string resourceId, string subscriptionId,
string webHookEndPoint, DateTime expirationDateTime, string accessToken, ClientContext context)
{
if (!ValidateExpirationDateTime(expirationDateTime))
throw new ArgumentOutOfRangeException(nameof(expirationDateTime), "The specified expiration date is invalid. Should be greater than today and within 6 months");

using (var handler = new HttpClientHandler())
{
if (String.IsNullOrEmpty(accessToken))
Expand All @@ -148,14 +176,14 @@ internal static async Task<bool> UpdateWebhookSubscriptionAsync(string webUrl, W
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod("PATCH"), requestUrl);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

request.Content = new StringContent(JsonConvert.SerializeObject(
new WebhookSubscription()
{
NotificationUrl = webHookEndPoint,
ExpirationDateTime = expirationDateTime.ToUniversalTime(),
}),
Encoding.UTF8, "application/json");
new WebhookSubscription()
{
NotificationUrl = webHookEndPoint,
ExpirationDateTime = expirationDateTime
}),
Encoding.UTF8, "application/json");

HttpResponseMessage response = await httpClient.SendAsync(request);

Expand Down Expand Up @@ -290,6 +318,20 @@ private static string GetResourceIdentifier(WebHookResourceType resourceType, st
return null;
}
}

/// <summary>
/// Checks whether the specified expiration datetime is within a valid period
/// </summary>
/// <param name="expirationDateTime">The datetime value to validate</param>
/// <returns><c>true</c> if valid, <c>false</c> otherwise</returns>
private static bool ValidateExpirationDateTime(DateTime expirationDateTime)
{
DateTime utcDateToValidate = expirationDateTime.ToUniversalTime();
DateTime utcNow = DateTime.UtcNow;

return utcDateToValidate > utcNow
&& utcDateToValidate <= MaxExpirationDateTime;
}
}
}
#endif