Refactor nunit tests off EF InMemory provider and web server
Extract IMicCheckDbContext so DB access can be mocked with Moq instead of the InMemory provider, per project testing guidelines. Add shared async DbSet test doubles (TestSupport/) to back the mocks. Rewrite HTTP-pipeline tests (WebApplicationFactory) to exercise controllers and auth handlers directly instead of running the web server. Drop DbContext/seeder tests that only made sense against a real provider, and remove stale duplicate test files left over from a namespace move.
This commit is contained in:
@@ -2,8 +2,8 @@ using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using MicCheck.Api.Webhooks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
@@ -52,28 +52,24 @@ public class WebhookDispatcherTests
|
||||
[Test]
|
||||
public async Task WhenDispatchingToActiveWebhook_ThenPayloadIsPostedWithSignatureHeader()
|
||||
{
|
||||
var (db, factory, capturedRequests) = SetUpDispatcher(HttpStatusCode.OK);
|
||||
var (db, webhooks, _, factory, capturedRequests) = SetUpDispatcher(HttpStatusCode.OK);
|
||||
|
||||
var org = new MicCheck.Api.Organizations.Organization { Name = "Org", CreatedAt = DateTimeOffset.UtcNow };
|
||||
db.Organizations.Add(org);
|
||||
db.SaveChanges();
|
||||
|
||||
db.Webhooks.Add(new Webhook
|
||||
const int orgId = 1;
|
||||
webhooks.Add(new Webhook
|
||||
{
|
||||
Url = "https://example.com/hook",
|
||||
Secret = "test-secret",
|
||||
Scope = WebhookScope.Organization,
|
||||
OrganizationId = org.Id,
|
||||
OrganizationId = orgId,
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
db.SaveChanges();
|
||||
|
||||
var dispatcher = new WebhookDispatcher(db, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
var dispatcher = new WebhookDispatcher(db.Object, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
var webhookEvent = new WebhookEvent
|
||||
{
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
OrganizationId = org.Id,
|
||||
OrganizationId = orgId,
|
||||
Data = new { test = true }
|
||||
};
|
||||
|
||||
@@ -86,28 +82,24 @@ public class WebhookDispatcherTests
|
||||
[Test]
|
||||
public async Task WhenDispatchingToWebhookWithoutSecret_ThenNoSignatureHeaderIsAdded()
|
||||
{
|
||||
var (db, factory, capturedRequests) = SetUpDispatcher(HttpStatusCode.OK);
|
||||
var (db, webhooks, _, factory, capturedRequests) = SetUpDispatcher(HttpStatusCode.OK);
|
||||
|
||||
var org = new MicCheck.Api.Organizations.Organization { Name = "Org", CreatedAt = DateTimeOffset.UtcNow };
|
||||
db.Organizations.Add(org);
|
||||
db.SaveChanges();
|
||||
|
||||
db.Webhooks.Add(new Webhook
|
||||
const int orgId = 1;
|
||||
webhooks.Add(new Webhook
|
||||
{
|
||||
Url = "https://example.com/hook",
|
||||
Secret = null,
|
||||
Scope = WebhookScope.Organization,
|
||||
OrganizationId = org.Id,
|
||||
OrganizationId = orgId,
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
db.SaveChanges();
|
||||
|
||||
var dispatcher = new WebhookDispatcher(db, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
var dispatcher = new WebhookDispatcher(db.Object, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
await dispatcher.DispatchAsync(new WebhookEvent
|
||||
{
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
OrganizationId = org.Id,
|
||||
OrganizationId = orgId,
|
||||
Data = new { }
|
||||
});
|
||||
|
||||
@@ -117,31 +109,27 @@ public class WebhookDispatcherTests
|
||||
[Test]
|
||||
public async Task WhenDeliverySucceeds_ThenDeliveryLogIsRecordedAsSuccess()
|
||||
{
|
||||
var (db, factory, _) = SetUpDispatcher(HttpStatusCode.OK);
|
||||
var (db, webhooks, deliveryLogs, factory, _) = SetUpDispatcher(HttpStatusCode.OK);
|
||||
|
||||
var org = new MicCheck.Api.Organizations.Organization { Name = "Org", CreatedAt = DateTimeOffset.UtcNow };
|
||||
db.Organizations.Add(org);
|
||||
db.SaveChanges();
|
||||
|
||||
db.Webhooks.Add(new Webhook
|
||||
const int orgId = 1;
|
||||
webhooks.Add(new Webhook
|
||||
{
|
||||
Url = "https://example.com/hook",
|
||||
Scope = WebhookScope.Organization,
|
||||
OrganizationId = org.Id,
|
||||
OrganizationId = orgId,
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
db.SaveChanges();
|
||||
|
||||
var dispatcher = new WebhookDispatcher(db, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
var dispatcher = new WebhookDispatcher(db.Object, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
await dispatcher.DispatchAsync(new WebhookEvent
|
||||
{
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
OrganizationId = org.Id,
|
||||
OrganizationId = orgId,
|
||||
Data = new { }
|
||||
});
|
||||
|
||||
var log = db.WebhookDeliveryLogs.First();
|
||||
var log = deliveryLogs.First();
|
||||
Assert.That(log.Success, Is.True);
|
||||
Assert.That(log.ResponseStatusCode, Is.EqualTo(200));
|
||||
Assert.That(log.AttemptNumber, Is.EqualTo(1));
|
||||
@@ -150,31 +138,27 @@ public class WebhookDispatcherTests
|
||||
[Test]
|
||||
public async Task WhenDeliveryFails_ThenDeliveryLogIsRecordedAsFailure()
|
||||
{
|
||||
var (db, factory, _) = SetUpDispatcher(HttpStatusCode.InternalServerError);
|
||||
var (db, webhooks, deliveryLogs, factory, _) = SetUpDispatcher(HttpStatusCode.InternalServerError);
|
||||
|
||||
var org = new MicCheck.Api.Organizations.Organization { Name = "Org", CreatedAt = DateTimeOffset.UtcNow };
|
||||
db.Organizations.Add(org);
|
||||
db.SaveChanges();
|
||||
|
||||
db.Webhooks.Add(new Webhook
|
||||
const int orgId = 1;
|
||||
webhooks.Add(new Webhook
|
||||
{
|
||||
Url = "https://example.com/hook",
|
||||
Scope = WebhookScope.Organization,
|
||||
OrganizationId = org.Id,
|
||||
OrganizationId = orgId,
|
||||
Enabled = true,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
db.SaveChanges();
|
||||
|
||||
var dispatcher = new WebhookDispatcher(db, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
var dispatcher = new WebhookDispatcher(db.Object, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
await dispatcher.DispatchAsync(new WebhookEvent
|
||||
{
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
OrganizationId = org.Id,
|
||||
OrganizationId = orgId,
|
||||
Data = new { }
|
||||
});
|
||||
|
||||
var log = db.WebhookDeliveryLogs.First();
|
||||
var log = deliveryLogs.First();
|
||||
Assert.That(log.Success, Is.False);
|
||||
Assert.That(log.ResponseStatusCode, Is.EqualTo(500));
|
||||
}
|
||||
@@ -182,9 +166,9 @@ public class WebhookDispatcherTests
|
||||
[Test]
|
||||
public async Task WhenNoWebhooksAreRegistered_ThenNoDeliveryLogsAreCreated()
|
||||
{
|
||||
var (db, factory, _) = SetUpDispatcher(HttpStatusCode.OK);
|
||||
var (db, _, deliveryLogs, factory, _) = SetUpDispatcher(HttpStatusCode.OK);
|
||||
|
||||
var dispatcher = new WebhookDispatcher(db, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
var dispatcher = new WebhookDispatcher(db.Object, factory, NullLogger<WebhookDispatcher>.Instance);
|
||||
await dispatcher.DispatchAsync(new WebhookEvent
|
||||
{
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
@@ -192,16 +176,17 @@ public class WebhookDispatcherTests
|
||||
Data = new { }
|
||||
});
|
||||
|
||||
Assert.That(db.WebhookDeliveryLogs.Count(), Is.EqualTo(0));
|
||||
Assert.That(deliveryLogs, Is.Empty);
|
||||
}
|
||||
|
||||
private static (MicCheckDbContext Db, IHttpClientFactory Factory, List<HttpRequestMessage> CapturedRequests)
|
||||
private static (Mock<IMicCheckDbContext> Db, List<Webhook> Webhooks, List<WebhookDeliveryLog> DeliveryLogs, IHttpClientFactory Factory, List<HttpRequestMessage> CapturedRequests)
|
||||
SetUpDispatcher(HttpStatusCode responseStatus)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
var db = new MicCheckDbContext(options);
|
||||
var db = new Mock<IMicCheckDbContext>();
|
||||
var webhooks = new List<Webhook>();
|
||||
db.SetupDbSetWithGeneratedIds(c => c.Webhooks, webhooks);
|
||||
var deliveryLogs = new List<WebhookDeliveryLog>();
|
||||
db.SetupDbSet(c => c.WebhookDeliveryLogs, deliveryLogs);
|
||||
|
||||
var capturedRequests = new List<HttpRequestMessage>();
|
||||
var handler = new Mock<HttpMessageHandler>();
|
||||
@@ -222,6 +207,6 @@ public class WebhookDispatcherTests
|
||||
var factory = new Mock<IHttpClientFactory>();
|
||||
factory.Setup(f => f.CreateClient(It.IsAny<string>())).Returns(httpClient);
|
||||
|
||||
return (db, factory.Object, capturedRequests);
|
||||
return (db, webhooks, deliveryLogs, factory.Object, capturedRequests);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using MicCheck.Api.Data;
|
||||
using MicCheck.Api.Tests.Unit.TestSupport;
|
||||
using MicCheck.Api.Webhooks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MicCheck.Api.Tests.Unit.Webhooks;
|
||||
@@ -24,28 +26,33 @@ public class WebhookRetryTests
|
||||
Assert.That(delay.TotalMinutes, Is.EqualTo(30));
|
||||
}
|
||||
|
||||
private static Mock<IMicCheckDbContext> CreateDb(List<WebhookDeliveryLog> deliveryLogs)
|
||||
{
|
||||
var db = new Mock<IMicCheckDbContext>();
|
||||
db.SetupDbSet(c => c.WebhookDeliveryLogs, deliveryLogs);
|
||||
return db;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhenFailedDeliveryIsOldEnough_ThenItIsEligibleForRetry()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
using var db = new MicCheckDbContext(options);
|
||||
|
||||
db.WebhookDeliveryLogs.Add(new WebhookDeliveryLog
|
||||
var deliveryLogs = new List<WebhookDeliveryLog>
|
||||
{
|
||||
WebhookId = 1,
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
PayloadJson = """{"event_type":"FLAG_UPDATED","data":{}}""",
|
||||
Success = false,
|
||||
AttemptNumber = 1,
|
||||
AttemptedAt = DateTimeOffset.UtcNow.AddMinutes(-6),
|
||||
Duration = TimeSpan.FromMilliseconds(100)
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
new()
|
||||
{
|
||||
WebhookId = 1,
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
PayloadJson = """{"event_type":"FLAG_UPDATED","data":{}}""",
|
||||
Success = false,
|
||||
AttemptNumber = 1,
|
||||
AttemptedAt = DateTimeOffset.UtcNow.AddMinutes(-6),
|
||||
Duration = TimeSpan.FromMilliseconds(100)
|
||||
}
|
||||
};
|
||||
var db = CreateDb(deliveryLogs);
|
||||
|
||||
var retryAfter = DateTimeOffset.UtcNow - TimeSpan.FromMinutes(5);
|
||||
var eligible = await db.WebhookDeliveryLogs
|
||||
var eligible = await db.Object.WebhookDeliveryLogs
|
||||
.Where(d => !d.Success && d.AttemptNumber == 1 && d.AttemptedAt <= retryAfter)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -55,25 +62,23 @@ public class WebhookRetryTests
|
||||
[Test]
|
||||
public async Task WhenFailedDeliveryIsTooRecent_ThenItIsNotEligibleForRetry()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
using var db = new MicCheckDbContext(options);
|
||||
|
||||
db.WebhookDeliveryLogs.Add(new WebhookDeliveryLog
|
||||
var deliveryLogs = new List<WebhookDeliveryLog>
|
||||
{
|
||||
WebhookId = 1,
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
PayloadJson = """{"event_type":"FLAG_UPDATED","data":{}}""",
|
||||
Success = false,
|
||||
AttemptNumber = 1,
|
||||
AttemptedAt = DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||
Duration = TimeSpan.FromMilliseconds(100)
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
new()
|
||||
{
|
||||
WebhookId = 1,
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
PayloadJson = """{"event_type":"FLAG_UPDATED","data":{}}""",
|
||||
Success = false,
|
||||
AttemptNumber = 1,
|
||||
AttemptedAt = DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||
Duration = TimeSpan.FromMilliseconds(100)
|
||||
}
|
||||
};
|
||||
var db = CreateDb(deliveryLogs);
|
||||
|
||||
var retryAfter = DateTimeOffset.UtcNow - TimeSpan.FromMinutes(5);
|
||||
var eligible = await db.WebhookDeliveryLogs
|
||||
var eligible = await db.Object.WebhookDeliveryLogs
|
||||
.Where(d => !d.Success && d.AttemptNumber == 1 && d.AttemptedAt <= retryAfter)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -83,37 +88,34 @@ public class WebhookRetryTests
|
||||
[Test]
|
||||
public async Task WhenDeliveryHasAlreadyBeenRetried_ThenItIsNotRetriedAgain()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<MicCheckDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
using var db = new MicCheckDbContext(options);
|
||||
|
||||
var firstAttemptedAt = DateTimeOffset.UtcNow.AddMinutes(-10);
|
||||
|
||||
db.WebhookDeliveryLogs.Add(new WebhookDeliveryLog
|
||||
var deliveryLogs = new List<WebhookDeliveryLog>
|
||||
{
|
||||
WebhookId = 1,
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
PayloadJson = "{}",
|
||||
Success = false,
|
||||
AttemptNumber = 1,
|
||||
AttemptedAt = firstAttemptedAt,
|
||||
Duration = TimeSpan.Zero
|
||||
});
|
||||
new()
|
||||
{
|
||||
WebhookId = 1,
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
PayloadJson = "{}",
|
||||
Success = false,
|
||||
AttemptNumber = 1,
|
||||
AttemptedAt = firstAttemptedAt,
|
||||
Duration = TimeSpan.Zero
|
||||
},
|
||||
new()
|
||||
{
|
||||
WebhookId = 1,
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
PayloadJson = "{}",
|
||||
Success = false,
|
||||
AttemptNumber = 2,
|
||||
AttemptedAt = firstAttemptedAt.AddMinutes(5),
|
||||
Duration = TimeSpan.Zero
|
||||
}
|
||||
};
|
||||
var db = CreateDb(deliveryLogs);
|
||||
|
||||
db.WebhookDeliveryLogs.Add(new WebhookDeliveryLog
|
||||
{
|
||||
WebhookId = 1,
|
||||
EventType = WebhookEventTypes.FlagUpdated,
|
||||
PayloadJson = "{}",
|
||||
Success = false,
|
||||
AttemptNumber = 2,
|
||||
AttemptedAt = firstAttemptedAt.AddMinutes(5),
|
||||
Duration = TimeSpan.Zero
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var alreadyRetried = await db.WebhookDeliveryLogs
|
||||
var alreadyRetried = await db.Object.WebhookDeliveryLogs
|
||||
.AnyAsync(d => d.WebhookId == 1 && d.AttemptNumber == 2 && d.AttemptedAt > firstAttemptedAt);
|
||||
|
||||
Assert.That(alreadyRetried, Is.True);
|
||||
|
||||
Reference in New Issue
Block a user