diff --git a/backend/Api/Api.csproj b/backend/Api/Api.csproj
index 1db3e689..aa25375f 100644
--- a/backend/Api/Api.csproj
+++ b/backend/Api/Api.csproj
@@ -38,5 +38,9 @@
<_ContentIncludedByDefault Remove="Routes\obj\project.packagespec.json"/>
+
+
+
+
diff --git a/backend/Api/AppExtensions/DatabaseExtensions.cs b/backend/Api/AppExtensions/DatabaseExtensions.cs
deleted file mode 100644
index 12c07b46..00000000
--- a/backend/Api/AppExtensions/DatabaseExtensions.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using Infrastructure.DatabaseContext;
-using Microsoft.EntityFrameworkCore;
-
-namespace Api.AppExtensions;
-
-public static class DatabaseExtensions
-{
- public static WebApplication ApplyMigrations(this WebApplication app)
- {
- using var scope = app.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- db.Database.Migrate();
- return app;
- }
-}
\ No newline at end of file
diff --git a/backend/Api/Program.cs b/backend/Api/Program.cs
index 0f95775d..0cf280b7 100644
--- a/backend/Api/Program.cs
+++ b/backend/Api/Program.cs
@@ -1,24 +1,19 @@
using System.Text.Json.Serialization;
using Api.AppExtensions;
using Api.Options;
-using Infrastructure.DatabaseContext;
+using Infrastructure;
using Infrastructure.Repositories;
using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
-var connection = builder.Configuration.GetConnectionString("VibesDb");
-
-if (string.IsNullOrEmpty(connection))
- throw new InvalidOperationException("No connection string found");
+builder.AddInfrastructure();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration);
builder.Services.AddAuthorization(opt => { opt.FallbackPolicy = opt.DefaultPolicy; });
-builder.Services.AddDbContext(options => options.UseSqlServer(connection));
builder.Services.AddMemoryCache();
builder.AddRepositories();
@@ -26,7 +21,6 @@
.AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); });
builder.Services.AddEndpointsApiExplorer();
-
var adOptions = builder.Configuration.GetSection("AzureAd").Get();
if (adOptions == null) throw new InvalidOperationException("Required AzureAd options are missing");
@@ -40,8 +34,6 @@
var app = builder.Build();
-app.ApplyMigrations();
-
app.UsePathBase("/v0");
app.MapControllers();
@@ -61,4 +53,12 @@
app.UseAuthorization();
-await app.RunAsync();
\ No newline at end of file
+await app.RunAsync();
+
+namespace Api
+{
+ // ReSharper disable once PartialTypeWithSinglePart
+ // ReSharper disable once UnusedType.Global
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public partial class Program;
+}
\ No newline at end of file
diff --git a/backend/Api/appsettings.Local.json b/backend/Api/appsettings.Local.json
new file mode 100644
index 00000000..e9e7bd75
--- /dev/null
+++ b/backend/Api/appsettings.Local.json
@@ -0,0 +1,20 @@
+{
+ "AzureAd": {
+ "DisableAuthAd": false,
+ "Instance": "https://login.microsoftonline.com/",
+ "ClientId": "7ef3a24e-7093-41dc-9163-9618415137fe",
+ "TenantId": "0f16d077-bd82-4a6c-b498-52741239205f",
+ "ApiScope": "api://7ef3a24e-7093-41dc-9163-9618415137fe/Vibes.ReadWrite"
+ },
+ "ConnectionStrings": {
+ "VibesDb": "Server=localhost;Database=AzureVibesDb;User Id=sa;Password=yourStrong(!)Password;;TrustServerCertificate=true"
+ },
+ "InfrastructureConfig": {
+ "EnableSensitiveDataLogging": true
+ },
+ "TestContainersConfig": {
+ "Enabled": true,
+ "RunMigrations": true,
+ "SeedDatabase": false
+ }
+}
\ No newline at end of file
diff --git a/backend/Api/appsettings.Test.json b/backend/Api/appsettings.Test.json
new file mode 100644
index 00000000..fc4d38af
--- /dev/null
+++ b/backend/Api/appsettings.Test.json
@@ -0,0 +1,7 @@
+{
+ "TestContainersConfig": {
+ "Enabled": true,
+ "RunMigrations": true,
+ "SeedDatabase": false
+ }
+}
\ No newline at end of file
diff --git a/backend/Infrastructure/DatabaseContext/ApplicationContext.cs b/backend/Infrastructure/DatabaseContext/ApplicationContext.cs
index 471d174e..d79bbf72 100644
--- a/backend/Infrastructure/DatabaseContext/ApplicationContext.cs
+++ b/backend/Infrastructure/DatabaseContext/ApplicationContext.cs
@@ -10,10 +10,11 @@
using Core.Weeks;
using Infrastructure.ValueConverters;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Options;
namespace Infrastructure.DatabaseContext;
-public class ApplicationContext(DbContextOptions options) : DbContext(options)
+public class ApplicationContext(IOptions config) : DbContext
{
public DbSet Consultant { get; init; } = null!;
public DbSet Competence { get; init; } = null!;
@@ -28,6 +29,12 @@ public class ApplicationContext(DbContextOptions options) : DbContext(options)
public DbSet Staffing { get; init; } = null!;
public DbSet Agreements { get; init; } = null!;
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.UseSqlServer(config.Value.ConnectionString)
+ .EnableSensitiveDataLogging(config.Value
+ .EnableSensitiveDataLogging);
+ }
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
diff --git a/backend/Infrastructure/DependencyInjection.cs b/backend/Infrastructure/DependencyInjection.cs
new file mode 100644
index 00000000..7a9f75f1
--- /dev/null
+++ b/backend/Infrastructure/DependencyInjection.cs
@@ -0,0 +1,23 @@
+using Infrastructure.DatabaseContext;
+using Infrastructure.TestContainers;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Infrastructure;
+
+public static class DependencyInjection
+{
+ public static IHostApplicationBuilder AddInfrastructure(this IHostApplicationBuilder builder)
+ {
+ builder
+ .AddConfig(out _)
+ .AddConfig(out var currentTestContainersConfig);
+
+ if (currentTestContainersConfig.Enabled)
+ builder.AddTestContainers();
+
+ builder.Services.AddDbContext();
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/backend/Infrastructure/Infrastructure.csproj b/backend/Infrastructure/Infrastructure.csproj
index 257af2bb..b04187e2 100644
--- a/backend/Infrastructure/Infrastructure.csproj
+++ b/backend/Infrastructure/Infrastructure.csproj
@@ -16,7 +16,10 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/backend/Infrastructure/InfrastructureConfig.cs b/backend/Infrastructure/InfrastructureConfig.cs
new file mode 100644
index 00000000..95e23061
--- /dev/null
+++ b/backend/Infrastructure/InfrastructureConfig.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Infrastructure;
+
+public sealed record InfrastructureConfig
+{
+ public required string ConnectionString { get; set; }
+ public required bool EnableSensitiveDataLogging { get; set; }
+}
+
+internal static class ConfigExtensions
+{
+ internal static IHostApplicationBuilder AddConfig(this IHostApplicationBuilder builder, out T currentConfig)
+ where T : class
+ {
+ var name = typeof(T);
+ var configSection = builder.Configuration.GetSection(typeof(T).Name);
+ builder.Services.Configure(configSection);
+ currentConfig = configSection.Get()!;
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/backend/Infrastructure/TestContainers/TestContainersConfig.cs b/backend/Infrastructure/TestContainers/TestContainersConfig.cs
new file mode 100644
index 00000000..c122f2b9
--- /dev/null
+++ b/backend/Infrastructure/TestContainers/TestContainersConfig.cs
@@ -0,0 +1,8 @@
+namespace Infrastructure.TestContainers;
+
+public record TestContainersConfig
+{
+ public required bool Enabled { get; set; }
+ public required bool RunMigrations { get; set; }
+ public required bool SeedDatabase { get; set; }
+}
\ No newline at end of file
diff --git a/backend/Infrastructure/TestContainers/TestContainersFactory.cs b/backend/Infrastructure/TestContainers/TestContainersFactory.cs
new file mode 100644
index 00000000..325be490
--- /dev/null
+++ b/backend/Infrastructure/TestContainers/TestContainersFactory.cs
@@ -0,0 +1,82 @@
+using Infrastructure.DatabaseContext;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Testcontainers.SqlEdge;
+
+namespace Infrastructure.TestContainers;
+
+public class TestContainersFactory(TestContainersConfig config, ILogger logger)
+{
+ private const string DbPassword = "test123!";
+ private const int DbHostPort = 14333;
+
+ public static readonly string DefaultConnectionString =
+ $"Server=127.0.0.1,{DbHostPort};Database=master;User Id=sa;Password={DbPassword};TrustServerCertificate=True";
+
+ private SqlEdgeContainer? _sqlEdgeContainer;
+
+ public string? CurrentConnectionString => _sqlEdgeContainer?.GetConnectionString();
+
+ public async Task Start(CancellationToken cancellationToken = default, Overrides? overrides = null)
+ {
+ try
+ {
+ if (!config.Enabled) return;
+
+ var dbHostPort = overrides?.DbHostPortOverride ?? DbHostPort;
+ var dbContainerName = overrides?.DbContainerNameOverride ?? "testcontainers-api-db";
+
+ logger.LogInformation("Starting TestContainers");
+
+ _sqlEdgeContainer = new SqlEdgeBuilder()
+ .WithName(dbContainerName)
+ .WithReuse(true)
+ .WithPassword(DbPassword)
+ .WithPortBinding(dbHostPort, 1433)
+ .Build();
+
+ await _sqlEdgeContainer.StartAsync(cancellationToken);
+
+ var options = Options.Create(new InfrastructureConfig
+ {
+ ConnectionString = _sqlEdgeContainer.GetConnectionString(),
+ EnableSensitiveDataLogging = true
+ });
+
+ await using var context = new ApplicationContext(options);
+
+ if (config.RunMigrations)
+ await MigrateDatabase(context, cancellationToken);
+
+ if (config.SeedDatabase) logger.LogInformation("Seeding database with test data");
+ // TODO: DataBuilder
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error while starting TestContainers");
+ }
+ }
+
+ public Task Stop(CancellationToken cancellationToken = default)
+ {
+ var stopTask = _sqlEdgeContainer?.StopAsync(cancellationToken) ?? Task.CompletedTask;
+ return stopTask;
+ }
+
+ public record Overrides(string? DbContainerNameOverride, int? DbHostPortOverride);
+
+ private async Task MigrateDatabase(DbContext context, CancellationToken cancellationToken)
+ {
+ var retries = 0;
+ while (!await context.Database.CanConnectAsync(cancellationToken) && retries < 10)
+ {
+ retries++;
+ logger.LogInformation("Attempt #{AttemptNumber} to connect to DB failed, retrying in 1 second.", retries);
+ await Task.Delay(1000, cancellationToken);
+ }
+
+ logger.LogInformation("Running database migrations");
+ await context.Database.MigrateAsync(cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/backend/Infrastructure/TestContainers/TestContainersHostBuilder.cs b/backend/Infrastructure/TestContainers/TestContainersHostBuilder.cs
new file mode 100644
index 00000000..71f02189
--- /dev/null
+++ b/backend/Infrastructure/TestContainers/TestContainersHostBuilder.cs
@@ -0,0 +1,26 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Infrastructure.TestContainers;
+
+public static class TestContainersHostBuilder
+{
+ public static void AddTestContainers(this IHostApplicationBuilder builder)
+ {
+ if (!builder.Environment.IsValidTestContainersEnvironment())
+ throw new InvalidOperationException(
+ $"{nameof(AddTestContainers)} should only be called in dev environments. Current environment: {builder.Environment.EnvironmentName}");
+
+ builder.Services.AddHostedService();
+
+ builder.Services.Configure(opts =>
+ {
+ opts.ConnectionString = TestContainersFactory.DefaultConnectionString;
+ });
+ }
+
+ private static bool IsValidTestContainersEnvironment(this IHostEnvironment environment)
+ {
+ return environment.IsDevelopment() || environment.EnvironmentName == "Local";
+ }
+}
\ No newline at end of file
diff --git a/backend/Infrastructure/TestContainers/TestContainersService.cs b/backend/Infrastructure/TestContainers/TestContainersService.cs
new file mode 100644
index 00000000..69c0b82f
--- /dev/null
+++ b/backend/Infrastructure/TestContainers/TestContainersService.cs
@@ -0,0 +1,43 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Infrastructure.TestContainers;
+
+public class TestContainersService(IServiceProvider serviceProvider) : IHostedService
+{
+ private TestContainersFactory? _testContainersFactory;
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ using var scope = serviceProvider.CreateScope();
+ var logger = scope.ServiceProvider.GetRequiredService>();
+
+ try
+ {
+ logger.LogInformation("Running TestContainersService");
+
+ var config = scope.ServiceProvider.GetRequiredService>().Value;
+ if (config.Enabled)
+ {
+ var testContainersLogger = scope.ServiceProvider.GetRequiredService>();
+ _testContainersFactory = new TestContainersFactory(config, testContainersLogger);
+
+ await _testContainersFactory.Start(cancellationToken);
+ }
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Failed to start test containers service");
+ throw;
+ }
+
+ logger.LogInformation("Finished running TestContainersService");
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ return _testContainersFactory?.Stop(cancellationToken) ?? Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/backend/Tests/Api.E2E/ExampleTests.cs b/backend/Tests/Api.E2E/ExampleTests.cs
new file mode 100644
index 00000000..e214e19a
--- /dev/null
+++ b/backend/Tests/Api.E2E/ExampleTests.cs
@@ -0,0 +1,51 @@
+using Bogus;
+using Bogus.DataSets;
+using Core.Customers;
+using Core.Organizations;
+using FluentAssertions;
+using Microsoft.EntityFrameworkCore;
+using Tests.Api.E2E.Shared;
+
+namespace Tests.Api.E2E;
+
+[Collection(ApiTestCollection.CollectionName)]
+public class ExampleTests(ApiFactory apiFactory) : TestsBase(apiFactory)
+{
+
+ private static Faker CompanyFaker = new();
+
+ [Fact]
+ public async Task Can_Make_Database_Calls()
+ {
+ var faker = new Faker();
+ var orgName = faker.Company.CompanyName();
+
+ var org = CompanyFaker.Generate();
+
+ var organization = new Organization
+ {
+ Id = faker.Random.Guid().ToString(),
+ Name = orgName,
+ UrlKey = orgName.ToLower().Replace(" ", ""),
+ Country = faker.Address.Country(),
+ NumberOfVacationDaysInYear = faker.Random.Int(20, 30),
+ HasVacationInChristmas = faker.Random.Bool(),
+ HoursPerWorkday = faker.Random.Int(6,9),
+ Departments = [],
+ Customers = [],
+ AbsenceTypes = []
+ };
+
+ DatabaseContext.Organization.Add(organization);
+ await DatabaseContext.SaveChangesAsync();
+ DatabaseContext.ChangeTracker.Clear();
+
+ var fetchedOrganization = await DatabaseContext.Organization
+ .Include(x => x.Departments)
+ .Include(x => x.Customers)
+ .Include(x => x.AbsenceTypes)
+ .FirstOrDefaultAsync();
+
+ fetchedOrganization.Should().BeEquivalentTo(organization);
+ }
+}
\ No newline at end of file
diff --git a/backend/Tests/Api.E2E/Shared/ApiFactory.cs b/backend/Tests/Api.E2E/Shared/ApiFactory.cs
new file mode 100644
index 00000000..7f579ca0
--- /dev/null
+++ b/backend/Tests/Api.E2E/Shared/ApiFactory.cs
@@ -0,0 +1,97 @@
+using System.Data.Common;
+using System.Diagnostics;
+using Api;
+using Infrastructure;
+using Infrastructure.DatabaseContext;
+using Infrastructure.TestContainers;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Respawn;
+using Respawn.Graph;
+
+namespace Tests.Api.E2E.Shared;
+
+public class ApiFactory : WebApplicationFactory, IAsyncLifetime, ICollectionFixture
+{
+ public TestContainersFactory TestContainersFactory { get; private set; } = default!;
+ public ApplicationContext DbContext { get; private set; } = default!;
+ public HttpClient HttpClient { get; private set; } = default!;
+ private DbConnection _dbConnection = default!;
+ private Respawner _respawner = default!;
+
+ public async Task InitializeAsync()
+ {
+ var testContainersConfig = new TestContainersConfig
+ {
+ Enabled = true,
+ RunMigrations = true,
+ SeedDatabase = false
+ };
+ TestContainersFactory =
+ new TestContainersFactory(testContainersConfig, NullLogger.Instance);
+
+ await TestContainersFactory.Start(
+ overrides: new
+ TestContainersFactory.Overrides(
+ "testcontainers-api-e2e-db",
+ 14433
+ ));
+
+ DbContext = new ApplicationContext(Options.Create(new InfrastructureConfig
+ {
+ ConnectionString = TestContainersFactory.CurrentConnectionString ?? throw new UnreachableException(),
+ EnableSensitiveDataLogging = true
+ }));
+
+ HttpClient = CreateClient();
+
+ _dbConnection = new SqlConnection(TestContainersFactory.CurrentConnectionString);
+ await _dbConnection.OpenAsync();
+
+ _respawner = await Respawner.CreateAsync(_dbConnection, new RespawnerOptions
+ {
+ DbAdapter = DbAdapter.SqlServer,
+ SchemasToInclude = ["dbo"],
+ TablesToIgnore = [new Table("dbo", "__EFMigrationsHistory")]
+ });
+
+ }
+
+ public new Task DisposeAsync()
+ {
+ HttpClient.Dispose();
+ return TestContainersFactory.Stop();
+ }
+
+ protected override IHost CreateHost(IHostBuilder builder)
+ {
+ builder.ConfigureHostConfiguration(config => config.AddJsonFile("appsettings.Test.json", optional: true));
+ return base.CreateHost(builder);
+ }
+
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.RemoveAll(typeof(TestContainersService));
+ services.Configure(opts =>
+ {
+ opts.ConnectionString =
+ TestContainersFactory.CurrentConnectionString ?? throw new UnreachableException();
+ });
+ });
+ }
+
+ public async Task ResetDatabaseAsync()
+ {
+ await _respawner.ResetAsync(_dbConnection);
+ }
+}
\ No newline at end of file
diff --git a/backend/Tests/Api.E2E/Shared/ApiTestCollection.cs b/backend/Tests/Api.E2E/Shared/ApiTestCollection.cs
new file mode 100644
index 00000000..386900cd
--- /dev/null
+++ b/backend/Tests/Api.E2E/Shared/ApiTestCollection.cs
@@ -0,0 +1,7 @@
+namespace Tests.Api.E2E.Shared;
+
+[CollectionDefinition(CollectionName)]
+public class ApiTestCollection : ICollectionFixture
+{
+ public const string CollectionName = nameof(ApiTestCollection);
+}
\ No newline at end of file
diff --git a/backend/Tests/Api.E2E/TestsBase.cs b/backend/Tests/Api.E2E/TestsBase.cs
new file mode 100644
index 00000000..c08ce12c
--- /dev/null
+++ b/backend/Tests/Api.E2E/TestsBase.cs
@@ -0,0 +1,31 @@
+using Infrastructure.DatabaseContext;
+using Tests.Api.E2E.Shared;
+
+namespace Tests.Api.E2E;
+
+[Collection(ApiTestCollection.CollectionName)]
+public class TestsBase : IAsyncLifetime
+{
+ private readonly Func _resetDatabase;
+
+ protected readonly HttpClient Client;
+ protected readonly ApplicationContext DatabaseContext;
+
+ protected TestsBase(ApiFactory apiFactory)
+ {
+ Client = apiFactory.HttpClient;
+ _resetDatabase = apiFactory.ResetDatabaseAsync;
+ DatabaseContext = apiFactory.DbContext;
+ }
+
+ public Task InitializeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task DisposeAsync()
+ {
+ DatabaseContext.ChangeTracker.Clear();
+ return _resetDatabase();
+ }
+}
\ No newline at end of file
diff --git a/backend/Tests/CleanArchitectureLayerTests.cs b/backend/Tests/CleanArchitectureLayerTests.cs
deleted file mode 100644
index 9388c2f2..00000000
--- a/backend/Tests/CleanArchitectureLayerTests.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using ArchUnitNET.Domain;
-using ArchUnitNET.Fluent;
-using ArchUnitNET.Loader;
-using ArchUnitNET.NUnit;
-using static ArchUnitNET.Fluent.ArchRuleDefinition;
-using Assembly = System.Reflection.Assembly;
-
-namespace Tests;
-
-public class CleanArchitectureLayerTests
-{
- // TIP: load your architecture once at the start to maximize performance of your tests
- private static readonly Architecture Architecture = new ArchLoader().LoadAssemblies(
- Assembly.Load("Core"),
- Assembly.Load("Infrastructure"),
- Assembly.Load("Api")
- ).Build();
-
- private readonly IObjectProvider ApiLayer =
- Types().That().ResideInNamespace("Api").As("Api Layer");
-
- private readonly IObjectProvider CoreLayer =
- Types().That().ResideInAssembly("ApplicationCore").As("Application Core Layer");
-
- private readonly IObjectProvider DatabaseLayer =
- Types().That().ResideInNamespace("Infrastructure").As("Infrastructure Layer");
-
- [Test]
- public void CoreLayerShouldNotAccessApiLayer()
- {
- IArchRule applicationCoreLayerShouldNotAccessApiLayer = Types().That().Are(CoreLayer).Should()
- .NotDependOnAny(ApiLayer).Because("The ApplicationCore project should not depend on the Api project.");
- applicationCoreLayerShouldNotAccessApiLayer.Check(Architecture);
- }
-
- [Test]
- public void CoreLayerShouldNotAccessDatabaseLayer()
- {
- IArchRule applicationCoreLayerShouldNotAccessApiLayer = Types().That().Are(CoreLayer).Should()
- .NotDependOnAny(DatabaseLayer).Because("The ApplicationCore project should not depend on the Api project.");
- applicationCoreLayerShouldNotAccessApiLayer.Check(Architecture);
- }
-
- [Test]
- public void DatabaseLayerShouldNotAccessApiLayer()
- {
- IArchRule infrastructureLayerShouldNotAccessApiLayer = Types().That().Are(DatabaseLayer).Should()
- .NotDependOnAny(ApiLayer).Because("The Infrastructure project should not depend on the Api project.");
- infrastructureLayerShouldNotAccessApiLayer.Check(Architecture);
- }
-}
\ No newline at end of file
diff --git a/backend/Tests/AbsenceTest.cs b/backend/Tests/Core.Tests/AbsenceTest.cs
similarity index 84%
rename from backend/Tests/AbsenceTest.cs
rename to backend/Tests/Core.Tests/AbsenceTest.cs
index baa315cb..72f36f0e 100644
--- a/backend/Tests/AbsenceTest.cs
+++ b/backend/Tests/Core.Tests/AbsenceTest.cs
@@ -8,25 +8,26 @@
using Core.Staffings;
using Core.Vacations;
using Core.Weeks;
+using FluentAssertions;
using NSubstitute;
-namespace Tests;
+namespace Tests.Core.Tests;
-public class Tests
+public class AbsenceTests
{
- [TestCase(2, 15, 0, 0, 7.5)]
- [TestCase(0, 7.5, 0, 0, 30)]
- [TestCase(5, 37.5, 0, 0, 0)]
- [TestCase(0, 0, 0, 0, 37.5)]
- [TestCase(5, 30, 0, 0, 0)]
- [TestCase(5, 0, 0, 0, 0)]
- [TestCase(5, 37.5, 0, 0, 0)]
- [TestCase(0, 0, 1, 0, 30)]
- [TestCase(0, 0, 2, 0, 22.5)]
- [TestCase(0, 0, 5, 0, 0)]
- [TestCase(0, 0, 0, 37.5, 0)]
- [TestCase(0, 0, 0, 30, 7.5)]
- [TestCase(0, 7.5, 0, 22.5, 7.5)]
+ [Theory]
+ [InlineData(2, 15, 0, 0, 7.5)]
+ [InlineData(0, 7.5, 0, 0, 30)]
+ [InlineData(5, 37.5, 0, 0, 0)]
+ [InlineData(0, 0, 0, 0, 37.5)]
+ [InlineData(5, 30, 0, 0, 0)]
+ [InlineData(5, 0, 0, 0, 0)]
+ [InlineData(0, 0, 1, 0, 30)]
+ [InlineData(0, 0, 2, 0, 22.5)]
+ [InlineData(0, 0, 5, 0, 0)]
+ [InlineData(0, 0, 0, 37.5, 0)]
+ [InlineData(0, 0, 0, 30, 7.5)]
+ [InlineData(0, 7.5, 0, 22.5, 7.5)]
public void AvailabilityCalculation(
int vacationDays,
double plannedAbsenceHours,
@@ -119,16 +120,14 @@ public void AvailabilityCalculation(
var bookingModel = ReadModelFactory.MapToReadModelList(consultant, [week]).Bookings[0]
.BookingModel;
- Assert.Multiple(() =>
- {
- Assert.That(bookingModel.TotalBillable, Is.EqualTo(staffedHours));
- Assert.That(bookingModel.TotalPlannedAbsences, Is.EqualTo(plannedAbsenceHours));
- Assert.That(bookingModel.TotalHolidayHours, Is.EqualTo(numberOfHolidays * 7.5));
- Assert.That(bookingModel.TotalSellableTime, Is.EqualTo(expectedSellableHours));
- });
+
+ bookingModel.TotalBillable.Should().Be(staffedHours);
+ bookingModel.TotalPlannedAbsences.Should().Be(plannedAbsenceHours);
+ bookingModel.TotalHolidayHours.Should().Be(numberOfHolidays * 7.5);
+ bookingModel.TotalSellableTime.Should().Be(expectedSellableHours);
}
- [Test]
+ [Fact]
public void MultiplePlannedAbsences()
{
var org = new Organization
@@ -191,6 +190,6 @@ public void MultiplePlannedAbsences()
var bookedHours = ReadModelFactory.MapToReadModelList(consultant, [week]).Bookings[0]
.BookingModel;
- Assert.That(bookedHours.TotalPlannedAbsences, Is.EqualTo(30));
+ bookedHours.TotalPlannedAbsences.Should().Be(30);
}
}
\ No newline at end of file
diff --git a/backend/Tests/Core.Tests/ArchitectureTests.cs b/backend/Tests/Core.Tests/ArchitectureTests.cs
new file mode 100644
index 00000000..81c7c387
--- /dev/null
+++ b/backend/Tests/Core.Tests/ArchitectureTests.cs
@@ -0,0 +1,73 @@
+using ArchUnitNET.Domain;
+using ArchUnitNET.Loader;
+using ArchUnitNET.xUnit;
+using static ArchUnitNET.Fluent.ArchRuleDefinition;
+using Assembly = System.Reflection.Assembly;
+
+namespace Tests.Core.Tests;
+
+public class ArchitectureTests
+{
+ private static readonly Architecture Architecture = new ArchLoader().LoadAssemblies(
+ Assembly.Load("Core"),
+ Assembly.Load("Infrastructure"),
+ Assembly.Load("Api")
+ ).Build();
+
+ private readonly IObjectProvider _apiLayer =
+ Types()
+ .That()
+ .ResideInNamespace("Api")
+ .As("Api Layer");
+
+ private readonly IObjectProvider _coreLayer =
+ Types()
+ .That()
+ .ResideInAssembly("ApplicationCore")
+ .As("Application Core Layer");
+
+ private readonly IObjectProvider _databaseLayer =
+ Types()
+ .That()
+ .ResideInNamespace("Infrastructure")
+ .As("Infrastructure Layer");
+
+ [Fact]
+ public void CoreLayer_Should_Not_Access_ApiLayer()
+ {
+ Types()
+ .That()
+ .Are(_coreLayer)
+ .Should()
+ .NotDependOnAny(_apiLayer)
+ .Because("The ApplicationCore project should not depend on the Api project.")
+ .WithoutRequiringPositiveResults()
+ .Check(Architecture);
+ }
+
+ [Fact]
+ public void CoreLayer_Should_Not_Access_DatabaseLayer()
+ {
+ Types()
+ .That()
+ .Are(_coreLayer)
+ .Should()
+ .NotDependOnAny(_databaseLayer)
+ .Because("The ApplicationCore project should not depend on the Api project.")
+ .WithoutRequiringPositiveResults()
+ .Check(Architecture);
+ }
+
+ [Fact]
+ public void DatabaseLayer_Should_Not_Access_ApiLayer()
+ {
+ Types()
+ .That()
+ .Are(_databaseLayer)
+ .Should()
+ .NotDependOnAny(_apiLayer)
+ .Because("The Infrastructure project should not depend on the Api project.")
+ .WithoutRequiringPositiveResults()
+ .Check(Architecture);
+ }
+}
\ No newline at end of file
diff --git a/backend/Tests/GlobalUsings.cs b/backend/Tests/GlobalUsings.cs
index cefced49..8c927eb7 100644
--- a/backend/Tests/GlobalUsings.cs
+++ b/backend/Tests/GlobalUsings.cs
@@ -1 +1 @@
-global using NUnit.Framework;
\ No newline at end of file
+global using Xunit;
\ No newline at end of file
diff --git a/backend/Tests/Tests.csproj b/backend/Tests/Tests.csproj
index b8a7f49a..a1b7dffa 100644
--- a/backend/Tests/Tests.csproj
+++ b/backend/Tests/Tests.csproj
@@ -11,14 +11,20 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/backend/backend.sln.DotSettings b/backend/backend.sln.DotSettings
index 97106184..6a2c9e52 100644
--- a/backend/backend.sln.DotSettings
+++ b/backend/backend.sln.DotSettings
@@ -1,4 +1,5 @@
+ True
True
True
True
@@ -6,8 +7,10 @@
True
True
True
+ True
True
True
True
True
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/frontend/src/components/FilteredConsultantsList.tsx b/frontend/src/components/FilteredConsultantsList.tsx
index 1ea0a01d..47a890ed 100644
--- a/frontend/src/components/FilteredConsultantsList.tsx
+++ b/frontend/src/components/FilteredConsultantsList.tsx
@@ -34,7 +34,7 @@ export default function StaffingTable() {
{filteredConsultants
.at(0)
- ?.bookings.map((_, index) => )}
+ ?.bookings.map((x) => )}