Skip to content

Commit

Permalink
Use controllers in the server for easier testing (#807)
Browse files Browse the repository at this point in the history
* Reorganize models folder
* Use controller for webhooks/azure
* Use controllers for update_jobs/{id}/*
* Use controllers for management endpoint
  • Loading branch information
mburumaxwell authored Sep 19, 2023
1 parent 0b8d18d commit 9689187
Show file tree
Hide file tree
Showing 49 changed files with 794 additions and 707 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Tingle.Dependabot.Models;
using Tingle.Dependabot.Models.Dependabot;
using Xunit;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Tingle.Dependabot.Models;
using Tingle.Dependabot.Models.Dependabot;
using Xunit;

namespace Tingle.Dependabot.Tests.Models;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Logging;
using System.Net;
using System.Text;
using System.Text.Json;
using Tingle.Dependabot.Events;
using Tingle.Dependabot.Models;
using Tingle.EventBus;
Expand All @@ -17,19 +18,19 @@

namespace Tingle.Dependabot.Tests;

public class AzureDevOpsEventHandlerTests
public class WebhooksControllerIntegrationTests
{
private readonly ITestOutputHelper outputHelper;

public AzureDevOpsEventHandlerTests(ITestOutputHelper outputHelper)
public WebhooksControllerIntegrationTests(ITestOutputHelper outputHelper)
{
this.outputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper));
}

[Fact]
public async Task Returns_Unauthorized()
{
await TestAsync(async (harness, client, handler) =>
await TestAsync(async (harness, client) =>
{
// without Authorization header
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
Expand All @@ -44,31 +45,33 @@ await TestAsync(async (harness, client, handler) =>
response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Empty(await response.Content.ReadAsStringAsync());
Assert.Empty(handler.Calls);
Assert.Empty(await harness.PublishedAsync());
});
}

[Fact]
public async Task Returns_BadRequest_NoBody()
{
await TestAsync(async (harness, client, handler) =>
await TestAsync(async (harness, client) =>
{
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Content = new StringContent("", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Empty(await response.Content.ReadAsStringAsync());
Assert.Empty(handler.Calls);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("\"type\":\"https://tools.ietf.org/html/rfc7231#section-6.5.1\"", body);
Assert.Contains("\"title\":\"One or more validation errors occurred.\"", body);
Assert.Contains("\"status\":400", body);
Assert.Contains("\"errors\":{\"\":[\"A non-empty request body is required.\"],\"model\":[\"The model field is required.\"]}", body);
Assert.Empty(await harness.PublishedAsync());
});
}

[Fact]
public async Task Returns_BadRequest_MissingValues()
{
await TestAsync(async (harness, client, handler) =>
await TestAsync(async (harness, client) =>
{
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
Expand All @@ -82,32 +85,33 @@ await TestAsync(async (harness, client, handler) =>
Assert.Contains("\"SubscriptionId\":[\"The SubscriptionId field is required.\"]", body);
Assert.Contains("\"EventType\":[\"The EventType field is required.\"]", body);
Assert.Contains("\"Resource\":[\"The Resource field is required.\"]", body);
Assert.Empty(handler.Calls);
Assert.Empty(await harness.PublishedAsync());
});
}

[Fact]
public async Task Returns_UnsupportedMediaType()
{
await TestAsync(async (harness, client, handler) =>
await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsPullRequestUpdated1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes("vsts:burp-bump")));
request.Content = new StreamContent(stream);
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
Assert.Empty(await response.Content.ReadAsStringAsync());
Assert.Empty(handler.Calls);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("\"type\":\"https://tools.ietf.org/html/rfc7231#section-6.5.13\"", body);
Assert.Contains("\"title\":\"Unsupported Media Type\"", body);
Assert.Contains("\"status\":415", body);
Assert.Empty(await harness.PublishedAsync());
});
}

[Fact]
public async Task Returns_OK_CodePush()
{
await TestAsync(async (harness, client, handler) =>
await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsGitPush1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
Expand All @@ -117,10 +121,6 @@ await TestAsync(async (harness, client, handler) =>
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Empty(await response.Content.ReadAsStringAsync());
var call = Assert.Single(handler.Calls);
Assert.Equal("435e539d-3ce2-4283-8da9-8f3c0fe2e45e", call.SubscriptionId);
Assert.Equal(3, call.NotificationId);
Assert.Equal(AzureDevOpsEventType.GitPush, call.EventType);

// Ensure the message was published
var context = Assert.IsType<EventContext<ProcessSynchronization>>(Assert.Single(await harness.PublishedAsync(TimeSpan.FromSeconds(1f))));
Expand All @@ -135,7 +135,7 @@ await TestAsync(async (harness, client, handler) =>
[Fact]
public async Task Returns_OK_PullRequestUpdated()
{
await TestAsync(async (harness, client, handler) =>
await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsPullRequestUpdated1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
Expand All @@ -145,18 +145,14 @@ await TestAsync(async (harness, client, handler) =>
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Empty(await response.Content.ReadAsStringAsync());
var call = Assert.Single(handler.Calls);
Assert.Equal("435e539d-3ce2-4283-8da9-8f3c0fe2e45e", call.SubscriptionId);
Assert.Equal(3, call.NotificationId);
Assert.Equal(AzureDevOpsEventType.GitPullRequestUpdated, call.EventType);
Assert.Empty(await harness.PublishedAsync());
});
}

[Fact]
public async Task Returns_OK_PullRequestMerged()
{
await TestAsync(async (harness, client, handler) =>
await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsPullRequestMerged1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
Expand All @@ -166,18 +162,14 @@ await TestAsync(async (harness, client, handler) =>
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Empty(await response.Content.ReadAsStringAsync());
var call = Assert.Single(handler.Calls);
Assert.Equal("435e539d-3ce2-4283-8da9-8f3c0fe2e45e", call.SubscriptionId);
Assert.Equal(3, call.NotificationId);
Assert.Equal(AzureDevOpsEventType.GitPullRequestMerged, call.EventType);
Assert.Empty(await harness.PublishedAsync());
});
}

[Fact]
public async Task Returns_OK_PullRequestCommentEvent()
{
await TestAsync(async (harness, client, handler) =>
await TestAsync(async (harness, client) =>
{
var stream = TestSamples.GetAzureDevOpsPullRequestCommentEvent1();
var request = new HttpRequestMessage(HttpMethod.Post, "/webhooks/azure");
Expand All @@ -187,15 +179,11 @@ await TestAsync(async (harness, client, handler) =>
var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Empty(await response.Content.ReadAsStringAsync());
var call = Assert.Single(handler.Calls);
Assert.Equal("435e539d-3ce2-4283-8da9-8f3c0fe2e45e", call.SubscriptionId);
Assert.Equal(3, call.NotificationId);
Assert.Equal(AzureDevOpsEventType.GitPullRequestCommentEvent, call.EventType);
Assert.Empty(await harness.PublishedAsync());
});
}

private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, ModifiedAzureDevOpsEventHandler, Task> executeAndVerify)
private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, Task> executeAndVerify)
{
// Arrange
var builder = new WebHostBuilder()
Expand All @@ -209,6 +197,14 @@ private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, ModifiedAzure
})
.ConfigureServices((context, services) =>
{
services.AddControllers()
.AddApplicationPart(typeof(MainDbContext).Assembly)
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.AllowTrailingCommas = true;
options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
});

var dbName = Guid.NewGuid().ToString();
var configuration = context.Configuration;
services.AddDbContext<MainDbContext>(options =>
Expand All @@ -217,8 +213,6 @@ private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, ModifiedAzure
options.EnableDetailedErrors();
});
services.AddRouting();
services.AddNotificationsHandler();
services.AddSingleton<AzureDevOpsEventHandler, ModifiedAzureDevOpsEventHandler>();

services.AddAuthentication()
.AddBasic<BasicUserValidationService>(AuthConstants.SchemeNameServiceHooks, options => options.Realm = "Dependabot");
Expand All @@ -242,7 +236,7 @@ private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, ModifiedAzure
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapWebhooks();
endpoints.MapControllers();
});
});
using var server = new TestServer(builder);
Expand All @@ -253,16 +247,14 @@ private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, ModifiedAzure
var context = provider.GetRequiredService<MainDbContext>();
await context.Database.EnsureCreatedAsync();

var handler = Assert.IsType<ModifiedAzureDevOpsEventHandler>(provider.GetRequiredService<AzureDevOpsEventHandler>());

var harness = provider.GetRequiredService<InMemoryTestHarness>();
await harness.StartAsync();

try
{
var client = server.CreateClient();

await executeAndVerify(harness, client, handler);
await executeAndVerify(harness, client);

// Ensure there were no publish failures
Assert.Empty(await harness.FailedAsync());
Expand All @@ -272,18 +264,4 @@ private async Task TestAsync(Func<InMemoryTestHarness, HttpClient, ModifiedAzure
await harness.StopAsync();
}
}

class ModifiedAzureDevOpsEventHandler : AzureDevOpsEventHandler
{
public ModifiedAzureDevOpsEventHandler(IEventPublisher publisher, ILogger<AzureDevOpsEventHandler> logger)
: base(publisher, logger) { }

public List<AzureDevOpsEvent> Calls { get; } = new();

public override async Task HandleAsync(AzureDevOpsEvent model, CancellationToken cancellationToken)
{
Calls.Add(model);
await base.HandleAsync(model, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Tingle.Dependabot.Models;
using Tingle.Dependabot.Models.Dependabot;
using Tingle.Dependabot.Workflow;
using Xunit;
using Xunit.Abstractions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Microsoft.Extensions.Logging;
using Tingle.Dependabot.Events;
using Tingle.Dependabot.Models;
using Tingle.Dependabot.Models.Dependabot;
using Tingle.Dependabot.Models.Management;
using Tingle.Dependabot.Workflow;
using Tingle.EventBus;
using Tingle.EventBus.Transports.InMemory;
Expand Down
13 changes: 13 additions & 0 deletions server/Tingle.Dependabot/AuthConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Tingle.Dependabot;

internal static class AuthConstants
{
// These values are fixed strings due to configuration sections
internal const string SchemeNameManagement = "Management";
internal const string SchemeNameServiceHooks = "ServiceHooks";
internal const string SchemeNameUpdater = "Updater";

internal const string PolicyNameManagement = "Management";
internal const string PolicyNameServiceHooks = "ServiceHooks";
internal const string PolicyNameUpdater = "Updater";
}
Loading

0 comments on commit 9689187

Please sign in to comment.