Skip to content

Commit 4649c65

Browse files
committed
Fixed some issues around creating webhooks and improved webhook validation that were introduced with the .NET 8 upgrade
1 parent 41285f0 commit 4649c65

File tree

14 files changed

+169
-50
lines changed

14 files changed

+169
-50
lines changed

src/Exceptionless.Core/Models/WebHook.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,20 @@ public static class KnownVersions
2424
public const string Version1 = "v1";
2525
public const string Version2 = "v2";
2626
}
27+
28+
public static readonly string[] AllKnownEventTypes = new[]
29+
{
30+
KnownEventTypes.NewError, KnownEventTypes.CriticalError, KnownEventTypes.NewEvent,
31+
KnownEventTypes.CriticalEvent, KnownEventTypes.StackRegression, KnownEventTypes.StackPromoted
32+
};
33+
34+
public static class KnownEventTypes
35+
{
36+
public const string NewError = "NewError";
37+
public const string CriticalError = "CriticalError";
38+
public const string NewEvent = "NewEvent";
39+
public const string CriticalEvent = "CriticalEvent";
40+
public const string StackRegression = "StackRegression";
41+
public const string StackPromoted = "StackPromoted";
42+
}
2743
}

src/Exceptionless.Core/Pipeline/070_QueueNotificationAction.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,19 @@ private bool ShouldCallWebHook(WebHook hook, EventContext ctx)
8080
if (!String.IsNullOrEmpty(hook.ProjectId) && !String.Equals(ctx.Project.Id, hook.ProjectId))
8181
return false;
8282

83-
if (ctx.IsNew && ctx.Event.IsError() && hook.EventTypes.Contains(WebHookRepository.EventTypes.NewError))
83+
if (ctx.IsNew && ctx.Event.IsError() && hook.EventTypes.Contains(WebHook.KnownEventTypes.NewError))
8484
return true;
8585

86-
if (ctx.Event.IsCritical() && ctx.Event.IsError() && hook.EventTypes.Contains(WebHookRepository.EventTypes.CriticalError))
86+
if (ctx.Event.IsCritical() && ctx.Event.IsError() && hook.EventTypes.Contains(WebHook.KnownEventTypes.CriticalError))
8787
return true;
8888

89-
if (ctx.IsRegression && hook.EventTypes.Contains(WebHookRepository.EventTypes.StackRegression))
89+
if (ctx.IsRegression && hook.EventTypes.Contains(WebHook.KnownEventTypes.StackRegression))
9090
return true;
9191

92-
if (ctx.IsNew && hook.EventTypes.Contains(WebHookRepository.EventTypes.NewEvent))
92+
if (ctx.IsNew && hook.EventTypes.Contains(WebHook.KnownEventTypes.NewEvent))
9393
return true;
9494

95-
if (ctx.Event.IsCritical() && hook.EventTypes.Contains(WebHookRepository.EventTypes.CriticalEvent))
95+
if (ctx.Event.IsCritical() && hook.EventTypes.Contains(WebHook.KnownEventTypes.CriticalEvent))
9696
return true;
9797

9898
return false;

src/Exceptionless.Core/Repositories/WebHookRepository.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,6 @@ public async Task MarkDisabledAsync(string id)
4343
await SaveAsync(webHook, o => o.Cache()).AnyContext();
4444
}
4545

46-
public static class EventTypes
47-
{
48-
// TODO: Add support for these new web hook types.
49-
public const string NewError = "NewError";
50-
public const string CriticalError = "CriticalError";
51-
public const string NewEvent = "NewEvent";
52-
public const string CriticalEvent = "CriticalEvent";
53-
public const string StackRegression = "StackRegression";
54-
public const string StackPromoted = "StackPromoted";
55-
}
5646

