From 3cdbbbd8d449ea064740f4960b16ca25494be390 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:30:24 +0500 Subject: [PATCH 01/22] feat: #8: add validation configurations --- Api/Api.csproj | 2 ++ .../ValidationConfigurations.cs | 33 +++++++++++++++++++ Api/Program.cs | 6 ++++ 3 files changed, 41 insertions(+) create mode 100644 Api/Configurations/ValidationConfigurations.cs diff --git a/Api/Api.csproj b/Api/Api.csproj index f1f7922..1041a1a 100644 --- a/Api/Api.csproj +++ b/Api/Api.csproj @@ -13,6 +13,7 @@ + all @@ -23,6 +24,7 @@ + all diff --git a/Api/Configurations/ValidationConfigurations.cs b/Api/Configurations/ValidationConfigurations.cs new file mode 100644 index 0000000..b9c6bd4 --- /dev/null +++ b/Api/Configurations/ValidationConfigurations.cs @@ -0,0 +1,33 @@ +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Configurations; + +public static class ValidationConfiguration +{ + public static void ConfigureValidation(IServiceCollection services, IHostEnvironment environment) + { + services.AddProblemDetails(options => + { + options.IncludeExceptionDetails = (ctx, ex) => environment.IsDevelopment(); + }); + + services.AddControllers() + .ConfigureApiBehaviorOptions(options => + { + options.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState) + { + Type = "https://example.com/validation-error", + Title = "Validation error", + Status = StatusCodes.Status400BadRequest, + Detail = "Fill in all the fields", + Instance = context.HttpContext.Request.Path + }; + + throw new ProblemDetailsException(problemDetails); + }; + }); + } +} \ No newline at end of file diff --git a/Api/Program.cs b/Api/Program.cs index 7540491..2c9579f 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -1,4 +1,6 @@ +using Api.Configurations; using Application; +using Hellang.Middleware.ProblemDetails; using Microsoft.EntityFrameworkCore; using TourmalineCore.AspNetCore.JwtAuthentication.Core; using TourmalineCore.AspNetCore.JwtAuthentication.Core.Options; @@ -19,12 +21,16 @@ public static void Main(string[] args) builder.Services.AddApplication(configuration); + ValidationConfiguration.ConfigureValidation(builder.Services, builder.Environment); + var authenticationOptions = configuration.GetSection(nameof(AuthenticationOptions)).Get(); builder.Services.Configure(configuration.GetSection(nameof(AuthenticationOptions))); builder.Services.AddJwtAuthentication(authenticationOptions).WithUserClaimsProvider(UserClaimsProvider.PermissionClaimType); var app = builder.Build(); + app.UseProblemDetails(); + app.MapOpenApi("api/swagger/openapi/v1.json"); app.UseSwaggerUI(options => From 8b1e8a7dbf85f640717ba4bc76520c26772be154 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:29:31 +0500 Subject: [PATCH 02/22] feat: #8: add HttpClientTest and CreateWorkEntryAsync_ShouldThrowValidationErrorIfAtLeastOneOfRequiredFieldsIsEmpty test --- .../Tracking/TrackingControllerTests.cs | 41 ++++++++++ Api/HttpClientTest.cs | 76 +++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 Api/Features/Tracking/TrackingControllerTests.cs create mode 100644 Api/HttpClientTest.cs diff --git a/Api/Features/Tracking/TrackingControllerTests.cs b/Api/Features/Tracking/TrackingControllerTests.cs new file mode 100644 index 0000000..4b45b76 --- /dev/null +++ b/Api/Features/Tracking/TrackingControllerTests.cs @@ -0,0 +1,41 @@ +using Application.Commands; +using Application.TestsConfig; +using Core.Entities; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Api.Features.Tracking; + +[IntegrationTest] +public class TrackingControllerValidationTests : HttpClientTest +{ + public TrackingControllerValidationTests(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task CreateWorkEntryAsync_ShouldThrowValidationErrorIfAtLeastOneOfRequiredFieldsIsEmpty() + { + var createWorkEntryCommandParams = new CreateWorkEntryCommandParams + { + Title = "", + StartTime = new DateTime(2025, 11, 24, 9, 0, 0), + EndTime = new DateTime(2025, 11, 24, 10, 0, 0), + TaskId = "#2231", + Description = "Task description", + Type = EventType.Task + }; + + var response = await HttpClient.PostAsJsonAsync("/api/time/tracking/work-entries", createWorkEntryCommandParams); + + Assert.NotNull(response); + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + + var validationProblemDetails = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(validationProblemDetails); + Assert.Equal("Fill in all the fields", validationProblemDetails.Detail); + Assert.Equal("Validation error", validationProblemDetails.Title); + } +} diff --git a/Api/HttpClientTest.cs b/Api/HttpClientTest.cs new file mode 100644 index 0000000..a7a2ac0 --- /dev/null +++ b/Api/HttpClientTest.cs @@ -0,0 +1,76 @@ +using Api; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.Authentication; +using System.Security.Claims; +using Microsoft.Extensions.Options; +using System.Text.Encodings.Web; +using Xunit; +using Microsoft.AspNetCore.TestHost; +using Application; +using Moq; + +public class HttpClientTest : IClassFixture>, IAsyncLifetime +{ + protected const long EMPLOYEE_ID = 1; + protected const long TENANT_ID = 777; + + protected HttpClient HttpClient = null!; + private WebApplicationFactory _factory = null!; + + public HttpClientTest(WebApplicationFactory factory) + { + _factory = factory; + } + + public async Task InitializeAsync() + { + _factory = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + // Replacing the authentication service with a fake + services.AddAuthentication("Test") + .AddScheme("Test", options => { }); + + // Add fake mockClaimsProbider + var mockClaimsProvider = new Mock(); + mockClaimsProvider.Setup(x => x.EmployeeId).Returns(EMPLOYEE_ID); + mockClaimsProvider.Setup(x => x.TenantId).Returns(TENANT_ID); + + services.AddScoped(_ => mockClaimsProvider.Object); + }); + }); + + HttpClient = _factory.CreateClient(); + } + + public async Task DisposeAsync() + { + HttpClient.Dispose(); + _factory.Dispose(); + } +} + +public class FakeAuthHandler : AuthenticationHandler +{ + public FakeAuthHandler(IOptionsMonitor options, + ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) + { + } + + protected override Task HandleAuthenticateAsync() + { + var claims = new[] + { + // Adding permissions claims to bypass permissions + new Claim("permissions", UserClaimsProvider.CanManagePersonalTimeTracker), + new Claim("permissions", UserClaimsProvider.AUTO_TESTS_ONLY_IsWorkEntriesHardDeleteAllowed) + }; + + var identity = new ClaimsIdentity(claims, "Test"); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, "Test"); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } +} From 2fa98c7f09d477231fcf61f69fcc4cc3661fee4f Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:32:27 +0500 Subject: [PATCH 03/22] fix: fix lint --- Api/HttpClientTest.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Api/HttpClientTest.cs b/Api/HttpClientTest.cs index a7a2ac0..0e41f3a 100644 --- a/Api/HttpClientTest.cs +++ b/Api/HttpClientTest.cs @@ -1,13 +1,13 @@ -using Api; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.Authentication; using System.Security.Claims; -using Microsoft.Extensions.Options; using System.Text.Encodings.Web; -using Xunit; -using Microsoft.AspNetCore.TestHost; +using Api; using Application; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Options; using Moq; +using Xunit; public class HttpClientTest : IClassFixture>, IAsyncLifetime { From 1b9b6fa251fc6307e7b5f424da83b87fafb9202a Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:33:41 +0500 Subject: [PATCH 04/22] fix: fix lint --- Api/Configurations/ValidationConfigurations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Api/Configurations/ValidationConfigurations.cs b/Api/Configurations/ValidationConfigurations.cs index b9c6bd4..67db30f 100644 --- a/Api/Configurations/ValidationConfigurations.cs +++ b/Api/Configurations/ValidationConfigurations.cs @@ -30,4 +30,4 @@ public static void ConfigureValidation(IServiceCollection services, IHostEnviron }; }); } -} \ No newline at end of file +} From bbbdf68a26ec2c1b99c87e909b9facadbb635327 Mon Sep 17 00:00:00 2001 From: Workflow Action Date: Thu, 18 Dec 2025 04:36:08 +0000 Subject: [PATCH 05/22] docs(readme): bring test coverage score up to date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6d28b70..a4145f9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # inner-circle-time-api -[![coverage](https://img.shields.io/badge/e2e_coverage-42.99%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/units_coverage-23.56%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/integration_coverage-61.61%25-orange)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/full_coverage-95.29%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/e2e_coverage-40.48%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/units_coverage-21.33%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/integration_coverage-70.66%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/full_coverage-95.84%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) This repo contains Inner Circle Time API. From 3055abaafaf076f4220dbeb0def1d3ff66569609 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:36:42 +0500 Subject: [PATCH 06/22] ci: #8: add HttpClientTest to .dockerignore --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 29a98aa..744e9b7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,6 +33,7 @@ README.md **/*Tests.cs **/*TestsRelated.cs **/TestsConfig/** +**/HttpClientTest/** **/bin/* **/obj/* From d98207c1be5f230f6770d6686910715f2cfe53ed Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:42:19 +0500 Subject: [PATCH 07/22] fix: #8: fix file name in .dockerignore --- .dockerignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 744e9b7..0fcafd8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,7 +33,7 @@ README.md **/*Tests.cs **/*TestsRelated.cs **/TestsConfig/** -**/HttpClientTest/** +**/HttpClientTest.cs **/bin/* **/obj/* From 3fb09344cada5bcdd6598e30930c38bdc9e9104f Mon Sep 17 00:00:00 2001 From: akovylyaeva Date: Thu, 18 Dec 2025 10:54:30 +0500 Subject: [PATCH 08/22] refactor: rename TrackingControllerValidationTests to TrackingControllerTests and HttpClientTest to HttpClientTestBase --- .dockerignore | 2 +- .../Tracking/TrackingControllerTests.cs | 4 ++-- ...ttpClientTest.cs => HttpClientTestBase.cs} | 22 +++++++++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) rename Api/{HttpClientTest.cs => HttpClientTestBase.cs} (74%) diff --git a/.dockerignore b/.dockerignore index 0fcafd8..ce19690 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,7 +33,7 @@ README.md **/*Tests.cs **/*TestsRelated.cs **/TestsConfig/** -**/HttpClientTest.cs +**/HttpClientTestBase.cs **/bin/* **/obj/* diff --git a/Api/Features/Tracking/TrackingControllerTests.cs b/Api/Features/Tracking/TrackingControllerTests.cs index 4b45b76..00ebc44 100644 --- a/Api/Features/Tracking/TrackingControllerTests.cs +++ b/Api/Features/Tracking/TrackingControllerTests.cs @@ -8,9 +8,9 @@ namespace Api.Features.Tracking; [IntegrationTest] -public class TrackingControllerValidationTests : HttpClientTest +public class TrackingControllerTests : HttpClientTestBase { - public TrackingControllerValidationTests(WebApplicationFactory factory) : base(factory) + public TrackingControllerTests(WebApplicationFactory factory) : base(factory) { } diff --git a/Api/HttpClientTest.cs b/Api/HttpClientTestBase.cs similarity index 74% rename from Api/HttpClientTest.cs rename to Api/HttpClientTestBase.cs index 0e41f3a..cc57217 100644 --- a/Api/HttpClientTest.cs +++ b/Api/HttpClientTestBase.cs @@ -9,7 +9,7 @@ using Moq; using Xunit; -public class HttpClientTest : IClassFixture>, IAsyncLifetime +public class HttpClientTestBase : IClassFixture>, IAsyncLifetime { protected const long EMPLOYEE_ID = 1; protected const long TENANT_ID = 777; @@ -17,7 +17,7 @@ public class HttpClientTest : IClassFixture>, IAs protected HttpClient HttpClient = null!; private WebApplicationFactory _factory = null!; - public HttpClientTest(WebApplicationFactory factory) + public HttpClientTestBase(WebApplicationFactory factory) { _factory = factory; } @@ -29,13 +29,18 @@ public async Task InitializeAsync() builder.ConfigureTestServices(services => { // Replacing the authentication service with a fake - services.AddAuthentication("Test") + services + .AddAuthentication("Test") .AddScheme("Test", options => { }); // Add fake mockClaimsProbider var mockClaimsProvider = new Mock(); - mockClaimsProvider.Setup(x => x.EmployeeId).Returns(EMPLOYEE_ID); - mockClaimsProvider.Setup(x => x.TenantId).Returns(TENANT_ID); + mockClaimsProvider + .Setup(x => x.EmployeeId) + .Returns(EMPLOYEE_ID); + mockClaimsProvider + .Setup(x => x.TenantId) + .Returns(TENANT_ID); services.AddScoped(_ => mockClaimsProvider.Object); }); @@ -53,8 +58,11 @@ public async Task DisposeAsync() public class FakeAuthHandler : AuthenticationHandler { - public FakeAuthHandler(IOptionsMonitor options, - ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) + public FakeAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder + ) : base(options, logger, encoder) { } From 0f2250b3570480a2f1532f9e507ca27745e7e171 Mon Sep 17 00:00:00 2001 From: Workflow Action Date: Thu, 18 Dec 2025 05:57:22 +0000 Subject: [PATCH 09/22] docs(readme): bring test coverage score up to date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a4145f9..8e02d02 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # inner-circle-time-api -[![coverage](https://img.shields.io/badge/e2e_coverage-40.48%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/units_coverage-21.33%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/integration_coverage-70.66%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/full_coverage-95.84%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/e2e_coverage-40.27%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/units_coverage-21.22%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/integration_coverage-70.81%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/full_coverage-95.86%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) This repo contains Inner Circle Time API. From 26442242d302aa6fb6ee9394de412e54f275b0a5 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:53:31 +0500 Subject: [PATCH 10/22] refactor: #8: change CreateWorkEntryCommandParams to CreateWorkEntryRequest --- Api/Features/Tracking/TrackingControllerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Api/Features/Tracking/TrackingControllerTests.cs b/Api/Features/Tracking/TrackingControllerTests.cs index 00ebc44..eeb7d61 100644 --- a/Api/Features/Tracking/TrackingControllerTests.cs +++ b/Api/Features/Tracking/TrackingControllerTests.cs @@ -1,3 +1,4 @@ +using Api.Features.Tracking.CreateWorkEntry; using Application.Commands; using Application.TestsConfig; using Core.Entities; @@ -17,17 +18,16 @@ public TrackingControllerTests(WebApplicationFactory factory) : base(fa [Fact] public async Task CreateWorkEntryAsync_ShouldThrowValidationErrorIfAtLeastOneOfRequiredFieldsIsEmpty() { - var createWorkEntryCommandParams = new CreateWorkEntryCommandParams + var createWorkEntryRequest = new CreateWorkEntryRequest { Title = "", StartTime = new DateTime(2025, 11, 24, 9, 0, 0), EndTime = new DateTime(2025, 11, 24, 10, 0, 0), TaskId = "#2231", Description = "Task description", - Type = EventType.Task }; - var response = await HttpClient.PostAsJsonAsync("/api/time/tracking/work-entries", createWorkEntryCommandParams); + var response = await HttpClient.PostAsJsonAsync("/api/time/tracking/work-entries", createWorkEntryRequest); Assert.NotNull(response); Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); From fcfce92fe24d708986b127fdea7138e830414020 Mon Sep 17 00:00:00 2001 From: Workflow Action Date: Thu, 18 Dec 2025 06:56:49 +0000 Subject: [PATCH 11/22] docs(readme): bring test coverage score up to date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8e02d02..3551ff5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # inner-circle-time-api -[![coverage](https://img.shields.io/badge/e2e_coverage-40.27%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/units_coverage-21.22%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/integration_coverage-70.81%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/full_coverage-95.86%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/e2e_coverage-40.31%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/units_coverage-21.24%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/integration_coverage-70.78%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/full_coverage-95.85%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) This repo contains Inner Circle Time API. From d994e0510a7e76d64d40ec137bbe06aaaa05b830 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:08:07 +0500 Subject: [PATCH 12/22] refactor: #8: remove ValidationConfigurations and move problemDetails configuration to Program.cs --- .../ValidationConfigurations.cs | 33 ------------------- Api/Program.cs | 25 ++++++++++++-- 2 files changed, 23 insertions(+), 35 deletions(-) delete mode 100644 Api/Configurations/ValidationConfigurations.cs diff --git a/Api/Configurations/ValidationConfigurations.cs b/Api/Configurations/ValidationConfigurations.cs deleted file mode 100644 index 67db30f..0000000 --- a/Api/Configurations/ValidationConfigurations.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Hellang.Middleware.ProblemDetails; -using Microsoft.AspNetCore.Mvc; - -namespace Api.Configurations; - -public static class ValidationConfiguration -{ - public static void ConfigureValidation(IServiceCollection services, IHostEnvironment environment) - { - services.AddProblemDetails(options => - { - options.IncludeExceptionDetails = (ctx, ex) => environment.IsDevelopment(); - }); - - services.AddControllers() - .ConfigureApiBehaviorOptions(options => - { - options.InvalidModelStateResponseFactory = context => - { - var problemDetails = new ValidationProblemDetails(context.ModelState) - { - Type = "https://example.com/validation-error", - Title = "Validation error", - Status = StatusCodes.Status400BadRequest, - Detail = "Fill in all the fields", - Instance = context.HttpContext.Request.Path - }; - - throw new ProblemDetailsException(problemDetails); - }; - }); - } -} diff --git a/Api/Program.cs b/Api/Program.cs index c7c43a6..cf2e0c1 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -1,6 +1,6 @@ -using Api.Configurations; using Application; using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.EntityFrameworkCore; using TourmalineCore.AspNetCore.JwtAuthentication.Core; @@ -36,7 +36,28 @@ public static void Main(string[] args) builder.Services.AddApplication(configuration); - ValidationConfiguration.ConfigureValidation(builder.Services, builder.Environment); + builder.Services.AddProblemDetails(options => + { + options.IncludeExceptionDetails = (ctx, ex) => builder.Environment.IsDevelopment(); + }); + + builder.Services.AddControllers() + .ConfigureApiBehaviorOptions(options => + { + options.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState) + { + Type = "https://example.com/validation-error", + Title = "Validation error", + Status = StatusCodes.Status400BadRequest, + Detail = "Fill in all the fields", + Instance = context.HttpContext.Request.Path + }; + + throw new ProblemDetailsException(problemDetails); + }; + }); var authenticationOptions = configuration.GetSection(nameof(AuthenticationOptions)).Get(); builder.Services.Configure(configuration.GetSection(nameof(AuthenticationOptions))); From a6126668775e123e67c142d9f15ecf2c9c947bfd Mon Sep 17 00:00:00 2001 From: Workflow Action Date: Thu, 18 Dec 2025 07:10:54 +0000 Subject: [PATCH 13/22] docs(readme): bring test coverage score up to date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3551ff5..f90b95f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # inner-circle-time-api -[![coverage](https://img.shields.io/badge/e2e_coverage-40.31%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/units_coverage-21.24%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/integration_coverage-70.78%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/full_coverage-95.85%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/e2e_coverage-40.12%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/units_coverage-21.31%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/integration_coverage-70.69%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/full_coverage-95.84%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) This repo contains Inner Circle Time API. From 172a664796a97edb8400b4fe7969cd16c272e3b7 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:08:54 +0500 Subject: [PATCH 14/22] refactor: extract database migration to separate method --- Api/Program.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Api/Program.cs b/Api/Program.cs index cf2e0c1..ee4f741 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -75,14 +75,18 @@ public static void Main(string[] args) options.RoutePrefix = "api/swagger"; }); - using (var serviceScope = app.Services.CreateScope()) - { - var context = serviceScope.ServiceProvider.GetRequiredService(); - context.Database.Migrate(); - } + MigrateDatabase(app.Services); app.MapControllers(); app.Run(); } + + private static void MigrateDatabase(IServiceProvider serviceProvider) + { + using var serviceScope = serviceProvider.CreateScope(); + + var context = serviceScope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); + } } From 504592eb00c5ccd794ee40b16e6065406bbc60ed Mon Sep 17 00:00:00 2001 From: Workflow Action Date: Thu, 18 Dec 2025 08:13:07 +0000 Subject: [PATCH 15/22] docs(readme): bring test coverage score up to date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f90b95f..6684e0b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # inner-circle-time-api -[![coverage](https://img.shields.io/badge/e2e_coverage-40.12%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/units_coverage-21.31%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/integration_coverage-70.69%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/full_coverage-95.84%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/e2e_coverage-40.19%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/units_coverage-21.29%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/integration_coverage-70.72%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/full_coverage-95.85%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) This repo contains Inner Circle Time API. From 1f2620b4260b95efdf13eac01acd4b575d6830f5 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:15:46 +0500 Subject: [PATCH 16/22] refactor: remove Type field from problem details object --- Api/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Api/Program.cs b/Api/Program.cs index ee4f741..68c4179 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -48,7 +48,6 @@ public static void Main(string[] args) { var problemDetails = new ValidationProblemDetails(context.ModelState) { - Type = "https://example.com/validation-error", Title = "Validation error", Status = StatusCodes.Status400BadRequest, Detail = "Fill in all the fields", From ce171f4b1c44d77a5442ce46de0ada964fefc56f Mon Sep 17 00:00:00 2001 From: Workflow Action Date: Thu, 18 Dec 2025 09:18:34 +0000 Subject: [PATCH 17/22] docs(readme): bring test coverage score up to date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6684e0b..1638313 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # inner-circle-time-api -[![coverage](https://img.shields.io/badge/e2e_coverage-40.19%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/units_coverage-21.29%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/integration_coverage-70.72%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/full_coverage-95.85%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/e2e_coverage-40.23%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/units_coverage-21.31%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/integration_coverage-70.69%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/full_coverage-95.84%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) This repo contains Inner Circle Time API. From 5abd6034466b561adcca105b0b720f0fca9001b5 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:44:30 +0500 Subject: [PATCH 18/22] feat: add check that start time is not greater end time and create custom exception for it --- Api/Program.cs | 10 ++ Application/AppDbContext.cs | 12 ++- .../Commands/CreateWorkEntryCommand.cs | 49 +++++---- .../Commands/CreateWorkEntryCommandTests.cs | 27 +++++ .../Exceptions/InvalidTimeRangeException.cs | 6 ++ ...artTimeConstraintToWorkEntries.Designer.cs | 99 +++++++++++++++++++ ...terThanStartTimeConstraintToWorkEntries.cs | 27 +++++ .../Migrations/AppDbContextModelSnapshot.cs | 2 + 8 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 Application/Exceptions/InvalidTimeRangeException.cs create mode 100644 Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.Designer.cs create mode 100644 Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.cs diff --git a/Api/Program.cs b/Api/Program.cs index 68c4179..0c657ad 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -39,6 +39,16 @@ public static void Main(string[] args) builder.Services.AddProblemDetails(options => { options.IncludeExceptionDetails = (ctx, ex) => builder.Environment.IsDevelopment(); + + options.Map(ex => + { + return new ProblemDetails + { + Title = "Invalid time range", + Status = StatusCodes.Status400BadRequest, + Detail = ex.Message, + }; + }); }); builder.Services.AddControllers() diff --git a/Application/AppDbContext.cs b/Application/AppDbContext.cs index c22d60a..b28c70a 100644 --- a/Application/AppDbContext.cs +++ b/Application/AppDbContext.cs @@ -30,17 +30,23 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder .Entity() - .Property(e => e.StartTime) + .Property(p => p.StartTime) .HasColumnType("timestamp without time zone"); modelBuilder .Entity() - .Property(e => e.EndTime) + .Property(p => p.EndTime) .HasColumnType("timestamp without time zone"); modelBuilder .Entity() - .ToTable(b => b.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0")); + .ToTable(t => t.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0")); + + modelBuilder + .Entity() + .ToTable(t => t.HasCheckConstraint( + "ck_work_entries_end_time_is_greater_than_start_time", + "\"end_time\" > \"start_time\"")); base.OnModelCreating(modelBuilder); } diff --git a/Application/Commands/CreateWorkEntryCommand.cs b/Application/Commands/CreateWorkEntryCommand.cs index 723d7c7..5568104 100644 --- a/Application/Commands/CreateWorkEntryCommand.cs +++ b/Application/Commands/CreateWorkEntryCommand.cs @@ -1,4 +1,6 @@ using Core.Entities; +using Microsoft.EntityFrameworkCore; +using Npgsql; namespace Application.Commands; @@ -33,24 +35,35 @@ IClaimsProvider claimsProvider public async Task ExecuteAsync(CreateWorkEntryCommandParams createWorkEntryCommandParams) { - var workEntry = new WorkEntry + try { - TenantId = _claimsProvider.TenantId, - EmployeeId = _claimsProvider.EmployeeId, - Title = createWorkEntryCommandParams.Title, - StartTime = createWorkEntryCommandParams.StartTime, - EndTime = createWorkEntryCommandParams.EndTime, - TaskId = createWorkEntryCommandParams.TaskId, - Description = createWorkEntryCommandParams.Description, - Type = createWorkEntryCommandParams.Type, - }; - - await _context - .WorkEntries - .AddAsync(workEntry); - - await _context.SaveChangesAsync(); - - return workEntry.Id; + var workEntry = new WorkEntry + { + TenantId = _claimsProvider.TenantId, + EmployeeId = _claimsProvider.EmployeeId, + Title = createWorkEntryCommandParams.Title, + StartTime = createWorkEntryCommandParams.StartTime, + EndTime = createWorkEntryCommandParams.EndTime, + TaskId = createWorkEntryCommandParams.TaskId, + Description = createWorkEntryCommandParams.Description, + Type = createWorkEntryCommandParams.Type, + }; + + await _context + .WorkEntries + .AddAsync(workEntry); + + await _context.SaveChangesAsync(); + + return workEntry.Id; + } + catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx && + pgEx.ConstraintName == "ck_work_entries_end_time_is_greater_than_start_time") + { + throw new InvalidTimeRangeException( + "End time must be greater than start time", + ex + ); + } } } diff --git a/Application/Commands/CreateWorkEntryCommandTests.cs b/Application/Commands/CreateWorkEntryCommandTests.cs index 1e063ca..8f85f17 100644 --- a/Application/Commands/CreateWorkEntryCommandTests.cs +++ b/Application/Commands/CreateWorkEntryCommandTests.cs @@ -68,4 +68,31 @@ public async Task CreateWorkEntryAsync_ShouldThrowErrorIfTypeIsUnspecified() Assert.Contains("ck_work_entries_type_not_zero", ex.InnerException?.Message); } + + [Fact] + public async Task CreateWorkEntryAsync_ShouldThrowInvalidTimeRangeExceptionIfStartTimeIsGreaterEndTime() + { + var context = CreateTenantDbContext(); + + var mockClaimsProvider = GetMockClaimsProvider(); + + var createWorkEntryCommand = new CreateWorkEntryCommand(context, mockClaimsProvider); + + var createWorkEntryCommandParams = new CreateWorkEntryCommandParams + { + Title = "Task 1", + StartTime = new DateTime(2025, 11, 24, 11, 0, 0), + EndTime = new DateTime(2025, 11, 24, 10, 0, 0), + TaskId = "#2231", + Description = "Task description", + Type = EventType.Task + }; + + InvalidTimeRangeException ex = await Assert.ThrowsAsync( + async () => await createWorkEntryCommand.ExecuteAsync(createWorkEntryCommandParams) + ); + + Assert.Contains("ck_work_entries_end_time_is_greater_than_start_time", ex.InnerException?.InnerException?.Message); + Assert.Equal("End time must be greater than start time", ex.Message); + } } diff --git a/Application/Exceptions/InvalidTimeRangeException.cs b/Application/Exceptions/InvalidTimeRangeException.cs new file mode 100644 index 0000000..f97eee3 --- /dev/null +++ b/Application/Exceptions/InvalidTimeRangeException.cs @@ -0,0 +1,6 @@ +public class InvalidTimeRangeException : Exception +{ + public InvalidTimeRangeException() { } + public InvalidTimeRangeException(string message) : base(message) { } + public InvalidTimeRangeException(string message, Exception inner) : base(message, inner) { } +} diff --git a/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.Designer.cs b/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.Designer.cs new file mode 100644 index 0000000..f04ee64 --- /dev/null +++ b/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.Designer.cs @@ -0,0 +1,99 @@ +// +using System; +using Application; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Application.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries")] + partial class AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Core.Entities.WorkEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Duration") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("interval") + .HasColumnName("duration") + .HasComputedColumnSql("end_time - start_time", true); + + b.Property("EmployeeId") + .HasColumnType("bigint") + .HasColumnName("employee_id"); + + b.Property("EndTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("end_time"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("StartTime") + .HasColumnType("timestamp without time zone") + .HasColumnName("start_time"); + + b.Property("TaskId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("task_id"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasColumnName("tenant_id"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_work_entries"); + + b.ToTable("work_entries", null, t => + { + t.HasCheckConstraint("ck_work_entries_end_time_is_greater_than_start_time", "\"end_time\" > \"start_time\""); + + t.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.cs b/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.cs new file mode 100644 index 0000000..9b7511c --- /dev/null +++ b/Application/Migrations/20251219093235_AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Application.Migrations +{ + /// + public partial class AddEndTimeIsGreaterThanStartTimeConstraintToWorkEntries : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddCheckConstraint( + name: "ck_work_entries_end_time_is_greater_than_start_time", + table: "work_entries", + sql: "\"end_time\" > \"start_time\""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropCheckConstraint( + name: "ck_work_entries_end_time_is_greater_than_start_time", + table: "work_entries"); + } + } +} diff --git a/Application/Migrations/AppDbContextModelSnapshot.cs b/Application/Migrations/AppDbContextModelSnapshot.cs index 185b764..2d9a4d5 100644 --- a/Application/Migrations/AppDbContextModelSnapshot.cs +++ b/Application/Migrations/AppDbContextModelSnapshot.cs @@ -85,6 +85,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("work_entries", null, t => { + t.HasCheckConstraint("ck_work_entries_end_time_is_greater_than_start_time", "\"end_time\" > \"start_time\""); + t.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0"); }); }); From 469f93cf00694279a2902484922af631e13a78cf Mon Sep 17 00:00:00 2001 From: Workflow Action Date: Fri, 19 Dec 2025 09:47:05 +0000 Subject: [PATCH 19/22] docs(readme): bring test coverage score up to date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1638313..3be1f8f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # inner-circle-time-api -[![coverage](https://img.shields.io/badge/e2e_coverage-40.23%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/units_coverage-21.31%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/integration_coverage-70.69%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/full_coverage-95.84%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/e2e_coverage-36.29%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/units_coverage-19.20%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/integration_coverage-72.85%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/full_coverage-94.97%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) This repo contains Inner Circle Time API. From 654d890b403aaf56e2c0fca1ed334c19e37dfb66 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:51:57 +0500 Subject: [PATCH 20/22] refactor: change lambda expression parameter name --- Application/AppDbContext.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Application/AppDbContext.cs b/Application/AppDbContext.cs index b28c70a..51f3477 100644 --- a/Application/AppDbContext.cs +++ b/Application/AppDbContext.cs @@ -25,26 +25,26 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder .Entity() - .Property(p => p.Duration) + .Property(entry => entry.Duration) .HasComputedColumnSql("end_time - start_time", stored: true); modelBuilder .Entity() - .Property(p => p.StartTime) + .Property(entry => entry.StartTime) .HasColumnType("timestamp without time zone"); modelBuilder .Entity() - .Property(p => p.EndTime) + .Property(entry => entry.EndTime) .HasColumnType("timestamp without time zone"); modelBuilder .Entity() - .ToTable(t => t.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0")); + .ToTable(table => table.HasCheckConstraint("ck_work_entries_type_not_zero", "\"type\" <> 0")); modelBuilder .Entity() - .ToTable(t => t.HasCheckConstraint( + .ToTable(table => table.HasCheckConstraint( "ck_work_entries_end_time_is_greater_than_start_time", "\"end_time\" > \"start_time\"")); From d97bbf06b813441aaffac6766a7a52ffcf8a7d06 Mon Sep 17 00:00:00 2001 From: Dmitriy Myakotin <75628188+MDI74@users.noreply.github.com> Date: Sat, 20 Dec 2025 09:45:35 +0500 Subject: [PATCH 21/22] feat: add handling an error where the start time cannot be greater than the end time in the UpdateWorkEntryCommand --- .../Commands/UpdateWorkEntryCommand.cs | 33 ++++++++++------ .../Commands/UpdateWorkEntryCommandTests.cs | 39 +++++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/Application/Commands/UpdateWorkEntryCommand.cs b/Application/Commands/UpdateWorkEntryCommand.cs index d8f7e8f..5339e97 100644 --- a/Application/Commands/UpdateWorkEntryCommand.cs +++ b/Application/Commands/UpdateWorkEntryCommand.cs @@ -1,5 +1,6 @@ using Core.Entities; using Microsoft.EntityFrameworkCore; +using Npgsql; namespace Application.Commands; @@ -36,17 +37,27 @@ IClaimsProvider claimsProvider public async Task ExecuteAsync(UpdateWorkEntryCommandParams updateWorkEntryCommandParams) { - await _context - .QueryableWithinTenant() - .Where(x => x.EmployeeId == _claimsProvider.EmployeeId) - .Where(x => x.Id == updateWorkEntryCommandParams.Id) - .ExecuteUpdateAsync(setters => setters - .SetProperty(x => x.Title, updateWorkEntryCommandParams.Title) - .SetProperty(x => x.StartTime, updateWorkEntryCommandParams.StartTime) - .SetProperty(x => x.EndTime, updateWorkEntryCommandParams.EndTime) - .SetProperty(x => x.TaskId, updateWorkEntryCommandParams.TaskId) - .SetProperty(x => x.Description, updateWorkEntryCommandParams.Description) - .SetProperty(x => x.Type, updateWorkEntryCommandParams.Type) + try + { + await _context + .QueryableWithinTenant() + .Where(x => x.EmployeeId == _claimsProvider.EmployeeId) + .Where(x => x.Id == updateWorkEntryCommandParams.Id) + .ExecuteUpdateAsync(setters => setters + .SetProperty(x => x.Title, updateWorkEntryCommandParams.Title) + .SetProperty(x => x.StartTime, updateWorkEntryCommandParams.StartTime) + .SetProperty(x => x.EndTime, updateWorkEntryCommandParams.EndTime) + .SetProperty(x => x.TaskId, updateWorkEntryCommandParams.TaskId) + .SetProperty(x => x.Description, updateWorkEntryCommandParams.Description) + .SetProperty(x => x.Type, updateWorkEntryCommandParams.Type) + ); + } + catch (PostgresException pgEx) when (pgEx.ConstraintName == "ck_work_entries_end_time_is_greater_than_start_time") + { + throw new InvalidTimeRangeException( + "End time must be greater than start time", + pgEx ); + } } } diff --git a/Application/Commands/UpdateWorkEntryCommandTests.cs b/Application/Commands/UpdateWorkEntryCommandTests.cs index 5b96a3f..a29c955 100644 --- a/Application/Commands/UpdateWorkEntryCommandTests.cs +++ b/Application/Commands/UpdateWorkEntryCommandTests.cs @@ -51,4 +51,43 @@ public async Task UpdateWorkEntryAsync_ShouldUpdateWorkEntryDataInDb() Assert.Equal(updateWorkEntryCommandParams.Description, updatedWorkEntry.Description); Assert.Equal(updateWorkEntryCommandParams.EndTime - updateWorkEntryCommandParams.StartTime, updatedWorkEntry.Duration); } + + [Fact] + public async Task UpdateWorkEntryAsync_ShouldThrowInvalidTimeRangeExceptionIfStartTimeIsGreaterEndTime() + { + var context = CreateTenantDbContext(); + + var mockClaimsProvider = GetMockClaimsProvider(); + + var updateWorkEntryCommand = new UpdateWorkEntryCommand(context, mockClaimsProvider); + + var workEntry = await SaveEntityAsync(context, new WorkEntry + { + EmployeeId = EMPLOYEE_ID, + Title = "Task 1", + StartTime = new DateTime(2025, 11, 24, 9, 0, 0), + EndTime = new DateTime(2025, 11, 24, 10, 0, 0), + TaskId = "#2231", + Description = "Task description", + Type = EventType.Task + }); + + var updateWorkEntryCommandParams = new UpdateWorkEntryCommandParams + { + Id = workEntry.Id, + Title = "Task 2", + StartTime = new DateTime(2025, 11, 25, 12, 0, 0), + EndTime = new DateTime(2025, 11, 25, 11, 0, 0), + TaskId = "#22", + Description = "Task description", + Type = EventType.Task + }; + + InvalidTimeRangeException ex = await Assert.ThrowsAsync( + async () => await updateWorkEntryCommand.ExecuteAsync(updateWorkEntryCommandParams) + ); + + Assert.Contains("ck_work_entries_end_time_is_greater_than_start_time", ex.InnerException?.Message); + Assert.Equal("End time must be greater than start time", ex.Message); + } } From e5a1a13faf5746e55fdc57792d0e266a4f6cdf7d Mon Sep 17 00:00:00 2001 From: Workflow Action Date: Sat, 20 Dec 2025 04:48:11 +0000 Subject: [PATCH 22/22] docs(readme): bring test coverage score up to date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3be1f8f..37ca56c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # inner-circle-time-api -[![coverage](https://img.shields.io/badge/e2e_coverage-36.29%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/units_coverage-19.20%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/integration_coverage-72.85%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) -[![coverage](https://img.shields.io/badge/full_coverage-94.97%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/e2e_coverage-35.25%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/units_coverage-18.55%25-crimson)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/integration_coverage-73.76%25-yellow)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) +[![coverage](https://img.shields.io/badge/full_coverage-95.14%25-forestgreen)](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml) This repo contains Inner Circle Time API.