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