diff --git a/.vscode/settings.json b/.vscode/settings.json index f05d843..6552d06 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -420,5 +420,6 @@ "windowsphone", "Winsock", "worlflow" - ] + ], + "dotnet.preferCSharpExtension": true } \ No newline at end of file diff --git a/Fab2ApprovalSystem.sln b/Fab2ApprovalSystem.sln index 95aaedb..dddfd27 100644 --- a/Fab2ApprovalSystem.sln +++ b/Fab2ApprovalSystem.sln @@ -6,6 +6,9 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fab2ApprovalSystem", "Fab2ApprovalSystem\Fab2ApprovalSystem.csproj", "{AAE52608-4DD1-4732-92BD-CC8915DEC71E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MesaFabApproval.API", "MesaFabApproval.API\MesaFabApproval.API.csproj", "{852E528D-015A-43B5-999D-F281E3359E5E}" + ProjectSection(ProjectDependencies) = postProject + {2C16014D-B04E-46AF-AB4C-D2691D44A339} = {2C16014D-B04E-46AF-AB4C-D2691D44A339} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MesaFabApproval.Shared", "MesaFabApproval.Shared\MesaFabApproval.Shared.csproj", "{2C16014D-B04E-46AF-AB4C-D2691D44A339}" EndProject @@ -14,11 +17,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MesaFabApproval.Client", "M {2C16014D-B04E-46AF-AB4C-D2691D44A339} = {2C16014D-B04E-46AF-AB4C-D2691D44A339} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MesaFabApproval.API.Test", "MesaFabApproval.Test\MesaFabApproval.API.Test.csproj", "{D03AB305-BA29-4EB1-AC66-ABBF76FBF5C1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MesaFabApproval.API.Test", "MesaFabApproval.API.Test\MesaFabApproval.API.Test.csproj", "{D03AB305-BA29-4EB1-AC66-ABBF76FBF5C1}" + ProjectSection(ProjectDependencies) = postProject + {2C16014D-B04E-46AF-AB4C-D2691D44A339} = {2C16014D-B04E-46AF-AB4C-D2691D44A339} + {852E528D-015A-43B5-999D-F281E3359E5E} = {852E528D-015A-43B5-999D-F281E3359E5E} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MesaFabApproval.Client.Test", "MesaFabApproval.Client.Test\MesaFabApproval.Client.Test.csproj", "{A0E5BD7D-3910-43BD-BBA3-3820AD524423}" ProjectSection(ProjectDependencies) = postProject {2C16014D-B04E-46AF-AB4C-D2691D44A339} = {2C16014D-B04E-46AF-AB4C-D2691D44A339} {34D52F44-A81F-4247-8180-16E204824A07} = {34D52F44-A81F-4247-8180-16E204824A07} - {852E528D-015A-43B5-999D-F281E3359E5E} = {852E528D-015A-43B5-999D-F281E3359E5E} EndProjectSection EndProject Global @@ -47,6 +55,10 @@ Global {D03AB305-BA29-4EB1-AC66-ABBF76FBF5C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {D03AB305-BA29-4EB1-AC66-ABBF76FBF5C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {D03AB305-BA29-4EB1-AC66-ABBF76FBF5C1}.Release|Any CPU.Build.0 = Release|Any CPU + {A0E5BD7D-3910-43BD-BBA3-3820AD524423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0E5BD7D-3910-43BD-BBA3-3820AD524423}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0E5BD7D-3910-43BD-BBA3-3820AD524423}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0E5BD7D-3910-43BD-BBA3-3820AD524423}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MesaFabApproval.Test/MesaFabApproval.API.Test.csproj b/MesaFabApproval.API.Test/MesaFabApproval.API.Test.csproj similarity index 100% rename from MesaFabApproval.Test/MesaFabApproval.API.Test.csproj rename to MesaFabApproval.API.Test/MesaFabApproval.API.Test.csproj diff --git a/MesaFabApproval.Test/MonInUtilsTests.cs b/MesaFabApproval.API.Test/MonInUtilsTests.cs similarity index 98% rename from MesaFabApproval.Test/MonInUtilsTests.cs rename to MesaFabApproval.API.Test/MonInUtilsTests.cs index 061ddaf..4fa8fe4 100644 --- a/MesaFabApproval.Test/MonInUtilsTests.cs +++ b/MesaFabApproval.API.Test/MonInUtilsTests.cs @@ -4,7 +4,7 @@ using MesaFabApproval.API.Utilities; using MesaFabApproval.Shared.Models; using MesaFabApproval.Shared.Services; -namespace NICAIntegrationServiceTests.Util; +namespace MesaFabApproval.API.Test; public class MonInUtilsTests { private readonly Mock _mockMonInClient; diff --git a/MesaFabApproval.Test/PCRBServiceTests.cs b/MesaFabApproval.API.Test/PCRBServiceTests.cs similarity index 99% rename from MesaFabApproval.Test/PCRBServiceTests.cs rename to MesaFabApproval.API.Test/PCRBServiceTests.cs index 61c483c..cf1b95b 100644 --- a/MesaFabApproval.Test/PCRBServiceTests.cs +++ b/MesaFabApproval.API.Test/PCRBServiceTests.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; using Moq; -namespace MesaFabApproval.Tests.Services; +namespace MesaFabApproval.API.Test; public static class MockMemoryCacheService { public static Mock GetMemoryCache(object expectedValue) { diff --git a/MesaFabApproval.Client.Test/MesaFabApproval.Client.Test.csproj b/MesaFabApproval.Client.Test/MesaFabApproval.Client.Test.csproj new file mode 100644 index 0000000..a19130f --- /dev/null +++ b/MesaFabApproval.Client.Test/MesaFabApproval.Client.Test.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/MesaFabApproval.Client.Test/PCRBServiceTests.cs b/MesaFabApproval.Client.Test/PCRBServiceTests.cs new file mode 100644 index 0000000..7073309 --- /dev/null +++ b/MesaFabApproval.Client.Test/PCRBServiceTests.cs @@ -0,0 +1,347 @@ +using System.Net; +using System.Text.Json; + +using MesaFabApproval.Client.Services; +using MesaFabApproval.Shared.Models; + +using Microsoft.Extensions.Caching.Memory; + +using Moq; +using Moq.Protected; + +using MudBlazor; + +namespace MesaFabApproval.Client.Test; + +public class PCRBServiceTests { + private readonly Mock _mockCache; + private readonly Mock _mockHttpClientFactory; + private readonly Mock _mockSnackbar; + private readonly Mock _mockUserService; + private readonly PCRBService _pcrbService; + + private static IEnumerable FOLLOW_UPS = new List() { + new PCRBFollowUp { ID = 1, PlanNumber = 1, Step = 1, FollowUpDate = DateTime.Now } + }; + + private static HttpResponseMessage SUCCESSFUL_RESPONSE = new HttpResponseMessage(HttpStatusCode.OK); + private static HttpResponseMessage UNSUCCESSFUL_RESPONSE = new HttpResponseMessage(HttpStatusCode.InternalServerError); + + public static class MockMemoryCacheService { + public static Mock GetMemoryCache(object expectedValue) { + Mock mockMemoryCache = new Mock(); + mockMemoryCache + .Setup(x => x.TryGetValue(It.IsAny(), out expectedValue)) + .Returns(true); + mockMemoryCache + .Setup(x => x.CreateEntry(It.IsAny())) + .Returns(Mock.Of()); + return mockMemoryCache; + } + } + + public PCRBServiceTests() { + _mockCache = MockMemoryCacheService.GetMemoryCache(FOLLOW_UPS); + _mockHttpClientFactory = new Mock(); + _mockSnackbar = new Mock(); + _mockUserService = new Mock(); + + _pcrbService = new PCRBService( + _mockCache.Object, + _mockHttpClientFactory.Object, + _mockSnackbar.Object, + _mockUserService.Object); + } + + [Fact] + public async Task CreateFollowUp_WithValidParams_ShouldCallHttpPost_AndRefreshCache() { + PCRBFollowUp followUp = new PCRBFollowUp { + ID = 1, + PlanNumber = 123, + Step = 1, + FollowUpDate = DateTime.Now, + Comments = "Test" + }; + + HttpResponseMessage getResponse = new HttpResponseMessage { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(new List { followUp })) + }; + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Post), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(getResponse) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.CreateFollowUp(followUp); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Post && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp")), + ItExpr.IsAny()); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Get && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUps?planNumber=123&bypassCache=True")), + ItExpr.IsAny()); + } + + [Fact] + public async Task CreateFollowUp_WithBadResponse_ShouldThrowException() { + PCRBFollowUp followUp = new PCRBFollowUp { + ID = 1, + PlanNumber = 123, + Step = 1, + FollowUpDate = DateTime.Now, + Comments = "Test" + }; + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Post), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(followUp)); + } + + [Fact] + public async Task CreateFollowUp_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(null)); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithBypassCache_ShouldCallHttpGetAndReturnFollowUps() { + PCRBFollowUp followUp = new PCRBFollowUp { + ID = 1, + PlanNumber = 123, + Step = 1, + FollowUpDate = DateTime.Now, + Comments = "Test" + }; + + HttpResponseMessage getResponse = new HttpResponseMessage { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(new List { followUp })) + }; + + var mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(getResponse) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + IEnumerable followUps = await _pcrbService.GetFollowUpsByPlanNumber(123, true); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Get && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUps?planNumber=123&bypassCache=True")), + ItExpr.IsAny()); + + Assert.Single(followUps); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithoutBypassCache_ShouldReturnFollowUpsFromCache() { + IEnumerable followUps = await _pcrbService.GetFollowUpsByPlanNumber(1, false); + Assert.Single(followUps); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithBadResponse_ShouldThrowException() { + var mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.GetFollowUpsByPlanNumber(1, true)); + } + + [Fact] + public async Task UpdateFollowUp_WithValidParams_ShouldCallHttpPut() { + PCRBFollowUp followUp = new PCRBFollowUp { + ID = 1, + PlanNumber = 123, + Step = 1, + FollowUpDate = DateTime.Now, + Comments = "Test" + }; + + var mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Put), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.UpdateFollowUp(followUp); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Put && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp")), + ItExpr.IsAny()); + } + + [Fact] + public async Task UpdateFollowUp_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(null)); + } + + [Fact] + public async Task UpdateFollowUp_WithBadResponse_ShouldThrowException() { + PCRBFollowUp followUp = new PCRBFollowUp { + ID = 1, + PlanNumber = 123, + Step = 1, + FollowUpDate = DateTime.Now, + Comments = "Test" + }; + + var mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Put), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(followUp)); + } + + [Fact] + public async Task DeleteFollowUp_WithValidParams_ShouldCallHttpDelete() { + var mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Delete), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.DeleteFollowUp(1); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Delete && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp?id=1")), + ItExpr.IsAny()); + } + + [Fact] + public async Task DeleteFollowUp_WithBadId_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(0)); + } + + [Fact] + public async Task DeleteFollowUp_WithBadResponse_ShouldThrowException() { + var mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Delete), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(1)); + } +} diff --git a/MesaFabApproval.Client/Services/PCRBService.cs b/MesaFabApproval.Client/Services/PCRBService.cs index 42a6ae5..23a9ac0 100644 --- a/MesaFabApproval.Client/Services/PCRBService.cs +++ b/MesaFabApproval.Client/Services/PCRBService.cs @@ -4,7 +4,6 @@ using System.Text.Json; using MesaFabApproval.Shared.Models; -using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Caching.Memory; @@ -40,6 +39,10 @@ public interface IPCRBService { Task NotifyApprovers(PCRBNotification notification); Task NotifyOriginator(PCRBNotification notification); Task NotifyResponsiblePerson(PCRBActionItemNotification notification); + Task CreateFollowUp(PCRBFollowUp followUp); + Task> GetFollowUpsByPlanNumber(int planNumber, bool bypassCache); + Task UpdateFollowUp(PCRBFollowUp followUp); + Task DeleteFollowUp(int id); } public class PCRBService : IPCRBService { @@ -764,4 +767,86 @@ public class PCRBService : IPCRBService { if (!responseMessage.IsSuccessStatusCode) throw new Exception($"Unable to notify PCRB responsible person, because {responseMessage.ReasonPhrase}"); } + + public async Task CreateFollowUp(PCRBFollowUp followUp) { + if (followUp is null) throw new ArgumentNullException("follow up cannot be null"); + + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Post, $"pcrb/followUp") { + Content = new StringContent(JsonSerializer.Serialize(followUp), + Encoding.UTF8, + "application/json") + }; + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) + throw new Exception(responseMessage.ReasonPhrase); + + await GetFollowUpsByPlanNumber(followUp.PlanNumber, true); + } + + public async Task> GetFollowUpsByPlanNumber(int planNumber, bool bypassCache) { + if (planNumber <= 0) throw new ArgumentException($"{planNumber} is not a valid PCRB Plan#"); + + IEnumerable? followUps = null; + if (!bypassCache) + followUps = _cache.Get>($"pcrbFollowUps{planNumber}"); + + if (followUps is null) { + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Get, $"pcrb/followUps?planNumber={planNumber}&bypassCache={bypassCache}"); + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (responseMessage.IsSuccessStatusCode) { + string responseContent = await responseMessage.Content.ReadAsStringAsync(); + + JsonSerializerOptions jsonSerializerOptions = new() { + PropertyNameCaseInsensitive = true + }; + + followUps = JsonSerializer.Deserialize>(responseContent, jsonSerializerOptions) ?? + new List(); + + if (followUps.Count() > 0) + _cache.Set($"pcrbFollowUps{planNumber}", followUps, DateTimeOffset.Now.AddMinutes(5)); + } else { + throw new Exception(responseMessage.ReasonPhrase); + } + } + + return followUps; + } + + public async Task UpdateFollowUp(PCRBFollowUp followUp) { + if (followUp is null) throw new ArgumentNullException("follow up cannot be null"); + + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Put, $"pcrb/followUp") { + Content = new StringContent(JsonSerializer.Serialize(followUp), + Encoding.UTF8, + "application/json") + }; + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) + throw new Exception(responseMessage.ReasonPhrase); + } + + public async Task DeleteFollowUp(int id) { + if (id <= 0) throw new ArgumentException($"{id} is not a valid PCRB follow up ID"); + + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Delete, $"pcrb/followUp?id={id}"); + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) throw new Exception(responseMessage.ReasonPhrase); + } } diff --git a/MesaFabApproval.Client/pipeline.yml b/MesaFabApproval.Client/pipeline.yml index 6e7bdc9..9b91cba 100644 --- a/MesaFabApproval.Client/pipeline.yml +++ b/MesaFabApproval.Client/pipeline.yml @@ -30,6 +30,14 @@ stages: configuration: $(BuildConfiguration) projects: MesaFabApproval.Client + - task: DotNetCoreCLI@2 + displayName: "Test" + inputs: + command: "test" + configuration: $(BuildConfiguration) + publishTestResults: true + projects: MesaFabApproval.Client.Test + - task: DotNetCoreCLI@2 displayName: "Publish" inputs: @@ -66,6 +74,14 @@ stages: configuration: $(BuildConfiguration) projects: MesaFabApproval.Client + - task: DotNetCoreCLI@2 + displayName: "Test" + inputs: + command: "test" + configuration: $(BuildConfiguration) + publishTestResults: true + projects: MesaFabApproval.Client.Test + - task: DotNetCoreCLI@2 displayName: "Publish" inputs: