diff --git a/src/UserManager.API.IntegrationTests/Controllers/UserControllerIntegrationTests.cs b/src/UserManager.API.IntegrationTests/Controllers/UserControllerIntegrationTests.cs new file mode 100644 index 0000000..43b8aa1 --- /dev/null +++ b/src/UserManager.API.IntegrationTests/Controllers/UserControllerIntegrationTests.cs @@ -0,0 +1,73 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Net; +using System.Net.Http.Json; +using UserManager.API.IntegrationTests.Helpers; +using UserManager.Application.Features.Users.Requests; +using UserManager.Infrastructure; + +namespace UserManager.API.IntegrationTests.Controllers +{ + [TestClass] + public class UserControllerIntegrationTests + { + private HttpClient _client = null!; + private CustomWebApplicationFactory _factory = null!; + + [TestInitialize] + public void Initialize() + { + _factory = new CustomWebApplicationFactory(); + + // Create a database with all migrations applied + using var scope = _factory.Services.CreateScope(); + var services = scope.ServiceProvider; + var context = services.GetRequiredService(); + context.Database.EnsureCreated(); + + _client = _factory.CreateClient(); + } + + [TestCleanup] + public void Cleanup() + { + _client.Dispose(); + _factory.Dispose(); + } + + [TestMethod] + public async Task CreateUserAsync_Returns400WithValidation_ForInvalidRequest() + { + // Arrange + var request = new CreateUserRequestDto + { + FirstName = "", + LastName = "", + }; + + // Act + var response = await _client.PostAsJsonAsync("/api/user", request); + + // Assert + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + Assert.AreEqual("{\"FirstName\":[\"FirstName is required.\"],\"LastName\":[\"LastName is required.\"]}", response.Content.ReadAsStringAsync().Result); + } + + [TestMethod] + public async Task CreateUserAsync_Returns200OK_ForValidRequest() + { + // Arrange + var request = new CreateUserRequestDto + { + FirstName = "James", + LastName = "Smith", + }; + + // Act + var response = await _client.PostAsJsonAsync("/api/user", request); + + // Assert + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + } + } +} diff --git a/src/UserManager.API.IntegrationTests/Helpers/CustomWebApplicationFactory.cs b/src/UserManager.API.IntegrationTests/Helpers/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..e0bcd16 --- /dev/null +++ b/src/UserManager.API.IntegrationTests/Helpers/CustomWebApplicationFactory.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System.Data.Common; +using UserManager.Infrastructure; + +namespace UserManager.API.IntegrationTests.Helpers +{ + public class CustomWebApplicationFactory : WebApplicationFactory where TProgram : class + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // Override the configure services method to add a test context + builder.ConfigureServices(services => + { + var dbContextDescriptor = services.SingleOrDefault( + d => d.ServiceType == + typeof(DbContextOptions)); + + services.Remove(dbContextDescriptor!); + + var dbConnectionDescriptor = services.SingleOrDefault( + d => d.ServiceType == + typeof(DbConnection)); + + services.Remove(dbConnectionDescriptor!); + + // Create open SqliteConnection so EF won't automatically close it. + services.AddSingleton(container => + { + var connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + return connection; + }); + + services.AddDbContext((container, options) => + { + var connection = container.GetRequiredService(); + options.UseSqlite(connection); + }); + }); + } + } +} diff --git a/src/UserManager.API.IntegrationTests/UserManager.API.IntegrationTests.csproj b/src/UserManager.API.IntegrationTests/UserManager.API.IntegrationTests.csproj new file mode 100644 index 0000000..3651adf --- /dev/null +++ b/src/UserManager.API.IntegrationTests/UserManager.API.IntegrationTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/UserManager.API/Controllers/UserController.cs b/src/UserManager.API/Controllers/UserController.cs index 51aba64..1b258ac 100644 --- a/src/UserManager.API/Controllers/UserController.cs +++ b/src/UserManager.API/Controllers/UserController.cs @@ -27,7 +27,7 @@ namespace UserManager.API.Controllers if (!validationResult.IsValid) { - return BadRequest(validationResult.Errors); + return BadRequest(validationResult.ToDictionary()); } else { diff --git a/src/UserManager.API/Program.cs b/src/UserManager.API/Program.cs index 3b69458..9581687 100644 --- a/src/UserManager.API/Program.cs +++ b/src/UserManager.API/Program.cs @@ -28,3 +28,5 @@ app.UseAuthorization(); app.MapControllers(); app.Run(); + +public partial class Program { } \ No newline at end of file diff --git a/src/UserManager.Infrastructure/Migrations/20241022112315_BaseColumns.Designer.cs b/src/UserManager.Infrastructure/Migrations/20241022112315_BaseColumns.Designer.cs new file mode 100644 index 0000000..ba3db5e --- /dev/null +++ b/src/UserManager.Infrastructure/Migrations/20241022112315_BaseColumns.Designer.cs @@ -0,0 +1,63 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using UserManager.Infrastructure; + +#nullable disable + +namespace UserManager.Infrastructure.Migrations +{ + [DbContext(typeof(UserManagerContext))] + [Migration("20241022112315_BaseColumns")] + partial class BaseColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("UserManager.Domain.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId")); + + b.Property("CreateAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/UserManager.Infrastructure/Migrations/20241022112315_BaseColumns.cs b/src/UserManager.Infrastructure/Migrations/20241022112315_BaseColumns.cs new file mode 100644 index 0000000..4b6b928 --- /dev/null +++ b/src/UserManager.Infrastructure/Migrations/20241022112315_BaseColumns.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UserManager.Infrastructure.Migrations +{ + /// + public partial class BaseColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreateAtUtc", + table: "Users", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "DeletedAtUtc", + table: "Users", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsDeleted", + table: "Users", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "UpdatedAtUtc", + table: "Users", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreateAtUtc", + table: "Users"); + + migrationBuilder.DropColumn( + name: "DeletedAtUtc", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IsDeleted", + table: "Users"); + + migrationBuilder.DropColumn( + name: "UpdatedAtUtc", + table: "Users"); + } + } +} diff --git a/src/UserManager.Infrastructure/Migrations/UserManagerContextModelSnapshot.cs b/src/UserManager.Infrastructure/Migrations/UserManagerContextModelSnapshot.cs index ce89bb3..3790813 100644 --- a/src/UserManager.Infrastructure/Migrations/UserManagerContextModelSnapshot.cs +++ b/src/UserManager.Infrastructure/Migrations/UserManagerContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -29,14 +30,26 @@ namespace UserManager.Infrastructure.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId")); + b.Property("CreateAtUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAtUtc") + .HasColumnType("timestamp with time zone"); + b.Property("FirstName") .IsRequired() .HasColumnType("text"); + b.Property("IsDeleted") + .HasColumnType("boolean"); + b.Property("LastName") .IsRequired() .HasColumnType("text"); + b.Property("UpdatedAtUtc") + .HasColumnType("timestamp with time zone"); + b.HasKey("UserId"); b.ToTable("Users"); diff --git a/src/UserManager.sln b/src/UserManager.sln index d7e05e9..e854aea 100644 --- a/src/UserManager.sln +++ b/src/UserManager.sln @@ -17,7 +17,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManager.Application", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManager.Application.UnitTests", "..\tests\UserManager.Application.Tests\UserManager.Application.UnitTests.csproj", "{B0F63D11-6933-4D87-B0D8-6DD7DFC5A23C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManager.Application.IntegrationTests", "..\tests\UserManager.Application.IntegrationTests\UserManager.Application.IntegrationTests.csproj", "{2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManager.Application.IntegrationTests", "..\tests\UserManager.Application.IntegrationTests\UserManager.Application.IntegrationTests.csproj", "{2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManager.API.IntegrationTests", "UserManager.API.IntegrationTests\UserManager.API.IntegrationTests.csproj", "{FB1D2DF8-F06A-4414-B855-267BCD76B865}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -49,6 +51,10 @@ Global {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}.Release|Any CPU.Build.0 = Release|Any CPU + {FB1D2DF8-F06A-4414-B855-267BCD76B865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB1D2DF8-F06A-4414-B855-267BCD76B865}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB1D2DF8-F06A-4414-B855-267BCD76B865}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB1D2DF8-F06A-4414-B855-267BCD76B865}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,6 +66,7 @@ Global {C7215859-A216-4527-A192-7B8F57E50BB4} = {74E412ED-9D1B-4597-BBFD-05623C814F98} {B0F63D11-6933-4D87-B0D8-6DD7DFC5A23C} = {5B673FE8-32DA-4CD3-8394-1BD8E1275270} {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5} = {5B673FE8-32DA-4CD3-8394-1BD8E1275270} + {FB1D2DF8-F06A-4414-B855-267BCD76B865} = {5B673FE8-32DA-4CD3-8394-1BD8E1275270} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6E7F90F4-4A09-4CDB-8653-6E094E164853} diff --git a/tests/UserManager.Application.IntegrationTests/UnitTest1.cs b/tests/UserManager.Application.IntegrationTests/UnitTest1.cs deleted file mode 100644 index f227925..0000000 --- a/tests/UserManager.Application.IntegrationTests/UnitTest1.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace UserManager.Application.IntegrationTests -{ - [TestClass] - public class UnitTest1 - { - [TestMethod] - public void TestMethod1() - { - } - } -} \ No newline at end of file diff --git a/tests/UserManager.Application.Tests/Class1.cs b/tests/UserManager.Application.Tests/Class1.cs deleted file mode 100644 index 7e59215..0000000 --- a/tests/UserManager.Application.Tests/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace UserManager.Application.Tests -{ - public class Class1 - { - - } -}