diff --git a/FabApprovalWorkerService/Clients/SmtpClientWrapper.cs b/FabApprovalWorkerService/Clients/SmtpClientWrapper.cs new file mode 100644 index 0000000..5f71cf2 --- /dev/null +++ b/FabApprovalWorkerService/Clients/SmtpClientWrapper.cs @@ -0,0 +1,20 @@ +using System.Net.Mail; + +namespace FabApprovalWorkerService.Clients; + +public interface ISmtpClientWrapper { + void Send(MailMessage message); +} + +public class SmtpClientWrapper : ISmtpClientWrapper { + private SmtpClient _client; + + public SmtpClientWrapper(SmtpClient client) { + _client = client ?? + throw new ArgumentNullException("SmtpClient not injected"); + } + + public void Send(MailMessage message) { + _client.Send(message); + } +} diff --git a/FabApprovalWorkerService/Models/ECN.cs b/FabApprovalWorkerService/Models/ECN.cs new file mode 100644 index 0000000..1b1d313 --- /dev/null +++ b/FabApprovalWorkerService/Models/ECN.cs @@ -0,0 +1,10 @@ +namespace FabApprovalWorkerService.Models; + +public class ECN { + public required int ECNNumber { get; set; } + public bool IsTECN { get; set; } = false; + public DateTime ExpirationDate { get; set; } + public DateTime? ExtensionDate { get; set; } + public required int OriginatorID { get; set; } + public required string Title { get; set; } +} diff --git a/FabApprovalWorkerService/Program.cs b/FabApprovalWorkerService/Program.cs index 7ece500..57c4dda 100644 --- a/FabApprovalWorkerService/Program.cs +++ b/FabApprovalWorkerService/Program.cs @@ -1,14 +1,12 @@ +using FabApprovalWorkerService.Clients; using FabApprovalWorkerService.Services; using FabApprovalWorkerService.Workers; -using Microsoft.Data.Sqlite; - using NLog.Extensions.Logging; using Quartz; -using System.Data; -using Microsoft.Data.SqlClient; +using System.Net.Mail; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -21,7 +19,14 @@ builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped((serviceProvider) => { + return new SmtpClient("mailrelay-external.infineon.com"); +}); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddQuartz(q => { JobKey pendingOOOStatusJob = new JobKey("Pending OOO status job"); diff --git a/FabApprovalWorkerService/Services/ECNService.cs b/FabApprovalWorkerService/Services/ECNService.cs new file mode 100644 index 0000000..dafa272 --- /dev/null +++ b/FabApprovalWorkerService/Services/ECNService.cs @@ -0,0 +1,78 @@ +using FabApprovalWorkerService.Models; + +using System.Text; + +namespace FabApprovalWorkerService.Services; + +public interface IECNService { + Task> GetExpiringTECNs(); + Task> GetExpiredTECNs(); +} + +public class ECNService : IECNService { + private readonly ILogger _logger; + private readonly IDalService _dalService; + + public ECNService(ILogger logger, IDalService dalService) { + _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); + _dalService = dalService ?? throw new ArgumentNullException("IDalService not injected"); + } + + public async Task> GetExpiredTECNs() { + try { + _logger.LogInformation("Attempting to get all TECNs expired in the last day"); + + string today = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + string yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd HH:mm:ss"); + + StringBuilder queryBuilder = new StringBuilder(); + queryBuilder.Append("select ECNNumber, IsTECN, ExpirationDate, ExtensionDate, OriginatorID, Title "); + queryBuilder.Append($"from ECN where IsTECN = 1 and "); + queryBuilder.Append($"ExpirationDate between '{yesterday}' "); + queryBuilder.Append($"and '{today}'"); + + IEnumerable expiredTecns = (await _dalService.QueryAsync(queryBuilder.ToString())) + .Where(e => e.ExtensionDate is null || e.ExtensionDate < DateTime.Now) + .ToList(); + + _logger.LogInformation($"Found {expiredTecns.Count()} expired TECNs"); + + return expiredTecns; + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append("An exception occurred when attempting to get all TECNs expired in the last day. "); + errMsgBuilder.Append($"Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } + + public async Task> GetExpiringTECNs() { + try { + _logger.LogInformation("Attempting to get all TECNs expiring in the next five days"); + + string today = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + string fiveDaysFromToday = DateTime.Now.AddDays(5).ToString("yyyy-MM-dd HH:mm:ss"); + + StringBuilder queryBuilder = new StringBuilder(); + queryBuilder.Append("select ECNNumber, IsTECN, ExpirationDate, ExtensionDate, OriginatorID, Title "); + queryBuilder.Append($"from ECN where IsTECN = 1 and "); + queryBuilder.Append($"ExpirationDate between '{today}' "); + queryBuilder.Append($"and '{fiveDaysFromToday}'"); + + IEnumerable expiringTecns = (await _dalService.QueryAsync(queryBuilder.ToString())) + .Where(e => e.ExtensionDate is null || e.ExtensionDate <= DateTime.Now.AddDays(5)) + .ToList(); + + _logger.LogInformation($"Found {expiringTecns.Count()} expiring TECNs"); + + return expiringTecns; + } catch (Exception ex) { + StringBuilder errMsgBuilder = new(); + errMsgBuilder.Append("An exception occurred when attempting to get all TECNs expiring in the next five days. "); + errMsgBuilder.Append($"Exception: {ex.Message}"); + _logger.LogError(errMsgBuilder.ToString()); + throw; + } + } +} diff --git a/FabApprovalWorkerService/Services/SmtpService.cs b/FabApprovalWorkerService/Services/SmtpService.cs new file mode 100644 index 0000000..37090be --- /dev/null +++ b/FabApprovalWorkerService/Services/SmtpService.cs @@ -0,0 +1,71 @@ +using FabApprovalWorkerService.Clients; + +using Microsoft.IdentityModel.Tokens; + +using System.Net.Mail; + +namespace FabApprovalWorkerService.Services; + +public interface ISmtpService { + Task SendEmail(IEnumerable recipients, IEnumerable ccRecipients, string subject, string body); +} + +public class SmtpService : ISmtpService { + private ILogger _logger; + private ISmtpClientWrapper _smtpClient; + + public SmtpService(ILogger logger, ISmtpClientWrapper smtpClient) { + _logger = logger ?? + throw new ArgumentNullException("ILogger not injected"); + _smtpClient = smtpClient ?? + throw new ArgumentNullException("SmtpClient not injected"); + } + + public async Task SendEmail(IEnumerable recipients, + IEnumerable ccRecipients, + string subject, + string body) { + if (recipients.IsNullOrEmpty()) throw new ArgumentNullException("recipients cannot be null or empty!"); + if (ccRecipients.IsNullOrEmpty()) throw new ArgumentNullException("ccRecipients cannot be null or empty!"); + if (subject.IsNullOrEmpty()) throw new ArgumentNullException("subject cannot be null or empty!"); + if (body.IsNullOrEmpty()) throw new ArgumentNullException("body cannot be null or empty!"); + + return await Task.Run(() => { + int maxRetries = 3; + int backoffSeconds = 30; + + bool messageWasSent = false; + + try { + int remainingRetries = maxRetries; + while (!messageWasSent && remainingRetries > 0) { + try { + Task.Delay((maxRetries - remainingRetries--) * backoffSeconds * 1000); + + _logger.LogInformation($"Attempting to send notification. Remaining retries: {remainingRetries}"); + + MailMessage msg = new MailMessage(); + msg.IsBodyHtml = true; + msg.From = new MailAddress("MesaFabApproval@infineon.com", "Mesa Fab Approval"); + msg.Sender = new MailAddress("MesaFabApproval@infineon.com", "Mesa Fab Approval"); + foreach (MailAddress recipient in recipients) msg.To.Add(recipient); + msg.Bcc.Add("chase.tucker@infineon.com"); + foreach (MailAddress ccRecipient in ccRecipients) msg.CC.Add(ccRecipient); + msg.Subject = subject; + msg.Body = body; + + _smtpClient.Send(msg); + + messageWasSent = true; + } catch (Exception ex) { + _logger.LogError($"Message not sent successfully. Exception: {ex.Message}"); + } + } + } catch (Exception ex) { + _logger.LogError($"An exception occurred when attempting to send notification. Exception: {ex.Message}"); + } + + return messageWasSent; + }); + } +} diff --git a/FabApprovalWorkerService/appsettings.Development.json b/FabApprovalWorkerService/appsettings.Development.json index 9774599..1797133 100644 --- a/FabApprovalWorkerService/appsettings.Development.json +++ b/FabApprovalWorkerService/appsettings.Development.json @@ -1,9 +1,3 @@ { - "ConnectionStrings": { - "Default": "Data Source=MESTSV02EC.infineon.com\\TEST1,50572;Integrated Security=False;Initial Catalog=FabApprovalSystem;User ID=fab_approval_admin_test;Password=Fab_approval_admin_test2023!;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False; Min Pool Size=15" - }, - "MonIn": { - "resource": "FAB_APPROVAL_WORKER_SERVICE_MES_OP_FE_TEST", - "workerUrl": "https://mestsa008.infineon.com:7851/" - } + } diff --git a/FabApprovalWorkerService/appsettings.json b/FabApprovalWorkerService/appsettings.json index f71b03e..1797133 100644 --- a/FabApprovalWorkerService/appsettings.json +++ b/FabApprovalWorkerService/appsettings.json @@ -1,12 +1,3 @@ { - "ConnectionStrings": { - "Development": "Data Source=D:\\FabApprovalWorkerService\\LocalDb.db", - "Default": "Data Source=MESTSV02EC.infineon.com\\TEST1,50572;Integrated Security=False;Initial Catalog=FabApprovalSystem;User ID=fab_approval_admin_test;Password=Fab_approval_admin_test2023!;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False; Min Pool Size=15" - }, - "MonIn": { - "resource": "FAB_APPROVAL_WORKER_SERVICE_MES_OP_FE", - "workerUrl": "https://messa014.infineon.com:7851/", - "retries": 3, - "backoffInSeconds": 30 - } + } diff --git a/FabApprovalWorkerServiceTests/ECNServiceTests.cs b/FabApprovalWorkerServiceTests/ECNServiceTests.cs new file mode 100644 index 0000000..1c66e80 --- /dev/null +++ b/FabApprovalWorkerServiceTests/ECNServiceTests.cs @@ -0,0 +1,217 @@ +using FabApprovalWorkerService.Models; +using FabApprovalWorkerService.Services; + +using Microsoft.Extensions.Logging; + +using Moq; + +namespace FabApprovalWorkerServiceTests; +internal class ECNServiceTests { + Mock> _mockLogger; + Mock _mockDalService; + + ECNService _ecnService; + + [SetUp] + public void Setup() { + _mockLogger = new Mock>(); + _mockDalService = new Mock(); + } + + [Test] + public void EcnServiceWithNullLoggerShouldThrowException() { + Assert.Throws(() => new ECNService(null, _mockDalService.Object)); + } + + [Test] + public void EcnServiceWithNullDalServiceShouldThrowException() { + Assert.Throws(() => new ECNService(_mockLogger.Object, null)); + } + + [Test] + public async Task GetExpiringTECNsWithDbErrorShouldThrowException() { + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).ThrowsAsync(new Exception()); + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _ecnService.GetExpiringTECNs()); + } + + [Test] + public async Task GetExpiredTECNsWithDbErrorShouldThrowException() { + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).ThrowsAsync(new Exception()); + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + Assert.ThrowsAsync(async Task () => await _ecnService.GetExpiredTECNs()); + } + + [Test] + public async Task GetExpiringTECNsWithNoResultsFromDbShouldReturnEmpty() { + IEnumerable emptyEcns = new List(); + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(emptyEcns)); + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + IEnumerable actual = await _ecnService.GetExpiringTECNs(); + + Assert.That(actual, Is.Empty); + } + + [Test] + public async Task GetExpiredTECNsWithNoResultsFromDbShouldReturnEmpty() { + IEnumerable emptyEcns = new List(); + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(emptyEcns)); + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + IEnumerable actual = await _ecnService.GetExpiredTECNs(); + + Assert.That(actual, Is.Empty); + } + + [Test] + public async Task GetExpiringTECNsWithNoExtensionFromDbShouldReturnSameResults() { + IEnumerable ecns = new List() { + new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title1", + IsTECN = true, + ExpirationDate = DateTime.Now.AddDays(1) + }, + new ECN() { + ECNNumber = 2, + OriginatorID = 1, + Title = "title2", + IsTECN = true, + ExpirationDate = DateTime.Now.AddDays(2) + }, + new ECN() { + ECNNumber = 3, + OriginatorID = 1, + Title = "title3", + IsTECN = true, + ExpirationDate = DateTime.Now.AddDays(3) + }, + }; + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(ecns)); + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + IEnumerable actual = await _ecnService.GetExpiringTECNs(); + + Assert.That(actual.Count(), Is.EqualTo(3)); + } + + [Test] + public async Task GetExpiredTECNsWithNoExtensionFromDbShouldReturnSameResults() { + IEnumerable ecns = new List() { + new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title1", + IsTECN = true, + ExpirationDate = DateTime.Now.AddHours(-2) + }, + new ECN() { + ECNNumber = 2, + OriginatorID = 1, + Title = "title2", + IsTECN = true, + ExpirationDate = DateTime.Now.AddHours(-20) + }, + new ECN() { + ECNNumber = 3, + OriginatorID = 1, + Title = "title3", + IsTECN = true, + ExpirationDate = DateTime.Now.AddHours(-12) + }, + }; + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(ecns)); + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + IEnumerable actual = await _ecnService.GetExpiredTECNs(); + + Assert.That(actual.Count(), Is.EqualTo(3)); + } + + // Test when responses include extension dates + [Test] + public async Task GetExpiringTECNsWithExtensionsFromDbShouldReturnSameResults() { + IEnumerable ecns = new List() { + new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title1", + IsTECN = true, + ExpirationDate = DateTime.Now.AddDays(1) + }, + new ECN() { + ECNNumber = 2, + OriginatorID = 1, + Title = "title2", + IsTECN = true, + ExpirationDate = DateTime.Now.AddDays(2) + }, + new ECN() { + ECNNumber = 3, + OriginatorID = 1, + Title = "title3", + IsTECN = true, + ExpirationDate = DateTime.Now.AddDays(3), + ExtensionDate = DateTime.Now.AddDays(10) + }, + }; + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(ecns)); + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + IEnumerable actual = await _ecnService.GetExpiringTECNs(); + + Assert.That(actual.Count(), Is.EqualTo(2)); + } + + [Test] + public async Task GetExpiredTECNsWithExtensionsFromDbShouldReturnSameResults() { + IEnumerable ecns = new List() { + new ECN() { + ECNNumber = 1, + OriginatorID = 1, + Title = "title1", + IsTECN = true, + ExpirationDate = DateTime.Now.AddHours(-2) + }, + new ECN() { + ECNNumber = 2, + OriginatorID = 1, + Title = "title2", + IsTECN = true, + ExpirationDate = DateTime.Now.AddHours(-20) + }, + new ECN() { + ECNNumber = 3, + OriginatorID = 1, + Title = "title3", + IsTECN = true, + ExpirationDate = DateTime.Now.AddHours(-12), + ExtensionDate = DateTime.Now.AddDays(10) + }, + }; + + _mockDalService.Setup(d => d.QueryAsync(It.IsAny())).Returns(Task.FromResult(ecns)); + + _ecnService = new ECNService(_mockLogger.Object, _mockDalService.Object); + + IEnumerable actual = await _ecnService.GetExpiredTECNs(); + + Assert.That(actual.Count(), Is.EqualTo(2)); + } +} diff --git a/FabApprovalWorkerServiceTests/SmtpServiceTests.cs b/FabApprovalWorkerServiceTests/SmtpServiceTests.cs new file mode 100644 index 0000000..191ca69 --- /dev/null +++ b/FabApprovalWorkerServiceTests/SmtpServiceTests.cs @@ -0,0 +1,117 @@ +using FabApprovalWorkerService.Clients; +using FabApprovalWorkerService.Services; + +using Microsoft.Extensions.Logging; + +using Moq; + +using System.Net.Mail; + +namespace FabApprovalWorkerServiceTests; +internal class SmtpServiceTests { + private static readonly List ADDRESS_LIST = new List() { + new MailAddress("fake@email.com") + }; + + private Mock> _mockLogger; + private Mock _mockSmtpClient; + + private SmtpService _smtpService; + + [SetUp] + public void Setup() { + _mockLogger = new Mock>(); + _mockSmtpClient = new Mock(); + } + + [Test] + public void SmtpServiceWithNullLoggerShouldThrowException() { + Assert.Throws(() => new SmtpService(null, _mockSmtpClient.Object)); + } + + [Test] + public void SmtpServiceWithNullSmtpClientShouldThrowException() { + Assert.Throws(() => new SmtpService(_mockLogger.Object, null)); + } + + [Test] + public void SendMailWithNullRecipientsShouldThrowException() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.ThrowsAsync(async Task () => { + await _smtpService.SendEmail(null, ADDRESS_LIST, "subject", "body"); + }); + } + + [Test] + public void SendMailWithEmptyRecipientsShouldThrowException() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.ThrowsAsync(async Task () => { + await _smtpService.SendEmail(new List (), ADDRESS_LIST, "subject", "body"); + }); + } + + [Test] + public void SendMailWithNullccRecipientsShouldThrowException() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.ThrowsAsync(async Task () => { + await _smtpService.SendEmail(ADDRESS_LIST, null, "subject", "body"); + }); + } + + [Test] + public void SendMailWithEmptyccRecipientsShouldThrowException() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.ThrowsAsync(async Task () => { + await _smtpService.SendEmail(ADDRESS_LIST, new List(), "subject", "body"); + }); + } + + [Test] + public void SendMailWithNullSubjectShouldThrowException() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.ThrowsAsync(async Task () => { + await _smtpService.SendEmail(ADDRESS_LIST, ADDRESS_LIST, null, "body"); + }); + } + + [Test] + public void SendMailWithEmptySubjectShouldThrowException() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.ThrowsAsync(async Task () => { + await _smtpService.SendEmail(ADDRESS_LIST, ADDRESS_LIST, "", "body"); + }); + } + + [Test] + public void SendMailWithNullBodyShouldThrowException() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.ThrowsAsync(async Task () => { + await _smtpService.SendEmail(ADDRESS_LIST, ADDRESS_LIST, "subject", null); + }); + } + + [Test] + public void SendMailWithEmptyBodyShouldThrowException() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.ThrowsAsync(async Task () => { + await _smtpService.SendEmail(ADDRESS_LIST, ADDRESS_LIST, "subject", ""); + }); + } + + [Test] + public async Task SendEmailWithValidArgsShouldSendMailThroughClient() { + _smtpService = new SmtpService(_mockLogger.Object, _mockSmtpClient.Object); + + Assert.True(await _smtpService.SendEmail(ADDRESS_LIST, ADDRESS_LIST, "subject", "body")); + + _mockSmtpClient.Verify(s => s.Send(It.IsAny())); + } +} diff --git a/FabApprovalWorkerServiceTests/UserServiceTests.cs b/FabApprovalWorkerServiceTests/UserServiceTests.cs index 88192ec..9e81522 100644 --- a/FabApprovalWorkerServiceTests/UserServiceTests.cs +++ b/FabApprovalWorkerServiceTests/UserServiceTests.cs @@ -1,5 +1,3 @@ -using Dapper.Contrib.Extensions; - using FabApprovalWorkerService.Models; using FabApprovalWorkerService.Services; @@ -9,7 +7,7 @@ using Moq; namespace FabApprovalWorkerServiceTests; -public class UserServiceTests { +internal class UserServiceTests { private static readonly ILogger MOCK_LOGGER = Mock.Of>(); private static readonly IEnumerable MOCK_USERS = new List() {