diff --git a/src/Umbraco.Core/Services/IWebhookService.cs b/src/Umbraco.Core/Services/IWebhookService.cs index 38306839ba..657f29df59 100644 --- a/src/Umbraco.Core/Services/IWebhookService.cs +++ b/src/Umbraco.Core/Services/IWebhookService.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; @@ -8,19 +9,19 @@ public interface IWebhookService /// Creates a webhook. /// /// to create. - Task CreateAsync(Webhook webhook); + Task> CreateAsync(Webhook webhook); /// /// Updates a webhook. /// /// to update. - Task UpdateAsync(Webhook webhook); + Task> UpdateAsync(Webhook webhook); /// /// Deletes a webhook. /// /// The unique key of the webhook. - Task DeleteAsync(Guid key); + Task> DeleteAsync(Guid key); /// /// Gets a webhook by its key. diff --git a/src/Umbraco.Core/Services/OperationStatus/WebhookOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/WebhookOperationStatus.cs new file mode 100644 index 0000000000..c0514aea69 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/WebhookOperationStatus.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum WebhookOperationStatus +{ + Success, + CancelledByNotification, + NotFound, +} diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs index 40a827c51e..1f707606b5 100644 --- a/src/Umbraco.Core/Services/WebhookService.cs +++ b/src/Umbraco.Core/Services/WebhookService.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; @@ -20,30 +21,29 @@ public class WebhookService : IWebhookService } /// - public async Task CreateAsync(Webhook webhook) + public async Task> CreateAsync(Webhook webhook) { using ICoreScope scope = _provider.CreateCoreScope(); EventMessages eventMessages = _eventMessagesFactory.Get(); var savingNotification = new WebhookSavingNotification(webhook, eventMessages); - if (scope.Notifications.PublishCancelable(savingNotification)) + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) { scope.Complete(); - return null; + return Attempt.FailWithStatus(WebhookOperationStatus.CancelledByNotification, webhook); } Webhook created = await _webhookRepository.CreateAsync(webhook); - scope.Notifications.Publish( - new WebhookSavedNotification(webhook, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new WebhookSavedNotification(webhook, eventMessages).WithStateFrom(savingNotification)); scope.Complete(); - return created; + return Attempt.SucceedWithStatus(WebhookOperationStatus.Success, created); } /// - public async Task UpdateAsync(Webhook webhook) + public async Task> UpdateAsync(Webhook webhook) { using ICoreScope scope = _provider.CreateCoreScope(); @@ -51,15 +51,16 @@ public class WebhookService : IWebhookService if (currentWebhook is null) { - throw new ArgumentException("Webhook does not exist"); + scope.Complete(); + return Attempt.FailWithStatus(WebhookOperationStatus.NotFound, webhook); } EventMessages eventMessages = _eventMessagesFactory.Get(); var savingNotification = new WebhookSavingNotification(webhook, eventMessages); - if (scope.Notifications.PublishCancelable(savingNotification)) + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) { scope.Complete(); - return; + return Attempt.FailWithStatus(WebhookOperationStatus.CancelledByNotification, webhook); } currentWebhook.Enabled = webhook.Enabled; @@ -70,33 +71,37 @@ public class WebhookService : IWebhookService await _webhookRepository.UpdateAsync(currentWebhook); - scope.Notifications.Publish( - new WebhookSavedNotification(webhook, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish(new WebhookSavedNotification(webhook, eventMessages).WithStateFrom(savingNotification)); scope.Complete(); + + return Attempt.SucceedWithStatus(WebhookOperationStatus.Success, webhook); } /// - public async Task DeleteAsync(Guid key) + public async Task> DeleteAsync(Guid key) { using ICoreScope scope = _provider.CreateCoreScope(); Webhook? webhook = await _webhookRepository.GetAsync(key); - if (webhook is not null) + if (webhook is null) { - EventMessages eventMessages = _eventMessagesFactory.Get(); - var deletingNotification = new WebhookDeletingNotification(webhook, eventMessages); - if (scope.Notifications.PublishCancelable(deletingNotification)) - { - scope.Complete(); - return; - } - - await _webhookRepository.DeleteAsync(webhook); - scope.Notifications.Publish( - new WebhookDeletedNotification(webhook, eventMessages).WithStateFrom(deletingNotification)); + return Attempt.FailWithStatus(WebhookOperationStatus.NotFound, webhook); } + EventMessages eventMessages = _eventMessagesFactory.Get(); + var deletingNotification = new WebhookDeletingNotification(webhook, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(deletingNotification)) + { + scope.Complete(); + return Attempt.FailWithStatus(WebhookOperationStatus.CancelledByNotification, webhook); + } + + await _webhookRepository.DeleteAsync(webhook); + scope.Notifications.Publish(new WebhookDeletedNotification(webhook, eventMessages).WithStateFrom(deletingNotification)); + scope.Complete(); + + return Attempt.SucceedWithStatus(WebhookOperationStatus.Success, webhook); } /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs index 9db46330ed..61f583d8eb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs @@ -1,9 +1,12 @@ -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Webhooks; using Umbraco.Cms.Web.BackOffice.Services; using Umbraco.Cms.Web.Common.Attributes; @@ -46,19 +49,16 @@ public class WebhookController : UmbracoAuthorizedJsonController { Webhook webhook = _umbracoMapper.Map(webhookViewModel)!; - await _webhookService.UpdateAsync(webhook); - - return Ok(_webhookPresentationFactory.Create(webhook)); + Attempt result = await _webhookService.UpdateAsync(webhook); + return result.Success ? Ok(_webhookPresentationFactory.Create(webhook)) : WebhookOperationStatusResult(result.Status); } [HttpPost] public async Task Create(WebhookViewModel webhookViewModel) { Webhook webhook = _umbracoMapper.Map(webhookViewModel)!; - - await _webhookService.CreateAsync(webhook); - - return Ok(_webhookPresentationFactory.Create(webhook)); + Attempt result = await _webhookService.CreateAsync(webhook); + return result.Success ? Ok(_webhookPresentationFactory.Create(webhook)) : WebhookOperationStatusResult(result.Status); } [HttpGet] @@ -72,9 +72,8 @@ public class WebhookController : UmbracoAuthorizedJsonController [HttpDelete] public async Task Delete(Guid key) { - await _webhookService.DeleteAsync(key); - - return Ok(); + Attempt result = await _webhookService.DeleteAsync(key); + return result.Success ? Ok() : WebhookOperationStatusResult(result.Status); } [HttpGet] @@ -94,4 +93,15 @@ public class WebhookController : UmbracoAuthorizedJsonController Items = mappedLogs, }); } + + private IActionResult WebhookOperationStatusResult(WebhookOperationStatus status) => + status switch + { + WebhookOperationStatus.CancelledByNotification => ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification[] + { + new("Cancelled by notification", "The operation was cancelled by a notification", NotificationStyle.Error), + })), + WebhookOperationStatus.NotFound => NotFound("Could not find the webhook"), + _ => StatusCode(StatusCodes.Status500InternalServerError), + }; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs index 4290b7fb06..53368593c1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs @@ -22,7 +22,7 @@ public class WebhookServiceTests : UmbracoIntegrationTest public async Task Can_Create_And_Get(string url, string webhookEvent, Guid key) { var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, true, new[] { key }, new[] { webhookEvent })); - var webhook = await WebhookService.GetAsync(createdWebhook.Key); + var webhook = await WebhookService.GetAsync(createdWebhook.Result.Key); Assert.Multiple(() => { @@ -45,9 +45,9 @@ public class WebhookServiceTests : UmbracoIntegrationTest Assert.Multiple(() => { Assert.IsNotEmpty(webhooks.Items); - Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookOne.Key)); - Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookTwo.Key)); - Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookThree.Key)); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookOne.Result.Key)); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookTwo.Result.Key)); + Assert.IsNotNull(webhooks.Items.FirstOrDefault(x => x.Key == createdWebhookThree.Result.Key)); }); } @@ -60,11 +60,11 @@ public class WebhookServiceTests : UmbracoIntegrationTest public async Task Can_Delete(string url, string webhookEvent, Guid key) { var createdWebhook = await WebhookService.CreateAsync(new Webhook(url, true, new[] { key }, new[] { webhookEvent })); - var webhook = await WebhookService.GetAsync(createdWebhook.Key); + var webhook = await WebhookService.GetAsync(createdWebhook.Result.Key); Assert.IsNotNull(webhook); await WebhookService.DeleteAsync(webhook.Key); - var deletedWebhook = await WebhookService.GetAsync(createdWebhook.Key); + var deletedWebhook = await WebhookService.GetAsync(createdWebhook.Result.Key); Assert.IsNull(deletedWebhook); } @@ -72,7 +72,7 @@ public class WebhookServiceTests : UmbracoIntegrationTest public async Task Can_Create_With_No_EntityKeys() { var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.Aliases.ContentPublish })); - var webhook = await WebhookService.GetAsync(createdWebhook.Key); + var webhook = await WebhookService.GetAsync(createdWebhook.Result.Key); Assert.IsNotNull(webhook); Assert.IsEmpty(webhook.ContentTypeKeys); @@ -82,10 +82,10 @@ public class WebhookServiceTests : UmbracoIntegrationTest public async Task Can_Update() { var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.Aliases.ContentPublish })); - createdWebhook.Events = new[] { Constants.WebhookEvents.Aliases.ContentDelete }; - await WebhookService.UpdateAsync(createdWebhook); + createdWebhook.Result.Events = new[] { Constants.WebhookEvents.Aliases.ContentDelete }; + await WebhookService.UpdateAsync(createdWebhook.Result); - var updatedWebhook = await WebhookService.GetAsync(createdWebhook.Key); + var updatedWebhook = await WebhookService.GetAsync(createdWebhook.Result.Key); Assert.IsNotNull(updatedWebhook); Assert.AreEqual(1, updatedWebhook.Events.Length); Assert.IsTrue(updatedWebhook.Events.Contains(Constants.WebhookEvents.Aliases.ContentDelete));