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/Data/UsersDbContext.cs b/Backend/services/users-service/Data/UsersDbContext.cs new file mode 100644 index 0000000..4598f61 --- /dev/null +++ b/Backend/services/users-service/Data/UsersDbContext.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using ExpensesManager.UsersService.Models; + +namespace ExpensesManager.UsersService.Data +{ + public class UsersDbContext : DbContext + { + public UsersDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Users { get; set; } + } +} diff --git a/Backend/services/users-service/Models/Users.cs b/Backend/services/users-service/Models/Users.cs new file mode 100644 index 0000000..236d275 --- /dev/null +++ b/Backend/services/users-service/Models/Users.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ExpensesManager.UsersService.Models +{ + public class Users + { + [Key] + public int UserID { get; set; } + [Required] + [Column("Username", Order = 2)] + public string Username { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + [Required] + public string Password { get; set; } + [Required] + public int CreditCardChargeDay { get; set; } + public string SplitwisePassword { get; set; } + public string SW_User_ID { get; set; } + } +} 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..ac95ff5 --- /dev/null +++ b/Backend/services/users-service/UsersService.csproj @@ -0,0 +1,22 @@ + + + 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/UsersDbContextTests.cs b/Backend/services/users-service/tests/UsersService.Tests/UsersDbContextTests.cs new file mode 100644 index 0000000..d284d59 --- /dev/null +++ b/Backend/services/users-service/tests/UsersService.Tests/UsersDbContextTests.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using ExpensesManager.UsersService.Data; +using ExpensesManager.UsersService.Models; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace UsersService.Tests +{ + public class UsersDbContextTests + { + private UsersDbContext CreateInMemoryContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "test_users_db") + .Options; + return new UsersDbContext(options); + } + + [Fact] + public async Task Can_add_and_read_user() + { + using var ctx = CreateInMemoryContext(); + var user = new Users { UserID = 1, Username = "bob", Password = "p", CreditCardChargeDay = 1 }; + ctx.Users.Add(user); + await ctx.SaveChangesAsync(); + + var fetched = await ctx.Users.FirstOrDefaultAsync(u => u.UserID == 1); + Assert.NotNull(fetched); + Assert.Equal("bob", fetched.Username); + } + } +} 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..d7cb4cb --- /dev/null +++ b/Backend/services/users-service/tests/UsersService.Tests/UsersService.Tests.csproj @@ -0,0 +1,20 @@ + + + net7.0 + false + + + + + all + + + + + + + + + + +