5747
protected override async Task InvalidateCacheAsync(IReadOnlyCollection<ModifiedDocument<WebHook>> documents, ChangeType? changeType = null)
5848
{

src/Exceptionless.Core/Validation/WebHookValidator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public WebHookValidator()
1212
RuleFor(w => w.ProjectId).IsObjectId().When(p => String.IsNullOrEmpty(p.OrganizationId)).WithMessage("Please specify a valid project id.");
1313
RuleFor(w => w.Url).NotEmpty().WithMessage("Please specify a valid url.");
1414
RuleFor(w => w.EventTypes).NotEmpty().WithMessage("Please specify one or more event types.");
15+
RuleForEach(w => w.EventTypes).Must(et => WebHook.AllKnownEventTypes.Contains(et)).WithMessage("Please specify a valid event type.");
1516
RuleFor(w => w.Version).NotEmpty().WithMessage("Please specify a valid version.");
1617
}
1718
}

src/Exceptionless.Web/ClientApp.angular/app/project/manage/manage-controller.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
return dialogs
8585
.create("components/web-hook/add-web-hook-dialog.tpl.html", "AddWebHookDialog as vm")
8686
.result.then(function (data) {
87+
data.organization_id = vm.project.organization_id;
8788
data.project_id = vm._projectId;
8889
return createWebHook(data);
8990
})

src/Exceptionless.Web/Controllers/StackController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ public async Task<IActionResult> PromoteAsync(string id)
392392
if (!await _billingManager.HasPremiumFeaturesAsync(stack.OrganizationId))
393393
return PlanLimitReached("Promote to External is a premium feature used to promote an error stack to an external system. Please upgrade your plan to enable this feature.");
394394

395-
var promotedProjectHooks = (await _webHookRepository.GetByProjectIdAsync(stack.ProjectId)).Documents.Where(p => p.EventTypes.Contains(WebHookRepository.EventTypes.StackPromoted)).ToList();
395+
var promotedProjectHooks = (await _webHookRepository.GetByProjectIdAsync(stack.ProjectId)).Documents.Where(p => p.EventTypes.Contains(WebHook.KnownEventTypes.StackPromoted)).ToList();
396396
if (!promotedProjectHooks.Any())
397397
return NotImplemented("No promoted web hooks are configured for this project. Please add a promoted web hook to use this feature.");
398398

src/Exceptionless.Web/Controllers/WebHookController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Exceptionless.App.Controllers.API;
1717

1818
[Route(API_PREFIX + "/webhooks")]
1919
[Authorize(Policy = AuthorizationRoles.ClientPolicy)]
20-
public class WebHookController : RepositoryApiController<IWebHookRepository, WebHook, WebHook, NewWebHook, UpdateWebHook>
20+
public class WebHookController : RepositoryApiController<IWebHookRepository, WebHook, WebHook, NewWebHook, WebHook>
2121
{
2222
private readonly IProjectRepository _projectRepository;
2323
private readonly BillingManager _billingManager;

src/Exceptionless.Web/Models/WebHook/NewWebHook.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ public record NewWebHook : IOwnedByOrganizationAndProject
1212
/// <summary>
1313
/// The schema version that should be used.
1414
/// </summary>
15-
public Version Version { get; set; } = null!;
15+
public Version? Version { get; set; }
1616
}

src/Exceptionless.Web/Models/WebHook/UpdateWebHook.cs

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/Exceptionless.Web/Utility/AutoValidationActionFilter.cs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Dynamic;
12
using System.IO.Pipelines;
23
using System.Security.Claims;
34
using Exceptionless.Core.Extensions;
@@ -32,7 +33,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE
3233
continue;
3334

3435
// We don't support validating JSON Types
35-
if (subject is Newtonsoft.Json.Linq.JToken)
36+
if (subject is Newtonsoft.Json.Linq.JToken or DynamicObject)
3637
continue;
3738

3839
(bool isValid, var errors) = await MiniValidator.TryValidateAsync(subject, _serviceProvider, recurse: true);
@@ -55,28 +56,28 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE
5556

5657
if (hasErrors)
5758
{
58-
var validationProblem = controllerBase.ProblemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState, 422);
59-
context.Result = new UnprocessableEntityObjectResult(validationProblem);
59+
var validationProblem = controllerBase.ProblemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState, 422);
60+
context.Result = new UnprocessableEntityObjectResult(validationProblem);
6061

