Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .devin/environment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# https://docs.devin.ai/onboard-devin/environment-yaml
# Commands run from the repo root.

# One-time setup: install .NET 10 SDK and Docker (required for build and runtime).
initialize: |
wget -q https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh
chmod +x /tmp/dotnet-install.sh
/tmp/dotnet-install.sh --channel 10.0 --install-dir $HOME/.dotnet
echo 'export PATH="$HOME/.dotnet:$PATH"' >> ~/.bashrc
echo 'export DOTNET_ROOT="$HOME/.dotnet"' >> ~/.bashrc
export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"

# Install project dependencies.
maintenance: |
export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet"
cd src && dotnet restore Microservices.sln

knowledge:
- name: build
contents: |
cd src && dotnet build Microservices.sln
- name: test
contents: |
cd src && dotnet test Tests/Microservices.Tests/Microservices.Tests.csproj
- name: run
contents: |
cd src && docker compose up --build -d
# Health checks: curl http://localhost:{5000..5005}/healthz
# Notification service may need a restart if postgres isn't ready:
# docker compose restart notification-service
- name: lint
contents: |
cd src && dotnet build Microservices.sln --warnaserror
- name: project-structure
contents: |
.NET 10 microservices solution at src/Microservices.sln.
Services: Identity (5001), Customer (5002), Order (5003), Product (5004), Notification (5005).
API Gateway (YARP) on port 5000.
Shared libraries: Shared.Contracts (DTOs/events), Shared.Infrastructure (middleware).
Tests: src/Tests/Microservices.Tests (xUnit).
Docker Compose for local runtime with PostgreSQL and RabbitMQ.
261 changes: 247 additions & 14 deletions src/Microservices.sln

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/Services/Customer/Customer.API/Customer.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<ItemGroup>
<ProjectReference Include="..\Customer.Domain\Customer.Domain.csproj" />
<ProjectReference Include="..\Customer.Infrastructure\Customer.Infrastructure.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*" />
Expand Down
4 changes: 2 additions & 2 deletions src/Services/Identity/Identity.API/Identity.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<ItemGroup>
<ProjectReference Include="..\Identity.Domain\Identity.Domain.csproj" />
<ProjectReference Include="..\Identity.Infrastructure\Identity.Infrastructure.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<ItemGroup>
<ProjectReference Include="..\Notification.Domain\Notification.Domain.csproj" />
<ProjectReference Include="..\Notification.Infrastructure\Notification.Infrastructure.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*" />
Expand Down
4 changes: 2 additions & 2 deletions src/Services/Order/Order.API/Order.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<ItemGroup>
<ProjectReference Include="..\Order.Domain\Order.Domain.csproj" />
<ProjectReference Include="..\Order.Infrastructure\Order.Infrastructure.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*" />
Expand Down
4 changes: 2 additions & 2 deletions src/Services/Product/Product.API/Product.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<ItemGroup>
<ProjectReference Include="..\Product.Domain\Product.Domain.csproj" />
<ProjectReference Include="..\Product.Infrastructure\Product.Infrastructure.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\..\Shared\Shared.Infrastructure\Shared.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.*" />
Expand Down
27 changes: 27 additions & 0 deletions src/Tests/Microservices.Tests/Microservices.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../Shared/Shared.Contracts/Shared.Contracts.csproj" />
<ProjectReference Include="../../Services/Notification/Notification.API/Notification.API.csproj" />
<ProjectReference Include="../../Services/Notification/Notification.Domain/Notification.Domain.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
97 changes: 97 additions & 0 deletions src/Tests/Microservices.Tests/NotificationRendererTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Globalization;
using Notification.API.Services;
using Notification.Domain.Entities;

namespace Microservices.Tests;

