Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use controllers in the server for easier testing #807

Merged
merged 6 commits into from
Sep 19, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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