diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..fa63b4d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,63 @@
+name: CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ workflow_dispatch:
+
+jobs:
+ build-and-test:
+
+ runs-on: ubuntu-latest
+
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ steps:
+ - uses: actions/checkout@v7.0.1
+ with:
+ fetch-depth: 0
+
+ - 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
+ # RunSettingsFilePath points at tests/test.runsettings in cobertura format
+ run: dotnet test --no-build
+
+ - 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 b7c04c7..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
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d5b8811..008f955 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,8 +5,12 @@
+
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/Medications.slnx b/Medications.slnx
index f4c7203..ba4ce66 100644
--- a/Medications.slnx
+++ b/Medications.slnx
@@ -1,6 +1,10 @@
-
-
+
+
+
+
+
+
diff --git a/src/Medications.Api/Endpoints/MedicationEndpoints.cs b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
index 279d1f6..0c91d00 100644
--- a/src/Medications.Api/Endpoints/MedicationEndpoints.cs
+++ b/src/Medications.Api/Endpoints/MedicationEndpoints.cs
@@ -8,46 +8,65 @@ namespace Medications.Api.Endpoints;
public static class MedicationEndpoints
{
+ 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("/", GetAllMedications)
- .WithName("GetMedications")
+ group.MapGet("/", GetMedications)
+ .WithName(GetMedicationsEndpointName)
.WithDescription("Retrieve all medications.");
group.MapGet("/{id}", GetMedication)
- .WithName("GetMedication")
- .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("CreateMedication")
- .WithDescription("Create a new medication");
+ .WithName(CreateMedicationEndpointName)
+ .WithDescription("Create a new medication")
+ .ProducesValidationProblem();
+
+ group.MapDelete("/{id}", DeleteMedication)
+ .WithName(DeleteMedicationEndpointName)
+ .WithDescription("Delete a medication")
+ .ProducesProblem(StatusCodes.Status400BadRequest);
return endpoints;
}
- private static async Task>> GetAllMedications(MedicationsDbContext dbContext)
+ internal static async Task>> GetMedications(
+ MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
- var medications = await dbContext.Medications.ToListAsync();
+ var medications = await dbContext.Medications
+ .OrderBy(medication => medication.Id)
+ .ToListAsync(cancellationToken);
var medicationResponseList = medications.Select(medication => medication.ToResponse()).ToList();
return TypedResults.Ok(medicationResponseList);
}
- private static async Task, NotFound>> GetMedication(
- int id, MedicationsDbContext dbContext)
+ internal static async Task, NotFound, ProblemHttpResult>> GetMedication(
+ int id, MedicationsDbContext dbContext, CancellationToken cancellationToken)
{
- var medication = await dbContext.Medications.FindAsync(id);
+ if (id <= 0) return InvalidId();
+
+ var medication = await dbContext.Medications.FindAsync([id], cancellationToken);
if (medication is null) return TypedResults.NotFound();
return TypedResults.Ok(medication.ToResponse());
}
- private static async Task> CreateMedication(
+ internal static async Task> CreateMedication(
CreateMedicationRequest request,
MedicationsDbContext dbContext,
TimeProvider timeProvider,
@@ -61,6 +80,25 @@ private static async Task> CreateMedication(
var medicationResponse = medication.ToResponse();
- return TypedResults.CreatedAtRoute(medicationResponse, "GetMedication", new { id = medicationResponse.Id });
+ return TypedResults.CreatedAtRoute(medicationResponse, GetMedicationEndpointName, new { id = medicationResponse.Id });
}
+
+ 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();
+
+ dbContext.Medications.Remove(medication);
+ 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 08df76d..043f62a 100644
--- a/src/Medications.Api/Medications.Api.csproj
+++ b/src/Medications.Api/Medications.Api.csproj
@@ -4,6 +4,11 @@
+
+
+
+
+
diff --git a/src/Medications.Api/Program.cs b/src/Medications.Api/Program.cs
index e4d51d5..1f6c836 100644
--- a/src/Medications.Api/Program.cs
+++ b/src/Medications.Api/Program.cs
@@ -1,20 +1,40 @@
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"));
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();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
+ 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/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 @@
-
-
-
-
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/DeleteEndpointUnitTests.cs b/tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs
new file mode 100644
index 0000000..42ea574
--- /dev/null
+++ b/tests/Medications.Unit.Tests/DeleteEndpointUnitTests.cs
@@ -0,0 +1,50 @@
+using Medications.Api.Endpoints;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using static Medications.Unit.Tests.TestDbContextFactory;
+
+namespace Medications.Unit.Tests
+{
+ public class DeleteEndpointUnitTests
+ {
+ [Fact]
+ public async Task DeleteMedication_WhenMedicationExists_ReturnsNoContent()
+ {
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
+
+ var result = await MedicationEndpoints.DeleteMedication(1, dbContext, CancellationToken.None);
+
+ Assert.IsType(result.Result);
+ Assert.Null(dbContext.Medications.Find(1));
+ Assert.NotNull(dbContext.Medications.Find(2));
+ }
+
+ [Fact]
+ public async Task DeleteMedication_WhenMedicationDoesNotExist_ReturnsNotFound()
+ {
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
+
+ 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));
+ }
+
+ [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(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);
+ Assert.NotNull(dbContext.Medications.Find(1));
+ Assert.NotNull(dbContext.Medications.Find(2));
+ }
+ }
+}
diff --git a/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs b/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs
new file mode 100644
index 0000000..5bf4082
--- /dev/null
+++ b/tests/Medications.Unit.Tests/GetAllEndpointUnitTests.cs
@@ -0,0 +1,47 @@
+using Medications.Api.Contracts;
+using Medications.Api.Endpoints;
+using Microsoft.AspNetCore.Http.HttpResults;
+using static Medications.Unit.Tests.TestDbContextFactory;
+
+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.NotNull(response.Value);
+ Assert.Equal(2, response.Value.Count);
+
+ // 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.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);
+ }
+
+ [Fact]
+ public async Task GetAllMedications_WhenNoMedicationsExist_ReturnsEmptyList()
+ {
+ await using var dbContext = CreateContext();
+
+ var result = await MedicationEndpoints.GetMedications(dbContext, CancellationToken.None);
+
+ var response = Assert.IsType>>(result);
+ Assert.NotNull(response.Value);
+ Assert.Empty(response.Value);
+ }
+ }
+}
diff --git a/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs b/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs
new file mode 100644
index 0000000..3d6f5fc
--- /dev/null
+++ b/tests/Medications.Unit.Tests/GetByIdEndpointUnitTests.cs
@@ -0,0 +1,53 @@
+using Medications.Api.Contracts;
+using Medications.Api.Endpoints;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using static Medications.Unit.Tests.TestDbContextFactory;
+
+namespace Medications.Unit.Tests
+{
+ public class GetByIdEndpointUnitTests
+ {
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ public async Task GetMedicationById_WhenMedicationExists_ReturnsOK(int id)
+ {
+ var expected = NewMedication(id);
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
+
+ var result = await MedicationEndpoints.GetMedication(id, dbContext, CancellationToken.None);
+
+ 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]
+ public async Task GetMedicationById_WhenMedicationDoesNotExist_ReturnsNotFound()
+ {
+ await using var dbContext = CreateContext(NewMedication(1), NewMedication(2));
+
+ var result = await MedicationEndpoints.GetMedication(3, dbContext, CancellationToken.None);
+
+ Assert.IsType(result.Result);
+ }
+
+ [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);
+ }
+ }
+}
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
new file mode 100644
index 0000000..3fd85d6
--- /dev/null
+++ b/tests/Medications.Unit.Tests/Medications.Unit.Tests.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+ $(MSBuildThisFileDirectory)..\test.runsettings
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs b/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs
new file mode 100644
index 0000000..4f0e553
--- /dev/null
+++ b/tests/Medications.Unit.Tests/PostEndpointUnitTests.cs
@@ -0,0 +1,41 @@
+using Medications.Api.Contracts;
+using Medications.Api.Endpoints;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.Extensions.Time.Testing;
+using static Medications.Unit.Tests.TestDbContextFactory;
+
+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 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
+ };
+ }
+}
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$
+
+
+
+
+
+
+
+
+