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
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](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
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](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
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](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
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](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
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](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
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](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
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](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
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
-[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
+[](https://github.com/TourmalineCore/inner-circle-time-api/actions/workflows/calculate-tests-coverage-on-pull-request.yml)
This repo contains Inner Circle Time API.