61-
return;
62-
}
62+
return;
6363
}
64-
65-
await next();
6664
}
6765

68-
private static bool ShouldValidate(Type type, IServiceProviderIsService? isService = null) =>
69-
!IsNonValidatedType(type, isService) && MiniValidator.RequiresValidation(type);
66+
await next();
67+
}
68+
69+
private static bool ShouldValidate(Type type, IServiceProviderIsService? isService = null) =>
70+
!IsNonValidatedType(type, isService) && MiniValidator.RequiresValidation(type);
7071

71-
private static bool IsNonValidatedType(Type type, IServiceProviderIsService? isService) =>
72-
typeof(HttpContext) == type
73-
|| typeof(HttpRequest) == type
74-
|| typeof(HttpResponse) == type
75-
|| typeof(ClaimsPrincipal) == type
76-
|| typeof(CancellationToken) == type
77-
|| typeof(IFormFileCollection) == type
78-
|| typeof(IFormFile) == type
79-
|| typeof(Stream) == type
80-
|| typeof(PipeReader) == type
81-
|| isService?.IsService(type) == true;
72+
private static bool IsNonValidatedType(Type type, IServiceProviderIsService? isService) =>
73+
typeof(HttpContext) == type
74+
|| typeof(HttpRequest) == type
75+
|| typeof(HttpResponse) == type
76+
|| typeof(ClaimsPrincipal) == type
77+
|| typeof(CancellationToken) == type
78+
|| typeof(IFormFileCollection) == type
79+
|| typeof(IFormFile) == type
80+
|| typeof(Stream) == type
81+
|| typeof(PipeReader) == type
82+
|| isService?.IsService(type) == true;
8283
}

tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,66 @@ protected override async Task ResetDataAsync()
2424
await service.CreateDataAsync();
2525
}
2626

27+
[Fact]
28+
public async Task CanUpdateProject()
29+
{
30+
var project = await SendRequestAsAsync<ViewProject>(r => r
31+
.AsTestOrganizationUser()
32+
.Post()
33+
.AppendPath("projects")
34+
.Content(new NewProject
35+
{
36+
OrganizationId = SampleDataService.TEST_ORG_ID,
37+
Name = "Test Project",
38+
DeleteBotDataEnabled = true
39+
})
40+
.StatusCodeShouldBeCreated()
41+
);
42+
43+
var updatedProject = await SendRequestAsAsync<ViewProject>(r => r
44+
.AsTestOrganizationUser()
45+
.Patch()
46+
.AppendPath("projects", project.Id)
47+
.Content(new UpdateProject
48+
{
49+
Name = "Test Project 2",
50+
DeleteBotDataEnabled = true
51+
})
52+
.StatusCodeShouldBeOk()
53+
);
54+
55+
Assert.NotEqual(project.Name, updatedProject.Name);
56+
}
57+
58+
59+
[Fact]
60+
public async Task CanUpdateProjectWithExtraPayloadProperties()
61+
{
62+
var project = await SendRequestAsAsync<ViewProject>(r => r
63+
.AsTestOrganizationUser()
64+
.Post()
65+
.AppendPath("projects")
66+
.Content(new NewProject
67+
{
68+
OrganizationId = SampleDataService.TEST_ORG_ID,
69+
Name = "Test Project",
70+
DeleteBotDataEnabled = true
71+
})
72+
.StatusCodeShouldBeCreated()
73+
);
74+
75+
project.Name = "Updated";
76+
var updatedProject = await SendRequestAsAsync<ViewProject>(r => r
77+
.AsTestOrganizationUser()
78+
.Patch()
79+
.AppendPath("projects", project.Id)
80+
.Content(project)
81+
.StatusCodeShouldBeOk()
82+
);
83+
84+
Assert.Equal("Updated", updatedProject.Name);
85+
}
86+
2787
[Fact]
2888
public async Task CanGetProjectConfiguration()
2989
{
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Exceptionless.Core.Models;
2+
using Exceptionless.Core.Utility;
3+
using Exceptionless.Tests.Extensions;
4+
using Exceptionless.Web.Models;
5+
using Xunit;
6+
using Xunit.Abstractions;
7+
8+
namespace Exceptionless.Tests.Controllers;
9+
10+
public sealed class WebHookControllerTests : IntegrationTestsBase
11+
{
12+
public WebHookControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) { }
13+
14+
protected override async Task ResetDataAsync()
15+
{
16+
await base.ResetDataAsync();
17+
var service = GetService<SampleDataService>();
18+
await service.CreateDataAsync();
19+
}
20+
21+
[Fact]
22+
public Task CanCreateNewWebHook()
23+
{
24+
return SendRequestAsync(r => r
25+
.Post()
26+
.AsTestOrganizationUser()
27+
.AppendPath("webhooks")
28+
.Content(new NewWebHook
29+
{
30+
EventTypes = new[] { WebHook.KnownEventTypes.StackPromoted },
31+
OrganizationId = SampleDataService.TEST_ORG_ID,
32+
ProjectId = SampleDataService.TEST_PROJECT_ID,
33+
Url = "https://localhost/test"
34+
})
35+
.StatusCodeShouldBeCreated()
36+
);
37+
}
38+
39+
[Fact]
40+
public Task CreateNewWebHookWithInvalidEventTypeFails()
41+
{
42+
return SendRequestAsync(r => r
43+
.Post()
44+
.AsTestOrganizationUser()
45+
.AppendPath("webhooks")
46+
.Content(new NewWebHook
47+
{
48+
EventTypes = new[] { "Invalid" },
49+
OrganizationId = SampleDataService.TEST_ORG_ID,
50+
ProjectId = SampleDataService.TEST_PROJECT_ID,
51+
Url = "https://localhost/test"
52+
})
53+
.StatusCodeShouldBeBadRequest()
54+
);
55+
}
56+
}

