diff --git a/Backend/common/messaging/IKafkaProducer.cs b/Backend/common/messaging/IKafkaProducer.cs new file mode 100644 index 0000000..f75ea9e --- /dev/null +++ b/Backend/common/messaging/IKafkaProducer.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace ExpensesManager.Common.Messaging +{ + public interface IKafkaProducer + { + Task ProduceAsync(string topic, object message, CancellationToken cancellationToken = default); + } +} diff --git a/Backend/common/messaging/KafkaProducer.cs b/Backend/common/messaging/KafkaProducer.cs new file mode 100644 index 0000000..eeb5d9f --- /dev/null +++ b/Backend/common/messaging/KafkaProducer.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; + +namespace ExpensesManager.Common.Messaging +{ + public class KafkaProducer : IKafkaProducer + { + private readonly ILogger _logger; + private readonly string _brokers; + + public KafkaProducer(ILogger logger, IConfiguration config) + { + _logger = logger; + _brokers = config["KAFKA_BROKERS"] ?? "localhost:9092"; + } + + public Task ProduceAsync(string topic, object message, CancellationToken cancellationToken = default) + { + var json = JsonSerializer.Serialize(message); + _logger.LogInformation("[KafkaProducer] ProduceAsync -> brokers={Brokers} topic={Topic} message={Message}", _brokers, topic, json); + // No-op implementation for now (dev-friendly). Replace with real Kafka client when ready. + return Task.CompletedTask; + } + } +} diff --git a/Backend/common/messaging/Messaging.csproj b/Backend/common/messaging/Messaging.csproj new file mode 100644 index 0000000..7f1db26 --- /dev/null +++ b/Backend/common/messaging/Messaging.csproj @@ -0,0 +1,11 @@ + + + net7.0 + enable + enable + + + + + + diff --git a/Backend/services/users-service/Controllers/UsersController.cs b/Backend/services/users-service/Controllers/UsersController.cs new file mode 100644 index 0000000..14008e8 --- /dev/null +++ b/Backend/services/users-service/Controllers/UsersController.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using ExpensesManager.Common.Messaging; +using Microsoft.AspNetCore.Mvc; + +namespace ExpensesManager.UsersService.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class UsersController : ControllerBase + { + private readonly IKafkaProducer _producer; + + public UsersController(IKafkaProducer producer) + { + _producer = producer; + } + + [HttpGet("health")] + public IActionResult Health() => Ok(new { status = "ok" }); + + public record UserDto(int UserId, string Username, string? Email); + + [HttpPost("signup")] + public async Task SignUp([FromBody] UserDto dto) + { + var ev = new { eventType = "UserCreated", payload = dto }; + await _producer.ProduceAsync("UserCreated", ev); + return Created(string.Empty, dto); + } + } +} diff --git a/Backend/services/users-service/Program.cs b/Backend/services/users-service/Program.cs new file mode 100644 index 0000000..a601ab0 --- /dev/null +++ b/Backend/services/users-service/Program.cs @@ -0,0 +1,20 @@ +using ExpensesManager.Common.Messaging; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddSingleton(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapControllers(); + +app.Run(); diff --git a/Backend/services/users-service/UsersService.csproj b/Backend/services/users-service/UsersService.csproj new file mode 100644 index 0000000..8df6612 --- /dev/null +++ b/Backend/services/users-service/UsersService.csproj @@ -0,0 +1,17 @@ + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/Backend/services/users-service/tests/UsersService.Tests/UsersControllerTests.cs b/Backend/services/users-service/tests/UsersService.Tests/UsersControllerTests.cs new file mode 100644 index 0000000..f2a251a --- /dev/null +++ b/Backend/services/users-service/tests/UsersService.Tests/UsersControllerTests.cs @@ -0,0 +1,43 @@ +using System.Threading; +using System.Threading.Tasks; +using ExpensesManager.Common.Messaging; +using ExpensesManager.UsersService.Controllers; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace UsersService.Tests +{ + class RecordingProducer : IKafkaProducer + { + public bool WasCalled { get; private set; } + public Task ProduceAsync(string topic, object message, CancellationToken cancellationToken = default) + { + WasCalled = true; + return Task.CompletedTask; + } + } + + public class UsersControllerTests + { + [Fact] + public void Health_returns_ok() + { + var controller = new UsersController(new RecordingProducer()); + var result = controller.Health(); + var ok = Assert.IsType(result); + Assert.Equal(200, ok.StatusCode); + } + + [Fact] + public async Task SignUp_returns_created_and_calls_producer() + { + var producer = new RecordingProducer(); + var controller = new UsersController(producer); + var dto = new UsersController.UserDto(1, "alice", "a@a.com"); + var res = await controller.SignUp(dto); + var created = Assert.IsType(res); + Assert.Equal(dto, created.Value); + Assert.True(producer.WasCalled); + } + } +} diff --git a/Backend/services/users-service/tests/UsersService.Tests/UsersService.Tests.csproj b/Backend/services/users-service/tests/UsersService.Tests/UsersService.Tests.csproj new file mode 100644 index 0000000..325064c --- /dev/null +++ b/Backend/services/users-service/tests/UsersService.Tests/UsersService.Tests.csproj @@ -0,0 +1,17 @@ + + + net7.0 + false + + + + + all + + + + + + + +