diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/.gitinore b/src/.gitinore new file mode 100644 index 0000000..3bb4d67 --- /dev/null +++ b/src/.gitinore @@ -0,0 +1,2 @@ +**/bin/ +**/obj/ \ No newline at end of file diff --git a/src/Application.Tests/Application.Tests.csproj b/src/Application.Tests/Application.Tests.csproj new file mode 100644 index 0000000..6300111 --- /dev/null +++ b/src/Application.Tests/Application.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Application.Tests/Factories/CreateUserCommand.cs b/src/Application.Tests/Factories/CreateUserCommand.cs new file mode 100644 index 0000000..6d4212e --- /dev/null +++ b/src/Application.Tests/Factories/CreateUserCommand.cs @@ -0,0 +1,74 @@ +using Application.Messages.Commands; +using Bogus; +using Domain.Enums; + +namespace Application.Tests.Factories; + +public class CreateUserCommandFactory +{ + private readonly Faker _faker; + public CreateUserCommandFactory() + { + _faker = new Faker() + .RuleFor(u => u.Email, f => f.Internet.Email()) + .RuleFor(u => u.Password, f => f.Internet.Password()) + .RuleFor(u => u.Country, f => f.Address.Country()) + .RuleFor(u => u.AccessType, f => f.PickRandom(new []{AccessTypeEnum.DTC, AccessTypeEnum.Employer})) + .RuleFor(u => u.FullName, f => f.Name.FullName()) + .RuleFor(u => u.EmployerId, f => f.Random.Guid().ToString()) + .RuleFor(u => u.BirthDate, f => f.Date.Past(30)) + .RuleFor(u => u.Salary, f => f.Random.Decimal(30000, 100000)); + } + + public CreateUserCommandFactory WithEmail(string email) + { + _faker.RuleFor(x => x.Email, email); + return this; + } + public CreateUserCommand Create() + { + return _faker.Generate(); + } + + public CreateUserCommandFactory WithPassword(string password) + { + _faker.RuleFor(x => x.Password, password); + return this; + } + public CreateUserCommandFactory WithCountry(string country) + { + _faker.RuleFor(x => x.Country, country); + return this; + } + + public CreateUserCommandFactory WithAccessType(string accessType) + { + _faker.RuleFor(x => x.AccessType, accessType); + return this; + } + + public CreateUserCommandFactory WithFullName(string fullName) + { + _faker.RuleFor(x => x.FullName, fullName); + return this; + } + + public CreateUserCommandFactory WithEmployerId(string employerId) + { + _faker.RuleFor(x => x.EmployerId, employerId); + return this; + } + + public CreateUserCommandFactory WithBirthDate(DateTime? birthDate) + { + _faker.RuleFor(x => x.BirthDate, birthDate); + return this; + } + + public CreateUserCommandFactory WithSalary(decimal? salary) + { + _faker.RuleFor(x => x.Salary, salary); + return this; + } + +} \ No newline at end of file diff --git a/src/Application.Tests/Factories/CsvContentFactory.cs b/src/Application.Tests/Factories/CsvContentFactory.cs new file mode 100644 index 0000000..3cb45c2 --- /dev/null +++ b/src/Application.Tests/Factories/CsvContentFactory.cs @@ -0,0 +1,29 @@ +using Application.DTOs; +using Bogus; + +namespace Application.Tests.Factories; + + +public static class CsvContentFactory +{ + public static (string, List) GenerateCsvContent(int numberOfLines) + { + var faker = new Faker() + .RuleFor(u => u.Email, f => f.Internet.Email()) + .RuleFor(u => u.FullName, f => f.Name.FullName()) + .RuleFor(u => u.Country, f => f.Address.Country()) + .RuleFor(u => u.BirthDate, f => f.Date.Past(30).ToString("MM/dd/yyyy")) + .RuleFor(u => u.Salary, f => f.Random.Decimal(30000, 100000)); + + var csvLines = new List { "Email,FullName,Country,BirthDate,Salary" }; + var models = new List(); + for (int i = 0; i < numberOfLines; i++) + { + var line = faker.Generate(); + models.Add(line); + csvLines.Add($"{line.Email},{line.FullName},{line.Country},{line.BirthDate},{line.Salary}"); + } + + return (string.Join("\n", csvLines), models); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Handlers/Commands/CreateUserCommandHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Commands/CreateUserCommandHandlerTests.cs new file mode 100644 index 0000000..5a56052 --- /dev/null +++ b/src/Application.Tests/Messages/Handlers/Commands/CreateUserCommandHandlerTests.cs @@ -0,0 +1,62 @@ +using Application.Messages.Commands; +using Application.Messages.Handlers.Commands; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using Moq; + +namespace Application.Tests.Messages.Handlers.Commands; + + +[Trait("Category", "Unit")] +public class CreateUserCommandHandlerTests +{ + [Fact] + public async Task Handle_GivenValidUser_CreatesUserSuccessfully() + { + // Arrange + var userServiceClientMock = new Mock(); + var handler = new CreateUserCommandHandler(userServiceClientMock.Object); + var command = new CreateUserCommand(email: "test@example.com", + fullName: "FullName", + password: "Password", + country: "Country", + accessType: "AccessType", + employerId: "EmployerId", + birthDate: DateTime.Now, + salary: 50000); + + userServiceClientMock.Setup(client => client.CreateUserAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + + // Act + var result = await handler.Handle(command, CancellationToken.None); + + // Assert + Assert.True(result); + userServiceClientMock.Verify(client => client.CreateUserAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_GivenInvalidUser_ReturnsFailure() + { + // Arrange + var userServiceClientMock = new Mock(); + var handler = new CreateUserCommandHandler(userServiceClientMock.Object); + var command = new CreateUserCommand(email: "test@example.com", + fullName: "FullName", + password: "Password", + country: "Country", + accessType: "AccessType", + employerId: "EmployerId", + birthDate: DateTime.Now, + salary: 50000); + + userServiceClientMock.Setup(client => client.CreateUserAsync(It.IsAny(), It.IsAny())).ReturnsAsync(false); + + // Act + var result = await handler.Handle(command, CancellationToken.None); + + // Assert + Assert.False(result); + userServiceClientMock.Verify(client => client.CreateUserAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandlerTests.cs new file mode 100644 index 0000000..78ae36d --- /dev/null +++ b/src/Application.Tests/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandlerTests.cs @@ -0,0 +1,145 @@ +using System.Net; +using System.Text; +using Application.Messages.Commands; +using Application.Messages.Handlers.Commands; +using Application.Messages.Queries; +using Application.Tests.Factories; +using Domain.Enums; +using Infrastructure.Records; +using MediatR; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Protected; + +namespace Application.Tests.Messages.Handlers.Commands; + +[Trait("Category", "Unit")] +public class ProcessEligibilityFileCommandHandlerTests +{ + [Fact] + public async Task Handle_EmptyFile_ReturnsEmptyResult() + { + // Arrange + var httpClientFactoryMock = new Mock(); + var httpMessageHandlerMock = new Mock(); + var httpClient = new HttpClient(httpMessageHandlerMock.Object); + httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + var emptyStream = new MemoryStream(Encoding.UTF8.GetBytes("")); // Empty content + httpMessageHandlerMock.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StreamContent(emptyStream) + }); + + var loggerMock = new Mock>(); + var mediatorMock = new Mock(); + + var handler = new ProcessEligibilityFileCommandHandler(httpClientFactoryMock.Object, loggerMock.Object, mediatorMock.Object); + + // Act + var result = await handler.Handle(new ProcessEligibilityFileCommand("http://example.com/empty.csv", "employerName"), CancellationToken.None); + + // Assert + Assert.Empty(result.ProcessedLines); + Assert.Empty(result.NonProcessedLines); + Assert.Empty(result.Errors); + } + + + [Fact] + public async Task Handle_DownloadFails_ThrowsException() + { + // Arrange + var httpClientFactoryMock = new Mock(); + var httpMessageHandlerMock = new Mock(); + var httpClient = new HttpClient(httpMessageHandlerMock.Object); + httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + httpMessageHandlerMock.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.NotFound // Simulate failure + }); + + var loggerMock = new Mock>(); + var mediatorMock = new Mock(); + + var handler = new ProcessEligibilityFileCommandHandler(httpClientFactoryMock.Object, loggerMock.Object, mediatorMock.Object); + + // Act & Assert + await Assert.ThrowsAsync(() => handler.Handle(new ProcessEligibilityFileCommand("http://example.com/nonexistent.csv", "employerName"), CancellationToken.None)); + } + + [Theory] + [InlineData(5)] + [InlineData(15)] + [InlineData(54)] + [InlineData(13)] + public async Task Handle_NonEmptyFile_ProcessesDataCorrectly(int countOfRecords) + { + // Arrange + var httpClientFactoryMock = new Mock(); + var httpMessageHandlerMock = new Mock(); + var loggerMock = new Mock>(); + var mediatorMock = new Mock(); + var httpClient = new HttpClient(httpMessageHandlerMock.Object); + httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + var (csvContent, lineModels) = CsvContentFactory.GenerateCsvContent(countOfRecords); + foreach (var lineModel in lineModels) + { + mediatorMock.Setup(x => x.Send(It.Is(q => q.Email == lineModel.Email), It.IsAny())) + .ReturnsAsync(new UserDto() + { + Email = lineModel.Email, + Country = lineModel.Country, + Salary = lineModel.Salary, + AccessType = AccessTypeEnum.Employer, + Id = Guid.NewGuid().ToString(), + BirthDate = DateTime.Now.AddYears(-30), + FullName = lineModel.FullName, + EmployerId = Guid.NewGuid().ToString() + }); + } + var csvStream = new MemoryStream(Encoding.UTF8.GetBytes(csvContent)); + httpMessageHandlerMock.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StreamContent(csvStream) + }); + + + var handler = new ProcessEligibilityFileCommandHandler(httpClientFactoryMock.Object, loggerMock.Object, mediatorMock.Object); + + // Act + var result = await handler.Handle(new ProcessEligibilityFileCommand("http://example.com/nonempty.csv", "employerName"), CancellationToken.None); + + // Assert + Assert.NotEmpty(result.ProcessedLines); + Assert.Empty(result.NonProcessedLines); + Assert.Empty(result.Errors); + mediatorMock.Verify(x => x.Send( + It.IsAny(), + It.IsAny()), + Times.Once); + + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Handlers/Commands/ProcessSignupCommandHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Commands/ProcessSignupCommandHandlerTests.cs new file mode 100644 index 0000000..d5e6c26 --- /dev/null +++ b/src/Application.Tests/Messages/Handlers/Commands/ProcessSignupCommandHandlerTests.cs @@ -0,0 +1,159 @@ +using Application.Messages.Commands; +using Application.Messages.Handlers.Commands; +using Application.Messages.Queries; +using MediatR; +using Microsoft.Extensions.Logging; +using Moq; +using FluentAssertions; +using Infrastructure.Records; + +namespace Application.Tests.Messages.Handlers.Commands; + +[Trait("Category", "Unit")] +public class ProcessSignupCommandHandlerTests +{ + [Fact] + public async Task Handle_EmployerDataIsNull_CreatesUser() + { + // Arrange + var mediatorMock = new Mock(); + var loggerMock = new Mock>(); + var handler = new ProcessSignupCommandHandler(mediatorMock.Object, loggerMock.Object); + var command = new ProcessSignupCommand("test@example.com", "Password123!", "Country"); + + mediatorMock.Setup(x => x.Send(It.IsAny(), + It.IsAny())) + .ReturnsAsync((EmployerIdRecord)null); + + // Act + await handler.Handle(command, CancellationToken.None); + + // Assert + mediatorMock.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_UserAlreadyExists_LogsError() + { + // Arrange + var mediatorMock = new Mock(); + var loggerMock = new Mock>(); + var handler = new ProcessSignupCommandHandler(mediatorMock.Object, loggerMock.Object); + var command = new ProcessSignupCommand("existing@example.com", "Password123!", "Country"); + + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(new EmployerIdRecord("1234")); + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(new UserDto()); + + var capturedLogs = new List<(LogLevel logLevel, Exception exception, string message)>(); + MockLogger(loggerMock, capturedLogs); + + // Act + await Assert.ThrowsAsync(() => handler.Handle(command, CancellationToken.None)); + + // Assert + capturedLogs.Should().ContainSingle(x => x.message == "User with email existing@example.com already exists."); + } + + private static void MockLogger(Mock> loggerMock, List<(LogLevel logLevel, Exception exception, string message)> capturedLogs) + { + loggerMock.Setup( + x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny()) + ).Callback((level, eventId, state, exception, formatter) => + { + var logMessage = state.ToString(); + capturedLogs.Add((level, exception, logMessage)); + }); + } + + [Fact] + public async Task Handle_InvalidPassword_ThrowsInvalidOperationException() + { + // Arrange + var mediatorMock = new Mock(); + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(new EmployerIdRecord("1234")); + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(default(UserDto)); + + var loggerMock = new Mock>(); + var handler = new ProcessSignupCommandHandler(mediatorMock.Object, loggerMock.Object); + var command = new ProcessSignupCommand("test@example.com", "short", "Country"); // Invalid password + var capturedLogs = new List<(LogLevel logLevel, Exception exception, string message)>(); + MockLogger(loggerMock, capturedLogs); + + + // Act & Assert + await Assert.ThrowsAsync(() => handler.Handle(command, CancellationToken.None)); + capturedLogs.Should().ContainSingle(x => x.message == "Password does not meet the strength requirements."); + } + + [Fact] + public async Task Handle_NullPassword_ThrowsInvalidOperationException() + { + // Arrange + var mediatorMock = new Mock(); + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(new EmployerIdRecord("1234")); + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())).ReturnsAsync(default(UserDto)); + var loggerMock = new Mock>(); + + var capturedLogs = new List<(LogLevel logLevel, Exception exception, string message)>(); + MockLogger(loggerMock, capturedLogs); + + var handler = new ProcessSignupCommandHandler(mediatorMock.Object, loggerMock.Object); + var command = new ProcessSignupCommand("test@example.com", null, "Country"); // Null password + + // Act & Assert + await Assert.ThrowsAsync(() => handler.Handle(command, CancellationToken.None)); + capturedLogs.Should().ContainSingle(x => x.message == "Password does not meet the strength requirements."); + } + + [Fact] + public async Task Handle_EmployerNotFound_CreatesUser() + { + // Arrange + var mediatorMock = new Mock(); + var loggerMock = new Mock>(); + var handler = new ProcessSignupCommandHandler(mediatorMock.Object, loggerMock.Object); + var command = new ProcessSignupCommand("newuser@example.com", "Password123!", "Country"); + + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync((EmployerIdRecord)null); // Employer not found + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(true)); // Simulate successful user creation + + // Act + await handler.Handle(command, CancellationToken.None); + + // Assert + mediatorMock.Verify(x => x.Send(It.Is(q => q.Email == "newuser@example.com"), It.IsAny()), Times.Once); + mediatorMock.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_EmployerFoundByEmail_CreatesUser() + { + // Arrange + var mediatorMock = new Mock(); + var loggerMock = new Mock>(); + var handler = new ProcessSignupCommandHandler(mediatorMock.Object, loggerMock.Object); + var command = new ProcessSignupCommand("newuser@example.com", "Password123!", "Country"); + + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync(new EmployerIdRecord("1234")); // Employer found + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .ReturnsAsync((UserDto)null); // User does not exist + mediatorMock.Setup(x => x.Send(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(true)); // Simulate successful user creation + + // Act + await handler.Handle(command, CancellationToken.None); + + // Assert + mediatorMock.Verify(x => x.Send(It.Is(q => q.Email == "newuser@example.com"), It.IsAny()), Times.Once); + mediatorMock.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Once); + mediatorMock.Verify(x => x.Send(It.IsAny(), It.IsAny()), Times.Once); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Handlers/Commands/TerminateUnlistedUsersCommandHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Commands/TerminateUnlistedUsersCommandHandlerTests.cs new file mode 100644 index 0000000..7ea0f53 --- /dev/null +++ b/src/Application.Tests/Messages/Handlers/Commands/TerminateUnlistedUsersCommandHandlerTests.cs @@ -0,0 +1,71 @@ +using Application.Messages.Commands; +using Application.Messages.Handlers.Commands; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Application.Tests.Messages.Handlers.Commands; + +[Trait("Category", "Unit")] +public class TerminateUnlistedUsersCommandHandlerTests +{ + + [Fact] + public async Task Handle_SuccessfullyTerminatesUsers() + { + // Arrange + var userServiceClientMock = new Mock(); + var handler = new TerminateUnlistedUsersCommandHandler(userServiceClientMock.Object, Mock.Of>()); + var userIds = new HashSet { "user1", "user2" }; + userServiceClientMock.Setup(client => client.TerminateUserAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + userServiceClientMock.Setup(client => client.GetUserByEmployerIdAsync("1234", It.IsAny())) + .ReturnsAsync(new List() + { + new UserDto() + { + Id = "user123" + }, + new UserDto() + { + Id = "user2234" + }, + }); + // Act + await handler.Handle(new TerminateUnlistedUsersCommand(userIds, "1234"), CancellationToken.None); + + // Assert + userServiceClientMock.Verify(client => client.TerminateUserAsync(It.IsAny(), + It.IsAny()), + Times.Exactly(userIds.Count())); + } + + [Fact] + public async Task Handle_ThrowsExceptionOnFailure() + { + // Arrange + var userServiceClientMock = new Mock(); + var handler = new TerminateUnlistedUsersCommandHandler(userServiceClientMock.Object, Mock.Of>()); + var userIds = new HashSet { "user1" }; + userServiceClientMock.Setup(client => client.TerminateUserAsync(It.IsAny(), It.IsAny())).ThrowsAsync(new InvalidOperationException()); + + // Act & Assert + await Assert.ThrowsAsync(() => handler.Handle(new TerminateUnlistedUsersCommand(userIds, "1234"), CancellationToken.None)); + } + + [Fact] + public async Task Handle_WithEmptyUserIds_DoesNotCallTerminateUserAsync() + { + // Arrange + var userServiceClientMock = new Mock(); + var loggerMock = Mock.Of>(); + var handler = new TerminateUnlistedUsersCommandHandler(userServiceClientMock.Object, loggerMock); + var emptyUserIds = new HashSet(); + + // Act + await handler.Handle(new TerminateUnlistedUsersCommand(emptyUserIds, "1234"), CancellationToken.None); + + // Assert + userServiceClientMock.Verify(client => client.TerminateUserAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Handlers/Commands/UpdateUserDataCommandHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Commands/UpdateUserDataCommandHandlerTests.cs new file mode 100644 index 0000000..619a932 --- /dev/null +++ b/src/Application.Tests/Messages/Handlers/Commands/UpdateUserDataCommandHandlerTests.cs @@ -0,0 +1,89 @@ +using Application.Messages.Commands; +using Application.Messages.Handlers.Commands; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Application.Tests.Messages.Handlers.Commands; + +[Trait("Category", "Unit")] +public class UpdateUserDataCommandHandlerTests +{ + [Fact] + public async Task Handle_UserNotFoundByEmail_ReturnsFalse() + { + // Arrange + var userServiceClientMock = new Mock(); + var loggerMock = Mock.Of>(); + var handler = new UpdateUserDataCommandHandler(new HttpClient(), loggerMock, userServiceClientMock.Object); + var command = new UpdateUserDataCommand(email: "nonexistent@example.com", country: "Country", salary: 5000, accessType: "AccessType"); + + userServiceClientMock.Setup(client => client.CheckUserByEmailAsync(It.IsAny(), It.IsAny())).ReturnsAsync((UserDto)null); + + // Act + var result = await handler.Handle(command, CancellationToken.None); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task Handle_FailureToUpdateUser_ReturnsFalse() + { + // Arrange + var userServiceClientMock = new Mock(); + var loggerMock = new Mock>(); + var handler = new UpdateUserDataCommandHandler(new HttpClient(), loggerMock.Object, userServiceClientMock.Object); + var command = new UpdateUserDataCommand(email: "test@example.com", country: "Country", salary: 5000, accessType: "AccessType"); + var userDto = new UserDto { Id = "1", Email = "test@example.com" }; + var capturedLogs = new List<(LogLevel, Exception, string)>(); + + loggerMock.Setup( + x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny()) + ).Callback((level, eventId, state, exception, formatter) => + { + var logMessage = state.ToString(); + capturedLogs.Add((level, exception, logMessage)); + }); + userServiceClientMock.Setup(client => client.CheckUserByEmailAsync(It.IsAny(), It.IsAny())).ReturnsAsync(userDto); + userServiceClientMock.Setup(client => client.UpdateUserAsync(It.IsAny(), It.IsAny())).ThrowsAsync(new System.Exception()); + + // Act + var result = await handler.Handle(command, CancellationToken.None); + + // Assert + Assert.False(result); + userServiceClientMock.Verify(client => client.CheckUserByEmailAsync("test@example.com", It.IsAny()), Times.Once); + userServiceClientMock.Verify(client => client.UpdateUserAsync(It.IsAny(), It.IsAny()), Times.Once); + Assert.Contains(capturedLogs, log => log.Item3.Contains("Error updating user data for test@example.com") && log.Item2 != null); + + } + + [Fact] + public async Task Handle_SuccessfullyUpdatesUser_ReturnsTrue() + { + // Arrange + var userServiceClientMock = new Mock(); + var loggerMock = new Mock>(); + var handler = new UpdateUserDataCommandHandler(new HttpClient(), loggerMock.Object, userServiceClientMock.Object); + var command = new UpdateUserDataCommand(email: "existing@example.com", country: "NewCountry", salary: 6000, accessType: "NewAccessType"); + var userDto = new UserDto { Id = "1", Email = "existing@example.com" }; + + userServiceClientMock.Setup(client => client.CheckUserByEmailAsync("existing@example.com", It.IsAny())).ReturnsAsync(userDto); + userServiceClientMock.Setup(client => client.UpdateUserAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + + // Act + var result = await handler.Handle(command, CancellationToken.None); + + // Assert + Assert.True(result); + userServiceClientMock.Verify(client => client.CheckUserByEmailAsync("existing@example.com", It.IsAny()), Times.Once); + userServiceClientMock.Verify(client => client.UpdateUserAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Handlers/Queries/CheckEmployerByEmailQueryHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Queries/CheckEmployerByEmailQueryHandlerTests.cs new file mode 100644 index 0000000..dcac5b5 --- /dev/null +++ b/src/Application.Tests/Messages/Handlers/Queries/CheckEmployerByEmailQueryHandlerTests.cs @@ -0,0 +1,49 @@ +using Application.Messages.Handlers.Queries; +using Application.Messages.Queries; +using Infrastructure.Configuration; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using Microsoft.Extensions.Options; +using Moq; + +namespace Application.Tests.Messages.Handlers.Queries; + +[Trait("Category", "Unit")] +public class CheckEmployerByEmailQueryHandlerTests +{ + private readonly Mock _mockEmployerServiceClient; + private readonly CheckEmployerByEmailQueryHandler _handler; + private readonly AppSettings _appSettings = new AppSettings(); + + public CheckEmployerByEmailQueryHandlerTests() + { + _mockEmployerServiceClient = new Mock(); + _handler = new CheckEmployerByEmailQueryHandler(_mockEmployerServiceClient.Object, Options.Create(_appSettings)); + } + + [Fact] + public async Task ReturnsEmployerIdRecord_WhenEmployerExists() + { + var email = "existing@example.com"; + var expectedRecord = new EmployerIdRecord( "12345"); + _mockEmployerServiceClient.Setup(x => x.GetEmployerByEmailAsync(email, It.IsAny())) + .ReturnsAsync(expectedRecord); + + var result = await _handler.Handle(new CheckEmployerByEmailQuery(email), CancellationToken.None); + + Assert.NotNull(result); + Assert.Equal(expectedRecord.Id, result?.Id); + } + + [Fact] + public async Task ReturnsNull_WhenEmployerDoesNotExist() + { + var email = "nonexisting@example.com"; + _mockEmployerServiceClient.Setup(x => x.GetEmployerByEmailAsync(email, It.IsAny())) + .ReturnsAsync((EmployerIdRecord?)null); + + var result = await _handler.Handle(new CheckEmployerByEmailQuery(email), CancellationToken.None); + + Assert.Null(result); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Handlers/Queries/GetEmployerByIdQueryHandlerTests.cs b/src/Application.Tests/Messages/Handlers/Queries/GetEmployerByIdQueryHandlerTests.cs new file mode 100644 index 0000000..c3d5052 --- /dev/null +++ b/src/Application.Tests/Messages/Handlers/Queries/GetEmployerByIdQueryHandlerTests.cs @@ -0,0 +1,6 @@ +namespace Application.Tests.Messages.Handlers.Queries; + +public class GetEmployerByIdQueryHandlerTests +{ + +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/Commands/CreateUserCommandValidatorTests.cs b/src/Application.Tests/Messages/Validators/Commands/CreateUserCommandValidatorTests.cs new file mode 100644 index 0000000..4e05468 --- /dev/null +++ b/src/Application.Tests/Messages/Validators/Commands/CreateUserCommandValidatorTests.cs @@ -0,0 +1,93 @@ +using Application.Messages.Validators.Commands; +using Application.Tests.Factories; + +namespace Application.Tests.Messages.Validators.Commands; + +[Trait("Category", "Unit")] +public class CreateUserCommandValidatorTests +{ + private readonly CreateUserCommandValidator _validator; + + public CreateUserCommandValidatorTests() + { + _validator = new CreateUserCommandValidator(); + } + [Fact] + public void Email_IsRequired() + { + // Arrange + var command = new CreateUserCommandFactory() + .WithEmail(string.Empty) + .Create(); + + // Act + var result = _validator.Validate(command); + + // Assert + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Email"); + } + + [Fact] + public void Password_IsRequired() + { + var command = new CreateUserCommandFactory().WithPassword(string.Empty).Create(); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Password"); + } + + [Fact] + public void Country_IsRequired() + { + var command = new CreateUserCommandFactory().WithCountry(string.Empty).Create(); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Country"); + } + + [Fact] + public void AccessType_IsRequired() + { + var command = new CreateUserCommandFactory().WithAccessType(null).Create(); // Assuming WithAccessType method is implemented to handle null + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "AccessType"); + } + + [Fact] + public void FullName_WhenProvided_MustNotBeEmpty() + { + var command = new CreateUserCommandFactory().WithFullName(string.Empty).Create(); // Assuming WithFullName method is implemented + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "FullName" && e.ErrorMessage.Contains("must not be empty if provided")); + } + + [Fact] + public void EmployerId_WhenProvided_MustNotBeEmpty() + { + var command = new CreateUserCommandFactory().WithEmployerId(string.Empty).Create(); // Assuming WithEmployerId method is implemented + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "EmployerId" && e.ErrorMessage.Contains("must not be empty if provided")); + } + + [Fact] + public void BirthDate_WhenProvided_MustBeValid() + { + var command = new CreateUserCommandFactory().WithBirthDate(DateTime.UtcNow.AddDays(1)).Create(); // Assuming WithBirthDate method is implemented + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "BirthDate" && e.ErrorMessage.Contains("must be a valid date if provided")); + } + + [Fact] + public void Salary_WhenProvided_MustBeNonNegative() + { + var command = new CreateUserCommandFactory().WithSalary(-1).Create(); // Assuming WithSalary method is implemented + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Salary" && e.ErrorMessage.Contains("must be a non-negative number if provided")); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/Commands/ProcessEligibilityFileCommandValidatorTests.cs b/src/Application.Tests/Messages/Validators/Commands/ProcessEligibilityFileCommandValidatorTests.cs new file mode 100644 index 0000000..0980061 --- /dev/null +++ b/src/Application.Tests/Messages/Validators/Commands/ProcessEligibilityFileCommandValidatorTests.cs @@ -0,0 +1,69 @@ +using Application.Messages.Commands; + +namespace Application.Tests.Messages.Validators.Commands; + +[Trait("Category", "Unit")] +public class ProcessEligibilityFileCommandValidatorTests +{ + private readonly ProcessEligibilityFileCommandValidator _validator; + + public ProcessEligibilityFileCommandValidatorTests() + { + _validator = new ProcessEligibilityFileCommandValidator(); + } + + [Fact] + public void CsvFileUrl_MustBeValidUrl() + { + // Arrange + var command = new ProcessEligibilityFileCommand("invalid-url", "ValidEmployerName"); + + // Act + var result = _validator.Validate(command); + + // Assert + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "CsvFileUrl" && e.ErrorMessage.Contains("valid URL")); + } + + [Fact] + public void CsvFileUrl_IsRequired() + { + // Arrange + var command = new ProcessEligibilityFileCommand(null, "ValidEmployerName"); + + // Act + var result = _validator.Validate(command); + + // Assert + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "CsvFileUrl"); + } + + [Fact] + public void EmployerName_IsRequired() + { + // Arrange + var command = new ProcessEligibilityFileCommand("http://validurl.com", null); + + // Act + var result = _validator.Validate(command); + + // Assert + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "EmployerName"); + } + + [Fact] + public void Command_WithValidData_ShouldPassValidation() + { + // Arrange + var command = new ProcessEligibilityFileCommand("http://validurl.com", "ValidEmployerName"); + + // Act + var result = _validator.Validate(command); + + // Assert + Assert.True(result.IsValid); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/Commands/ProcessSignupCommandValidatorTests.cs b/src/Application.Tests/Messages/Validators/Commands/ProcessSignupCommandValidatorTests.cs new file mode 100644 index 0000000..059322f --- /dev/null +++ b/src/Application.Tests/Messages/Validators/Commands/ProcessSignupCommandValidatorTests.cs @@ -0,0 +1,67 @@ +using Application.Messages.Commands; +using Application.Messages.Validators.Commands; + +namespace Application.Tests.Messages.Validators.Commands; + +[Trait("Category", "Unit")] +public class ProcessSignupCommandValidatorTests +{ + private ProcessSignupCommandValidator _validator; + + public ProcessSignupCommandValidatorTests() + { + _validator = new ProcessSignupCommandValidator(); + } + [Fact] + public void Email_IsRequired() + { + var command = new ProcessSignupCommand(null, "ValidPassword123", "CountryName"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Email"); + } + + [Fact] + public void Email_MustBeValidEmail() + { + var command = new ProcessSignupCommand("invalid-email", "ValidPassword123", "CountryName"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Email"); + } + + [Fact] + public void Password_IsRequired() + { + var command = new ProcessSignupCommand("email@example.com", null, "CountryName"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Password"); + } + + [Fact] + public void Password_MustBeAtLeast8CharactersLong() + { + var command = new ProcessSignupCommand("email@example.com", "short", "CountryName"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Password"); + } + + [Fact] + public void Country_IsRequired() + { + var command = new ProcessSignupCommand("email@example.com", "ValidPassword123", null); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Country"); + } + + [Fact] + public void Command_WithValidData_ShouldPassValidation() + { + var command = new ProcessSignupCommand("email@example.com", "ValidPassword123", "CountryName"); + var result = _validator.Validate(command); + Assert.True(result.IsValid); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/Commands/TerminateUnlistedUsersCommandValidatorTests.cs b/src/Application.Tests/Messages/Validators/Commands/TerminateUnlistedUsersCommandValidatorTests.cs new file mode 100644 index 0000000..9a40dca --- /dev/null +++ b/src/Application.Tests/Messages/Validators/Commands/TerminateUnlistedUsersCommandValidatorTests.cs @@ -0,0 +1,32 @@ +using Application.Messages.Commands; +using Application.Messages.Validators.Commands; +using FluentValidation.TestHelper; + +namespace Application.Tests.Messages.Validators.Commands; + +[Trait("Category", "Unit")] +public class TerminateUnlistedUsersCommandValidatorTests +{ + private readonly TerminateUnlistedUsersCommandValidator _validator; + + public TerminateUnlistedUsersCommandValidatorTests() + { + _validator = new TerminateUnlistedUsersCommandValidator(); + } + + [Fact] + public void EmployerId_IsRequired() + { + var command = new TerminateUnlistedUsersCommand(new HashSet(), null); + var result = _validator.TestValidate(command); + result.ShouldHaveValidationErrorFor(c => c.EmployerId); + } + + [Fact] + public void Command_WithValidEmployerId_ShouldPassValidation() + { + var command = new TerminateUnlistedUsersCommand(new HashSet(), "validEmployerId"); + var result = _validator.TestValidate(command); + result.ShouldNotHaveValidationErrorFor(c => c.EmployerId); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/Commands/UpdateUserDataCommandValidatorTests.cs b/src/Application.Tests/Messages/Validators/Commands/UpdateUserDataCommandValidatorTests.cs new file mode 100644 index 0000000..feea0ea --- /dev/null +++ b/src/Application.Tests/Messages/Validators/Commands/UpdateUserDataCommandValidatorTests.cs @@ -0,0 +1,68 @@ +using Application.Messages.Commands; +using Application.Messages.Validators.Commands; + +namespace Application.Tests.Messages.Validators.Commands; + +[Trait("Category", "Unit")] +public class UpdateUserDataCommandValidatorTests +{ + private readonly UpdateUserDataCommandValidator _validator; + + public UpdateUserDataCommandValidatorTests() + { + _validator = new UpdateUserDataCommandValidator(); + } + + [Fact] + public void Email_IsRequired() + { + var command = new UpdateUserDataCommand(null, "CountryName", 50000, "User"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Email"); + } + + [Fact] + public void Email_MustBeValidEmail() + { + var command = new UpdateUserDataCommand("invalid-email", "CountryName", 50000, "User"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Email"); + } + + [Fact] + public void Country_IsRequired() + { + var command = new UpdateUserDataCommand("email@example.com", null, 50000, "User"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Country"); + } + + [Fact] + public void Salary_MustBePositive_WhenProvided() + { + var command = new UpdateUserDataCommand("email@example.com", "CountryName", -1, "User"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "Salary"); + } + + [Fact] + public void AccessType_MustMatchSpecificValues() + { + var command = new UpdateUserDataCommand("email@example.com", "CountryName", 50000, "InvalidAccessType"); + var result = _validator.Validate(command); + Assert.False(result.IsValid); + Assert.Contains(result.Errors, e => e.PropertyName == "AccessType"); + } + + [Fact] + public void Command_WithValidData_ShouldPassValidation() + { + var command = new UpdateUserDataCommand("email@example.com", "CountryName", 50000, "User"); + var result = _validator.Validate(command); + Assert.True(result.IsValid); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/CountryCodeValidatorTests.cs b/src/Application.Tests/Messages/Validators/CountryCodeValidatorTests.cs new file mode 100644 index 0000000..6aed66c --- /dev/null +++ b/src/Application.Tests/Messages/Validators/CountryCodeValidatorTests.cs @@ -0,0 +1,45 @@ +using Application.Messages.Validators; + +namespace Application.Tests.Messages.Validators; + +[Trait("Category", "Unit")] +public class CountryCodeValidatorTests +{ + [Fact] + public void TestValidCountryCode_ShouldReturnTrue() + { + // Arrange + var validCode = "US"; // Assuming "US" is in the list + + // Act + var result = CountryCodeValidator.IsValidIso3166CountryCode(validCode); + + // Assert + Assert.True(result); + } + + [Fact] + public void TestInvalidCountryCode_ShouldReturnFalse() + { + // Arrange + var invalidCode = "XX"; // Assuming "XX" is not a valid code + + // Act + var result = CountryCodeValidator.IsValidIso3166CountryCode(invalidCode); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void TestEmptyOrNullCountryCode_ShouldReturnFalse(string code) + { + // Act + var result = CountryCodeValidator.IsValidIso3166CountryCode(code); + + // Assert + Assert.False(result); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/Queries/CheckEmployerByEmailValidatorTests.cs b/src/Application.Tests/Messages/Validators/Queries/CheckEmployerByEmailValidatorTests.cs new file mode 100644 index 0000000..56b424e --- /dev/null +++ b/src/Application.Tests/Messages/Validators/Queries/CheckEmployerByEmailValidatorTests.cs @@ -0,0 +1,41 @@ +using Application.Messages.Queries; +using Application.Messages.Validators.Queries; +using FluentValidation.TestHelper; + +namespace Application.Tests.Messages.Validators.Queries; + +[Trait("Category", "Unit")] +public class CheckEmployerByEmailQueryValidatorTests +{ + + private readonly CheckEmployerByEmailQueryValidator _validator; + + public CheckEmployerByEmailQueryValidatorTests() + { + _validator = new CheckEmployerByEmailQueryValidator(); + } + + [Fact] + public void Email_IsRequired() + { + var query = new CheckEmployerByEmailQuery(null); + var result = _validator.TestValidate(query); + result.ShouldHaveValidationErrorFor(q => q.Email); + } + + [Fact] + public void Email_MustBeValidEmail() + { + var query = new CheckEmployerByEmailQuery("invalid-email"); + var result = _validator.TestValidate(query); + result.ShouldHaveValidationErrorFor(q => q.Email); + } + + [Fact] + public void Query_WithValidEmail_ShouldPassValidation() + { + var query = new CheckEmployerByEmailQuery("email@example.com"); + var result = _validator.TestValidate(query); + result.ShouldNotHaveValidationErrorFor(q => q.Email); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/Queries/GetEmployerByIdQueryValidatorTests.cs b/src/Application.Tests/Messages/Validators/Queries/GetEmployerByIdQueryValidatorTests.cs new file mode 100644 index 0000000..b0d5e83 --- /dev/null +++ b/src/Application.Tests/Messages/Validators/Queries/GetEmployerByIdQueryValidatorTests.cs @@ -0,0 +1,40 @@ +using Application.Messages.Queries; +using Application.Messages.Validators.Queries; +using FluentValidation.TestHelper; + +namespace Application.Tests.Messages.Validators.Queries; + +[Trait("Category", "Unit")] +public class GetEmployerByIdQueryValidatorTests +{ + private readonly GetEmployerByIdQueryValidator _validator; + + public GetEmployerByIdQueryValidatorTests() + { + _validator = new GetEmployerByIdQueryValidator(); + } + + [Fact] + public void Id_IsRequired() + { + var query = new GetEmployerByIdQuery(null); + var result = _validator.TestValidate(query); + result.ShouldHaveValidationErrorFor(q => q.Id); + } + + [Fact] + public void Id_MustBeAlphanumericWithDashes() + { + var query = new GetEmployerByIdQuery("invalid id"); + var result = _validator.TestValidate(query); + result.ShouldHaveValidationErrorFor(q => q.Id); + } + + [Fact] + public void Query_WithValidId_ShouldPassValidation() + { + var query = new GetEmployerByIdQuery("valid-id-123"); + var result = _validator.TestValidate(query); + result.ShouldNotHaveValidationErrorFor(q => q.Id); + } +} \ No newline at end of file diff --git a/src/Application.Tests/Messages/Validators/Queries/GetUserByEmailQueryValidatorTests.cs b/src/Application.Tests/Messages/Validators/Queries/GetUserByEmailQueryValidatorTests.cs new file mode 100644 index 0000000..42e5122 --- /dev/null +++ b/src/Application.Tests/Messages/Validators/Queries/GetUserByEmailQueryValidatorTests.cs @@ -0,0 +1,40 @@ +using Application.Messages.Queries; +using Application.Messages.Validators.Queries; +using FluentValidation.TestHelper; + +namespace Application.Tests.Messages.Validators.Queries; + +[Trait("Category", "Unit")] +public class GetUserByEmailQueryValidatorTests +{ + private readonly GetUserByEmailQueryValidator _validator; + + public GetUserByEmailQueryValidatorTests() + { + _validator = new GetUserByEmailQueryValidator(); + } + + [Fact] + public void Email_IsRequired() + { + var query = new GetUserByEmailQuery(null); + var result = _validator.TestValidate(query); + result.ShouldHaveValidationErrorFor(q => q.Email); + } + + [Fact] + public void Email_MustBeValidEmail() + { + var query = new GetUserByEmailQuery("invalid-email"); + var result = _validator.TestValidate(query); + result.ShouldHaveValidationErrorFor(q => q.Email); + } + + [Fact] + public void Query_WithValidEmail_ShouldPassValidation() + { + var query = new GetUserByEmailQuery("email@example.com"); + var result = _validator.TestValidate(query); + result.ShouldNotHaveValidationErrorFor(q => q.Email); + } +} \ No newline at end of file diff --git a/src/Application.md b/src/Application.md new file mode 100644 index 0000000..8a9f37e --- /dev/null +++ b/src/Application.md @@ -0,0 +1,113 @@ +# Application Functionality + +This application is a backend system developed using C# and the ASP.NET Core framework. It is designed to process eligibility files and user registrations. Below are the details on how each part of the application operates. + +## Project Structure + +The project is divided into several layers, including: + +- **Domain/Models**: Contains the data model definitions used in the application. +- **Application/Messages/Commands**: Defines the commands for interacting with the system through the CQRS (Command Query Responsibility Segregation) pattern. +- **Origin.API/Controllers**: Contains the controllers that expose API endpoints for external interaction. + +## Data Models + +### EligibilityFile + +- **File**: Path or content of the eligibility file. +- **EmployerName**: Name of the employer related to the file. + +### SignupModel + +- **Email**: User's email address. +- **Password**: User's password. +- **Country**: User's country. + +## Controllers + +### EligibilityFileController + +- **Endpoint**: `POST /api/eligibility/process` +- **Description**: Receives an eligibility file and the employer's name, processes this information, and returns a result. +- **Conditions for Calling**: This endpoint is called when there is a new eligibility file to be processed. + +### SignupController + +- **Endpoint**: `POST /api/signup` +- **Description**: Receives registration information for a new user, such as email, password, and country, and processes the registration. +- **Conditions for Calling**: This endpoint is called when a new user wishes to register in the system. + +## Processing + +- **ProcessEligibilityFileCommand**: Command sent by `EligibilityFileController` to process the eligibility file. +- **ProcessSignupCommand**: Command sent by `SignupController` to process the registration of a new user. + +## Technologies and Patterns Used + +- **ASP.NET Core**: Framework for building web applications and APIs. +- **MediatR**: Library for implementing the Mediator pattern, facilitating communication between application components. +- **CQRS**: Pattern used to separate the execution of commands (actions that change state) from queries (actions that return data). + +## External Service Endpoints + +This section describes the external service endpoints that the application interacts with, including the User Service and the Employer Service. + +### User Service + +The User Service manages user information and authentication. The application interacts with the following endpoints: + +- **POST /users** + - **Description**: Registers a new user with the required details. + - **Payload**: Includes `email`, `password`, `country`, `access_type`, and optionally `full_name`, `employer_id`, `birth_date`, `salary`. + - **Conditions for Calling**: Called during the user registration process. + +- **GET /users?email=value** + - **Description**: Retrieves user information based on the email address. + - **Query Parameter**: `email` + - **Conditions for Calling**: Used to verify if a user already exists with the given email. + +- **GET /users/{id}** + - **Description**: Fetches details of a user by their unique identifier. + - **Path Parameter**: `id` + - **Conditions for Calling**: Used to retrieve user details post-registration or for user profile management. + +- **PATCH /users/{id}** + - **Description**: Updates specific fields of a user's information. + - **Path Parameter**: `id` + - **Payload**: An array of objects specifying the `field` to update and its new `value`. + - **Conditions for Calling**: Called to update user information, such as changing the country. +### UserServiceClient + +The `UserServiceClient` serves as an abstraction for interacting with the User Service. It encapsulates the HTTP requests to the User Service endpoints, providing a simplified interface for the application components. Below are the functionalities provided by the `UserServiceClient`: + +- **CreateUser**: Invokes the `POST /users` endpoint to register a new user. This is typically called during the user registration process within the application. + +- **GetUserByEmail**: Utilizes the `GET /users?email=value` endpoint to retrieve user information based on their email address. This function is essential for checking if a user already exists before attempting to create a new one. + +- **GetUserById**: Calls the `GET /users/{id}` endpoint to fetch detailed information about a user by their unique identifier. This is used for user profile management and retrieval operations within the application. + +- **UpdateUser**: Makes a request to the `PATCH /users/{id}` endpoint to update specific fields of a user's information. This could be used for various purposes, such as updating a user's country or other profile details. + +These functionalities ensure that the application can manage user information effectively without directly handling the HTTP request logic, thereby promoting separation of concerns and simplifying the codebase. +### Employer Service + +The Employer Service manages employer-related information. The application interacts with the following endpoint: + +- **GET /employers?name=value** + - **Description**: Searches for employers by name. + - **Query Parameter**: `name` + - **Conditions for Calling**: Used to validate employer names during the eligibility file processing or user registration when an employer's name is provided. +### EmployerServiceClient + +The `EmployerServiceClient` is designed to facilitate interactions with the Employer Service, abstracting the complexities of HTTP requests and responses. It provides methods for creating employers and retrieving employer information by ID or email. Here's an overview of its capabilities: + +- **CreateEmployerAsync**: This method attempts to create a new employer with the provided email. It calls an unspecified endpoint, likely `POST /employers`, and handles HTTP responses. Successful creation results in a `StatusCode` of `Created`, while a `BadRequest` response indicates a failure in creation due to invalid data. + +- **GetEmployerByIdAsync**: Fetches employer details based on a unique identifier. This method likely interacts with an endpoint such as `GET /employers/{id}`, where `{id}` is the employer's unique identifier. It returns an `EmployerDto` object if the employer is found (`StatusCode` `OK`), and `null` if not found (`StatusCode` `NotFound`). + +- **GetEmployerByEmailAsync**: Similar to `GetEmployerByIdAsync`, this method retrieves employer details based on their email. It likely calls an endpoint such as `GET /employers?email=value`, returning an `EmployerIdRecord` if the employer exists or `null` if there is no employer with the specified email. + +These methods ensure that the application can manage employer information efficiently, leveraging the `EmployerServiceClient` for clean and maintainable code. +## Conclusion + +This application exemplifies a simple system for processing eligibility files and user registrations, using modern software development practices such as ASP.NET Core, CQRS, and the Mediator pattern with the MediatR library. \ No newline at end of file diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj new file mode 100644 index 0000000..6ff7949 --- /dev/null +++ b/src/Application/Application.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/src/Application/Common/Behaviours/LoggingBehaviour.cs b/src/Application/Common/Behaviours/LoggingBehaviour.cs new file mode 100644 index 0000000..8343119 --- /dev/null +++ b/src/Application/Common/Behaviours/LoggingBehaviour.cs @@ -0,0 +1,36 @@ +namespace Application.Common.Behaviours; +using MediatR; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; + +public class LoggingBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly ILogger> _logger; + + public LoggingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("Handling {Name} with request: {request} - started.", typeof(TRequest).Name, + request); + + var response = await next(); + + _logger.LogInformation("Handled {Name} - done.", typeof(TRequest).Name); + return response; + } + catch (Exception e) + { + _logger.LogError(e, "Error handling {Name}.", typeof(TRequest).Name); + throw; + } + + } +} \ No newline at end of file diff --git a/src/Application/Common/Behaviours/ValidationBehaviour.cs b/src/Application/Common/Behaviours/ValidationBehaviour.cs new file mode 100644 index 0000000..ad830cc --- /dev/null +++ b/src/Application/Common/Behaviours/ValidationBehaviour.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using MediatR; + +namespace Application.Common.Behaviours; + +public class ValidationBehaviour : IPipelineBehavior + where TRequest : notnull +{ + private readonly IEnumerable> _validators; + + public ValidationBehaviour(IEnumerable> validators) + { + _validators = validators; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + if (_validators.Any()) + { + var context = new ValidationContext(request); + + var validationResults = await Task.WhenAll( + _validators.Select(v => + v.ValidateAsync(context, cancellationToken))); + + var failures = validationResults + .Where(r => r.Errors.Any()) + .SelectMany(r => r.Errors) + .ToList(); + + if (failures.Any()) + throw new ValidationException(failures); + } + return await next(); + } +} \ No newline at end of file diff --git a/src/Application/DTOs/CountryCode.cs b/src/Application/DTOs/CountryCode.cs new file mode 100644 index 0000000..b24cf3d --- /dev/null +++ b/src/Application/DTOs/CountryCode.cs @@ -0,0 +1,8 @@ +namespace Application.DTOs; + +public class CountryCodeDto +{ + public string Name { get; set; } + public string Alpha2 { get; set; } + +} \ No newline at end of file diff --git a/src/Application/DTOs/CsvLineModel.cs b/src/Application/DTOs/CsvLineModel.cs new file mode 100644 index 0000000..bfa09cd --- /dev/null +++ b/src/Application/DTOs/CsvLineModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace Application.DTOs; + +public class CsvLineModel +{ + [Required] + public string Email { get; set; } + public string? FullName { get; set; } + [Required] + public string Country { get; set; } + public string BirthDate { get; set; } + public decimal? Salary { get; set; } +} \ No newline at end of file diff --git a/src/Application/Messages/Commands/CreateUserCommand.cs b/src/Application/Messages/Commands/CreateUserCommand.cs new file mode 100644 index 0000000..bf37137 --- /dev/null +++ b/src/Application/Messages/Commands/CreateUserCommand.cs @@ -0,0 +1,39 @@ +using MediatR; + +namespace Application.Messages.Commands; + +public class CreateUserCommand : IRequest +{ + public string Email { get; set; } + public string Password { get; set; } + public string Country { get; set; } + public string AccessType { get; set; } + public string? FullName { get; set; } + public string? EmployerId { get; set; } + public DateTime? BirthDate { get; set; } + public decimal? Salary { get; set; } + + public CreateUserCommand(string email, + string password, + string country, + string accessType, + string? fullName, + string? employerId, + DateTime? birthDate, + decimal? salary) + { + Email = email; + Password = password; + Country = country; + AccessType = accessType; + FullName = fullName; + EmployerId = employerId; + BirthDate = birthDate; + Salary = salary; + } + + public CreateUserCommand() + { + + } +} \ No newline at end of file diff --git a/src/Application/Messages/Commands/ProcessEligibilityFileCommand.cs b/src/Application/Messages/Commands/ProcessEligibilityFileCommand.cs new file mode 100644 index 0000000..b98bb0b --- /dev/null +++ b/src/Application/Messages/Commands/ProcessEligibilityFileCommand.cs @@ -0,0 +1,16 @@ +using Domain.Models; +using MediatR; + +namespace Application.Messages.Commands; + +public class ProcessEligibilityFileCommand : IRequest +{ + public string CsvFileUrl { get; init; } + public string EmployerName { get; init; } + + public ProcessEligibilityFileCommand(string csvFileUrl, string employerName) + { + CsvFileUrl = csvFileUrl; + EmployerName = employerName; + } +} \ No newline at end of file diff --git a/src/Application/Messages/Commands/ProcessSignupCommand.cs b/src/Application/Messages/Commands/ProcessSignupCommand.cs new file mode 100644 index 0000000..c313d74 --- /dev/null +++ b/src/Application/Messages/Commands/ProcessSignupCommand.cs @@ -0,0 +1,18 @@ +using MediatR; + +namespace Application.Messages.Commands; + +public class ProcessSignupCommand : IRequest +{ + public string Email { get; set; } + public string Password { get; set; } + public string Country { get; set; } + + public ProcessSignupCommand(string email, string password, string country) + { + Email = email; + Password = password; + Country = country; + } + +} \ No newline at end of file diff --git a/src/Application/Messages/Commands/TerminateUnlistedUsersCommand.cs b/src/Application/Messages/Commands/TerminateUnlistedUsersCommand.cs new file mode 100644 index 0000000..2d9a131 --- /dev/null +++ b/src/Application/Messages/Commands/TerminateUnlistedUsersCommand.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace Application.Messages.Commands; + +public class TerminateUnlistedUsersCommand : IRequest +{ + public HashSet UserIds { get; } + + public string EmployerId { get; set; } + + public TerminateUnlistedUsersCommand(HashSet userIds, string employerId) + { + UserIds = userIds; + EmployerId = employerId; + } +} \ No newline at end of file diff --git a/src/Application/Messages/Commands/UpdateUserDataCommand.cs b/src/Application/Messages/Commands/UpdateUserDataCommand.cs new file mode 100644 index 0000000..d60d926 --- /dev/null +++ b/src/Application/Messages/Commands/UpdateUserDataCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace Application.Messages.Commands; + +public class UpdateUserDataCommand: IRequest +{ + public string Email { get; set; } + public string Country { get; set; } + public decimal? Salary { get; set; } + public string AccessType { get; set; } + + public UpdateUserDataCommand(string email, string country, decimal? salary, string accessType) + { + Email = email; + Country = country; + Salary = salary; + AccessType = accessType; + } +} \ No newline at end of file diff --git a/src/Application/Messages/Handlers/Commands/CreateUserCommandHandler.cs b/src/Application/Messages/Handlers/Commands/CreateUserCommandHandler.cs new file mode 100644 index 0000000..6fd58f6 --- /dev/null +++ b/src/Application/Messages/Handlers/Commands/CreateUserCommandHandler.cs @@ -0,0 +1,32 @@ +using Application.Messages.Commands; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using MediatR; + +namespace Application.Messages.Handlers.Commands; + +public class CreateUserCommandHandler : IRequestHandler +{ + private readonly IUserServiceClient _userServiceClient; + + public CreateUserCommandHandler(IUserServiceClient userServiceClient) + { + _userServiceClient = userServiceClient; + } + + public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) + { + var result = await _userServiceClient.CreateUserAsync(new CreateUserDto + { + Email = request.Email, + Password = request.Password, + Country = request.Country, + AccessType = request.AccessType, + FullName = request.FullName, + EmployerId = request.EmployerId, + BirthDate= request.BirthDate, + Salary = request.Salary + }, cancellationToken); + return result; + } +} \ No newline at end of file diff --git a/src/Application/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandler.cs b/src/Application/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandler.cs new file mode 100644 index 0000000..e13398b --- /dev/null +++ b/src/Application/Messages/Handlers/Commands/ProcessEligibilityFileCommandHandler.cs @@ -0,0 +1,162 @@ +using System.Text; +using Application.DTOs; +using Application.Messages.Commands; +using Domain.Models; +using MediatR; +using Microsoft.Extensions.Logging; +using Application.Messages.Queries; +using Domain.Enums; +using Infrastructure.Records; + +namespace Application.Messages.Handlers.Commands; + +public class ProcessEligibilityFileCommandHandler : IRequestHandler +{ + private readonly IMediator _mediator; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public ProcessEligibilityFileCommandHandler(IHttpClientFactory httpClientFactory, + ILogger logger, + IMediator mediator) + { + _httpClient = httpClientFactory.CreateClient(); + _logger = logger; + _mediator = mediator; + } + + public async Task Handle(ProcessEligibilityFileCommand request, CancellationToken cancellationToken) + { + // Step 1: Download the CSV file from the provided URL + var (result ,hashUsers) = await DownloadCsv(request.CsvFileUrl); + + await TerminateUnlistedUsers(hashUsers, request.EmployerName); + return result; + } + + private async Task<(EligibilityProcessResult, HashSet)> DownloadCsv(string requestCsvFileUrl) + { + var result = new EligibilityProcessResult(); + var hashUsers = new HashSet(); + try + { + var response = await _httpClient.GetAsync(requestCsvFileUrl, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + var firstLine = true; + var index = 0; + using (var stream = await response.Content.ReadAsStreamAsync()) + using (var reader = new StreamReader(stream)) + { + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (firstLine) // Assuming the first line is headers + { + firstLine = false; + continue; + } + index++; + + await ProcessLine(line, result, hashUsers, index); + } + } + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw; + } + + return (result, hashUsers); + } + + private async Task ProcessLine( + string? line, EligibilityProcessResult result, HashSet hashUsers, int index) + { + var processedLineModels = ParseCsvLines(line); + foreach (var processedLineModel in processedLineModels) + { + try + { + var user = await GetUser(processedLineModel.Email); + if (user != null) + { + await UpdateUserData(processedLineModel, user); + hashUsers.Add(user.Id); + } + else result.NonProcessedLines.Add(index); + + result.ProcessedLines.Add(index); + } + catch (Exception ex) + { + result.Errors.Add(ex.Message); + } + } + } + + private async Task TerminateUnlistedUsers(HashSet hashUserIds, string employerId) + { + await _mediator.Send(new TerminateUnlistedUsersCommand(hashUserIds, employerId)); + } + + private async Task UpdateUserData(CsvLineModel line, UserDto user) + { + await _mediator.Send(new UpdateUserDataCommand(user.Email, line.Country, line.Salary, AccessTypeEnum.Employer)); + } + + private async Task GetUser(string lineEmail) + { + return await _mediator.Send(new GetUserByEmailQuery(lineEmail)); + } + + private IEnumerable ParseCsvLines(string? csvData) + { + if (string.IsNullOrWhiteSpace(csvData)) return Enumerable.Empty(); + var lines = csvData.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + var csvLineModels = new List(); + + foreach (var line in lines) + { + var values = SplitCsvLine(line); + var csvLineModel = new CsvLineModel + { + Email = values[0], + FullName = values.Length > 1 ? values[1] : null, + Country = values.Length > 2 ? values[2] : null, + BirthDate = values.Length > 3 ? values[3] : null, + Salary = values.Length > 4 && decimal.TryParse(values[4], out var salary) ? salary : null + }; + csvLineModels.Add(csvLineModel); + } + + return csvLineModels; + } + + private string?[] SplitCsvLine(string line) + { + var values = new List(); + var column = new StringBuilder(); + bool inQuotes = false; + + foreach (var character in line) + { + if (character == '\"') + { + inQuotes = !inQuotes; + } + else if (character == ',' && !inQuotes) + { + values.Add(column.ToString()); + column.Clear(); + } + else + { + column.Append(character); + } + } + + values.Add(column.ToString()); // Add the last column + return values.ToArray(); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Handlers/Commands/ProcessSignupCommandHandler.cs b/src/Application/Messages/Handlers/Commands/ProcessSignupCommandHandler.cs new file mode 100644 index 0000000..f77e775 --- /dev/null +++ b/src/Application/Messages/Handlers/Commands/ProcessSignupCommandHandler.cs @@ -0,0 +1,76 @@ +using Application.Messages.Commands; +using Application.Messages.Queries; +using Domain.Enums; +using Infrastructure.Records; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace Application.Messages.Handlers.Commands; + +public class ProcessSignupCommandHandler : IRequestHandler +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public ProcessSignupCommandHandler(IMediator mediator, ILogger logger) + { + _mediator = mediator; + _logger = logger; + } + + public async Task Handle(ProcessSignupCommand request, CancellationToken cancellationToken) + { + var employerData = await _mediator.Send(new CheckEmployerByEmailQuery(request.Email), cancellationToken); + if (employerData == null) + { + await CreateUser(request.Email, request.Password, request.Country, employerData); + return; + } + + var existingUser = await _mediator.Send(new GetUserByEmailQuery(request.Email), cancellationToken); + if (existingUser != null) + { + _logger.LogError($"User with email {request.Email} already exists."); + throw new InvalidOperationException("User already exists."); + } + + if (!IsValidPassword(request.Password)) + { + _logger.LogError("Password does not meet the strength requirements."); + throw new InvalidOperationException("Password does not meet the strength requirements."); + } + + await CreateUser(request.Email, request.Password, request.Country, employerData); + } + + // Minimum 8 characters, letters, symbols, and numbers + private bool IsValidPassword(string password) + { + return !string.IsNullOrWhiteSpace(password) && + (password.Length >= 8 && + password.Any(char.IsLetter) && + password.Any(char.IsDigit) && + password.Any(ch => !char.IsLetterOrDigit(ch))); + } + + private async Task CreateUser(string email, string password, string country, EmployerIdRecord? employerData) + { + EmployerDto? employerDto = null; + if (employerData != null) + { + _logger.LogInformation("Extracting Employer information for Id: {Id}.", employerData.Id); + employerDto = await _mediator.Send(new GetEmployerByIdQuery(employerData.Id)); + } + + _logger.LogInformation("Creating user for {Email}.", email); + await _mediator.Send(new CreateUserCommand(email, + password, + country, + AccessTypeEnum.Employer, + employerDto?.FullName, + employerData?.Id, + employerDto?.BirthDate, + employerDto?.Salary)); + // Use _mediator to send a command to create the user + } +} \ No newline at end of file diff --git a/src/Application/Messages/Handlers/Commands/TerminateUnlistedUsersCommandHandler.cs b/src/Application/Messages/Handlers/Commands/TerminateUnlistedUsersCommandHandler.cs new file mode 100644 index 0000000..f7253fa --- /dev/null +++ b/src/Application/Messages/Handlers/Commands/TerminateUnlistedUsersCommandHandler.cs @@ -0,0 +1,32 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using Application.Messages.Commands; +using Infrastructure.Services.Interfaces; + +namespace Application.Messages.Handlers.Commands; + +public class TerminateUnlistedUsersCommandHandler : IRequestHandler +{ + private readonly IUserServiceClient _userServiceClient; + private readonly ILogger _logger; + + public TerminateUnlistedUsersCommandHandler(IUserServiceClient userServiceClient, ILogger logger) + { + _userServiceClient = userServiceClient; + _logger = logger; + } + + /// + /// Get the list of users for specific employer, and terminate the users that are not in the list + /// + /// + /// + public async Task Handle(TerminateUnlistedUsersCommand request, CancellationToken cancellationToken) + { + var existingUserId = await _userServiceClient.GetUserByEmployerIdAsync(request.EmployerId, cancellationToken); + foreach (var userId in request.UserIds.Where(userId => existingUserId.All(existingUser => existingUser.Id != userId))) + { + await _userServiceClient.TerminateUserAsync(userId, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Messages/Handlers/Commands/UpdateUserDataCommandHandler.cs b/src/Application/Messages/Handlers/Commands/UpdateUserDataCommandHandler.cs new file mode 100644 index 0000000..35e5b14 --- /dev/null +++ b/src/Application/Messages/Handlers/Commands/UpdateUserDataCommandHandler.cs @@ -0,0 +1,45 @@ +using Application.Messages.Commands; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace Application.Messages.Handlers.Commands; + +public class UpdateUserDataCommandHandler : IRequestHandler +{ + private readonly ILogger _logger; + private readonly IUserServiceClient _userServiceClient; + + public UpdateUserDataCommandHandler(HttpClient httpClient, + ILogger logger, + IUserServiceClient userServiceClient) + { + _logger = logger; + _userServiceClient = userServiceClient; + } + + public async Task Handle(UpdateUserDataCommand request, CancellationToken cancellationToken) + { + try + { + var user = await _userServiceClient.CheckUserByEmailAsync(request.Email, cancellationToken); + if (user == null) return false; + + var updateData = new UserDto() + { + Id = user.Id, + Email = request.Email, + Country = request.Country, + AccessType = string.IsNullOrWhiteSpace(request.AccessType) ? user.AccessType : request.AccessType, + }; + await _userServiceClient.UpdateUserAsync(updateData, cancellationToken); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating user data for {Email}", request.Email); + return false; + } + } +} \ No newline at end of file diff --git a/src/Application/Messages/Handlers/Queries/CheckEmployerByEmailQueryHandler.cs b/src/Application/Messages/Handlers/Queries/CheckEmployerByEmailQueryHandler.cs new file mode 100644 index 0000000..f60101e --- /dev/null +++ b/src/Application/Messages/Handlers/Queries/CheckEmployerByEmailQueryHandler.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Options; +using Application.Messages.Queries; +using Infrastructure.Configuration; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using MediatR; + +namespace Application.Messages.Handlers.Queries; + +public class CheckEmployerByEmailQueryHandler : IRequestHandler +{ + private readonly IEmployerServiceClient _employerService; + private readonly AppSettings _appSettings; + + public CheckEmployerByEmailQueryHandler(IEmployerServiceClient employerService, IOptions appSettings) + { + _employerService = employerService; + _appSettings = appSettings.Value; + } + + public async Task Handle(CheckEmployerByEmailQuery request, CancellationToken cancellationToken) + { + var response = await _employerService.GetEmployerByEmailAsync(request.Email, cancellationToken); + + return response; + } +} \ No newline at end of file diff --git a/src/Application/Messages/Handlers/Queries/GetEmployerByIdQueryHandler.cs b/src/Application/Messages/Handlers/Queries/GetEmployerByIdQueryHandler.cs new file mode 100644 index 0000000..18378ce --- /dev/null +++ b/src/Application/Messages/Handlers/Queries/GetEmployerByIdQueryHandler.cs @@ -0,0 +1,31 @@ +using Application.Messages.Queries; +using MediatR; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; + +namespace Application.Messages.Handlers.Queries; + +public class GetEmployerByIdQueryHandler : IRequestHandler +{ + private readonly IEmployerServiceClient _employerService; + + public GetEmployerByIdQueryHandler(IEmployerServiceClient employerService) + { + _employerService = employerService; + } + + public async Task Handle(GetEmployerByIdQuery request, CancellationToken cancellationToken) + { + var employer = await _employerService.GetEmployerByIdAsync(request.Id, cancellationToken); + if (employer == null) return null; + + return new EmployerDto + { + Id = employer.Id, + FullName = employer.FullName, + Email = employer.Email, + BirthDate = employer.BirthDate, + Salary = employer.Salary + }; + } +} diff --git a/src/Application/Messages/Handlers/Queries/GetUserByEmailQueryHandler.cs b/src/Application/Messages/Handlers/Queries/GetUserByEmailQueryHandler.cs new file mode 100644 index 0000000..67340f8 --- /dev/null +++ b/src/Application/Messages/Handlers/Queries/GetUserByEmailQueryHandler.cs @@ -0,0 +1,31 @@ +using Application.Messages.Queries; +using Infrastructure.Records; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace Application.Messages.Handlers.Queries; + +public class GetUserByEmailQueryHandler : IRequestHandler +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public GetUserByEmailQueryHandler(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public async Task Handle(GetUserByEmailQuery request, CancellationToken cancellationToken) + { + var userResponse = await _httpClient.GetAsync($"users?email={request.Email}", cancellationToken); + if (!userResponse.IsSuccessStatusCode) + { + _logger.LogError("User with email {Email} not found.", request.Email); + return default; + } + + var user = await userResponse.Content.ReadAsAsync(cancellationToken); + return user; + } +} \ No newline at end of file diff --git a/src/Application/Messages/Queries/CheckEmployerByEmailQuery.cs b/src/Application/Messages/Queries/CheckEmployerByEmailQuery.cs new file mode 100644 index 0000000..1a56fc3 --- /dev/null +++ b/src/Application/Messages/Queries/CheckEmployerByEmailQuery.cs @@ -0,0 +1,14 @@ +using Infrastructure.Records; +using MediatR; + +namespace Application.Messages.Queries; + +public class CheckEmployerByEmailQuery : IRequest +{ + public CheckEmployerByEmailQuery(string email) + { + Email = email; + } + + public string Email { get; set; } +} \ No newline at end of file diff --git a/src/Application/Messages/Queries/GetEmployerByIdQuery.cs b/src/Application/Messages/Queries/GetEmployerByIdQuery.cs new file mode 100644 index 0000000..bfad6c4 --- /dev/null +++ b/src/Application/Messages/Queries/GetEmployerByIdQuery.cs @@ -0,0 +1,9 @@ +using Infrastructure.Records; +using MediatR; + +namespace Application.Messages.Queries; + +public class GetEmployerByIdQuery(string id) : IRequest +{ + public string Id { get; init; } = id; +} \ No newline at end of file diff --git a/src/Application/Messages/Queries/GetUserByEmailQuery.cs b/src/Application/Messages/Queries/GetUserByEmailQuery.cs new file mode 100644 index 0000000..2ce0915 --- /dev/null +++ b/src/Application/Messages/Queries/GetUserByEmailQuery.cs @@ -0,0 +1,9 @@ +using Infrastructure.Records; +using MediatR; + +namespace Application.Messages.Queries; + +public class GetUserByEmailQuery(string email) : IRequest +{ + public string Email { get; init; } = email; +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/Commands/CreateUserCommandValidator.cs b/src/Application/Messages/Validators/Commands/CreateUserCommandValidator.cs new file mode 100644 index 0000000..f874631 --- /dev/null +++ b/src/Application/Messages/Validators/Commands/CreateUserCommandValidator.cs @@ -0,0 +1,55 @@ +using Application.Messages.Commands; +using FluentValidation; + +namespace Application.Messages.Validators.Commands; + +public class CreateUserCommandValidator : AbstractValidator +{ + public CreateUserCommandValidator() + { + RuleFor(command => command.Email) + .NotEmpty().WithMessage("Email is required.") + .EmailAddress().WithMessage("Email is not a valid email address."); + + RuleFor(command => command.Password) + .NotEmpty().WithMessage("Password is required."); + + RuleFor(command => command.Country) + .NotEmpty() + .WithMessage("Country is required.") + .Matches("^[A-Z]{2}$").WithMessage("Country must be a valid alpha-2 code.") + .Must(IsValidIso3166CountryCode).WithMessage("Country must be a valid ISO-3166 country code."); + + + RuleFor(command => command.AccessType) + .NotEmpty().WithMessage("Access type is required.") + .Must(accessType => accessType == "dtc" || accessType == "employer") + .WithMessage("Access type must be either 'dtc' or 'employer'."); + + RuleFor(command => command.FullName) + .NotEmpty().When(command => command.FullName != null) + .WithMessage("Full name is optional but must not be empty if provided."); + + RuleFor(command => command.EmployerId) + .NotEmpty().When(command => command.EmployerId != null) + .WithMessage("Employer ID is optional but must not be empty if provided."); + + RuleFor(command => command.BirthDate) + .Must(BeAValidDate).When(command => command.BirthDate != null) + .WithMessage("Birth date is optional but must be a valid date if provided."); + + RuleFor(command => command.Salary) + .GreaterThanOrEqualTo(0).When(command => command.Salary.HasValue) + .WithMessage("Salary is optional but must be a non-negative number if provided."); + } + + private bool BeAValidDate(DateTime? date) + { + return !date.HasValue || date.Value < DateTime.UtcNow; + } + + private bool IsValidIso3166CountryCode(string code) + { + return CountryCodeValidator.IsValidIso3166CountryCode(code.ToLowerInvariant()); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/Commands/ProcessEligibilityFileCommandValidator.cs b/src/Application/Messages/Validators/Commands/ProcessEligibilityFileCommandValidator.cs new file mode 100644 index 0000000..536afc4 --- /dev/null +++ b/src/Application/Messages/Validators/Commands/ProcessEligibilityFileCommandValidator.cs @@ -0,0 +1,21 @@ +using Application.Messages.Commands; +using FluentValidation; + +public class ProcessEligibilityFileCommandValidator : AbstractValidator +{ + public ProcessEligibilityFileCommandValidator() + { + RuleFor(command => command.CsvFileUrl) + .NotEmpty().WithMessage("CSV file URL is required.") + .Must(BeAValidUrl).WithMessage("CSV file URL must be a valid URL."); + + RuleFor(command => command.EmployerName) + .NotEmpty().WithMessage("Employer name is required."); + } + + private bool BeAValidUrl(string url) + { + return Uri.TryCreate(url, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/Commands/ProcessSignupCommandValidator.cs b/src/Application/Messages/Validators/Commands/ProcessSignupCommandValidator.cs new file mode 100644 index 0000000..b5e764c --- /dev/null +++ b/src/Application/Messages/Validators/Commands/ProcessSignupCommandValidator.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using Application.Messages.Commands; + +namespace Application.Messages.Validators.Commands; + + +public class ProcessSignupCommandValidator : AbstractValidator +{ + public ProcessSignupCommandValidator() + { + RuleFor(command => command.Email) + .NotEmpty().WithMessage("Email is required.") + .EmailAddress().WithMessage("Email must be a valid email address."); + + RuleFor(command => command.Password) + .NotEmpty().WithMessage("Password is required.") + .MinimumLength(8).WithMessage("Password must be at least 8 characters long."); + + RuleFor(command => command.Country) + .NotEmpty().WithMessage("Country is required."); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/Commands/TerminateUnlistedUsersCommandValidator.cs b/src/Application/Messages/Validators/Commands/TerminateUnlistedUsersCommandValidator.cs new file mode 100644 index 0000000..dac0a7b --- /dev/null +++ b/src/Application/Messages/Validators/Commands/TerminateUnlistedUsersCommandValidator.cs @@ -0,0 +1,13 @@ +using Application.Messages.Commands; +using FluentValidation; + +namespace Application.Messages.Validators.Commands; + +public class TerminateUnlistedUsersCommandValidator : AbstractValidator +{ + public TerminateUnlistedUsersCommandValidator() + { + RuleFor(command => command.EmployerId) + .NotEmpty().WithMessage("EmployerId is required."); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/Commands/UpdateUserDataCommandValidator.cs b/src/Application/Messages/Validators/Commands/UpdateUserDataCommandValidator.cs new file mode 100644 index 0000000..ca3bc63 --- /dev/null +++ b/src/Application/Messages/Validators/Commands/UpdateUserDataCommandValidator.cs @@ -0,0 +1,27 @@ +using Application.Messages.Commands; +using FluentValidation; + +namespace Application.Messages.Validators.Commands; + + +public class UpdateUserDataCommandValidator : AbstractValidator +{ + public UpdateUserDataCommandValidator() + { + RuleFor(command => command.Email) + .NotEmpty().WithMessage("Email is required.") + .EmailAddress().WithMessage("Email must be a valid email address."); + + RuleFor(command => command.Country) + .NotEmpty().WithMessage("Country is required."); + + RuleFor(command => command.Salary) + .GreaterThanOrEqualTo(0).When(command => command.Salary.HasValue) + .WithMessage("Salary must be a positive number."); + + RuleFor(command => command.AccessType) + .NotEmpty().WithMessage("Access type is required.") + .Must(accessType => new[] { "Admin", "User", "Guest" }.Contains(accessType)) + .WithMessage("Access type must be either 'Admin', 'User', or 'Guest'."); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/CountryCodeValidator.cs b/src/Application/Messages/Validators/CountryCodeValidator.cs new file mode 100644 index 0000000..2ffdd68 --- /dev/null +++ b/src/Application/Messages/Validators/CountryCodeValidator.cs @@ -0,0 +1,209 @@ +using Application.DTOs; + +namespace Application.Messages.Validators; + +public class CountryCodeValidator +{ + private static readonly List _countryCodes = new() + { + new CountryCodeDto { Name = "Afghanistan", Alpha2 = "AF" }, + new CountryCodeDto { Name = "Åland Islands", Alpha2 = "AX" }, + new CountryCodeDto { Name = "Albania", Alpha2 = "AL" }, + new CountryCodeDto { Name = "Algeria", Alpha2 = "DZ" }, + new CountryCodeDto { Name = "Andorra", Alpha2 = "AD" }, + new CountryCodeDto { Name = "Angola", Alpha2 = "AO" }, + new CountryCodeDto { Name = "Antigua and Barbuda", Alpha2 = "AG" }, + new CountryCodeDto { Name = "Argentina", Alpha2 = "AR" }, + new CountryCodeDto { Name = "Armenia", Alpha2 = "AM" }, + new CountryCodeDto { Name = "Australia", Alpha2 = "AU" }, + new CountryCodeDto { Name = "Austria", Alpha2 = "AT" }, + new CountryCodeDto { Name = "Azerbaijan", Alpha2 = "AZ" }, + new CountryCodeDto { Name = "Bahamas", Alpha2 = "BS" }, + new CountryCodeDto { Name = "Bahrain", Alpha2 = "BH" }, + new CountryCodeDto { Name = "Bangladesh", Alpha2 = "BD" }, + new CountryCodeDto { Name = "Barbados", Alpha2 = "BB" }, + new CountryCodeDto { Name = "Belarus", Alpha2 = "BY" }, + new CountryCodeDto { Name = "Belgium", Alpha2 = "BE" }, + new CountryCodeDto { Name = "Belize", Alpha2 = "BZ" }, + new CountryCodeDto { Name = "Benin", Alpha2 = "BJ" }, + new CountryCodeDto { Name = "Bhutan", Alpha2 = "BT" }, + new CountryCodeDto { Name = "Bolivia, Plurinational State of", Alpha2 = "BO" }, + new CountryCodeDto { Name = "Bosnia and Herzegovina", Alpha2 = "BA" }, + new CountryCodeDto { Name = "Botswana", Alpha2 = "BW" }, + new CountryCodeDto { Name = "Brazil", Alpha2 = "BR" }, + new CountryCodeDto { Name = "Brunei Darussalam", Alpha2 = "BN" }, + new CountryCodeDto { Name = "Bulgaria", Alpha2 = "BG" }, + new CountryCodeDto { Name = "Burkina Faso", Alpha2 = "BF" }, + new CountryCodeDto { Name = "Burundi", Alpha2 = "BI" }, + new CountryCodeDto { Name = "Cabo Verde", Alpha2 = "CV" }, + new CountryCodeDto { Name = "Cambodia", Alpha2 = "KH" }, + new CountryCodeDto { Name = "Cameroon", Alpha2 = "CM" }, + new CountryCodeDto { Name = "Canada", Alpha2 = "CA" }, + new CountryCodeDto { Name = "Central African Republic", Alpha2 = "CF" }, + new CountryCodeDto { Name = "Chad", Alpha2 = "TD" }, + new CountryCodeDto { Name = "Chile", Alpha2 = "CL" }, + new CountryCodeDto { Name = "China", Alpha2 = "CN" }, + new CountryCodeDto { Name = "Colombia", Alpha2 = "CO" }, + new CountryCodeDto { Name = "Comoros", Alpha2 = "KM" }, + new CountryCodeDto { Name = "Congo", Alpha2 = "CG" }, + new CountryCodeDto { Name = "Congo, Democratic Republic of the", Alpha2 = "CD" }, + new CountryCodeDto { Name = "Costa Rica", Alpha2 = "CR" }, + new CountryCodeDto { Name = "Côte d'Ivoire", Alpha2 = "CI" }, + new CountryCodeDto { Name = "Croatia", Alpha2 = "HR" }, + new CountryCodeDto { Name = "Cuba", Alpha2 = "CU" }, + new CountryCodeDto { Name = "Cyprus", Alpha2 = "CY" }, + new CountryCodeDto { Name = "Czechia", Alpha2 = "CZ" }, + new CountryCodeDto { Name = "Denmark", Alpha2 = "DK" }, + new CountryCodeDto { Name = "Djibouti", Alpha2 = "DJ" }, + new CountryCodeDto { Name = "Dominica", Alpha2 = "DM" }, + new CountryCodeDto { Name = "Dominican Republic", Alpha2 = "DO" }, + new CountryCodeDto { Name = "Ecuador", Alpha2 = "EC" }, + new CountryCodeDto { Name = "Egypt", Alpha2 = "EG" }, + new CountryCodeDto { Name = "El Salvador", Alpha2 = "SV" }, + new CountryCodeDto { Name = "Equatorial Guinea", Alpha2 = "GQ" }, + new CountryCodeDto { Name = "Eritrea", Alpha2 = "ER" }, + new CountryCodeDto { Name = "Estonia", Alpha2 = "EE" }, + new CountryCodeDto { Name = "Eswatini", Alpha2 = "SZ" }, + new CountryCodeDto { Name = "Ethiopia", Alpha2 = "ET" }, + new CountryCodeDto { Name = "Fiji", Alpha2 = "FJ" }, + new CountryCodeDto { Name = "Finland", Alpha2 = "FI" }, + new CountryCodeDto { Name = "France", Alpha2 = "FR" }, + new CountryCodeDto { Name = "Gabon", Alpha2 = "GA" }, + new CountryCodeDto { Name = "Gambia", Alpha2 = "GM" }, + new CountryCodeDto { Name = "Georgia", Alpha2 = "GE" }, + new CountryCodeDto { Name = "Germany", Alpha2 = "DE" }, + new CountryCodeDto { Name = "Ghana", Alpha2 = "GH" }, + new CountryCodeDto { Name = "Greece", Alpha2 = "GR" }, + new CountryCodeDto { Name = "Grenada", Alpha2 = "GD" }, + new CountryCodeDto { Name = "Guatemala", Alpha2 = "GT" }, + new CountryCodeDto { Name = "Guinea", Alpha2 = "GN" }, + new CountryCodeDto { Name = "Guinea-Bissau", Alpha2 = "GW" }, + new CountryCodeDto { Name = "Guyana", Alpha2 = "GY" }, + new CountryCodeDto { Name = "Haiti", Alpha2 = "HT" }, + new CountryCodeDto { Name = "Honduras", Alpha2 = "HN" }, + new CountryCodeDto { Name = "Hungary", Alpha2 = "HU" }, + new CountryCodeDto { Name = "Iceland", Alpha2 = "IS" }, + new CountryCodeDto { Name = "India", Alpha2 = "IN" }, + new CountryCodeDto { Name = "Indonesia", Alpha2 = "ID" }, + new CountryCodeDto { Name = "Iran, Islamic Republic of", Alpha2 = "IR" }, + new CountryCodeDto { Name = "Iraq", Alpha2 = "IQ" }, + new CountryCodeDto { Name = "Ireland", Alpha2 = "IE" }, + new CountryCodeDto { Name = "Israel", Alpha2 = "IL" }, + new CountryCodeDto { Name = "Italy", Alpha2 = "IT" }, + new CountryCodeDto { Name = "Jamaica", Alpha2 = "JM" }, + new CountryCodeDto { Name = "Japan", Alpha2 = "JP" }, + new CountryCodeDto { Name = "Jordan", Alpha2 = "JO" }, + new CountryCodeDto { Name = "Kazakhstan", Alpha2 = "KZ" }, + new CountryCodeDto { Name = "Kenya", Alpha2 = "KE" }, + new CountryCodeDto { Name = "Kiribati", Alpha2 = "KI" }, + new CountryCodeDto { Name = "Korea, Democratic People's Republic of", Alpha2 = "KP" }, + new CountryCodeDto { Name = "Korea, Republic of", Alpha2 = "KR" }, + new CountryCodeDto { Name = "Kuwait", Alpha2 = "KW" }, + new CountryCodeDto { Name = "Kyrgyzstan", Alpha2 = "KG" }, + new CountryCodeDto { Name = "Lao People's Democratic Republic", Alpha2 = "LA" }, + new CountryCodeDto { Name = "Latvia", Alpha2 = "LV" }, + new CountryCodeDto { Name = "Lebanon", Alpha2 = "LB" }, + new CountryCodeDto { Name = "Lesotho", Alpha2 = "LS" }, + new CountryCodeDto { Name = "Liberia", Alpha2 = "LR" }, + new CountryCodeDto { Name = "Libya", Alpha2 = "LY" }, + new CountryCodeDto { Name = "Liechtenstein", Alpha2 = "LI" }, + new CountryCodeDto { Name = "Lithuania", Alpha2 = "LT" }, + new CountryCodeDto { Name = "Luxembourg", Alpha2 = "LU" }, + new CountryCodeDto { Name = "Madagascar", Alpha2 = "MG" }, + new CountryCodeDto { Name = "Malawi", Alpha2 = "MW" }, + new CountryCodeDto { Name = "Malaysia", Alpha2 = "MY" }, + new CountryCodeDto { Name = "Maldives", Alpha2 = "MV" }, + new CountryCodeDto { Name = "Mali", Alpha2 = "ML" }, + new CountryCodeDto { Name = "Malta", Alpha2 = "MT" }, + new CountryCodeDto { Name = "Marshall Islands", Alpha2 = "MH" }, + new CountryCodeDto { Name = "Mauritania", Alpha2 = "MR" }, + new CountryCodeDto { Name = "Mauritius", Alpha2 = "MU" }, + new CountryCodeDto { Name = "Mexico", Alpha2 = "MX" }, + new CountryCodeDto { Name = "Micronesia, Federated States of", Alpha2 = "FM" }, + new CountryCodeDto { Name = "Moldova, Republic of", Alpha2 = "MD" }, + new CountryCodeDto { Name = "Monaco", Alpha2 = "MC" }, + new CountryCodeDto { Name = "Mongolia", Alpha2 = "MN" }, + new CountryCodeDto { Name = "Montenegro", Alpha2 = "ME" }, + new CountryCodeDto { Name = "Morocco", Alpha2 = "MA" }, + new CountryCodeDto { Name = "Mozambique", Alpha2 = "MZ" }, + new CountryCodeDto { Name = "Myanmar", Alpha2 = "MM" }, + new CountryCodeDto { Name = "Namibia", Alpha2 = "NA" }, + new CountryCodeDto { Name = "Nauru", Alpha2 = "NR" }, + new CountryCodeDto { Name = "Nepal", Alpha2 = "NP" }, + new CountryCodeDto { Name = "Netherlands", Alpha2 = "NL" }, + new CountryCodeDto { Name = "New Zealand", Alpha2 = "NZ" }, + new CountryCodeDto { Name = "Nicaragua", Alpha2 = "NI" }, + new CountryCodeDto { Name = "Niger", Alpha2 = "NE" }, + new CountryCodeDto { Name = "Nigeria", Alpha2 = "NG" }, + new CountryCodeDto { Name = "North Macedonia", Alpha2 = "MK" }, + new CountryCodeDto { Name = "Norway", Alpha2 = "NO" }, + new CountryCodeDto { Name = "Oman", Alpha2 = "OM" }, + new CountryCodeDto { Name = "Pakistan", Alpha2 = "PK" }, + new CountryCodeDto { Name = "Palau", Alpha2 = "PW" }, + new CountryCodeDto { Name = "Panama", Alpha2 = "PA" }, + new CountryCodeDto { Name = "Papua New Guinea", Alpha2 = "PG" }, + new CountryCodeDto { Name = "Paraguay", Alpha2 = "PY" }, + new CountryCodeDto { Name = "Peru", Alpha2 = "PE" }, + new CountryCodeDto { Name = "Philippines", Alpha2 = "PH" }, + new CountryCodeDto { Name = "Poland", Alpha2 = "PL" }, + new CountryCodeDto { Name = "Portugal", Alpha2 = "PT" }, + new CountryCodeDto { Name = "Qatar", Alpha2 = "QA" }, + new CountryCodeDto { Name = "Romania", Alpha2 = "RO" }, + new CountryCodeDto { Name = "Russian Federation", Alpha2 = "RU" }, + new CountryCodeDto { Name = "Rwanda", Alpha2 = "RW" }, + new CountryCodeDto { Name = "Saint Kitts and Nevis", Alpha2 = "KN" }, + new CountryCodeDto { Name = "Saint Lucia", Alpha2 = "LC" }, + new CountryCodeDto { Name = "Saint Vincent and the Grenadines", Alpha2 = "VC" }, + new CountryCodeDto { Name = "Samoa", Alpha2 = "WS" }, + new CountryCodeDto { Name = "San Marino", Alpha2 = "SM" }, + new CountryCodeDto { Name = "Sao Tome and Principe", Alpha2 = "ST" }, + new CountryCodeDto { Name = "Saudi Arabia", Alpha2 = "SA" }, + new CountryCodeDto { Name = "Senegal", Alpha2 = "SN" }, + new CountryCodeDto { Name = "Serbia", Alpha2 = "RS" }, + new CountryCodeDto { Name = "Seychelles", Alpha2 = "SC" }, + new CountryCodeDto { Name = "Sierra Leone", Alpha2 = "SL" }, + new CountryCodeDto { Name = "Singapore", Alpha2 = "SG" }, + new CountryCodeDto { Name = "Slovakia", Alpha2 = "SK" }, + new CountryCodeDto { Name = "Slovenia", Alpha2 = "SI" }, + new CountryCodeDto { Name = "Solomon Islands", Alpha2 = "SB" }, + new CountryCodeDto { Name = "Somalia", Alpha2 = "SO" }, + new CountryCodeDto { Name = "South Africa", Alpha2 = "ZA" }, + new CountryCodeDto { Name = "South Sudan", Alpha2 = "SS" }, + new CountryCodeDto { Name = "Spain", Alpha2 = "ES" }, + new CountryCodeDto { Name = "Sri Lanka", Alpha2 = "LK" }, + new CountryCodeDto { Name = "Sudan", Alpha2 = "SD" }, + new CountryCodeDto { Name = "Suriname", Alpha2 = "SR" }, + new CountryCodeDto { Name = "Sweden", Alpha2 = "SE" }, + new CountryCodeDto { Name = "Switzerland", Alpha2 = "CH" }, + new CountryCodeDto { Name = "Syrian Arab Republic", Alpha2 = "SY" }, + new CountryCodeDto { Name = "Tajikistan", Alpha2 = "TJ" }, + new CountryCodeDto { Name = "Tanzania, United Republic of", Alpha2 = "TZ" }, + new CountryCodeDto { Name = "Thailand", Alpha2 = "TH" }, + new CountryCodeDto { Name = "Timor-Leste", Alpha2 = "TL" }, + new CountryCodeDto { Name = "Togo", Alpha2 = "TG" }, + new CountryCodeDto { Name = "Tonga", Alpha2 = "TO" }, + new CountryCodeDto { Name = "Trinidad and Tobago", Alpha2 = "TT" }, + new CountryCodeDto { Name = "Tunisia", Alpha2 = "TN" }, + new CountryCodeDto { Name = "Türkiye", Alpha2 = "TR" }, + new CountryCodeDto { Name = "Turkmenistan", Alpha2 = "TM" }, + new CountryCodeDto { Name = "Tuvalu", Alpha2 = "TV" }, + new CountryCodeDto { Name = "Uganda", Alpha2 = "UG" }, + new CountryCodeDto { Name = "Ukraine", Alpha2 = "UA" }, + new CountryCodeDto { Name = "United Arab Emirates", Alpha2 = "AE" }, + new CountryCodeDto { Name = "United Kingdom of Great Britain and Northern Ireland", Alpha2 = "GB" }, + new CountryCodeDto { Name = "United States of America", Alpha2 = "US" }, + new CountryCodeDto { Name = "Uruguay", Alpha2 = "UY" }, + new CountryCodeDto { Name = "Uzbekistan", Alpha2 = "UZ" }, + new CountryCodeDto { Name = "Vanuatu", Alpha2 = "VU" }, + new CountryCodeDto { Name = "Venezuela, Bolivarian Republic of", Alpha2 = "VE" }, + new CountryCodeDto { Name = "Viet Nam", Alpha2 = "VN" }, + new CountryCodeDto { Name = "Yemen", Alpha2 = "YE" }, + new CountryCodeDto { Name = "Zambia", Alpha2 = "ZM" }, + new CountryCodeDto { Name = "Zimbabwe", Alpha2 = "ZW" } + }; + + public static bool IsValidIso3166CountryCode(string code) + { + return _countryCodes.Any(c => string.Equals(c.Alpha2, code, StringComparison.InvariantCultureIgnoreCase)); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/Queries/CheckEmployerByEmailQueryValidator.cs b/src/Application/Messages/Validators/Queries/CheckEmployerByEmailQueryValidator.cs new file mode 100644 index 0000000..ac26fd1 --- /dev/null +++ b/src/Application/Messages/Validators/Queries/CheckEmployerByEmailQueryValidator.cs @@ -0,0 +1,14 @@ +using Application.Messages.Queries; +using FluentValidation; + +namespace Application.Messages.Validators.Queries; + +public class CheckEmployerByEmailQueryValidator : AbstractValidator +{ + public CheckEmployerByEmailQueryValidator() + { + RuleFor(query => query.Email) + .NotEmpty().WithMessage("Email is required.") + .EmailAddress().WithMessage("Email must be a valid email address."); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/Queries/GetEmployerByIdQueryValidator.cs b/src/Application/Messages/Validators/Queries/GetEmployerByIdQueryValidator.cs new file mode 100644 index 0000000..ffec657 --- /dev/null +++ b/src/Application/Messages/Validators/Queries/GetEmployerByIdQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Application.Messages.Queries; + +namespace Application.Messages.Validators.Queries; + +public class GetEmployerByIdQueryValidator : AbstractValidator +{ + public GetEmployerByIdQueryValidator() + { + RuleFor(query => query.Id) + .NotEmpty().WithMessage("Id is required.") + .Matches("^[a-zA-Z0-9-]*$").WithMessage("Id must be alphanumeric with dashes."); + } +} \ No newline at end of file diff --git a/src/Application/Messages/Validators/Queries/GetUserByEmailQueryValidator.cs b/src/Application/Messages/Validators/Queries/GetUserByEmailQueryValidator.cs new file mode 100644 index 0000000..1d9c19c --- /dev/null +++ b/src/Application/Messages/Validators/Queries/GetUserByEmailQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Application.Messages.Queries; + +namespace Application.Messages.Validators.Queries; + +public class GetUserByEmailQueryValidator : AbstractValidator +{ + public GetUserByEmailQueryValidator() + { + RuleFor(query => query.Email) + .NotEmpty().WithMessage("Email is required.") + .EmailAddress().WithMessage("Email must be a valid email address."); + } +} \ No newline at end of file diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/src/Domain/Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Domain/Enums/AccessTypeEnum.cs b/src/Domain/Enums/AccessTypeEnum.cs new file mode 100644 index 0000000..ae7f410 --- /dev/null +++ b/src/Domain/Enums/AccessTypeEnum.cs @@ -0,0 +1,7 @@ +namespace Domain.Enums; + +public static class AccessTypeEnum +{ + public const string DTC = "dtc"; + public const string Employer = "employer"; +} \ No newline at end of file diff --git a/src/Domain/Interfaces/IPerson.cs b/src/Domain/Interfaces/IPerson.cs new file mode 100644 index 0000000..a9ec02a --- /dev/null +++ b/src/Domain/Interfaces/IPerson.cs @@ -0,0 +1,13 @@ +namespace Domain.Interfaces; + +public interface IPerson +{ + public string Id { get; set; } + public string Email { get; set; } + public string Country { get; set; } + public string AccessType { get; set; } + public string FullName { get; set; } + public string EmployerId { get; set; } + public DateTime? BirthDate { get; set; } + public decimal? Salary { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Models/EligibilityFile.cs b/src/Domain/Models/EligibilityFile.cs new file mode 100644 index 0000000..2de7464 --- /dev/null +++ b/src/Domain/Models/EligibilityFile.cs @@ -0,0 +1,7 @@ +namespace Domain.Models; + +public class EligibilityFile +{ + public string File { get; set; } + public string EmployerName { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Models/EligibilityProcessResult.cs b/src/Domain/Models/EligibilityProcessResult.cs new file mode 100644 index 0000000..d1f285b --- /dev/null +++ b/src/Domain/Models/EligibilityProcessResult.cs @@ -0,0 +1,8 @@ +namespace Domain.Models; + +public class EligibilityProcessResult +{ + public HashSet ProcessedLines { get; init; } = new(); + public HashSet NonProcessedLines { get; init; } = new(); + public List Errors { get; init; } = new List(); +} \ No newline at end of file diff --git a/src/Domain/Models/Signup.cs b/src/Domain/Models/Signup.cs new file mode 100644 index 0000000..15b58c1 --- /dev/null +++ b/src/Domain/Models/Signup.cs @@ -0,0 +1,8 @@ +namespace Domain.Models; + +public class SignupModel +{ + public string Email { get; set; } + public string Password { get; set; } + public string Country { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure.Tests/Infrastructure.Tests.csproj b/src/Infrastructure.Tests/Infrastructure.Tests.csproj new file mode 100644 index 0000000..03b173c --- /dev/null +++ b/src/Infrastructure.Tests/Infrastructure.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Infrastructure.Tests/Services/EmployerServiceClientTests.cs b/src/Infrastructure.Tests/Services/EmployerServiceClientTests.cs new file mode 100644 index 0000000..c6a105d --- /dev/null +++ b/src/Infrastructure.Tests/Services/EmployerServiceClientTests.cs @@ -0,0 +1,175 @@ + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; +using Newtonsoft.Json; +using System.Net; +using FluentAssertions; +using Infrastructure.Configuration; +using Infrastructure.Records; +using Infrastructure.Services; + +namespace Infrastructure.Tests.Services; + +[Trait("Category", "Unit")] +public class EmployerServiceClientTests +{ + private readonly Mock _mockHttpMessageHandler = new(); + private readonly EmployerServiceClient _employerServiceClient; + private readonly Mock> _mockOptions = new(); + private readonly Mock> _mockLogger = new(); + + public EmployerServiceClientTests() + { + _mockOptions.Setup(o => o.Value).Returns(new AppSettings { EmployerServiceBaseUrl = "http://localhost" }); + var httpClient = new HttpClient(_mockHttpMessageHandler.Object); + var httpClientFactory = new Mock(); + httpClientFactory.Setup(x => x.CreateClient(It.IsAny())) + .Returns(httpClient); + _employerServiceClient = new EmployerServiceClient(httpClientFactory.Object, _mockOptions.Object, _mockLogger.Object); + } + + [Fact] + public async Task GetEmployerByIdAsync_ReturnsEmployer_WhenSuccessful() + { + // Arrange + var expectedEmployer = new EmployerDto + { + Id = "123", + Email = "test@example.com" + + }; + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(expectedEmployer)) + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + var result = await _employerServiceClient.GetEmployerByIdAsync("123", CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.Equal("123", result.Id); + Assert.Equal("test@example.com", result.Email); + } + + [Fact] + public async Task GetEmployerByIdAsync_ReturnsNull_WhenNotFound() + { + // Arrange + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.NotFound + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + var result = await _employerServiceClient.GetEmployerByIdAsync("nonexistent-id", CancellationToken.None); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task CreateEmployerAsync_CompletesSuccessfully_WhenCalledWithValidData() + { + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.Created + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + var exception = await Record.ExceptionAsync(() => _employerServiceClient.CreateEmployerAsync("test@example.com", CancellationToken.None)); + + Assert.Null(exception); // No exception should be thrown for a successful operation + } + + [Fact] + public async Task CreateEmployerAsync_LogsError_WhenBadRequest() + { + // Arrange + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + var expectedMessage = "Failed to save employer by email. Status code:"; + + var capturedLogs = new List<(LogLevel logLevel, Exception exception, string message)>(); + MockLogger(_mockLogger, capturedLogs); + + + // Act + await _employerServiceClient.CreateEmployerAsync("invalid@example.com", CancellationToken.None); + + // Assert + capturedLogs.Should().Satisfy(x => x.message.StartsWith(expectedMessage)); + } + private static void MockLogger(Mock> loggerMock, List<(LogLevel logLevel, Exception exception, string message)> capturedLogs) + { + loggerMock.Setup( + x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny()) + ).Callback((level, eventId, state, exception, formatter) => + { + var logMessage = state.ToString(); + capturedLogs.Add((level, exception, logMessage)); + }); + } + [Fact] + public async Task GetEmployerByEmailAsync_ReturnsEmployer_WhenSuccessful() + { + var expectedEmployer = new EmployerIdRecord("123"); + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(expectedEmployer)) + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + var result = await _employerServiceClient.GetEmployerByEmailAsync("test@example.com", CancellationToken.None); + + Assert.NotNull(result); + Assert.Equal("123", result.Id); + } + + [Fact] + public async Task GetEmployerByEmailAsync_ReturnsNull_WhenNotFound() + { + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.NotFound + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + var result = await _employerServiceClient.GetEmployerByEmailAsync("notfound@example.com", CancellationToken.None); + + Assert.Null(result); + } +} \ No newline at end of file diff --git a/src/Infrastructure.Tests/Services/UserServiceClientTests.cs b/src/Infrastructure.Tests/Services/UserServiceClientTests.cs new file mode 100644 index 0000000..7831f0e --- /dev/null +++ b/src/Infrastructure.Tests/Services/UserServiceClientTests.cs @@ -0,0 +1,333 @@ +using Infrastructure.Records; +using Infrastructure.Services; +using Newtonsoft.Json; +using Moq; +using Moq.Protected; +using System.Net; +using Infrastructure.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + + +namespace Infrastructure.Tests.Services; + +[Trait("Category", "Unit")] +public class UserServiceClientTests +{ + private readonly Mock _mockHttpMessageHandler = new(); + private readonly UserServiceClient _userServiceClient; + private readonly Mock> _mockLogger = new(); + private readonly Mock> _mockOptions = new(); + + public UserServiceClientTests() + { + _mockOptions.Setup(o => o.Value).Returns(new AppSettings { UserServiceBaseUrl = "http://localhost" }); + var httpClient = new HttpClient(_mockHttpMessageHandler.Object); + var httpClientFactory = new Mock(); + httpClientFactory.Setup(x => x.CreateClient(It.IsAny())) + .Returns(httpClient); + _userServiceClient = new UserServiceClient(httpClientFactory.Object, _mockOptions.Object, _mockLogger.Object); + } + + [Fact] + public async Task CheckUserByEmailAsync_ReturnsUser_WhenSuccessful() + { + // Arrange + var expectedUser = new UserDto { Email = "test@example.com" }; + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(expectedUser)) + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + var result = await _userServiceClient.CheckUserByEmailAsync("test@example.com", CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.Equal("test@example.com", result.Email); + } + + [Fact] + public async Task CheckUserByEmailAsync_ReturnsNull_WhenNotFound() + { + // Arrange + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.NotFound + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + var result = await _userServiceClient.CheckUserByEmailAsync("notfound@example.com", CancellationToken.None); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task CreateUserAsync_ReturnsTrue_WhenSuccessful() + { + // Arrange + var createUserDto = new CreateUserDto { Email = "success@example.com" }; + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.Created + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + var result = await _userServiceClient.CreateUserAsync(createUserDto, CancellationToken.None); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task CreateUserAsync_ReturnsFalse_WhenBadRequest() + { + // Arrange + var createUserDto = new CreateUserDto { Email = "fail@example.com" }; + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + var result = await _userServiceClient.CreateUserAsync(createUserDto, CancellationToken.None); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task CreateUserAsync_LogsError_WhenExceptionOccurs() + { + // Arrange + var exceptionMessage = "An error occurred during user creation"; + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ThrowsAsync(new HttpRequestException(exceptionMessage)); + + _mockLogger.Setup(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Exception occurred while creating user")), + It.IsAny(), + It.IsAny>())); + + var createUserDto = new CreateUserDto { Email = "error@example.com" }; + + // Act + _userServiceClient.CreateUserAsync(createUserDto, CancellationToken.None); + + // Assert + _mockLogger.Verify(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Exception occurred while creating user") && v.ToString().Contains(exceptionMessage)), + It.IsAny(), + It.IsAny>()), Times.Once); + } + + [Fact] + public async Task GetUserByEmployerIdAsync_ReturnsUsers_WhenSuccessful() + { + // Arrange + var expectedUsers = new List { new UserDto { Email = "test@example.com" } }; + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(expectedUsers)) + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + var result = await _userServiceClient.GetUserByEmployerIdAsync("valid-id", CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal("test@example.com", result[0].Email); + } + + [Fact] + public async Task GetUserByEmployerIdAsync_ReturnsNull_WhenNotFound() + { + // Arrange + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.NotFound + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + var result = await _userServiceClient.GetUserByEmployerIdAsync("invalid-id", CancellationToken.None); + + // Assert + Assert.Null(result); + } + + + [Fact] + public async Task UpdateUserAsync_Success() + { + // Arrange + var userDto = new UserDto { Id = "1", Email = "success@example.com" }; + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act & Assert + await _userServiceClient.UpdateUserAsync(userDto, CancellationToken.None); + } + + [Fact] + public async Task UpdateUserAsync_LogsError_WhenExceptionOccurs() + { + // Arrange + var exceptionMessage = "An error occurred"; + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ThrowsAsync(new HttpRequestException(exceptionMessage)); + + _mockLogger.Setup(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Exception occurred while updating user")), + It.IsAny(), + It.IsAny>())); + + // Act + await _userServiceClient.UpdateUserAsync(new UserDto(), CancellationToken.None); + + // Assert + _mockLogger.Verify(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Exception occurred while updating user") && v.ToString().Contains(exceptionMessage)), + It.IsAny(), + It.IsAny>()), Times.Once); + } + [Fact] + public async Task UpdateUserAsync_ReturnsFalse_WhenBadRequest() + { + // Arrange + var userDto = new UserDto { Id = "1", Email = "fail@example.com" }; + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + await _userServiceClient.UpdateUserAsync(userDto, CancellationToken.None); + + //Assert + _mockLogger.Verify(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to update user. Status code: BadRequest")), + It.IsAny(), + It.IsAny>()), Times.Once); + } + + [Fact] + public async Task TerminateUserAsync_Success() + { + // Arrange + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act & Assert + await _userServiceClient.TerminateUserAsync("valid-user-id", CancellationToken.None); + } + + [Fact] + public async Task TerminateUserAsync_LogsError_WhenBadRequest() + { + // Arrange + var httpResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest + }; + + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(httpResponse); + + // Act + await _userServiceClient.TerminateUserAsync("invalid-user-id", CancellationToken.None); + + // Assert + _mockLogger.Verify(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Failed to terminate user. Status code: BadRequest")), + It.IsAny(), + It.IsAny>()), Times.Once); + } + + [Fact] + public async Task TerminateUserAsync_LogsError_WhenExceptionOccurs() + { + // Arrange + var exceptionMessage = "An error occurred"; + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ThrowsAsync(new HttpRequestException(exceptionMessage)); + + _mockLogger.Setup(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Exception occurred while terminating user")), + It.IsAny(), + It.IsAny>())); + + // Act + await _userServiceClient.TerminateUserAsync("any-user-id", CancellationToken.None); + + // Assert + _mockLogger.Verify(x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Exception occurred while terminating user") && v.ToString().Contains(exceptionMessage)), + It.IsAny(), + It.IsAny>()), Times.Once); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Configuration/AppSettings.cs b/src/Infrastructure/Configuration/AppSettings.cs new file mode 100644 index 0000000..102de4a --- /dev/null +++ b/src/Infrastructure/Configuration/AppSettings.cs @@ -0,0 +1,7 @@ +namespace Infrastructure.Configuration; + +public class AppSettings +{ + public string EmployerServiceBaseUrl { get; set; } + public string UserServiceBaseUrl { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj new file mode 100644 index 0000000..b432389 --- /dev/null +++ b/src/Infrastructure/Infrastructure.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/Infrastructure/Records/CreateUserDto.cs b/src/Infrastructure/Records/CreateUserDto.cs new file mode 100644 index 0000000..38bbe49 --- /dev/null +++ b/src/Infrastructure/Records/CreateUserDto.cs @@ -0,0 +1,15 @@ +namespace Infrastructure.Records; + +public class CreateUserDto +{ + public string Id { get; set; } + public string Password { get; set; } + public string Email { get; set; } + public string Country { get; set; } + public string AccessType { get; set; } + public string FullName { get; set; } + public string EmployerId { get; set; } + public DateTime? BirthDate { get; set; } + public decimal? Salary { get; set; } + +} \ No newline at end of file diff --git a/src/Infrastructure/Records/EmployeDto.cs b/src/Infrastructure/Records/EmployeDto.cs new file mode 100644 index 0000000..c100ca5 --- /dev/null +++ b/src/Infrastructure/Records/EmployeDto.cs @@ -0,0 +1,15 @@ +using Domain.Interfaces; + +namespace Infrastructure.Records; + +public class EmployerDto : IPerson +{ + public string Id { get; set; } + public string Email { get; set; } + public string Country { get; set; } + public string AccessType { get; set; } + public string FullName { get; set; } + public string EmployerId { get; set; } + public DateTime? BirthDate { get; set; } + public decimal? Salary { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure/Records/EmployerIdRecord.cs b/src/Infrastructure/Records/EmployerIdRecord.cs new file mode 100644 index 0000000..6556a3d --- /dev/null +++ b/src/Infrastructure/Records/EmployerIdRecord.cs @@ -0,0 +1,3 @@ +namespace Infrastructure.Records; + +public record EmployerIdRecord(string Id); \ No newline at end of file diff --git a/src/Infrastructure/Records/UserDto.cs b/src/Infrastructure/Records/UserDto.cs new file mode 100644 index 0000000..b0a9992 --- /dev/null +++ b/src/Infrastructure/Records/UserDto.cs @@ -0,0 +1,16 @@ +using Domain.Interfaces; + +namespace Infrastructure.Records; + +public class UserDto : IPerson +{ + public string Id { get; set; } + public string Email { get; set; } + public string Country { get; set; } + public string AccessType { get; set; } + public string FullName { get; set; } + public string EmployerId { get; set; } + public DateTime? BirthDate { get; set; } + public decimal? Salary { get; set; } + +} \ No newline at end of file diff --git a/src/Infrastructure/Services/EmployerServiceClient.cs b/src/Infrastructure/Services/EmployerServiceClient.cs new file mode 100644 index 0000000..629f99f --- /dev/null +++ b/src/Infrastructure/Services/EmployerServiceClient.cs @@ -0,0 +1,86 @@ +using System.Text; +using Domain.Interfaces; +using Infrastructure.Configuration; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Infrastructure.Services; + +public class EmployerServiceClient : IEmployerServiceClient +{ + private readonly AppSettings _appSettings; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + public EmployerServiceClient(IHttpClientFactory httpClientFactory, IOptions appSettings, ILogger logger) + { + _logger = logger; + _httpClient = httpClientFactory.CreateClient(); + _appSettings = appSettings.Value; + } + + public async Task GetEmployerByIdAsync(string id, CancellationToken cancellationToken) + { + var url = $"{_appSettings.EmployerServiceBaseUrl}/employers/{id}"; + var response = await _httpClient.GetAsync(url, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Failed to get employer by Id. Status code: {StatusCode}",response.StatusCode); + return default; // Or handle errors as appropriate + } + + var content = await response.Content.ReadAsStringAsync(cancellationToken); + var employerIdRecord = JsonConvert.DeserializeObject(content); + return employerIdRecord; + } + + public async Task GetEmployerByEmailAsync(string email, CancellationToken cancellationToken) + { + var url = $"{_appSettings.EmployerServiceBaseUrl}/employers?email={email}"; + try + { + var response = await _httpClient.GetAsync(url, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Failed to get employer by email. Status code: {StatusCode}", response.StatusCode); + return default; // Or handle errors as appropriate + } + + var content = await response.Content.ReadAsStringAsync(cancellationToken); + var employerIdRecord = JsonConvert.DeserializeObject(content); + return employerIdRecord; + } + catch (Exception e) + { + _logger.LogError(e, "Error on fetching employer by email {Email}: {Message}", email, e.Message); + throw; + } + } + + public async Task CreateEmployerAsync(string email, CancellationToken cancellationToken) + { + try + { + var url = $"{_appSettings.EmployerServiceBaseUrl}/employers"; + var employerRecord = new EmployerIdRecord(email); + var content = new StringContent(JsonConvert.SerializeObject(employerRecord), + Encoding.UTF8, + "application/json"); + var response = await _httpClient.PostAsync(url, content, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Failed to save employer by email. Status code: {StatusCode}", response.StatusCode); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error on creating employer by email {Email}: {Message}", email, e.Message); + throw; + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/Services/Interfaces/IEmployerService.cs b/src/Infrastructure/Services/Interfaces/IEmployerService.cs new file mode 100644 index 0000000..f2f5ba5 --- /dev/null +++ b/src/Infrastructure/Services/Interfaces/IEmployerService.cs @@ -0,0 +1,12 @@ +using Domain.Interfaces; +using Infrastructure.Records; + +namespace Infrastructure.Services.Interfaces; + +public interface IEmployerServiceClient +{ + Task GetEmployerByIdAsync(string id, CancellationToken cancellationToken); + Task GetEmployerByEmailAsync(string email, CancellationToken cancellationToken); + Task CreateEmployerAsync(string email, CancellationToken cancellationToken); +} + diff --git a/src/Infrastructure/Services/Interfaces/IUserServiceClient.cs b/src/Infrastructure/Services/Interfaces/IUserServiceClient.cs new file mode 100644 index 0000000..cc61660 --- /dev/null +++ b/src/Infrastructure/Services/Interfaces/IUserServiceClient.cs @@ -0,0 +1,13 @@ +using Infrastructure.Records; + +namespace Infrastructure.Services.Interfaces; + +public interface IUserServiceClient +{ + Task CheckUserByEmailAsync(string email, CancellationToken cancellationToken); + Task CreateUserAsync(CreateUserDto user, CancellationToken cancellationToken); + Task> GetUserByEmployerIdAsync(string id, CancellationToken cancellationToken); + Task UpdateUserAsync(UserDto user, CancellationToken cancellationToken); + + Task TerminateUserAsync(string userId, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Infrastructure/Services/UserServiceClient.cs b/src/Infrastructure/Services/UserServiceClient.cs new file mode 100644 index 0000000..a879472 --- /dev/null +++ b/src/Infrastructure/Services/UserServiceClient.cs @@ -0,0 +1,141 @@ +using System.Net; +using System.Text; +using Domain.Enums; +using Infrastructure.Configuration; +using Infrastructure.Records; +using Infrastructure.Services.Interfaces; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Infrastructure.Services; + +public class UserServiceClient : IUserServiceClient +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly AppSettings _appSettings; + + public UserServiceClient(IHttpClientFactory httpClientFactory, + IOptions appSettings, + ILogger logger) + { + _httpClient = httpClientFactory.CreateClient(); + _logger = logger; + _appSettings = appSettings.Value; + } + + public async Task CheckUserByEmailAsync(string email, CancellationToken cancellationToken) + { + var requestUri = $"{_appSettings.UserServiceBaseUrl}users?email={email}"; + var userResponse = await _httpClient.GetAsync(requestUri, cancellationToken); + if (!userResponse.IsSuccessStatusCode) + { + _logger.LogError("User with email {Email} not found.", email); + return default; + } + + var content = await userResponse.Content.ReadAsStringAsync(cancellationToken); + var user = JsonConvert.DeserializeObject(content); + return user; + } + + public async Task CreateUserAsync(CreateUserDto user, CancellationToken cancellationToken) + { + var requestContent = JsonConvert.SerializeObject(user); + + var content = new StringContent(requestContent, Encoding.UTF8, "application/json"); + + try + { + var uri = $"{_appSettings.UserServiceBaseUrl}/users"; + var response = await _httpClient.PostAsync(uri, content, cancellationToken); + + if (response.StatusCode is HttpStatusCode.Created or HttpStatusCode.OK) + { + return true; + } + + _logger.LogError("Failed to create user. Status code: {StatusCode}", response.StatusCode); + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occurred while creating user: {Message}", ex.Message); + return false; + } + } + + public async Task> GetUserByEmployerIdAsync(string id, CancellationToken cancellationToken) + { + var uri = $"{_appSettings.UserServiceBaseUrl}users/employerId={id}"; + var userResponse = await _httpClient.GetAsync(uri, cancellationToken); + if (!userResponse.IsSuccessStatusCode) + { + _logger.LogError("Search for users with EmployerID {Id} with error {StatusCode}.", id, userResponse.StatusCode); + return default; + } + + var content = await userResponse.Content.ReadAsStringAsync(cancellationToken); + var user = JsonConvert.DeserializeObject>(content); + return user; + } + + public async Task UpdateUserAsync(UserDto user, CancellationToken cancellationToken) + { + var updateRequestBody = new[] + { + new { field = "country", value = user.Country }, + new { field = "salary", value = user.Salary?.ToString() }, + new { field = "accessType", value = user.AccessType } + }; + var requestContent = JsonConvert.SerializeObject(updateRequestBody); + + var content = new StringContent(requestContent, Encoding.UTF8, "application/json"); + + try + { + var uri = $"{_appSettings.UserServiceBaseUrl}/users/{user.Id}"; + var response = await _httpClient.PostAsync(uri, content, cancellationToken); + + if (response.StatusCode is HttpStatusCode.Created or HttpStatusCode.OK) + { + return; + } + + _logger.LogError("Failed to update user. Status code: {StatusCode}", response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occurred while updating user: {Message}", ex.Message); + } + } + + public async Task TerminateUserAsync(string userId, CancellationToken cancellationToken) + { + var updateRequestBody = new[] + { + new { field = "accessType", value = AccessTypeEnum.DTC } + }; + var requestContent = JsonConvert.SerializeObject(updateRequestBody); + + var content = new StringContent(requestContent, Encoding.UTF8, "application/json"); + + try + { + var uri = $"{_appSettings.UserServiceBaseUrl}/users/{userId}"; + var response = await _httpClient.PostAsync(uri, content, cancellationToken); + + if (response.StatusCode is HttpStatusCode.Created or HttpStatusCode.OK) + { + return; + } + + _logger.LogError("Failed to terminate user. Status code: {StatusCode}", response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occurred while terminating user: {Message}", ex.Message); + } + } +} \ No newline at end of file diff --git a/src/Origin.API/Controllers/EligibilityFileController.cs b/src/Origin.API/Controllers/EligibilityFileController.cs new file mode 100644 index 0000000..53e0174 --- /dev/null +++ b/src/Origin.API/Controllers/EligibilityFileController.cs @@ -0,0 +1,25 @@ +using Application.Messages.Commands; +using Domain.Models; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace Origin.API.Controllers; + +[ApiController] +[Route("api/eligibility")] +public class EligibilityFileController : ControllerBase +{ + private readonly IMediator _mediator; + + public EligibilityFileController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpPost("process")] + public async Task> ProcessEligibilityFile([FromBody] EligibilityFile model) + { + var result = await _mediator.Send(new ProcessEligibilityFileCommand(model.File, model.EmployerName)); + return Ok(result); + } +} \ No newline at end of file diff --git a/src/Origin.API/Controllers/SignupController.cs b/src/Origin.API/Controllers/SignupController.cs new file mode 100644 index 0000000..6935b93 --- /dev/null +++ b/src/Origin.API/Controllers/SignupController.cs @@ -0,0 +1,26 @@ +using Application.Messages.Commands; +using Domain.Models; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace Origin.API.Controllers; + + +[ApiController] +[Route("api/signup")] +public class SignupController : ControllerBase +{ + private readonly IMediator _mediator; + + public SignupController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpPost] + public async Task Post([FromBody] SignupModel model) + { + await _mediator.Send(new ProcessSignupCommand(model.Email, model.Password, model.Country)); + return Ok(); + } +} \ No newline at end of file diff --git a/src/Origin.API/DependencyInjection.cs b/src/Origin.API/DependencyInjection.cs new file mode 100644 index 0000000..37ffbfb --- /dev/null +++ b/src/Origin.API/DependencyInjection.cs @@ -0,0 +1,61 @@ +using System.Reflection; +using Application.Common.Behaviours; +using Application.Messages.Commands; +using Application.Messages.Handlers.Commands; +using Application.Messages.Handlers.Queries; +using Application.Messages.Queries; +using Domain.Models; +using Infrastructure.Records; +using Infrastructure.Services; +using Infrastructure.Services.Interfaces; +using MediatR; + +namespace Origin.API; + +public static class DependencyInjection +{ + public static IServiceCollection AddServiceClients(this IServiceCollection services) + { + services.AddHttpClient() + .AddScoped() + .AddScoped(); + return services; + } + public static IServiceCollection AddMediatrDependencies(this IServiceCollection services) + { + services.AddMediatR(cf => + { + var assemblies = new List + { + typeof(Program).Assembly + }; + cf.RegisterServicesFromAssemblies(assemblies.ToArray()); + cf.AddOpenBehavior(typeof(LoggingBehavior<,>)); + cf.AddOpenBehavior(typeof(ValidationBehaviour<,>)); + }); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); + services.AddScoped, ProcessSignupCommandHandler>() + .AddScoped, CreateUserCommandHandler>() + .AddScoped, ProcessEligibilityFileCommandHandler>() + .AddScoped, ProcessSignupCommandHandler>() + .AddScoped, TerminateUnlistedUsersCommandHandler>() + .AddScoped, UpdateUserDataCommandHandler>() + .AddScoped, CheckEmployerByEmailQueryHandler>() + .AddScoped, CreateUserCommandHandler>() + .AddScoped, GetEmployerByIdQueryHandler>() + .AddScoped, GetUserByEmailQueryHandler>(); + + services.AddScoped, ProcessSignupCommandHandler>() + .AddScoped, CreateUserCommandHandler>() + .AddScoped, ProcessEligibilityFileCommandHandler>() + .AddScoped, ProcessSignupCommandHandler>() + .AddScoped, TerminateUnlistedUsersCommandHandler>() + .AddScoped, UpdateUserDataCommandHandler>() + .AddScoped, CheckEmployerByEmailQueryHandler>() + .AddScoped, CreateUserCommandHandler>() + .AddScoped, GetEmployerByIdQueryHandler>() + .AddScoped, GetUserByEmailQueryHandler>(); + return services; + } +} \ No newline at end of file diff --git a/src/Origin.API/Dockerfile b/src/Origin.API/Dockerfile new file mode 100644 index 0000000..c289c06 --- /dev/null +++ b/src/Origin.API/Dockerfile @@ -0,0 +1,24 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["Origin.API/Origin.API.csproj", "Origin.API/"] +COPY ["Infrastructure/Infrastructure.csproj", "Infrastructure/"] +COPY ["Domain/Domain.csproj", "Domain/"] +COPY ["Application/Application.csproj", "Application/"] +RUN dotnet restore "Origin.API/Origin.API.csproj" +COPY . . +WORKDIR "/src/Origin.API" +RUN dotnet build "Origin.API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Origin.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Origin.API.dll"] diff --git a/src/Origin.API/Origin.API.csproj b/src/Origin.API/Origin.API.csproj new file mode 100644 index 0000000..b4dc204 --- /dev/null +++ b/src/Origin.API/Origin.API.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + Origin.API + Linux + True + + + + + + + + + + + + + + + diff --git a/src/Origin.API/Program.cs b/src/Origin.API/Program.cs new file mode 100644 index 0000000..6f5370b --- /dev/null +++ b/src/Origin.API/Program.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using Application.Common.Behaviours; +using Application.Messages.Validators.Queries; +using FluentValidation; +using Infrastructure.Configuration; +using MediatR; +using Origin.API; + +var builder = WebApplication.CreateBuilder(args); +var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .Build(); +builder.Services.Configure(configuration); +// Add services to the container. +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddServiceClients() + .AddMediatrDependencies(); +builder.Services.AddValidatorsFromAssemblyContaining(); +var services = builder.Services; +services.BuildServiceProvider(new ServiceProviderOptions() +{ + ValidateScopes = true, + ValidateOnBuild = true +}); + +var app = builder.Build(); + +app.MapControllers(); +app.UseSwagger(); +app.UseSwaggerUI(); +app.UseHttpsRedirection(); + +app.Run(); +public partial class Program {} diff --git a/src/Origin.API/Properties/launchSettings.json b/src/Origin.API/Properties/launchSettings.json new file mode 100644 index 0000000..570dd5a --- /dev/null +++ b/src/Origin.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:19780", + "sslPort": 44349 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5006", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7102;http://localhost:5006", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Origin.API/appsettings.Development.json b/src/Origin.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Origin.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Origin.API/appsettings.Development2.json b/src/Origin.API/appsettings.Development2.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Origin.API/appsettings.Development2.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Origin.API/appsettings.json b/src/Origin.API/appsettings.json new file mode 100644 index 0000000..78b4640 --- /dev/null +++ b/src/Origin.API/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "EmployerServiceBaseUrl": "http://localhost:5001", + "UserServiceBaseUrl": "http://localhost:5002" +} diff --git a/src/Origin.API/origin-backend-take-home-assignment.http b/src/Origin.API/origin-backend-take-home-assignment.http new file mode 100644 index 0000000..0e9e9cf --- /dev/null +++ b/src/Origin.API/origin-backend-take-home-assignment.http @@ -0,0 +1,6 @@ +@origin_backend_take_home_assignment_HostAddress = http://localhost:5006 + +GET {{origin_backend_take_home_assignment_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/origin-backend-take-home-assignment.sln b/src/origin-backend-take-home-assignment.sln new file mode 100644 index 0000000..676c1ba --- /dev/null +++ b/src/origin-backend-take-home-assignment.sln @@ -0,0 +1,58 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Origin.API", "Origin.API\Origin.API.csproj", "{E77A8BD7-36BD-4DED-9B12-35E95F806B26}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8A27C65F-9BCE-494E-A84E-3FA812EA8A8D}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + Makefile = Makefile + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{FFBA3C8D-EE88-4A22-9B2F-D824D38C1BC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\Application.csproj", "{BB9183E0-4282-478E-9E3F-07A916F27D70}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{AC5BF5A2-57E0-451D-9B5C-B34FB466DC4A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0A7E0C23-9879-4D45-BBDF-DF8E85DB350C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.Tests", "Application.Tests\Application.Tests.csproj", "{64D3801E-872F-4B0F-8C59-94647D59F973}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Tests", "Infrastructure.Tests\Infrastructure.Tests.csproj", "{8A362128-03FF-4994-9864-58E88AFAEC75}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E77A8BD7-36BD-4DED-9B12-35E95F806B26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E77A8BD7-36BD-4DED-9B12-35E95F806B26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E77A8BD7-36BD-4DED-9B12-35E95F806B26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E77A8BD7-36BD-4DED-9B12-35E95F806B26}.Release|Any CPU.Build.0 = Release|Any CPU + {FFBA3C8D-EE88-4A22-9B2F-D824D38C1BC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFBA3C8D-EE88-4A22-9B2F-D824D38C1BC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFBA3C8D-EE88-4A22-9B2F-D824D38C1BC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFBA3C8D-EE88-4A22-9B2F-D824D38C1BC2}.Release|Any CPU.Build.0 = Release|Any CPU + {BB9183E0-4282-478E-9E3F-07A916F27D70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB9183E0-4282-478E-9E3F-07A916F27D70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB9183E0-4282-478E-9E3F-07A916F27D70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB9183E0-4282-478E-9E3F-07A916F27D70}.Release|Any CPU.Build.0 = Release|Any CPU + {AC5BF5A2-57E0-451D-9B5C-B34FB466DC4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC5BF5A2-57E0-451D-9B5C-B34FB466DC4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC5BF5A2-57E0-451D-9B5C-B34FB466DC4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC5BF5A2-57E0-451D-9B5C-B34FB466DC4A}.Release|Any CPU.Build.0 = Release|Any CPU + {64D3801E-872F-4B0F-8C59-94647D59F973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64D3801E-872F-4B0F-8C59-94647D59F973}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64D3801E-872F-4B0F-8C59-94647D59F973}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64D3801E-872F-4B0F-8C59-94647D59F973}.Release|Any CPU.Build.0 = Release|Any CPU + {8A362128-03FF-4994-9864-58E88AFAEC75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A362128-03FF-4994-9864-58E88AFAEC75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A362128-03FF-4994-9864-58E88AFAEC75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A362128-03FF-4994-9864-58E88AFAEC75}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {64D3801E-872F-4B0F-8C59-94647D59F973} = {0A7E0C23-9879-4D45-BBDF-DF8E85DB350C} + {8A362128-03FF-4994-9864-58E88AFAEC75} = {0A7E0C23-9879-4D45-BBDF-DF8E85DB350C} + EndGlobalSection +EndGlobal diff --git a/src/origin-backend-take-home-assignment.sln.DotSettings.user b/src/origin-backend-take-home-assignment.sln.DotSettings.user new file mode 100644 index 0000000..444589a --- /dev/null +++ b/src/origin-backend-take-home-assignment.sln.DotSettings.user @@ -0,0 +1,13 @@ + + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Handle_GivenInvalidUser_ReturnsFailure" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Or> + <TestAncestor> + <TestId>xUnit::7A217357-FA1D-4682-AC28-18B7A2FE8D0A::net8.0::Origin.API.IntegrationTests.EligibilityFileControllerIntegrationTests.ProcessEligibilityFile_ReturnsSuccessResult</TestId> + </TestAncestor> + <And> + <Namespace>Application.Tests.Messages</Namespace> + <Project Location="C:\repos\origin-backend-take-home-assignment\src\Application.Tests" Presentation="&lt;Tests&gt;\&lt;Application.Tests&gt;" /> + </And> + </Or> +</SessionState> \ No newline at end of file diff --git a/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/.gitignore b/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/.gitignore new file mode 100644 index 0000000..c7025b7 --- /dev/null +++ b/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.origin-backend-take-home-assignment.iml +/projectSettingsUpdater.xml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/indexLayout.xml b/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/vcs.xml b/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/src/origin-backend-take-home-assignment/.idea/.idea.origin-backend-take-home-assignment/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/origin-backend-take-home-assignment/.vs/origin-backend-take-home-assignment/v16/.suo b/src/origin-backend-take-home-assignment/.vs/origin-backend-take-home-assignment/v16/.suo new file mode 100644 index 0000000..c78e910 Binary files /dev/null and b/src/origin-backend-take-home-assignment/.vs/origin-backend-take-home-assignment/v16/.suo differ