tests/Exceptionless.Tests/Plugins/WebHookDataTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private WebHookDataContext GetWebHookDataContext(string version)
8282
OrganizationId = TestConstants.OrganizationId,
8383
ProjectId = TestConstants.ProjectId,
8484
Url = "http://localhost:40000/test",
85-
EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted },
85+
EventTypes = new[] { WebHook.KnownEventTypes.StackPromoted },
8686
Version = version,
8787
CreatedUtc = SystemClock.UtcNow
8888
};

tests/Exceptionless.Tests/Repositories/WebHookRepositoryTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ public WebHookRepositoryTests(ITestOutputHelper output, AppWebHostFactory factor
1818
[Fact]
1919
public async Task GetByOrganizationIdOrProjectIdAsync()
2020
{
21-
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, Url = "http://localhost:40000/test", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 });
22-
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 });
23-
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 });
21+
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, Url = "http://localhost:40000/test", EventTypes = new[] { WebHook.KnownEventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 });
22+
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHook.KnownEventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 });
23+
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHook.KnownEventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 });
2424

2525
await RefreshDataAsync();
2626
Assert.Equal(3, (await _repository.GetByOrganizationIdAsync(TestConstants.OrganizationId)).Total);
@@ -32,8 +32,8 @@ public async Task GetByOrganizationIdOrProjectIdAsync()
3232
[Fact]
3333
public async Task CanSaveWebHookVersionAsync()
3434
{
35-
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, Url = "http://localhost:40000/test", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version1 });
36-
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHookRepository.EventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 });
35+
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectId, Url = "http://localhost:40000/test", EventTypes = new[] { WebHook.KnownEventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version1 });
36+
await _repository.AddAsync(new WebHook { OrganizationId = TestConstants.OrganizationId, ProjectId = TestConstants.ProjectIdWithNoRoles, Url = "http://localhost:40000/test1", EventTypes = new[] { WebHook.KnownEventTypes.StackPromoted }, Version = WebHook.KnownVersions.Version2 });
3737

3838
await RefreshDataAsync();
3939
Assert.Equal(WebHook.KnownVersions.Version1, (await _repository.GetByProjectIdAsync(TestConstants.ProjectId)).Documents.First().Version);

0 commit comments

Comments
 (0)