public async Task HandleAsync_ShouldReturnFalseAndLogEvent_WhenPublishingThrows()
        {
            // Arrange
            var data = new RequestBlobAnalysisCreateDTO()
            {
                AnalyzerSpecificData = JsonHelpers.JsonToJObject(_expectedAnalyzerSpecificData, true),
                BlobUri          = new Uri(_expectedInboxUrl),
                OperationContext = JsonHelpers.DeserializeOperationContext(_expectedOperationContext),
            };
            var topicEndpointUri = new Uri("https://www.topichost.com");
            var testEvent        = new EventGridEvent
            {
                EventType   = CustomEventTypes.RequestBlobAnalysisCreate,
                DataVersion = "1.0",
                Data        = JsonConvert.SerializeObject(data)
            };
            JObject report = JsonHelpers.JsonToJObject("{}", true);

            // Arrange Mocks
            Mock.Get(_settingsProvider)
            .Setup(x => x.GetAppSettingsValue(Publishing.TopicOutboundEndpointSettingName))
            .Returns(topicEndpointUri.ToString());
            JObject blobMetadata = null;

            Mock.Get(_storageService)
            .Setup(x => x.GetBlobMetadataAsync(It.IsAny <Uri>(), It.IsAny <StorageClientProviderContext>()))
            .ReturnsAsync(blobMetadata);
            Mock.Get(_eventGridPublisher)
            .Setup(x => x.PublishEventToTopic(It.IsAny <EventGridEvent>()))
            .ThrowsAsync(new InvalidOperationException());
            Mock.Get(_mediaInfoReportService)
            .Setup(x => x.GetMediaInfoCompleteInformForUriAsync(data.BlobUri, It.IsAny <StorageClientProviderContext>()))
            .ReturnsAsync(report);

            // Act
            var handleAsyncResult = await _handler.HandleAsync(testEvent).ConfigureAwait(true);

            // Assert
            handleAsyncResult.ShouldBe(false);
            Mock.Get(_logger).Verify(x =>
                                     x.LogExceptionObject(LogEventIds.FailedCriticallyToPublishEvent,
                                                          It.IsAny <InvalidOperationException>(), It.IsAny <object>()),
                                     Times.AtLeastOnce,
                                     "An exception should be logged when the publishing fails");
        }
        /// <summary>
        /// Convert an OperationContext string into a JObject.  The conversion allows for handling
        /// of blank/null or GUID context strings input, in addition to the normal JSON string format.
        /// </summary>
        /// <remarks>
        /// As the most common init/reset will likely be via resetting the ClientRequest
        /// string, be lazy about ensuring that the JObject equivalent is present/correct.
        /// </remarks>
        /// <param name="operationContextString">The JSON string count on a ClientRequestId header.
        /// e.g. <c>{"abc":"def"}</c></param>
        /// <param name="isArtificial">True if the return value was not the result of a simple JSON
        /// conversion of the input string.  i.e., if the return value is some JSON wrapping of the
        /// input string which was then converted to a JObject.</param>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown if the method couldn't
        /// understand the input and therefore couldn't perform the converstion to JObject</exception>
        private static JObject StringContextToJObject(string operationContextString, out bool isArtificial)
        {
            isArtificial = true; // i.e., we couldn't successfully parse it directly into a JObject.

            // If nothing, return a non-property JObject
            if (string.IsNullOrWhiteSpace(operationContextString))
            {
                return(new JObject());
            }

            JObject result = null;

            try
            {
                result       = JsonHelpers.DeserializeOperationContext(operationContextString);
                isArtificial = false;
            }
            catch (JsonReaderException)
            {
                // Well, it's not the well-formed JSON we'd hoped for. We'll create an empty context.
                // Most likely, it's a GUID.  If so, process it into a fully-blown, but not muted
                // See commentary at top of file for why this is not muted.  Why encapsulate the
                // GUID into JSON?  Simply to not lose the value.
                if (Guid.TryParse(operationContextString, out Guid g))
                {
                    result = new JObject();
                    result.Add(GuidPropertyName, g.ToString());
                }
                else
                {
                    // We know it's not empty/blanks, JSON, or a GUID. Time to give up.
                    throw new ArgumentOutOfRangeException(
                              nameof(operationContextString),
                              operationContextString,
                              "Could not parse operationContext");
                }
            }
            return(result);
        }
        /// <summary>
        /// Create a context instance, being more lenient than the constructors regarding
        /// the input value.  In addition to the normal JSON input, this method will also
        /// tolerate null/empty strings, GUID strings and other non-JSON strings.  It
        /// produces a context for each.  For any of the tolerated cases, it will produce
        /// either an empty context or a wrap-up of the passed value.  Muting, etc. are
        /// processed/included in those results as dictated by the parameters.
        ///
        /// If the caller provides either logging Action argument, they will be used to
        /// records errors encountered.
        /// </summary>
        /// <remarks>
        /// This method does not throw an exception for non-JSON cases, as the constructors
        /// tend to (thus "Safe" in the name).  But, it retains a code path that will
        /// log/throw an exception on bad input.  This should never happen in real use
        /// since if the code reaches that point, it will be that the method itself constructed
        /// JSON that was unparseable.  The code remains to ensure that will be logged if
        /// logger Actions were given and the problem somehow occurs.  Generally, application
        /// code wouldn't bother using a try/catch around calls to CreateSafe - otherwise, a
        /// constructor may suffice.
        /// </remarks>
        /// If there is an error (e.g. JSON parsing or otherwise), and the corresponging log*Error
        /// argument is non-null, invoke that to let the caller log the error.
        /// </summary>
        /// <param name="operationContextString">
        /// A string representing the Operation Context as a single JSON object.
        /// e.g. { "a" : "b" }</param>
        /// <param name="muteContext">If true, context should be set as muted -- i.e., intended for
        /// internal operations whose resulting notifications (if any) are not expected to be routed
        /// to callers (e.g., Requestor).</param>
        /// <param name="trackETag">If true, send the eTag value on each request (if it's not currently
        /// empty).</param>
        /// <param name="eTag">The current eTag string.  With each response, this value is updated to
        /// reflect the value returned last from Azure Storage (regardless of trackETag's value).
        /// For each HTTP request, the eTag value is only set if trackETag is true and the value of eTag
        /// is not empty.</param>
        /// <param name="logParseError">A callback invoked (if non-null) should a JSON parse error
        /// occur in processing operationContextString. Second argument to this callback is string that
        /// was being parsed.</param>
        /// <param name="logOtherError">A callback invoked (if non-null) should an exception, other
        ///  than a JSON parse error occur in processing operationContextString.</param>
        /// <returns>An instance of StorageProviderContext.</returns>
        public static StorageClientProviderContext CreateSafe(
            string operationContextString,
            bool?muteContext = null,
            bool?trackETag   = null,
            string eTag      = null,
            Action <Exception, string> logParseError = null,
            Action <Exception> logOtherError         = null)
        {
            var muteContextSpecified = muteContext.HasValue;

            if (!trackETag.HasValue)
            {
                trackETag = false; // default, if none given
            }

            JObject contextObject         = null;
            bool    haveLoggedParseIssues = false;

            var emptyInput = string.IsNullOrWhiteSpace(operationContextString);

            // #1 -- handle case where there's some string to try as JSON
            if (!emptyInput)
            {
                try
                {
                    contextObject = JsonHelpers.DeserializeOperationContext(operationContextString);
                }
                catch (Exception ep)
                {
                    logParseError?.Invoke(ep, $"Error parsing Storage operation context from: '{operationContextString}'");
                    haveLoggedParseIssues = true;
                    // and continue...
                }
            }

            // #2 -- didn't work as JSON, check it for GUID, etc.
            if (contextObject == null)
            {
                try
                {
                    contextObject = StringContextToJObject(operationContextString, out bool _);
                }
                catch (Exception es)
                {
                    if (!haveLoggedParseIssues)
                    {
                        logParseError?.Invoke(es, $"Error converting Storage operation context from: '{operationContextString}'");
                        haveLoggedParseIssues = true;
                        // and continue...
                    }
                    // Something (really) went wrong trying to create the StorageContext instance.
                    // It could be being passed incomplete JSON  or someone sending in a random sentence of text.
                    // Rather than stop the Gridwich with an exception, wrap whatever was given into
                    // a blank OperationContext as the value of a known JSON property.
                    contextObject = new JObject();
                    contextObject.Add(GeneralPropertyName, operationContextString);
                }
            }

            // #3 -- we finally have something to use as an OperationContext, now just wrap it up in a StorageContext.
            StorageClientProviderContext result = null;

            try
            {
                result = new StorageClientProviderContext(contextObject, muteContext, trackETag, eTag);
            }
            catch (Exception eo)
            {
                // If we get here, it's not the fault of the caller.  It means that this
                // method has somehow manipulated the input string into invalid JSON.
                // This should not occur in the real world.
                logOtherError?.Invoke(eo);
                throw;
            }

            return(result);
        }
        public async Task HandleAsync_ShouldReturnTrueAndNotLog_WhenNoErrors()
        {
            // Arrange
            var data = new RequestBlobAnalysisCreateDTO()
            {
                AnalyzerSpecificData = JsonHelpers.JsonToJObject(_expectedAnalyzerSpecificData, true),
                BlobUri          = new Uri(_expectedInboxUrl),
                OperationContext = JsonHelpers.DeserializeOperationContext(_expectedOperationContext),
            };
            var topicEndpointUri = new Uri("https://www.topichost.com");
            var testEvent        = new EventGridEvent
            {
                EventType   = CustomEventTypes.RequestBlobAnalysisCreate,
                DataVersion = "1.0",
                Data        = JsonConvert.SerializeObject(data)
            };
            const string   connectionString = "CONNECTION_STRING";
            JObject        report           = JsonHelpers.JsonToJObject("{}", true);
            JObject        metadata         = JsonHelpers.JsonToJObject("{}", true);
            EventGridEvent resultEvent      = null;

            // Arrange Mocks
            Mock.Get(_settingsProvider)
            .Setup(x => x.GetAppSettingsValue(Publishing.TopicOutboundEndpointSettingName))
            .Returns(topicEndpointUri.ToString());
            Mock.Get(_storageService)
            .Setup(x => x.GetConnectionStringForAccount(It.IsAny <string>(), It.IsAny <StorageClientProviderContext>()))
            .Returns(connectionString);
            Mock.Get(_eventGridPublisher)
            .Setup(x => x.PublishEventToTopic(It.IsAny <EventGridEvent>()))
            .Callback <EventGridEvent>((eventGridEvent) => resultEvent = eventGridEvent)
            .ReturnsAsync(true);
            Mock.Get(_mediaInfoReportService)
            .Setup(x => x.GetMediaInfoCompleteInformForUriAsync(data.BlobUri, It.IsAny <StorageClientProviderContext>()))
            .ReturnsAsync(report);
            Mock.Get(_storageService)
            .Setup(x => x.GetBlobMetadataAsync(data.BlobUri, It.IsAny <StorageClientProviderContext>()))
            .ReturnsAsync(metadata);

            // Act
            var handleAsyncResult = await _handler.HandleAsync(testEvent).ConfigureAwait(true);

            // Assert positive results
            handleAsyncResult.ShouldBe(true, "handleAsync should always return true");

            resultEvent.ShouldNotBeNull();
            resultEvent.Data.ShouldNotBeNull();
            resultEvent.Data.ShouldBeOfType(typeof(ResponseBlobAnalysisSuccessDTO));

            ResponseBlobAnalysisSuccessDTO eventData = (ResponseBlobAnalysisSuccessDTO)resultEvent.Data;

            eventData.AnalysisResult.ShouldNotBeNull();
            eventData.BlobMetadata.ShouldNotBeNull();
            eventData.BlobUri.ToString().ShouldBe(_expectedInboxUrl);
            // eventData.Md5.ShouldBe(_expectedMd5);  // TODO
            eventData.AnalysisResult.ShouldBe(report);

            Mock.Get(_logger).Verify(x =>
                                     x.LogEventObject(LogEventIds.StartingEventHandling, It.IsAny <object>()),
                                     Times.Once,
                                     "An accepted event type should log information when it is about to begin");
            Mock.Get(_logger).Verify(x =>
                                     x.LogEventObject(LogEventIds.AboutToCallAnalysisDeliveryEntry, It.IsAny <object>()),
                                     Times.Once,
                                     "An accepted event type should log information when it is about to begin analysis");
            Mock.Get(_logger).Verify(x =>
                                     x.LogEventObject(LogEventIds.AnalysisOfDeliveryFileSuccessful, It.IsAny <object>()),
                                     Times.Once,
                                     "An accepted event type should log information when analysis is successful");
            Mock.Get(_logger).Verify(x =>
                                     x.LogEventObject(LogEventIds.FinishedEventHandling, It.IsAny <object>()),
                                     Times.Once,
                                     "An accepted event type should log information when the event handling is complete");
            // Assert negative results
            Mock.Get(_logger).Verify(x =>
                                     x.LogExceptionObject(LogEventIds.EventNotSupported, It.IsAny <Exception>(), It.IsAny <object>()),
                                     Times.Never,
                                     "An exception should NOT be logged when the publishing succeeds");
            Mock.Get(_logger).Verify(x =>
                                     x.LogExceptionObject(LogEventIds.FailedToPublishEvent, It.IsAny <Exception>(), It.IsAny <object>()),
                                     Times.Never,
                                     "An exception should NOT be logged when the publishing succeeds");
        }
        public async Task HandleAsync_ShouldHandleReportGenerationResult_WhenReportGenerationFails()
        {
            // Arrange
            var fileUri = new Uri(_expectedInboxUrl);
            var data    = new RequestBlobAnalysisCreateDTO()
            {
                AnalyzerSpecificData = JsonHelpers.JsonToJObject(_expectedAnalyzerSpecificData, true),
                BlobUri          = new Uri(_expectedInboxUrl),
                OperationContext = JsonHelpers.DeserializeOperationContext(_expectedOperationContext),
            };
            var topicEndpointUri = new Uri("https://www.topichost.com");
            var appInsightsUri   = new Uri("https://www.appinsights.com");
            var testEvent        = new EventGridEvent
            {
                EventType   = CustomEventTypes.RequestBlobAnalysisCreate,
                DataVersion = "1.0",
                Data        = JsonConvert.SerializeObject(data)
            };
            var storageContext = new StorageClientProviderContext(_expectedOperationContext);

            const string   connectionString = "CONNECTION_STRING";
            EventGridEvent resultEvent      = null;

            // Arrange Mocks
            Mock.Get(_settingsProvider)
            .Setup(x => x.GetAppSettingsValue(Publishing.TopicOutboundEndpointSettingName))
            .Returns(topicEndpointUri.ToString());
            Mock.Get(_storageService)
            .Setup(x => x.GetConnectionStringForAccount(It.IsAny <string>(), It.IsAny <StorageClientProviderContext>()))
            .Returns(connectionString);
            Mock.Get(_eventGridPublisher)
            .Setup(x => x.PublishEventToTopic(It.IsAny <EventGridEvent>()))
            .Callback <EventGridEvent>((eventGridEvent) => resultEvent = eventGridEvent)
            .ReturnsAsync(true);
            // Note the exact LogEventId does not matter
            Mock.Get(_mediaInfoReportService)
            .Setup(x => x.GetMediaInfoCompleteInformForUriAsync(fileUri, It.IsAny <StorageClientProviderContext>()))
            .ThrowsAsync(new GridwichMediaInfoLibException("Error", LogEventIds.MediaInfoLibOpenBufferInitFailed, storageContext.ClientRequestIdAsJObject));
            JObject blobMetadata = null;

            Mock.Get(_storageService)
            .Setup(x => x.GetBlobMetadataAsync(It.IsAny <Uri>(), It.IsAny <StorageClientProviderContext>()))
            .ReturnsAsync(blobMetadata);
            Mock.Get(_logger)
            .Setup(x => x.LogEvent(
                       out appInsightsUri,
                       LogEventIds.AnalysisOfDeliveryFileFailed.Id,
                       It.IsAny <string>()));
            Mock.Get(_logger)
            .Setup(x => x.LogExceptionObject(
                       out appInsightsUri,
                       LogEventIds.FailedCriticallyToPublishEvent,
                       It.IsAny <GridwichMediaInfoLibException>(),
                       It.IsAny <object>()));

            // Act
            var handleAsyncResult = await _handler.HandleAsync(testEvent).ConfigureAwait(true);

            // Assert positive results
            handleAsyncResult.ShouldBe(false, "handleAsync should still make an event on failure");

            resultEvent.ShouldNotBeNull();
            resultEvent.Data.ShouldNotBeNull();
            resultEvent.Data.ShouldBeOfType(typeof(ResponseFailureDTO));

            var eventData = (ResponseFailureDTO)resultEvent.Data;

            eventData.HandlerId.ShouldBe("A7250940-98C8-4CC5-A66C-45A2972FF3A2");
            eventData.LogEventMessage.ShouldNotBeNullOrEmpty();
            eventData.EventHandlerClassName.ShouldNotBeNullOrEmpty();
            eventData.OperationContext.ShouldBe(JsonHelpers.JsonToJObject(_expectedOperationContext, true));

            Mock.Get(_logger).Verify(x =>
                                     x.LogExceptionObject(out appInsightsUri, It.IsAny <EventId>(),
                                                          It.IsAny <GridwichMediaInfoLibException>(), It.IsAny <object>()),
                                     Times.Once,
                                     "An exception should be logged when report generation fails");
        }