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:
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Audit;
|
||||
|
||||
public class AuditLogQueryService(MicCheckDbContext db)
|
||||
public class AuditLogQueryService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<(int Total, IReadOnlyList<AuditLogResponse> Items)> ListByOrganizationAsync(
|
||||
int organizationId, AuditLogFilter filter, CancellationToken ct = default)
|
||||
|
||||
@@ -4,7 +4,7 @@ using MicCheck.Api.Webhooks;
|
||||
|
||||
namespace MicCheck.Api.Audit;
|
||||
|
||||
public class AuditService(MicCheckDbContext db, IHttpContextAccessor httpContextAccessor, WebhookQueue webhookQueue)
|
||||
public class AuditService(IMicCheckDbContext db, IHttpContextAccessor httpContextAccessor, WebhookQueue webhookQueue)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Common.Security.ApiKeys;
|
||||
|
||||
public class ApiKeyService(MicCheckDbContext db)
|
||||
public class ApiKeyService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<(ApiKey Key, string RawKey)> CreateAsync(
|
||||
int organizationId, string name, DateTimeOffset? expiresAt, CancellationToken ct = default)
|
||||
|
||||
@@ -8,7 +8,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MicCheck.Api.Common.Security.Authentication;
|
||||
|
||||
public class ApiKeyAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, MicCheckDbContext db)
|
||||
public class ApiKeyAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, IMicCheckDbContext db)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
{
|
||||
public const string SchemeName = "ApiKey";
|
||||
|
||||
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MicCheck.Api.Common.Security.Authentication;
|
||||
|
||||
public class EnvironmentKeyAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, MicCheckDbContext db)
|
||||
public class EnvironmentKeyAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, IMicCheckDbContext db)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
{
|
||||
public const string SchemeName = "EnvironmentKey";
|
||||
|
||||
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace MicCheck.Api.Common.Security.Authorization;
|
||||
|
||||
public class AuthService(
|
||||
MicCheckDbContext db,
|
||||
IMicCheckDbContext db,
|
||||
ITokenService tokenService,
|
||||
IPasswordHasher<User> passwordHasher)
|
||||
{
|
||||
@@ -81,7 +81,9 @@ public class AuthService(
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
await db.Entry(user).Collection(u => u.Organizations).LoadAsync(ct);
|
||||
var organizationUsers = await db.OrganizationUsers.Where(ou => ou.UserId == user.Id).ToListAsync(ct);
|
||||
foreach (var organizationUser in organizationUsers)
|
||||
user.Organizations.Add(organizationUser);
|
||||
|
||||
var accessToken = tokenService.GenerateToken(user);
|
||||
var refreshTokenValue = GenerateSecureToken();
|
||||
|
||||
@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace MicCheck.Api.Common.Security.Authorization;
|
||||
|
||||
public class ProjectPermissionRequirementHandler(
|
||||
MicCheckDbContext db,
|
||||
IMicCheckDbContext db,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: AuthorizationHandler<ProjectPermissionRequirement>
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ using AppEnvironment = MicCheck.Api.Environments.Environment;
|
||||
|
||||
namespace MicCheck.Api.Data;
|
||||
|
||||
public class MicCheckDbContext : DbContext
|
||||
public class MicCheckDbContext : DbContext, IMicCheckDbContext
|
||||
{
|
||||
public MicCheckDbContext(DbContextOptions<MicCheckDbContext> options) : base(options) { }
|
||||
|
||||
@@ -42,3 +42,31 @@ public class MicCheckDbContext : DbContext
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
=> modelBuilder.ApplyConfigurationsFromAssembly(typeof(MicCheckDbContext).Assembly);
|
||||
}
|
||||
|
||||
public interface IMicCheckDbContext
|
||||
{
|
||||
DbSet<Organization> Organizations { get; }
|
||||
DbSet<OrganizationUser> OrganizationUsers { get; }
|
||||
DbSet<Project> Projects { get; }
|
||||
DbSet<AppEnvironment> Environments { get; }
|
||||
DbSet<Feature> Features { get; }
|
||||
DbSet<FeatureState> FeatureStates { get; }
|
||||
DbSet<FeatureSegment> FeatureSegments { get; }
|
||||
DbSet<Tag> Tags { get; }
|
||||
DbSet<Segment> Segments { get; }
|
||||
DbSet<SegmentRule> SegmentRules { get; }
|
||||
DbSet<SegmentCondition> SegmentConditions { get; }
|
||||
DbSet<Identity> Identities { get; }
|
||||
DbSet<IdentityTrait> IdentityTraits { get; }
|
||||
DbSet<AuditLog> AuditLogs { get; }
|
||||
DbSet<Webhook> Webhooks { get; }
|
||||
DbSet<WebhookDeliveryLog> WebhookDeliveryLogs { get; }
|
||||
DbSet<ApiKey> ApiKeys { get; }
|
||||
DbSet<User> Users { get; }
|
||||
DbSet<RefreshToken> RefreshTokens { get; }
|
||||
DbSet<UserProjectPermission> UserProjectPermissions { get; }
|
||||
DbSet<FeatureUsageDaily> FeatureUsageDaily { get; }
|
||||
|
||||
int SaveChanges();
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using AppEnvironment = MicCheck.Api.Environments.Environment;
|
||||
|
||||
namespace MicCheck.Api.Environments;
|
||||
|
||||
public class EnvironmentDocumentService(MicCheckDbContext db)
|
||||
public class EnvironmentDocumentService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<EnvironmentDocumentResponse?> GetAsync(int environmentId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ using AppEnvironment = MicCheck.Api.Environments.Environment;
|
||||
|
||||
namespace MicCheck.Api.Environments;
|
||||
|
||||
public class EnvironmentService(MicCheckDbContext db, AuditService auditService)
|
||||
public class EnvironmentService(IMicCheckDbContext db, AuditService auditService)
|
||||
{
|
||||
public async Task<IReadOnlyList<AppEnvironment>> ListByProjectAsync(int projectId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class FeatureEvaluationService(
|
||||
MicCheckDbContext db,
|
||||
IMicCheckDbContext db,
|
||||
SegmentEvaluator segmentEvaluator,
|
||||
FlagCache flagCache,
|
||||
FeatureUsageMetrics usageMetrics)
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class FeatureSegmentService(MicCheckDbContext db)
|
||||
public class FeatureSegmentService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<IReadOnlyList<FeatureSegmentResponse>> ListByFeatureAsync(
|
||||
int featureId, int environmentId, CancellationToken ct = default)
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class FeatureService(MicCheckDbContext db, AuditService auditService, WebhookQueue webhookQueue)
|
||||
public class FeatureService(IMicCheckDbContext db, AuditService auditService, WebhookQueue webhookQueue)
|
||||
{
|
||||
private const int MaxFeaturesPerProject = 400;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class FeatureStateService(MicCheckDbContext db, WebhookQueue webhookQueue, AuditService auditService)
|
||||
public class FeatureStateService(IMicCheckDbContext db, WebhookQueue webhookQueue, AuditService auditService)
|
||||
{
|
||||
public async Task<IReadOnlyList<FeatureState>> ListByEnvironmentAsync(int environmentId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class FeatureUsageQueryService(MicCheckDbContext db)
|
||||
public class FeatureUsageQueryService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<DashboardUsageResponse> GetDashboardUsageAsync(int environmentId, int days, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Features;
|
||||
|
||||
public class TagService(MicCheckDbContext db)
|
||||
public class TagService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<IReadOnlyList<Tag>> ListByProjectAsync(int projectId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Identities;
|
||||
|
||||
public class AdminIdentityService(MicCheckDbContext db)
|
||||
public class AdminIdentityService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<(int Total, IReadOnlyList<Identity> Items)> ListAsync(
|
||||
int environmentId, int page, int pageSize, CancellationToken ct = default)
|
||||
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Identities;
|
||||
|
||||
public class IdentityResolutionService(MicCheckDbContext db)
|
||||
public class IdentityResolutionService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<Identity> ResolveAsync(
|
||||
int environmentId,
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Organizations;
|
||||
|
||||
public class OrganizationService(MicCheckDbContext db, AuditService auditService)
|
||||
public class OrganizationService(IMicCheckDbContext db, AuditService auditService)
|
||||
{
|
||||
public async Task<IReadOnlyList<(Organization Org, bool IsPrimary)>> ListForUserAsync(int userId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -152,6 +152,7 @@ try
|
||||
|
||||
builder.Services.AddDbContext<MicCheckDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
builder.Services.AddScoped<IMicCheckDbContext>(sp => sp.GetRequiredService<MicCheckDbContext>());
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Projects;
|
||||
|
||||
public class ProjectService(MicCheckDbContext db, AuditService auditService)
|
||||
public class ProjectService(IMicCheckDbContext db, AuditService auditService)
|
||||
{
|
||||
public async Task<IReadOnlyList<Project>> ListByOrganizationAsync(int organizationId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ public record SegmentConditionDefinition(string Property, SegmentConditionOperat
|
||||
|
||||
public record SegmentRuleDefinition(SegmentRuleType Type, IReadOnlyList<SegmentConditionDefinition> Conditions, IReadOnlyList<SegmentRuleDefinition>? ChildRules = null);
|
||||
|
||||
public class SegmentService(MicCheckDbContext db, AuditService auditService)
|
||||
public class SegmentService(IMicCheckDbContext db, AuditService auditService)
|
||||
{
|
||||
private const int MaxSegmentsPerProject = 100;
|
||||
private const int MaxConditionsPerSegment = 100;
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Users;
|
||||
|
||||
public class UserService(MicCheckDbContext db, IPasswordHasher<User> passwordHasher)
|
||||
public class UserService(IMicCheckDbContext db, IPasswordHasher<User> passwordHasher)
|
||||
{
|
||||
public async Task<User?> FindByIdAsync(int userId, CancellationToken ct = default) =>
|
||||
await db.Users.FirstOrDefaultAsync(u => u.Id == userId && u.IsActive, ct);
|
||||
|
||||
@@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Webhooks;
|
||||
|
||||
public class WebhookDispatcher(MicCheckDbContext db, IHttpClientFactory httpClientFactory, ILogger<WebhookDispatcher> logger)
|
||||
public class WebhookDispatcher(IMicCheckDbContext db, IHttpClientFactory httpClientFactory, ILogger<WebhookDispatcher> logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MicCheck.Api.Webhooks;
|
||||
|
||||
public class WebhookService(MicCheckDbContext db)
|
||||
public class WebhookService(IMicCheckDbContext db)
|
||||
{
|
||||
public async Task<IReadOnlyList<Webhook>> ListByEnvironmentAsync(int environmentId, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user