public class NotificationRendererTests : IDisposable
{
private readonly NotificationRenderer _renderer = new();
private readonly CultureInfo _originalCulture = CultureInfo.CurrentCulture;

public NotificationRendererTests()
{
CultureInfo.CurrentCulture = new CultureInfo("en-US");
}

public void Dispose()
{
CultureInfo.CurrentCulture = _originalCulture;
}

[Fact]
public void RenderNotification_OrderConfirmation_ReturnsSubjectWithFormattedAmount()
{
var notification = CreateTestNotification(orderTotal: 4999m);

var (subject, _) = _renderer.RenderNotification(notification);

Assert.Contains("$49.99", subject);
Assert.StartsWith("Order Confirmed", subject);
}

[Fact]
public void RenderNotification_OrderConfirmation_ReturnsHtmlBody()
{
var notification = CreateTestNotification(orderTotal: 10000m);

var (_, body) = _renderer.RenderNotification(notification);

Assert.Contains("<!DOCTYPE html>", body);
Assert.Contains("Order Confirmed", body);
Assert.Contains("$100.00", body);
}

[Fact]
public void RenderNotification_OrderConfirmation_IncludesCustomerName()
{
var notification = CreateTestNotification();
notification.CustomerName = "Jane Doe";

var (_, body) = _renderer.RenderNotification(notification);

Assert.Contains("Jane Doe", body);
}

[Fact]
public void RenderNotification_UnknownType_ReturnsFallback()
{
var notification = CreateTestNotification();
notification.Type = NotificationType.OrderShipped;

var (subject, body) = _renderer.RenderNotification(notification);

Assert.Equal("Notification", subject);
Assert.Contains(notification.OrderId.ToString(), body);
}

[Theory]
[InlineData(0, "$0.00")]
[InlineData(100, "$1.00")]
[InlineData(1550, "$15.50")]
[InlineData(99999, "$999.99")]
public void RenderNotification_CurrencyFormatting_ConvertsFromCents(decimal cents, string expected)
{
var notification = CreateTestNotification(orderTotal: cents);

var (subject, _) = _renderer.RenderNotification(notification);

Assert.Contains(expected, subject);
}

private static OrderNotification CreateTestNotification(decimal orderTotal = 5000m)
{
return new OrderNotification
{
Id = Guid.NewGuid(),
OrderId = Guid.NewGuid(),
CustomerId = Guid.NewGuid(),
OrderTotal = orderTotal,
CustomerEmail = "test@example.com",
CustomerName = "Test User",
Type = NotificationType.OrderConfirmation,
Status = NotificationStatus.Pending,
CreatedAt = new DateTime(2026, 1, 15, 14, 30, 0, DateTimeKind.Utc)
};
}
}
47 changes: 47 additions & 0 deletions src/Tests/Microservices.Tests/SharedContractsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Shared.Contracts.Events;
using Shared.Contracts.DTOs;

namespace Microservices.Tests;

public class SharedContractsTests
{
[Fact]
public void OrderPlacedEvent_RecordProperties_AreAssignedCorrectly()
{
var orderId = Guid.NewGuid();
var customerId = Guid.NewGuid();
var amount = 2500m;
var placedAt = DateTime.UtcNow;

var evt = new OrderPlacedEvent(orderId, customerId, amount, placedAt);

Assert.Equal(orderId, evt.OrderId);
Assert.Equal(customerId, evt.CustomerId);
Assert.Equal(amount, evt.TotalAmount);
Assert.Equal(placedAt, evt.PlacedAt);
}

[Fact]
public void OrderPlacedEvent_RecordEquality_WorksCorrectly()
{
var orderId = Guid.NewGuid();
var customerId = Guid.NewGuid();
var now = DateTime.UtcNow;

var a = new OrderPlacedEvent(orderId, customerId, 100m, now);
var b = new OrderPlacedEvent(orderId, customerId, 100m, now);

Assert.Equal(a, b);
}

[Fact]
public void ServiceHealthDto_RecordProperties_AreAssignedCorrectly()
{
var now = DateTime.UtcNow;
var dto = new ServiceHealthDto("identity-service", "Healthy", now);

Assert.Equal("identity-service", dto.ServiceName);
Assert.Equal("Healthy", dto.Status);
Assert.Equal(now, dto.CheckedAt);
}
}