diff --git a/Behavioral.Automation.API/Behavioral.Automation.API.csproj b/Behavioral.Automation.API/Behavioral.Automation.API.csproj new file mode 100644 index 00000000..4de88ee4 --- /dev/null +++ b/Behavioral.Automation.API/Behavioral.Automation.API.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + Debug;Release;Test;Dev;Prod + AnyCPU + + + + + + + + + + + + + + + + + + + diff --git a/Behavioral.Automation.API/Bindings/HttpRequestSteps.cs b/Behavioral.Automation.API/Bindings/HttpRequestSteps.cs new file mode 100644 index 00000000..e6dd9864 --- /dev/null +++ b/Behavioral.Automation.API/Bindings/HttpRequestSteps.cs @@ -0,0 +1,194 @@ +using System.Text; +using System.Web; +using Behavioral.Automation.API.Configs; +using Behavioral.Automation.API.Context; +using Behavioral.Automation.API.Models; +using Behavioral.Automation.API.Services; +using Behavioral.Automation.Configs; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Assist; + +namespace Behavioral.Automation.API.Bindings; + +[Binding] +public class HttpRequestSteps +{ + private readonly ApiContext _apiContext; + private readonly HttpService _httpService; + + public HttpRequestSteps(ApiContext apiContext, HttpService httpService) + { + _apiContext = apiContext; + _httpService = httpService; + } + + [When("user sends a \"(.*)\" request to \"(.*)\" url")] + public HttpResponseMessage UserSendsHttpRequest(string httpMethod, string url) + { + var method = new HttpMethod(httpMethod.ToUpper()); + + if(!IsAbsoluteUrl(url)) + { + url = ConfigManager.GetConfig().ApiHost + url; + } + + _apiContext.Request = new HttpRequestMessage(method, url); + _httpService.SendContextRequest(); + + return _apiContext.Response; + } + + [When("user sends a \"(.*)\" request to \"(.*)\" url with the following parameters:")] + public HttpResponseMessage UserSendsHttpRequestWithParameters(string httpMethod, string url, Table tableParameters) + { + UserCreatesHttpRequestWithParameters(httpMethod, url, tableParameters); + _httpService.SendContextRequest(); + + return _apiContext.Response; + } + + [When("user sends a \"(.*)\" request to \"(.*)\" url with the json:")] + public HttpResponseMessage UserSendsHttpRequestWithParameters(string httpMethod, string url, string jsonToSend) + { + //TODO: add support for the following types of body block: + // form-data + // x-www-form-urlencoded + // raw (Text, JavaScript, JSON, HTML, XML) + // binary + // GraphQL + // Consider adding other types + var method = new HttpMethod(httpMethod.ToUpper()); + if(!IsAbsoluteUrl(url)) + { + url = ConfigManager.GetConfig().ApiHost + url; + } + var uriBuilder = new UriBuilder(url); + + _apiContext.Request = new HttpRequestMessage(method, uriBuilder.Uri); + _apiContext.Request.Content = new StringContent(jsonToSend, Encoding.UTF8, "application/json"); + + _httpService.SendContextRequest(); + + return _apiContext.Response; + } + + [Given("user creates a \"(.*)\" request to \"(.*)\" url with the json:")] + public HttpRequestMessage UserCreatesHttpRequestWithJson(string httpMethod, string url, string jsonToSend) + { + var method = new HttpMethod(httpMethod.ToUpper()); + if(!IsAbsoluteUrl(url)) + { + url = ConfigManager.GetConfig().ApiHost + url; + } + var uriBuilder = new UriBuilder(url); + + _apiContext.Request = new HttpRequestMessage(method, uriBuilder.Uri); + _apiContext.Request.Content = new StringContent(jsonToSend, Encoding.UTF8, "application/json"); + return _apiContext.Request; + } + + [Given("user creates a \"(.*)\" request to \"(.*)\" url")] + public HttpRequestMessage GivenUserCreatesARequestToUrl(string httpMethod, string url) + { + var method = new HttpMethod(httpMethod.ToUpper()); + if(!IsAbsoluteUrl(url)) + { + url = ConfigManager.GetConfig().ApiHost + url; + } + var uriBuilder = new UriBuilder(url); + + _apiContext.Request = new HttpRequestMessage(method, uriBuilder.Uri); + return _apiContext.Request; + } + + [Given("user creates a \"(.*)\" request to \"(.*)\" url with the following parameters:")] + public HttpRequestMessage UserCreatesHttpRequestWithParameters(string httpMethod, string url, Table tableParameters) + { + var method = new HttpMethod(httpMethod.ToUpper()); + if(!IsAbsoluteUrl(url)) + { + url = ConfigManager.GetConfig().ApiHost + url; + } + + _apiContext.Request = new HttpRequestMessage(method, url); + AddParametersToRequest(_apiContext.Request, tableParameters); + return _apiContext.Request; + } + + [When("user adds a body and send the request:")] + public HttpResponseMessage WhenUserAddsJsonBodyAndSendRequest(string jsonToSend) + { + _apiContext.Request.Content = new StringContent(jsonToSend, Encoding.UTF8, "application/json"); + + _httpService.SendContextRequest(); + + return _apiContext.Response; + } + + [When("user adds parameters and send request:")] + public HttpResponseMessage WhenUserAddsParametersAndSendRequest(Table tableParameters) + { + AddParametersToRequest(_apiContext.Request, tableParameters); + _httpService.SendContextRequest(); + + return _apiContext.Response; + } + + [When("user sends request")] + public HttpResponseMessage WhenUserSendsRequest() + { + _httpService.SendContextRequest(); + return _apiContext.Response; + } + + [Given("the response status code should be \"(\\d*)\"")] + public void ChangeResponseStatusCode(int statusCode) + { + _apiContext.ExpectedStatusCode = statusCode; + } + + private static bool IsAbsoluteUrl(string url) + { + return Uri.IsWellFormedUriString(url, UriKind.Absolute); + } + + private static void AddParametersToRequest(HttpRequestMessage request, Table tableParameters) + { + var uriBuilder = new UriBuilder(request.RequestUri); + + var parameters = tableParameters.CreateSet(); + + var headers = new List>>(); + if (parameters is not null) + { + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + foreach (var parameter in parameters) + { + var parameterKind = Enum.Parse(parameter.Kind); + + if (parameterKind is RequestParameterKind.Param) + { + query.Add(parameter.Name, parameter.Value); + } + + if (parameterKind is RequestParameterKind.Header) + { + var headerValue = parameter.Value.Trim().Split(","); + headers.Add(new KeyValuePair>(parameter.Name, headerValue)); + } + } + + uriBuilder.Query = query.ToString(); + } + + request.RequestUri = uriBuilder.Uri; + + if (headers.Any()) + { + foreach (var header in headers) + { + request.Headers.Add(header.Key, header.Value); + } + } + } +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Bindings/HttpResponseSteps.cs b/Behavioral.Automation.API/Bindings/HttpResponseSteps.cs new file mode 100644 index 00000000..2d391450 --- /dev/null +++ b/Behavioral.Automation.API/Bindings/HttpResponseSteps.cs @@ -0,0 +1,282 @@ +using System.Text.RegularExpressions; +using Behavioral.Automation.API.Context; +using Behavioral.Automation.API.Models; +using Behavioral.Automation.API.Services; +using Behavioral.Automation.Configs.utils; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Polly; +using TechTalk.SpecFlow; + +namespace Behavioral.Automation.API.Bindings; + +[Binding] +public class HttpResponseSteps +{ + private readonly ApiContext _apiContext; + private readonly HttpService _httpService; + private readonly int _retryCount = 10; + private readonly TimeSpan _retryDelay = TimeSpan.FromSeconds(3); + + public HttpResponseSteps(ApiContext apiContext, HttpService httpService) + { + _apiContext = apiContext; + _httpService = httpService; + } + + [Then("response attachment filename is \"(.*)\"")] + public void ThenResponseAttachmentFilenameIs(string filename) + { + if (_apiContext.Response is null) throw new NullReferenceException("Http response is empty."); + var contentDispositionHeader = _apiContext.Response.Content.Headers.ContentDisposition; + if (contentDispositionHeader == null) + { + Assert.Fail("Response header \"ContentDisposition disposition\" is null"); + } + if (!contentDispositionHeader.ToString().StartsWith("attachment")) + { + Assert.Fail("Content disposition is not attachment?"); + } + + if (!contentDispositionHeader.FileName.Equals(filename)) + { + Assert.Fail($"filename is wrong.\n\nActual result: {contentDispositionHeader.FileName}\nExpected result: {filename}"); + } + } + + [Given("response attachment is saved as \"(.*)\"")] + public void GivenResponseAttachmentIsSavedAs(string filePath) + { + if (_apiContext.Response is null) throw new NullReferenceException("Http response is empty."); + var fullPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), filePath)) + .NormalizePathAccordingOs(); + + var directoryPath = Path.GetDirectoryName(fullPath); + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + var responseContentByteArray = _apiContext.Response.Content.ReadAsByteArrayAsync().Result; + File.WriteAllBytes(fullPath, responseContentByteArray); + } + + [Then("response json path \"(.*)\" value should match regex \"(.*)\"")] + public void ThenResponseJsonPathValueShouldMatchRegexp(string jsonPath, string regex) + { + var actualJTokens = GetActualJTokensFromResponse(jsonPath); + if (actualJTokens.Count != 1) + { + Assert.Fail($"Error! To check regexp match, json path should return single value. Number of returned values is {actualJTokens.Count}"); + } + var stringToCheck = actualJTokens[0].ToString(); + if (!Regex.IsMatch(stringToCheck, regex)) + { + Assert.Fail($"Response json value '{stringToCheck}' doesn't match regexp {regex}"); + } + } + + [Then("response json path \"(.*)\" value should (be|become):")] + [Then("response json path \"(.*)\" value should (be|become) \"(.*)\"")] + public void CheckResponseJsonPath(string jsonPath, AssertionType assertionType, string expected) + { + expected = AddSquareBrackets(expected); + + JToken parsedExpectedJson; + try + { + parsedExpectedJson = JToken.Parse(expected); + } + catch (JsonReaderException e) + { + throw new ArgumentException($"Error while parsing \"{expected}\". Expected value should be a valid json", e); + } + + var expectedJTokens = parsedExpectedJson.Select(token => token).ToList(); + + var actualJTokens = GetActualJTokensFromResponse(jsonPath); + + if (actualJTokens.Count != expectedJTokens.Count) + { + if (assertionType == AssertionType.Become) + { + Policy.HandleResult(count => !count.Equals(expectedJTokens.Count)) + .WaitAndRetry(_retryCount, _ => _retryDelay).Execute(() => + { + _httpService.SendContextRequest(); + actualJTokens = GetActualJTokensFromResponse(jsonPath); + return actualJTokens.Count; + }); + } + + if (actualJTokens.Count != expectedJTokens.Count) + FailJTokensAssertion(actualJTokens, expectedJTokens, "Elements count mismatch."); + } + + if (!IsJTokenListsSimilar(expectedJTokens, actualJTokens)) + { + if (assertionType == AssertionType.Become) + { + Policy.HandleResult>(_ => !IsJTokenListsSimilar(expectedJTokens, actualJTokens)) + .WaitAndRetry(_retryCount, _ => _retryDelay).Execute(() => + { + _httpService.SendContextRequest(); + actualJTokens = GetActualJTokensFromResponse(jsonPath); + return actualJTokens; + }); + } + + if (!IsJTokenListsSimilar(expectedJTokens, actualJTokens)) + FailJTokensAssertion(actualJTokens, expectedJTokens, + "The actual result is not equal to the expected result."); + } + } + + [Then("response json path \"(.*)\" value should not (be|become) empty")] + public void CheckResponseJsonPathNotEmpty(string jsonPath, AssertionType assertionType) + { + var actualJTokens = GetActualJTokensFromResponse(jsonPath); + + if (actualJTokens.Count == 0) + { + if (assertionType == AssertionType.Become) + { + Policy.HandleResult(count => count == 0).WaitAndRetry(_retryCount, _ => _retryDelay).Execute(() => + { + _httpService.SendContextRequest(); + actualJTokens = GetActualJTokensFromResponse(jsonPath); + return actualJTokens.Count; + }); + } + + if (actualJTokens.Count == 0) Assert.Fail("Expected response json path value is empty"); + } + } + + [Then("response json path \"(.*)\" count should be \"(\\d*)\"")] + public void ThenResponseJsonPathValueShouldBecome(string jsonPath, int expectedQuantity) + { + var actualQuantity = GetActualJTokensFromResponse(jsonPath).Count; + Assert.AreEqual(expectedQuantity, actualQuantity); + } + + + [Given("expected response status code is \"(\\d*)\"")] + public void ChangeResponseStatusCode(int statusCode) + { + _apiContext.ExpectedStatusCode = statusCode; + } + + [Then("response time is less then \"(.*)\" millis")] + public void ThenResponseTimeIsLessThenMillis(string timeoutString) + { + var timeout = Convert.ToInt64(timeoutString); + Assert.Less(_apiContext.ResponseTimeMillis, timeout, + $"API response time should be less then {timeout}, but was {_apiContext.ResponseTimeMillis}"); + } + + [Then("response json path \"(.*)\" should be equal to the file \"(.*)\"")] + public void ThenTheResponseJsonPathShouldBeEqualToFile(string jsonPath, string filePath) + { + var fullPath = Path.GetFullPath(Path.Join(Directory.GetCurrentDirectory(), filePath)) + .NormalizePathAccordingOs(); + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException("The file does not exist", fullPath); + } + + var actualJTokens = GetActualJTokensFromResponse(jsonPath); + var expectedJTokens = GetExpectedJTokensFromFile(fullPath); + if (!IsJTokenListsSimilar(expectedJTokens, actualJTokens)) + FailJTokensAssertion(actualJTokens, expectedJTokens, + "The actual result is not equal to the expected result."); + } + + + private static bool IsJTokenListsSimilar(List expectedJTokens, List actualJTokens) + { + if (expectedJTokens.Count == 0 && actualJTokens.Count == 0) return true; + bool areEqual = false; + foreach (var expectedJToken in expectedJTokens) + { + foreach (var actualJToken in actualJTokens) + { + if (JToken.DeepEquals(expectedJToken, actualJToken)) + { + areEqual = true; + break; + } + + areEqual = false; + } + + if (!areEqual) + { + return false; + } + } + + return areEqual; + } + + private List GetActualJTokensFromResponse(string jsonPath) + { + if (_apiContext.Response is null) throw new NullReferenceException("Http response is empty."); + var responseString = _apiContext.Response.Content.ReadAsStringAsync().Result; + + JToken responseJToken; + try + { + responseJToken = JToken.Parse(responseString); + } + catch (JsonReaderException e) + { + throw new ArgumentException("Response content is not a valid json", e); + } + + var actualJTokens = responseJToken.SelectTokens(jsonPath, false).ToList(); + return actualJTokens; + } + + private static string AddSquareBrackets(string expected) + { + if (!expected.Trim().StartsWith("[")) + { + expected = expected.Insert(0, "["); + } + + if (!expected.Trim().EndsWith("]")) + { + expected = expected.Insert(expected.Length, "]"); + } + + return expected; + } + + private static void FailJTokensAssertion(List actualJTokens, List expectedJTokens, + string? message = null) + { + var actualJson = JsonConvert.SerializeObject(actualJTokens); + var expectedJson = JsonConvert.SerializeObject(expectedJTokens); + message = string.IsNullOrEmpty(message) ? message : message + Environment.NewLine; + Assert.Fail($"{message}Actual: {actualJson}{Environment.NewLine}Expected: {expectedJson}"); + } + + private List GetExpectedJTokensFromFile(string filePath) + { + var expectedString = File.ReadAllText(filePath); + + JToken responseJToken; + try + { + responseJToken = JToken.Parse(expectedString); + } + catch (JsonReaderException e) + { + throw new ArgumentException($"File {filePath} is not a valid json", e); + } + + return responseJToken.ToList(); + } +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Bindings/SaveToContextSteps.cs b/Behavioral.Automation.API/Bindings/SaveToContextSteps.cs new file mode 100644 index 00000000..cb0523ef --- /dev/null +++ b/Behavioral.Automation.API/Bindings/SaveToContextSteps.cs @@ -0,0 +1,71 @@ +using Behavioral.Automation.API.Context; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using TechTalk.SpecFlow; +using TechTalk.SpecFlow.Infrastructure; + +namespace Behavioral.Automation.API.Bindings; + +[Binding] +public class SaveToContextSteps +{ + private readonly ApiContext _apiContext; + private readonly ScenarioContext _scenarioContext; + private readonly ISpecFlowOutputHelper _specFlowOutputHelper; + + public SaveToContextSteps(ApiContext apiContext, ScenarioContext scenarioContext, ISpecFlowOutputHelper specFlowOutputHelper) + { + _apiContext = apiContext; + _scenarioContext = scenarioContext; + _specFlowOutputHelper = specFlowOutputHelper; + } + + [Given("save response json path \"(.*)\" as \"(.*)\"")] + [When("save response json path \"(.*)\" as \"(.*)\"")] + public void SaveResponseJsonPathAs(string jsonPath, string variableName) + { + var stringToSave = GetStringByJsonPath(jsonPath); + _scenarioContext.Add(variableName, stringToSave); + _specFlowOutputHelper.WriteLine($"Saved '{stringToSave}' with key '{variableName}' in scenario context"); + } + + public string GetStringByJsonPath(string jsonPath) + { + var actualJTokens = GetActualJTokensFromResponse(jsonPath); + var stringToSave = ConvertJTokensToString(actualJTokens); + if (stringToSave.Equals("[]")) + { + Assert.Fail($"Empty value by jsonpath {jsonPath}. Can't save empty string in scenario context."); + } + return stringToSave; + } + + public string ConvertJTokensToString(List tokens) + { + return tokens.Count == 1 ? tokens[0].ToString() : JsonConvert.SerializeObject(tokens); + } + + public List GetActualJTokensFromResponse(string jsonPath) + { + if (_apiContext.Response is null) throw new NullReferenceException("Http response is empty."); + var responseString = _apiContext.Response.Content.ReadAsStringAsync().Result; + + JToken responseJToken; + try + { + responseJToken = JToken.Parse(responseString); + } + catch (JsonReaderException e) + { + throw new ArgumentException("Response content is not a valid json", e); + } + + var actualJTokens = responseJToken.SelectTokens(jsonPath, false).ToList(); + if (actualJTokens == null) + { + Assert.Fail($"No value by json path: {jsonPath}"); + } + return actualJTokens; + } +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Configs/Config.cs b/Behavioral.Automation.API/Configs/Config.cs new file mode 100644 index 00000000..491b1bc3 --- /dev/null +++ b/Behavioral.Automation.API/Configs/Config.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Configuration; + +namespace Behavioral.Automation.API.Configs; + +public class Config +{ + [ConfigurationKeyName("API_HOST")] + public string ApiHost { get; set; } +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Context/ApiContext.cs b/Behavioral.Automation.API/Context/ApiContext.cs new file mode 100644 index 00000000..eb9277ef --- /dev/null +++ b/Behavioral.Automation.API/Context/ApiContext.cs @@ -0,0 +1,22 @@ +namespace Behavioral.Automation.API.Context; + +public class ApiContext +{ + private HttpRequestMessage _request; + + public HttpRequestMessage Request + { + get => _request; + set + { + _request = value; + ExpectedStatusCode = 200; + } + } + + public HttpResponseMessage Response { get; set; } + + public long ResponseTimeMillis { get; set; } + + public int ExpectedStatusCode { get; set; } = 200; +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Models/AssertionType.cs b/Behavioral.Automation.API/Models/AssertionType.cs new file mode 100644 index 00000000..32f33c22 --- /dev/null +++ b/Behavioral.Automation.API/Models/AssertionType.cs @@ -0,0 +1,7 @@ +namespace Behavioral.Automation.API.Models; + +public enum AssertionType +{ + Be, + Become +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Models/HttpParameters.cs b/Behavioral.Automation.API/Models/HttpParameters.cs new file mode 100644 index 00000000..263c9e05 --- /dev/null +++ b/Behavioral.Automation.API/Models/HttpParameters.cs @@ -0,0 +1,14 @@ +namespace Behavioral.Automation.API.Models; + +public class HttpParameters +{ + public string Name; + public string Value; + public string Kind; +} + +public enum RequestParameterKind +{ + Param, + Header +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Services/HttpApiClient.cs b/Behavioral.Automation.API/Services/HttpApiClient.cs new file mode 100644 index 00000000..48c747d8 --- /dev/null +++ b/Behavioral.Automation.API/Services/HttpApiClient.cs @@ -0,0 +1,19 @@ +using TechTalk.SpecFlow.Infrastructure; + +namespace Behavioral.Automation.API.Services; + +public class HttpApiClient : IHttpApiClient +{ + private readonly ISpecFlowOutputHelper _specFlowOutputHelper; + + public HttpApiClient(ISpecFlowOutputHelper specFlowOutputHelper) + { + _specFlowOutputHelper = specFlowOutputHelper; + } + + public HttpResponseMessage SendHttpRequest(HttpRequestMessage httpRequestMessage) + { + HttpClient client = new(new LoggingHandler(new HttpClientHandler(), _specFlowOutputHelper)); + return client.Send(httpRequestMessage); + } +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Services/HttpClientService.cs b/Behavioral.Automation.API/Services/HttpClientService.cs new file mode 100644 index 00000000..d68ece42 --- /dev/null +++ b/Behavioral.Automation.API/Services/HttpClientService.cs @@ -0,0 +1,26 @@ +using Behavioral.Automation.API.Context; +using FluentAssertions; + +namespace Behavioral.Automation.API.Services; + +public class HttpService +{ + private readonly IHttpApiClient _client; + private readonly ApiContext _apiContext; + + public HttpService(IHttpApiClient client, ApiContext apiContext) + { + _client = client; + _apiContext = apiContext; + } + + public HttpResponseMessage SendContextRequest() + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + _apiContext.Response = _client.SendHttpRequest(_apiContext.Request.Clone()); + watch.Stop(); + ((int)_apiContext.Response.StatusCode).Should().Be(_apiContext.ExpectedStatusCode); + _apiContext.ResponseTimeMillis = watch.ElapsedMilliseconds; //save TTFB + Content download time + return _apiContext.Response; + } +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Services/HttpRequestMessageExtension.cs b/Behavioral.Automation.API/Services/HttpRequestMessageExtension.cs new file mode 100644 index 00000000..b39a6e1b --- /dev/null +++ b/Behavioral.Automation.API/Services/HttpRequestMessageExtension.cs @@ -0,0 +1,24 @@ +namespace Behavioral.Automation.API.Services; + +public static class HttpRequestMessageExtension +{ + public static HttpRequestMessage Clone(this HttpRequestMessage req) + { + var clone = new HttpRequestMessage(req.Method, req.RequestUri); + + clone.Content = req.Content; + clone.Version = req.Version; + + foreach (KeyValuePair prop in req.Properties) + { + clone.Properties.Add(prop); + } + + foreach (KeyValuePair> header in req.Headers) + { + clone.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clone; + } +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Services/IHttpApiClient.cs b/Behavioral.Automation.API/Services/IHttpApiClient.cs new file mode 100644 index 00000000..736549f6 --- /dev/null +++ b/Behavioral.Automation.API/Services/IHttpApiClient.cs @@ -0,0 +1,6 @@ +namespace Behavioral.Automation.API.Services; + +public interface IHttpApiClient +{ + public HttpResponseMessage SendHttpRequest(HttpRequestMessage httpRequestMessage); +} \ No newline at end of file diff --git a/Behavioral.Automation.API/Services/LoggingHandler.cs b/Behavioral.Automation.API/Services/LoggingHandler.cs new file mode 100644 index 00000000..4f43107a --- /dev/null +++ b/Behavioral.Automation.API/Services/LoggingHandler.cs @@ -0,0 +1,29 @@ +using TechTalk.SpecFlow.Infrastructure; + +namespace Behavioral.Automation.API.Services; + +public class LoggingHandler : DelegatingHandler +{ + private readonly ISpecFlowOutputHelper _specFlowOutputHelper; + public LoggingHandler(HttpMessageHandler innerHandler, ISpecFlowOutputHelper specFlowOutputHelper) + : base(innerHandler) + { + _specFlowOutputHelper = specFlowOutputHelper; + } + + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + _specFlowOutputHelper.WriteLine($"Request:{Environment.NewLine}{request}"); + if (request.Content != null) + { + _specFlowOutputHelper.WriteLine(request.Content.ReadAsStringAsync(cancellationToken).Result); + } + + HttpResponseMessage response = base.Send(request, cancellationToken); + + _specFlowOutputHelper.WriteLine($"Response:{Environment.NewLine}{response}"); + _specFlowOutputHelper.WriteLine(response.Content.ReadAsStringAsync(cancellationToken).Result); + + return response; + } +} \ No newline at end of file diff --git a/Behavioral.Automation.API/StepArgumentTransformations.cs b/Behavioral.Automation.API/StepArgumentTransformations.cs new file mode 100644 index 00000000..ef2802e3 --- /dev/null +++ b/Behavioral.Automation.API/StepArgumentTransformations.cs @@ -0,0 +1,22 @@ +using Behavioral.Automation.API.Models; +using TechTalk.SpecFlow; + +namespace Behavioral.Automation.API; + +[Binding] +public class StepArgumentTransformations +{ + [StepArgumentTransformation] + public AssertionType ParseBehavior(string verb) + { + switch (verb) + { + case "be": + return AssertionType.Be; + case "become": + return AssertionType.Become; + default: + throw new ArgumentException($"Unknown behaviour verb: {verb}"); + } + } +} \ No newline at end of file diff --git a/Behavioral.Automation.sln b/Behavioral.Automation.sln index 59f0bd7a..811c5725 100644 --- a/Behavioral.Automation.sln +++ b/Behavioral.Automation.sln @@ -22,6 +22,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Behavioral.Automation.Selen EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Behavioral.Automation.Playwright", "Behavioral.Automation.Playwright", "{2A26C467-6A03-4942-9F9B-62F77F992F70}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Behavioral.Automation.API", "Behavioral.Automation.API\Behavioral.Automation.API.csproj", "{ED37DAD3-B800-434B-9207-AD546F0099D3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +58,10 @@ Global {7460A60F-85B2-4DCE-B190-000503AE8550}.Debug|Any CPU.Build.0 = Debug|Any CPU {7460A60F-85B2-4DCE-B190-000503AE8550}.Release|Any CPU.ActiveCfg = Release|Any CPU {7460A60F-85B2-4DCE-B190-000503AE8550}.Release|Any CPU.Build.0 = Release|Any CPU + {ED37DAD3-B800-434B-9207-AD546F0099D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED37DAD3-B800-434B-9207-AD546F0099D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED37DAD3-B800-434B-9207-AD546F0099D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED37DAD3-B800-434B-9207-AD546F0099D3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE