diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Acceptance/GlobalUsings.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Acceptance/GlobalUsings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Acceptance/GlobalUsings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Acceptance/OrcaHello.Web.Api.Tests.Acceptance.csproj b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Acceptance/OrcaHello.Web.Api.Tests.Acceptance.csproj new file mode 100644 index 00000000..b8cb93df --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Acceptance/OrcaHello.Web.Api.Tests.Acceptance.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Default.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Default.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..090ca176 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Default.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync.cs @@ -0,0 +1,35 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class CommentsControllerTests + { + [TestMethod] + public async Task Default_GetGetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync_Expect_DetectionListResponse() + { + CommentListResponse response = new() + { + Count = 2, + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Comments = new List { new() } + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Comments.Count(), + ((CommentListResponse)contentResult.Value).Comments.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Default.GetPaginatedPositiveCommentsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Default.GetPaginatedPositiveCommentsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..c3d4d3f3 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Default.GetPaginatedPositiveCommentsForGivenTimeframeAsync.cs @@ -0,0 +1,35 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class CommentsControllerTests + { + [TestMethod] + public async Task Default_GetGetPaginatedPositiveCommentsForGivenTimeframeAsync_Expect_DetectionListResponse() + { + CommentListResponse response = new() + { + Count = 2, + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Comments = new List { new() } + }; + + _orchestrationServiceMock.Setup(service => + service.RetrievePositiveCommentsForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetPaginatedPositiveCommentsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Comments.Count(), + ((CommentListResponse)contentResult.Value).Comments.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrievePositiveCommentsForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Setup.cs new file mode 100644 index 00000000..b0da317d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/Setup.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class CommentsControllerTests + { + private readonly Mock _orchestrationServiceMock; + private readonly CommentsController _controller; + + public CommentsControllerTests() + { + _orchestrationServiceMock = new Mock(); + + _controller = new CommentsController( + commentOrchestrationService: _orchestrationServiceMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _orchestrationServiceMock.VerifyNoOtherCalls(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/TryCatch.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/TryCatch.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..d5e7e71a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/TryCatch.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class CommentsControllerTests + { + [TestMethod] + public async Task TryCatch_GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new CommentOrchestrationValidationException(new Exception())) + .Throws(new CommentOrchestrationDependencyValidationException(new Exception())) + + .Throws(new CommentOrchestrationDependencyException(new Exception())) + .Throws(new CommentOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveNegativeComments(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveNegativeComments(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveNegativeComments(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/TryCatch.GetPaginatedPositiveCommentsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/TryCatch.GetPaginatedPositiveCommentsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..7a97faa3 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/CommentsControllerTests/TryCatch.GetPaginatedPositiveCommentsForGivenTimeframeAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class CommentsControllerTests + { + [TestMethod] + public async Task TryCatch_GetPaginatedPositiveCommentsForGivenTimeframeAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrievePositiveCommentsForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new CommentOrchestrationValidationException(new Exception())) + .Throws(new CommentOrchestrationDependencyValidationException(new Exception())) + + .Throws(new CommentOrchestrationDependencyException(new Exception())) + .Throws(new CommentOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveComments(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveComments(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrievePositiveCommentsForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveComments(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetPaginatedPositiveCommentsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetDetectionByIdAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetDetectionByIdAsync.cs new file mode 100644 index 00000000..75af930e --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetDetectionByIdAsync.cs @@ -0,0 +1,30 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + + [TestMethod] + public async Task Default_GetDetectionByIdAsync_Expect_Detection() + { + Detection expectedResult = new(); + + _orchestrationServiceMock.Setup(service => + service.RetrieveDetectionByIdAsync(It.IsAny())) + .ReturnsAsync(expectedResult); + + ActionResult actionResult = + await _controller.GetDetectionByIdAsync(Guid.NewGuid().ToString()); + + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.IsNotNull((Detection)contentResult.Value); + + _orchestrationServiceMock.Verify(service => + service.RetrieveDetectionByIdAsync(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetDetectionsForGivenInterestLabelAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetDetectionsForGivenInterestLabelAsync.cs new file mode 100644 index 00000000..6dd4fea8 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetDetectionsForGivenInterestLabelAsync.cs @@ -0,0 +1,34 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + [TestMethod] + public async Task Default_GetDetectionsForGivenInterestLabelAsyncc_Expect_DetectionListForInterestLabelResponse() + { + DetectionListForInterestLabelResponse response = new() + { + TotalCount = 1, + Detections = new List { new Detection() }, + InterestLabel = "test" + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveDetectionsForGivenInterestLabelAsync(It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetDetectionsForGivenInterestLabelAsync("test"); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Detections.Count(), + ((DetectionListForInterestLabelResponse)contentResult.Value).Detections.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrieveDetectionsForGivenInterestLabelAsync(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetPaginatedDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetPaginatedDetectionsAsync.cs new file mode 100644 index 00000000..ada9c2a9 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetPaginatedDetectionsAsync.cs @@ -0,0 +1,41 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + [TestMethod] + public async Task Default_GetPaginatedDetectionsAsync_Expect_DetectionListResponse() + { + DetectionListResponse response = new() + { + Count = 2, + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Detections = new List { new() }, + State = "Positive", + SortBy = "timestamp", + SortOrder = "desc", + Location = "location" + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveFilteredDetectionsAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetPaginatedDetectionsAsync("Positive", DateTime.Now, DateTime.Now.AddDays(1), "timestamp", true, 1, 10, null); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Detections.Count(), + ((DetectionListResponse)contentResult.Value).Detections.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrieveFilteredDetectionsAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetPaginatedDetectionsForGivenTimeframeAndTagAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetPaginatedDetectionsForGivenTimeframeAndTagAsync.cs new file mode 100644 index 00000000..f2b997e4 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.GetPaginatedDetectionsForGivenTimeframeAndTagAsync.cs @@ -0,0 +1,35 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + [TestMethod] + public async Task Default_GetPaginatedDetectionsForGivenTimeframeAndTagAsync_Expect_DetectionListResponse() + { + DetectionListForTagResponse response = new() + { + Count = 2, + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Detections = new List { new Detection() } + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveDetectionsForGivenTimeframeAndTagAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetPaginatedDetectionsForGivenTimeframeAndTagAsync("tag", DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Detections.Count, + ((DetectionListForTagResponse)contentResult.Value).Detections.Count); + + _orchestrationServiceMock.Verify(service => + service.RetrieveDetectionsForGivenTimeframeAndTagAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.PutModeratedInfoAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.PutModeratedInfoAsync.cs new file mode 100644 index 00000000..ab1fdee7 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Default.PutModeratedInfoAsync.cs @@ -0,0 +1,39 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + + [TestMethod] + public async Task Default_PutModeratedInfoAsync_Expect_Detection() + { + Detection expectedResult = new(); + + _orchestrationServiceMock.Setup(service => + service.ModerateDetectionByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + ModerateDetectionRequest request = new() + { + Id = Guid.NewGuid().ToString(), + Moderator = "Ira M. Goober", + DateModerated = DateTime.UtcNow, + Comments = "Comments", + Tags = new List() { "Tag1" } + }; + + ActionResult actionResult = + await _controller.PutModeratedInfoAsync(Guid.NewGuid().ToString(), request); + + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.IsNotNull((Detection)contentResult.Value); + + _orchestrationServiceMock.Verify(service => + service.ModerateDetectionByIdAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Setup.cs new file mode 100644 index 00000000..385040c5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/Setup.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + [TestClass] + [ExcludeFromCodeCoverage] + public partial class DetectionsControllerTests + { + private readonly Mock _orchestrationServiceMock; + private readonly DetectionsController _controller; + + public DetectionsControllerTests() + { + _orchestrationServiceMock = new Mock(); + + _controller = new DetectionsController( + detectionOrchestrationService: _orchestrationServiceMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _orchestrationServiceMock.VerifyNoOtherCalls(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetDetectionByIdAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetDetectionByIdAsync.cs new file mode 100644 index 00000000..477f8af3 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetDetectionByIdAsync.cs @@ -0,0 +1,45 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + [TestMethod] + public async Task TryCatch_GetDetectionByIdAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveDetectionByIdAsync(It.IsAny())) + + .Throws(new DetectionOrchestrationValidationException(new NotFoundMetadataException("id"))) + + .Throws(new DetectionOrchestrationValidationException(new Exception())) + .Throws(new DetectionOrchestrationDependencyValidationException(new Exception())) + + .Throws(new DetectionOrchestrationDependencyException(new Exception())) + .Throws(new DetectionOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveDetectionById(1, StatusCodes.Status404NotFound); + await ExecuteRetrieveDetectionById(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveDetectionById(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveDetectionByIdAsync(It.IsAny()), + Times.Exactly(6)); + + } + + private async Task ExecuteRetrieveDetectionById(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetDetectionByIdAsync(It.IsAny()); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetDetectionsForGivenInterestLabelAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetDetectionsForGivenInterestLabelAsync.cs new file mode 100644 index 00000000..e643f36e --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetDetectionsForGivenInterestLabelAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + [TestMethod] + public async Task TryCatch_GetDetectionsForGivenInterestLabelAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveDetectionsForGivenInterestLabelAsync(It.IsAny())) + + .Throws(new DetectionOrchestrationValidationException(new Exception())) + .Throws(new DetectionOrchestrationDependencyValidationException(new Exception())) + + .Throws(new DetectionOrchestrationDependencyException(new Exception())) + .Throws(new DetectionOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveDetectionsByLabel(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveDetectionsByLabel(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveDetectionsForGivenInterestLabelAsync(It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveDetectionsByLabel(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetDetectionsForGivenInterestLabelAsync(It.IsAny()); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetPaginatedDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetPaginatedDetectionsAsync.cs new file mode 100644 index 00000000..0ced373c --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetPaginatedDetectionsAsync.cs @@ -0,0 +1,47 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + + + [TestMethod] + public async Task TryCatch_GetPaginatedDetectionsAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveFilteredDetectionsAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new DetectionOrchestrationValidationException(new Exception())) + .Throws(new DetectionOrchestrationDependencyValidationException(new Exception())) + + .Throws(new DetectionOrchestrationDependencyException(new Exception())) + .Throws(new DetectionOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveDetections(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveDetections(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveFilteredDetectionsAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveDetections(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetPaginatedDetectionsAsync("state", DateTime.Now, DateTime.Now.AddDays(1), "sortBy", true, + 1, 10, "location"); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetPaginatedDetectionsForGivenTimeframeAndTagAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetPaginatedDetectionsForGivenTimeframeAndTagAsync.cs new file mode 100644 index 00000000..98f1a682 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.GetPaginatedDetectionsForGivenTimeframeAndTagAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + [TestMethod] + public async Task TryCatch_GetPaginatedDetectionsForGivenTimeframeAndTagAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveDetectionsForGivenTimeframeAndTagAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new DetectionOrchestrationValidationException(new Exception())) + .Throws(new DetectionOrchestrationDependencyValidationException(new Exception())) + + .Throws(new DetectionOrchestrationDependencyException(new Exception())) + .Throws(new DetectionOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveDetectionsForTag(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveDetectionsForTag(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveDetectionsForGivenTimeframeAndTagAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveDetectionsForTag(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetPaginatedDetectionsForGivenTimeframeAndTagAsync("tag", DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.PutModeratedInfoAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.PutModeratedInfoAsync.cs new file mode 100644 index 00000000..2cb56f87 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/DetectionsControllerTests/TryCatch.PutModeratedInfoAsync.cs @@ -0,0 +1,49 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class DetectionsControllerTests + { + [TestMethod] + public async Task TryCatch_PutModeratedInfoAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.ModerateDetectionByIdAsync(It.IsAny(), It.IsAny())) + + .Throws(new DetectionOrchestrationValidationException(new NotFoundMetadataException("id"))) + + .Throws(new DetectionOrchestrationValidationException(new DetectionNotDeletedException("id"))) + .Throws(new DetectionOrchestrationValidationException(new DetectionNotInsertedException("id"))) + + .Throws(new DetectionOrchestrationValidationException(new Exception())) + .Throws(new DetectionOrchestrationDependencyValidationException(new Exception())) + + .Throws(new DetectionOrchestrationDependencyException(new Exception())) + .Throws(new DetectionOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteModerateDetectionById(1, StatusCodes.Status404NotFound); + await ExecuteModerateDetectionById(2, StatusCodes.Status422UnprocessableEntity); + await ExecuteModerateDetectionById(2, StatusCodes.Status400BadRequest); + await ExecuteModerateDetectionById(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .ModerateDetectionByIdAsync(It.IsAny(), It.IsAny()), + Times.Exactly(8)); + + } + + private async Task ExecuteModerateDetectionById(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.PutModeratedInfoAsync("id", new ModerateDetectionRequest()); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/Default.GetHydrophones.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/Default.GetHydrophones.cs new file mode 100644 index 00000000..e3fa8eb5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/Default.GetHydrophones.cs @@ -0,0 +1,39 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class HydrophonesControllerTests + { + [TestMethod] + public async Task Default_GetHydrophones_Expect_HydrophoneListResponse() + { + HydrophoneListResponse response = new() + { + Hydrophones = new List + { + new() + { + Name = "Test" + } + }, + Count = 1 + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveHydrophoneLocations()) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetHydrophones(); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Hydrophones.Count, + ((HydrophoneListResponse)contentResult.Value!).Hydrophones.Count); + + _orchestrationServiceMock.Verify(service => + service.RetrieveHydrophoneLocations(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/Setup.cs new file mode 100644 index 00000000..255d7a02 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/Setup.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + [TestClass] + [ExcludeFromCodeCoverage] + public partial class HydrophonesControllerTests + { + private readonly Mock _orchestrationServiceMock; + private readonly HydrophonesController _controller; + + public HydrophonesControllerTests() + { + _orchestrationServiceMock = new Mock(); + + _controller = new HydrophonesController( + hydrophoneOrchestrationService: _orchestrationServiceMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _orchestrationServiceMock.VerifyNoOtherCalls(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/TryCatch.GetHydrophones.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/TryCatch.GetHydrophones.cs new file mode 100644 index 00000000..4b469d9d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/HydrophoneControllerTests/TryCatch.GetHydrophones.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class HydrophonesControllerTests + { + + [TestMethod] + public async Task TryCatch_GetHydrophones_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveHydrophoneLocations()) + + .Throws(new HydrophoneOrchestrationValidationException(new InvalidHydrophoneException())) + + .Throws(new HydrophoneOrchestrationValidationException(new Exception())) + .Throws(new HydrophoneOrchestrationDependencyValidationException(new Exception())) + + .Throws(new HydrophoneOrchestrationDependencyException(new Exception())) + .Throws(new HydrophoneOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveHydrophones(1, StatusCodes.Status404NotFound); + await ExecuteRetrieveHydrophones(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveHydrophones(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveHydrophoneLocations(), + Times.Exactly(6)); + + } + + private async Task ExecuteRetrieveHydrophones(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetHydrophones(); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.AddInterestLabelToDetectionAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.AddInterestLabelToDetectionAsync.cs new file mode 100644 index 00000000..a4709084 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.AddInterestLabelToDetectionAsync.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class InterestLabelsControllerTests + { + [TestMethod] + public async Task Default_AddInterestLabelToDetectionAsync_Expect_Detection() + { + InterestLabelAddResponse expectedResult = new(); + + _orchestrationServiceMock.Setup(service => + service.AddInterestLabelToDetectionAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + ActionResult actionResult = + await _controller.AddInterestLabelToDetectionAsync("id", "label"); + + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.IsNotNull((InterestLabelAddResponse)contentResult.Value); + + _orchestrationServiceMock.Verify(service => + service.AddInterestLabelToDetectionAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.GetAllInterestLabelsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.GetAllInterestLabelsAsync.cs new file mode 100644 index 00000000..e827b6dd --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.GetAllInterestLabelsAsync.cs @@ -0,0 +1,35 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class InterestLabelsControllerTests + { + [TestMethod] + public async Task Default_GetAllInterestLabelsAsync_Expect_TagRemovalResponse() + { + InterestLabelListResponse response = new() + { + InterestLabels = new List() { "Label1", "Label2" }, + Count = 2 + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveAllInterestLabelsAsync()) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetAllInterestLabelsAsync(); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + var result = (InterestLabelListResponse)contentResult.Value; + + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Count); + + _orchestrationServiceMock.Verify(service => + service.RetrieveAllInterestLabelsAsync(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.RemoveInterestLabelFromDetectionAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.RemoveInterestLabelFromDetectionAsync.cs new file mode 100644 index 00000000..e16006d6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Default.RemoveInterestLabelFromDetectionAsync.cs @@ -0,0 +1,37 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class InterestLabelsControllerTests + { + [TestMethod] + public async Task Default_RemoveInterestLabelFromDetectionAsync_Expect_TagRemovalResponse() + { + var labelToRemove = "labelToRemove"; + + InterestLabelRemovalResponse response = new() + { + Id = "id", + LabelRemoved = labelToRemove + }; + + _orchestrationServiceMock.Setup(service => + service.RemoveInterestLabelFromDetectionAsync(It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.RemoveInterestLabelFromDetectionAsync("id"); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + var result = (InterestLabelRemovalResponse)contentResult.Value; + + Assert.IsNotNull(result); + Assert.AreEqual(labelToRemove, result.LabelRemoved); + + _orchestrationServiceMock.Verify(service => + service.RemoveInterestLabelFromDetectionAsync(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Setup.cs new file mode 100644 index 00000000..c6fafed7 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/Setup.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + [TestClass] + [ExcludeFromCodeCoverage] + public partial class InterestLabelsControllerTests + { + private readonly Mock _orchestrationServiceMock; + private readonly InterestLabelsController _controller; + + public InterestLabelsControllerTests() + { + _orchestrationServiceMock = new Mock(); + + _controller = new InterestLabelsController( + interestLabelOrchestrationService: _orchestrationServiceMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _orchestrationServiceMock.VerifyNoOtherCalls(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.AddInterestLabelToDetectionAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.AddInterestLabelToDetectionAsync.cs new file mode 100644 index 00000000..9fa6bacd --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.AddInterestLabelToDetectionAsync.cs @@ -0,0 +1,47 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class InterestLabelsControllerTests + { + [TestMethod] + public async Task TryCatch_AddInterestLabelToDetectionAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.AddInterestLabelToDetectionAsync(It.IsAny(), It.IsAny())) + + .Throws(new InterestLabelOrchestrationValidationException(new NotFoundMetadataException("id"))) + + .Throws(new InterestLabelOrchestrationValidationException(new Exception())) + .Throws(new InterestLabelOrchestrationDependencyValidationException(new Exception())) + + .Throws(new InterestLabelOrchestrationDependencyException(new Exception())) + .Throws(new InterestLabelOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + + await ExecuteAddLabel(1, StatusCodes.Status404NotFound); + await ExecuteAddLabel(2, StatusCodes.Status400BadRequest); + await ExecuteAddLabel(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .AddInterestLabelToDetectionAsync(It.IsAny(), It.IsAny()), + Times.Exactly(6)); + + } + + private async Task ExecuteAddLabel(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.AddInterestLabelToDetectionAsync("id", "label"); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.GetAllInterestLabelsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.GetAllInterestLabelsAsync.cs new file mode 100644 index 00000000..b9728f78 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.GetAllInterestLabelsAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class InterestLabelsControllerTests + { + [TestMethod] + public async Task TryCatch_GetAllInterestLabelsAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveAllInterestLabelsAsync()) + + .Throws(new InterestLabelOrchestrationValidationException(new Exception())) + .Throws(new InterestLabelOrchestrationDependencyValidationException(new Exception())) + + .Throws(new InterestLabelOrchestrationDependencyException(new Exception())) + .Throws(new InterestLabelOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveAllLabels(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveAllLabels(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveAllInterestLabelsAsync(), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveAllLabels(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetAllInterestLabelsAsync(); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.RemoveInterestLabelFromDetectionAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.RemoveInterestLabelFromDetectionAsync.cs new file mode 100644 index 00000000..92fe1e59 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/InterestLabelsControllerTests/TryCatch.RemoveInterestLabelFromDetectionAsync.cs @@ -0,0 +1,48 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class InterestLabelsControllerTests + { + [TestMethod] + public async Task TryCatch_RemoveInterestLabelFromDetectionAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RemoveInterestLabelFromDetectionAsync(It.IsAny())) + + .Throws(new InterestLabelOrchestrationValidationException(new NotFoundMetadataException("id"))) + + .Throws(new InterestLabelOrchestrationValidationException(new DetectionNotDeletedException("id"))) + .Throws(new InterestLabelOrchestrationValidationException(new DetectionNotInsertedException("id"))) + + .Throws(new InterestLabelOrchestrationValidationException(new Exception())) + .Throws(new InterestLabelOrchestrationDependencyValidationException(new Exception())) + + .Throws(new InterestLabelOrchestrationDependencyException(new Exception())) + .Throws(new InterestLabelOrchestrationServiceException(new Exception())) + .Throws(new Exception()); + + await ExecuteRemoveLabel(1, StatusCodes.Status404NotFound); + await ExecuteRemoveLabel(2, StatusCodes.Status422UnprocessableEntity); + await ExecuteRemoveLabel(2, StatusCodes.Status400BadRequest); + await ExecuteRemoveLabel(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RemoveInterestLabelFromDetectionAsync(It.IsAny()), + Times.Exactly(8)); + + } + + private async Task ExecuteRemoveLabel(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.RemoveInterestLabelFromDetectionAsync("id"); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/Default.GetMetricsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/Default.GetMetricsAsync.cs new file mode 100644 index 00000000..4844fedd --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/Default.GetMetricsAsync.cs @@ -0,0 +1,37 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class MetricsControllerTests + { + [TestMethod] + public async Task Default_GetMetricsAsync_Expect_DetectionListResponse() + { + MetricsResponse response = new MetricsResponse + { + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Positive = 1, + Negative = 3, + Unknown = 5, + Unreviewed = 10 + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveMetricsForGivenTimeframeAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetMetricsAsync(DateTime.Now, DateTime.Now.AddDays(1)); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Positive, + ((MetricsResponse)contentResult.Value).Positive); + + _orchestrationServiceMock.Verify(service => + service.RetrieveMetricsForGivenTimeframeAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/Setup.cs new file mode 100644 index 00000000..ed2215ca --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/Setup.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + [TestClass] + [ExcludeFromCodeCoverage] + public partial class MetricsControllerTests + { + private readonly Mock _orchestrationServiceMock; + private readonly MetricsController _controller; + + public MetricsControllerTests() + { + _orchestrationServiceMock = new Mock(); + + _controller = new MetricsController( + metricsOrchestrationService: _orchestrationServiceMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _orchestrationServiceMock.VerifyNoOtherCalls(); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/TryCatch.GetMetricsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/TryCatch.GetMetricsAsync.cs new file mode 100644 index 00000000..cade9a14 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/MetricsControllerTests/TryCatch.GetMetricsAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class MetricsControllerTests + { + [TestMethod] + public async Task TryCatch_GetMetricsAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveMetricsForGivenTimeframeAsync(It.IsAny(), It.IsAny())) + + .Throws(new MetricOrchestrationValidationException(new Exception())) + .Throws(new MetricOrchestrationDependencyValidationException(new Exception())) + + .Throws(new MetricOrchestrationDependencyException(new Exception())) + .Throws(new MetricOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveMetrics(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveMetrics(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveMetricsForGivenTimeframeAsync(It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveMetrics(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetMetricsAsync(DateTime.Now, DateTime.Now.AddDays(1)); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetMetricsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetMetricsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..de4087d6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetMetricsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,37 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task Default_GetMetricsAsync_Expect_MetricsForModeratorResponse() + { + MetricsForModeratorResponse response = new MetricsForModeratorResponse + { + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Positive = 1, + Negative = 3, + Unknown = 5, + Moderator = "Moderator" + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveMetricsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetMetricsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1)); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Positive, + ((MetricsForModeratorResponse)contentResult.Value).Positive); + + _orchestrationServiceMock.Verify(service => + service.RetrieveMetricsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetModeratorsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetModeratorsAsync.cs new file mode 100644 index 00000000..97503442 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetModeratorsAsync.cs @@ -0,0 +1,33 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task Default_GetModeratorsAsync_Expect_DetectionListResponse() + { + ModeratorListResponse response = new ModeratorListResponse + { + Moderators = new List { "Moderator 1", "Moderator 2" }, + Count = 2 + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveModeratorsAsync()) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetModeratorsAsync(); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Moderators.Count(), + ((ModeratorListResponse)contentResult.Value).Moderators.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrieveModeratorsAsync(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..d0c94043 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,36 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task Default_GetGetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync_Expect_DetectionListResponse() + { + CommentListForModeratorResponse response = new CommentListForModeratorResponse + { + Count = 2, + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Comments = new List { new Comment() }, + Moderator = "Moderator" + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Comments.Count(), + ((CommentListForModeratorResponse)contentResult.Value).Comments.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..81ace854 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,36 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task Default_GetGetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync_Expect_DetectionListResponse() + { + CommentListForModeratorResponse response = new CommentListForModeratorResponse + { + Count = 2, + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Comments = new List { new Comment() }, + Moderator = "Moderator" + }; + + _orchestrationServiceMock.Setup(service => + service.RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Comments.Count(), + ((CommentListForModeratorResponse)contentResult.Value).Comments.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetTagsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetTagsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..6a655116 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Default.GetTagsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,36 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task Default_GetTagsForGivenTimePeriodAndModerator_Expect_TagListResponse() + { + TagListForModeratorResponse response = new TagListForModeratorResponse + { + Count = 2, + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Tags = new List { "Tag1", "Tag2" }, + Moderator = "Moderator" + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveTagsForGivenTimePeriodAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetTagsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1)); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Tags.Count(), + ((TagListResponse)contentResult.Value).Tags.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrieveTagsForGivenTimePeriodAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Setup.cs new file mode 100644 index 00000000..132e6064 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/Setup.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + [TestClass] + [ExcludeFromCodeCoverage] + public partial class ModeratorsControllerTests + { + private readonly Mock _orchestrationServiceMock; + private readonly ModeratorsController _controller; + + public ModeratorsControllerTests() + { + _orchestrationServiceMock = new Mock(); + + _controller = new ModeratorsController( + moderatorOrchestrationService: _orchestrationServiceMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _orchestrationServiceMock.VerifyNoOtherCalls(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetMetricsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetMetricsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..0f199e93 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetMetricsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,43 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task TryCatch_GetMetricsForGivenTimeframeAndModeratorAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveMetricsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new ModeratorOrchestrationValidationException(new Exception())) + .Throws(new ModeratorOrchestrationDependencyValidationException(new Exception())) + + .Throws(new ModeratorOrchestrationDependencyException(new Exception())) + .Throws(new ModeratorOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveMetrics(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveMetrics(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveMetricsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveMetrics(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetMetricsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1)); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetModeratorsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetModeratorsAsync.cs new file mode 100644 index 00000000..c7f3b0c5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetModeratorsAsync.cs @@ -0,0 +1,43 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task TryCatch_GetModeratorsAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveModeratorsAsync()) + + .Throws(new ModeratorOrchestrationValidationException(new Exception())) + .Throws(new ModeratorOrchestrationDependencyValidationException(new Exception())) + + .Throws(new ModeratorOrchestrationDependencyException(new Exception())) + .Throws(new ModeratorOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveModerators(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveModerators(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveModeratorsAsync(), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveModerators(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetModeratorsAsync(); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..e2d1e507 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,79 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task TryCatch_CommentListForModeratorResponse_Positive_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new ModeratorOrchestrationValidationException(new Exception())) + .Throws(new ModeratorOrchestrationDependencyValidationException(new Exception())) + + .Throws(new ModeratorOrchestrationDependencyException(new Exception())) + .Throws(new ModeratorOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrievePositiveComments(2, StatusCodes.Status400BadRequest); + await ExecuteRetrievePositiveComments(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrievePositiveComments(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + + [TestMethod] + public async Task TryCatch_GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new ModeratorOrchestrationValidationException(new Exception())) + .Throws(new ModeratorOrchestrationDependencyValidationException(new Exception())) + + .Throws(new ModeratorOrchestrationDependencyException(new Exception())) + .Throws(new ModeratorOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveNegativeAndUnknownComments(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveNegativeAndUnknownComments(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveNegativeAndUnknownComments(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..ce202440 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task TryCatch_GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new ModeratorOrchestrationValidationException(new Exception())) + .Throws(new ModeratorOrchestrationDependencyValidationException(new Exception())) + + .Throws(new ModeratorOrchestrationDependencyException(new Exception())) + .Throws(new ModeratorOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrievePositiveCommentForModerator(2, StatusCodes.Status400BadRequest); + await ExecuteRetrievePositiveCommentForModerator(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrievePositiveCommentForModerator(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetTagsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetTagsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..bdcbf033 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/ModeratorsControllerTests/TryCatch.GetTagsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class ModeratorsControllerTests + { + [TestMethod] + public async Task TryCatch_GetTagsForGivenTimeframeAndModeratorAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveTagsForGivenTimePeriodAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny())) + + .Throws(new ModeratorOrchestrationValidationException(new Exception())) + .Throws(new ModeratorOrchestrationDependencyValidationException(new Exception())) + + .Throws(new ModeratorOrchestrationDependencyException(new Exception())) + .Throws(new ModeratorOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveTags(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveTags(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveTagsForGivenTimePeriodAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveTags(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetTagsForGivenTimeframeAndModeratorAsync("Moderator", DateTime.Now, DateTime.Now.AddDays(1)); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.DeleteTagFromAllDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.DeleteTagFromAllDetectionsAsync.cs new file mode 100644 index 00000000..e8210db3 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.DeleteTagFromAllDetectionsAsync.cs @@ -0,0 +1,40 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class TagsControllerTests + { + [TestMethod] + public async Task Default_DeleteTagFromAllDetectionsAsync_Expect_TagRemovalResponse() + { + var tagToRemove = "tagToRemove"; + + TagRemovalResponse response = new() + { + Tag = tagToRemove, + TotalMatching = 2, + TotalRemoved = 2 + }; + + _orchestrationServiceMock.Setup(service => + service.RemoveTagFromAllDetectionsAsync(It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.DeleteTagFromAllDetectionsAsync(tagToRemove); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + var result = (TagRemovalResponse)contentResult.Value; + + Assert.IsNotNull(result); + Assert.AreEqual(tagToRemove, result.Tag); + Assert.AreEqual(2, result.TotalMatching); + Assert.AreEqual(2, result.TotalRemoved); + + _orchestrationServiceMock.Verify(service => + service.RemoveTagFromAllDetectionsAsync(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.GetAllTagsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.GetAllTagsAsync.cs new file mode 100644 index 00000000..e271fa71 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.GetAllTagsAsync.cs @@ -0,0 +1,35 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class TagsControllerTests + { + [TestMethod] + public async Task Default_GetAllTagsAsync_Expect_TagRemovalResponse() + { + TagListResponse response = new() + { + Tags = new List() { "Tag1", "Tag2" }, + Count = 2 + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveAllTagsAsync()) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetAllTagsAsync(); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + var result = (TagListResponse)contentResult.Value; + + Assert.IsNotNull(result); + Assert.AreEqual(2, result.Count); + + _orchestrationServiceMock.Verify(service => + service.RetrieveAllTagsAsync(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.GetTagsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.GetTagsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..0d698dcb --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.GetTagsForGivenTimeframeAsync.cs @@ -0,0 +1,35 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class TagsControllerTests + { + [TestMethod] + public async Task Default_GetTagsForGivenTimePeriod_Expect_TagListResponse() + { + TagListForTimeframeResponse response = new() + { + Count = 2, + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + Tags = new List { "Tag1", "Tag2" } + }; + + _orchestrationServiceMock.Setup(service => + service.RetrieveTagsForGivenTimePeriodAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.GetTagsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1)); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + Assert.AreEqual(response.Tags.Count(), + ((TagListResponse)contentResult.Value).Tags.Count()); + + _orchestrationServiceMock.Verify(service => + service.RetrieveTagsForGivenTimePeriodAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.ReplaceTagInAllDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.ReplaceTagInAllDetectionsAsync.cs new file mode 100644 index 00000000..133130c9 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Default.ReplaceTagInAllDetectionsAsync.cs @@ -0,0 +1,43 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class TagsControllerTests + { + [TestMethod] + public async Task Default_ReplaceTagInAllDetectionsAsync_Expect_TagReplaceResponse() + { + var oldTag = "oldTag"; + var newTag = "newTag"; + + TagReplaceResponse response = new() + { + OldTag = oldTag, + NewTag = newTag, + TotalMatching = 2, + TotalReplaced = 2 + }; + + _orchestrationServiceMock.Setup(service => + service.ReplaceTagInAllDetectionsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + ActionResult actionResult = + await _controller.ReplaceTagInAllDetectionsAsync(oldTag, newTag); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(200, contentResult.StatusCode); + + var result = (TagReplaceResponse)contentResult.Value; + + Assert.IsNotNull(result); + Assert.AreEqual(oldTag, result.OldTag); + Assert.AreEqual(newTag, result.NewTag); + Assert.AreEqual(2, result.TotalMatching); + Assert.AreEqual(2, result.TotalReplaced); + + _orchestrationServiceMock.Verify(service => + service.ReplaceTagInAllDetectionsAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Setup.cs new file mode 100644 index 00000000..56731c2d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/Setup.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + [TestClass] + [ExcludeFromCodeCoverage] + public partial class TagsControllerTests + { + private readonly Mock _orchestrationServiceMock; + private readonly TagsController _controller; + + public TagsControllerTests() + { + _orchestrationServiceMock = new Mock(); + + _controller = new TagsController( + tagOrchestrationService: _orchestrationServiceMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _orchestrationServiceMock.VerifyNoOtherCalls(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.DeleteTagFromAllDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.DeleteTagFromAllDetectionsAsync.cs new file mode 100644 index 00000000..caae4609 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.DeleteTagFromAllDetectionsAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class TagsControllerTests + { + [TestMethod] + public async Task TryCatch_DeleteTagFromAllDetectionsAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RemoveTagFromAllDetectionsAsync(It.IsAny())) + + .Throws(new TagOrchestrationValidationException(new Exception())) + .Throws(new TagOrchestrationDependencyValidationException(new Exception())) + + .Throws(new TagOrchestrationDependencyException(new Exception())) + .Throws(new TagOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRemoveTag(2, StatusCodes.Status400BadRequest); + await ExecuteRemoveTag(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RemoveTagFromAllDetectionsAsync(It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRemoveTag(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.DeleteTagFromAllDetectionsAsync("tag"); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.GetAllTagsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.GetAllTagsAsync.cs new file mode 100644 index 00000000..28cfe746 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.GetAllTagsAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class TagsControllerTests + { + [TestMethod] + public async Task TryCatch_GetAllTagsAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveAllTagsAsync()) + + .Throws(new TagOrchestrationValidationException(new Exception())) + .Throws(new TagOrchestrationDependencyValidationException(new Exception())) + + .Throws(new TagOrchestrationDependencyException(new Exception())) + .Throws(new TagOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveAllTags(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveAllTags(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveAllTagsAsync(), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveAllTags(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetAllTagsAsync(); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.GetTagsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.GetTagsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..29993395 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.GetTagsForGivenTimeframeAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class TagsControllerTests + { + [TestMethod] + public async Task TryCatch_GetTagsForGivenTimeframeAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.RetrieveTagsForGivenTimePeriodAsync(It.IsAny(), It.IsAny())) + + .Throws(new TagOrchestrationValidationException(new Exception())) + .Throws(new TagOrchestrationDependencyValidationException(new Exception())) + + .Throws(new TagOrchestrationDependencyException(new Exception())) + .Throws(new TagOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteRetrieveTags(2, StatusCodes.Status400BadRequest); + await ExecuteRetrieveTags(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .RetrieveTagsForGivenTimePeriodAsync(It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteRetrieveTags(int count, int statusCode) + { + for(int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.GetTagsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1)); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.ReplaceTagInAllDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.ReplaceTagInAllDetectionsAsync.cs new file mode 100644 index 00000000..da2ccd50 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Controllers/TagsControllerTests/TryCatch.ReplaceTagInAllDetectionsAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Controllers +{ + public partial class TagsControllerTests + { + [TestMethod] + public async Task TryCatch_ReplaceTagInAllDetectionsAsync_Expect_Exception() + { + _orchestrationServiceMock + .SetupSequence(p => p.ReplaceTagInAllDetectionsAsync(It.IsAny(), It.IsAny())) + + .Throws(new TagOrchestrationValidationException(new Exception())) + .Throws(new TagOrchestrationDependencyValidationException(new Exception())) + + .Throws(new TagOrchestrationDependencyException(new Exception())) + .Throws(new TagOrchestrationServiceException(new Exception())) + + .Throws(new Exception()); + + await ExecuteReplaceTag(2, StatusCodes.Status400BadRequest); + await ExecuteReplaceTag(3, StatusCodes.Status500InternalServerError); + + _orchestrationServiceMock + .Verify(service => service + .ReplaceTagInAllDetectionsAsync(It.IsAny(), It.IsAny()), + Times.Exactly(5)); + + } + + private async Task ExecuteReplaceTag(int count, int statusCode) + { + for (int x = 0; x < count; x++) + { + ActionResult actionResult = + await _controller.ReplaceTagInAllDetectionsAsync("oldTag", "newTag"); + + var contentResult = actionResult.Result as ObjectResult; + Assert.IsNotNull(contentResult); + Assert.AreEqual(statusCode, contentResult.StatusCode); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/GlobalUsings.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/GlobalUsings.cs new file mode 100644 index 00000000..e5aaac9d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/GlobalUsings.cs @@ -0,0 +1,20 @@ +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Azure.Cosmos; +global using Microsoft.Extensions.Logging; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; +global using OrcaHello.Web.Api.Brokers.Hydrophones; +global using OrcaHello.Web.Api.Controllers; +global using OrcaHello.Web.Api.Models; +global using OrcaHello.Web.Api.Services; +global using OrcaHello.Web.Api.Tests.Unit.Services.Metadatas; +global using OrcaHello.Web.Shared.Models.Comments; +global using OrcaHello.Web.Shared.Models.Detections; +global using OrcaHello.Web.Shared.Models.Hydrophones; +global using OrcaHello.Web.Shared.Models.InterestLabels; +global using OrcaHello.Web.Shared.Models.Metrics; +global using OrcaHello.Web.Shared.Models.Moderators; +global using OrcaHello.Web.Shared.Models.Tags; +global using System.Diagnostics.CodeAnalysis; +global using System.Net; \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/OrcaHello.Web.Api.Tests.Unit.csproj b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/OrcaHello.Web.Api.Tests.Unit.csproj new file mode 100644 index 00000000..eb3d2b3b --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/OrcaHello.Web.Api.Tests.Unit.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + + + + + + diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/CommentOrchestrationServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/CommentOrchestrationServiceWrapper.cs new file mode 100644 index 00000000..b0e5a977 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/CommentOrchestrationServiceWrapper.cs @@ -0,0 +1,22 @@ +using OrcaHello.Web.Api.Services; +using OrcaHello.Web.Shared.Models.Comments; +using System.Diagnostics.CodeAnalysis; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + public class CommentOrchestrationServiceWrapper : CommentOrchestrationService + { + public new void Validate(DateTime? date, string propertyName) => + base.Validate(date, propertyName); + + public new void ValidatePage(int page) => + base.ValidatePage(page); + + public new void ValidatePageSize(int pageSize) => + base.ValidatePageSize(pageSize); + + public new ValueTask TryCatch(ReturningCommentListResponseFunction returningCommentListResponseFunction) => + base.TryCatch(returningCommentListResponseFunction); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Default.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Default.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..f87ab8ed --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Default.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync.cs @@ -0,0 +1,38 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Comments; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class CommentOrchestrationTests + { + [TestMethod] + public async Task Default_RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync_Expect() + { + var nullModeratedMetadata = CreateRandomMetadata(); + nullModeratedMetadata.DateModerated = null; + + var expectedResults = new QueryableMetadataForTimeframe + { + QueryableRecords = (new List { CreateRandomMetadata(), nullModeratedMetadata }).AsQueryable(), + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + TotalCount = 1, + Page = 1, + PageSize = 10 + }; + + _metadataServiceMock.Setup(service => + service.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + CommentListResponse result = await _orchestrationService.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Comments.Count()); + + _metadataServiceMock.Verify(service => + service.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Default.RetrievePositiveCommentsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Default.RetrievePositiveCommentsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..611fc0d6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Default.RetrievePositiveCommentsForGivenTimeframeAsync.cs @@ -0,0 +1,38 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Comments; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class CommentOrchestrationTests + { + [TestMethod] + public async Task Default_RetrievePositiveCommentsForGivenTimeframeAsync_Expect() + { + var nullModeratedMetadata = CreateRandomMetadata(); + nullModeratedMetadata.DateModerated = null; + + var expectedResults = new QueryableMetadataForTimeframe + { + QueryableRecords = (new List { CreateRandomMetadata(), nullModeratedMetadata }).AsQueryable(), + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + TotalCount = 1, + Page = 1, + PageSize = 10 + }; + + _metadataServiceMock.Setup(service => + service.RetrievePositiveMetadataForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + CommentListResponse result = await _orchestrationService.RetrievePositiveCommentsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1), 1, 10); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Comments.Count()); + + _metadataServiceMock.Verify(service => + service.RetrievePositiveMetadataForGivenTimeframeAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Guards.cs new file mode 100644 index 00000000..5fca2dc7 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Guards.cs @@ -0,0 +1,30 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class CommentOrchestrationTests + { + [TestMethod] + public void Guard_AllGuardConditions_Expect_Exception() + { + var wrapper = new CommentOrchestrationServiceWrapper(); + + DateTime? invalidDate = DateTime.MinValue; + + Assert.ThrowsException(() => + wrapper.Validate(invalidDate, nameof(invalidDate))); + + DateTime? nullDate = null; + + Assert.ThrowsException(() => + wrapper.Validate(nullDate, nameof(nullDate))); + + int invalidPage = 0; + + Assert.ThrowsException(() => + wrapper.ValidatePage(invalidPage)); + + int invalidPageSize = 0; + Assert.ThrowsException(() => + wrapper.ValidatePageSize(invalidPageSize)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Setup.cs new file mode 100644 index 00000000..613a44e0 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/Setup.cs @@ -0,0 +1,61 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class CommentOrchestrationTests + { + private readonly Mock _metadataServiceMock; + private readonly Mock> _loggerMock; + + private readonly ICommentOrchestrationService _orchestrationService; + + public CommentOrchestrationTests() + { + _metadataServiceMock = new Mock(); + _loggerMock = new Mock>(); + + _orchestrationService = new CommentOrchestrationService( + metadataService: _metadataServiceMock.Object, + logger: _loggerMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _metadataServiceMock.VerifyNoOtherCalls(); + } + + public Metadata CreateRandomMetadata() + { + return new Metadata + { + Id = Guid.NewGuid().ToString(), + State = "Unreviewed", + LocationName = "location", + AudioUri = "https://url", + ImageUri = "https://url", + Timestamp = DateTime.Now, + WhaleFoundConfidence = 0.66M, + Location = new Models.Location + { + Name = "location", + Id = "location_guid", + Latitude = 1.00, + Longitude = 1.00 + }, + Predictions = new List + { + new Prediction + { + Id = 1, + StartTime = 5.00M, + Duration = 1.0M, + Confidence = 0.66M + } + }, + DateModerated = DateTime.Now.ToString() + }; + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/TryCatch.CommentListResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/TryCatch.CommentListResponse.cs new file mode 100644 index 00000000..d729675d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/CommentOrchestrationTests/TryCatch.CommentListResponse.cs @@ -0,0 +1,43 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using static OrcaHello.Web.Api.Services.CommentOrchestrationService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class CommentOrchestrationTests + { + [TestMethod] + public void TryCatch_CommentListResponse_Expect_Exception() + { + var wrapper = new CommentOrchestrationServiceWrapper(); + var delegateMock = new Mock(); + + delegateMock + .SetupSequence(p => p()) + + .Throws(new InvalidCommentOrchestrationException()) + + .Throws(new MetadataValidationException()) + .Throws(new MetadataDependencyValidationException()) + + .Throws(new MetadataDependencyException()) + .Throws(new MetadataServiceException()) + + .Throws(new Exception()); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.ModerateDetectionByIdAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.ModerateDetectionByIdAsync.cs new file mode 100644 index 00000000..9bba8ee5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.ModerateDetectionByIdAsync.cs @@ -0,0 +1,102 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class DetectionOrchestrationServiceTests + { + + [TestMethod] + public async Task Default_ModerateDetectionById_Expect() + { + var storedMetadata = CreateRandomMetadata(); + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataByIdAsync(It.IsAny())) + .ReturnsAsync(storedMetadata); + + _metadataServiceMock.Setup(service => + service.RemoveMetadataByIdAndStateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + _metadataServiceMock.Setup(service => + service.AddMetadataAsync(It.IsAny())) + .ReturnsAsync(true); + + ModerateDetectionRequest request = new() + { + Id = Guid.NewGuid().ToString(), + State = DetectionState.Positive.ToString(), + Moderator = "Ira M. Goober", + DateModerated = DateTime.UtcNow, + Comments = "Comments", + Tags = new List() { "Tag1", "Tag2" } + }; + + Detection result = await _orchestrationService.ModerateDetectionByIdAsync(request.Id, request); + + Assert.AreEqual(storedMetadata.LocationName, result.LocationName); + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataByIdAsync(It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.RemoveMetadataByIdAndStateAsync(It.IsAny(), It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.AddMetadataAsync(It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Error_ModerateDetectionById_ExpectException() + { + var storedMetadata = CreateRandomMetadata(); + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataByIdAsync(It.IsAny())) + .ReturnsAsync(storedMetadata); + + _metadataServiceMock.Setup(service => + service.RemoveMetadataByIdAndStateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + _metadataServiceMock.SetupSequence(service => + service.AddMetadataAsync(It.IsAny())) + .ReturnsAsync(false) + .ReturnsAsync(true); + + ModerateDetectionRequest request = new() + { + Id = Guid.NewGuid().ToString(), + State = DetectionState.Positive.ToString(), + Moderator = "Ira M. Goober", + DateModerated = DateTime.UtcNow, + Comments = "Comments", + Tags = new List() { "Tag1", "Tag2" } + }; + + try + { + + Detection result = await _orchestrationService.ModerateDetectionByIdAsync(request.Id, request); + } + catch(Exception ex) + { + Assert.IsTrue(ex is DetectionOrchestrationValidationException && + ex.InnerException is DetectionNotInsertedException); + } + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataByIdAsync(It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.RemoveMetadataByIdAndStateAsync(It.IsAny(), It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.AddMetadataAsync(It.IsAny()), + Times.Exactly(2)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionByIdAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionByIdAsync.cs new file mode 100644 index 00000000..e69343ed --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionByIdAsync.cs @@ -0,0 +1,23 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class DetectionOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveDetectionByIdAsync_Expect() + { + var expectedMetadata = CreateRandomMetadata(); + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataByIdAsync(It.IsAny())) + .ReturnsAsync(expectedMetadata); + + Detection result = await _orchestrationService.RetrieveDetectionByIdAsync(Guid.NewGuid().ToString()); + + Assert.AreEqual(expectedMetadata.LocationName, result.LocationName); + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataByIdAsync(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionsForGivenInterestLabelAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionsForGivenInterestLabelAsync.cs new file mode 100644 index 00000000..56043db5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionsForGivenInterestLabelAsync.cs @@ -0,0 +1,30 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class DetectionOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveDetectionsForGivenInterestLabelAsync_Expect() + { + var nullModeratedMetadata = CreateRandomMetadata(); + nullModeratedMetadata.DateModerated = null; + + var expectedResults = new QueryableMetadata + { + QueryableRecords = (new List { CreateRandomMetadata(), nullModeratedMetadata }).AsQueryable(), + TotalCount = 2, + }; + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataForInterestLabelAsync(It.IsAny())) + .ReturnsAsync(expectedResults); + + DetectionListForInterestLabelResponse result = await _orchestrationService.RetrieveDetectionsForGivenInterestLabelAsync("label"); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Detections.Count()); + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataForInterestLabelAsync(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionsForGivenTimeframeAndTagAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionsForGivenTimeframeAndTagAsync.cs new file mode 100644 index 00000000..2242e260 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveDetectionsForGivenTimeframeAndTagAsync.cs @@ -0,0 +1,35 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class DetectionOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveDetectionsForGivenTimeframeAndTagAsync_Expect() + { + var nullModeratedMetadata = CreateRandomMetadata(); + nullModeratedMetadata.DateModerated = null!; + + var expectedResults = new QueryableMetadataForTimeframeAndTag + { + QueryableRecords = (new List { CreateRandomMetadata(), nullModeratedMetadata }).AsQueryable(), + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + TotalCount = 1, + Tag = "tag", + Page = 1, + PageSize = 10 + }; + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataForGivenTimeframeAndTagAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + DetectionListForTagResponse result = await _orchestrationService.RetrieveDetectionsForGivenTimeframeAndTagAsync(DateTime.Now, DateTime.Now.AddDays(1), "tag", 1, 10); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Detections.Count); + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataForGivenTimeframeAndTagAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveFilteredDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveFilteredDetectionsAsync.cs new file mode 100644 index 00000000..2fab980f --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Default.RetrieveFilteredDetectionsAsync.cs @@ -0,0 +1,45 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Detections; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class DetectionOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveFilteredDetectionsAsync_Expect() + { + var nullModeratedMetadata = CreateRandomMetadata(); + nullModeratedMetadata.DateModerated = null; + + var expectedResults = new QueryableMetadataFiltered + { + QueryableRecords = (new List { CreateRandomMetadata(), nullModeratedMetadata }).AsQueryable(), + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + TotalCount = 1, + Page = 1, + PageSize = 10, + State = "Positive", + Location = "Haro Straight", + SortBy = "timestamp", + SortOrder = "DESC" + }; + + _metadataServiceMock.Setup(service => + service.RetrievePaginatedMetadataAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + DetectionListResponse result = await _orchestrationService. + RetrieveFilteredDetectionsAsync(DateTime.Now, DateTime.Now.AddDays(1), "Positive", "timestamp",true, null, 1, 10); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Detections.Count()); + + _metadataServiceMock.Verify(service => + service.RetrievePaginatedMetadataAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/DetectionOrchestationServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/DetectionOrchestationServiceWrapper.cs new file mode 100644 index 00000000..dfd64a28 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/DetectionOrchestationServiceWrapper.cs @@ -0,0 +1,36 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + public class DetectionOrchestrationServiceWrapper : DetectionOrchestrationService + { + public new void Validate(DateTime? date, string propertyName) => + base.Validate(date, propertyName); + + public new void Validate(string propertyValue, string propertyName) => + base.Validate(propertyValue, propertyName); + + public new void ValidatePage(int page) => + base.ValidatePage(page); + + public new void ValidatePageSize(int pageSize) => + base.ValidatePageSize(pageSize); + + public new void ValidateStorageMetadata(Metadata storageMetadata, string id) => + base.ValidateStorageMetadata(storageMetadata, id); + + public new void ValidateStateIsAcceptable(string state) => + base.ValidateStateIsAcceptable(state); + + public new void ValidateDeleted(bool deleted, string id) => + base.ValidateDeleted(deleted, id); + + public new void ValidateInserted(bool inserted, string id) => + base.ValidateInserted(inserted, id); + + public new void ValidateModerateRequestOnUpdate(ModerateDetectionRequest request) => + base.ValidateModerateRequestOnUpdate(request); + + public new ValueTask TryCatch(ReturningGenericFunction returningValueTaskFunction) => + base.TryCatch(returningValueTaskFunction); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Guards.cs new file mode 100644 index 00000000..c30278e5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Guards.cs @@ -0,0 +1,74 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class DetectionOrchestrationServiceTests + { + [TestMethod] + public void Guard_AllGuardConditions_Expect_Exception() + { + var wrapper = new DetectionOrchestrationServiceWrapper(); + + DateTime? invalidDate = DateTime.MinValue; + + Assert.ThrowsException(() => + wrapper.Validate(invalidDate, nameof(invalidDate))); + + DateTime? nullDate = null; + + Assert.ThrowsException(() => + wrapper.Validate(nullDate, nameof(nullDate))); + + string invalidProperty = string.Empty; + + Assert.ThrowsException(() => + wrapper.Validate(invalidProperty, nameof(invalidProperty))); + + int invalidPage = 0; + + Assert.ThrowsException(() => + wrapper.ValidatePage(invalidPage)); + + int invalidPageSize = 0; + Assert.ThrowsException(() => + wrapper.ValidatePageSize(invalidPageSize)); + + Metadata nullMetadata = null; + + Assert.ThrowsException(() => + wrapper.ValidateStorageMetadata(nullMetadata, Guid.NewGuid().ToString())); + + string invalidState = "Goober"; + + Assert.ThrowsException(() => + wrapper.ValidateStateIsAcceptable(invalidState)); + + bool notDeleted = false; + + Assert.ThrowsException(() => + wrapper.ValidateDeleted(notDeleted, "id")); + + bool notInserted = false; + + Assert.ThrowsException(() => + wrapper.ValidateInserted(notInserted, "id")); + + ModerateDetectionRequest nullRequest = null; + + Assert.ThrowsException(() => + wrapper.ValidateModerateRequestOnUpdate(nullRequest)); + + ModerateDetectionRequest invalidRequest = new() + { + State = string.Empty, + Moderator = string.Empty + }; + + Assert.ThrowsException(() => + wrapper.ValidateModerateRequestOnUpdate(invalidRequest)); + + invalidRequest.State = DetectionState.Positive.ToString(); + + Assert.ThrowsException(() => + wrapper.ValidateModerateRequestOnUpdate(invalidRequest)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Setup.cs new file mode 100644 index 00000000..5acb06dc --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/Setup.cs @@ -0,0 +1,62 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class DetectionOrchestrationServiceTests + { + private readonly Mock _metadataServiceMock; + private readonly Mock> _loggerMock; + + private readonly IDetectionOrchestrationService _orchestrationService; + + public DetectionOrchestrationServiceTests() + { + _metadataServiceMock = new Mock(); + _loggerMock = new Mock>(); + + _orchestrationService = new DetectionOrchestrationService( + metadataService: _metadataServiceMock.Object, + logger: _loggerMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _metadataServiceMock.VerifyNoOtherCalls(); + } + + public Metadata CreateRandomMetadata() + { + return new Metadata + { + Id = Guid.NewGuid().ToString(), + State = DetectionState.Unreviewed.ToString(), + LocationName = "location", + AudioUri = "https://url", + ImageUri = "https://url", + Timestamp = DateTime.Now, + WhaleFoundConfidence = 0.66M, + Location = new() + { + Name = "location", + Id = "location_guid", + Latitude = 1.00, + Longitude = 1.00 + }, + Predictions = new List + { + new() + { + Id = 1, + StartTime = 5.00M, + Duration = 1.0M, + Confidence = 0.66M + } + }, + DateModerated = DateTime.Now.ToString() + }; + } + } + +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs new file mode 100644 index 00000000..6d2dc6ec --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/DetectionOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs @@ -0,0 +1,43 @@ +using static OrcaHello.Web.Api.Services.DetectionOrchestrationService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class DetectionOrchestrationServiceTests + { + [TestMethod] + public void TryCatch_ReturningGenericFunction_Expect_Exception() + { + var wrapper = new DetectionOrchestrationServiceWrapper(); + var delegateMock = new Mock>(); + + delegateMock + .SetupSequence(p => p()) + + .Throws(new NotFoundMetadataException("id")) + .Throws(new InvalidDetectionOrchestrationException()) + + .Throws(new MetadataValidationException()) + .Throws(new MetadataDependencyValidationException()) + + .Throws(new MetadataDependencyException()) + .Throws(new MetadataServiceException()) + + .Throws(new Exception()); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/Default.RetrieveHydrophoneLocations.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/Default.RetrieveHydrophoneLocations.cs new file mode 100644 index 00000000..0822eef6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/Default.RetrieveHydrophoneLocations.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class HydrophoneOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveHydrophoneLocations_Expect() + { + var expectedResults = new QueryableHydrophoneData + { + QueryableRecords = (new List { new() { Attributes = new() { NodeName = "test_id", Name = "test" } } } ).AsQueryable(), + TotalCount = 1 + }; + + _hydrophoneServiceMock.Setup(service => + service.RetrieveAllHydrophonesAsync()) + .ReturnsAsync(expectedResults); + + HydrophoneListResponse result = await _orchestrationService. + RetrieveHydrophoneLocations(); + + Assert.AreEqual(1, result.Count); + + _hydrophoneServiceMock.Verify(service => + service.RetrieveAllHydrophonesAsync(), + Times.Once); + + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/HydrophoneOrchestrationServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/HydrophoneOrchestrationServiceWrapper.cs new file mode 100644 index 00000000..38650366 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/HydrophoneOrchestrationServiceWrapper.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + public class HydrophoneOrchestrationServiceWrapper : HydrophoneOrchestrationService + { + public new ValueTask TryCatch(ReturningGenericFunction returningValueTaskFunction) => + base.TryCatch(returningValueTaskFunction); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/Setup.cs new file mode 100644 index 00000000..832f69e6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/Setup.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class HydrophoneOrchestrationServiceTests + { + private readonly Mock _hydrophoneServiceMock; + private readonly Mock> _loggerMock; + + private readonly IHydrophoneOrchestrationService _orchestrationService; + + public HydrophoneOrchestrationServiceTests() + { + _loggerMock = new Mock>(); + _hydrophoneServiceMock = new Mock(); + + _orchestrationService = new HydrophoneOrchestrationService( + hydrophoneService: _hydrophoneServiceMock.Object, + logger: _loggerMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _hydrophoneServiceMock.VerifyNoOtherCalls(); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs new file mode 100644 index 00000000..5fd909e0 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs @@ -0,0 +1,41 @@ +using static OrcaHello.Web.Api.Services.HydrophoneOrchestrationService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class HydrophoneOrchestrationServiceTests + { + [TestMethod] + public void TryCatch_HydrophoneListResponse_Expect_Exception() + { + var wrapper = new HydrophoneOrchestrationServiceWrapper(); + var delegateMock = new Mock>(); + + delegateMock + .SetupSequence(p => p()) + + .Throws(new InvalidHydrophoneOrchestrationException()) + + .Throws(new HydrophoneValidationException()) + .Throws(new HydrophoneDependencyValidationException()) + + .Throws(new HydrophoneDependencyException()) + .Throws(new HydrophoneServiceException()) + + .Throws(new Exception()); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + } + } +}; \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/Default.RetrieveAllHydrophonesAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/Default.RetrieveAllHydrophonesAsync.cs new file mode 100644 index 00000000..4e5d1081 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/Default.RetrieveAllHydrophonesAsync.cs @@ -0,0 +1,27 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class HydrophoneServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveAllHydrophonesAsync() + { + var hydrophones = new List + { + new() + }; + + _hydrophoneBrokerMock.Setup(broker => + broker.GetFeedsAsync()) + .ReturnsAsync(hydrophones); + + var result = await _hydrophoneService. + RetrieveAllHydrophonesAsync(); + + Assert.AreEqual(hydrophones.Count, result.QueryableRecords.Count()); + + _hydrophoneBrokerMock.Verify(broker => + broker.GetFeedsAsync(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/HydrophoneServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/HydrophoneServiceWrapper.cs new file mode 100644 index 00000000..2ce5f737 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/HydrophoneServiceWrapper.cs @@ -0,0 +1,8 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public class HydrophoneServiceWrapper : HydrophoneService + { + public new ValueTask TryCatch(ReturningGenericFunction returningValueTaskFunction) => + base.TryCatch(returningValueTaskFunction); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/Setup.cs new file mode 100644 index 00000000..231ecfe1 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/Setup.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class HydrophoneServiceTests + { + private readonly Mock _hydrophoneBrokerMock; + private readonly Mock> _loggerMock; + + private readonly IHydrophoneService _hydrophoneService; + + public HydrophoneServiceTests() + { + _hydrophoneBrokerMock = new Mock(); + _loggerMock = new Mock>(); + + _hydrophoneService = new HydrophoneService( + hydrophoneBroker: _hydrophoneBrokerMock.Object, + logger: _loggerMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _hydrophoneBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/TryCatch.ReturningGenericFunction.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/TryCatch.ReturningGenericFunction.cs new file mode 100644 index 00000000..34e88f44 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/HydrophoneServiceTests/TryCatch.ReturningGenericFunction.cs @@ -0,0 +1,80 @@ +using static OrcaHello.Web.Api.Services.HydrophoneService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class HydrophoneServiceTests + { + [TestMethod] + public void TryCatch_ReturningGenericFunction_Expect_Exception() + { + var wrapper = new HydrophoneServiceWrapper(); + var delegateMock = new Mock>(); + + delegateMock + .SetupSequence(p => p()) + + .Throws(new InvalidHydrophoneException()) + + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.BadRequest)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.NotFound)) + + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.Unauthorized)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.Forbidden)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.MethodNotAllowed)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.Conflict)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.PreconditionFailed)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.RequestEntityTooLarge)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.RequestTimeout)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.ServiceUnavailable)) + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.InternalServerError)) + + .Throws(new HttpRequestException("Message", new Exception(), HttpStatusCode.Ambiguous)) + + .Throws(new Exception()); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 9; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + + } + } +} + + + +//if (exception is HttpRequestException exception1) +//{ +// var statusCode = exception1.StatusCode; +// var innerException = new InvalidHydrophoneException($"Error encountered accessing {_hydrophoneBroker.ApiUrl}: {exception.Message}"); + +// if (statusCode == HttpStatusCode.BadRequest || +// statusCode == HttpStatusCode.NotFound) +// throw LoggingUtilities.CreateAndLogException(_logger, innerException); + +// if (statusCode == HttpStatusCode.Unauthorized || +// statusCode == HttpStatusCode.Forbidden || +// statusCode == HttpStatusCode.MethodNotAllowed || +// statusCode == HttpStatusCode.Conflict || +// statusCode == HttpStatusCode.PreconditionFailed || +// statusCode == HttpStatusCode.RequestEntityTooLarge || +// statusCode == HttpStatusCode.RequestTimeout || +// statusCode == HttpStatusCode.ServiceUnavailable || +// statusCode == HttpStatusCode.InternalServerError) +// throw LoggingUtilities.CreateAndLogException(_logger, innerException); + +// throw LoggingUtilities.CreateAndLogException(_logger, innerException); +//} + +//throw LoggingUtilities.CreateAndLogException(_logger, exception); \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.AddInterestLabelToDetectionAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.AddInterestLabelToDetectionAsync.cs new file mode 100644 index 00000000..64898312 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.AddInterestLabelToDetectionAsync.cs @@ -0,0 +1,33 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class InterestLabelOrchestrationServiceTests + { + [TestMethod] + public async Task Default_AddInterestLabelToDetectionAsync_Expect() + { + Metadata metadata = new(); + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataByIdAsync(It.IsAny())) + .ReturnsAsync(metadata); + + _metadataServiceMock.Setup(service => + service.UpdateMetadataAsync(It.IsAny())) + .ReturnsAsync(true); + + InterestLabelAddResponse result = await _orchestrationService.AddInterestLabelToDetectionAsync("id", "label"); + + Assert.IsNotNull(result); + Assert.AreEqual("label", result.LabelAdded); + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataByIdAsync(It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.UpdateMetadataAsync(It.IsAny()), + Times.Once); + } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.RemoveInterestLabelFromDetectionAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.RemoveInterestLabelFromDetectionAsync.cs new file mode 100644 index 00000000..3ebe635a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.RemoveInterestLabelFromDetectionAsync.cs @@ -0,0 +1,88 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class InterestLabelOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RemoveInterestLabelFromDetectionAsync_Expect() + { + Metadata metadata = new() + { + InterestLabel = "test" + }; + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataByIdAsync(It.IsAny())) + .ReturnsAsync(metadata); + + _metadataServiceMock.Setup(service => + service.RemoveMetadataByIdAndStateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + _metadataServiceMock.Setup(service => + service.AddMetadataAsync(It.IsAny())) + .ReturnsAsync(true); + + + InterestLabelRemovalResponse result = await _orchestrationService.RemoveInterestLabelFromDetectionAsync("id"); + + Assert.IsNotNull(result); + Assert.AreEqual("test", result.LabelRemoved); + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataByIdAsync(It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.RemoveMetadataByIdAndStateAsync(It.IsAny(), It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.AddMetadataAsync(It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_RemoveInterestLabelFromDetectionAsync_Expect_Rollback() + { + Metadata metadata = new() + { + InterestLabel = "test" + }; + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataByIdAsync(It.IsAny())) + .ReturnsAsync(metadata); + + _metadataServiceMock.Setup(service => + service.RemoveMetadataByIdAndStateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + _metadataServiceMock.SetupSequence(service => + service.AddMetadataAsync(It.IsAny())) + .ReturnsAsync(false) + .ReturnsAsync(true); + + try + { + InterestLabelRemovalResponse result = await _orchestrationService.RemoveInterestLabelFromDetectionAsync("id"); + } + catch (Exception ex) + { + Assert.IsTrue(ex is InterestLabelOrchestrationValidationException && + ex.InnerException is DetectionNotInsertedException); + } + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataByIdAsync(It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.RemoveMetadataByIdAndStateAsync(It.IsAny(), It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.AddMetadataAsync(It.IsAny()), + Times.Exactly(2)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.RetrieveAllInterestLabelsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.RetrieveAllInterestLabelsAsync.cs new file mode 100644 index 00000000..4d143e5c --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Default.RetrieveAllInterestLabelsAsync.cs @@ -0,0 +1,27 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class InterestLabelOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveAllInterestLabelsAsync_Expect() + { + var expectedResults = new QueryableInterestLabels + { + QueryableRecords = (new List { "Label" }).AsQueryable(), + TotalCount = 1 + }; + + _metadataServiceMock.Setup(service => + service.RetrieveAllInterestLabelsAsync()) + .ReturnsAsync(expectedResults); + + InterestLabelListResponse result = await _orchestrationService.RetrieveAllInterestLabelsAsync(); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.InterestLabels.Count()); + + _metadataServiceMock.Verify(service => + service.RetrieveAllInterestLabelsAsync(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Guards.cs new file mode 100644 index 00000000..a0ef9578 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Guards.cs @@ -0,0 +1,34 @@ +using OrcaHello.Web.Shared.Utilities; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class InterestLabelOrchestrationServiceTests + { + [TestMethod] + public void Guard_AllGuardConditions_Expect_Exception() + { + var wrapper = new InterestLabelOrchestrationServiceWrapper(); + + string invalidProperty = string.Empty; + + Assert.ThrowsException(() => + wrapper.Validate(invalidProperty, nameof(invalidProperty))); + + Metadata nullMetadata = null; + + Assert.ThrowsException(() => + wrapper.ValidateMetadataFound(nullMetadata, "id")); + + bool notDeleted = false; + + Assert.ThrowsException(() => + wrapper.ValidateDeleted(notDeleted, "id")); + + bool notInserted = false; + + Assert.ThrowsException(() => + wrapper.ValidateInserted(notInserted, "id")); + + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/InterestLabelOrchestrationServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/InterestLabelOrchestrationServiceWrapper.cs new file mode 100644 index 00000000..cd7be108 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/InterestLabelOrchestrationServiceWrapper.cs @@ -0,0 +1,21 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + public class InterestLabelOrchestrationServiceWrapper : InterestLabelOrchestrationService + { + public new void Validate(string propertyValue, string propertyName) => + base.Validate(propertyValue, propertyName); + + public new void ValidateMetadataFound(Metadata metadata, string id) => + base.ValidateMetadataFound(metadata, id); + + public new void ValidateDeleted(bool deleted, string id) => + base.ValidateDeleted(deleted, id); + + public new void ValidateInserted(bool inserted, string id) => + base.ValidateInserted(inserted, id); + + public new ValueTask TryCatch(ReturningGenericFunction returningValueTaskFunction) => + base.TryCatch(returningValueTaskFunction); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Setup.cs new file mode 100644 index 00000000..57cd8cc5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/Setup.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class InterestLabelOrchestrationServiceTests + { + private readonly Mock _metadataServiceMock; + private readonly Mock> _loggerMock; + + private readonly IInterestLabelOrchestrationService _orchestrationService; + + public InterestLabelOrchestrationServiceTests() + { + _metadataServiceMock = new Mock(); + _loggerMock = new Mock>(); + + _orchestrationService = new InterestLabelOrchestrationService( + metadataService: _metadataServiceMock.Object, + logger: _loggerMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _metadataServiceMock.VerifyNoOtherCalls(); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs new file mode 100644 index 00000000..19e0ee1e --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/InterestLabelOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs @@ -0,0 +1,44 @@ +using static OrcaHello.Web.Api.Services.InterestLabelOrchestrationService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class InterestLabelOrchestrationServiceTests + { + [TestMethod] + public void TryCatch_ReturningGenericFunction_Expect_Exception() + { + var wrapper = new InterestLabelOrchestrationServiceWrapper(); + var delegateMock = new Mock>(); + + delegateMock + .SetupSequence(p => p()) + + .Throws(new NotFoundMetadataException("id")) + .Throws(new InvalidInterestLabelOrchestrationException()) + + .Throws(new MetadataValidationException()) + .Throws(new MetadataDependencyValidationException()) + + .Throws(new MetadataDependencyException()) + .Throws(new MetadataServiceException()) + + .Throws(new Exception()); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.AddMetadataAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.AddMetadataAsync.cs new file mode 100644 index 00000000..b58b525f --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.AddMetadataAsync.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_AddMetadataAsync() + { + _storageBrokerMock.Setup(broker => + broker.InsertMetadata(It.IsAny())) + .ReturnsAsync(true); + + Metadata newRecord = CreateRandomMetadata(); + + var result = await _metadataService. + AddMetadataAsync(newRecord); + + Assert.IsTrue(result); + + _storageBrokerMock.Verify(broker => + broker.InsertMetadata(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RemoveMetadataByIdAndStateAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RemoveMetadataByIdAndStateAsync.cs new file mode 100644 index 00000000..f57404c2 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RemoveMetadataByIdAndStateAsync.cs @@ -0,0 +1,22 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RemoveMetadataByIdAndStateAsync() + { + _storageBrokerMock.Setup(broker => + broker.DeleteMetadataByIdAndState(It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + var result = await _metadataService. + RemoveMetadataByIdAndStateAsync(Guid.NewGuid().ToString(), "Unreviewed"); + + Assert.IsTrue(result); + + _storageBrokerMock.Verify(broker => + broker.DeleteMetadataByIdAndState(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveAllInterestLabelsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveAllInterestLabelsAsync.cs new file mode 100644 index 00000000..07dcba53 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveAllInterestLabelsAsync.cs @@ -0,0 +1,28 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveAllInterestLabelsAsync() + { + var labels = new List + { + "Label1", + "Label2" + }; + + _storageBrokerMock.Setup(broker => + broker.GetAllInterestLabels()) + .ReturnsAsync(labels); + + var result = await _metadataService. + RetrieveAllInterestLabelsAsync(); + + Assert.AreEqual(labels.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetAllInterestLabels(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveAllTags.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveAllTags.cs new file mode 100644 index 00000000..69558041 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveAllTags.cs @@ -0,0 +1,28 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveAllTagsAsync() + { + var tags = new List + { + "Tag1", + "Tag2" + }; + + _storageBrokerMock.Setup(broker => + broker.GetAllTagList()) + .ReturnsAsync(tags); + + var result = await _metadataService. + RetrieveAllTagsAsync(); + + Assert.AreEqual(tags.Count, result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetAllTagList(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataByIdAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataByIdAsync.cs new file mode 100644 index 00000000..bb9f2fab --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataByIdAsync.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveMetadataByIdAsync() + { + Metadata expectedResult = CreateRandomMetadata(); + + _storageBrokerMock.Setup(broker => + broker.GetMetadataById(It.IsAny())) + .ReturnsAsync(expectedResult); + + var result = await _metadataService. + RetrieveMetadataByIdAsync(Guid.NewGuid().ToString()); + + Assert.AreEqual(expectedResult.LocationName, result.LocationName); + + _storageBrokerMock.Verify(broker => + broker.GetMetadataById(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForGivenTimeframeAndTagAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForGivenTimeframeAndTagAsync.cs new file mode 100644 index 00000000..1141b0c2 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForGivenTimeframeAndTagAsync.cs @@ -0,0 +1,121 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveMetadataForGivenTimeframeAndTagAsync() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new List + { + new() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetMetadataListByTimeframeAndTag(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveMetadataForGivenTimeframeAndTagAsync(fromDate, toDate, "tag", 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count, result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetMetadataListByTimeframeAndTag(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrieveMetadataForGivenTimeframeAndTagAsync_AndTags() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new List + { + new() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetMetadataListByTimeframeAndTag(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveMetadataForGivenTimeframeAndTagAsync(fromDate, toDate, "tag1,tag2", 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count, result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetMetadataListByTimeframeAndTag(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrieveMetadataForGivenTimeframeAndTagAsync_OrTags() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new List + { + new() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetMetadataListByTimeframeAndTag(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveMetadataForGivenTimeframeAndTagAsync(fromDate, toDate, "tag1|tag2", 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count, result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetMetadataListByTimeframeAndTag(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrieveMetadataForGivenTimeframeAndTagAsync_ZeroPageAndPageSize() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new List + { + new() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetMetadataListByTimeframeAndTag(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveMetadataForGivenTimeframeAndTagAsync(fromDate, toDate, "tag", -1, -10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count, result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetMetadataListByTimeframeAndTag(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForInterestLabelAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForInterestLabelAsync.cs new file mode 100644 index 00000000..9bc4b212 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForInterestLabelAsync.cs @@ -0,0 +1,31 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveMetadataForInterestLabelAsync() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new List + { + new() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetAllMetadataListByInterestLabel(It.IsAny())) + .ReturnsAsync(expectedResult); + + var result = await _metadataService. + RetrieveMetadataForInterestLabelAsync("test"); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetAllMetadataListByInterestLabel(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForTagAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForTagAsync.cs new file mode 100644 index 00000000..58f36676 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetadataForTagAsync.cs @@ -0,0 +1,31 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveMetadataForTagAsync() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new List + { + new() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetAllMetadataListByTag(It.IsAny())) + .ReturnsAsync(expectedResult); + + var result = await _metadataService. + RetrieveMetadataForTagAsync("tag"); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetAllMetadataListByTag(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetricsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetricsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..e4cba81c --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetricsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,32 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveMetricsForGivenTimeframeAndModeratorAsync() + { + List expectedResults = new() + { + new() { State = "Positive", Count = 5 }, + new() { State = "Negative", Count = 10 }, + new() { State = "Unknown", Count = 2 }, + }; + + _storageBrokerMock.Setup(broker => + broker.GetMetricsListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveMetricsForGivenTimeframeAndModeratorAsync(fromDate, toDate, "Moderator"); + + Assert.AreEqual(3, result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetMetricsListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetricsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetricsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..0a88d63e --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveMetricsForGivenTimeframeAsync.cs @@ -0,0 +1,33 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveMetricsForGivenTimeframeAsync() + { + List expectedResults = new() + { + new() { State = "Unreviewed", Count = 1 }, + new() { State = "Positive", Count = 5 }, + new() { State = "Negative", Count = 10 }, + new() { State = "Unknown", Count = 2 }, + }; + + _storageBrokerMock.Setup(broker => + broker.GetMetricsListByTimeframe(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveMetricsForGivenTimeframeAsync(fromDate, toDate); + + Assert.AreEqual(4, result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetMetricsListByTimeframe(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveModeratorsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveModeratorsAsync.cs new file mode 100644 index 00000000..17f4095d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveModeratorsAsync.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveModeratorsAsync() + { + List expectedResults = new() + { + "Moderator 1", + "Moderator 2" + }; + + _storageBrokerMock.Setup(broker => + broker.GetModeratorList()) + .ReturnsAsync(expectedResults); + + var result = await _metadataService. + RetrieveModeratorsAsync(); + + Assert.AreEqual(expectedResults.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetModeratorList(), + Times.Once); + } + } + +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..943353f2 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,63 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetNegativeAndUnknownMetadataListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync(fromDate, toDate, "Moderator", 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetNegativeAndUnknownMetadataListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync_ZeroPageAndPageSize() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + new() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetNegativeAndUnknownMetadataListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync(fromDate, toDate, "Moderator", -1, -10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetNegativeAndUnknownMetadataListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync.cs new file mode 100644 index 00000000..ab371fda --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync.cs @@ -0,0 +1,66 @@ +using Moq; +using OrcaHello.Web.Api.Models; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetNegativeAndUnknownMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync(fromDate, toDate, 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetNegativeAndUnknownMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync_ZeroPageAndPageSize() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetNegativeAndUnknownMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync(fromDate, toDate, -1, -10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetNegativeAndUnknownMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePaginatedMetadataAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePaginatedMetadataAsync.cs new file mode 100644 index 00000000..52245208 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePaginatedMetadataAsync.cs @@ -0,0 +1,67 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrievePaginatedMetdataAsync() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetMetadataListFiltered(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrievePaginatedMetadataAsync("Positive", fromDate, toDate, "timestamp", true, "Haro Straight", 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetMetadataListFiltered(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrievePaginatedMetdataAsync_ZeroPageAndPageSize() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetMetadataListFiltered(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrievePaginatedMetadataAsync("Positive", fromDate, toDate, "timestamp", true, "Haro Straight", -1, -10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetMetadataListFiltered(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..a8d88661 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,63 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync() + { + ListMetadataAndCount expectedResult = new ListMetadataAndCount + { + PaginatedRecords = new List + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetPositiveMetadataListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync(fromDate, toDate, "Moderator", 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetPositiveMetadataListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync_ZeroPageAndPageSize() + { + ListMetadataAndCount expectedResult = new ListMetadataAndCount + { + PaginatedRecords = new List + { + new Metadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetPositiveMetadataListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync(fromDate, toDate, "Moderator", -1, -10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetPositiveMetadataListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePositiveMetadataForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePositiveMetadataForGivenTimeframeAsync.cs new file mode 100644 index 00000000..7839a934 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrievePositiveMetadataForGivenTimeframeAsync.cs @@ -0,0 +1,67 @@ +using Moq; +using OrcaHello.Web.Api.Models; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrievePositiveMetadataForGivenTimeframeAsync() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetPositiveMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrievePositiveMetadataForGivenTimeframeAsync(fromDate, toDate, 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetPositiveMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrievePositiveMetadataForGivenTimeframeAsync_ZeroPageAndPageSize() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + + _storageBrokerMock.Setup(broker => + broker.GetPositiveMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrievePositiveMetadataForGivenTimeframeAsync(fromDate, toDate, -1, -10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetPositiveMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveTagsForGivenTimePeriodAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveTagsForGivenTimePeriodAndModeratorAsync.cs new file mode 100644 index 00000000..a8bbf8b8 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveTagsForGivenTimePeriodAndModeratorAsync.cs @@ -0,0 +1,31 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveTagsForGivenTimePeriodAndModeratorAsync() + { + var tags = new List + { + "Tag1", + "Tag2" + }; + + _storageBrokerMock.Setup(broker => + broker.GetTagListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(tags); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveTagsForGivenTimePeriodAndModeratorAsync(fromDate, toDate, "moderator"); + + Assert.AreEqual(tags.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetTagListByTimeframeAndModerator(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveTagsForGivenTimePeriodAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveTagsForGivenTimePeriodAsync.cs new file mode 100644 index 00000000..1eebcc1a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveTagsForGivenTimePeriodAsync.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveTagsForGivenTimePeriodAsync() + { + var tags = new List + { + "Tag1", + "Tag2" + }; + + _storageBrokerMock.Setup(broker => + broker.GetTagListByTimeframe(It.IsAny(), It.IsAny())) + .ReturnsAsync(tags); + + var result = await _metadataService. + RetrieveTagsForGivenTimePeriodAsync(DateTime.Now, DateTime.Now.AddDays(1)); + + Assert.AreEqual(tags.Count(), result.TotalCount); + + _storageBrokerMock.Verify(broker => + broker.GetTagListByTimeframe(It.IsAny(), It.IsAny()), + Times.Once); + } + + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveUnreviewedMetadataForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveUnreviewedMetadataForGivenTimeframeAsync.cs new file mode 100644 index 00000000..f8fff63d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.RetrieveUnreviewedMetadataForGivenTimeframeAsync.cs @@ -0,0 +1,63 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_RetrieveUnreviewedMetadataForGivenTimeframeAsync() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetUnreviewedMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveUnreviewedMetadataForGivenTimeframeAsync(fromDate, toDate, 1, 10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetUnreviewedMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [TestMethod] + public async Task Default_Expect_RetrieveUnreviewedMetadataForGivenTimeframeAsync_ZeroPageAndPageSize() + { + ListMetadataAndCount expectedResult = new() + { + PaginatedRecords = new() + { + CreateRandomMetadata() + }, + TotalCount = 1 + }; + + _storageBrokerMock.Setup(broker => + broker.GetUnreviewedMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + DateTime fromDate = DateTime.Now; + DateTime toDate = DateTime.Now.AddDays(1); + + var result = await _metadataService. + RetrieveUnreviewedMetadataForGivenTimeframeAsync(fromDate, toDate, -1, -10); + + Assert.AreEqual(expectedResult.PaginatedRecords.Count(), result.QueryableRecords.Count()); + + _storageBrokerMock.Verify(broker => + broker.GetUnreviewedMetadataListByTimeframe(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.UpdateMetadataAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.UpdateMetadataAsync.cs new file mode 100644 index 00000000..65d6a310 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Default.UpdateMetadataAsync.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public async Task Default_Expect_UpdateMetadataAsync() + { + _storageBrokerMock.Setup(broker => + broker.UpdateMetadataInPartition(It.IsAny())) + .ReturnsAsync(true); + + Metadata metadata = CreateRandomMetadata(); + + var result = await _metadataService. + UpdateMetadataAsync(metadata); + + Assert.IsTrue(result); + + _storageBrokerMock.Verify(broker => + broker.UpdateMetadataInPartition(It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Guards.cs new file mode 100644 index 00000000..3bac28ae --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Guards.cs @@ -0,0 +1,82 @@ +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Api.Tests.Unit.Services.Metadatas; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public void Guard_AllGuardConditions_Expect_Exception() + { + var wrapper = new MetadataServiceWrapper(); + + DateTime invalidDate = DateTime.MinValue; + + Assert.ThrowsException(() => + wrapper.Validate(invalidDate, nameof(invalidDate))); + + DateTime invalidFromDate = DateTime.Now; + DateTime invalidToDate = DateTime.Now.AddDays(-10); + + Assert.ThrowsException(() => + wrapper.ValidateDatesAreWithinRange(invalidFromDate, invalidToDate)); + + string invalidTag = string.Empty; + + Assert.ThrowsException(() => + wrapper.Validate(invalidTag, nameof(invalidTag))); + + Metadata nullMetadata = null; + + Assert.ThrowsException(() => + wrapper.ValidateMetadataOnCreate(nullMetadata)); + + Assert.ThrowsException(() => + wrapper.ValidateMetadataOnUpdate(nullMetadata)); + + Metadata invalidMetadata = new() + { + Id = string.Empty, + State = string.Empty, + LocationName = string.Empty + }; + + Assert.ThrowsException(() => + wrapper.ValidateMetadataOnCreate(invalidMetadata)); + + Assert.ThrowsException(() => + wrapper.ValidateMetadataOnUpdate(invalidMetadata)); + + invalidMetadata.Id = Guid.NewGuid().ToString(); + + Assert.ThrowsException(() => + wrapper.ValidateMetadataOnCreate(invalidMetadata)); + + Assert.ThrowsException(() => + wrapper.ValidateMetadataOnUpdate(invalidMetadata)); + + invalidMetadata.State = "Positive"; + + Assert.ThrowsException(() => + wrapper.ValidateMetadataOnCreate(invalidMetadata)); + + Assert.ThrowsException(() => + wrapper.ValidateMetadataOnUpdate(invalidMetadata)); + + string invalidState = "Goober"; + + Assert.ThrowsException(() => + wrapper.ValidateStateIsAcceptable(invalidState)); + + string invalidTags = "Goober;Goober2"; + + Assert.ThrowsException(() => + wrapper.ValidateTagContainsOnlyValidCharacters(invalidTags)); + + string invalidConjunctions = "Goober,Goober2|Goober3"; + + Assert.ThrowsException(() => + wrapper.ValidateTagContainsOnlyValidCharacters(invalidConjunctions)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/MetadataServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/MetadataServiceWrapper.cs new file mode 100644 index 00000000..3b68b377 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/MetadataServiceWrapper.cs @@ -0,0 +1,30 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services.Metadatas +{ + [ExcludeFromCodeCoverage] + public class MetadataServiceWrapper : MetadataService + { + public new void Validate(DateTime date, string propertyName) => + base.Validate(date, propertyName); + + public new void Validate(string propertyValue, string propertyName) => + base.Validate(propertyValue, propertyName); + + public new void ValidateDatesAreWithinRange(DateTime fromDate, DateTime toDate) => + base.ValidateDatesAreWithinRange(fromDate, toDate); + + public new void ValidateMetadataOnCreate(Metadata metadata) => + base.ValidateMetadataOnCreate(metadata); + + public new void ValidateMetadataOnUpdate(Metadata metadata) => + base.ValidateMetadataOnUpdate(metadata); + + public new void ValidateStateIsAcceptable(string state) => + base.ValidateStateIsAcceptable(state); + + public new void ValidateTagContainsOnlyValidCharacters(string tag) => + base.ValidateTagContainsOnlyValidCharacters(tag); + + public new ValueTask TryCatch(ReturningGenericFunction returningValueTaskFunction) => + base.TryCatch(returningValueTaskFunction); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Setup.cs new file mode 100644 index 00000000..90ea84f3 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/Setup.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Logging; +using Moq; +using OrcaHello.Web.Api.Brokers.Storages; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Api.Services; +using System.Diagnostics.CodeAnalysis; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class MetadataServiceTests + { + private readonly Mock _storageBrokerMock; + private readonly Mock> _loggerMock; + + private readonly IMetadataService _metadataService; + + public MetadataServiceTests() + { + _storageBrokerMock = new Mock(); + _loggerMock = new Mock>(); + + _metadataService = new MetadataService( + storageBroker: _storageBrokerMock.Object, + logger: _loggerMock.Object); + + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _storageBrokerMock.VerifyNoOtherCalls(); + } + + public static Metadata CreateRandomMetadata() + { + return new Metadata + { + Id = Guid.NewGuid().ToString(), + State = "Unreviewed", + LocationName = "location", + AudioUri = "https://url", + ImageUri = "https://url", + Timestamp = DateTime.Now, + WhaleFoundConfidence = 0.66M, + Location = new Models.Location + { + Name = "location", + Id = "location_guid", + Latitude = 1.00, + Longitude = 1.00 + }, + Predictions = new List + { + new Prediction + { + Id = 1, + StartTime = 5.00M, + Duration = 1.0M, + Confidence = 0.66M + } + }, + DateModerated = DateTime.Now.ToString() + }; + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/TryCatch.ReturningGenericFunction.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/TryCatch.ReturningGenericFunction.cs new file mode 100644 index 00000000..14788ed9 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetadataServiceTests/TryCatch.ReturningGenericFunction.cs @@ -0,0 +1,65 @@ +using static OrcaHello.Web.Api.Services.MetadataService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetadataServiceTests + { + [TestMethod] + public void TryCatch_ReturningGenericFunction_Expect_Exception() + { + var wrapper = new MetadataServiceWrapper(); + var delegateMock = new Mock>(); + + var cosmosErrorMessage = "{\"errors\":[{\"severity\":\"Error\",\"location\":{\"start\":22,\"end\":26},\"code\":\"SC2001\",\"message\":\"error message.\"}]}););\""; + + delegateMock + .SetupSequence(p => p()) + + .Throws(new InvalidMetadataException()) + + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.BadRequest, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.NotFound, 0, null, 0.0)) + + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.Unauthorized, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.Forbidden, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.MethodNotAllowed, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.Conflict, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.PreconditionFailed, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.RequestEntityTooLarge, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.RequestTimeout, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.ServiceUnavailable, 0, null, 0.0)) + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.InternalServerError, 0, null, 0.0)) + + .Throws(new CosmosException(cosmosErrorMessage, HttpStatusCode.AlreadyReported, 0, null, 0.0)) + + .Throws(new ArgumentNullException()) + .Throws(new ArgumentException()) + .Throws(new HttpRequestException()) + .Throws(new AggregateException()) + .Throws(new InvalidOperationException()) + + .Throws(new Exception()); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 9; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 5; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Default.RetrieveMetricsForGivenTimeframeAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Default.RetrieveMetricsForGivenTimeframeAsync.cs new file mode 100644 index 00000000..72e7ac79 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Default.RetrieveMetricsForGivenTimeframeAsync.cs @@ -0,0 +1,35 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Metrics; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetricsOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveMetricsForGivenTimeframeAsync_Expect() + { + MetricsSummaryForTimeframe expectedResult = new MetricsSummaryForTimeframe + { + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + QueryableRecords = (new List + { + new MetricResult { State = "Positive", Count = 1} + }).AsQueryable() + }; + + _metadataServiceMock.Setup(service => + service.RetrieveMetricsForGivenTimeframeAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + MetricsResponse result = await _orchestrationService.RetrieveMetricsForGivenTimeframeAsync(DateTime.Now, DateTime.Now.AddDays(1)); + + Assert.AreEqual(expectedResult.QueryableRecords.Where(x => x.State == "Positive").Select(x => x.Count).FirstOrDefault(), result.Positive); + + _metadataServiceMock.Verify(service => + service.RetrieveMetricsForGivenTimeframeAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Guards.cs new file mode 100644 index 00000000..b3a122f9 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Guards.cs @@ -0,0 +1,23 @@ +using OrcaHello.Web.Api.Models; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetricsOrchestrationServiceTests + { + [TestMethod] + public void Guard_AllGuardConditions_Expect_Exception() + { + var wrapper = new MetricsOrchestrationServiceWrapper(); + + DateTime? invalidDate = DateTime.MinValue; + + Assert.ThrowsException(() => + wrapper.Validate(invalidDate, nameof(invalidDate))); + + DateTime? nullDate = null; + + Assert.ThrowsException(() => + wrapper.Validate(nullDate, nameof(nullDate))); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/MetricsOrchestrationServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/MetricsOrchestrationServiceWrapper.cs new file mode 100644 index 00000000..11f3dca6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/MetricsOrchestrationServiceWrapper.cs @@ -0,0 +1,16 @@ +using OrcaHello.Web.Api.Services; +using OrcaHello.Web.Shared.Models.Metrics; +using System.Diagnostics.CodeAnalysis; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + public class MetricsOrchestrationServiceWrapper : MetricsOrchestrationService + { + public new void Validate(DateTime? date, string propertyName) => + base.Validate(date, propertyName); + + public new ValueTask TryCatch(ReturningMetricsResponseFunction returningMetricsResponseFunction) => + base.TryCatch(returningMetricsResponseFunction); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Setup.cs new file mode 100644 index 00000000..16474a19 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/Setup.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging; +using Moq; +using OrcaHello.Web.Api.Services; +using System.Diagnostics.CodeAnalysis; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class MetricsOrchestrationServiceTests + { + private readonly Mock _metadataServiceMock; + private readonly Mock> _loggerMock; + + private readonly IMetricsOrchestrationService _orchestrationService; + + public MetricsOrchestrationServiceTests() + { + _metadataServiceMock = new Mock(); + _loggerMock = new Mock>(); + + _orchestrationService = new MetricsOrchestrationService( + metadataService: _metadataServiceMock.Object, + logger: _loggerMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _metadataServiceMock.VerifyNoOtherCalls(); + } + + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/TryCatch.MetricsResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/TryCatch.MetricsResponse.cs new file mode 100644 index 00000000..b526acc9 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/MetricsOrchestrationServiceTests/TryCatch.MetricsResponse.cs @@ -0,0 +1,43 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using static OrcaHello.Web.Api.Services.MetricsOrchestrationService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class MetricsOrchestrationServiceTests + { + [TestMethod] + public void TryCatch_MetricsResponse_Expect_Exception() + { + var wrapper = new MetricsOrchestrationServiceWrapper(); + var delegateMock = new Mock(); + + delegateMock + .SetupSequence(p => p()) + + .Throws(new InvalidMetricOrchestrationException()) + + .Throws(new MetadataValidationException()) + .Throws(new MetadataDependencyValidationException()) + + .Throws(new MetadataDependencyException()) + .Throws(new MetadataServiceException()) + + .Throws(new Exception()); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveMetricsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveMetricsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..11972d63 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveMetricsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,36 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Moderators; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class ModeratorOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveMetricsForGivenTimeframeAndModeratorAsync_Expect() + { + MetricsSummaryForTimeframeAndModerator expectedResult = new MetricsSummaryForTimeframeAndModerator + { + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + QueryableRecords = (new List + { + new MetricResult { State = "Positive", Count = 1} + }).AsQueryable(), + Moderator = "Moderator" + }; + + _metadataServiceMock.Setup(service => + service.RetrieveMetricsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + MetricsForModeratorResponse result = await _orchestrationService.RetrieveMetricsForGivenTimeframeAndModeratorAsync(DateTime.Now, DateTime.Now.AddDays(1), "Moderator"); + + Assert.AreEqual(expectedResult.QueryableRecords.Where(x => x.State == "Positive").Select(x => x.Count).FirstOrDefault(), result.Positive); + + _metadataServiceMock.Verify(service => + service.RetrieveMetricsForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveModeratorsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveModeratorsAsync.cs new file mode 100644 index 00000000..cb4f443b --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveModeratorsAsync.cs @@ -0,0 +1,35 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Moderators; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class ModeratorOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveModeratorsAsync_Expect() + { + QueryableModerators expectedResult = new QueryableModerators + { + QueryableRecords = (new List + { + "Moderator 1", + "Moderator 2" + }).AsQueryable(), + TotalCount = 2 + }; + + _metadataServiceMock.Setup(service => + service.RetrieveModeratorsAsync()) + .ReturnsAsync(expectedResult); + + ModeratorListResponse result = await _orchestrationService.RetrieveModeratorsAsync(); + + Assert.AreEqual(expectedResult.QueryableRecords.Count(), result.Moderators.Count()); + + _metadataServiceMock.Verify(service => + service.RetrieveModeratorsAsync(), + Times.Once); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..065e8858 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,40 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Comments; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class ModeratorOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync_Expect() + { + var nullModeratedMetadata = CreateRandomMetadata(); + nullModeratedMetadata.DateModerated = null; + + var expectedResults = new QueryableMetadataForTimeframeAndModerator + { + QueryableRecords = (new List { CreateRandomMetadata(), nullModeratedMetadata }).AsQueryable(), + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + TotalCount = 1, + Page = 1, + PageSize = 10, + Moderator = "Moderator" + }; + + _metadataServiceMock.Setup(service => service. + RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + CommentListResponse result = await _orchestrationService. + RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync(DateTime.Now, DateTime.Now.AddDays(1), "Moderator", 1, 10); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Comments.Count()); + + _metadataServiceMock.Verify(service => + service.RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync.cs new file mode 100644 index 00000000..06cc2eba --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync.cs @@ -0,0 +1,39 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Comments; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class ModeratorOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync_Expect() + { + var nullModeratedMetadata = CreateRandomMetadata(); + nullModeratedMetadata.DateModerated = null; + + var expectedResults = new QueryableMetadataForTimeframeAndModerator + { + QueryableRecords = (new List { CreateRandomMetadata(), nullModeratedMetadata }).AsQueryable(), + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + TotalCount = 1, + Page = 1, + PageSize = 10, + Moderator = "Moderator" + }; + + _metadataServiceMock.Setup(service => + service.RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + CommentListResponse result = await _orchestrationService.RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(DateTime.Now, DateTime.Now.AddDays(1), "Moderator", 1, 10); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Comments.Count()); + + _metadataServiceMock.Verify(service => + service.RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveTagsForGivenTimePeriodAndModeratorAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveTagsForGivenTimePeriodAndModeratorAsync.cs new file mode 100644 index 00000000..cf61cc42 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Default.RetrieveTagsForGivenTimePeriodAndModeratorAsync.cs @@ -0,0 +1,34 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Tags; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class ModeratorOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveTagsForGivenTimeframeAndModeratorAsync_Expect() + { + var expectedResults = new QueryableTagsForTimeframeAndModerator + { + QueryableRecords = (new List { "Tag" }).AsQueryable(), + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + TotalCount = 1, + Moderator = "Moderator" + }; + + _metadataServiceMock.Setup(service => + service.RetrieveTagsForGivenTimePeriodAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + TagListResponse result = await _orchestrationService.RetrieveTagsForGivenTimePeriodAndModeratorAsync(DateTime.Now, DateTime.Now.AddDays(1), "Moderator"); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Tags.Count()); + + _metadataServiceMock.Verify(service => + service.RetrieveTagsForGivenTimePeriodAndModeratorAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Guards.cs new file mode 100644 index 00000000..042470ef --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Guards.cs @@ -0,0 +1,37 @@ +using OrcaHello.Web.Api.Models; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class ModeratorOrchestrationServiceTests + { + [TestMethod] + public void Guard_AllGuardConditions_Expect_Exception() + { + var wrapper = new ModeratorOrchestrationServiceWrapper(); + + DateTime? invalidDate = DateTime.MinValue; + + Assert.ThrowsException(() => + wrapper.Validate(invalidDate, nameof(invalidDate))); + + DateTime? nullDate = null; + + Assert.ThrowsException(() => + wrapper.Validate(nullDate, nameof(nullDate))); + + string invalidProperty = string.Empty; + + Assert.ThrowsException(() => + wrapper.Validate(invalidProperty, nameof(invalidProperty))); + + int invalidPage = 0; + + Assert.ThrowsException(() => + wrapper.ValidatePage(invalidPage)); + + int invalidPageSize = 0; + Assert.ThrowsException(() => + wrapper.ValidatePageSize(invalidPageSize)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/ModeratorOrchestrationServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/ModeratorOrchestrationServiceWrapper.cs new file mode 100644 index 00000000..bd757bc9 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/ModeratorOrchestrationServiceWrapper.cs @@ -0,0 +1,24 @@ +using OrcaHello.Web.Api.Services; +using System.Diagnostics.CodeAnalysis; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + public class ModeratorOrchestrationServiceWrapper : ModeratorOrchestrationService + { + public new void Validate(DateTime? date, string propertyName) => + base.Validate(date, propertyName); + + public new void Validate(string propertyValue, string propertyName) => + base.Validate(propertyValue, propertyName); + + public new void ValidatePage(int page) => + base.ValidatePage(page); + + public new void ValidatePageSize(int pageSize) => + base.ValidatePageSize(pageSize); + + public new ValueTask TryCatch(ReturningGenericFunction returningGenericFunction) => + base.TryCatch(returningGenericFunction); + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Setup.cs new file mode 100644 index 00000000..db815e76 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/Setup.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.Logging; +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Api.Services; +using System.Diagnostics.CodeAnalysis; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class ModeratorOrchestrationServiceTests + { + private readonly Mock _metadataServiceMock; + private readonly Mock> _loggerMock; + + private readonly IModeratorOrchestrationService _orchestrationService; + + public ModeratorOrchestrationServiceTests() + { + _metadataServiceMock = new Mock(); + _loggerMock = new Mock>(); + + _orchestrationService = new ModeratorOrchestrationService( + metadataService: _metadataServiceMock.Object, + logger: _loggerMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _metadataServiceMock.VerifyNoOtherCalls(); + } + + public Metadata CreateRandomMetadata() + { + return new Metadata + { + Id = Guid.NewGuid().ToString(), + State = "Positive", + LocationName = "location", + AudioUri = "https://url", + ImageUri = "https://url", + Timestamp = DateTime.Now, + WhaleFoundConfidence = 0.66M, + Location = new() + { + Name = "location", + Id = "location_guid", + Latitude = 1.00, + Longitude = 1.00 + }, + Predictions = new List + { + new() + { + Id = 1, + StartTime = 5.00M, + Duration = 1.0M, + Confidence = 0.66M + } + }, + DateModerated = DateTime.Now.ToString(), + Moderator = "Moderator", + Comments = "Comments are here" + }; + } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/TryCatch.ValueT.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/TryCatch.ValueT.cs new file mode 100644 index 00000000..b8928db6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/ModeratorOrchestrationServiceTests/TryCatch.ValueT.cs @@ -0,0 +1,44 @@ +using Moq; +using OrcaHello.Web.Api.Models; +using OrcaHello.Web.Shared.Models.Moderators; +using static OrcaHello.Web.Api.Services.ModeratorOrchestrationService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class ModeratorOrchestrationServiceTests + { + [TestMethod] + public void TryCatch_GenericResponse_Expect_Exception() + { + var wrapper = new ModeratorOrchestrationServiceWrapper(); + var delegateMock = new Mock>(); + + delegateMock + .SetupSequence(p => p()) + + .Throws(new InvalidModeratorOrchestrationException()) + + .Throws(new MetadataValidationException()) + .Throws(new MetadataDependencyValidationException()) + + .Throws(new MetadataDependencyException()) + .Throws(new MetadataServiceException()) + + .Throws(new Exception()); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RemoveTagFromAllDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RemoveTagFromAllDetectionsAsync.cs new file mode 100644 index 00000000..84ec8603 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RemoveTagFromAllDetectionsAsync.cs @@ -0,0 +1,40 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class TagOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RemoveTagFromAllDetectionsAsync_Expect() + { + var tagToRemove = "TagToRemove"; + + QueryableMetadata metadataWithTag = new() + { + QueryableRecords = (new List() { new(), new() }).AsQueryable(), + TotalCount = 2 + }; + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataForTagAsync(It.IsAny())) + .ReturnsAsync(metadataWithTag); + + _metadataServiceMock.Setup(service => + service.UpdateMetadataAsync(It.IsAny())) + .ReturnsAsync(true); + + TagRemovalResponse result = await _orchestrationService.RemoveTagFromAllDetectionsAsync(tagToRemove); + + Assert.IsNotNull(result); + Assert.AreEqual(tagToRemove, result.Tag); + Assert.AreEqual(2, result.TotalMatching); + Assert.AreEqual(2, result.TotalRemoved); + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataForTagAsync(It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.UpdateMetadataAsync(It.IsAny()), + Times.Exactly(2)); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.ReplaceTagInAllDetectionsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.ReplaceTagInAllDetectionsAsync.cs new file mode 100644 index 00000000..5fdce784 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.ReplaceTagInAllDetectionsAsync.cs @@ -0,0 +1,42 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class TagOrchestrationServiceTests + { + [TestMethod] + public async Task Default_ReplaceTagInAllDetectionsAsync_Expect() + { + var oldTag = "OldTag"; + var newTag = "NewTag"; + + QueryableMetadata metadataWithTag = new() + { + QueryableRecords = (new List() { new() { Tags = new List() { "OldTag" } }, new() { Tags = new List() { "OldTag" } } }).AsQueryable(), + TotalCount = 2 + }; + + _metadataServiceMock.Setup(service => + service.RetrieveMetadataForTagAsync(It.IsAny())) + .ReturnsAsync(metadataWithTag); + + _metadataServiceMock.Setup(service => + service.UpdateMetadataAsync(It.IsAny())) + .ReturnsAsync(true); + + TagReplaceResponse result = await _orchestrationService.ReplaceTagInAllDetectionsAsync(oldTag, newTag); + + Assert.IsNotNull(result); + Assert.AreEqual(oldTag, result.OldTag); + Assert.AreEqual(newTag, result.NewTag); + Assert.AreEqual(2, result.TotalMatching); + Assert.AreEqual(2, result.TotalReplaced); + + _metadataServiceMock.Verify(service => + service.RetrieveMetadataForTagAsync(It.IsAny()), + Times.Once); + + _metadataServiceMock.Verify(service => + service.UpdateMetadataAsync(It.IsAny()), + Times.Exactly(2)); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RetrieveAllTagsAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RetrieveAllTagsAsync.cs new file mode 100644 index 00000000..d04d92c6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RetrieveAllTagsAsync.cs @@ -0,0 +1,27 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class TagOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveAllTagsAsync_Expect() + { + var expectedResults = new QueryableTags + { + QueryableRecords = (new List { "Tag" }).AsQueryable(), + TotalCount = 1 + }; + + _metadataServiceMock.Setup(service => + service.RetrieveAllTagsAsync()) + .ReturnsAsync(expectedResults); + + TagListResponse result = await _orchestrationService.RetrieveAllTagsAsync(); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Tags.Count); + + _metadataServiceMock.Verify(service => + service.RetrieveAllTagsAsync(), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RetrieveTagsForGivenTimePeriodAsync.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RetrieveTagsForGivenTimePeriodAsync.cs new file mode 100644 index 00000000..25c643d8 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Default.RetrieveTagsForGivenTimePeriodAsync.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class TagOrchestrationServiceTests + { + [TestMethod] + public async Task Default_RetrieveTagsForGivenTimeframeAsync_Expect() + { + var expectedResults = new QueryableTagsForTimeframe + { + QueryableRecords = (new List { "Tag" }).AsQueryable(), + FromDate = DateTime.Now, + ToDate = DateTime.Now.AddDays(1), + TotalCount = 1 + }; + + _metadataServiceMock.Setup(service => + service.RetrieveTagsForGivenTimePeriodAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResults); + + TagListResponse result = await _orchestrationService.RetrieveTagsForGivenTimePeriodAsync(DateTime.Now, DateTime.Now.AddDays(1)); + + Assert.AreEqual(expectedResults.QueryableRecords.Count(), result.Tags.Count()); + + _metadataServiceMock.Verify(service => + service.RetrieveTagsForGivenTimePeriodAsync(It.IsAny(), It.IsAny()), + Times.Once); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Guards.cs new file mode 100644 index 00000000..5dd1e3ba --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Guards.cs @@ -0,0 +1,26 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class TagOrchestrationServiceTests + { + [TestMethod] + public void Guard_AllGuardConditions_Expect_Exception() + { + var wrapper = new TagOrchestrationServiceWrapper(); + + DateTime? invalidDate = DateTime.MinValue; + + Assert.ThrowsException(() => + wrapper.Validate(invalidDate, nameof(invalidDate))); + + DateTime? nullDate = null; + + Assert.ThrowsException(() => + wrapper.Validate(nullDate, nameof(nullDate))); + + string invalidProperty = string.Empty; + + Assert.ThrowsException(() => + wrapper.Validate(invalidProperty, nameof(invalidProperty))); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Setup.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Setup.cs new file mode 100644 index 00000000..2f8a7e6b --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/Setup.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + [TestClass] + public partial class TagOrchestrationServiceTests + { + private readonly Mock _metadataServiceMock; + private readonly Mock> _loggerMock; + + private readonly ITagOrchestrationService _orchestrationService; + + public TagOrchestrationServiceTests() + { + _metadataServiceMock = new Mock(); + _loggerMock = new Mock>(); + + _orchestrationService = new TagOrchestrationService( + metadataService: _metadataServiceMock.Object, + logger: _loggerMock.Object); + } + + [TestCleanup] + public void TestTeardown() + { + _loggerMock.VerifyNoOtherCalls(); + _metadataServiceMock.VerifyNoOtherCalls(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/TagOrchestrationServiceWrapper.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/TagOrchestrationServiceWrapper.cs new file mode 100644 index 00000000..19ae4e8b --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/TagOrchestrationServiceWrapper.cs @@ -0,0 +1,15 @@ +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + [ExcludeFromCodeCoverage] + public class TagOrchestrationServiceWrapper : TagOrchestrationService + { + public new void Validate(DateTime? date, string propertyName) => + base.Validate(date, propertyName); + + public new void Validate(string propertyValue, string propertyName) => + base.Validate(propertyValue, propertyName); + + public new ValueTask TryCatch(ReturningGenericFunction returningValueTaskFunction) => + base.TryCatch(returningValueTaskFunction); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs new file mode 100644 index 00000000..74f82922 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api.Tests.Unit/Services/TagOrchestrationServiceTests/TryCatch.ReturningGenericFunction.cs @@ -0,0 +1,41 @@ +using static OrcaHello.Web.Api.Services.TagOrchestrationService; + +namespace OrcaHello.Web.Api.Tests.Unit.Services +{ + public partial class TagOrchestrationServiceTests + { + [TestMethod] + public void TryCatch_ReturningGenericFunction_Expect_Exception() + { + var wrapper = new TagOrchestrationServiceWrapper(); + var delegateMock = new Mock>(); + + delegateMock + .SetupSequence(p => p()) + + .Throws(new InvalidTagOrchestrationException()) + + .Throws(new MetadataValidationException()) + .Throws(new MetadataDependencyValidationException()) + + .Throws(new MetadataDependencyException()) + .Throws(new MetadataServiceException()) + + .Throws(new Exception()); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + for (int x = 0; x < 2; x++) + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + + Assert.ThrowsExceptionAsync(async () => + await wrapper.TryCatch(delegateMock.Object)); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Hydrophones/HydrophoneBroker.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Hydrophones/HydrophoneBroker.cs new file mode 100644 index 00000000..6cdff098 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Hydrophones/HydrophoneBroker.cs @@ -0,0 +1,35 @@ +namespace OrcaHello.Web.Api.Brokers.Hydrophones +{ + [ExcludeFromCodeCoverage] + public partial class HydrophoneBroker : IHydrophoneBroker + { + private readonly AppSettings _appSettings; + private readonly string _apiUrl; + + public HydrophoneBroker(AppSettings appSettings) + { + _appSettings = appSettings; + _apiUrl = _appSettings.HydrophoneFeedUrl; + } + + public async Task> GetFeedsAsync() + { + using HttpClient client = new(); + + // Send a GET request to the API endpoint + HttpResponseMessage response = await client.GetAsync(_apiUrl); + + // Ensure a successful response + response.EnsureSuccessStatusCode(); + + // Read the JSON response as a string + string json = await response.Content.ReadAsStringAsync(); + + // Deserialize the JSON string into a list of Data objects + HydrophoneRootObject root = JsonConvert.DeserializeObject(json); + + // Extract and return the 'data' property + return root?.Data ?? new List(); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Hydrophones/IHydrophoneBroker.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Hydrophones/IHydrophoneBroker.cs new file mode 100644 index 00000000..ee443bc4 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Hydrophones/IHydrophoneBroker.cs @@ -0,0 +1,7 @@ +namespace OrcaHello.Web.Api.Brokers.Hydrophones +{ + public partial interface IHydrophoneBroker + { + Task> GetFeedsAsync(); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Loggings/ILoggingBroker.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Loggings/ILoggingBroker.cs new file mode 100644 index 00000000..175ef036 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Loggings/ILoggingBroker.cs @@ -0,0 +1,6 @@ +namespace OrcaHello.Web.Api.Brokers.Loggings +{ + public interface ILoggingBroker + { + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Loggings/LoggingBroker.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Loggings/LoggingBroker.cs new file mode 100644 index 00000000..02e2d4b2 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Loggings/LoggingBroker.cs @@ -0,0 +1,6 @@ +namespace OrcaHello.Web.Api.Brokers.Loggings +{ + public class LoggingBroker : ILoggingBroker + { + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/IStorageBroker.Metadatas.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/IStorageBroker.Metadatas.cs new file mode 100644 index 00000000..cf003499 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/IStorageBroker.Metadatas.cs @@ -0,0 +1,33 @@ +namespace OrcaHello.Web.Api.Brokers.Storages +{ + public partial interface IStorageBroker + { + Task> GetModeratorList(); + Task> GetTagListByTimeframe(DateTime fromDate, DateTime toDate); + Task> GetTagListByTimeframeAndModerator(DateTime fromDate, DateTime toDate, string moderator); + Task GetMetadataListByTimeframeAndTag(DateTime fromDate, DateTime toDate, + List tags, string tagOperator, int page = 1, int pageSize = 10); + Task GetPositiveMetadataListByTimeframe(DateTime fromDate, DateTime toDate, + int page = 1, int pageSize = 10); + Task GetPositiveMetadataListByTimeframeAndModerator(DateTime fromDate, DateTime toDate, + string moderator, int page = 1, int pageSize = 10); + Task GetNegativeAndUnknownMetadataListByTimeframe(DateTime fromDate, DateTime toDate, + int page = 1, int pageSize = 10); + Task GetNegativeAndUnknownMetadataListByTimeframeAndModerator(DateTime fromDate, DateTime toDate, + string moderator, int page = 1, int pageSize = 10); + Task GetUnreviewedMetadataListByTimeframe(DateTime fromDate, DateTime toDate, + int page = 1, int pageSize = 10); + Task> GetMetricsListByTimeframe(DateTime fromDate, DateTime toDate); + Task> GetMetricsListByTimeframeAndModerator(DateTime fromDate, DateTime toDate, string moderator); + Task GetMetadataListFiltered(string state, DateTime fromDate, DateTime toDate, string sortBy, + string sortOrder, string location, int page = 1, int pageSize = 1); + Task GetMetadataById(string id); + Task DeleteMetadataByIdAndState(string id, string currentState); + Task InsertMetadata(Metadata metadata); + Task GetAllMetadataListByTag(string tag); + Task UpdateMetadataInPartition(Metadata metadata); + Task> GetAllTagList(); + Task GetAllMetadataListByInterestLabel(string interestLabel); + Task> GetAllInterestLabels(); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/IStorageBroker.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/IStorageBroker.cs new file mode 100644 index 00000000..ce94e4c8 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/IStorageBroker.cs @@ -0,0 +1,6 @@ +namespace OrcaHello.Web.Api.Brokers.Storages +{ + public partial interface IStorageBroker + { + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/StorageBroker.Metadatas.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/StorageBroker.Metadatas.cs new file mode 100644 index 00000000..7b99c5b0 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/StorageBroker.Metadatas.cs @@ -0,0 +1,315 @@ +using Location = OrcaHello.Web.Api.Models.Location; + +namespace OrcaHello.Web.Api.Brokers.Storages +{ + public partial class StorageBroker + { + #region Tag Queries + + public async Task> GetAllTagList() + { + var queryDefinition = new QueryDefinition("SELECT DISTINCT VALUE tag FROM c JOIN tag IN c.tags"); + + return await ExecuteListStringQuery(queryDefinition); + } + + public async Task> GetTagListByTimeframe(DateTime fromDate, DateTime toDate) + { + var queryDefinition = new QueryDefinition("SELECT DISTINCT VALUE tag FROM c JOIN tag IN c.tags WHERE c.timestamp BETWEEN @startTime AND @endTime") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)); + + return await ExecuteListStringQuery(queryDefinition); + } + + public async Task> GetTagListByTimeframeAndModerator(DateTime fromDate, DateTime toDate, string moderator) + { + var queryDefinition = new QueryDefinition("SELECT DISTINCT VALUE tag FROM c JOIN tag IN c.tags WHERE (c.timestamp BETWEEN @startTime AND @endTime) AND c.moderator = @moderator") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)) + .WithParameter("@moderator", moderator); + + return await ExecuteListStringQuery(queryDefinition); + } + + private async Task> ExecuteListStringQuery(QueryDefinition queryDefinition) + { + var queryIterator = _detectionsContainer.GetItemQueryIterator(queryDefinition); + var results = new List(); + + while (queryIterator.HasMoreResults) + { + var response = await queryIterator.ReadNextAsync(); + results.AddRange(response); + } + + return results; + } + + #endregion + + #region Interest Label Queries + + public async Task> GetAllInterestLabels() + { + var queryDefinition = new QueryDefinition("SELECT DISTINCT VALUE c.interestLabel FROM c WHERE IS_DEFINED(c.interestLabel)"); + + return await ExecuteListStringQuery(queryDefinition); + } + + #endregion + + #region Moderator Queries + + public async Task> GetModeratorList() + { + var queryDefinition = new QueryDefinition("SELECT DISTINCT VALUE c.moderator FROM c WHERE IS_DEFINED(c.moderator) AND c.moderator != null"); + + return await ExecuteListStringQuery(queryDefinition); + } + + #endregion + + #region Metadata Queries + + public async Task GetMetadataById(string id) + { + var queryDefinition = new QueryDefinition("SELECT * FROM c WHERE c.id = @id") + .WithParameter("@id", id); + + Metadata result = null!; + + var queryIterator = _detectionsContainer.GetItemQueryIterator(queryDefinition); + + while (queryIterator.HasMoreResults) + { + var response = await queryIterator.ReadNextAsync(); + + if (response.Count > 0) + { + result = response.First(); + } + } + + return result; + } + + public async Task DeleteMetadataByIdAndState(string id, string currentState) + { + ItemResponse deleteResponse = await _detectionsContainer.DeleteItemAsync(id, new PartitionKey(currentState)); + + return deleteResponse.StatusCode == System.Net.HttpStatusCode.NoContent; + } + + public async Task InsertMetadata(Metadata metadata) + { + ItemResponse insertResponse = await _detectionsContainer.CreateItemAsync(metadata, new PartitionKey(metadata.State)); + + return insertResponse.StatusCode == System.Net.HttpStatusCode.Created; + } + + public async Task UpdateMetadataInPartition(Metadata metadata) + { + var updateResponse = await _detectionsContainer.ReplaceItemAsync(metadata, metadata.Id, new PartitionKey(metadata.State)); + + return updateResponse.StatusCode == System.Net.HttpStatusCode.OK; + } + + public async Task GetMetadataListByTimeframeAndTag(DateTime fromDate, DateTime toDate, + List tags, string tagOperator, int page = 1, int pageSize = 10) + { + string queryText = "SELECT * FROM c WHERE(c.timestamp BETWEEN @startTime AND @endTime) AND ("; + for (int i = 0; i < tags.Count; i++) + { + if (i > 0) + { + queryText += $"{tagOperator} "; + } + queryText += $"ARRAY_CONTAINS(c.tags, @tag{i}) "; + } + + queryText += ")"; + + var queryDefinition = new QueryDefinition(queryText) + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)); + + for (int i = 0; i < tags.Count; i++) + { + queryDefinition.WithParameter($"@tag{i}", tags[i]); + } + + return await ExecutePaginatedMetadataQuery(queryDefinition, page, pageSize); + } + + public async Task GetAllMetadataListByTag(string tag) + { + var queryDefinition = new QueryDefinition("SELECT * FROM c WHERE ARRAY_CONTAINS(c.tags, @tag)") + .WithParameter("@tag", tag); + + return await ExecuteMetadataQuery(queryDefinition); + } + + public async Task GetAllMetadataListByInterestLabel(string interestLabel) + { + var queryDefinition = new QueryDefinition("SELECT * FROM c WHERE c.interestLabel = @interestLabel") + .WithParameter("@interestLabel", interestLabel); + + return await ExecuteMetadataQuery(queryDefinition); + } + + public async Task GetPositiveMetadataListByTimeframe(DateTime fromDate, DateTime toDate, + int page = 1, int pageSize = 10) + { + var queryDefinition = new QueryDefinition($"SELECT * FROM c WHERE c.state = '{DetectionState.Positive}' AND (c.timestamp BETWEEN @startTime AND @endTime)") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)); + + return await ExecutePaginatedMetadataQuery(queryDefinition, page, pageSize); + } + + public async Task GetPositiveMetadataListByTimeframeAndModerator(DateTime fromDate, DateTime toDate, + string moderator, int page = 1, int pageSize = 10) + { + var queryDefinition = new QueryDefinition($"SELECT * FROM c WHERE c.state = '{DetectionState.Positive}' AND (c.timestamp BETWEEN @startTime AND @endTime) AND c.moderator = @moderator") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)) + .WithParameter("@moderator", moderator); + + return await ExecutePaginatedMetadataQuery(queryDefinition, page, pageSize); + } + + public async Task GetNegativeAndUnknownMetadataListByTimeframe(DateTime fromDate, DateTime toDate, + int page = 1, int pageSize = 10) + { + var queryDefinition = new QueryDefinition($"SELECT * FROM c WHERE c.state IN ( '{DetectionState.Negative}', '{DetectionState.Unknown}') AND (c.timestamp BETWEEN @startTime AND @endTime)") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)); + + return await ExecutePaginatedMetadataQuery(queryDefinition, page, pageSize); + } + + public async Task GetNegativeAndUnknownMetadataListByTimeframeAndModerator(DateTime fromDate, DateTime toDate, + string moderator, int page = 1, int pageSize = 10) + { + var queryDefinition = new QueryDefinition($"SELECT * FROM c WHERE c.state IN ('{DetectionState.Negative}', '{DetectionState.Unknown}') AND (c.timestamp BETWEEN @startTime AND @endTime) AND c.moderator = @moderator") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)) + .WithParameter("@moderator", moderator); + + return await ExecutePaginatedMetadataQuery(queryDefinition, page, pageSize); + } + + public async Task GetUnreviewedMetadataListByTimeframe(DateTime fromDate, DateTime toDate, + int page = 1, int pageSize = 10) + { + var queryDefinition = new QueryDefinition($"SELECT * FROM c WHERE c.state = '{DetectionState.Unreviewed}' AND (c.timestamp BETWEEN @startTime AND @endTime)") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)); + + return await ExecutePaginatedMetadataQuery(queryDefinition, page, pageSize); + } + + public async Task GetMetadataListFiltered(string state, DateTime fromDate, DateTime toDate, string sortBy, + string sortOrder, string location, int page = 1, int pageSize = 1) + { + var queryText = "SELECT * " + + "FROM c " + + "WHERE c.state = @state " + + "AND (c.timestamp BETWEEN @startTime AND @endTime) "; + + if (!string.IsNullOrEmpty(location)) + { + queryText += "AND (c.locationName = @location OR c.locationName = '') "; + } + + queryText += $"ORDER BY c.{sortBy} {sortOrder}"; + + var queryDefinition = new QueryDefinition(queryText) + .WithParameter("@state", state) + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)) + .WithParameter("@location", location); + + return await ExecutePaginatedMetadataQuery(queryDefinition, page, pageSize); + } + + private async Task ExecuteMetadataQuery(QueryDefinition queryDefinition) + { + var queryIterator = _detectionsContainer.GetItemQueryIterator(queryDefinition); + var fullResults = new List(); + + while (queryIterator.HasMoreResults) + { + var response = await queryIterator.ReadNextAsync(); + fullResults.AddRange(response); + } + ListMetadataAndCount results = new() + { + PaginatedRecords = fullResults, + TotalCount = fullResults.Count + }; + + return results; + } + + private async Task ExecutePaginatedMetadataQuery(QueryDefinition queryDefinition, int page, int pageSize) + { + var queryIterator = _detectionsContainer.GetItemQueryIterator(queryDefinition); + var fullResults = new List(); + + while (queryIterator.HasMoreResults) + { + var response = await queryIterator.ReadNextAsync(); + fullResults.AddRange(response); + } + + ListMetadataAndCount results = new() + { + PaginatedRecords = fullResults.Skip((page - 1) * pageSize).Take(pageSize).ToList(), + TotalCount = fullResults.Count + }; + + return results; + } + + #endregion + + #region Metrics Queries + + public async Task> GetMetricsListByTimeframe(DateTime fromDate, DateTime toDate) + { + var queryDefinition = new QueryDefinition("SELECT c.state AS State, COUNT(1) AS Count FROM c WHERE (c.timestamp BETWEEN @startTime AND @endTime) GROUP BY c.state") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)); + + return await ExecuteMetricsQuery(queryDefinition); + } + + public async Task> GetMetricsListByTimeframeAndModerator(DateTime fromDate, DateTime toDate, string moderator) + { + var queryDefinition = new QueryDefinition("SELECT c.state AS State, COUNT(1) AS Count FROM c WHERE (c.timestamp BETWEEN @startTime AND @endTime) AND c.moderator = @moderator GROUP BY c.state") + .WithParameter("@startTime", CosmosUtilities.FormatDate(fromDate)) + .WithParameter("@endTime", CosmosUtilities.FormatDate(toDate)) + .WithParameter("@moderator", moderator); + + return await ExecuteMetricsQuery(queryDefinition); + } + + private async Task> ExecuteMetricsQuery(QueryDefinition queryDefinition) + { + var queryIterator = _detectionsContainer.GetItemQueryIterator(queryDefinition); + var results = new List(); + + while (queryIterator.HasMoreResults) + { + var response = await queryIterator.ReadNextAsync(); + results.AddRange(response); + } + + return results; + } + + #endregion + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/StorageBroker.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/StorageBroker.cs new file mode 100644 index 00000000..a2af966a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Brokers/Storages/StorageBroker.cs @@ -0,0 +1,45 @@ +namespace OrcaHello.Web.Api.Brokers.Storages +{ + [ExcludeFromCodeCoverage] + public partial class StorageBroker : IStorageBroker, IDisposable + { + private readonly AppSettings _appSettings; + private readonly CosmosClient _cosmosClient; + private readonly Container _detectionsContainer; + + public StorageBroker(AppSettings appSettings) + { + _appSettings = appSettings; + + _cosmosClient = new CosmosClient(_appSettings.CosmosConnectionString); + + Database database; + + try + { + database = _cosmosClient.GetDatabase(_appSettings.DetectionsDatabaseName); + database.ReadAsync().Wait(); + } + catch(Exception exception) + { + throw new Exception($"Database '{_appSettings.DetectionsDatabaseName}' was not found or could not be opened: {exception.Message}"); + } + + try + { + _detectionsContainer = database.GetContainer(_appSettings.MetadataContainerName); + _detectionsContainer.ReadContainerAsync().Wait(); + } + catch(Exception exception) + { + throw new Exception($"Container '{_appSettings.MetadataContainerName}' was not found or could not be opened: {exception.Message}."); + } + } + + public void Dispose() + { + _cosmosClient.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/1HomeController.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/1HomeController.cs new file mode 100644 index 00000000..41c8efef --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/1HomeController.cs @@ -0,0 +1,28 @@ +namespace OrcaHello.Web.Api.Controllers +{ + [Route("api/home")] + [SwaggerTag("This controller is responsible for indicating API status.")] + [ApiController] + public class HomeController : ControllerBase + { + [HttpGet] + [SwaggerOperation(Summary = "Indicates if the API is operational.")] + [SwaggerResponse(StatusCodes.Status200OK, "Indicates the API is operational.")] + + [AllowAnonymous] + [ExcludeFromCodeCoverage ] + public ActionResult Get() => + Ok("Welcome to the OrcaHello API v2.0!"); + + [HttpGet("moderator")] + [SwaggerOperation(Summary = "Indicates if the API is operational and the user is properly logged in as a Moderator.")] + [SwaggerResponse(StatusCodes.Status200OK, "Indicates the API is operational and the user is logged in.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "If the user is not a verified moderator.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [Authorize("Moderators")] + [ExcludeFromCodeCoverage] + public ActionResult GetLoggedIn() => + Ok("Welcome to the OrcaHello API v2.0! You are logged in as a Moderator."); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/2HydrophonesController.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/2HydrophonesController.cs new file mode 100644 index 00000000..1b199063 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/2HydrophonesController.cs @@ -0,0 +1,50 @@ +namespace OrcaHello.Web.Api.Controllers +{ + [Route("api/hydrophones")] + [SwaggerTag("This controller is responsible retrieving Hydrophone locations.")] + [ApiController] + public class HydrophonesController : ControllerBase + { + private readonly IHydrophoneOrchestrationService _hydrophoneOrchestrationService; + + public HydrophonesController(IHydrophoneOrchestrationService hydrophoneOrchestrationService) + { + _hydrophoneOrchestrationService = hydrophoneOrchestrationService; + } + + // For pulling and updating hydrophone definitions + + [HttpGet] + [SwaggerOperation(Summary = "Gets a list of unique hydrophones.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of hydrophones.", typeof(HydrophoneListResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetHydrophones() + { + try + { + var hydrophoneListResponse = await _hydrophoneOrchestrationService. + RetrieveHydrophoneLocations(); + + return Ok(hydrophoneListResponse); + } + catch (Exception exception) + { + if (exception is HydrophoneOrchestrationValidationException && + exception.InnerException is InvalidHydrophoneException) + return NotFound(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is HydrophoneOrchestrationValidationException || + exception is HydrophoneOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is HydrophoneOrchestrationDependencyException || + exception is HydrophoneOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/3DetectionsController.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/3DetectionsController.cs new file mode 100644 index 00000000..afeb6c1a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/3DetectionsController.cs @@ -0,0 +1,191 @@ +namespace OrcaHello.Web.Api.Controllers +{ + [Route("api/detections")] + [SwaggerTag("This controller is responsible for retrieving and curating detections.")] + [ApiController] + public class DetectionsController : ControllerBase + { + private readonly IDetectionOrchestrationService _detectionOrchestrationService; + + public DetectionsController(IDetectionOrchestrationService detectionOrchestrationService) + { + _detectionOrchestrationService = detectionOrchestrationService; + } + + [HttpGet("{detectionId}")] + [SwaggerOperation(Summary = "Gets a Detection for the given id.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the detection for the given id.", typeof(Detection))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status404NotFound, "If the detection for the given id was not found.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetDetectionByIdAsync( + [SwaggerParameter("The desired id.", Required = true)] string detectionId) + { + try + { + var detection = await _detectionOrchestrationService.RetrieveDetectionByIdAsync(detectionId); + + return Ok(detection); + } + catch (Exception exception) + { + if (exception is DetectionOrchestrationValidationException && + exception.InnerException is NotFoundMetadataException) + return NotFound(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is DetectionOrchestrationValidationException || + exception is DetectionOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is DetectionOrchestrationDependencyException || + exception is DetectionOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpGet("bytag/{tag}")] + [SwaggerOperation(Summary = "Gets a list of Detections for the given timeframe and tag.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of detections for the given timeframe and tag.", typeof(DetectionListForTagResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetPaginatedDetectionsForGivenTimeframeAndTagAsync( + [SwaggerParameter("The desired tag(s) (i.e. tag1,tag2 for AND tag1|tag2 for OR).", Required = true)] string tag, + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate, + [SwaggerParameter("The page in the list to request.", Required = true)] int page, + [SwaggerParameter("The page size to request.", Required = true)] int pageSize) + { + try + { + var detectionListForTagResponse = await _detectionOrchestrationService.RetrieveDetectionsForGivenTimeframeAndTagAsync(fromDate, toDate, tag, page, pageSize); + + return Ok(detectionListForTagResponse); + } + catch (Exception exception) + { + if (exception is DetectionOrchestrationValidationException || + exception is DetectionOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is DetectionOrchestrationDependencyException || + exception is DetectionOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + + [HttpGet("byinterestlabel/{interestLabel}")] + [SwaggerOperation(Summary = "Gets a list of Detections for the given interest label.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of detections for the given interest label.", typeof(DetectionListForInterestLabelResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetDetectionsForGivenInterestLabelAsync( + [SwaggerParameter("The desired interest label.", Required = true)] string interestLabel) + { + try + { + var detectionListForInterestLabelResponse = await _detectionOrchestrationService. + RetrieveDetectionsForGivenInterestLabelAsync(interestLabel); + + return Ok(detectionListForInterestLabelResponse); + } + catch (Exception exception) + { + if (exception is DetectionOrchestrationValidationException || + exception is DetectionOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is DetectionOrchestrationDependencyException || + exception is DetectionOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpGet] + [SwaggerOperation(Summary = "Gets a list of Detections for the passed filter conditions.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of detections for the given filter conditions.", typeof(DetectionListResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetPaginatedDetectionsAsync( + [SwaggerParameter("The desired state.", Required = true)] string state, + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate, + [SwaggerParameter("The name of the field to sort by (give examples here).", Required = true)] string sortBy, + [SwaggerParameter("Flag indicating if the sort order should be descending.", Required = true)] bool isDescending, + [SwaggerParameter("The page in the list to request.", Required = true)] int page, + [SwaggerParameter("The page size to request.", Required = true)] int pageSize, + [SwaggerParameter("The name of the location of the Hydrophone (optional).", Required = false)] string location = "") + { + try + { + var detectionListResponse = await _detectionOrchestrationService + .RetrieveFilteredDetectionsAsync(fromDate, toDate, state, sortBy, isDescending, location, page, pageSize); + + return Ok(detectionListResponse); + } + catch (Exception exception) + { + if (exception is DetectionOrchestrationValidationException || + exception is DetectionOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is DetectionOrchestrationDependencyException || + exception is DetectionOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpPut("{detectionId}/moderator")] + [SwaggerOperation(Summary = "Perform Moderator-related update of an existing Detection.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the detection for the given id.", typeof(Detection))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status404NotFound, "If the detection for the given id was not found.")] + [SwaggerResponse(StatusCodes.Status422UnprocessableEntity, "If the update failed for some reason.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [Authorize("Moderators")] + public async ValueTask> PutModeratedInfoAsync( + [SwaggerParameter("The desired id.", Required = true)] string detectionId, + [FromBody][SwaggerParameter("The moderator-related fields to update.", Required = true)] ModerateDetectionRequest request) + { + try + { + var detection = await _detectionOrchestrationService.ModerateDetectionByIdAsync(detectionId, request); + + return Ok(detection); + } + catch (Exception exception) + { + if (exception is DetectionOrchestrationValidationException && + exception.InnerException is NotFoundMetadataException) + return NotFound(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is DetectionOrchestrationValidationException && + (exception.InnerException is DetectionNotDeletedException || + exception.InnerException is DetectionNotInsertedException)) + return UnprocessableEntity(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is DetectionOrchestrationValidationException || + exception is DetectionOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is DetectionOrchestrationDependencyException || + exception is DetectionOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/4CommentsController.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/4CommentsController.cs new file mode 100644 index 00000000..83569577 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/4CommentsController.cs @@ -0,0 +1,80 @@ +namespace OrcaHello.Web.Api.Controllers +{ + [Route("api/comments")] + [SwaggerTag("This controller is responsible for retrieving comments submitted by moderators regarding detections.")] + [ApiController] + public class CommentsController : ControllerBase + { + private readonly ICommentOrchestrationService _commentOrchestrationService; + + public CommentsController(ICommentOrchestrationService commentOrchestrationService) + { + _commentOrchestrationService = commentOrchestrationService; + } + + [HttpGet("negative-unknown")] + [SwaggerOperation(Summary = "Gets a paginated list of comments for negative and unknown Detections for the given timeframe.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of comments for the give timeframe.", typeof(CommentListResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAsync( + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate, + [SwaggerParameter("The page in the list to request.", Required = true)] int page, + [SwaggerParameter("The page size to request.", Required = true)] int pageSize) + { + try + { + var commentListResponse = await _commentOrchestrationService.RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync(fromDate, toDate, page, pageSize); + + return Ok(commentListResponse); + } + catch (Exception exception) + { + if (exception is CommentOrchestrationValidationException || + exception is CommentOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is CommentOrchestrationDependencyException || + exception is CommentOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpGet("positive")] + [SwaggerOperation(Summary = "Gets a paginated list of comments for positive Detections for the given timeframe.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of comments for the give timeframe.", typeof(TagListResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetPaginatedPositiveCommentsForGivenTimeframeAsync( + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate, + [SwaggerParameter("The page in the list to request.", Required = true)] int page, + [SwaggerParameter("The page size to request.", Required = true)] int pageSize) + { + try + { + var commentListResponse = await _commentOrchestrationService.RetrievePositiveCommentsForGivenTimeframeAsync(fromDate, toDate, page, pageSize); + + return Ok(commentListResponse); + } + catch (Exception exception) + { + if (exception is CommentOrchestrationValidationException || + exception is CommentOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is CommentOrchestrationDependencyException || + exception is CommentOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/5MetricsController.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/5MetricsController.cs new file mode 100644 index 00000000..04c605aa --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/5MetricsController.cs @@ -0,0 +1,45 @@ +namespace OrcaHello.Web.Api.Controllers +{ + [Route("api/metrics")] + [SwaggerTag("This controller is responsible for retrieving detection metrics.")] + [ApiController] + public class MetricsController : ControllerBase + { + private readonly IMetricsOrchestrationService _metricsOrchestrationService; + + public MetricsController(IMetricsOrchestrationService metricsOrchestrationService) + { + _metricsOrchestrationService = metricsOrchestrationService; + } + + [HttpGet] + [SwaggerOperation(Summary = "Gets the state metrics for the given timeframe.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the state metrics for the given timeframe.", typeof(MetricsResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetMetricsAsync( + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate) + { + try + { + var metricsResponse = await _metricsOrchestrationService.RetrieveMetricsForGivenTimeframeAsync(fromDate, toDate); + + return Ok(metricsResponse); + } + catch (Exception exception) + { + if (exception is MetricOrchestrationValidationException || + exception is MetricOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is MetricOrchestrationDependencyException || + exception is MetricOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/6ModeratorsController.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/6ModeratorsController.cs new file mode 100644 index 00000000..6dc32175 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/6ModeratorsController.cs @@ -0,0 +1,176 @@ +namespace OrcaHello.Web.Api.Controllers +{ + [Route("api/moderators")] + [SwaggerTag("This controller is responsible for retrieving various kinds of data that are relevant to the moderator role.")] + [ApiController] + public class ModeratorsController : ControllerBase + { + private readonly IModeratorOrchestrationService _moderatorOrchestrationService; + + public ModeratorsController(IModeratorOrchestrationService moderatorOrchestrationService) + { + _moderatorOrchestrationService = moderatorOrchestrationService; + } + + [HttpGet] + [SwaggerOperation(Summary = "Gets a list of unique moderators.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of moderators.", typeof(ModeratorListResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetModeratorsAsync() + { + try + { + var moderatorListResponse = await _moderatorOrchestrationService + .RetrieveModeratorsAsync(); + + return Ok(moderatorListResponse); + } + catch (Exception exception) + { + if (exception is ModeratorOrchestrationValidationException || + exception is ModeratorOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is ModeratorOrchestrationDependencyException || + exception is ModeratorOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpGet("{moderator}/comments/positive")] + [SwaggerOperation(Summary = "Gets a paginated list of comments for positive Detections for the given timeframe and moderator.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of comments for the give timeframe and moderator.", typeof(CommentListForModeratorResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetPaginatedPositiveCommentsForGivenTimeframeAndModeratorAsync( + [SwaggerParameter("The name of the moderator.", Required = true)] string moderator, + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate, + [SwaggerParameter("The page in the list to request.", Required = true)] int page, + [SwaggerParameter("The page size to request.", Required = true)] int pageSize) + { + try + { + var commentListForModeratorResponse = await _moderatorOrchestrationService + .RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(fromDate, toDate, moderator, page, pageSize); + + return Ok(commentListForModeratorResponse); + } + catch (Exception exception) + { + if (exception is ModeratorOrchestrationValidationException || + exception is ModeratorOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is ModeratorOrchestrationDependencyException || + exception is ModeratorOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpGet("{moderator}/comments/negative-unknown")] + [SwaggerOperation(Summary = "Gets a paginated list of comments for negative or unknown Detections for the given timeframe and moderator.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of comments for the give timeframe and moderator.", typeof(CommentListForModeratorResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetPaginatedNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync( + [SwaggerParameter("The name of the moderator.", Required = true)] string moderator, + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate, + [SwaggerParameter("The page in the list to request.", Required = true)] int page, + [SwaggerParameter("The page size to request.", Required = true)] int pageSize) + { + try + { + var commentListForModeratorResponse = await _moderatorOrchestrationService + .RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync(fromDate, toDate, moderator, page, pageSize); + + return Ok(commentListForModeratorResponse); + } + catch (Exception exception) + { + if (exception is ModeratorOrchestrationValidationException || + exception is ModeratorOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is ModeratorOrchestrationDependencyException || + exception is ModeratorOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpGet("{moderator}/tags")] + [SwaggerOperation(Summary = "Gets a list of tags for the given timeframe and moderator.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of tags for the give timeframe and moderator.", typeof(CommentListForModeratorResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetTagsForGivenTimeframeAndModeratorAsync( + [SwaggerParameter("The name of the moderator.", Required = true)] string moderator, + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate) + { + try + { + var tagListForModeratorResponse = await _moderatorOrchestrationService + .RetrieveTagsForGivenTimePeriodAndModeratorAsync(fromDate, toDate, moderator); + + return Ok(tagListForModeratorResponse); + } + catch (Exception exception) + { + if (exception is ModeratorOrchestrationValidationException || + exception is ModeratorOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is ModeratorOrchestrationDependencyException || + exception is ModeratorOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpGet("{moderator}/metrics")] + [SwaggerOperation(Summary = "Gets a list of review metrics for the given timeframe and moderator.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of metrics for the give timeframe and moderator.", typeof(MetricsForModeratorResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetMetricsForGivenTimeframeAndModeratorAsync( + [SwaggerParameter("The name of the moderator.", Required = true)] string moderator, + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate) + { + try + { + var metricsForModeratorResponse = await _moderatorOrchestrationService + .RetrieveMetricsForGivenTimeframeAndModeratorAsync(fromDate, toDate, moderator); + + return Ok(metricsForModeratorResponse); + } + catch (Exception exception) + { + if (exception is ModeratorOrchestrationValidationException || + exception is ModeratorOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is ModeratorOrchestrationDependencyException || + exception is ModeratorOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/7TagsController.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/7TagsController.cs new file mode 100644 index 00000000..41c20d95 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/7TagsController.cs @@ -0,0 +1,132 @@ +namespace OrcaHello.Web.Api.Controllers +{ + [Route("api/tags")] + [SwaggerTag("This controller is responsible for retrieving and curating tags submitted by moderators against reviewed detections.")] + [ApiController] + public class TagsController : ControllerBase + { + private readonly ITagOrchestrationService _tagOrchestrationService; + + public TagsController(ITagOrchestrationService tagOrchestrationService) + { + _tagOrchestrationService = tagOrchestrationService; + } + + [HttpGet] + [SwaggerOperation(Summary = "Gets a list of all unique tags.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of tags.", typeof(TagListResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetAllTagsAsync() + { + try + { + var tagListResponse = await _tagOrchestrationService.RetrieveAllTagsAsync(); + + return Ok(tagListResponse); + } + catch (Exception exception) + { + if (exception is TagOrchestrationValidationException || + exception is TagOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is TagOrchestrationDependencyException || + exception is TagOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpGet("bytimeframe")] + [SwaggerOperation(Summary = "Gets a list of tags for the given timeframe.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of tags for the give timeframe.", typeof(TagListForTimeframeResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetTagsForGivenTimeframeAsync( + [SwaggerParameter("The start date of the search (MM/DD/YYYY).", Required = true)] DateTime? fromDate, + [SwaggerParameter("The end date of the search (MM/DD/YYYY).", Required = true)] DateTime? toDate) + { + try + { + var tagListForTimeframeResponse = await _tagOrchestrationService.RetrieveTagsForGivenTimePeriodAsync(fromDate, toDate); + + return Ok(tagListForTimeframeResponse); + } + catch(Exception exception) + { + if (exception is TagOrchestrationValidationException || + exception is TagOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if(exception is TagOrchestrationDependencyException || + exception is TagOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpDelete("{tag}")] + [SwaggerOperation(Summary = "Delete all occurences of the tag from all detections.")] + [SwaggerResponse(StatusCodes.Status200OK, "The tag was deleted from all detections.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [Authorize("Moderators")] + public async ValueTask> DeleteTagFromAllDetectionsAsync( + [SwaggerParameter("The tag to remove.", Required = true)] string tag) + { + try + { + var result = await _tagOrchestrationService.RemoveTagFromAllDetectionsAsync(tag); + + return Ok(result); + } + catch (Exception exception) + { + if (exception is TagOrchestrationValidationException || + exception is TagOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is TagOrchestrationDependencyException || + exception is TagOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpPut("replace")] + [SwaggerOperation(Summary = "Replace the occurence of a tag in all detections with another tag.")] + [SwaggerResponse(StatusCodes.Status200OK, "The tag was replaced in all detections.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [Authorize("Moderators")] + public async ValueTask> ReplaceTagInAllDetectionsAsync( + [SwaggerParameter("The old tag to replace.", Required = true)] string oldTag, + [SwaggerParameter("The new tag to replace it with.", Required = true)] string newTag) + { + try + { + var result = await _tagOrchestrationService.ReplaceTagInAllDetectionsAsync(oldTag, newTag); + + return Ok(result); + } + catch (Exception exception) + { + if (exception is TagOrchestrationValidationException || + exception is TagOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is TagOrchestrationDependencyException || + exception is TagOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/8InterestLabelsController.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/8InterestLabelsController.cs new file mode 100644 index 00000000..74ba1233 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Controllers/8InterestLabelsController.cs @@ -0,0 +1,118 @@ +namespace OrcaHello.Web.Api.Controllers +{ + [Route("api/interestlabels")] + [SwaggerTag("This controller is responsible for retrieving and curating interest labels submitted by moderators against reviewed detections.")] + [ApiController] + public class InterestLabelsController : ControllerBase + { + private readonly IInterestLabelOrchestrationService _interestLabelOrchestrationService; + + public InterestLabelsController(IInterestLabelOrchestrationService interestLabelOrchestrationService) + { + _interestLabelOrchestrationService = interestLabelOrchestrationService; + } + + [HttpGet] + [SwaggerOperation(Summary = "Gets a list of all unique interest labels.")] + [SwaggerResponse(StatusCodes.Status200OK, "Returns the list of interest labels.", typeof(InterestLabelListResponse))] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [AllowAnonymous] + public async ValueTask> GetAllInterestLabelsAsync() + { + try + { + var interestLabelListResponse = await _interestLabelOrchestrationService.RetrieveAllInterestLabelsAsync(); + + return Ok(interestLabelListResponse); + } + catch (Exception exception) + { + if (exception is InterestLabelOrchestrationValidationException || + exception is InterestLabelOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is InterestLabelOrchestrationDependencyException || + exception is InterestLabelOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpDelete("{id}")] + [SwaggerOperation(Summary = "Deletes the interest label for the passed detection.")] + [SwaggerResponse(StatusCodes.Status200OK, "The interest label was removed from the detection.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status404NotFound, "If the target metadata could not be found to update.")] + [SwaggerResponse(StatusCodes.Status422UnprocessableEntity, "If the update failed for some reason.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [Authorize("Moderators")] + public async ValueTask> RemoveInterestLabelFromDetectionAsync( + [SwaggerParameter("The detection id.", Required = true)] string id) + { + try + { + var result = await _interestLabelOrchestrationService.RemoveInterestLabelFromDetectionAsync(id); + + return Ok(result); + } + catch (Exception exception) + { + if (exception is InterestLabelOrchestrationValidationException && + exception.InnerException is NotFoundMetadataException) + return NotFound(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is InterestLabelOrchestrationValidationException && + (exception.InnerException is DetectionNotDeletedException || + exception.InnerException is DetectionNotInsertedException)) + return UnprocessableEntity(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is InterestLabelOrchestrationValidationException || + exception is InterestLabelOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is InterestLabelOrchestrationDependencyException || + exception is InterestLabelOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + + [HttpPut("{id}")] + [SwaggerOperation(Summary = "Adds the interest label to the passed detection.")] + [SwaggerResponse(StatusCodes.Status200OK, "The interest label was added to the detection.")] + [SwaggerResponse(StatusCodes.Status400BadRequest, "If the request was malformed (missing parameters).")] + [SwaggerResponse(StatusCodes.Status404NotFound, "If the target metadata could not be found to update.")] + [SwaggerResponse(StatusCodes.Status500InternalServerError, "If there is an internal error reading or processing data from the data source.")] + [Authorize("Moderators")] + public async ValueTask> AddInterestLabelToDetectionAsync( + [SwaggerParameter("The detection id.", Required = true)] string id, + [SwaggerParameter("The interest label to add.", Required = true)] string interestLabel) + { + try + { + var result = await _interestLabelOrchestrationService.AddInterestLabelToDetectionAsync(id, interestLabel); + + return Ok(result); + } + catch (Exception exception) + { + if (exception is InterestLabelOrchestrationValidationException && + exception.InnerException is NotFoundMetadataException) + return NotFound(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is InterestLabelOrchestrationValidationException || + exception is InterestLabelOrchestrationDependencyValidationException) + return BadRequest(ValidatorUtilities.GetInnerMessage(exception)); + + if (exception is InterestLabelOrchestrationDependencyException || + exception is InterestLabelOrchestrationServiceException) + return Problem(exception.Message); + + return Problem(exception.Message); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/CommentOrchestations/CommentOrchestrationExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/CommentOrchestations/CommentOrchestrationExceptions.cs new file mode 100644 index 00000000..8263438f --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/CommentOrchestations/CommentOrchestrationExceptions.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class InvalidCommentOrchestrationException : Exception + { + public InvalidCommentOrchestrationException() { } + + public InvalidCommentOrchestrationException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class CommentOrchestrationValidationException : Exception + { + public CommentOrchestrationValidationException() { } + + public CommentOrchestrationValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class CommentOrchestrationDependencyException : Exception + { + public CommentOrchestrationDependencyException() { } + + public CommentOrchestrationDependencyException(Exception innerException) + : base($"CommentOrchestrationDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class CommentOrchestrationDependencyValidationException : Exception + { + public CommentOrchestrationDependencyValidationException() { } + + public CommentOrchestrationDependencyValidationException(Exception innerException) + : base($"CommentOrchestrationDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class CommentOrchestrationServiceException : Exception + { + public CommentOrchestrationServiceException() { } + + public CommentOrchestrationServiceException(Exception innerException) + : base($"Internal or unknown system failure (CommentOrchestrationServiceException): {innerException.Message}", innerException) { } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/AppSettings.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/AppSettings.cs new file mode 100644 index 00000000..df75a2c8 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/AppSettings.cs @@ -0,0 +1,14 @@ +namespace OrcaHello.Web.Api.Models.Configurations +{ + [ExcludeFromCodeCoverage] + public class AppSettings + { + public const string Section = "AppSettings"; + public string CosmosConnectionString { get; set; } + public string DetectionsDatabaseName { get; set; } + public string MetadataContainerName { get; set; } + public string AllowedOrigin { get; set; } + public string HydrophoneFeedUrl { get; set; } + public AzureAd AzureAd { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/AzureAd.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/AzureAd.cs new file mode 100644 index 00000000..6aadb471 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/AzureAd.cs @@ -0,0 +1,14 @@ +namespace OrcaHello.Web.Api.Models.Configurations +{ + [ExcludeFromCodeCoverage] + public class AzureAd + { + public string Instance { get; set; } + public string Domain { get; set; } + public string TenantId { get; set; } + public string ClientId { get; set; } + public string Scopes { get; set; } + public string CallbackPath { get; set; } + public string ModeratorGroupId { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/ComponentDependencyInjector.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/ComponentDependencyInjector.cs new file mode 100644 index 00000000..ec5db8f2 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Configurations/ComponentDependencyInjector.cs @@ -0,0 +1,137 @@ +using OrcaHello.Web.Api.Brokers.Hydrophones; + +namespace OrcaHello.Web.Api.Models.Configurations +{ + /// + /// Static class for performing dependency injection of the various components + /// of the environment. + /// + [ExcludeFromCodeCoverage] + public static class ComponentDependencyInjector + { + public static void AddBrokers(WebApplicationBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + } + + public static void AddFoundationServices(WebApplicationBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + } + + public static void AddOrchestrationServices(WebApplicationBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + } + + // Allow CORS access from anywhere + public static void ConfigureCors(WebApplicationBuilder builder) + { + builder.Services.AddCors(o => o.AddPolicy("AllowAnyOrigin", + builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + })); + } + + // Implement Jwt Authentication + public static void ConfigureJwtAuthentication(WebApplicationBuilder builder) + { + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AppSettings:AzureAd")); + + builder.Services.Configure( + JwtBearerDefaults.AuthenticationScheme, options => + { + options.TokenValidationParameters.NameClaimType = "name"; + }); + } + + // Implement Moderator Authorization policy + public static void ConfigureModeratorPolicy(WebApplicationBuilder builder, AppSettings appSettings) + { + builder.Services.AddAuthorization(options => + { + var moderatorGroupId = !string.IsNullOrWhiteSpace(appSettings.AzureAd.ModeratorGroupId) + ? appSettings.AzureAd.ModeratorGroupId : Guid.NewGuid().ToString(); + + options.AddPolicy("Moderators", + policy => policy.RequireClaim("groups", moderatorGroupId)); + }); + } + + // Set up Swagger so users can use OAuth to authenticate against it + public static void ConfigureSwagger(WebApplicationBuilder builder, AppSettings appSettings) + { + var instance = !string.IsNullOrWhiteSpace(appSettings.AzureAd.Instance) ? + appSettings.AzureAd.Instance : string.Empty; + var tenantId = !string.IsNullOrWhiteSpace(appSettings.AzureAd.TenantId) ? + appSettings.AzureAd.TenantId : Guid.NewGuid().ToString(); + var clientId = !string.IsNullOrWhiteSpace(appSettings.AzureAd.ClientId) ? + appSettings.AzureAd.ClientId : Guid.NewGuid().ToString(); + var scopes = !string.IsNullOrWhiteSpace(appSettings.AzureAd.Scopes) ? + appSettings.AzureAd.Scopes : string.Empty; + + var authUrl = $"{instance}/{tenantId}/oauth2/v2.0/authorize"; + var tokenUrl = $"{instance}/{tenantId}/oauth2/v2.0/token"; + + builder.Services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "AI For Orcas API", + Version = "v1.2", + Description = "REST API for accessing and updating AI For Orcas detections, tags, and metrics." + }); + + c.EnableAnnotations(); + + // Set the comments path for the controllers. + var baseXmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var baseXmlPath = Path.Combine(AppContext.BaseDirectory, baseXmlFile); + c.IncludeXmlComments(baseXmlPath, includeControllerXmlComments: true); + + c.AddSecurityDefinition("OAuth2", new OpenApiSecurityScheme + { + Description = "OAuth2.0 Auth with Proof Key for Code Exchange (PKCE)", + Name = "OAuth2", + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows + { + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri(authUrl), + TokenUrl = new Uri(tokenUrl), + Scopes = new Dictionary + { + { $"api://{clientId}/{scopes}", "For accessing endpoints requiring authorization." } + } + } + } + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "OAuth2" } + }, + new[] { scopes } + } + }); + }); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionNotDeletedException.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionNotDeletedException.cs new file mode 100644 index 00000000..0bd77608 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionNotDeletedException.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class DetectionNotDeletedException : Exception + { + public DetectionNotDeletedException(string id) + : base(message: $"Could not delete metadata with id: {id}.") { } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionNotInsertedException.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionNotInsertedException.cs new file mode 100644 index 00000000..f43b8c1c --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionNotInsertedException.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class DetectionNotInsertedException : Exception + { + public DetectionNotInsertedException(string id) + : base(message: $"Could not insert metadata with id: {id}.") { } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionOrchestrationExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionOrchestrationExceptions.cs new file mode 100644 index 00000000..39fd2f39 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/DetectionOrchestrationExceptions.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class InvalidDetectionOrchestrationException : Exception + { + public InvalidDetectionOrchestrationException() { } + + public InvalidDetectionOrchestrationException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class DetectionOrchestrationValidationException : Exception + { + public DetectionOrchestrationValidationException() { } + + public DetectionOrchestrationValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class DetectionOrchestrationDependencyException : Exception + { + public DetectionOrchestrationDependencyException() { } + + public DetectionOrchestrationDependencyException(Exception innerException) + : base($"DetectionOrchestrationDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class DetectionOrchestrationDependencyValidationException : Exception + { + public DetectionOrchestrationDependencyValidationException() { } + + public DetectionOrchestrationDependencyValidationException(Exception innerException) + : base($"DetectionOrchestrationDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class DetectionOrchestrationServiceException : Exception + { + public DetectionOrchestrationServiceException() { } + + public DetectionOrchestrationServiceException(Exception innerException) + : base($"Internal or unknown system failure (DetectionOrchestrationServiceException): {innerException.Message}", innerException) { } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/NullModerateDetectionRequestException.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/NullModerateDetectionRequestException.cs new file mode 100644 index 00000000..f055465a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/DetectionOrchestrations/NullModerateDetectionRequestException.cs @@ -0,0 +1,8 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class NullModerateDetectionRequestException : Exception + { + public NullModerateDetectionRequestException() : base(message: "The request is null.") { } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/HydrophoneOrchestrations/HydrophoneOrchestrationExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/HydrophoneOrchestrations/HydrophoneOrchestrationExceptions.cs new file mode 100644 index 00000000..8a7138b8 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/HydrophoneOrchestrations/HydrophoneOrchestrationExceptions.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class InvalidHydrophoneOrchestrationException : Exception + { + public InvalidHydrophoneOrchestrationException() { } + + public InvalidHydrophoneOrchestrationException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneOrchestrationValidationException : Exception + { + public HydrophoneOrchestrationValidationException() { } + + public HydrophoneOrchestrationValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneOrchestrationDependencyException : Exception + { + public HydrophoneOrchestrationDependencyException() { } + + public HydrophoneOrchestrationDependencyException(Exception innerException) + : base($"HydrophoneOrchestrationDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneOrchestrationDependencyValidationException : Exception + { + public HydrophoneOrchestrationDependencyValidationException() { } + + public HydrophoneOrchestrationDependencyValidationException(Exception innerException) + : base($"HydrophoneOrchestrationDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneOrchestrationServiceException : Exception + { + public HydrophoneOrchestrationServiceException() { } + + public HydrophoneOrchestrationServiceException(Exception innerException) + : base($"Internal or unknown system failure (HydrophoneOrchestrationServiceException): {innerException.Message}", innerException) { } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/HydrophoneData.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/HydrophoneData.cs new file mode 100644 index 00000000..011211c2 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/HydrophoneData.cs @@ -0,0 +1,68 @@ +namespace OrcaHello.Web.Api.Models +{ + /// + /// Hydrophone data retrieved from the provider service. + /// + [ExcludeFromCodeCoverage] + public class HydrophoneRootObject + { + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public List Data { get; set; } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneData + { + [JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)] + public HydrophoneAttributes Attributes { get; set; } + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; set; } + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneAttributes + { + [JsonProperty("image_url", NullValueHandling = NullValueHandling.Ignore)] + public string ImageUrl { get; set; } + [JsonProperty("intro_html", NullValueHandling = NullValueHandling.Ignore)] + public string IntroHtml { get; set; } + [JsonProperty("location_point", NullValueHandling = NullValueHandling.Ignore)] + public HydrophoneLocationPoint LocationPoint { get; set; } + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + [JsonProperty("node_name", NullValueHandling = NullValueHandling.Ignore)] + public string NodeName { get; set; } + [JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] + public string Slug { get; set; } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneLocationPoint + { + [JsonProperty("coordinates", NullValueHandling = NullValueHandling.Ignore)] + public List Coordinates { get; set; } + [JsonProperty("crs", NullValueHandling = NullValueHandling.Ignore)] + public HydrophoneCrs Crs { get; set; } + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneCrs + { + [JsonProperty("properties", NullValueHandling = NullValueHandling.Ignore)] + public HydrophoneProperties Properties { get; set; } + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneProperties + { + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + } + +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/HydrophoneExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/HydrophoneExceptions.cs new file mode 100644 index 00000000..d09faaf2 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/HydrophoneExceptions.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class InvalidHydrophoneException : Exception + { + public InvalidHydrophoneException() { } + + public InvalidHydrophoneException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneValidationException : Exception + { + public HydrophoneValidationException() { } + + public HydrophoneValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneDependencyException : Exception + { + public HydrophoneDependencyException() { } + + public HydrophoneDependencyException(Exception innerException) + : base($"HydrophoneDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneDependencyValidationException : Exception + { + public HydrophoneDependencyValidationException() { } + + public HydrophoneDependencyValidationException(Exception innerException) + : base($"HydrophoneDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class HydrophoneServiceException : Exception + { + public HydrophoneServiceException() { } + + public HydrophoneServiceException(Exception innerException) + : base($"Internal or unknown system failure (HydrophoneServiceException): {innerException.Message}", innerException) { } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/QueryableHydrophoneData.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/QueryableHydrophoneData.cs new file mode 100644 index 00000000..a4ce3cf6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Hydrophones/QueryableHydrophoneData.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class QueryableHydrophoneData + { + public IQueryable QueryableRecords { get; set; } + public int TotalCount { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/InterestLabelOrchestrations/InterestLabelOrchestrationExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/InterestLabelOrchestrations/InterestLabelOrchestrationExceptions.cs new file mode 100644 index 00000000..a5d4f525 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/InterestLabelOrchestrations/InterestLabelOrchestrationExceptions.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class InvalidInterestLabelOrchestrationException : Exception + { + public InvalidInterestLabelOrchestrationException() { } + + public InvalidInterestLabelOrchestrationException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class InterestLabelOrchestrationValidationException : Exception + { + public InterestLabelOrchestrationValidationException() { } + + public InterestLabelOrchestrationValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class InterestLabelOrchestrationDependencyException : Exception + { + public InterestLabelOrchestrationDependencyException() { } + + public InterestLabelOrchestrationDependencyException(Exception innerException) + : base($"InterestLabelOrchestrationDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class InterestLabelOrchestrationDependencyValidationException : Exception + { + public InterestLabelOrchestrationDependencyValidationException() { } + + public InterestLabelOrchestrationDependencyValidationException(Exception innerException) + : base($"InterestLabelOrchestrationDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class InterestLabelOrchestrationServiceException : Exception + { + public InterestLabelOrchestrationServiceException() { } + + public InterestLabelOrchestrationServiceException(Exception innerException) + : base($"Internal or unknown system failure (InterestLabelOrchestrationServiceException): {innerException.Message}", innerException) { } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/ListMetadataAndCount.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/ListMetadataAndCount.cs new file mode 100644 index 00000000..8e9e11ea --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/ListMetadataAndCount.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class ListMetadataAndCount + { + public List PaginatedRecords { get; set; } = new List(); + public int TotalCount { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/Metadata.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/Metadata.cs new file mode 100644 index 00000000..a734117c --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/Metadata.cs @@ -0,0 +1,172 @@ +namespace OrcaHello.Web.Api.Models +{ + /// + /// Metadata collected from a hydrophone sampling that might contain whale sounds. + /// + [ExcludeFromCodeCoverage] + public class Metadata + { + /// + /// The metadata's generated unique Id. + /// + /// 00000000-0000-0000-0000-000000000000 + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; set; } + + /// + /// The metadata's moderation state (Unreviewed, Positive, Negative, Unknown). + /// + /// Positive + [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] + public string State { get; set; } + + /// + /// The name of the hydrophone where the metadata was collected. + /// + /// Haro Strait + [JsonProperty("locationName", NullValueHandling = NullValueHandling.Ignore)] + public string LocationName { get; set; } + + /// + /// URI of the metadata's audio file (.wav) in blob storage. + /// + /// https://storagesite.blob.core.windows.net/audiowavs/audiofilename.wav + [JsonProperty("audioUri", NullValueHandling = NullValueHandling.Ignore)] + public string AudioUri { get; set; } + + /// + /// URI of the metadata's image file (.png) in blob storage. + /// + /// https://storagesite.blob.core.windows.net/spectrogramspng/imagefilename.png + [JsonProperty("imageUri", NullValueHandling = NullValueHandling.Ignore)] + public string ImageUri { get; set; } + + /// + /// Date and time of when the detection occurred. + /// + /// 2020-09-30T11:03:56.057346Z + [JsonProperty("timestamp")] + public DateTime Timestamp { get; set; } + + /// + /// Calculated average confidence that the metadata contains a whale sound. + /// + /// 84.39 + [JsonProperty("whaleFoundConfidence")] + public decimal WhaleFoundConfidence { get; set; } + + /// + /// Detailed location of the hydrophone that collected the metadata. + /// + [JsonProperty("location")] + public Location Location { get; set; } + + /// + /// List of sections within the collected audio that might contain whale sounds. + /// + [JsonProperty("predictions")] + public List Predictions { get; set; } = new List(); + + /// + /// Any text comments entered by the human moderator during review. + /// + /// Clear whale sounds detected. + [JsonProperty("comments", NullValueHandling = NullValueHandling.Ignore)] + public string Comments { get; set; } + + /// + /// Date and time of when the metadata was reviewed by the human moderator. + /// + /// 2020-09-30T11:03:56Z + [JsonProperty("dateModerated", NullValueHandling = NullValueHandling.Ignore)] + public string DateModerated { get; set; } + + /// + /// Identity of the human moderator (User Principal Name for AzureAD) performing the review. + /// + /// user@gmail.com + [JsonProperty("moderator", NullValueHandling = NullValueHandling.Ignore)] + public string Moderator { get; set; } + + /// + /// Any descriptive tags entered by the moderator during the review. + /// + /// S7 and S10 + [JsonProperty("tags")] + public List Tags { get; set; } = new List(); + + /// + /// A flag indicating that the metadata might be a special item of interest for the public facing pages. + /// + /// other whales + [JsonProperty("interestLabel", NullValueHandling = NullValueHandling.Ignore)] + public string InterestLabel { get; set; } + } + + /// + /// Geographical location of the hydrophone that collected the metadata. + /// + [ExcludeFromCodeCoverage] + public class Location + { + /// + /// The id of the hydrophone location. + /// + /// hydrophone1 + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; set; } + + /// + /// Name of the hydrophone location. + /// + /// Haro Strait + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + /// + /// Longitude of the hydrophone's location. + /// + /// -123.2166658 + [JsonProperty("longitude")] + public double Longitude { get; set; } + + /// + /// Latitude of the hydrophone's location. + /// + /// 48.5499978 + [JsonProperty("latitude")] + public double Latitude { get; set; } + } + + [ExcludeFromCodeCoverage] + public class Prediction + { + /// + /// Unique identifier (within the audio file) of the annotation. + /// + /// 1 + [JsonProperty("id")] + public int Id { get; set; } + + /// + /// Start time (within the audio file) of the annotation as measured in seconds. + /// + /// 35 + [JsonProperty("startTime")] + public decimal StartTime { get; set; } + + /// + /// Duration (within the audio file) of the annotation as measured in seconds. + /// + /// 37.5 + [JsonProperty("duration")] + public decimal Duration { get; set; } + + /// + /// Calculated confidence that the annotation contains a whale sound. + /// + /// 84.39 + [JsonProperty("confidence")] + public decimal Confidence { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetadataExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetadataExceptions.cs new file mode 100644 index 00000000..751c9e69 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetadataExceptions.cs @@ -0,0 +1,60 @@ +namespace OrcaHello.Web.Api.Models +{ + + [ExcludeFromCodeCoverage] + public class NotFoundMetadataException : Exception + { + public NotFoundMetadataException(string id) + : base(message: $"Couldn't find metadata with id: {id}.") { } + } + + [ExcludeFromCodeCoverage] + public class NullMetadataException : Exception + { + public NullMetadataException() : base(message: "The metadata is null.") { } + } + + [ExcludeFromCodeCoverage] + public class InvalidMetadataException : Exception + { + public InvalidMetadataException() { } + + public InvalidMetadataException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class MetadataValidationException : Exception + { + public MetadataValidationException() { } + + public MetadataValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class MetadataDependencyException : Exception + { + public MetadataDependencyException() { } + + public MetadataDependencyException(Exception innerException) + : base($"MetadataDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class MetadataDependencyValidationException : Exception + { + public MetadataDependencyValidationException() { } + + public MetadataDependencyValidationException(Exception innerException) + : base($"MetadataDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class MetadataServiceException : Exception + { + public MetadataServiceException() { } + + public MetadataServiceException(Exception innerException) + : base($"Internal or unknown system failure (MetadataServiceException): {innerException.Message}", innerException) { } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetricResult.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetricResult.cs new file mode 100644 index 00000000..629e2017 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetricResult.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class MetricResult + { + public string State { get; set; } + public int Count { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetricsSummaryForTimeframe.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetricsSummaryForTimeframe.cs new file mode 100644 index 00000000..2404613d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/MetricsSummaryForTimeframe.cs @@ -0,0 +1,16 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class MetricsSummaryForTimeframe + { + public IQueryable QueryableRecords { get; set; } + public DateTime FromDate { get; set; } + public DateTime ToDate { get; set; } + } + + [ExcludeFromCodeCoverage] + public class MetricsSummaryForTimeframeAndModerator : MetricsSummaryForTimeframe + { + public string Moderator { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableInterstLabels.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableInterstLabels.cs new file mode 100644 index 00000000..205d9fbc --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableInterstLabels.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class QueryableInterestLabels + { + public IQueryable QueryableRecords { get; set; } + public int TotalCount { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableMetadata.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableMetadata.cs new file mode 100644 index 00000000..5daeec88 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableMetadata.cs @@ -0,0 +1,51 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class QueryableMetadata + { + public IQueryable QueryableRecords { get; set; } + public int TotalCount { get; set; } + } + + [ExcludeFromCodeCoverage] + public class QueryableMetadataForTimeframeAndTag : QueryableMetadata + { + public DateTime FromDate { get; set; } + public DateTime ToDate { get; set; } + public string Tag { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + } + + [ExcludeFromCodeCoverage] + public class QueryableMetadataForTimeframe : QueryableMetadata + { + public DateTime FromDate { get; set; } + public DateTime ToDate { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + } + + [ExcludeFromCodeCoverage] + public class QueryableMetadataForTimeframeAndModerator : QueryableMetadata + { + public DateTime FromDate { get; set; } + public DateTime ToDate { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + public string Moderator { get; set; } + } + + [ExcludeFromCodeCoverage] + public class QueryableMetadataFiltered : QueryableMetadata + { + public DateTime FromDate { get; set; } + public DateTime ToDate { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + public string State { get; set; } + public string SortBy { get; set; } + public string SortOrder { get; set; } + public string Location { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableModerators.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableModerators.cs new file mode 100644 index 00000000..9c5d57c1 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableModerators.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class QueryableModerators + { + public IQueryable QueryableRecords { get; set; } + public int TotalCount { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableTags.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableTags.cs new file mode 100644 index 00000000..1ee8a7c5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/Metadatas/QueryableTags.cs @@ -0,0 +1,24 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class QueryableTags + { + public IQueryable QueryableRecords { get; set; } + public int TotalCount { get; set; } + } + + [ExcludeFromCodeCoverage] + public class QueryableTagsForTimeframe : QueryableTags + { + public DateTime FromDate { get; set; } + public DateTime ToDate { get; set; } + } + + [ExcludeFromCodeCoverage] + public class QueryableTagsForTimeframeAndModerator : QueryableTags + { + public DateTime FromDate { get; set; } + public DateTime ToDate { get; set; } + public string Moderator { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/MetricOrchestrations/MetricOrchestrationExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/MetricOrchestrations/MetricOrchestrationExceptions.cs new file mode 100644 index 00000000..c9bb26a4 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/MetricOrchestrations/MetricOrchestrationExceptions.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class InvalidMetricOrchestrationException : Exception + { + public InvalidMetricOrchestrationException() { } + + public InvalidMetricOrchestrationException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class MetricOrchestrationValidationException : Exception + { + public MetricOrchestrationValidationException() { } + + public MetricOrchestrationValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class MetricOrchestrationDependencyException : Exception + { + public MetricOrchestrationDependencyException() { } + + public MetricOrchestrationDependencyException(Exception innerException) + : base($"MetricOrchestrationDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class MetricOrchestrationDependencyValidationException : Exception + { + public MetricOrchestrationDependencyValidationException() { } + + public MetricOrchestrationDependencyValidationException(Exception innerException) + : base($"MetricOrchestrationDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class MetricOrchestrationServiceException : Exception + { + public MetricOrchestrationServiceException() { } + + public MetricOrchestrationServiceException(Exception innerException) + : base($"Internal or unknown system failure (MetricOrchestrationServiceException): {innerException.Message}", innerException) { } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/ModeratorOrchestrations/ModeratorOrchestrationExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/ModeratorOrchestrations/ModeratorOrchestrationExceptions.cs new file mode 100644 index 00000000..a92809d8 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/ModeratorOrchestrations/ModeratorOrchestrationExceptions.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class InvalidModeratorOrchestrationException : Exception + { + public InvalidModeratorOrchestrationException() { } + + public InvalidModeratorOrchestrationException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class ModeratorOrchestrationValidationException : Exception + { + public ModeratorOrchestrationValidationException() { } + + public ModeratorOrchestrationValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class ModeratorOrchestrationDependencyException : Exception + { + public ModeratorOrchestrationDependencyException() { } + + public ModeratorOrchestrationDependencyException(Exception innerException) + : base($"ModeratorOrchestrationDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class ModeratorOrchestrationDependencyValidationException : Exception + { + public ModeratorOrchestrationDependencyValidationException() { } + + public ModeratorOrchestrationDependencyValidationException(Exception innerException) + : base($"ModeratorOrchestrationDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class ModeratorOrchestrationServiceException : Exception + { + public ModeratorOrchestrationServiceException() { } + + public ModeratorOrchestrationServiceException(Exception innerException) + : base($"Internal or unknown system failure (ModeratorOrchestrationServiceException): {innerException.Message}", innerException) { } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/TagOrchestrations/TagOrchestrationExceptions.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/TagOrchestrations/TagOrchestrationExceptions.cs new file mode 100644 index 00000000..5e799712 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Models/TagOrchestrations/TagOrchestrationExceptions.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Models +{ + [ExcludeFromCodeCoverage] + public class InvalidTagOrchestrationException : Exception + { + public InvalidTagOrchestrationException() { } + + public InvalidTagOrchestrationException(string message) : base(message) { } + } + + [ExcludeFromCodeCoverage] + public class TagOrchestrationValidationException : Exception + { + public TagOrchestrationValidationException() { } + + public TagOrchestrationValidationException(Exception innerException) + : base($"Invalid input: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class TagOrchestrationDependencyException : Exception + { + public TagOrchestrationDependencyException() { } + + public TagOrchestrationDependencyException(Exception innerException) + : base($"TagOrchestrationDependency exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class TagOrchestrationDependencyValidationException : Exception + { + public TagOrchestrationDependencyValidationException() { } + + public TagOrchestrationDependencyValidationException(Exception innerException) + : base($"TagOrchestrationDependencyValidation exception: {innerException.Message}", innerException) { } + } + + [ExcludeFromCodeCoverage] + public class TagOrchestrationServiceException : Exception + { + public TagOrchestrationServiceException() { } + + public TagOrchestrationServiceException(Exception innerException) + : base($"Internal or unknown system failure (TagOrchestrationServiceException): {innerException.Message}", innerException) { } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/OrcaHello.Web.Api.csproj b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/OrcaHello.Web.Api.csproj new file mode 100644 index 00000000..09f33904 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/OrcaHello.Web.Api.csproj @@ -0,0 +1,35 @@ + + + + net7.0 + enable + enable + 84e68496-647e-43b5-9863-dc97f799ecc9 + True + + + + 1701;1702;1591;8618 + + + + 1701;1702;1591;8618 + + + + + + + + + + + + + + + + + + + diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Program.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Program.cs new file mode 100644 index 00000000..86700ed7 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Program.cs @@ -0,0 +1,69 @@ +[ExcludeFromCodeCoverage] +internal class Program +{ + private static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add AppSettings + + var appSettings = new AppSettings(); + builder.Configuration.GetSection("AppSettings").Bind(appSettings); + builder.Services.AddSingleton(appSettings); + + // Add services to the container + + ComponentDependencyInjector.AddBrokers(builder); + ComponentDependencyInjector.AddFoundationServices(builder); + ComponentDependencyInjector.AddOrchestrationServices(builder); + + // Add authentication/authorization middleware + + ComponentDependencyInjector.ConfigureCors(builder); + ComponentDependencyInjector.ConfigureJwtAuthentication(builder); + ComponentDependencyInjector.ConfigureModeratorPolicy(builder, appSettings); + + // Add Swagger configiration + + ComponentDependencyInjector.ConfigureSwagger(builder, appSettings); + + // Add built-in middleware + + builder.Services.AddControllers().ConfigureApiBehaviorOptions(x => { x.SuppressMapClientErrors = true; }); + builder.Services.AddEndpointsApiExplorer(); + + var app = builder.Build(); + + // Add Swagger to the HTTP request pipeline + + app.UseSwagger(); + + app.UseSwaggerUI(c => + { + var clientId = !string.IsNullOrWhiteSpace(appSettings.AzureAd.ClientId) ? + appSettings.AzureAd.ClientId : Guid.NewGuid().ToString(); + + c.OAuthClientId(clientId); + c.OAuthUsePkce(); + c.OAuthScopeSeparator(" "); + c.DefaultModelsExpandDepth(-1); + }); + + app.UseHttpsRedirection(); + + // Add the CORS policy to the HTTP Request pipeline + + app.UseCors("AllowAnyOrigin"); + + // Add authentication and authorization to the HTTP Request pipeline + + app.UseAuthentication(); + app.UseAuthorization(); + + // Add the API endpoint controllers to the HTTP Request pipeline + + app.MapControllers(); + + app.Run(); + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Properties/launchSettings.json b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Properties/launchSettings.json new file mode 100644 index 00000000..6ccc8596 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51266", + "sslPort": 44386 + } + }, + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/HydrophoneService.TryCatchValueTaskT.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/HydrophoneService.TryCatchValueTaskT.cs new file mode 100644 index 00000000..065e11f4 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/HydrophoneService.TryCatchValueTaskT.cs @@ -0,0 +1,46 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class HydrophoneService + { + public delegate ValueTask ReturningGenericFunction(); + + protected async ValueTask TryCatch(ReturningGenericFunction returningGenericFunction) + { + try + { + return await returningGenericFunction(); + } + catch (Exception exception) + { + if (exception is InvalidHydrophoneException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is HttpRequestException exception1) + { + var statusCode = exception1.StatusCode; + var innerException = new InvalidHydrophoneException($"Error encountered accessing down range service defined by 'HydrophoneFeedUrl' setting: {exception1.Message}"); + + if(statusCode == HttpStatusCode.BadRequest || + statusCode == HttpStatusCode.NotFound) + throw LoggingUtilities.CreateAndLogException(_logger, innerException); + + if (statusCode == HttpStatusCode.Unauthorized || + statusCode == HttpStatusCode.Forbidden || + statusCode == HttpStatusCode.MethodNotAllowed || + statusCode == HttpStatusCode.Conflict || + statusCode == HttpStatusCode.PreconditionFailed || + statusCode == HttpStatusCode.RequestEntityTooLarge || + statusCode == HttpStatusCode.RequestTimeout || + statusCode == HttpStatusCode.ServiceUnavailable || + statusCode == HttpStatusCode.InternalServerError) + throw LoggingUtilities.CreateAndLogException(_logger, innerException); + + throw LoggingUtilities.CreateAndLogException(_logger, innerException); + + } + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/HydrophoneService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/HydrophoneService.cs new file mode 100644 index 00000000..80fdf1dc --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/HydrophoneService.cs @@ -0,0 +1,30 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class HydrophoneService : IHydrophoneService + { + private readonly IHydrophoneBroker _hydrophoneBroker; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + public HydrophoneService() { } + + public HydrophoneService(IHydrophoneBroker hydrophoneBroker, + ILogger logger) + { + _hydrophoneBroker = hydrophoneBroker; + _logger = logger; + } + + public ValueTask RetrieveAllHydrophonesAsync() => + TryCatch(async () => + { + List hydrophoneList = await _hydrophoneBroker.GetFeedsAsync(); + + return new QueryableHydrophoneData + { + QueryableRecords = hydrophoneList.AsQueryable(), + TotalCount = hydrophoneList.Count + }; + }); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/IHydrophoneService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/IHydrophoneService.cs new file mode 100644 index 00000000..b59460e6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Hydrophones/IHydrophoneService.cs @@ -0,0 +1,7 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface IHydrophoneService + { + ValueTask RetrieveAllHydrophonesAsync(); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/IMetadataService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/IMetadataService.cs new file mode 100644 index 00000000..d814fdd2 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/IMetadataService.cs @@ -0,0 +1,30 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface IMetadataService + { + ValueTask RetrieveModeratorsAsync(); + ValueTask RetrieveTagsForGivenTimePeriodAsync(DateTime fromDate, DateTime toDate); + ValueTask RetrieveTagsForGivenTimePeriodAndModeratorAsync(DateTime fromDate, DateTime toDate, string moderator); + ValueTask RetrieveMetadataForGivenTimeframeAndTagAsync(DateTime fromDate, DateTime toDate, string tag, int page, int pageSize); + ValueTask RetrievePositiveMetadataForGivenTimeframeAsync(DateTime fromDate, DateTime toDate, int page, int pageSize); + ValueTask RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync(DateTime fromDate, DateTime toDate, + string moderator, int page, int pageSize); + ValueTask RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync(DateTime fromDate, DateTime toDate, int page, int pageSize); + ValueTask RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync(DateTime fromDate, DateTime toDate, + string moderator, int page, int pageSize); + ValueTask RetrieveUnreviewedMetadataForGivenTimeframeAsync(DateTime fromDate, DateTime toDate, int page, int pageSize); + ValueTask RetrieveMetricsForGivenTimeframeAsync(DateTime fromDate, DateTime toDate); + ValueTask RetrieveMetricsForGivenTimeframeAndModeratorAsync(DateTime fromDate, DateTime toDate, + string moderator); + ValueTask RetrievePaginatedMetadataAsync(string state, DateTime fromDate, DateTime toDate, string sortBy, + bool isDescending, string location, int page, int pageSize); + ValueTask RetrieveMetadataByIdAsync(string id); + ValueTask RemoveMetadataByIdAndStateAsync(string id, string state); + ValueTask AddMetadataAsync(Metadata metadata); + ValueTask RetrieveMetadataForTagAsync(string tag); + ValueTask UpdateMetadataAsync(Metadata metadata); + ValueTask RetrieveAllTagsAsync(); + ValueTask RetrieveMetadataForInterestLabelAsync(string interestLabel); + ValueTask RetrieveAllInterestLabelsAsync(); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.Guards.cs new file mode 100644 index 00000000..4b42b942 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.Guards.cs @@ -0,0 +1,92 @@ +using System.Text.RegularExpressions; + +namespace OrcaHello.Web.Api.Services +{ + public partial class MetadataService + { + protected void Validate(DateTime date, string propertyName) + { + if (ValidatorUtilities.IsInvalid(date)) + throw new InvalidMetadataException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void Validate(string propertyValue, string propertyName) + { + if (ValidatorUtilities.IsInvalid(propertyValue)) + throw new InvalidMetadataException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void ValidateDatesAreWithinRange(DateTime fromDate, DateTime toDate) + { + if (toDate < fromDate) + throw new InvalidMetadataException("'toDate' must be after 'fromDate'."); + } + + protected void ValidateMetadataOnCreate(Metadata metadata) + { + ValidateMetadataIsNotNull(metadata); + + // TODO: Are there any other required fields + + switch(metadata) + { + case { } when ValidatorUtilities.IsInvalid(metadata.Id): + throw new InvalidMetadataException(LoggingUtilities.MissingRequiredProperty(nameof(metadata.Id))); + + case { } when ValidatorUtilities.IsInvalid(metadata.State): + throw new InvalidMetadataException(LoggingUtilities.MissingRequiredProperty(nameof(metadata.State))); + + case { } when ValidatorUtilities.IsInvalid(metadata.LocationName): + throw new InvalidMetadataException(LoggingUtilities.MissingRequiredProperty(nameof(metadata.LocationName))); + } + } + + protected void ValidateMetadataOnUpdate(Metadata metadata) + { + ValidateMetadataIsNotNull(metadata); + + // TODO: Are there any other required fields + + switch (metadata) + { + case { } when ValidatorUtilities.IsInvalid(metadata.Id): + throw new InvalidMetadataException(LoggingUtilities.MissingRequiredProperty(nameof(metadata.Id))); + + case { } when ValidatorUtilities.IsInvalid(metadata.State): + throw new InvalidMetadataException(LoggingUtilities.MissingRequiredProperty(nameof(metadata.State))); + + case { } when ValidatorUtilities.IsInvalid(metadata.LocationName): + throw new InvalidMetadataException(LoggingUtilities.MissingRequiredProperty(nameof(metadata.LocationName))); + } + } + + private static void ValidateMetadataIsNotNull(Metadata metadata) + { + if (metadata is null) + { + throw new NullMetadataException(); + } + } + + protected void ValidateStateIsAcceptable(string state) + { + if (ValidatorUtilities.GetMatchingEnumValue(state, typeof(DetectionState)) == null) + throw new InvalidMetadataException($"'{state}' is not a valid Detection state."); + + } + + protected void ValidateTagContainsOnlyValidCharacters(string tag) + { + string pattern = "[^a-zA-Z0-9,|]"; + Regex regex = new(pattern); + bool hasInvalidChars = regex.IsMatch(tag); + + if (hasInvalidChars) + throw new InvalidMetadataException($"'{tag}' contains one or more invalid characters (use , for AND and use | for OR)."); + + if (tag.Contains(',') && tag.Contains('|')) + throw new InvalidMetadataException($"'{tag}' can only work on a single operator (, or |)."); + + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.TryCatchValueTaskT.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.TryCatchValueTaskT.cs new file mode 100644 index 00000000..15c7c6db --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.TryCatchValueTaskT.cs @@ -0,0 +1,56 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class MetadataService + { + public delegate ValueTask ReturningGenericFunction(); + + protected async ValueTask TryCatch(ReturningGenericFunction returningGenericFunction) + { + try + { + return await returningGenericFunction(); + } + catch (Exception exception) + { + if (exception is InvalidMetadataException || + exception is NullMetadataException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is CosmosException) + { + var statusCode = CosmosUtilities.GetHttpStatusCode(exception); + var cosmosReason = CosmosUtilities.GetReason(exception); + + var innerException = new InvalidMetadataException(cosmosReason); + + if (statusCode == HttpStatusCode.BadRequest || + statusCode == HttpStatusCode.NotFound) + throw LoggingUtilities.CreateAndLogException(_logger, innerException); + + if (statusCode == HttpStatusCode.Unauthorized || + statusCode == HttpStatusCode.Forbidden || + statusCode == HttpStatusCode.MethodNotAllowed || + statusCode == HttpStatusCode.Conflict || + statusCode == HttpStatusCode.PreconditionFailed || + statusCode == HttpStatusCode.RequestEntityTooLarge || + statusCode == HttpStatusCode.RequestTimeout || + statusCode == HttpStatusCode.ServiceUnavailable || + statusCode == HttpStatusCode.InternalServerError) + throw LoggingUtilities.CreateAndLogException(_logger, innerException); + + throw LoggingUtilities.CreateAndLogException(_logger, innerException); + } + + if (exception is ArgumentNullException || + exception is ArgumentException || + exception is HttpRequestException || + exception is AggregateException || + exception is InvalidOperationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + } + } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.cs new file mode 100644 index 00000000..e3df134c --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Foundations/Metadatas/MetadataService.cs @@ -0,0 +1,507 @@ +using Location = OrcaHello.Web.Api.Models.Location; + +namespace OrcaHello.Web.Api.Services +{ + public partial class MetadataService : IMetadataService + { + private readonly IStorageBroker _storageBroker; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + public MetadataService() { } + + public MetadataService(IStorageBroker storageBroker, + ILogger logger) + { + _storageBroker = storageBroker; + _logger = logger; + } + + #region metadata + + public ValueTask RetrievePositiveMetadataForGivenTimeframeAsync(DateTime fromDate, DateTime toDate, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + page = NormalizePage(page); + pageSize = NormalizePageSize(pageSize); + + ValidateDatesAreWithinRange(fromDate, toDate); + + ListMetadataAndCount queryResults = await _storageBroker.GetPositiveMetadataListByTimeframe(fromDate, toDate, page, pageSize); + + return new QueryableMetadataForTimeframe + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + FromDate = fromDate, + ToDate = toDate, + Page = page, + PageSize = pageSize + }; + }); + + public ValueTask RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync(DateTime fromDate, DateTime toDate, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + page = NormalizePage(page); + pageSize = NormalizePageSize(pageSize); + + ValidateDatesAreWithinRange(fromDate, toDate); + + ListMetadataAndCount queryResults = await _storageBroker.GetNegativeAndUnknownMetadataListByTimeframe(fromDate, toDate, page, pageSize); + + return new QueryableMetadataForTimeframe + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + FromDate = fromDate, + ToDate = toDate, + Page = page, + PageSize = pageSize + }; + }); + + public ValueTask RetrieveUnreviewedMetadataForGivenTimeframeAsync(DateTime fromDate, DateTime toDate, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + page = NormalizePage(page); + pageSize = NormalizePageSize(pageSize); + + ValidateDatesAreWithinRange(fromDate, toDate); + + ListMetadataAndCount queryResults = await _storageBroker.GetUnreviewedMetadataListByTimeframe(fromDate, toDate, page, pageSize); + + return new QueryableMetadataForTimeframe + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + FromDate = fromDate, + ToDate = toDate, + Page = page, + PageSize = pageSize + }; + }); + + public ValueTask RetrievePaginatedMetadataAsync(string state, DateTime fromDate, DateTime toDate, string sortBy, bool isDescending, string location, int page, int pageSize) => + TryCatch(async() => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(state, nameof(state)); + Validate(sortBy, nameof(sortBy)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + page = NormalizePage(page); + pageSize = NormalizePageSize(pageSize); + + ValidateStateIsAcceptable(state); + ValidateDatesAreWithinRange(fromDate, toDate); + + string sortOrder = GetSortOrder(isDescending); + string sortByString = GetSortField(sortBy); + + ListMetadataAndCount queryResults = await _storageBroker.GetMetadataListFiltered(state, fromDate, toDate, sortByString, sortOrder, location, page, pageSize); + + return new QueryableMetadataFiltered + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + FromDate = fromDate, + ToDate = toDate, + Page = page, + PageSize = pageSize, + State = state, + SortBy = sortByString, + SortOrder = sortOrder, + Location = location + }; + }); + + public ValueTask RetrieveMetadataByIdAsync(string id) => + TryCatch(async () => + { + Validate(id, nameof(id)); + + return await _storageBroker.GetMetadataById(id); + }); + + public ValueTask RemoveMetadataByIdAndStateAsync(string id, string state) => + TryCatch(async () => + { + Validate(id, nameof(id)); + Validate(state, nameof(state)); + ValidateStateIsAcceptable(state); + + return await _storageBroker.DeleteMetadataByIdAndState(id, state); + }); + + public ValueTask AddMetadataAsync(Metadata metadata) => + TryCatch(async () => + { + ValidateMetadataOnCreate(metadata); + + return await _storageBroker.InsertMetadata(metadata); + }); + + public ValueTask RetrieveMetadataForTagAsync(string tag) => + TryCatch(async () => + { + Validate(tag, nameof(tag)); + + ListMetadataAndCount queryResults = await _storageBroker.GetAllMetadataListByTag(tag); + + return new QueryableMetadata + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + }; + }); + + public ValueTask RetrieveMetadataForInterestLabelAsync(string interestLabel) => + TryCatch(async () => + { + Validate(interestLabel, nameof(interestLabel)); + + ListMetadataAndCount queryResults = await _storageBroker.GetAllMetadataListByInterestLabel(interestLabel); + + return new QueryableMetadata + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + }; + }); + + public ValueTask UpdateMetadataAsync(Metadata metadata) => + TryCatch(async () => + { + ValidateMetadataOnUpdate(metadata); + + return await _storageBroker.UpdateMetadataInPartition(metadata); + }); + + #endregion + + #region moderator + + public ValueTask RetrieveModeratorsAsync() => + TryCatch(async () => + { + List moderatorList = await _storageBroker.GetModeratorList(); + + return new QueryableModerators + { + QueryableRecords = moderatorList.AsQueryable(), + TotalCount = moderatorList.Count + }; + }); + + public ValueTask RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync(DateTime fromDate, DateTime toDate, + string moderator, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(moderator, nameof(moderator)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + page = NormalizePage(page); + pageSize = NormalizePageSize(pageSize); + + ValidateDatesAreWithinRange(fromDate, toDate); + + ListMetadataAndCount queryResults = await _storageBroker.GetPositiveMetadataListByTimeframeAndModerator(fromDate, toDate, moderator, page, pageSize); + + return new QueryableMetadataForTimeframeAndModerator + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + FromDate = fromDate, + ToDate = toDate, + Page = page, + PageSize = pageSize, + Moderator = moderator + }; + }); + + public ValueTask RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync(DateTime fromDate, DateTime toDate, + string moderator, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + page = NormalizePage(page); + pageSize = NormalizePageSize(pageSize); + + ValidateDatesAreWithinRange(fromDate, toDate); + + ListMetadataAndCount queryResults = await _storageBroker. + GetNegativeAndUnknownMetadataListByTimeframeAndModerator(fromDate, toDate, moderator, page, pageSize); + + return new QueryableMetadataForTimeframeAndModerator + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + FromDate = fromDate, + ToDate = toDate, + Page = page, + PageSize = pageSize, + Moderator = moderator + }; + }); + + public ValueTask RetrieveMetricsForGivenTimeframeAndModeratorAsync(DateTime fromDate, DateTime toDate, + string moderator) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(moderator, nameof(moderator)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + var queryResults = await _storageBroker.GetMetricsListByTimeframeAndModerator(fromDate, toDate, moderator); + + return new MetricsSummaryForTimeframeAndModerator + { + QueryableRecords = queryResults.AsQueryable(), + FromDate = fromDate, + ToDate = toDate, + Moderator = moderator + }; + }); + + public ValueTask RetrieveTagsForGivenTimePeriodAndModeratorAsync(DateTime fromDate, DateTime toDate, string moderator) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(moderator, nameof(moderator)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + ValidateDatesAreWithinRange(fromDate, toDate); + + List TagList = await _storageBroker.GetTagListByTimeframeAndModerator(fromDate, toDate, moderator); + + return new QueryableTagsForTimeframeAndModerator + { + QueryableRecords = TagList.AsQueryable(), + FromDate = fromDate, + ToDate = toDate, + TotalCount = TagList.Count, + Moderator = moderator + }; + }); + + #endregion + + #region tags + + public ValueTask RetrieveTagsForGivenTimePeriodAsync(DateTime fromDate, DateTime toDate) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + ValidateDatesAreWithinRange(fromDate, toDate); + + List TagList = await _storageBroker.GetTagListByTimeframe(fromDate, toDate); + + return new QueryableTagsForTimeframe + { + QueryableRecords = TagList.AsQueryable(), + FromDate = fromDate, + ToDate = toDate, + TotalCount = TagList.Count + }; + }); + + public ValueTask RetrieveMetadataForGivenTimeframeAndTagAsync(DateTime fromDate, DateTime toDate, string tag, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(tag, nameof(tag)); + + ValidateTagContainsOnlyValidCharacters(tag); + + List tags = new(); + string tagOperator = ""; + + if(tag.Contains(',')) + { + tagOperator = "AND"; + tags = tag.Split(',').ToList(); + } + else if(tag.Contains('|')) + { + tagOperator = "OR"; + tags = tag.Split('|').ToList(); + } + else + { + tags.Add(tag); + } + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + page = NormalizePage(page); + pageSize = NormalizePageSize(pageSize); + + ValidateDatesAreWithinRange(fromDate, toDate); + + ListMetadataAndCount queryResults = await _storageBroker.GetMetadataListByTimeframeAndTag(fromDate, toDate, tags, tagOperator, page, pageSize); + + return new QueryableMetadataForTimeframeAndTag + { + QueryableRecords = queryResults.PaginatedRecords.AsQueryable(), + TotalCount = queryResults.TotalCount, + FromDate = fromDate, + ToDate = toDate, + Tag = tag, + Page = page, + PageSize = pageSize + }; + }); + + public ValueTask RetrieveAllTagsAsync() => + TryCatch(async () => + { + List TagList = await _storageBroker.GetAllTagList(); + + + return new QueryableTags + { + QueryableRecords = TagList.AsQueryable(), + TotalCount = TagList.Count + }; + }); + + #endregion + + #region interest labels + + public ValueTask RetrieveAllInterestLabelsAsync() => + TryCatch(async () => + { + List InterestLabelList = await _storageBroker.GetAllInterestLabels(); + + + return new QueryableInterestLabels + { + QueryableRecords = InterestLabelList.AsQueryable(), + TotalCount = InterestLabelList.Count + }; + }); + + #endregion + + #region metrics + + public ValueTask RetrieveMetricsForGivenTimeframeAsync(DateTime fromDate, DateTime toDate) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + + fromDate = NormalizeStartDate(fromDate); + toDate = NormalizeEndDate(toDate); + + var queryResults = await _storageBroker.GetMetricsListByTimeframe(fromDate, toDate); + + return new MetricsSummaryForTimeframe + { + QueryableRecords = queryResults.AsQueryable(), + FromDate = fromDate, + ToDate = toDate + }; + }); + + #endregion + + #region helpers + + [ExcludeFromCodeCoverage] + private static string GetSortField(string sortBy) + { + // Add logic to map sortBy parameter to corresponding field in the Clip object + // For example, map "date" to "DateAndTimeCollected" + // You can add more cases for different sorting options + return (sortBy.ToLower()) switch + { + "date" => "timestamp", + "confidence" => "whaleFoundConfidence", + "moderator" => "moderator", + "moderateddate" => "dateModerated", + _ => "timestamp" + }; + } + + [ExcludeFromCodeCoverage] + private static string GetSortOrder(bool isDescending) + { + return isDescending ? "DESC" : "ASC"; + } + + [ExcludeFromCodeCoverage] + private static int NormalizePage(int page) + { + return page < 0 ? 1 : page; + } + + [ExcludeFromCodeCoverage] + private static int NormalizePageSize(int pageSize) + { + return pageSize < 0 ? 10 : pageSize; + } + + // Start at midnight + [ExcludeFromCodeCoverage] + private static DateTime NormalizeStartDate(DateTime startDate) + { + return startDate.Date; + } + + // Stop at 23:59:59 + [ExcludeFromCodeCoverage] + private static DateTime NormalizeEndDate(DateTime endDate) + { + return endDate.Date.AddHours(23).AddMinutes(59).AddSeconds(59); + } + + #endregion + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.Guards.cs new file mode 100644 index 00000000..5e2213b6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.Guards.cs @@ -0,0 +1,23 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class CommentOrchestrationService + { + protected void Validate(DateTime? date, string propertyName) + { + if (!date.HasValue || ValidatorUtilities.IsInvalid(date.Value)) + throw new InvalidCommentOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void ValidatePage(int page) + { + if (ValidatorUtilities.IsZeroOrLess(page)) + throw new InvalidCommentOrchestrationException(LoggingUtilities.InvalidProperty("page")); + } + + protected void ValidatePageSize(int pageSize) + { + if (ValidatorUtilities.IsZeroOrLess(pageSize)) + throw new InvalidCommentOrchestrationException(LoggingUtilities.InvalidProperty("pageSize")); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.TryCatchCommentListResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.TryCatchCommentListResponse.cs new file mode 100644 index 00000000..50a2c3fc --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.TryCatchCommentListResponse.cs @@ -0,0 +1,31 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class CommentOrchestrationService + { + public delegate ValueTask ReturningCommentListResponseFunction(); + + protected async ValueTask TryCatch(ReturningCommentListResponseFunction returningCommentListResponseFunction) + { + try + { + return await returningCommentListResponseFunction(); + } + catch (Exception exception) + { + if (exception is InvalidCommentOrchestrationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataValidationException || + exception is MetadataDependencyValidationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataDependencyException || + exception is MetadataServiceException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.cs new file mode 100644 index 00000000..b4db466f --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/CommentOrchestrationService.cs @@ -0,0 +1,85 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class CommentOrchestrationService : ICommentOrchestrationService + { + private readonly IMetadataService _metadataService; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + + public CommentOrchestrationService() { } + + public CommentOrchestrationService(IMetadataService metadataService, + ILogger logger) + { + _metadataService = metadataService; + _logger = logger; + } + + public ValueTask RetrievePositiveCommentsForGivenTimeframeAsync(DateTime? fromDate, DateTime? toDate, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + ValidatePage(page); + ValidatePageSize(pageSize); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + QueryableMetadataForTimeframe results = await _metadataService. + RetrievePositiveMetadataForGivenTimeframeAsync(nonNullableFromDate, nonNullableToDate, page, pageSize); + + return new CommentListResponse + { + Comments = results.QueryableRecords.Select(r => AsComment(r)).ToList(), + FromDate = results.FromDate, + ToDate = results.ToDate, + Page = results.Page, + PageSize = results.PageSize, + TotalCount = results.TotalCount, + Count = results.QueryableRecords.Count() + }; + }); + + public ValueTask RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync(DateTime? fromDate, DateTime? toDate, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + ValidatePage(page); + ValidatePageSize(pageSize); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + QueryableMetadataForTimeframe results = await _metadataService. + RetrieveNegativeAndUnknownMetadataForGivenTimeframeAsync(nonNullableFromDate, nonNullableToDate, page, pageSize); + + return new CommentListResponse + { + Comments = results.QueryableRecords.Select(r => AsComment(r)).ToList(), + FromDate = results.FromDate, + ToDate = results.ToDate, + Page = results.Page, + PageSize = results.PageSize, + TotalCount = results.TotalCount, + Count = results.QueryableRecords.Count() + }; + }); + + private Comment AsComment(Metadata metadata) + { + var comment = new Comment + { + Id = metadata.Id, + Comments = metadata.Comments, + LocationName = metadata.LocationName, + Moderator = metadata.Moderator, + Moderated = !string.IsNullOrWhiteSpace(metadata.DateModerated) ? DateTime.Parse(metadata.DateModerated) : null, + }; + + return comment; + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/ICommentOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/ICommentOrchestrationService.cs new file mode 100644 index 00000000..315a61e1 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Comments/ICommentOrchestrationService.cs @@ -0,0 +1,8 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface ICommentOrchestrationService + { + ValueTask RetrievePositiveCommentsForGivenTimeframeAsync(DateTime? fromDate, DateTime? toDate, int page, int pageSize); + ValueTask RetrieveNegativeAndUnknownCommentsForGivenTimeframeAsync(DateTime? fromDate, DateTime? toDate, int page, int pageSize); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.Guards.cs new file mode 100644 index 00000000..e924c75e --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.Guards.cs @@ -0,0 +1,74 @@ +using System.Text.RegularExpressions; + +namespace OrcaHello.Web.Api.Services +{ + public partial class DetectionOrchestrationService + { + protected void Validate(DateTime? date, string propertyName) + { + if (!date.HasValue || ValidatorUtilities.IsInvalid(date.Value)) + throw new InvalidDetectionOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void Validate(string propertyValue, string propertyName) + { + if (ValidatorUtilities.IsInvalid(propertyValue)) + throw new InvalidDetectionOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void ValidateModerateRequestOnUpdate(ModerateDetectionRequest request) + { + if (request is null) + throw new NullModerateDetectionRequestException(); + + switch(request) + { + case { } when ValidatorUtilities.IsInvalid(request.State) : + throw new InvalidDetectionOrchestrationException(LoggingUtilities.MissingRequiredProperty(nameof(request.State))); + + case { } when ValidatorUtilities.IsInvalid(request.Moderator) : + throw new InvalidDetectionOrchestrationException(LoggingUtilities.MissingRequiredProperty(nameof(request.Moderator))); + } + + ValidateStateIsAcceptable(request!.State); + } + + protected void ValidateStorageMetadata(Metadata storageMetadata, string id) + { + if (storageMetadata is null) + { + throw new NotFoundMetadataException(id); + } + } + + protected void ValidateDeleted(bool deleted, string id) + { + if (!deleted) + throw new DetectionNotDeletedException(id); + } + + protected void ValidateInserted(bool inserted, string id) + { + if (!inserted) + throw new DetectionNotInsertedException(id); + } + + protected void ValidatePage(int page) + { + if (ValidatorUtilities.IsZeroOrLess(page)) + throw new InvalidDetectionOrchestrationException(LoggingUtilities.InvalidProperty("page")); + } + + protected void ValidatePageSize(int pageSize) + { + if (ValidatorUtilities.IsZeroOrLess(pageSize)) + throw new InvalidDetectionOrchestrationException(LoggingUtilities.InvalidProperty("pageSize")); + } + + protected void ValidateStateIsAcceptable(string state) + { + if (ValidatorUtilities.GetMatchingEnumValue(state, typeof(DetectionState)) == null) + throw new InvalidDetectionOrchestrationException($"'{state}' is not a valid Detection state."); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.TryCatchValueTaskT.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.TryCatchValueTaskT.cs new file mode 100644 index 00000000..e955006d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.TryCatchValueTaskT.cs @@ -0,0 +1,34 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class DetectionOrchestrationService + { + public delegate ValueTask ReturningGenericFunction(); + + protected async ValueTask TryCatch(ReturningGenericFunction returningGenericFunction) + { + try + { + return await returningGenericFunction(); + } + catch (Exception exception) + { + if (exception is NotFoundMetadataException || + exception is DetectionNotDeletedException || + exception is DetectionNotInsertedException || + exception is InvalidDetectionOrchestrationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataValidationException || + exception is MetadataDependencyValidationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataDependencyException || + exception is MetadataServiceException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.cs new file mode 100644 index 00000000..f2a7ce57 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/DetectionOrchestrationService.cs @@ -0,0 +1,185 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class DetectionOrchestrationService : IDetectionOrchestrationService + { + private readonly IMetadataService _metadataService; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + + public DetectionOrchestrationService() { } + + public DetectionOrchestrationService(IMetadataService metadataService, + ILogger logger) + { + _metadataService = metadataService; + _logger = logger; + } + + public ValueTask RetrieveDetectionsForGivenTimeframeAndTagAsync(DateTime? fromDate, DateTime? toDate, string tag, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(tag, nameof(tag)); + ValidatePage(page); + ValidatePageSize(pageSize); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + QueryableMetadataForTimeframeAndTag results = await _metadataService. + RetrieveMetadataForGivenTimeframeAndTagAsync(nonNullableFromDate, nonNullableToDate, tag, page, pageSize); + + return new DetectionListForTagResponse + { + Detections = results.QueryableRecords.Select(r => AsDetection(r)).ToList(), + FromDate = results.FromDate, + ToDate = results.ToDate, + Tag = results.Tag, + Page = results.Page, + PageSize = results.PageSize, + TotalCount = results.TotalCount, + Count = results.QueryableRecords.Count() + }; + }); + + public ValueTask RetrieveFilteredDetectionsAsync(DateTime? fromDate, DateTime? toDate, string state, string sortBy, bool isDescending, + string location, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(state, nameof(state)); + Validate(sortBy, nameof(sortBy)); + ValidatePage(page); + ValidatePageSize(pageSize); + + ValidateStateIsAcceptable(state); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + QueryableMetadataFiltered results = await _metadataService. + RetrievePaginatedMetadataAsync(state, nonNullableFromDate, nonNullableToDate, sortBy, isDescending, location, page, pageSize); + + return new DetectionListResponse + { + Detections = results.QueryableRecords.Select(r => AsDetection(r)).ToList(), + FromDate = results.FromDate, + ToDate = results.ToDate, + Page = results.Page, + PageSize = results.PageSize, + TotalCount = results.TotalCount, + Count = results.QueryableRecords.Count(), + State = results.State, + SortBy = results.SortBy, + SortOrder = results.SortOrder, + Location = results.Location + }; + }); + + public ValueTask RetrieveDetectionByIdAsync(string id) => + TryCatch(async () => + { + Validate(id, nameof(id)); + Metadata maybeMetadata = await _metadataService.RetrieveMetadataByIdAsync(id); + ValidateStorageMetadata(maybeMetadata, id); + + return AsDetection(maybeMetadata); + }); + + public ValueTask ModerateDetectionByIdAsync(string id, ModerateDetectionRequest request) => + TryCatch(async () => + { + Validate(id, nameof(id)); + ValidateModerateRequestOnUpdate(request); + + // Get the current record + Metadata existingRecord = await _metadataService.RetrieveMetadataByIdAsync(id); + ValidateStorageMetadata(existingRecord, id); + + var existingState = existingRecord.State; + + // Make updates so they can be added as a new record + Metadata newRecord = existingRecord; + newRecord.State = request.State; + newRecord.Moderator = request.Moderator; + newRecord.DateModerated = request.DateModerated.ToString(); + newRecord.Comments = request.Comments; + newRecord.Tags = request.Tags; + + bool existingRecordDeleted = await _metadataService.RemoveMetadataByIdAndStateAsync(id, existingState); + ValidateDeleted(existingRecordDeleted, id); + + bool newRecordCreated = await _metadataService.AddMetadataAsync(newRecord); + + if(!newRecordCreated) + { + bool existingRecordRecreated = await _metadataService.AddMetadataAsync(existingRecord); + ValidateInserted(existingRecordRecreated, id); + } + + return AsDetection(newRecord); + }); + + public ValueTask RetrieveDetectionsForGivenInterestLabelAsync(string interestLabel) => + TryCatch(async () => + { + Validate(interestLabel, nameof(interestLabel)); + + QueryableMetadata results = await _metadataService. + RetrieveMetadataForInterestLabelAsync(interestLabel); + + return new DetectionListForInterestLabelResponse + { + Detections = results.QueryableRecords.Select(r => AsDetection(r)).ToList(), + InterestLabel = interestLabel, + TotalCount = results.QueryableRecords.Count() + }; + }); + + private static Detection AsDetection(Metadata metadata) + { + var detection = new Detection + { + Id = metadata.Id, + AudioUri = metadata.AudioUri, + SpectrogramUri = metadata.ImageUri, + State = metadata.State, + LocationName = metadata.LocationName, + Timestamp = metadata.Timestamp, + Tags = metadata.Tags, + Comments = metadata.Comments, + Confidence = metadata.WhaleFoundConfidence, + Moderator = metadata.Moderator, + Moderated = !string.IsNullOrWhiteSpace(metadata.DateModerated) ? DateTime.Parse(metadata.DateModerated) : null, + }; + + if(metadata.Location != null) + { + detection.Location = new Shared.Models.Detections.Location + { + Name = metadata.Location.Name, + Longitude = metadata.Location.Longitude, + Latitude = metadata.Location.Latitude + }; + } + + detection.Annotations = metadata.Predictions.Select(r => AsAnnotation(r)).ToList(); + + return detection; + } + + private static Annotation AsAnnotation(Prediction prediction) + { + return new Annotation + { + Id = prediction.Id, + StartTime = prediction.StartTime, + EndTime = prediction.StartTime + prediction.Duration, + Confidence = prediction.Confidence + }; + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/IDetectionOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/IDetectionOrchestrationService.cs new file mode 100644 index 00000000..ef36dc6f --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Detections/IDetectionOrchestrationService.cs @@ -0,0 +1,11 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface IDetectionOrchestrationService + { + ValueTask RetrieveDetectionsForGivenTimeframeAndTagAsync(DateTime? fromDate, DateTime? toDate, string tag, int page, int pageSize); + ValueTask RetrieveFilteredDetectionsAsync(DateTime? fromDate, DateTime? toDate, string state, string sortBy, bool isDescending, string location, int page, int pageSize); + ValueTask RetrieveDetectionByIdAsync(string id); + ValueTask ModerateDetectionByIdAsync(string id, ModerateDetectionRequest request); + ValueTask RetrieveDetectionsForGivenInterestLabelAsync(string interestLabel); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/HydrophoneOrchestrationService.TryCatchValueTaskT.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/HydrophoneOrchestrationService.TryCatchValueTaskT.cs new file mode 100644 index 00000000..fb14c756 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/HydrophoneOrchestrationService.TryCatchValueTaskT.cs @@ -0,0 +1,30 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class HydrophoneOrchestrationService + { + public delegate ValueTask ReturningGenericFunction(); + + protected async ValueTask TryCatch(ReturningGenericFunction returningGenericFunction) + { + try + { + return await returningGenericFunction(); + } + catch (Exception exception) + { + if (exception is InvalidHydrophoneOrchestrationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is HydrophoneValidationException || + exception is HydrophoneDependencyValidationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is HydrophoneDependencyException || + exception is HydrophoneServiceException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + } + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/HydrophoneOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/HydrophoneOrchestrationService.cs new file mode 100644 index 00000000..e53a43c0 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/HydrophoneOrchestrationService.cs @@ -0,0 +1,61 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class HydrophoneOrchestrationService : IHydrophoneOrchestrationService + { + private readonly IHydrophoneService _hydrophoneService; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + + public HydrophoneOrchestrationService() { } + + public HydrophoneOrchestrationService(IHydrophoneService hydrophoneService, + ILogger logger) + { + _hydrophoneService = hydrophoneService; + _logger = logger; + } + + public ValueTask RetrieveHydrophoneLocations() => + TryCatch(async () => + { + QueryableHydrophoneData results = await _hydrophoneService.RetrieveAllHydrophonesAsync(); + + return new HydrophoneListResponse + { + Hydrophones = results.QueryableRecords.Select(h => AsHydrophone(h)).OrderBy(h => h.Name).ToList(), + Count = results.QueryableRecords.Count() + }; + }); + + [ExcludeFromCodeCoverage] + private static Hydrophone AsHydrophone(HydrophoneData hydrophoneData) + { + var attributes = hydrophoneData?.Attributes; + + if (attributes is not null) + { + Hydrophone result = new() + { + Id = attributes.NodeName, + Name = attributes.Name, + ImageUrl = attributes.ImageUrl, + IntroHtml = attributes.IntroHtml + }; + + if(attributes.LocationPoint is not null) + { + if(attributes.LocationPoint.Coordinates != null && attributes.LocationPoint.Coordinates.Count == 2) + { + result.Longitude = attributes.LocationPoint.Coordinates[0]; + result.Latitude = attributes.LocationPoint.Coordinates[1]; + } + } + + return result; + } + else + return null!; + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/IHydrophoneOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/IHydrophoneOrchestrationService.cs new file mode 100644 index 00000000..efe502c5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Hydrohpones/IHydrophoneOrchestrationService.cs @@ -0,0 +1,7 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface IHydrophoneOrchestrationService + { + ValueTask RetrieveHydrophoneLocations(); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/IInterestLabelOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/IInterestLabelOrchestrationService.cs new file mode 100644 index 00000000..7fe0f677 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/IInterestLabelOrchestrationService.cs @@ -0,0 +1,9 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface IInterestLabelOrchestrationService + { + ValueTask RetrieveAllInterestLabelsAsync(); + ValueTask RemoveInterestLabelFromDetectionAsync(string id); + ValueTask AddInterestLabelToDetectionAsync(string id, string interestLabel); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.Guards.cs new file mode 100644 index 00000000..34aac3f6 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.Guards.cs @@ -0,0 +1,28 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class InterestLabelOrchestrationService + { + protected void Validate(string propertyValue, string propertyName) + { + if (ValidatorUtilities.IsInvalid(propertyValue)) + throw new InvalidInterestLabelOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void ValidateMetadataFound(Metadata metadata, string id) + { + if (metadata is null) + throw new NotFoundMetadataException(id); + } + protected void ValidateDeleted(bool deleted, string id) + { + if (!deleted) + throw new DetectionNotDeletedException(id); + } + + protected void ValidateInserted(bool inserted, string id) + { + if (!inserted) + throw new DetectionNotInsertedException(id); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.TryCatchValueTaskT.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.TryCatchValueTaskT.cs new file mode 100644 index 00000000..4bada4b4 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.TryCatchValueTaskT.cs @@ -0,0 +1,34 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class InterestLabelOrchestrationService + { + public delegate ValueTask ReturningGenericFunction(); + + protected async ValueTask TryCatch(ReturningGenericFunction returningGenericFunction) + { + try + { + return await returningGenericFunction(); + } + catch (Exception exception) + { + if (exception is NotFoundMetadataException || + exception is DetectionNotDeletedException || + exception is DetectionNotInsertedException || + exception is InvalidInterestLabelOrchestrationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataValidationException || + exception is MetadataDependencyValidationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataDependencyException || + exception is MetadataServiceException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.cs new file mode 100644 index 00000000..23ec99c8 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/InterestLabels/InterestLabelOrchestrationService.cs @@ -0,0 +1,85 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class InterestLabelOrchestrationService : IInterestLabelOrchestrationService + { + private readonly IMetadataService _metadataService; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + + public InterestLabelOrchestrationService() { } + + public InterestLabelOrchestrationService(IMetadataService metadataService, + ILogger logger) + { + _metadataService = metadataService; + _logger = logger; + } + + public ValueTask RetrieveAllInterestLabelsAsync() => + TryCatch(async () => + { + var candidateRecords = await _metadataService.RetrieveAllInterestLabelsAsync(); + + return new InterestLabelListResponse + { + InterestLabels = candidateRecords.QueryableRecords.OrderBy(s => s).ToList(), + Count = candidateRecords.TotalCount + }; + }); + + public ValueTask RemoveInterestLabelFromDetectionAsync(string id) => + TryCatch(async () => + { + Validate(id, nameof(id)); + + Metadata existingRecord = await _metadataService.RetrieveMetadataByIdAsync(id); + ValidateMetadataFound(existingRecord, id); + + InterestLabelRemovalResponse result = new() + { + LabelRemoved = existingRecord.InterestLabel, + Id = id + }; + + // Make updates so they can be added as a new record + Metadata newRecord = existingRecord; + newRecord.InterestLabel = null!; + + bool existingRecordDeleted = await _metadataService.RemoveMetadataByIdAndStateAsync(id, existingRecord.State); + ValidateDeleted(existingRecordDeleted, id); + + bool newRecordCreated = await _metadataService.AddMetadataAsync(newRecord); + + if (!newRecordCreated) + { + bool existingRecordRecreated = await _metadataService.AddMetadataAsync(existingRecord); + ValidateInserted(existingRecordRecreated, id); + } + + return result; + }); + + public ValueTask AddInterestLabelToDetectionAsync(string id, string interestLabel) => + TryCatch(async () => + { + Validate(id, nameof(id)); + Validate(interestLabel, nameof(interestLabel)); + + Metadata storedMetadata = await _metadataService.RetrieveMetadataByIdAsync(id); + ValidateMetadataFound(storedMetadata, id); + + storedMetadata.InterestLabel = interestLabel; + + var updatedMetdata = await _metadataService.UpdateMetadataAsync(storedMetadata); + + InterestLabelAddResponse result = new() + { + LabelAdded = storedMetadata.InterestLabel, + Id = id + }; + + return result; + }); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/IMetricsOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/IMetricsOrchestrationService.cs new file mode 100644 index 00000000..00bfc92a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/IMetricsOrchestrationService.cs @@ -0,0 +1,7 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface IMetricsOrchestrationService + { + ValueTask RetrieveMetricsForGivenTimeframeAsync(DateTime? fromDate, DateTime? toDate); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.Guards.cs new file mode 100644 index 00000000..c2cf307f --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.Guards.cs @@ -0,0 +1,11 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class MetricsOrchestrationService + { + protected void Validate(DateTime? date, string propertyName) + { + if (!date.HasValue || ValidatorUtilities.IsInvalid(date.Value)) + throw new InvalidMetricOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.TryCatchMetricsResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.TryCatchMetricsResponse.cs new file mode 100644 index 00000000..6ac51a94 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.TryCatchMetricsResponse.cs @@ -0,0 +1,31 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class MetricsOrchestrationService + { + public delegate ValueTask ReturningMetricsResponseFunction(); + + protected async ValueTask TryCatch(ReturningMetricsResponseFunction returningMetricsResponseFunction) + { + try + { + return await returningMetricsResponseFunction(); + } + catch (Exception exception) + { + if (exception is InvalidMetricOrchestrationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataValidationException || + exception is MetadataDependencyValidationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataDependencyException || + exception is MetadataServiceException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.cs new file mode 100644 index 00000000..41ea85c0 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Metrics/MetricsOrchestrationService.cs @@ -0,0 +1,43 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class MetricsOrchestrationService : IMetricsOrchestrationService + { + private readonly IMetadataService _metadataService; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + + public MetricsOrchestrationService() { } + + public MetricsOrchestrationService(IMetadataService metadataService, + ILogger logger) + { + _metadataService = metadataService; + _logger = logger; + } + + public ValueTask RetrieveMetricsForGivenTimeframeAsync(DateTime? fromDate, DateTime? toDate) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + MetricsSummaryForTimeframe results = await _metadataService. + RetrieveMetricsForGivenTimeframeAsync(nonNullableFromDate, nonNullableToDate); + + return new MetricsResponse + { + Unreviewed = results.QueryableRecords.Where(x => x.State == "Unreviewed").Select(x => x.Count).FirstOrDefault(), + Positive = results.QueryableRecords.Where(x => x.State == "Positive").Select(x => x.Count).FirstOrDefault(), + Negative = results.QueryableRecords.Where(x => x.State == "Negative").Select(x => x.Count).FirstOrDefault(), + Unknown = results.QueryableRecords.Where(x => x.State == "Unknown").Select(x => x.Count).FirstOrDefault(), + FromDate = results.FromDate, + ToDate = results.ToDate + }; + }); + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/IModeratorOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/IModeratorOrchestrationService.cs new file mode 100644 index 00000000..7bf34eec --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/IModeratorOrchestrationService.cs @@ -0,0 +1,11 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface IModeratorOrchestrationService + { + ValueTask RetrieveModeratorsAsync(); + ValueTask RetrieveMetricsForGivenTimeframeAndModeratorAsync(DateTime? fromDate, DateTime? toDate, string moderator); + ValueTask RetrieveTagsForGivenTimePeriodAndModeratorAsync(DateTime? fromDate, DateTime? toDate, string moderator); + ValueTask RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(DateTime? fromDate, DateTime? toDate, string moderator, int page, int pageSize); + ValueTask RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync(DateTime? fromDate, DateTime? toDate, string moderator, int page, int pageSize); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.Guards.cs new file mode 100644 index 00000000..60710f39 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.Guards.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class ModeratorOrchestrationService + { + protected void Validate(DateTime? date, string propertyName) + { + if (!date.HasValue || ValidatorUtilities.IsInvalid(date.Value)) + throw new InvalidModeratorOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void Validate(string propertyValue, string propertyName) + { + if (ValidatorUtilities.IsInvalid(propertyValue)) + throw new InvalidModeratorOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void ValidatePage(int page) + { + if (ValidatorUtilities.IsZeroOrLess(page)) + throw new InvalidModeratorOrchestrationException(LoggingUtilities.InvalidProperty("page")); + } + + protected void ValidatePageSize(int pageSize) + { + if (ValidatorUtilities.IsZeroOrLess(pageSize)) + throw new InvalidModeratorOrchestrationException(LoggingUtilities.InvalidProperty("pageSize")); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.TryCatchValueTaskT.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.TryCatchValueTaskT.cs new file mode 100644 index 00000000..977095d3 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.TryCatchValueTaskT.cs @@ -0,0 +1,31 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class ModeratorOrchestrationService + { + public delegate ValueTask ReturningGenericFunction(); + + protected async ValueTask TryCatch(ReturningGenericFunction returningGenericFunction) + { + try + { + return await returningGenericFunction(); + } + catch (Exception exception) + { + if (exception is InvalidModeratorOrchestrationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataValidationException || + exception is MetadataDependencyValidationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataDependencyException || + exception is MetadataServiceException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.cs new file mode 100644 index 00000000..bb4d0590 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Moderators/ModeratorOrchestrationService.cs @@ -0,0 +1,149 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class ModeratorOrchestrationService : IModeratorOrchestrationService + { + private readonly IMetadataService _metadataService; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + + public ModeratorOrchestrationService() { } + + public ModeratorOrchestrationService(IMetadataService metadataService, + ILogger logger) + { + _metadataService = metadataService; + _logger = logger; + } + + public ValueTask RetrieveModeratorsAsync() => + TryCatch(async () => + { + QueryableModerators queryableModerators = await _metadataService. + RetrieveModeratorsAsync(); + + return new ModeratorListResponse + { + Moderators = queryableModerators.QueryableRecords.OrderBy(s => s).ToList(), + Count = queryableModerators.TotalCount + }; + }); + + public ValueTask RetrieveMetricsForGivenTimeframeAndModeratorAsync(DateTime? fromDate, DateTime? toDate, string moderator) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(moderator, nameof(moderator)); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + MetricsSummaryForTimeframeAndModerator results = await _metadataService. + RetrieveMetricsForGivenTimeframeAndModeratorAsync(nonNullableFromDate, nonNullableToDate, moderator); + + return new MetricsForModeratorResponse + { + Positive = results.QueryableRecords.Where(x => x.State == "Positive").Select(x => x.Count).FirstOrDefault(), + Negative = results.QueryableRecords.Where(x => x.State == "Negative").Select(x => x.Count).FirstOrDefault(), + Unknown = results.QueryableRecords.Where(x => x.State == "Unknown").Select(x => x.Count).FirstOrDefault(), + FromDate = results.FromDate, + ToDate = results.ToDate, + Moderator = moderator + }; + }); + + public ValueTask RetrieveTagsForGivenTimePeriodAndModeratorAsync(DateTime? fromDate, DateTime? toDate, string moderator) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(moderator, nameof(moderator)); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + var candidateRecords = await _metadataService. + RetrieveTagsForGivenTimePeriodAndModeratorAsync(nonNullableFromDate, nonNullableToDate, moderator); + + return new TagListForModeratorResponse + { + Tags = candidateRecords.QueryableRecords.OrderBy(s => s).ToList(), + FromDate = candidateRecords.FromDate, + ToDate = candidateRecords.ToDate, + Count = candidateRecords.TotalCount, + Moderator = moderator + }; +}); + + public ValueTask RetrievePositiveCommentsForGivenTimeframeAndModeratorAsync(DateTime? fromDate, DateTime? toDate, string moderator, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(moderator, nameof(moderator)); + ValidatePage(page); + ValidatePageSize(pageSize); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + QueryableMetadataForTimeframeAndModerator results = await _metadataService. + RetrievePositiveMetadataForGivenTimeframeAndModeratorAsync(nonNullableFromDate, nonNullableToDate, moderator, page, pageSize); + + return new CommentListForModeratorResponse + { + Comments = results.QueryableRecords.Select(r => AsComment(r)).ToList(), + FromDate = results.FromDate, + ToDate = results.ToDate, + Page = results.Page, + PageSize = results.PageSize, + TotalCount = results.TotalCount, + Count = results.QueryableRecords.Count(), + Moderator = moderator + }; + }); + + public ValueTask RetrieveNegativeAndUnknownCommentsForGivenTimeframeAndModeratorAsync(DateTime? fromDate, DateTime? toDate, string moderator, int page, int pageSize) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + Validate(moderator, nameof(moderator)); + ValidatePage(page); + ValidatePageSize(pageSize); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + QueryableMetadataForTimeframeAndModerator results = await _metadataService. + RetrieveNegativeAndUnknownMetadataForGivenTimeframeAndModeratorAsync(nonNullableFromDate, nonNullableToDate, moderator, page, pageSize); + + return new CommentListForModeratorResponse + { + Comments = results.QueryableRecords.Select(r => AsComment(r)).ToList(), + FromDate = results.FromDate, + ToDate = results.ToDate, + Page = results.Page, + PageSize = results.PageSize, + TotalCount = results.TotalCount, + Count = results.QueryableRecords.Count(), + Moderator = moderator + }; + }); + + private Comment AsComment(Metadata metadata) + { + var comment = new Comment + { + Id = metadata.Id, + Comments = metadata.Comments, + LocationName = metadata.LocationName, + Moderator = metadata.Moderator, + Moderated = !string.IsNullOrWhiteSpace(metadata.DateModerated) ? DateTime.Parse(metadata.DateModerated) : null, + }; + + return comment; + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/ITagOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/ITagOrchestrationService.cs new file mode 100644 index 00000000..f6ceee44 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/ITagOrchestrationService.cs @@ -0,0 +1,10 @@ +namespace OrcaHello.Web.Api.Services +{ + public interface ITagOrchestrationService + { + ValueTask RetrieveAllTagsAsync(); + ValueTask RetrieveTagsForGivenTimePeriodAsync(DateTime? fromDate, DateTime? toDate); + ValueTask RemoveTagFromAllDetectionsAsync(string tagToRemove); + ValueTask ReplaceTagInAllDetectionsAsync(string oldTag, string newTag); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.Guards.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.Guards.cs new file mode 100644 index 00000000..94b58da5 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.Guards.cs @@ -0,0 +1,17 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class TagOrchestrationService + { + protected void Validate(DateTime? date, string propertyName) + { + if (!date.HasValue || ValidatorUtilities.IsInvalid(date.Value)) + throw new InvalidTagOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + + protected void Validate(string propertyValue, string propertyName) + { + if (ValidatorUtilities.IsInvalid(propertyValue)) + throw new InvalidTagOrchestrationException(LoggingUtilities.MissingRequiredProperty(propertyName)); + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.TryCatchValueTaskT.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.TryCatchValueTaskT.cs new file mode 100644 index 00000000..722c7437 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.TryCatchValueTaskT.cs @@ -0,0 +1,31 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class TagOrchestrationService + { + public delegate ValueTask ReturningGenericFunction(); + + protected async ValueTask TryCatch(ReturningGenericFunction returningGenericFunction) + { + try + { + return await returningGenericFunction(); + } + catch(Exception exception) + { + if (exception is InvalidTagOrchestrationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if(exception is MetadataValidationException || + exception is MetadataDependencyValidationException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + if (exception is MetadataDependencyException || + exception is MetadataServiceException) + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + throw LoggingUtilities.CreateAndLogException(_logger, exception); + + } + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.cs new file mode 100644 index 00000000..e548cc23 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Services/Orchestrations/Tags/TagOrchestrationService.cs @@ -0,0 +1,116 @@ +namespace OrcaHello.Web.Api.Services +{ + public partial class TagOrchestrationService : ITagOrchestrationService + { + private readonly IMetadataService _metadataService; + private readonly ILogger _logger; + + // Needed for unit testing wrapper to work properly + + public TagOrchestrationService() { } + + public TagOrchestrationService(IMetadataService metadataService, + ILogger logger) + { + _metadataService = metadataService; + _logger = logger; + } + + public ValueTask RetrieveTagsForGivenTimePeriodAsync(DateTime? fromDate, DateTime? toDate) => + TryCatch(async () => + { + Validate(fromDate, nameof(fromDate)); + Validate(toDate, nameof(toDate)); + + DateTime nonNullableFromDate = fromDate ?? default; + DateTime nonNullableToDate = toDate ?? default; + + var candidateRecords = await _metadataService. + RetrieveTagsForGivenTimePeriodAsync(nonNullableFromDate, nonNullableToDate); + + return new TagListForTimeframeResponse + { + Tags = candidateRecords.QueryableRecords.OrderBy(s => s).ToList(), + FromDate = candidateRecords.FromDate, + ToDate = candidateRecords.ToDate, + Count = candidateRecords.TotalCount + }; + }); + + public ValueTask RetrieveAllTagsAsync() => + TryCatch(async () => + { + var candidateRecords = await _metadataService.RetrieveAllTagsAsync(); + + return new TagListResponse + { + Tags = candidateRecords.QueryableRecords.OrderBy(s => s).ToList(), + Count = candidateRecords.TotalCount + }; + }); + + public ValueTask RemoveTagFromAllDetectionsAsync(string tagToRemove) => + TryCatch(async () => + { + Validate(tagToRemove, nameof(tagToRemove)); + + var allMetadataWithTag = await _metadataService.RetrieveMetadataForTagAsync(tagToRemove); + + int totalRemoved = 0; + + foreach(Metadata item in allMetadataWithTag.QueryableRecords) + { + item.Tags.Remove(tagToRemove); + + bool recordUpdated = await _metadataService.UpdateMetadataAsync(item); + + if (recordUpdated) + totalRemoved++; + } + + TagRemovalResponse result = new() + { + Tag = tagToRemove, + TotalMatching = allMetadataWithTag.TotalCount, + TotalRemoved = totalRemoved + }; + + return result; + }); + + public ValueTask ReplaceTagInAllDetectionsAsync(string oldTag, string newTag) => + TryCatch(async () => + { + Validate(oldTag, nameof(oldTag)); + Validate(newTag, nameof(newTag)); + + var allMetadataWithTag = await _metadataService.RetrieveMetadataForTagAsync(oldTag); + + int totalReplaced = 0; + + foreach (Metadata item in allMetadataWithTag.QueryableRecords) + { + int indexOfOldTag = item.Tags.IndexOf(oldTag); + if (indexOfOldTag >= 0) + { + item.Tags[indexOfOldTag] = newTag; + } + + bool recordUpdated = await _metadataService.UpdateMetadataAsync(item); + + if (recordUpdated) + totalReplaced++; + } + + TagReplaceResponse result = new() + { + OldTag = oldTag, + NewTag = newTag, + TotalMatching = allMetadataWithTag.TotalCount, + TotalReplaced = totalReplaced + }; + + return result; + }); + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Usings.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Usings.cs new file mode 100644 index 00000000..7149f823 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/Usings.cs @@ -0,0 +1,25 @@ +global using Microsoft.AspNetCore.Authentication.JwtBearer; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Azure.Cosmos; +global using Microsoft.Identity.Web; +global using Microsoft.OpenApi.Models; +global using Newtonsoft.Json; +global using OrcaHello.Web.Api.Brokers.Hydrophones; +global using OrcaHello.Web.Api.Brokers.Loggings; +global using OrcaHello.Web.Api.Brokers.Storages; +global using OrcaHello.Web.Api.Models; +global using OrcaHello.Web.Api.Models.Configurations; +global using OrcaHello.Web.Api.Services; +global using OrcaHello.Web.Shared.Models.Comments; +global using OrcaHello.Web.Shared.Models.Detections; +global using OrcaHello.Web.Shared.Models.Hydrophones; +global using OrcaHello.Web.Shared.Models.InterestLabels; +global using OrcaHello.Web.Shared.Models.Metrics; +global using OrcaHello.Web.Shared.Models.Moderators; +global using OrcaHello.Web.Shared.Models.Tags; +global using OrcaHello.Web.Shared.Utilities; +global using Swashbuckle.AspNetCore.Annotations; +global using System.Diagnostics.CodeAnalysis; +global using System.Net; +global using System.Reflection; diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/appsettings.json b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/appsettings.json new file mode 100644 index 00000000..3e8bb4de --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Api/appsettings.json @@ -0,0 +1,46 @@ +/* + WARNING - This file will be checked into source control. Do not change this file, except for the HydrophoneLocations as these + show remain the same across all instances. + Use this as an example file. + + Use {env.EnvironmentName}.json as your configuration file as it will not be checked into source control. + {env.EnvironmentName} values : development, staging, production + + CosmosConnectionString - The AccountEndpoint=;AccountKey= combination defining source of the database + DetectionsDatabaseName - The name of the Cosmos DB database + MetadataContainerName - The name of the container within the database that contains the metadata .JSON + AllowedOrigin - The url of the frontend accessing the API (for CORS purposes) + AzureAd:Instance - The AAD endpoint for the Azure public cloud; used to sign in to srvices and applications that are hosted in the Azure public cloud + AzureAd:Domain - The default domain name for a new user when you create a new user in AAD + AzureAd:TenantId - AAD unique identifier that represents the organization (aka directory ID or a domain name) + AzureAd:ClientId - AAD unique identifier that represents the application being authenticated (aka application ID or a service principal ID) + AzureAd:Scopes - indicates the permissions that an application requests from a user or an administrator to access a web-hosted resource (i.e. API.Access) + AzureAd:CallbackPath - AAD configuration setting that specifies the path where the server will redirect during authentication + AzureAd:ModeratorGroupId - The GUID of the "Moderator" group used for access control +*/ + +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AppSettings": { + "CosmosConnectionString": "AccountEndpoint=https://localhost:8081/;AccountKey=AAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AA==", + "DetectionsDatabaseName": "detectionsDatabaseName", + "MetadataContainerName": "metadataContainerName", + "AllowedOrigin": "https://localhost:44399", + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "ai4orcasoutlook.onmicrosoft.com", + "TenantId": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA", + "ClientId": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA", + "Scopes": "API.Access", + "CallbackPath": "/signin-oidc", + "ModeratorGroupId": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA" + }, + "HydrophoneFeedUrl": "https://beta.orcasound.net/api/json/feeds" + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Comments/Comment.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Comments/Comment.cs new file mode 100644 index 00000000..8916b0d3 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Comments/Comment.cs @@ -0,0 +1,22 @@ +namespace OrcaHello.Web.Shared.Models.Comments +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("Comment-related content for a hydrophone sampling that might contain whale sounds.")] + public class Comment + { + [SwaggerSchema("The detection's generated unique Id.")] + public string Id { get; set; } + + [SwaggerSchema("Any text comments entered by the human moderator during review.")] + public string Comments { get; set; } + + [SwaggerSchema("The location of the hydrophone.")] + public string LocationName { get; set; } + + [SwaggerSchema("Identity of the human moderator (User Principal Name for AzureAD) performing the review.")] + public string Moderator { get; set; } + + [SwaggerSchema("Date and time of when the detection was reviewed by the human moderator.")] + public DateTime? Moderated { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Comments/CommentListResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Comments/CommentListResponse.cs new file mode 100644 index 00000000..99b093ba --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Comments/CommentListResponse.cs @@ -0,0 +1,28 @@ +namespace OrcaHello.Web.Shared.Models.Comments +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A paginated list of Comments for a given timeframe.")] + public class CommentListResponse + { + [SwaggerSchema("A paginated list of Comments for the given filter information.")] + public List Comments { get; set; } + + [SwaggerSchema("The total number of detections in the list (for pagination).")] + public int TotalCount { get; set; } + + [SwaggerSchema("The number of detections for this page.")] + public int Count { get; set; } + + [SwaggerSchema("The starting date of the timeframe.")] + public DateTime FromDate { get; set; } + + [SwaggerSchema("The ending date of the timeframe.")] + public DateTime ToDate { get; set; } + + [SwaggerSchema("The requested page number.")] + public int Page { get; set; } + + [SwaggerSchema("The requested page size.")] + public int PageSize { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/Detection.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/Detection.cs new file mode 100644 index 00000000..07eae49c --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/Detection.cs @@ -0,0 +1,72 @@ +namespace OrcaHello.Web.Shared.Models.Detections +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A hydrophone sampling that might contain whale sounds.")] + public class Detection + { + [SwaggerSchema("The detection's generated unique Id.")] + public string Id { get; set; } + + [SwaggerSchema("URI of the detection's audio file (.wav) in blob storage.")] + public string AudioUri { get; set; } + + [SwaggerSchema("URI of the detection's image file (.png) in blob storage.")] + public string SpectrogramUri { get; set; } + + [SwaggerSchema("The review state of the detection (Unreviewed, Positive, Negative Unknown).")] + public string State { get; set; } + + [SwaggerSchema("The name of the hydrophone's location.")] + public string LocationName { get; set; } + + [SwaggerSchema("Calculated average confidence that the detection contains a whale sound.")] + public decimal Confidence { get; set; } + + [SwaggerSchema("Location geodata.")] + public Location Location { get; set; } + + [SwaggerSchema("Date and time of when the detection occurred.")] + public DateTime Timestamp { get; set; } + + [SwaggerSchema("List of sections within the detection that might contain whale sounds.")] + public List Annotations { get; set; } = new List(); + + [SwaggerSchema("List of tags entered by the human moderator during review.")] + public List Tags { get; set; } = new List(); + + [SwaggerSchema("Any text comments entered by the human moderator during review.")] + public string Comments { get; set; } + + [SwaggerSchema("Identity of the human moderator (User Principal Name for AzureAD) performing the review.")] + public string Moderator { get; set; } + + [SwaggerSchema("Date and time of when the detection was reviewed by the human moderator.")] + public DateTime? Moderated { get; set; } + } + + [ExcludeFromCodeCoverage] + [SwaggerSchema("Geographical location of the hydrophone that collected the detection.")] + public class Location + { + [SwaggerSchema("Name of the hydrophone location.")] + public string Name { get; set; } + [SwaggerSchema("Longitude of the hydrophone's location.")] + public double Longitude { get; set; } + [SwaggerSchema("Latitude of the hydrophone's location.")] + public double Latitude { get; set; } + } + + [ExcludeFromCodeCoverage] + [SwaggerSchema("Section within the detection that might contain whale sounds.")] + public class Annotation + { + [SwaggerSchema("Unique identifier (within the detection) of the annotation.")] + public int Id { get; set; } + [SwaggerSchema("Start time (within the detection) of the annotation as measured in seconds.")] + public decimal StartTime { get; set; } + [SwaggerSchema("End time (within the detection) of the annotation as measured in seconds.")] + public decimal EndTime { get; set; } + [SwaggerSchema("Calculated confidence that the annotation contains a whale sound.")] + public decimal Confidence { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/DetectionListResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/DetectionListResponse.cs new file mode 100644 index 00000000..5edd5a03 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/DetectionListResponse.cs @@ -0,0 +1,76 @@ +namespace OrcaHello.Web.Shared.Models.Detections +{ + [ExcludeFromCodeCoverage] + public class DetectionListResponseBase + { + [SwaggerSchema("A paginated list of Detections for the given filter information.")] + public List Detections { get; set; } + + [SwaggerSchema("The total number of detections in the list (for pagination).")] + public int TotalCount { get; set; } + } + + [ExcludeFromCodeCoverage] + [SwaggerSchema("List of Detections by timeframe and tag.")] + public class DetectionListForTagResponse : DetectionListResponseBase + { + [SwaggerSchema("The starting date of the timeframe.")] + public DateTime FromDate { get; set; } + + [SwaggerSchema("The ending date of the timeframe.")] + public DateTime ToDate { get; set; } + + [SwaggerSchema("The filtering tag.")] + public string Tag { get; set; } + + [SwaggerSchema("The number of detections for this page.")] + public int Count { get; set; } + + [SwaggerSchema("The requested page number.")] + public int Page { get; set; } + + [SwaggerSchema("The requested page size.")] + public int PageSize { get; set; } + } + + [ExcludeFromCodeCoverage] + [SwaggerSchema("List of Detections by interest label.")] + public class DetectionListForInterestLabelResponse : DetectionListResponseBase + { + [SwaggerSchema("The filtering interest label.")] + public string InterestLabel { get; set; } + } + + [ExcludeFromCodeCoverage] + [SwaggerSchema("Sorted list of Detections by state, timeframe, location.")] + public class DetectionListResponse : DetectionListResponseBase + { + [SwaggerSchema("The starting date of the timeframe.")] + public DateTime FromDate { get; set; } + + [SwaggerSchema("The ending date of the timeframe.")] + public DateTime ToDate { get; set; } + + [SwaggerSchema("The state of the detections.")] + public string State { get; set; } + + [SwaggerSchema("The location of the detections.")] + public string Location { get; set; } + + [SwaggerSchema("The sort by property.")] + public string SortBy { get; set; } + + [SwaggerSchema("The sort order.")] + public string SortOrder { get; set; } + + [SwaggerSchema("The number of detections for this page.")] + public int Count { get; set; } + + [SwaggerSchema("The requested page number.")] + public int Page { get; set; } + + [SwaggerSchema("The requested page size.")] + public int PageSize { get; set; } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/DetectionState.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/DetectionState.cs new file mode 100644 index 00000000..b59059de --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/DetectionState.cs @@ -0,0 +1,11 @@ +namespace OrcaHello.Web.Shared.Models.Detections +{ + public enum DetectionState + { + Unreviewed, + Positive, + Negative, + Unknown + } + +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/ModerateDetectionRequest.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/ModerateDetectionRequest.cs new file mode 100644 index 00000000..f91ebc3e --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Detections/ModerateDetectionRequest.cs @@ -0,0 +1,25 @@ +namespace OrcaHello.Web.Shared.Models.Detections +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A request for updating a detection with moderator-related information.")] + public class ModerateDetectionRequest + { + [SwaggerSchema("The detection id.")] + public string Id { get; set; } + + [SwaggerSchema("The detection's new state.")] + public string State { get; set; } + + [SwaggerSchema("The name of the moderator.")] + public string Moderator { get; set; } + + [SwaggerSchema("The datetime the detection was moderated.")] + public DateTime DateModerated { get; set; } + + [SwaggerSchema("Comments the moderator made about the detection.")] + public string Comments { get; set; } + + [SwaggerSchema("The list of tags the moderator added to the detection.")] + public List Tags { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Hydrophones/Hydrophone.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Hydrophones/Hydrophone.cs new file mode 100644 index 00000000..bee565ab --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Hydrophones/Hydrophone.cs @@ -0,0 +1,26 @@ +namespace OrcaHello.Web.Shared.Models.Hydrophones +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("Hydrophone location information.")] + public class Hydrophone + { + [SwaggerSchema("The hydrophone's id.")] + public string Id { get; set; } + + [SwaggerSchema("The hydrophone's location name.")] + public string Name { get; set; } + + [SwaggerSchema("The hydrophone's location logitude.")] + public double Longitude { get; set; } + + [SwaggerSchema("The hydrophone's location latitude.")] + public double Latitude { get; set; } + + [SwaggerSchema("Link to an image showing the hydrophone location.")] + public string ImageUrl { get; set; } + + [SwaggerSchema("HTML-formatted description of the hydrophone location.")] + public string IntroHtml { get; set; } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Hydrophones/HydrophoneListResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Hydrophones/HydrophoneListResponse.cs new file mode 100644 index 00000000..15610f07 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Hydrophones/HydrophoneListResponse.cs @@ -0,0 +1,13 @@ +namespace OrcaHello.Web.Shared.Models.Hydrophones +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A list of all hydrophone locations.")] + public class HydrophoneListResponse + { + [SwaggerSchema("The list of hydrophones.")] + public List Hydrophones { get; set; } = new List(); + + [SwaggerSchema("The total number of hydrophones in the list")] + public int Count { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelAddResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelAddResponse.cs new file mode 100644 index 00000000..ad17190b --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelAddResponse.cs @@ -0,0 +1,14 @@ +namespace OrcaHello.Web.Shared.Models.InterestLabels +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("The results of an interest label add action.")] + public class InterestLabelAddResponse + { + [SwaggerSchema("The id of the detection updated.")] + public string Id { get; set; } + + [SwaggerSchema("The interest label that was added.")] + public string LabelAdded { get; set; } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelListResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelListResponse.cs new file mode 100644 index 00000000..f0e9d8ed --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelListResponse.cs @@ -0,0 +1,12 @@ +namespace OrcaHello.Web.Shared.Models.InterestLabels +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A list of unique interest labels.")] + public class InterestLabelListResponse + { + [SwaggerSchema("The list of interest labels in ascending order")] + public List InterestLabels { get; set; } = new List(); + [SwaggerSchema("The total number of interst labels in the list")] + public int Count { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelRemovalResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelRemovalResponse.cs new file mode 100644 index 00000000..d12a7c85 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/InterestLabels/InterestLabelRemovalResponse.cs @@ -0,0 +1,14 @@ +namespace OrcaHello.Web.Shared.Models.InterestLabels +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("The results of an interest label removal action.")] + public class InterestLabelRemovalResponse + { + [SwaggerSchema("The id of the detection updated.")] + public string Id { get; set; } + + [SwaggerSchema("The interest label that was removed.")] + public string LabelRemoved { get; set; } + + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Metrics/MetricsResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Metrics/MetricsResponse.cs new file mode 100644 index 00000000..2675b759 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Metrics/MetricsResponse.cs @@ -0,0 +1,27 @@ +namespace OrcaHello.Web.Shared.Models.Metrics +{ + [ExcludeFromCodeCoverage] + public class MetricsResponseBase + { + [SwaggerSchema("The number of reviewed detections with no whale call.")] + public int Negative { get; set; } + [SwaggerSchema("The number of reviewed detections with confirmed whale call.")] + public int Positive { get; set; } + [SwaggerSchema("The number of reviewed detections where whale call could not be determined.")] + public int Unknown { get; set; } + + [SwaggerSchema("The starting date of the timeframe.")] + public DateTime FromDate { get; set; } + + [SwaggerSchema("The ending date of the timeframe.")] + public DateTime ToDate { get; set; } + } + + [ExcludeFromCodeCoverage] + [SwaggerSchema("The State metrics for the given timeframe.")] + public class MetricsResponse : MetricsResponseBase + { + [SwaggerSchema("The number of unreviewed detections.")] + public int Unreviewed { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/CommentListForModeratorResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/CommentListForModeratorResponse.cs new file mode 100644 index 00000000..6b71b928 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/CommentListForModeratorResponse.cs @@ -0,0 +1,10 @@ +namespace OrcaHello.Web.Shared.Models.Moderators +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A paginated list of Comments for a given timeframe and moderator.")] + public class CommentListForModeratorResponse : CommentListResponse + { + [SwaggerSchema("The name of the moderator.")] + public string Moderator { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/MetricsForModeratorResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/MetricsForModeratorResponse.cs new file mode 100644 index 00000000..e4fb05db --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/MetricsForModeratorResponse.cs @@ -0,0 +1,10 @@ +namespace OrcaHello.Web.Shared.Models.Moderators +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("The State metrics for the given timeframe and moderator.")] + public class MetricsForModeratorResponse : MetricsResponseBase + { + [SwaggerSchema("The name of the moderator.")] + public string Moderator { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/ModeratorListResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/ModeratorListResponse.cs new file mode 100644 index 00000000..b868913f --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/ModeratorListResponse.cs @@ -0,0 +1,12 @@ +namespace OrcaHello.Web.Shared.Models.Moderators +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A unique list of moderators.")] + public class ModeratorListResponse + { + [SwaggerSchema("The unique names of the moderators.")] + public List Moderators { get; set; } + [SwaggerSchema("The total number of moderators in the list.")] + public int Count { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/TagListForModeratorResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/TagListForModeratorResponse.cs new file mode 100644 index 00000000..52996a3a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Moderators/TagListForModeratorResponse.cs @@ -0,0 +1,10 @@ +namespace OrcaHello.Web.Shared.Models.Moderators +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A list of tags for the given timeframe and moderator")] + public class TagListForModeratorResponse : TagListForTimeframeResponse + { + [SwaggerSchema("The name of the moderator.")] + public string Moderator { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagListResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagListResponse.cs new file mode 100644 index 00000000..56c2cc1d --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagListResponse.cs @@ -0,0 +1,21 @@ +namespace OrcaHello.Web.Shared.Models.Tags +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("A list of tags")] + public class TagListResponse + { + [SwaggerSchema("The list of tags in ascending order")] + public List Tags { get; set; } = new List(); + [SwaggerSchema("The total number of tags in the list")] + public int Count { get; set; } + } + + [ExcludeFromCodeCoverage] + [SwaggerSchema("A list of tags for the given timeframe")] + public class TagListForTimeframeResponse : TagListResponse + { + public DateTime FromDate { get; set; } + [SwaggerSchema("The ending date of the timeframe")] + public DateTime ToDate { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagRemovalResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagRemovalResponse.cs new file mode 100644 index 00000000..9745be29 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagRemovalResponse.cs @@ -0,0 +1,15 @@ +namespace OrcaHello.Web.Shared.Models.Tags +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("The results of a Tag removal request.")] + public class TagRemovalResponse + { + [SwaggerSchema("The Tag to remove.")] + public string Tag { get; set; } + [SwaggerSchema("The total number of Detections with the Tag.")] + public int TotalMatching { get; set; } + + [SwaggerSchema("The total number of Detections where the Tag was successfully removed.")] + public int TotalRemoved { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagReplaceResponse.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagReplaceResponse.cs new file mode 100644 index 00000000..ecfc5a00 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Models/Tags/TagReplaceResponse.cs @@ -0,0 +1,18 @@ +namespace OrcaHello.Web.Shared.Models.Tags +{ + [ExcludeFromCodeCoverage] + [SwaggerSchema("The results of a Tag replace request.")] + public class TagReplaceResponse + { + [SwaggerSchema("The Tag to replace.")] + public string OldTag { get; set; } + [SwaggerSchema("The Tag to replace it with.")] + public string NewTag { get; set; } + + [SwaggerSchema("The total number of Detections with the Tag.")] + public int TotalMatching { get; set; } + + [SwaggerSchema("The total number of Detections where the Tag was successfully replaced.")] + public int TotalReplaced { get; set; } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/OrcaHello.Web.Shared.csproj b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/OrcaHello.Web.Shared.csproj new file mode 100644 index 00000000..cf02784a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/OrcaHello.Web.Shared.csproj @@ -0,0 +1,23 @@ + + + + net7.0 + enable + enable + + + + 1701;1702; CS8618 + + + + 1701;1702; CS8618 + + + + + + + + + diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Usings.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Usings.cs new file mode 100644 index 00000000..d6c019a0 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Usings.cs @@ -0,0 +1,9 @@ +global using Microsoft.Azure.Cosmos; +global using Microsoft.Extensions.Logging; +global using Newtonsoft.Json.Linq; +global using OrcaHello.Web.Shared.Models.Comments; +global using OrcaHello.Web.Shared.Models.Metrics; +global using OrcaHello.Web.Shared.Models.Tags; +global using Swashbuckle.AspNetCore.Annotations; +global using System.Diagnostics.CodeAnalysis; +global using System.Net; \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/CosmosUtilities.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/CosmosUtilities.cs new file mode 100644 index 00000000..c3408d7e --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/CosmosUtilities.cs @@ -0,0 +1,32 @@ +namespace OrcaHello.Web.Shared.Utilities +{ + [ExcludeFromCodeCoverage] + public static class CosmosUtilities + { + public static HttpStatusCode GetHttpStatusCode(Exception ex) + { + return ((CosmosException)ex).StatusCode; + } + + public static string GetReason(Exception ex) + { + string message = ex.Message; // get the exception message + + var startIndex = message.IndexOf("{"); + var reasonString = message[startIndex..]; + var endIndex = reasonString.IndexOf(")"); + reasonString = reasonString[..endIndex]; + + JObject reasonObject = JObject.Parse(reasonString); + + string reasonMessage = (string)reasonObject["errors"][0]["message"]; // get the error message as a string + + return $"Cosmos DB error: {reasonMessage} Contact support for resolution."; + } + + public static string FormatDate(DateTime dateTime) + { + return dateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"); + } + } +} \ No newline at end of file diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/LoggingUtilities.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/LoggingUtilities.cs new file mode 100644 index 00000000..17154c2a --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/LoggingUtilities.cs @@ -0,0 +1,87 @@ +namespace OrcaHello.Web.Shared.Utilities +{ + [ExcludeFromCodeCoverage] + public static class LoggingUtilities + { + public static void Log(ILogger logger, LogLevel level, Exception? exception, string message) + { + logger.Log(level, 1, exception, message); + } + + public static void LogInfo(ILogger logger, string message, Exception? exception = null) + { + Log(logger, LogLevel.Information, exception, message); + } + + public static void LogWarn(ILogger logger, string message, Exception? exception = null) + { + Log(logger, LogLevel.Warning, exception, message); + } + + public static void LogWarn(ILogger logger, Exception exception) + { + if (exception is not null) + { + var innerException = exception.InnerException; + + if (innerException is not null) + Log(logger, LogLevel.Warning, exception, innerException.Message); + else + Log(logger, LogLevel.Warning, exception, exception.Message); + } + } + + public static void LogError(ILogger logger, string message, Exception? exception = null) + { + Log(logger, LogLevel.Error, exception, message); + } + + public static void LogError(ILogger logger, Exception exception) + { + if (exception is not null) + { + var innerException = exception.InnerException; + + if (innerException is not null) + Log(logger, LogLevel.Error, exception, innerException.Message); + else + Log(logger, LogLevel.Error, exception, exception.Message); + } + } + + public static void LogTrace(ILogger logger, string message, Exception? exception = null) + { + Log(logger, LogLevel.Trace, exception, message); + } + + public static void LogDebug(ILogger logger, string message, Exception? exception = null) + { + Log(logger, LogLevel.Debug, exception, message); + } + + public static T CreateAndLogException(ILogger logger, Exception innerException) where T : new() + { + T exception = (T)Activator.CreateInstance(typeof(T), innerException)!; + + if (logger is not null && exception is not null) + LogError(logger, exception as Exception); + + return exception; + } + + public static T CreateAndLogWarning(ILogger logger, Exception innerException) where T : new() + { + T exception = (T)Activator.CreateInstance(typeof(T), innerException)!; + + if (logger is not null && exception is not null) + LogWarn(logger, exception as Exception); + return exception; + } + + public static string MissingRequiredProperty(string propertyName) => $"Required property '{propertyName}' is invalid or missing from request."; + + public static string InvalidProperty(string propertyName) => $"Property '{propertyName}' in request is invalid."; + + public static string EmptyRequestBody() => "Request body is empty."; + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/ValidatorUtilities.cs b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/ValidatorUtilities.cs new file mode 100644 index 00000000..68fd5b05 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.Web.Shared/Utilities/ValidatorUtilities.cs @@ -0,0 +1,29 @@ +namespace OrcaHello.Web.Shared.Utilities +{ + [ExcludeFromCodeCoverage] + public static class ValidatorUtilities + { + public static bool IsInvalid(string input) => string.IsNullOrWhiteSpace(input); + public static bool IsInvalid(long input) => input == default(long); + public static bool IsInvalid(Guid input) => input == Guid.Empty; + public static bool IsInvalid(this TValue value) where TValue : Enum => !Enum.IsDefined(typeof(TValue), value); + public static bool IsNegative(long input) => input < 0; + public static bool IsNegative(int input) => input < 0; + public static bool IsZeroOrLess(int input) => input <= 0; + public static bool IsInvalid(Object input) => input == null; + public static bool IsInvalid(DateTime input) => input == default(DateTime); + public static bool IsInvalidGuidString(string input) => !Guid.TryParse(input, out Guid dummy); + public static string GetInnerMessage(Exception exception) => exception.InnerException.Message; + public static string GetMessage(Exception exception) => exception.Message; + + public static string GetMatchingEnumValue(string input, Type enumType) + { + if (Enum.TryParse(enumType, input, true, out object enumValue)) + { + return enumValue.ToString(); + } + + return null; + } + } +} diff --git a/ModeratorFrontEnd/OrcaHello/OrcaHello.sln b/ModeratorFrontEnd/OrcaHello/OrcaHello.sln new file mode 100644 index 00000000..f7a93fb0 --- /dev/null +++ b/ModeratorFrontEnd/OrcaHello/OrcaHello.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34009.444 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrcaHello.Web.Api", "OrcaHello.Web.Api\OrcaHello.Web.Api.csproj", "{7AA0448F-F0E4-43C8-A833-5038E38AAC22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrcaHello.Web.Shared", "OrcaHello.Web.Shared\OrcaHello.Web.Shared.csproj", "{68316C13-0DBF-459E-B1E5-081778C244EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrcaHello.Web.Api.Tests.Unit", "OrcaHello.Web.Api.Tests.Unit\OrcaHello.Web.Api.Tests.Unit.csproj", "{01375E6C-8A92-49B1-8CAD-2696A7DC8DDD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7AA0448F-F0E4-43C8-A833-5038E38AAC22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AA0448F-F0E4-43C8-A833-5038E38AAC22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AA0448F-F0E4-43C8-A833-5038E38AAC22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AA0448F-F0E4-43C8-A833-5038E38AAC22}.Release|Any CPU.Build.0 = Release|Any CPU + {68316C13-0DBF-459E-B1E5-081778C244EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68316C13-0DBF-459E-B1E5-081778C244EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68316C13-0DBF-459E-B1E5-081778C244EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68316C13-0DBF-459E-B1E5-081778C244EB}.Release|Any CPU.Build.0 = Release|Any CPU + {01375E6C-8A92-49B1-8CAD-2696A7DC8DDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01375E6C-8A92-49B1-8CAD-2696A7DC8DDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01375E6C-8A92-49B1-8CAD-2696A7DC8DDD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01375E6C-8A92-49B1-8CAD-2696A7DC8DDD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {36D12860-2BDA-4813-9BE8-201886EA5BE5} + EndGlobalSection +EndGlobal diff --git a/ModeratorFrontEnd/schema/example_v2.json b/ModeratorFrontEnd/schema/example_v2.json new file mode 100644 index 00000000..462111e3 --- /dev/null +++ b/ModeratorFrontEnd/schema/example_v2.json @@ -0,0 +1,51 @@ + { + "id": "000000000-000000000-00000000-00000000000", // Guid + "state": "Unreviewed", // State of the review (Unreviewed, Positive, Negative, Unknown) + "locationName": "Haro Strait", // the name of the location (does duplicate name in location below) + "audioUri": "https://livemlaudiospecstorage.blob.core.windows.net/audiowavs/rpi_orcasound_lab_2020_09_30_03_51_56_PDT.wav", // string + "imageUri": "https://livemlaudiospecstorage.blob.core.windows.net/spectrogramspng/rpi_orcasound_lab_2020_09_30_03_51_56_PDT.png", // string + "timestamp": "2020-09-30T10:51:56.057346Z", // ISO format - UTC + "whaleFoundConfidence": 88.55000000000001, // double + "location": { + "id": "rpi_orcasound_lab", + "name": "Haro Strait", + "longitude": -123.2166658, + "latitude": 48.5499978 + }, + "predictions": [ + { + "id": 0, + "startTime": 20, + "duration": 2.5, + "confidence": 0.828 + }, + { + "id": 1, + "startTime": 27.5, + "duration": 2.5, + "confidence": 0.942 + }, + { + "id": 2, + "startTime": 32.5, + "duration": 2.5, + "confidence": 0.917 + }, + { + "id": 3, + "startTime": 52.5, + "duration": 2.5, + "confidence": 0.855 + } + ], + "comments": "moderator comments", // string - moderator comments if any + "dateModerated": "2020-09-30T11:55:31Z", ISO format - UTC + "moderator": "John Smith", // string - moderator name + "tags": [ "click", "pop", "multiple" ], // added as a way to standardize what was heard in case they want to filter later or for AI training + "interestLabel": "humpback", // added as way to highlight a specific detection for inclusion in the Summary page and other front-facing locations + "_rid": "pF8dANwj9mtNFwAAAAAAAA==", + "_self": "dbs/pF8dAA==/colls/pF8dANwj9ms=/docs/pF8dANwj9mtNFwAAAAAAAA==/", + "_etag": "\"00000000-0000-0000-e1cf-0db2d8c501d9\"", + "_attachments": "attachments/", + "_ts": 1694120738 + } \ No newline at end of file