/// <inheritdoc />
 public override int GetHashCode()
 {
     unchecked
     {
         int hashCode = Mode.GetHashCode();
         hashCode = (hashCode * 397) ^ WaitForSave.GetHashCode();
         hashCode = (hashCode * 397) ^ SimulateDelay.GetHashCode();
         hashCode = (hashCode * 397) ^ RequestRecordMode.GetHashCode();
         hashCode = (hashCode * 397) ^ RequestPlaybackMode.GetHashCode();
         return(hashCode);
     }
 }
Beispiel #2
0
#pragma warning restore DF0000 // Marks undisposed anonymous objects from object creations.
#pragma warning restore DF0001 // Marks undisposed anonymous objects from method invocations.
        // ReSharper restore ExplicitCallerInfoArgument

        /// <summary>
        /// Records/playbacks the <see cref="HttpResponseMessage" /> to specified <see cref="HttpRequestMessage" />.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="getResponseAsync">The function to call if a recording is needed.</param>
        /// <param name="options">The options.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <param name="callerFilePath">The caller file path; set automatically.</param>
        /// <param name="callerMemberName">Name of the caller member; set automatically.</param>
        /// <param name="callerLineNumber">The caller line number; set automatically.</param>
        /// <returns>
        /// The <see cref="HttpResponseMessage" />.
        /// </returns>
        /// <exception cref="ArgumentNullException">request
        /// or
        /// request</exception>
        /// <exception cref="ArgumentOutOfRangeException">mode - null</exception>
        public async Task <HttpResponseMessage> RecordAsync(
            HttpRequestMessage request,
            GetResponseAsync getResponseAsync,
            CassetteOptions options                    = null,
            CancellationToken cancellationToken        = default(CancellationToken),
            [CallerFilePath] string callerFilePath     = "",
            [CallerMemberName] string callerMemberName = "",
            [CallerLineNumber] int callerLineNumber    = 0)
        {
            if (request is null)
            {
                throw new ArgumentNullException(nameof(request));
            }
            if (getResponseAsync is null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            // Overwrite defaults with options
            options = DefaultOptions & options;
            // ReSharper disable once PossibleInvalidOperationException
            RecordMode mode = options.Mode.Value;

            // If we're in 'none' mode skip recording.
            if (mode == RecordMode.None)
            {
                return(await getResponseAsync(request, cancellationToken).ConfigureAwait(false));
            }

            // Ensure request has been completed before serialization attempts
            if (!(request.Content is null))
            {
                await request.Content.LoadIntoBufferAsync().ConfigureAwait(false);
            }

            // Get key data.
            byte[] key = await KeyGenerator.Generate(request, cancellationToken);

            // Get key data hash.
            string hash = key.GetKeyHash();

            /*
             * Lock based on hash - so only one operation is allowed for the same hash at the same time.
             */
            IDisposable @lock = await _keyedSemaphoreSlim.WaitAsync(hash, cancellationToken);

            try
            {
                // Try to get recording from the store.
                Recording           recording;
                bool                found         = false;
                HttpResponseMessage response      = null;
                byte[]              recordingData = null;

                /*
                 * Unless we're in overwrite mode, try to get a response from the store.
                 */
                if (mode != RecordMode.Overwrite)
                {
                    // Logs an error
                    // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
                    void Error(string message, Exception exception = null, bool @throw = false)
                    {
                        CassetteException cassetteException = new CassetteException(
                            message,
                            Store.Name,
                            callerFilePath,
                            callerMemberName,
                            callerLineNumber,
                            exception);

                        if (@throw)
                        {
                            Logger.LogCritical(cassetteException);
                            throw cassetteException;
                        }

                        Logger.LogError(cassetteException);
                        recording = null;
                        found     = false;
                    }

                    try
                    {
                        recordingData = await Store.GetAsync(hash, cancellationToken);
                    }
                    catch (Exception e)
                    {
                        Error("The underlying store threw an exception when attempting to retrieve a recording.", e);
                    }

                    // If we got a response and it has more than 0 bytes consider it fond!
                    if (!(recordingData is null) && recordingData.Length > 0)
                    {
                        found = true;

                        // If we're in recording mode don't bother to deserialize it as we're not going to use it
                        if (mode != RecordMode.Record)
                        {
                            // Deserialize recording
                            try
                            {
                                recording = MessagePackSerializer.Deserialize <Recording>(recordingData,
                                                                                          RecorderResolver.Instance);
                            }
                            catch (Exception e)
                            {
                                Error("The recording could not be deserialized.", e);
                            }

                            // Validate key
                            if (found && !string.Equals(hash, recording.Hash))
                            {
                                Error("The recording's hash did not match, ignoring.");
                            }
                            if (found && !string.Equals(KeyGenerator.Name, recording.KeyGeneratorName))
                            {
                                Error("The recording's key generator name did not match, ignoring.");
                            }

                            /*
                             * If we're in playback or auto mode we need to replay response
                             */
                            if (found && (mode == RecordMode.Playback || mode == RecordMode.Auto))
                            {
                                if (recording.ResponseData is null)
                                {
                                    Error("No response data found in recording, ignoring.");
                                }

                                // Deserialize response
                                if (found)
                                {
                                    try
                                    {
                                        response = MessagePackSerializer.Deserialize <HttpResponseMessage>(
                                            recording.ResponseData, RecorderResolver.Instance);
                                    }
                                    catch (Exception e)
                                    {
                                        Error("Failed to deserialize the response from the store, ignoring.", e);
                                    }
                                }

                                // ReSharper disable once PossibleInvalidOperationException
                                RequestPlaybackMode requestPlaybackMode = options.RequestPlaybackMode.Value;
                                if (found)
                                {
                                    if (recording.RequestData is null ||
                                        requestPlaybackMode == RequestPlaybackMode.IgnoreRecorded)
                                    {
                                        if (requestPlaybackMode == RequestPlaybackMode.UseRecorded)
                                        {
                                            Error(
                                                "No request found in the recording, and in RequestPlaybackMode UseRecorded.",
                                                null,
                                                true);
                                        }

                                        // ReSharper disable once PossibleNullReferenceException
                                        response.RequestMessage = request;
                                    }
                                    else
                                    {
                                        // Deserialize request
                                        try
                                        {
#pragma warning disable DF0023 // Marks undisposed objects assinged to a property, originated from a method invocation.
                                            // ReSharper disable once PossibleNullReferenceException
                                            response.RequestMessage =
                                                MessagePackSerializer.Deserialize <HttpRequestMessage>(
                                                    recording.RequestData, RecorderResolver.Instance);
#pragma warning restore DF0023 // Marks undisposed objects assinged to a property, originated from a method invocation.
                                        }
                                        catch (Exception e)
                                        {
                                            if (requestPlaybackMode == RequestPlaybackMode.UseRecorded)
                                            {
                                                Error(
                                                    "Failed to deserialize the request from the store, and in RequestPlaybackMode UseRecorded.",
                                                    e,
                                                    true);
                                            }
                                            else
                                            {
                                                Error("Failed to deserialize the request from the store, ignoring.", e);
                                            }
                                        }
                                    }
                                }

                                // ReSharper disable once PossibleInvalidOperationException
                                if (found)
                                {
                                    TimeSpan simulateDelay = options.SimulateDelay.Value;
                                    if (simulateDelay != default(TimeSpan))
                                    {
                                        int delay = simulateDelay < TimeSpan.Zero
                                            ? recording.DurationMs
                                            : (int)simulateDelay.TotalMilliseconds;

                                        Logger.LogInformation(
                                            $"Responding with matching recording from '{recording.RecordedUtc.ToLocalTime()}' after {delay}ms simulated delay.",
                                            Store.Name,
                                            callerFilePath,
                                            callerMemberName,
                                            callerLineNumber);

                                        await Task.Delay(delay, cancellationToken);
                                    }
                                    else
                                    {
                                        Logger.LogInformation(
                                            $"Responding with matching recording from '{recording.RecordedUtc.ToLocalTime()}'.",
                                            Store.Name,
                                            callerFilePath,
                                            callerMemberName,
                                            callerLineNumber);
                                    }

                                    return(response);
                                }
                            }
                        }
                    }
                }

                // If we're in playback mode we've failed to get a recording so error
                if (mode == RecordMode.Playback)
                {
                    // Recording not found so error in playback mode.
                    CassetteNotFoundException exception = new CassetteNotFoundException(
                        Store.Name,
                        callerFilePath,
                        callerMemberName,
                        callerLineNumber);
                    Logger.LogError(exception);
                    throw exception;
                }

                /*
                 * Record original request to detect changes if options set to RequestRecordMode.RecordIfChanged
                 */
                byte[] requestData;
                // ReSharper disable once PossibleInvalidOperationException
                RequestRecordMode requestRecordMode = options.RequestRecordMode.Value;
                if (!found && requestRecordMode == RequestRecordMode.RecordIfChanged)
                {
                    // If the key was generated with the FullRequestKeyGenerator.Instance then the request is already serialized.
                    if (ReferenceEquals(KeyGenerator, FullRequestKeyGenerator.Instance))
                    {
                        requestData = key;
                    }
                    else
                    {
                        try
                        {
                            requestData = MessagePackSerializer.Serialize(request, RecorderResolver.Instance);
                        }
                        catch (Exception e)
                        {
                            CassetteException ce = new CassetteException(
                                "Failed to serialize the request.",
                                Store.Name,
                                callerFilePath,
                                callerMemberName,
                                callerLineNumber,
                                e);
                            Logger.LogCritical(ce);
                            throw ce;
                        }
                    }
                }
                else
                {
                    requestData = null;
                }

                /*
                 * Retrieve response from endpoint.
                 */
                int      durationMs;
                DateTime recordedUtc;
                try
                {
                    // Use stopwatch to record how long it takes to get a response.
                    Stopwatch stopwatch = Stopwatch.StartNew();
#pragma warning disable DF0010 // Marks undisposed local variables.
                    response = await getResponseAsync(request, cancellationToken).ConfigureAwait(false);

#pragma warning restore DF0010 // Marks undisposed local variables.
                    durationMs  = (int)stopwatch.ElapsedMilliseconds;
                    recordedUtc = DateTime.UtcNow;
                }
                catch (Exception e)
                {
                    // TODO We could save the exception an repeat on playback, useful for testing handlers
                    // Unfortunately MessagePack-CSharp doesn't support exception serialization normally so would need to be
                    // handled in a custom way.
                    CassetteException re = new CassetteException("Fatal error occured retrieving the response.",
                                                                 Store.Name,
                                                                 callerFilePath,
                                                                 callerMemberName,
                                                                 callerLineNumber,
                                                                 e);
                    Logger.LogError(re);
                    throw re;
                }

                // If we have a recording, don't overwrite just return the new response here.
                if (found)
                {
                    Logger.LogInformation(
                        "Existing recording found so not overwriting it.",
                        Store.Name,
                        callerFilePath,
                        callerMemberName,
                        callerLineNumber);
                    return(response);
                }


                // Serialize response
                byte[] responseData;
                try
                {
                    responseData = MessagePackSerializer.Serialize(response, RecorderResolver.Instance);
                }
                catch (Exception e)
                {
                    CassetteException re = new CassetteException(
                        "Failed to serialize response, not storing.",
                        Store.Name,
                        callerFilePath,
                        callerMemberName,
                        callerLineNumber,
                        e);
                    Logger.LogError(re);
                    return(response);
                }


                if (requestRecordMode != RequestRecordMode.Ignore)
                {
                    byte[] oldRequestData = requestData;
                    // Serialize the request
                    try
                    {
                        requestData =
                            MessagePackSerializer.Serialize(response.RequestMessage, RecorderResolver.Instance);

                        // If we're only recording requests on change, check for changes
                        if (requestRecordMode == RequestRecordMode.RecordIfChanged &&
                            // ReSharper disable once AssignNullToNotNullAttribute
                            requestData.SequenceEqual(oldRequestData))
                        {
                            requestData = null;
                        }
                    }
                    catch (Exception e)
                    {
                        CassetteException re = new CassetteException(
                            "Failed to serialize response's request message, so ignoring check.",
                            Store.Name,
                            callerFilePath,
                            callerMemberName,
                            callerLineNumber,
                            e);
                        Logger.LogError(re);
                        requestData = null;
                    }
                }

                // Create new recording
                recording = new Recording(
                    hash,
                    KeyGenerator.Name,
                    recordedUtc,
                    durationMs,
                    responseData,
                    requestData);

                // Finally serialize the recording
                try
                {
                    recordingData = MessagePackSerializer.Serialize(recording, RecorderResolver.Instance);
                }
                catch (Exception e)
                {
                    CassetteException re = new CassetteException(
                        "Failed to serialize recording, not storing.",
                        Store.Name,
                        callerFilePath,
                        callerMemberName,
                        callerLineNumber,
                        e);
                    Logger.LogError(re);
                    return(response);
                }

                // Set the response
                Logger.LogInformation(
                    $"Recording response at '{recordedUtc.ToLocalTime()}' (took {durationMs} ms).",
                    Store.Name,
                    callerFilePath,
                    callerMemberName,
                    callerLineNumber);

                if (options.WaitForSave == true)
                {
                    try
                    {
                        await Store.StoreAsync(hash, recordingData);
                    }
                    catch (Exception e)
                    {
                        // Just log the error.
                        CassetteException re = new CassetteException(
                            "Failed to store recording.",
                            Store.Name,
                            callerFilePath,
                            callerMemberName,
                            callerLineNumber,
                            e);
                        Logger.LogError(re);
                    }

                    // We can now dispose the lock safely.
                    @lock.Dispose();
                }
                else
                {
                    // Store the recording asynchronously, and don't wait the result (errors will be logged and suppressed).
                    StoreAsync(hash, recordingData, @lock, callerFilePath, callerMemberName, callerLineNumber);
                }

                // Return the response.
                return(response);
            }