From 1a908840f812a48a7d3201ef94e485c44fc805b3 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Mon, 3 Aug 2026 23:50:20 +0100
Subject: [PATCH 01/11] implement DELETE
---
Directory.Packages.props | 1 -
.../Endpoints/MedicationEndpoints.cs | 41 +++++++++++++++----
src/Medications.Api/Medications.Api.csproj | 1 +
src/Medications.Api/Program.cs | 2 +
src/Medications.AppHost/AppHost.cs | 15 +------
.../Medications.AppHost.csproj | 4 --
6 files changed, 39 insertions(+), 25 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d5b8811..9c2bc78 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,7 +6,6 @@
-
\ No newline at end of file
diff --git a/src/Medications.Api/Endpoints/MedicationEndpoints.cs b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
index 279d1f6..06fdb22 100644
--- a/src/Medications.Api/Endpoints/MedicationEndpoints.cs
+++ b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
@@ -8,27 +8,39 @@ namespace Medications.Api.Endpoints;
public static class MedicationEndpoints
{
+ private const string GetMedicationsEndpoint = "GetMedications";
+ private const string GetMedicationEndpoint = "GetMedication";
+ private const string CreateMedicationEndpoint = "CreateMedication";
+ private const string DeleteMedicationEndpoint = "CreateMedication";
+
+
public static IEndpointRouteBuilder MapMedicationEndpoints(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/medications")
.WithTags("Medications");
- group.MapGet("/", GetAllMedications)
- .WithName("GetMedications")
+ group.MapGet("/", GetMedications)
+ .WithName(GetMedicationsEndpoint)
.WithDescription("Retrieve all medications.");
group.MapGet("/{id}", GetMedication)
- .WithName("GetMedication")
+ .WithName(GetMedicationEndpoint)
.WithDescription("Retrieve a specific medication by its ID.");
group.MapPost("/", CreateMedication)
- .WithName("CreateMedication")
+ .WithName(CreateMedicationEndpoint)
.WithDescription("Create a new medication");
+ group.MapDelete("/{id}", DeleteMedication)
+ .WithName(DeleteMedicationEndpoint)
+ .WithDescription("Delete a medication");
+
return endpoints;
}
- private static async Task>> GetAllMedications(MedicationsDbContext dbContext)
+ #region Private Methods
+
+ private static async Task>> GetMedications(MedicationsDbContext dbContext)
{
var medications = await dbContext.Medications.ToListAsync();
@@ -38,9 +50,9 @@ private static async Task>> GetAllMedications(Medica
}
private static async Task, NotFound>> GetMedication(
- int id, MedicationsDbContext dbContext)
+ int id, MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
- var medication = await dbContext.Medications.FindAsync(id);
+ var medication = await dbContext.Medications.FindAsync(id, cancellationToken);
if (medication is null) return TypedResults.NotFound();
@@ -63,4 +75,19 @@ private static async Task> CreateMedication(
return TypedResults.CreatedAtRoute(medicationResponse, "GetMedication", new { id = medicationResponse.Id });
}
+
+ private static async Task> DeleteMedication(
+ int id, MedicationsDbContext dbContext, CancellationToken cancellationToken)
+ {
+ var medication = await dbContext.Medications.FindAsync(id, cancellationToken);
+
+ if (medication is null) return TypedResults.NotFound();
+
+ dbContext.Medications.Remove(medication);
+ await dbContext.SaveChangesAsync(cancellationToken);
+ return TypedResults.NoContent();
+ }
+
+ #endregion
+
}
diff --git a/src/Medications.Api/Medications.Api.csproj b/src/Medications.Api/Medications.Api.csproj
index 08df76d..8cfa09a 100644
--- a/src/Medications.Api/Medications.Api.csproj
+++ b/src/Medications.Api/Medications.Api.csproj
@@ -4,6 +4,7 @@
+
diff --git a/src/Medications.Api/Program.cs b/src/Medications.Api/Program.cs
index e4d51d5..231de75 100644
--- a/src/Medications.Api/Program.cs
+++ b/src/Medications.Api/Program.cs
@@ -1,6 +1,7 @@
using Medications.Api.Data;
using Medications.Api.Endpoints;
using Microsoft.EntityFrameworkCore;
+using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("Medications"));
@@ -13,6 +14,7 @@
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
+ app.MapScalarApiReference();
}
app.UseHttpsRedirection();
diff --git a/src/Medications.AppHost/AppHost.cs b/src/Medications.AppHost/AppHost.cs
index 6f0f455..e602bb6 100644
--- a/src/Medications.AppHost/AppHost.cs
+++ b/src/Medications.AppHost/AppHost.cs
@@ -1,19 +1,8 @@
using Projects;
-using Scalar.Aspire;
var builder = DistributedApplication.CreateBuilder(args);
-var apiService = builder.AddProject("medications-api");
-
-var scalar = builder.AddScalarApiReference("scalar", options =>
-{
- options
- .WithTheme(ScalarTheme.Default)
- .PreferHttpsEndpoint()
- .AllowSelfSignedCertificates();
-});
-
-scalar
- .WithApiReference(apiService);
+builder.AddProject("medications-api")
+ .WithUrlForEndpoint("https", url => new() { Url = "/scalar", DisplayText = "Scalar UI", DisplayOrder = 100 });
await builder.Build().RunAsync();
diff --git a/src/Medications.AppHost/Medications.AppHost.csproj b/src/Medications.AppHost/Medications.AppHost.csproj
index 706261d..185a886 100644
--- a/src/Medications.AppHost/Medications.AppHost.csproj
+++ b/src/Medications.AppHost/Medications.AppHost.csproj
@@ -9,8 +9,4 @@
-
-
-
-
From 8baea684958309ca20a43dae91efda19734c2a67 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Tue, 4 Aug 2026 01:28:01 +0100
Subject: [PATCH 02/11] test project setup and first unit test for delete
method
---
Directory.Packages.props | 8 ++-
Medications.slnx | 7 ++-
.../Endpoints/MedicationEndpoints.cs | 12 ++---
src/Medications.Api/Medications.Api.csproj | 1 +
.../EndpointUnitTests.cs | 51 +++++++++++++++++++
.../Medications.Unit.Tests.csproj | 33 ++++++++++++
6 files changed, 103 insertions(+), 9 deletions(-)
create mode 100644 tests/Medications.Unit.Tests/EndpointUnitTests.cs
create mode 100644 tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9c2bc78..f17537f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,9 +3,15 @@
true
+
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Medications.slnx b/Medications.slnx
index f4c7203..cc3840a 100644
--- a/Medications.slnx
+++ b/Medications.slnx
@@ -1,6 +1,9 @@
-
-
+
+
+
+
+
diff --git a/src/Medications.Api/Endpoints/MedicationEndpoints.cs b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
index 06fdb22..468892b 100644
--- a/src/Medications.Api/Endpoints/MedicationEndpoints.cs
+++ b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
@@ -11,7 +11,7 @@ public static class MedicationEndpoints
private const string GetMedicationsEndpoint = "GetMedications";
private const string GetMedicationEndpoint = "GetMedication";
private const string CreateMedicationEndpoint = "CreateMedication";
- private const string DeleteMedicationEndpoint = "CreateMedication";
+ private const string DeleteMedicationEndpoint = "DeleteMedication";
public static IEndpointRouteBuilder MapMedicationEndpoints(this IEndpointRouteBuilder endpoints)
@@ -38,9 +38,9 @@ public static IEndpointRouteBuilder MapMedicationEndpoints(this IEndpointRouteBu
return endpoints;
}
- #region Private Methods
+ #region Internal Methods
- private static async Task>> GetMedications(MedicationsDbContext dbContext)
+ internal static async Task>> GetMedications(MedicationsDbContext dbContext)
{
var medications = await dbContext.Medications.ToListAsync();
@@ -49,7 +49,7 @@ private static async Task>> GetMedications(Medicatio
return TypedResults.Ok(medicationResponseList);
}
- private static async Task, NotFound>> GetMedication(
+ internal static async Task, NotFound>> GetMedication(
int id, MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
var medication = await dbContext.Medications.FindAsync(id, cancellationToken);
@@ -59,7 +59,7 @@ private static async Task, NotFound>> GetMedicati
return TypedResults.Ok(medication.ToResponse());
}
- private static async Task> CreateMedication(
+ internal static async Task> CreateMedication(
CreateMedicationRequest request,
MedicationsDbContext dbContext,
TimeProvider timeProvider,
@@ -76,7 +76,7 @@ private static async Task> CreateMedication(
return TypedResults.CreatedAtRoute(medicationResponse, "GetMedication", new { id = medicationResponse.Id });
}
- private static async Task> DeleteMedication(
+ internal static async Task> DeleteMedication(
int id, MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
var medication = await dbContext.Medications.FindAsync(id, cancellationToken);
diff --git a/src/Medications.Api/Medications.Api.csproj b/src/Medications.Api/Medications.Api.csproj
index 8cfa09a..da4566d 100644
--- a/src/Medications.Api/Medications.Api.csproj
+++ b/src/Medications.Api/Medications.Api.csproj
@@ -5,6 +5,7 @@
+
diff --git a/tests/Medications.Unit.Tests/EndpointUnitTests.cs b/tests/Medications.Unit.Tests/EndpointUnitTests.cs
new file mode 100644
index 0000000..2812b69
--- /dev/null
+++ b/tests/Medications.Unit.Tests/EndpointUnitTests.cs
@@ -0,0 +1,51 @@
+using Medications.Api.Data;
+using Medications.Api.Endpoints;
+using Medications.Api.Entities;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.EntityFrameworkCore;
+
+namespace Medications.Unit.Tests
+{
+ public class EndpointUnitTests
+ {
+ [Fact]
+ public async Task DeleteMedicationById_WhenMedicationExists_ReturnsNoContent()
+ {
+ var medication1 = new Medication
+ {
+ Id = 1,
+ Name = "Test Medication",
+ Quantity = 10,
+ CreationDate = DateTimeOffset.UtcNow
+ };
+
+ var medication2 = new Medication
+ {
+ Id = 2,
+ Name = "Test Medication 2",
+ Quantity = 20,
+ CreationDate = DateTimeOffset.UtcNow
+ };
+
+ var dbContextOptions = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
+
+ var dbContext = new MedicationsDbContext(dbContextOptions);
+
+
+ dbContext.Medications.Add(medication1);
+ dbContext.Medications.Add(medication2);
+ dbContext.SaveChanges();
+
+ var result = await MedicationEndpoints.DeleteMedication(1, dbContext, CancellationToken.None);
+
+ var value1 = dbContext.Medications.Find(1);
+ var value2 = dbContext.Medications.Find(2);
+
+
+ Assert.IsType(result.Result);
+ Assert.Null(value1);
+ Assert.NotNull(value2);
+ }
+ }
+}
diff --git a/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj b/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
new file mode 100644
index 0000000..4b9167b
--- /dev/null
+++ b/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 274e08021f26c0c682fa4ef77e50e63bb2d79a8c Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Tue, 4 Aug 2026 01:40:34 +0100
Subject: [PATCH 03/11] cleanup
---
Directory.Packages.props | 2 --
src/Medications.Api/Endpoints/MedicationEndpoints.cs | 12 +++---------
.../Medications.Unit.Tests.csproj | 2 --
3 files changed, 3 insertions(+), 13 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f17537f..e703b56 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,11 +3,9 @@
true
-
-
diff --git a/src/Medications.Api/Endpoints/MedicationEndpoints.cs b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
index 468892b..78dfe76 100644
--- a/src/Medications.Api/Endpoints/MedicationEndpoints.cs
+++ b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
@@ -13,7 +13,6 @@ public static class MedicationEndpoints
private const string CreateMedicationEndpoint = "CreateMedication";
private const string DeleteMedicationEndpoint = "DeleteMedication";
-
public static IEndpointRouteBuilder MapMedicationEndpoints(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/medications")
@@ -38,11 +37,9 @@ public static IEndpointRouteBuilder MapMedicationEndpoints(this IEndpointRouteBu
return endpoints;
}
- #region Internal Methods
-
- internal static async Task>> GetMedications(MedicationsDbContext dbContext)
+ internal static async Task>> GetMedications(MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
- var medications = await dbContext.Medications.ToListAsync();
+ var medications = await dbContext.Medications.ToListAsync(cancellationToken);
var medicationResponseList = medications.Select(medication => medication.ToResponse()).ToList();
@@ -73,7 +70,7 @@ internal static async Task> CreateMedication(
var medicationResponse = medication.ToResponse();
- return TypedResults.CreatedAtRoute(medicationResponse, "GetMedication", new { id = medicationResponse.Id });
+ return TypedResults.CreatedAtRoute(medicationResponse, GetMedicationEndpoint, new { id = medicationResponse.Id });
}
internal static async Task> DeleteMedication(
@@ -87,7 +84,4 @@ internal static async Task> DeleteMedication(
await dbContext.SaveChangesAsync(cancellationToken);
return TypedResults.NoContent();
}
-
- #endregion
-
}
diff --git a/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj b/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
index 4b9167b..e77a9b8 100644
--- a/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
+++ b/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
@@ -12,9 +12,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
From fba67cc87a6c092bd5cf6a7d7a2d73c3364da642 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Tue, 4 Aug 2026 03:35:15 +0100
Subject: [PATCH 04/11] improvements
---
.../Endpoints/MedicationEndpoints.cs | 44 ++++++----
src/Medications.Api/Medications.Api.csproj | 3 +
src/Medications.Api/Program.cs | 18 +++++
.../EndpointUnitTests.cs | 80 ++++++++++++-------
4 files changed, 100 insertions(+), 45 deletions(-)
diff --git a/src/Medications.Api/Endpoints/MedicationEndpoints.cs b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
index 78dfe76..d22e3b5 100644
--- a/src/Medications.Api/Endpoints/MedicationEndpoints.cs
+++ b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
@@ -8,31 +8,36 @@ namespace Medications.Api.Endpoints;
public static class MedicationEndpoints
{
- private const string GetMedicationsEndpoint = "GetMedications";
- private const string GetMedicationEndpoint = "GetMedication";
- private const string CreateMedicationEndpoint = "CreateMedication";
- private const string DeleteMedicationEndpoint = "DeleteMedication";
+ private const string RouteGroupPrefix = "/api/medications";
+
+ private const string GetMedicationsEndpointName = "GetMedications";
+ private const string GetMedicationEndpointName = "GetMedication";
+ private const string CreateMedicationEndpointName = "CreateMedication";
+ private const string DeleteMedicationEndpointName = "DeleteMedication";
public static IEndpointRouteBuilder MapMedicationEndpoints(this IEndpointRouteBuilder endpoints)
{
- var group = endpoints.MapGroup("/api/medications")
+ var group = endpoints.MapGroup(RouteGroupPrefix)
.WithTags("Medications");
group.MapGet("/", GetMedications)
- .WithName(GetMedicationsEndpoint)
+ .WithName(GetMedicationsEndpointName)
.WithDescription("Retrieve all medications.");
group.MapGet("/{id}", GetMedication)
- .WithName(GetMedicationEndpoint)
- .WithDescription("Retrieve a specific medication by its ID.");
+ .WithName(GetMedicationEndpointName)
+ .WithDescription("Retrieve a specific medication by its ID.")
+ .ProducesProblem(StatusCodes.Status400BadRequest);
group.MapPost("/", CreateMedication)
- .WithName(CreateMedicationEndpoint)
- .WithDescription("Create a new medication");
+ .WithName(CreateMedicationEndpointName)
+ .WithDescription("Create a new medication")
+ .ProducesValidationProblem();
group.MapDelete("/{id}", DeleteMedication)
- .WithName(DeleteMedicationEndpoint)
- .WithDescription("Delete a medication");
+ .WithName(DeleteMedicationEndpointName)
+ .WithDescription("Delete a medication")
+ .ProducesProblem(StatusCodes.Status400BadRequest);
return endpoints;
}
@@ -46,9 +51,11 @@ internal static async Task>> GetMedications(Medicati
return TypedResults.Ok(medicationResponseList);
}
- internal static async Task, NotFound>> GetMedication(
+ internal static async Task, NotFound, ProblemHttpResult>> GetMedication(
int id, MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
+ if (id <= 0) return InvalidId();
+
var medication = await dbContext.Medications.FindAsync(id, cancellationToken);
if (medication is null) return TypedResults.NotFound();
@@ -70,12 +77,14 @@ internal static async Task> CreateMedication(
var medicationResponse = medication.ToResponse();
- return TypedResults.CreatedAtRoute(medicationResponse, GetMedicationEndpoint, new { id = medicationResponse.Id });
+ return TypedResults.CreatedAtRoute(medicationResponse, GetMedicationEndpointName, new { id = medicationResponse.Id });
}
- internal static async Task> DeleteMedication(
+ internal static async Task> DeleteMedication(
int id, MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
+ if (id <= 0) return InvalidId();
+
var medication = await dbContext.Medications.FindAsync(id, cancellationToken);
if (medication is null) return TypedResults.NotFound();
@@ -84,4 +93,9 @@ internal static async Task> DeleteMedication(
await dbContext.SaveChangesAsync(cancellationToken);
return TypedResults.NoContent();
}
+
+ private static ProblemHttpResult InvalidId() =>
+ TypedResults.Problem(
+ detail: "Id must be greater than zero.",
+ statusCode: StatusCodes.Status400BadRequest);
}
diff --git a/src/Medications.Api/Medications.Api.csproj b/src/Medications.Api/Medications.Api.csproj
index da4566d..043f62a 100644
--- a/src/Medications.Api/Medications.Api.csproj
+++ b/src/Medications.Api/Medications.Api.csproj
@@ -5,6 +5,9 @@
+
+
+
diff --git a/src/Medications.Api/Program.cs b/src/Medications.Api/Program.cs
index 231de75..1f6c836 100644
--- a/src/Medications.Api/Program.cs
+++ b/src/Medications.Api/Program.cs
@@ -8,6 +8,16 @@
builder.Services.AddOpenApi();
builder.Services.AddValidation();
builder.Services.AddSingleton(TimeProvider.System);
+builder.Services.AddProblemDetails(options =>
+{
+ options.CustomizeProblemDetails = context =>
+ {
+ if (context.Exception is BadHttpRequestException badRequest)
+ {
+ context.ProblemDetails.Detail ??= badRequest.Message;
+ }
+ };
+});
var app = builder.Build();
@@ -17,6 +27,14 @@
app.MapScalarApiReference();
}
+app.UseExceptionHandler(new ExceptionHandlerOptions
+{
+ StatusCodeSelector = ex => ex is BadHttpRequestException badRequest
+ ? badRequest.StatusCode
+ : StatusCodes.Status500InternalServerError
+});
+app.UseStatusCodePages();
+
app.UseHttpsRedirection();
app.MapMedicationEndpoints();
diff --git a/tests/Medications.Unit.Tests/EndpointUnitTests.cs b/tests/Medications.Unit.Tests/EndpointUnitTests.cs
index 2812b69..b9768f0 100644
--- a/tests/Medications.Unit.Tests/EndpointUnitTests.cs
+++ b/tests/Medications.Unit.Tests/EndpointUnitTests.cs
@@ -1,6 +1,7 @@
using Medications.Api.Data;
using Medications.Api.Endpoints;
using Medications.Api.Entities;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
@@ -11,41 +12,60 @@ public class EndpointUnitTests
[Fact]
public async Task DeleteMedicationById_WhenMedicationExists_ReturnsNoContent()
{
- var medication1 = new Medication
- {
- Id = 1,
- Name = "Test Medication",
- Quantity = 10,
- CreationDate = DateTimeOffset.UtcNow
- };
-
- var medication2 = new Medication
- {
- Id = 2,
- Name = "Test Medication 2",
- Quantity = 20,
- CreationDate = DateTimeOffset.UtcNow
- };
-
- var dbContextOptions = new DbContextOptionsBuilder()
- .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
-
- var dbContext = new MedicationsDbContext(dbContextOptions);
-
-
- dbContext.Medications.Add(medication1);
- dbContext.Medications.Add(medication2);
- dbContext.SaveChanges();
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
var result = await MedicationEndpoints.DeleteMedication(1, dbContext, CancellationToken.None);
- var value1 = dbContext.Medications.Find(1);
- var value2 = dbContext.Medications.Find(2);
+ Assert.IsType(result.Result);
+ Assert.Null(dbContext.Medications.Find(1));
+ Assert.NotNull(dbContext.Medications.Find(2));
+ }
+ [Fact]
+ public async Task DeleteMedicationById_WhenMedicationDoesNotExist_ReturnsNotFound()
+ {
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
- Assert.IsType(result.Result);
- Assert.Null(value1);
- Assert.NotNull(value2);
+ var result = await MedicationEndpoints.DeleteMedication(3, dbContext, CancellationToken.None);
+
+ Assert.IsType(result.Result);
+ Assert.NotNull(dbContext.Medications.Find(1));
+ Assert.NotNull(dbContext.Medications.Find(2));
+ }
+
+ [Fact]
+ public async Task DeleteMedicationById_WhenIdIsNegative_ReturnsBadRequest()
+ {
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
+
+ var result = await MedicationEndpoints.DeleteMedication(-1, dbContext, CancellationToken.None);
+
+ var problem = Assert.IsType(result.Result);
+ Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
+ Assert.NotNull(dbContext.Medications.Find(1));
+ Assert.NotNull(dbContext.Medications.Find(2));
}
+
+ private static MedicationsDbContext CreateContext(params Medication[] seed)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+
+ var dbContext = new MedicationsDbContext(options);
+
+ dbContext.Medications.AddRange(seed);
+ dbContext.SaveChanges();
+
+ return dbContext;
+ }
+
+ private static Medication NewMedication(int id) => new()
+ {
+ Id = id,
+ Name = $"Test Medication {id}",
+ Quantity = 10 * id,
+ CreationDate = DateTimeOffset.UtcNow
+ };
}
}
From 21a7fc1f900f3b7d4953dc4a957a6e249a2e1034 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Tue, 4 Aug 2026 03:40:53 +0100
Subject: [PATCH 05/11] add tests to CI
---
.github/workflows/dotnet.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index b7c04c7..c3d15c6 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -24,5 +24,5 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- # - name: Test
- # run: dotnet test --no-build --verbosity normal
+ - name: Test
+ run: dotnet test --no-build --verbosity normal
From 1183de827ffd1dd810f83c7f7d72d6eefcdc3be6 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Tue, 4 Aug 2026 04:19:56 +0100
Subject: [PATCH 06/11] better CI with sonarCloud scan
---
.github/workflows/ci.yml | 62 ++++++++++++++++++++++++++++++++++++
.github/workflows/dotnet.yml | 28 ----------------
2 files changed, 62 insertions(+), 28 deletions(-)
create mode 100644 .github/workflows/ci.yml
delete mode 100644 .github/workflows/dotnet.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..873ef43
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,62 @@
+name: CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ workflow_dispatch:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ steps:
+ - uses: actions/checkout@v7.0.1
+ with:
+ fetch-depth: 0 # full history so Sonar can attribute new code via blame
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v6.0.0
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Set up JDK 21 for SonarQube analysis
+ if: env.SONAR_TOKEN != ''
+ uses: actions/setup-java@v5.7.0
+ with:
+ distribution: temurin
+ java-version: 21
+
+ - name: Cache SonarQube packages
+ if: env.SONAR_TOKEN != ''
+ uses: actions/cache@v6.1.0
+ with:
+ path: ~/.sonar/cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+
+ - name: Begin Sonar analysis
+ if: env.SONAR_TOKEN != ''
+ run: |
+ dotnet tool install --global dotnet-sonarscanner
+ dotnet-sonarscanner begin \
+ /k:"fernandotonacoder_medications-rest-api" \
+ /o:"fernandotonacoder" \
+ /d:sonar.token="$SONAR_TOKEN" \
+ /d:sonar.host.url="https://sonarcloud.io" \
+ /d:sonar.cs.cobertura.reportsPaths="**/coverage.cobertura.xml"
+
+ - name: Build
+ run: dotnet build --no-incremental
+
+ - name: Test
+ run: dotnet test --no-build --collect:"XPlat Code Coverage"
+
+ - name: End Sonar analysis
+ if: env.SONAR_TOKEN != ''
+ run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN"
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
deleted file mode 100644
index c3d15c6..0000000
--- a/.github/workflows/dotnet.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-# This workflow will build a .NET project
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
-
-name: .NET
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v7.0.1
- - name: Setup .NET
- uses: actions/setup-dotnet@v6.0.0
- with:
- dotnet-version: 10.0.x
- - name: Restore dependencies
- run: dotnet restore
- - name: Build
- run: dotnet build --no-restore
- - name: Test
- run: dotnet test --no-build --verbosity normal
From c2cca6ed38318bb245adcb08695618a60c53fc17 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Tue, 4 Aug 2026 04:30:54 +0100
Subject: [PATCH 07/11] address snarcloud detected issues
---
src/Medications.Api/Endpoints/MedicationEndpoints.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Medications.Api/Endpoints/MedicationEndpoints.cs b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
index d22e3b5..388435b 100644
--- a/src/Medications.Api/Endpoints/MedicationEndpoints.cs
+++ b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
@@ -56,7 +56,7 @@ internal static async Task, NotFound, ProblemHttp
{
if (id <= 0) return InvalidId();
- var medication = await dbContext.Medications.FindAsync(id, cancellationToken);
+ var medication = await dbContext.Medications.FindAsync([id], cancellationToken);
if (medication is null) return TypedResults.NotFound();
@@ -85,7 +85,7 @@ internal static async Task> Dele
{
if (id <= 0) return InvalidId();
- var medication = await dbContext.Medications.FindAsync(id, cancellationToken);
+ var medication = await dbContext.Medications.FindAsync([id], cancellationToken);
if (medication is null) return TypedResults.NotFound();
From e4ce65b6586b96f35e174405a8449e701e8eab58 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Tue, 4 Aug 2026 04:36:03 +0100
Subject: [PATCH 08/11] rename job
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 873ef43..a9fc611 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
jobs:
- build:
+ CI:
runs-on: ubuntu-latest
From 4ca1a5ab2bdd047c8e88c80a2ea96c733750a1cc Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Tue, 4 Aug 2026 04:38:45 +0100
Subject: [PATCH 09/11] job rename
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a9fc611..23b537a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
jobs:
- CI:
+ build-and-test:
runs-on: ubuntu-latest
From 23d41f09c787c378d5c5466e69aef6bb32f512a1 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Wed, 5 Aug 2026 01:53:52 +0100
Subject: [PATCH 10/11] tests
---
.github/workflows/ci.yml | 5 +-
Directory.Packages.props | 1 +
Medications.slnx | 1 +
.../Endpoints/MedicationEndpoints.cs | 3 +-
tests/.gitkeep | 0
.../CreateMedicationRequestUnitTests.cs | 43 +++++++++
...nitTests.cs => DeleteEndpointUnitTests.cs} | 18 ++--
.../GetAllEndpointUnitTests.cs | 62 +++++++++++++
.../GetByIdEndpointUnitTests.cs | 87 +++++++++++++++++++
.../MedicationMappingUnitTests.cs | 42 +++++++++
.../Medications.Unit.Tests.csproj | 4 +
.../PostEndpointUnitTests.cs | 51 +++++++++++
tests/test.runsettings | 38 ++++++++
13 files changed, 345 insertions(+), 10 deletions(-)
delete mode 100644 tests/.gitkeep
create mode 100644 tests/Medications.Unit.Tests/CreateMedicationRequestUnitTests.cs
rename tests/Medications.Unit.Tests/{EndpointUnitTests.cs => DeleteEndpointUnitTests.cs} (78%)
create mode 100644 tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs
create mode 100644 tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs
create mode 100644 tests/Medications.Unit.Tests/MedicationMappingUnitTests.cs
create mode 100644 tests/Medications.Unit.Tests/PostEndpointUnitTests.cs
create mode 100644 tests/test.runsettings
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 23b537a..fa63b4d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@v7.0.1
with:
- fetch-depth: 0 # full history so Sonar can attribute new code via blame
+ fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v6.0.0
@@ -55,7 +55,8 @@ jobs:
run: dotnet build --no-incremental
- name: Test
- run: dotnet test --no-build --collect:"XPlat Code Coverage"
+ # RunSettingsFilePath points at tests/test.runsettings in cobertura format
+ run: dotnet test --no-build
- name: End Sonar analysis
if: env.SONAR_TOKEN != ''
diff --git a/Directory.Packages.props b/Directory.Packages.props
index e703b56..008f955 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,6 +5,7 @@
+
diff --git a/Medications.slnx b/Medications.slnx
index cc3840a..ba4ce66 100644
--- a/Medications.slnx
+++ b/Medications.slnx
@@ -4,6 +4,7 @@
+
diff --git a/src/Medications.Api/Endpoints/MedicationEndpoints.cs b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
index 388435b..6e23302 100644
--- a/src/Medications.Api/Endpoints/MedicationEndpoints.cs
+++ b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
@@ -42,7 +42,8 @@ public static IEndpointRouteBuilder MapMedicationEndpoints(this IEndpointRouteBu
return endpoints;
}
- internal static async Task>> GetMedications(MedicationsDbContext dbContext, CancellationToken cancellationToken)
+ internal static async Task>> GetMedications(
+ MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
var medications = await dbContext.Medications.ToListAsync(cancellationToken);
diff --git a/tests/.gitkeep b/tests/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/Medications.Unit.Tests/CreateMedicationRequestUnitTests.cs b/tests/Medications.Unit.Tests/CreateMedicationRequestUnitTests.cs
new file mode 100644
index 0000000..336bb79
--- /dev/null
+++ b/tests/Medications.Unit.Tests/CreateMedicationRequestUnitTests.cs
@@ -0,0 +1,43 @@
+using Medications.Api.Contracts;
+using System.ComponentModel.DataAnnotations;
+
+namespace Medications.Unit.Tests
+{
+ public class CreateMedicationRequestUnitTests
+ {
+ [Fact]
+ public void Name_WhenEmptyString_IsInvalid()
+ {
+ var results = Validate(new CreateMedicationRequest { Name = "", Quantity = 10 });
+
+ var error = Assert.Single(results);
+ Assert.Equal("The Name field is required.", error.ErrorMessage);
+ Assert.Contains(nameof(CreateMedicationRequest.Name), error.MemberNames);
+ Assert.DoesNotContain(nameof(CreateMedicationRequest.Quantity), error.MemberNames);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ public void Quantity_WhenNotGreaterThanZero_IsInvalid(int quantity)
+ {
+ var results = Validate(new CreateMedicationRequest { Name = "Aspirin", Quantity = quantity });
+
+ var error = Assert.Single(results);
+ Assert.Equal("Quantity must be greater than 0", error.ErrorMessage);
+ Assert.DoesNotContain(nameof(CreateMedicationRequest.Name), error.MemberNames);
+ Assert.Contains(nameof(CreateMedicationRequest.Quantity), error.MemberNames);
+ }
+
+ private static List Validate(CreateMedicationRequest request)
+ {
+ var results = new List();
+ Validator.TryValidateObject(
+ request,
+ new ValidationContext(request),
+ results,
+ validateAllProperties: true);
+ return results;
+ }
+ }
+}
diff --git a/tests/Medications.Unit.Tests/EndpointUnitTests.cs b/tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs
similarity index 78%
rename from tests/Medications.Unit.Tests/EndpointUnitTests.cs
rename to tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs
index b9768f0..376516d 100644
--- a/tests/Medications.Unit.Tests/EndpointUnitTests.cs
+++ b/tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs
@@ -7,10 +7,10 @@
namespace Medications.Unit.Tests
{
- public class EndpointUnitTests
+ public class DeleteEndpointUnitTests
{
[Fact]
- public async Task DeleteMedicationById_WhenMedicationExists_ReturnsNoContent()
+ public async Task DeleteMedication_WhenMedicationExists_ReturnsNoContent()
{
await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
@@ -22,7 +22,7 @@ public async Task DeleteMedicationById_WhenMedicationExists_ReturnsNoContent()
}
[Fact]
- public async Task DeleteMedicationById_WhenMedicationDoesNotExist_ReturnsNotFound()
+ public async Task DeleteMedication_WhenMedicationDoesNotExist_ReturnsNotFound()
{
await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
@@ -33,17 +33,20 @@ public async Task DeleteMedicationById_WhenMedicationDoesNotExist_ReturnsNotFoun
Assert.NotNull(dbContext.Medications.Find(2));
}
- [Fact]
- public async Task DeleteMedicationById_WhenIdIsNegative_ReturnsBadRequest()
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ public async Task DeleteMedication_WhenIdNotGreaterThanZero_ReturnsBadRequest(int id)
{
await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
- var result = await MedicationEndpoints.DeleteMedication(-1, dbContext, CancellationToken.None);
+ var result = await MedicationEndpoints.DeleteMedication(id, dbContext, CancellationToken.None);
var problem = Assert.IsType(result.Result);
Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
Assert.NotNull(dbContext.Medications.Find(1));
Assert.NotNull(dbContext.Medications.Find(2));
+ Assert.Equal("Id must be greater than zero.", problem.ProblemDetails.Detail);
}
private static MedicationsDbContext CreateContext(params Medication[] seed)
@@ -65,7 +68,8 @@ private static MedicationsDbContext CreateContext(params Medication[] seed)
Id = id,
Name = $"Test Medication {id}",
Quantity = 10 * id,
- CreationDate = DateTimeOffset.UtcNow
+ CreationDate = new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero)
};
+
}
}
diff --git a/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs b/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs
new file mode 100644
index 0000000..dd497c4
--- /dev/null
+++ b/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs
@@ -0,0 +1,62 @@
+using Medications.Api.Contracts;
+using Medications.Api.Data;
+using Medications.Api.Endpoints;
+using Medications.Api.Entities;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.EntityFrameworkCore;
+
+namespace Medications.Unit.Tests
+{
+ public class GetAllEndpointUnitTests
+ {
+ [Fact]
+ public async Task GetAllMedications_WhenMedicationsExist_ReturnsOK()
+ {
+ var medication1 = NewMedication(1);
+ var medication2 = NewMedication(2);
+ await using var dbContext = CreateContext(medication1, medication2);
+
+ var result = await MedicationEndpoints.GetMedications(dbContext, CancellationToken.None);
+
+ var response = Assert.IsType>>(result);
+ Assert.Equal(StatusCodes.Status200OK, response.StatusCode);
+ Assert.NotNull(response.Value);
+
+ Assert.NotNull(response.Value[0]);
+ Assert.Equal(medication1.Id, response.Value[0].Id);
+ Assert.Equal(medication1.Name, response.Value[0].Name);
+ Assert.Equal(medication1.Quantity, response.Value[0].Quantity);
+ Assert.Equal(medication1.CreationDate, response.Value[0].CreationDate);
+
+ Assert.NotNull(response.Value[1]);
+ Assert.Equal(medication2.Id, response.Value[1].Id);
+ Assert.Equal(medication2.Name, response.Value[1].Name);
+ Assert.Equal(medication2.Quantity, response.Value[1].Quantity);
+ Assert.Equal(medication2.CreationDate, response.Value[1].CreationDate);
+ }
+
+
+ private static MedicationsDbContext CreateContext(params Medication[] seed)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+
+ var dbContext = new MedicationsDbContext(options);
+
+ dbContext.Medications.AddRange(seed);
+ dbContext.SaveChanges();
+
+ return dbContext;
+ }
+
+ private static Medication NewMedication(int id) => new()
+ {
+ Id = id,
+ Name = $"Test Medication {id}",
+ Quantity = 10 * id,
+ CreationDate = new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero)
+ };
+ }
+}
diff --git a/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs b/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs
new file mode 100644
index 0000000..f606806
--- /dev/null
+++ b/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs
@@ -0,0 +1,87 @@
+using Medications.Api.Contracts;
+using Medications.Api.Data;
+using Medications.Api.Endpoints;
+using Medications.Api.Entities;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.EntityFrameworkCore;
+
+namespace Medications.Unit.Tests
+{
+ public class GetByIdEndpointUnitTests
+ {
+ [Fact]
+ public async Task GetMedicationById_WhenMedicationExists_ReturnsOK()
+ {
+ var medication1 = NewMedication(1);
+ var medication2 = NewMedication(2);
+ await using var dbContext = CreateContext(medication1, medication2);
+
+ var result1 = await MedicationEndpoints.GetMedication(1, dbContext, CancellationToken.None);
+ var result2 = await MedicationEndpoints.GetMedication(2, dbContext, CancellationToken.None);
+
+ var response1 = Assert.IsType>(result1.Result);
+ Assert.Equal(StatusCodes.Status200OK, response1.StatusCode);
+ Assert.NotNull(response1.Value);
+ Assert.Equal(medication1.Id, response1.Value.Id);
+ Assert.Equal(medication1.Name, response1.Value.Name);
+ Assert.Equal(medication1.Quantity, response1.Value.Quantity);
+ Assert.Equal(medication1.CreationDate, response1.Value.CreationDate);
+
+ var response2 = Assert.IsType>(result2.Result);
+ Assert.Equal(StatusCodes.Status200OK, response2.StatusCode);
+ Assert.NotNull(response2.Value);
+ Assert.Equal(medication2.Id, response2.Value.Id);
+ Assert.Equal(medication2.Name, response2.Value.Name);
+ Assert.Equal(medication2.Quantity, response2.Value.Quantity);
+ Assert.Equal(medication2.CreationDate, response2.Value.CreationDate);
+ }
+
+ [Fact]
+ public async Task GetMedicationById_WhenMedicationDoesNotExist_ReturnsNotFound()
+ {
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
+
+ var result1 = await MedicationEndpoints.GetMedication(3, dbContext, CancellationToken.None);
+
+ var response1 = Assert.IsType(result1.Result);
+ Assert.Equal(StatusCodes.Status404NotFound, response1.StatusCode);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ public async Task GetMedicationById_WhenNotGreaterThanZero_ReturnsBadRequest(int id)
+ {
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
+
+ var result = await MedicationEndpoints.GetMedication(id, dbContext, CancellationToken.None);
+
+ var problem = Assert.IsType(result.Result);
+ Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
+ Assert.Equal("Id must be greater than zero.", problem.ProblemDetails.Detail);
+ }
+
+ private static MedicationsDbContext CreateContext(params Medication[] seed)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+
+ var dbContext = new MedicationsDbContext(options);
+
+ dbContext.Medications.AddRange(seed);
+ dbContext.SaveChanges();
+
+ return dbContext;
+ }
+
+ private static Medication NewMedication(int id) => new()
+ {
+ Id = id,
+ Name = $"Test Medication {id}",
+ Quantity = 10 * id,
+ CreationDate = new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero)
+ };
+ }
+}
diff --git a/tests/Medications.Unit.Tests/MedicationMappingUnitTests.cs b/tests/Medications.Unit.Tests/MedicationMappingUnitTests.cs
new file mode 100644
index 0000000..fb990a4
--- /dev/null
+++ b/tests/Medications.Unit.Tests/MedicationMappingUnitTests.cs
@@ -0,0 +1,42 @@
+using Medications.Api.Contracts;
+using Medications.Api.Entities;
+using Medications.Api.Mapping;
+using Microsoft.Extensions.Time.Testing;
+
+namespace Medications.Unit.Tests
+{
+ public class MedicationMappingUnitTests
+ {
+ [Fact]
+ public void ToMedication_MapsCorrectly()
+ {
+ var clock = new FakeTimeProvider(new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero));
+ var request = new CreateMedicationRequest { Name = " Aspirin ", Quantity = 5 };
+
+ var medication = request.ToMedication(clock);
+
+ Assert.Equal(clock.GetUtcNow(), medication.CreationDate);
+ Assert.Equal(request.Name.Trim(), medication.Name);
+ Assert.Equal(5, medication.Quantity);
+ }
+
+ [Fact]
+ public void ToResponse_MapsCorrectly()
+ {
+ var medication = new Medication
+ {
+ Name = "Aspirin",
+ Quantity = 5,
+ CreationDate = new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero)
+ };
+
+ var response = medication.ToResponse();
+
+ Assert.Equal(medication.Id, response.Id);
+ Assert.Equal(medication.Name, response.Name);
+ Assert.Equal(medication.Quantity, response.Quantity);
+ Assert.Equal(medication.CreationDate, response.CreationDate);
+ }
+
+ }
+}
diff --git a/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj b/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
index e77a9b8..3fd85d6 100644
--- a/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
+++ b/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
@@ -5,6 +5,9 @@
enable
enable
false
+
+ $(MSBuildThisFileDirectory)..\test.runsettings
@@ -12,6 +15,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
all
diff --git a/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs b/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs
new file mode 100644
index 0000000..f740631
--- /dev/null
+++ b/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs
@@ -0,0 +1,51 @@
+using Medications.Api.Contracts;
+using Medications.Api.Data;
+using Medications.Api.Endpoints;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Time.Testing;
+
+namespace Medications.Unit.Tests
+{
+ public class PostEndpointUnitTests
+ {
+ [Fact]
+ public async Task PostMedication_Returns201CreatedAtRoute()
+ {
+ await using var dbContext = CreateContext();
+ var medicationRequest = GenerateCreateMedicationRequest("Paracetamol", 10);
+ var clock = new FakeTimeProvider(new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero));
+
+ var result = await MedicationEndpoints.CreateMedication(
+ medicationRequest, dbContext, clock, CancellationToken.None);
+
+ var response = Assert.IsType>(result);
+ Assert.NotNull(response.Value);
+ Assert.Equal(1, response.Value.Id);
+ Assert.Equal("GetMedication", response.RouteName);
+ Assert.Equal(1, response.RouteValues["id"]);
+ Assert.Equal(medicationRequest.Name, response.Value.Name);
+ Assert.Equal(medicationRequest.Quantity, response.Value.Quantity);
+
+ var persistedMedication = dbContext.Medications.Find(1);
+ Assert.NotNull(persistedMedication);
+ Assert.Equal(medicationRequest.Name, persistedMedication.Name);
+ Assert.Equal(medicationRequest.Quantity, persistedMedication.Quantity);
+ Assert.Equal(clock.GetUtcNow(), persistedMedication.CreationDate);
+ }
+
+ private static MedicationsDbContext CreateContext()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+
+ return new MedicationsDbContext(options);
+ }
+
+ private static CreateMedicationRequest GenerateCreateMedicationRequest(string name, int quantity)
+ {
+ return new() { Name = name, Quantity = quantity };
+ }
+ }
+}
diff --git a/tests/test.runsettings b/tests/test.runsettings
new file mode 100644
index 0000000..8084fb0
--- /dev/null
+++ b/tests/test.runsettings
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+ cobertura
+ GeneratedCodeAttribute
+
+
+
+
+
+
+
+
+
+ .*tests\.dll$
+
+
+
+
+ ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$
+ ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$
+
+
+
+
+
+
+
+
+
From 71fd9330fa5c905067e053c08b5bf814da358613 Mon Sep 17 00:00:00 2001
From: Fernando Tona <105774270+fernandotonacoder@users.noreply.github.com>
Date: Wed, 5 Aug 2026 02:07:49 +0100
Subject: [PATCH 11/11] cleanup
---
.../Endpoints/MedicationEndpoints.cs | 4 +-
.../DeleteEndpointUnitTests.cs | 29 +-------
.../GetAllEndpointUnitTests.cs | 35 +++-------
.../GetByIdEndpointUnitTests.cs | 66 +++++--------------
.../PostEndpointUnitTests.cs | 12 +---
.../TestDbContextFactory.cs | 36 ++++++++++
6 files changed, 68 insertions(+), 114 deletions(-)
create mode 100644 tests/Medications.Unit.Tests/TestDbContextFactory.cs
diff --git a/src/Medications.Api/Endpoints/MedicationEndpoints.cs b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
index 6e23302..0c91d00 100644
--- a/src/Medications.Api/Endpoints/MedicationEndpoints.cs
+++ b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
@@ -45,7 +45,9 @@ public static IEndpointRouteBuilder MapMedicationEndpoints(this IEndpointRouteBu
internal static async Task>> GetMedications(
MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
- var medications = await dbContext.Medications.ToListAsync(cancellationToken);
+ var medications = await dbContext.Medications
+ .OrderBy(medication => medication.Id)
+ .ToListAsync(cancellationToken);
var medicationResponseList = medications.Select(medication => medication.ToResponse()).ToList();
diff --git a/tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs b/tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs
index 376516d..42ea574 100644
--- a/tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs
+++ b/tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs
@@ -1,9 +1,7 @@
-using Medications.Api.Data;
using Medications.Api.Endpoints;
-using Medications.Api.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
-using Microsoft.EntityFrameworkCore;
+using static Medications.Unit.Tests.TestDbContextFactory;
namespace Medications.Unit.Tests
{
@@ -44,32 +42,9 @@ public async Task DeleteMedication_WhenIdNotGreaterThanZero_ReturnsBadRequest(in
var problem = Assert.IsType(result.Result);
Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
+ Assert.Equal("Id must be greater than zero.", problem.ProblemDetails.Detail);
Assert.NotNull(dbContext.Medications.Find(1));
Assert.NotNull(dbContext.Medications.Find(2));
- Assert.Equal("Id must be greater than zero.", problem.ProblemDetails.Detail);
}
-
- private static MedicationsDbContext CreateContext(params Medication[] seed)
- {
- var options = new DbContextOptionsBuilder()
- .UseInMemoryDatabase(Guid.NewGuid().ToString())
- .Options;
-
- var dbContext = new MedicationsDbContext(options);
-
- dbContext.Medications.AddRange(seed);
- dbContext.SaveChanges();
-
- return dbContext;
- }
-
- private static Medication NewMedication(int id) => new()
- {
- Id = id,
- Name = $"Test Medication {id}",
- Quantity = 10 * id,
- CreationDate = new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero)
- };
-
}
}
diff --git a/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs b/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs
index dd497c4..5bf4082 100644
--- a/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs
+++ b/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs
@@ -1,10 +1,7 @@
using Medications.Api.Contracts;
-using Medications.Api.Data;
using Medications.Api.Endpoints;
-using Medications.Api.Entities;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
-using Microsoft.EntityFrameworkCore;
+using static Medications.Unit.Tests.TestDbContextFactory;
namespace Medications.Unit.Tests
{
@@ -20,43 +17,31 @@ public async Task GetAllMedications_WhenMedicationsExist_ReturnsOK()
var result = await MedicationEndpoints.GetMedications(dbContext, CancellationToken.None);
var response = Assert.IsType>>(result);
- Assert.Equal(StatusCodes.Status200OK, response.StatusCode);
Assert.NotNull(response.Value);
+ Assert.Equal(2, response.Value.Count);
- Assert.NotNull(response.Value[0]);
+ // Index order is meaningful because the endpoint orders by id.
Assert.Equal(medication1.Id, response.Value[0].Id);
Assert.Equal(medication1.Name, response.Value[0].Name);
Assert.Equal(medication1.Quantity, response.Value[0].Quantity);
Assert.Equal(medication1.CreationDate, response.Value[0].CreationDate);
- Assert.NotNull(response.Value[1]);
Assert.Equal(medication2.Id, response.Value[1].Id);
Assert.Equal(medication2.Name, response.Value[1].Name);
Assert.Equal(medication2.Quantity, response.Value[1].Quantity);
Assert.Equal(medication2.CreationDate, response.Value[1].CreationDate);
}
-
- private static MedicationsDbContext CreateContext(params Medication[] seed)
+ [Fact]
+ public async Task GetAllMedications_WhenNoMedicationsExist_ReturnsEmptyList()
{
- var options = new DbContextOptionsBuilder()
- .UseInMemoryDatabase(Guid.NewGuid().ToString())
- .Options;
+ await using var dbContext = CreateContext();
- var dbContext = new MedicationsDbContext(options);
-
- dbContext.Medications.AddRange(seed);
- dbContext.SaveChanges();
+ var result = await MedicationEndpoints.GetMedications(dbContext, CancellationToken.None);
- return dbContext;
+ var response = Assert.IsType>>(result);
+ Assert.NotNull(response.Value);
+ Assert.Empty(response.Value);
}
-
- private static Medication NewMedication(int id) => new()
- {
- Id = id,
- Name = $"Test Medication {id}",
- Quantity = 10 * id,
- CreationDate = new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero)
- };
}
}
diff --git a/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs b/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs
index f606806..3d6f5fc 100644
--- a/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs
+++ b/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs
@@ -1,40 +1,29 @@
using Medications.Api.Contracts;
-using Medications.Api.Data;
using Medications.Api.Endpoints;
-using Medications.Api.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
-using Microsoft.EntityFrameworkCore;
+using static Medications.Unit.Tests.TestDbContextFactory;
namespace Medications.Unit.Tests
{
public class GetByIdEndpointUnitTests
{
- [Fact]
- public async Task GetMedicationById_WhenMedicationExists_ReturnsOK()
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ public async Task GetMedicationById_WhenMedicationExists_ReturnsOK(int id)
{
- var medication1 = NewMedication(1);
- var medication2 = NewMedication(2);
- await using var dbContext = CreateContext(medication1, medication2);
-
- var result1 = await MedicationEndpoints.GetMedication(1, dbContext, CancellationToken.None);
- var result2 = await MedicationEndpoints.GetMedication(2, dbContext, CancellationToken.None);
+ var expected = NewMedication(id);
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
- var response1 = Assert.IsType>(result1.Result);
- Assert.Equal(StatusCodes.Status200OK, response1.StatusCode);
- Assert.NotNull(response1.Value);
- Assert.Equal(medication1.Id, response1.Value.Id);
- Assert.Equal(medication1.Name, response1.Value.Name);
- Assert.Equal(medication1.Quantity, response1.Value.Quantity);
- Assert.Equal(medication1.CreationDate, response1.Value.CreationDate);
+ var result = await MedicationEndpoints.GetMedication(id, dbContext, CancellationToken.None);
- var response2 = Assert.IsType>(result2.Result);
- Assert.Equal(StatusCodes.Status200OK, response2.StatusCode);
- Assert.NotNull(response2.Value);
- Assert.Equal(medication2.Id, response2.Value.Id);
- Assert.Equal(medication2.Name, response2.Value.Name);
- Assert.Equal(medication2.Quantity, response2.Value.Quantity);
- Assert.Equal(medication2.CreationDate, response2.Value.CreationDate);
+ var response = Assert.IsType>(result.Result);
+ Assert.NotNull(response.Value);
+ Assert.Equal(expected.Id, response.Value.Id);
+ Assert.Equal(expected.Name, response.Value.Name);
+ Assert.Equal(expected.Quantity, response.Value.Quantity);
+ Assert.Equal(expected.CreationDate, response.Value.CreationDate);
}
[Fact]
@@ -42,10 +31,9 @@ public async Task GetMedicationById_WhenMedicationDoesNotExist_ReturnsNotFound()
{
await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
- var result1 = await MedicationEndpoints.GetMedication(3, dbContext, CancellationToken.None);
+ var result = await MedicationEndpoints.GetMedication(3, dbContext, CancellationToken.None);
- var response1 = Assert.IsType(result1.Result);
- Assert.Equal(StatusCodes.Status404NotFound, response1.StatusCode);
+ Assert.IsType(result.Result);
}
[Theory]
@@ -61,27 +49,5 @@ public async Task GetMedicationById_WhenNotGreaterThanZero_ReturnsBadRequest(int
Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
Assert.Equal("Id must be greater than zero.", problem.ProblemDetails.Detail);
}
-
- private static MedicationsDbContext CreateContext(params Medication[] seed)
- {
- var options = new DbContextOptionsBuilder()
- .UseInMemoryDatabase(Guid.NewGuid().ToString())
- .Options;
-
- var dbContext = new MedicationsDbContext(options);
-
- dbContext.Medications.AddRange(seed);
- dbContext.SaveChanges();
-
- return dbContext;
- }
-
- private static Medication NewMedication(int id) => new()
- {
- Id = id,
- Name = $"Test Medication {id}",
- Quantity = 10 * id,
- CreationDate = new DateTimeOffset(2026, 1, 15, 9, 30, 0, TimeSpan.Zero)
- };
}
}
diff --git a/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs b/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs
index f740631..4f0e553 100644
--- a/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs
+++ b/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs
@@ -1,9 +1,8 @@
using Medications.Api.Contracts;
-using Medications.Api.Data;
using Medications.Api.Endpoints;
using Microsoft.AspNetCore.Http.HttpResults;
-using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Time.Testing;
+using static Medications.Unit.Tests.TestDbContextFactory;
namespace Medications.Unit.Tests
{
@@ -34,15 +33,6 @@ public async Task PostMedication_Returns201CreatedAtRoute()
Assert.Equal(clock.GetUtcNow(), persistedMedication.CreationDate);
}
- private static MedicationsDbContext CreateContext()
- {
- var options = new DbContextOptionsBuilder()
- .UseInMemoryDatabase(Guid.NewGuid().ToString())
- .Options;
-
- return new MedicationsDbContext(options);
- }
-
private static CreateMedicationRequest GenerateCreateMedicationRequest(string name, int quantity)
{
return new() { Name = name, Quantity = quantity };
diff --git a/tests/Medications.Unit.Tests/TestDbContextFactory.cs b/tests/Medications.Unit.Tests/TestDbContextFactory.cs
new file mode 100644
index 0000000..84eb8b4
--- /dev/null
+++ b/tests/Medications.Unit.Tests/TestDbContextFactory.cs
@@ -0,0 +1,36 @@
+using Medications.Api.Data;
+using Medications.Api.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Medications.Unit.Tests
+{
+ ///
+ /// Shared fixture helpers.
+ ///
+ internal static class TestDbContextFactory
+ {
+ internal static readonly DateTimeOffset CreationDate = new(2026, 1, 15, 9, 30, 0, TimeSpan.Zero);
+
+ internal static MedicationsDbContext CreateContext(params Medication[] seed)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+
+ var dbContext = new MedicationsDbContext(options);
+
+ dbContext.Medications.AddRange(seed);
+ dbContext.SaveChanges();
+
+ return dbContext;
+ }
+
+ internal static Medication NewMedication(int id) => new()
+ {
+ Id = id,
+ Name = $"Test Medication {id}",
+ Quantity = 10 * id,
+ CreationDate = CreationDate
+ };
+ }
+}