diff --git a/APITestingRunner.Unit.Tests/APITestingRunner.Unit.Tests.csproj b/APITestingRunner.Unit.Tests/APITestingRunner.Unit.Tests.csproj index dd5c491..179b606 100644 --- a/APITestingRunner.Unit.Tests/APITestingRunner.Unit.Tests.csproj +++ b/APITestingRunner.Unit.Tests/APITestingRunner.Unit.Tests.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -7,13 +7,29 @@ false true + False - - - - + + Always + + + Always + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/APITestingRunner.Unit.Tests/DataAccessTests.cs b/APITestingRunner.Unit.Tests/DataAccessTests.cs index 1047fa9..408aafa 100644 --- a/APITestingRunner.Unit.Tests/DataAccessTests.cs +++ b/APITestingRunner.Unit.Tests/DataAccessTests.cs @@ -1,74 +1,150 @@ -using static ConfigurationManager; +using APITestingRunner.Database; +using APITestingRunner.Excetions; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using static ConfigurationManager; -namespace APITestingRunner.Unit.Tests -{ +namespace APITestingRunner.Unit.Tests { [TestClass] - public class DataAccessTests - { - private Config? _config; + public class DataAccessTests : TestBase { + private readonly Config _config = new() { + UrlBase = "http://localhost:5152/", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/Data", + UrlParam = new List + { + new Param("urlKey", "test"), + new Param("id", "sqlId") + }, + HeaderParam = new List { + new Param("accept","application/json") + }, + RequestBody = null, + DBConnectionString = "Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True;", + DBQuery = "select id as sqlId from dbo.sampleTable;", + DBFields = new List + { + new Param("sqlId", "sqlId") + }, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.All, + ConfigMode = TesterConfigMode.Run, + OutputLocation = DirectoryServices.AssemblyDirectory + }; [TestInitialize] - public void TestInit() - { - _config = new() - { - UrlBase = "http://localhost:5152/", - CompareUrlBase = string.Empty, - CompareUrlPath = string.Empty, - UrlPath = "/Data", - UrlParam = new List - { - new Param("urlKey", "test"), - new Param("id", "sqlId") - }, - HeaderParam = new List { - new Param("accept","application/json") - }, - RequestBody = null, - DBConnectionString = "Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True;", - DBQuery = "select id as sqlId from dbo.sampleTable;", - DBFields = new List - { - new Param("sqlId", "sqlId") - }, - RequestType = RequestType.GET, - ResultsStoreOption = StoreResultsOption.All, - ConfigMode = TesterConfigMode.Run, - LogLocation = DirectoryServices.AssemblyDirectory - }; + public void TestInit() { + } [TestMethod] - public void DataAccess_Tests_ConstructorShouldPass() - { - _ = new DataAccess(_config); + public void DataAccess_Tests_ConstructorShouldPass() { + _ = new DataAccess(_config, new Mock().Object); } + //Type Safety takes care of this - shouldn't be needed. + [TestMethod] + public void DataAccess_Tests_MissingConfig_ConstructorShouldThrowArgumentNullException() { + _ = Assert.ThrowsException(() => new DataAccess(null, null)); + } [TestMethod] - public void DataAccess_Tests_ConstructorShouldThrowArgumentNullException() - { - _ = Assert.ThrowsException(() => new DataAccess(null)); + public void DataAccess_Tests_MissingLogger_ConstructorShouldThrowArgumentNullException() { + _ = Assert.ThrowsException(() => new DataAccess(_config, null)); } [TestMethod] - public async Task FetchDataForRunnerAsync_PassNullForConnectionString_shouldThrowConfigurationErrorsException() - { + public async Task FetchDataForRunnerAsync_PassNullForConnectionString_shouldThrowConfigurationErrorsException() { Config testConfig = _config; - try - { + try { testConfig.DBConnectionString = null; - DataAccess da = new(testConfig); + DataAccess da = new(testConfig, new Mock().Object); _ = await da.FetchDataForRunnerAsync(); Assert.Fail(); - } - catch (TestRunnerConfigurationErrorsException ex) - { + } catch (TestRunnerConfigurationErrorsException ex) { Assert.AreEqual("Failed to load connection string", ex.Message); - } - catch - { + } catch { Assert.Fail(); } } + + [TestMethod] + public async Task FetchDataForRunner_GetDataFromDatabase_ShouldReturn_DataSet_withOneFieldFromDbForBinder() { + _config.DBConnectionString = "Data Source=(LocalDB)\\MSSQLLocalDB;AttachDbFilename=C:\\code\\cpoDesign\\APITestingRunner\\APITestingRunner.Unit.Tests\\SampleDb.mdf;Integrated Security=True"; + + var data = new DataAccess(_config, new Mock().Object); + + var records = await data.FetchDataForRunnerAsync(); + + _ = records.Should().NotBeEmpty(); + _ = records.Should().HaveCount(3); + + _ = records.Last().Should().BeEquivalentTo(new DataQueryResult { + RowId = 3, + Results = new List> { new KeyValuePair("sqlId", "3"), + } + }); + + _ = records.Last().Results.Should().HaveCount(1); + } + + [TestMethod] + public async Task FetchDataForRunner_GetDataFromDatabase_ShouldReturn_EmptyDataSet_withOneFieldFromDbForBinder() { + _config.DBConnectionString = "Data Source=(LocalDB)\\MSSQLLocalDB;AttachDbFilename=C:\\code\\cpoDesign\\APITestingRunner\\APITestingRunner.Unit.Tests\\SampleDb.mdf;Integrated Security=True"; + + _config.DBQuery = "select id as sqlId from dbo.sampleTable where id>5;"; + var data = new DataAccess(_config, new Mock().Object); + + var records = await data.FetchDataForRunnerAsync(); + + _ = records.Should().BeEmpty(); + } + + + [TestMethod] + public async Task FetchDataForRunner_GetDataFromDatabase_ShouldReturn_SingleFieldDataSet_withOneFieldFromDbForBinder() { + _config.DBConnectionString = "Data Source=(LocalDB)\\MSSQLLocalDB;AttachDbFilename=C:\\code\\cpoDesign\\APITestingRunner\\APITestingRunner.Unit.Tests\\SampleDb.mdf;Integrated Security=True"; + + _config.DBQuery = "select id as sqlId, name as fieldName from dbo.sampleTable"; + var data = new DataAccess(_config, new Mock().Object); + + var records = await data.FetchDataForRunnerAsync(); + + _ = records.Should().NotBeEmpty(); + _ = records.Should().HaveCount(3); + + _ = records.Last().Should().BeEquivalentTo(new DataQueryResult { + RowId = 3, + Results = new List> { new KeyValuePair("sqlId", "3"), + } + }); + + _ = records.Last().Results.Should().HaveCount(1); + } + + [TestMethod] + public async Task FetchDataForRunner_GetDataFromDatabase_ShouldReturn_TwoFieldDataSet_withOneFieldFromDbForBinder() { + _config.DBConnectionString = "Data Source=(LocalDB)\\MSSQLLocalDB;AttachDbFilename=C:\\code\\cpoDesign\\APITestingRunner\\APITestingRunner.Unit.Tests\\SampleDb.mdf;Integrated Security=True"; + + _config.DBFields = new List + { + new Param("sqlId", "sqlId"), + new Param("fieldName", "fieldName") + }; + + _config.DBQuery = "select id as sqlId, name as fieldName from dbo.sampleTable"; + var data = new DataAccess(_config, new Mock().Object); + + var records = await data.FetchDataForRunnerAsync(); + + _ = records.Should().NotBeEmpty(); + _ = records.Should().HaveCount(3); + + _ = records.Last().RowId.Should().Be(3); + _ = records.Last().Results.Should().HaveCount(2); + _ = records.Last().Results.First().Should().BeEquivalentTo(new KeyValuePair("sqlId", "3")); + _ = records.Last().Results.Last().Should().BeEquivalentTo(new KeyValuePair("fieldName", "Linux"), because: ""); + } } } \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/DataRequestTests.cs b/APITestingRunner.Unit.Tests/DataRequestTests.cs index 1fd82b7..a4dc8ac 100644 --- a/APITestingRunner.Unit.Tests/DataRequestTests.cs +++ b/APITestingRunner.Unit.Tests/DataRequestTests.cs @@ -1,138 +1,152 @@ -using static ConfigurationManager; - -namespace APITestingRunner.Unit.Tests -{ - [TestClass] - public class DataRequestTests - { - private Config? _config; - - [TestInitialize] - public void TestInit() - { - _config = new() - { - UrlBase = "http://localhost:5152/", - CompareUrlBase = null, - CompareUrlPath = null, - UrlPath = "/Data", - UrlParam = new List - { - new Param("urlKey", "test"), - new Param("id", "sqlId") - }, - HeaderParam = new List { - new Param("accept","application/json") - }, - RequestBody = null, - DBConnectionString = "Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True;", - DBQuery = "select id as sqlId from dbo.sampleTable;", - DBFields = new List - { - new Param("sqlId", "sqlId"), - - }, - RequestType = RequestType.GET, - ResultsStoreOption = StoreResultsOption.All, - ConfigMode = TesterConfigMode.Run, - LogLocation = DirectoryServices.AssemblyDirectory - }; - } - - [TestMethod] - public void ComposeRequest_PassNullUrlParam_WillSkipArgumentGeneration() - { - _config.UrlParam = null; - var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null); - Assert.AreEqual("/Data", result); - } - - [TestMethod] - public void ComposeRequest_PassNullUrlParam_WillSkipArgumentDbMatching() - { - _config.UrlParam = new List +using APITestingRunner.ApiRequest; +using APITestingRunner.Database; +using FluentAssertions; +using static ConfigurationManager; + +namespace APITestingRunner.Unit.Tests { + [TestClass] + public class DataRequestTests { + private Config? _config; + + [TestInitialize] + public void TestInit() { + _config = new() { + UrlBase = "http://localhost:5152/", + CompareUrlBase = null, + CompareUrlPath = null, + UrlPath = "/Data", + UrlParam = new List { + new Param("urlKey", "test"), + new Param("id", "sqlId") + }, + HeaderParam = new List { + new Param("accept","application/json") + }, + RequestBody = null, + DBConnectionString = "Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True;", + DBQuery = "select id as sqlId from dbo.sampleTable;", + DBFields = new List { + new Param("sqlId", "sqlId"), + }, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.All, + ConfigMode = TesterConfigMode.Run, + OutputLocation = DirectoryServices.AssemblyDirectory + }; + } + + [TestMethod] + public void DataRequestGetFullUrl() { + var result = new DataRequestConstructor().AddBaseUrl(_config.UrlBase).GetFullUrl(); + Assert.AreEqual("http://localhost:5152/", result); + } + + + [TestMethod] + public void DataRequestGetFullUrlWithEndpoint() { + + _config.UrlParam = new List(); + var result = new DataRequestConstructor() + .AddBaseUrl("http://localhost:5152") + .ComposeUrlAddressForRequest(_config.UrlPath, _config, null) + .GetFullUrl(); + + _ = result.Should().BeEquivalentTo("http://localhost:5152/Data"); + } + + [TestMethod] + public void DataRequestGetOnlyPathAndQuery() { + _config.UrlParam = new List(); + var result = new DataRequestConstructor() + .AddBaseUrl("http://localhost:5152") + .ComposeUrlAddressForRequest(_config.UrlPath, _config, null) + .GetPathAndQuery(); + + _ = result.Should().BeEquivalentTo("/Data"); + } + + [TestMethod] + public void ComposeRequest_PassNullUrlParam_WillSkipArgumentGeneration() { + _config.UrlParam = null; + var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null).GetFullUrl(); + Assert.AreEqual("/Data", result); + } + + [TestMethod] + public void ComposeRequest_PassNullUrlParam_WillSkipArgumentDbMatching() { + _config.UrlParam = new List { new Param("location","Paris") }; - var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null); - Assert.AreEqual("/Data?location=Paris", result); - } - - [TestMethod] - public void ComposeRequest_PassEmptyUrlParam_ReturnVanillaPath() - { - _config.UrlParam = new List - { - }; - var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null); - Assert.AreEqual("/Data", result); - } - - [TestMethod] - public void ComposeRequest_PassNullUrlParam_AppliesArgumentGeneration() - { - var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null); - Assert.IsTrue(result.Contains("urlKey")); - Assert.IsTrue(result.Contains("id")); - Assert.AreEqual("/Data?urlKey=test&id=sqlId", result); - } - - [TestMethod] - public void ComposeRequest_PassNullDBFields_AppliesArgumentGeneration() - { - _config.DBFields = null; - var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null); - Assert.IsTrue(result.Contains("urlKey")); - Assert.IsTrue(result.Contains("id")); - Assert.AreEqual("/Data?urlKey=test&id=sqlId", result); - } - - - [TestMethod] - public void ComposeRequest_PassMatchingDbFieldReplacesValues() - { - - var dbResult = new List> - { - new KeyValuePair ("sqlId", "123") - }; - - var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, new DataQueryResult - { - RowId = 1, - Results = dbResult - }) - ; - Assert.IsTrue(result.Contains("urlKey")); - Assert.IsTrue(result.Contains("id")); - Assert.AreEqual("/Data?urlKey=test&id=123", result); - } - - - [TestMethod] - public void ComposeRequest_PassMatchingDbFieldReplacesValuesMultiple() - { - - var dbResult = new List> - { - new KeyValuePair ("sqlId", "123"), - new KeyValuePair ("sqlName", "joe") - }; - - _config.UrlParam.Add(new Param("name", "sqlName")); - - // add the configuration to ensure the application is aware of the mapping - _config.DBFields.Add(new Param("sqlName", "sqlName")); - - var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, new DataQueryResult - { - RowId = 1, - Results = dbResult - }); - - Assert.IsTrue(result.Contains("urlKey")); - Assert.IsTrue(result.Contains("id")); - Assert.AreEqual("/Data?urlKey=test&id=123&name=joe", result); + var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null).GetFullUrl(); + Assert.AreEqual("/Data?location=Paris", result); + } + + [TestMethod] + public void ComposeRequest_PassEmptyUrlParam_ReturnVanillaPath() { + _config.UrlParam = new List { + }; + var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null).GetFullUrl(); + Assert.AreEqual("/Data", result); + } + + [TestMethod] + public void ComposeRequest_PassNullUrlParam_AppliesArgumentGeneration() { + var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null).GetFullUrl(); + Assert.IsTrue(result.Contains("urlKey")); + Assert.IsTrue(result.Contains("id")); + Assert.AreEqual("/Data?urlKey=test&id=sqlId", result); + } + + [TestMethod] + public void ComposeRequest_PassNullDBFields_AppliesArgumentGeneration() { + _config.DBFields = null; + var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null).GetFullUrl(); + Assert.IsTrue(result.Contains("urlKey")); + Assert.IsTrue(result.Contains("id")); + Assert.AreEqual("/Data?urlKey=test&id=sqlId", result); + } + + [TestMethod] + public void ComposeRequest_PassMatchingDbFieldReplacesValues() { + + var dbResult = new List> + { + new KeyValuePair ("sqlId", "123") + }; + + var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, new DataQueryResult { + RowId = 1, + Results = dbResult + }).GetFullUrl(); + + Assert.IsTrue(result.Contains("urlKey")); + Assert.IsTrue(result.Contains("id")); + Assert.AreEqual("/Data?urlKey=test&id=123", result); + } + + [TestMethod] + public void ComposeRequest_PassMatchingDbFieldReplacesValuesMultiple() { + + var dbResult = new List> { + new KeyValuePair ("sqlId", "123"), + new KeyValuePair ("sqlName", "joe") + }; + + _config.UrlParam.Add(new Param("name", "sqlName")); + + // add the configuration to ensure the application is aware of the mapping + _config.DBFields.Add(new Param("sqlName", "sqlName")); + + var result = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, new DataQueryResult { + RowId = 1, + Results = dbResult + }) + .GetFullUrl(); + + Assert.IsTrue(result.Contains("urlKey")); + Assert.IsTrue(result.Contains("id")); + Assert.AreEqual("/Data?urlKey=test&id=123&name=joe", result); + } } - } } \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/DataComparrisonTests.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/DataComparrisonTests.cs new file mode 100644 index 0000000..5714eeb --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/DataComparrisonTests.cs @@ -0,0 +1,52 @@ +using APITestingRunner.ApiRequest; +using FluentAssertions; + +namespace APITestingRunner.Unit.Tests { + + [TestClass] + public class DataComparrisonTests { + + [TestMethod] + public void CompareAPiResults_ShouldReturnMatching() { + ApiCallResult apiResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "", null, null, null, true, null); + + ApiCallResult fileResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "", null, null, null, true, null); + ComparissonStatus expectedResult = ComparissonStatus.Matching; + + + var result = DataComparrison.CompareAPiResults(apiResult, fileResult); + _ = result.Should().Be(expectedResult); + } + + [TestMethod] + public void CompareAPiResults_ShouldReturnDifferent() { + ApiCallResult apiResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "", null, null, null, true, null); + ApiCallResult fileResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "test", null, null, null, true, null); + ComparissonStatus expectedResult = ComparissonStatus.Different; + + + var result = DataComparrison.CompareAPiResults(apiResult, fileResult); + _ = result.Should().Be(expectedResult); + } + + [TestMethod] + public void CompareAPiResults_ShouldReturnDifferent_StatusCodeIsDifferent() { + ApiCallResult apiResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "", null, null, null, true, null); + ApiCallResult fileResult = new ApiCallResult(System.Net.HttpStatusCode.Accepted, "test", null, null, null, true, null); + ComparissonStatus expectedResult = ComparissonStatus.Different; + + var result = DataComparrison.CompareAPiResults(apiResult, fileResult); + _ = result.Should().Be(expectedResult); + } + + [TestMethod] + public void CompareAPiResults_ShouldReturnDifferent_IsSuccessCodeIsDifferent() { + ApiCallResult apiResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "", null, null, null, true, null); + ApiCallResult fileResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "test", null, null, null, false, null); + ComparissonStatus expectedResult = ComparissonStatus.Different; + + var result = DataComparrison.CompareAPiResults(apiResult, fileResult); + _ = result.Should().Be(expectedResult); + } + } +} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/IndividualActionsTests.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/IndividualActionsTests.cs new file mode 100644 index 0000000..7425752 --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/IndividualActionsTests.cs @@ -0,0 +1,9 @@ +namespace APITestingRunner.Unit.Tests { + [TestClass] + public class IndividualActionsTests { + [TestMethod] + public void IndividualActions_SetLogger_PassNull_ShouldReturnNull() { + _ = Assert.ThrowsException(() => new ApiTesterRunner(null)); + } + } +} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/PopulateRequestBody_Tests.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/PopulateRequestBody_Tests.cs new file mode 100644 index 0000000..11f6cb9 --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/PopulateRequestBody_Tests.cs @@ -0,0 +1,92 @@ +using FluentAssertions; +using static ConfigurationManager; + +namespace APITestingRunner.Unit.Tests { + [TestClass] + public class PopulateRequestBody_Tests { + + + [TestMethod] + public void PopulateRequestBody_ShouldThrowExcpetion_becauseOfconfig() { + Action action = () => TestRunner.PopulateRequestBody(null, new Database.DataQueryResult() { + RowId = 1, + Results = new List> { + new KeyValuePair("requestBody","JoeDoe"), + new KeyValuePair("bindingId", "3") } + }); + + _ = action.Should().Throw().WithMessage("Value cannot be null. (Parameter 'config')"); + } + + [TestMethod] + public void PopulateRequestBody_ShouldThrowExcpetion_becauseOfNoDataQueryResult() { + Config config = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + RequestBody = null, + HeaderParam = new List { + }, + UrlParam = new List { + }, + DBConnectionString = null, + DBQuery = "select id as bindingId, userName as requestBody from dbo.sampleTable where id in (1,3)", + DBFields = null, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.All, + ResultFileNamePattern = "{fileRecordType}-{bindingId}", + ConfigMode = TesterConfigMode.CaptureAndCompare, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + Action action = () => TestRunner.PopulateRequestBody(config, null); + + _ = action.Should().Throw().WithMessage("Value cannot be null. (Parameter 'dataQueryResult')"); + } + + [TestMethod] + [DataRow("just", "just")] + [DataRow(null, "")] + [DataRow("{\"name\":\"{requestBody}\"}", "{\"name\":\"JoeDoe\"}")] + [DataRow("{\"name\":\"{requestBody}\",\"id\":\"{bindingId}\"}", "{\"name\":\"JoeDoe\",\"id\":\"3\"}")] + public void PopulateRequestBody(string request, string expected) { + + Config config = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + RequestBody = request, + HeaderParam = new List { + new Param("accept", "application/json") + }, + UrlParam = new List { + new Param("urlKey", "configKey"), + new Param("id", "bindingId") + }, + DBConnectionString = null, + DBQuery = "select id as bindingId, userName as requestBody from dbo.sampleTable where id in (1,3)", + DBFields = new List + { + new Param("bindingId", "bindingId"), + new Param("requestBody", "requestBody"), + }, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.All, + ResultFileNamePattern = "{fileRecordType}-{bindingId}", + ConfigMode = TesterConfigMode.CaptureAndCompare, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var actual = TestRunner.PopulateRequestBody(config, new Database.DataQueryResult() { + RowId = 1, + Results = new List> { + new KeyValuePair("requestBody","JoeDoe"), + new KeyValuePair("bindingId", "3") } + }); + + Assert.AreEqual(expected, actual); + } + } +} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/TestBase.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/TestBase.cs new file mode 100644 index 0000000..14b3a3a --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/TestBase.cs @@ -0,0 +1,17 @@ + +using System.Diagnostics; + +namespace APITestingRunner.Unit.Tests { + /// + /// TODO refactor shared code into this class + /// + public class TestBase { + + public readonly string _dbConnectionStringForTests = "Data Source=(LocalDB)\\MSSQLLocalDB;AttachDbFilename=C:\\code\\cpoDesign\\APITestingRunner\\APITestingRunner.Unit.Tests\\SampleDb.mdf;Integrated Security=True"; + + internal void Initialize() { + // TODO: for now update this related to your checkout location and support LUT + Debug.WriteLine(_dbConnectionStringForTests); + } + } +} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/TestDifferentRequestTypes.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/TestDifferentRequestTypes.cs new file mode 100644 index 0000000..1b5dddf --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/TestDifferentRequestTypes.cs @@ -0,0 +1,161 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using WireMock.Matchers.Request; +using WireMock.ResponseBuilders; +using WireMock.Server; +using static ConfigurationManager; + +namespace APITestingRunner.Unit.Tests { + + [TestClass] + public class TestDifferentRequestTypes : TestBase { + private WireMockServer? server; + + [TestInitialize] + public void Initialize() { + // This starts a new mock server instance listening at port 9876 + server = WireMockServer.Start(7055); + + // ref for CORS: https://github.com/WireMock-Net/WireMock.Net/wiki/Cors + // ref for matching: https://github.com/WireMock-Net/WireMock.Net/wiki/Request-Matching + } + + [TestCleanup] + public void Cleanup() { + + var expectedFilePath = DirectoryServices.AssemblyDirectory; + + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + if (Directory.Exists(testDirectory)) { + Directory.Delete(testDirectory, true); + } + + // This stops the mock server to clean up after ourselves + _ = server ?? throw new ArgumentNullException(nameof(server)); + server.Stop(); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnConfig")] + [DataRow(RequestType.GET)] + [DataRow(RequestType.POST)] + [DataRow(RequestType.PUT)] + [DataRow(RequestType.PATCH)] + [DataRow(RequestType.DELETE)] + public async Task RequestDifferentRequestTypes(RequestType request) { + _ = server ?? throw new ArgumentNullException(nameof(server)); + + server.Given( + SetupMockForARequestType(request) + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "text/plain") + .WithBody("Hello, world!") + ); + + Config config = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + UrlParam = null, + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + DBConnectionString = null, + DBQuery = null, + DBFields = null, + RequestType = request, + ResultsStoreOption = StoreResultsOption.None, + ConfigMode = TesterConfigMode.Run, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + var testRunner = await new ApiTesterRunner(logger) + .RunTests(config); + + + _ = testRunner.Errors.Should().BeEmpty(); + + _ = logger.Messages.Should().ContainEquivalentOf(new Tuple(LogLevel.Information, $"{request} /WeatherForecast 200 success")); + } + + // TODO: Fix this text wiremock does not recognise the request when validating against a body + //[TestMethod] + //[TestCategory("SimpleAPICallBasedOnConfig")] + //[DataRow(RequestType.POST)] + //public async Task Request_Post_With_Static_BodyFromConfig(RequestType requestType) { + // _ = server ?? throw new ArgumentNullException(nameof(server)); + // var data = JsonConvert.SerializeObject( + // new { + // Name = "Test Name", + // Age = 5 + // }); + // //var dummDataToPost = new StringContent(data, Encoding.UTF8, MediaTypeNames.Application.Json); + // server.Given( + // WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast") + // .WithBody("Test") + // .UsingPost() + // ) + // .RespondWith( + // Response.Create() + // .WithStatusCode(200) + // .WithHeader("Content-Type", "application/json") + // .WithBody(@"{ ""msg"": ""Hello I'm a little bit slow!"" }") + // ); + + + // Config config = new() { + // UrlBase = "http://localhost:7055", + // CompareUrlBase = string.Empty, + // CompareUrlPath = string.Empty, + // UrlPath = "/WeatherForecast", + // UrlParam = null, + // RequestBody = data, //"{\"Name\":\"Test Name\",\"Age\":5}", + // HeaderParam = new List { + // new Param("accept","application/json") + // }, + // DBConnectionString = null, + // DBQuery = null, + // DBFields = null, + // RequestType = requestType, + // ResultsStoreOption = StoreResultsOption.None, + // ConfigMode = TesterConfigMode.Run, + // OutputLocation = DirectoryServices.AssemblyDirectory, + // }; + + // var logger = new TestLogger(); + + // var testRunner = await new ApiTesterRunner() + // .AddLogger(logger) + // .RunTests(config); + + // _ = testRunner.Errors.Should().BeEmpty(); + + // _ = logger.Messages.Should().ContainEquivalentOf(new Tuple(LogLevel.Information, $"{requestType} /WeatherForecast 200 success")); + //} + + private IRequestMatcher SetupMockForARequestType(RequestType request) { + switch (request) { + case RequestType.GET: + return WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingGet(); + case RequestType.POST: + return WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingPost(); + case RequestType.PUT: + return WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingPut(); + case RequestType.PATCH: + return WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingPatch(); + case RequestType.DELETE: + return WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingDelete(); + + default: + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/TestLogger.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/TestLogger.cs new file mode 100644 index 0000000..d74fa21 --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/TestLogger.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace APITestingRunner.Unit.Tests { + + public class TestLogger : ILogger { + public List> Messages = new(); + public IDisposable? BeginScope(TState state) where TState : notnull { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) { + throw new NotImplementedException(); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { + if (logLevel == LogLevel.Information) { + Messages.Add(new Tuple(logLevel, state.ToString())); + } else { + Debug.WriteLine(state.ToString()); + } + } + } +} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsTests.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsTests.cs new file mode 100644 index 0000000..36df0cd --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsTests.cs @@ -0,0 +1,324 @@ +using APITestingRunner.Database; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using WireMock.ResponseBuilders; +using WireMock.Server; +using static ConfigurationManager; + +namespace APITestingRunner.Unit.Tests { + + [TestClass] + public partial class TestRunnerWithOptionsWithConfigAPICallsTests { + private WireMockServer? server; + + [TestInitialize] + public void Initialize() { + // This starts a new mock server instance listening at port 9876 + server = WireMockServer.Start(7055); + } + + [TestCleanup] + public void Cleanup() { + + var expectedFilePath = DirectoryServices.AssemblyDirectory; + + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + if (Directory.Exists(testDirectory)) { + Directory.Delete(testDirectory, true); + } + + // This stops the mock server to clean up after ourselves + _ = server ?? throw new ArgumentNullException(nameof(server)); + server.Stop(); + } + + [TestMethod] + public void GenerateResultName_ShouldThrowExceptionIfPassedNull() { + Action? act = () => TestRunner.GenerateResultName(null, null); + _ = act.Should().Throw(); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnConfig")] + public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICall_WithResult_200_noStoreOfFiles() { + _ = server ?? throw new ArgumentNullException(nameof(server)); + + server.Given( + WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "text/plain") + .WithBody("Hello, world!") + ); + + Config config = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + UrlParam = null, + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + DBConnectionString = null, + DBQuery = null, + DBFields = null, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.None, + ConfigMode = TesterConfigMode.Run, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + var testRunner = await new ApiTesterRunner(logger) + .RunTests(config); + + _ = testRunner.Errors.Should().BeEmpty(); + + _ = logger.Messages.Should().ContainEquivalentOf(new Tuple(LogLevel.Information, $"{config.RequestType} /WeatherForecast 200 success")); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnConfig")] + [TestCategory("StoreFiles")] + public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICall_WithResult_200_WithStoringFiles() { + _ = server ?? throw new ArgumentNullException(nameof(server)); + + server.Given( + WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "text/plain") + .WithBody("Hello, world!") + ); + + Config config = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + UrlParam = null, + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + DBConnectionString = null, + DBQuery = null, + DBFields = null, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.All, + ConfigMode = TesterConfigMode.Capture, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + var testRunner = await new ApiTesterRunner(logger) + .RunTests(config); + + _ = testRunner.Errors.Should().BeEmpty(); + + _ = logger.Messages.Should().ContainEquivalentOf(new Tuple(LogLevel.Information, $"{config.RequestType} /WeatherForecast 200 success Results/request-0.json")); + + + var expectedFilePath = DirectoryServices.AssemblyDirectory; + + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + _ = Directory.Exists(testDirectory).Should().BeTrue(because: $"directory is: {testDirectory}"); + + var fileName = Path.Combine(testDirectory, TestRunner.GenerateResultName(new DataQueryResult() { RowId = 0 })); + + _ = File.Exists(fileName).Should().BeTrue(); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnConfig")] + [TestCategory("StoreFiles")] + public async Task CreateConfigForSingleAPICall_ShouldStoreFile_whenErrorIsReceived_WithResult500() { + _ = server ?? throw new ArgumentNullException(nameof(server)); + + server.Given( + WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(500) + .WithHeader("Content-Type", "text/plain") + .WithBody("Exception on the server") + ); + + Config config = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + UrlParam = null, + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + DBConnectionString = null, + DBQuery = null, + DBFields = null, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.FailuresOnly, + ConfigMode = TesterConfigMode.Capture, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + // Act + var testRunner = await new ApiTesterRunner(logger) + .RunTests(config); + + + // assert + _ = testRunner.Errors.Should().HaveCount(0); + _ = logger.Messages.Should().ContainEquivalentOf(new Tuple(LogLevel.Information, $"{config.RequestType} /WeatherForecast 500 fail Results/request-0.json")); + + + var expectedFilePath = DirectoryServices.AssemblyDirectory; + + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + _ = Directory.Exists(testDirectory).Should().BeTrue(because: $"directory is: {testDirectory}"); + + var fileName = Path.Combine(testDirectory, TestRunner.GenerateResultName(new DataQueryResult() { RowId = 0 })); + + _ = File.Exists(fileName).Should().BeTrue(); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnConfig")] + [TestCategory("StoreFiles")] + public async Task CreateConfigForSingleAPICall_ShouldStoreFile_whenErrorIsReceived_WithResult200_ShouldNotStoreResultFile() { + _ = server ?? throw new ArgumentNullException(nameof(server)); + + server.Given( + WireMock.RequestBuilders.Request. + Create() + .WithPath("/WeatherForecast") + .UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "text/plain") + .WithBody("Exception on the server") + ); + + Config config = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + UrlParam = null, + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + DBConnectionString = null, + DBQuery = null, + DBFields = null, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.FailuresOnly, + ConfigMode = TesterConfigMode.Capture, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + // Act + var testRunner = await new ApiTesterRunner(logger) + .RunTests(config); + + + // assert + _ = testRunner.Errors.Should().HaveCount(0); + _ = logger.Messages.Should().ContainEquivalentOf(new Tuple(LogLevel.Information, $"{config.RequestType} /WeatherForecast 200 success")); + + var expectedFilePath = DirectoryServices.AssemblyDirectory; + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + var fileName = Path.Combine(testDirectory, TestRunner.GenerateResultName(new DataQueryResult() { RowId = 0 })); + + _ = Directory.Exists(testDirectory).Should().BeFalse(because: $"directory is: {testDirectory}"); + + + _ = File.Exists(fileName).Should().BeFalse(); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnConfig")] + [DataRow(200, "success", StoreResultsOption.None, false)] + [DataRow(500, "fail", StoreResultsOption.None, false)] + [DataRow(200, "success", StoreResultsOption.FailuresOnly, false)] + [DataRow(500, "fail", StoreResultsOption.FailuresOnly, true)] + [DataRow(200, "success", StoreResultsOption.All, true)] + [DataRow(500, "fail", StoreResultsOption.All, true)] + public async Task CreateConfigForSingleAPICall_ShouldValidateStoreOptionsBaseOnAPiResult(int statusCode, string determination, StoreResultsOption storeOption, bool directoryAndFileExists) { + _ = server ?? throw new ArgumentNullException(nameof(server)); + + server.Given( + WireMock.RequestBuilders.Request.Create().WithPath("/WeatherForecast").UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(statusCode) + .WithHeader("Content-Type", "text/plain") + .WithBody("Exception on the server") + ); + + Config config = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + UrlParam = null, + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + DBConnectionString = null, + DBQuery = null, + DBFields = null, + RequestType = RequestType.GET, + ResultsStoreOption = storeOption, + ConfigMode = TesterConfigMode.Capture, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + // Act + var testRunner = await new ApiTesterRunner(logger) + .RunTests(config); + + // assert + _ = testRunner.Errors.Should().HaveCount(0); + + var fileNameLog = string.Empty; + if (directoryAndFileExists) { + fileNameLog += $" Results/request-0.json"; + } + _ = logger.Messages.Should().ContainEquivalentOf(new Tuple(LogLevel.Information, $"{config.RequestType} /WeatherForecast {statusCode} {determination}{fileNameLog}")); + + var expectedFilePath = DirectoryServices.AssemblyDirectory; + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + var fileName = Path.Combine(testDirectory, TestRunner.GenerateResultName(new DataQueryResult() { RowId = 0 })); + + if (directoryAndFileExists) { + _ = Directory.Exists(testDirectory).Should().BeTrue(because: $"directory is: {testDirectory}"); + _ = File.Exists(fileName).Should().BeTrue(); + } else { + _ = Directory.Exists(testDirectory).Should().BeFalse(because: $"directory is: {testDirectory}"); + _ = File.Exists(fileName).Should().BeFalse(); + } + } + } +} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsWithConfigAPIBasedOnDatabaseCallsTests.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsWithConfigAPIBasedOnDatabaseCallsTests.cs new file mode 100644 index 0000000..84d0e43 --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsWithConfigAPIBasedOnDatabaseCallsTests.cs @@ -0,0 +1,464 @@ +using APITestingRunner.Database; +using FluentAssertions; +using WireMock.Matchers; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace APITestingRunner.Unit.Tests { + [TestClass] + public class TestRunnerWithOptionsWithConfigAPIBasedOnDatabaseCallsTests : TestBase { + + private WireMockServer server; + + [TestInitialize] + public void Initialize() { + // This starts a new mock server instance listening at port 9876 + server = WireMockServer.Start(7055); + + base.Initialize(); + } + + [TestCleanup] + public void Cleanup() { + var expectedFilePath = DirectoryServices.AssemblyDirectory; + + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + if (Directory.Exists(testDirectory)) { + Directory.Delete(testDirectory, true); + } + + // This stops the mock server to clean up after ourselves + server.Stop(); + } + + [TestMethod] + public void GenerateResultName_ShouldThrowExceptionIfPassedNull() { + Action act = () => TestRunner.GenerateResultName(null); + _ = act.Should().Throw(); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnDbSource")] + [TestCategory("dbcapture")] + public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICall_WithResult_200_noStoreOfFiles() { + + server.Given( + WireMock.RequestBuilders.Request.Create() + .WithPath("/WeatherForecast") + .WithParam("urlKey", "configKey") + .WithParam("id", new WildcardMatcher(MatchBehaviour.AcceptOnMatch, "*", true)) + .UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "text/plain") + .WithBody("Hello, world!") + ); + + Config apiTesterConfig = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + UrlParam = new List + { + new Param("urlKey", "configKey"), + new Param("id", "bindingId") + }, + DBConnectionString = _dbConnectionStringForTests, + DBQuery = "select id as bindingId from dbo.sampleTable;", + DBFields = new List + { + new Param("bindingId", "bindingId"), + }, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.None, + ConfigMode = TesterConfigMode.Run, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + var testRunner = await new ApiTesterRunner(logger) + .RunTests(apiTesterConfig); + + _ = testRunner.Errors.Should().BeEmpty(); + _ = logger.Messages.Count().Should().Be(6); + + _ = logger.Messages[0].Item2.Should().ContainEquivalentOf("Validating database based data source start"); + _ = logger.Messages[1].Item2.Should().ContainEquivalentOf("Found database connection string"); + _ = logger.Messages[2].Item2.Should().ContainEquivalentOf("Found database query and db fields. Attempting to load data from database."); + _ = logger.Messages[3].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=1 200 success"); + _ = logger.Messages[4].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=2 200 success"); + _ = logger.Messages[5].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=3 200 success"); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnDbSourceWithCapture")] + [TestCategory("dbcapture")] + public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICall_WithResult_404_WithFailureCapture() { + + server.Given( + WireMock.RequestBuilders.Request.Create() + .WithPath("/WeatherForecast") + .WithParam("urlKey", "configKey") + .WithParam("id", new WildcardMatcher(MatchBehaviour.AcceptOnMatch, "*", true)) + .UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(404) + .WithHeader("Content-Type", "text/plain") + .WithBody("Hello, world!") + + ); + + Config apiTesterConfig = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + UrlParam = new List + { + new Param("urlKey", "configKey"), + new Param("id", "bindingId") + }, + DBConnectionString = _dbConnectionStringForTests, + DBQuery = "select id as bindingId from dbo.sampleTable;", + DBFields = new List + { + new Param("bindingId", "bindingId"), + }, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.FailuresOnly, + ConfigMode = TesterConfigMode.Capture, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + var testRunner = await new ApiTesterRunner(logger) + .RunTests(apiTesterConfig); + + _ = testRunner.Errors.Should().BeEmpty(); + _ = logger.Messages.Count().Should().Be(6); + + _ = logger.Messages[0].Item2.Should().ContainEquivalentOf("Validating database based data source start"); + _ = logger.Messages[1].Item2.Should().ContainEquivalentOf("Found database connection string"); + _ = logger.Messages[2].Item2.Should().ContainEquivalentOf("Found database query and db fields. Attempting to load data from database."); + _ = logger.Messages[3].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=1 404 fail Results/request-1.json"); + _ = logger.Messages[4].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=2 404 fail Results/request-2.json"); + _ = logger.Messages[5].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=3 404 fail Results/request-3.json"); + + var expectedFilePath = DirectoryServices.AssemblyDirectory; + + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + _ = Directory.Exists(testDirectory).Should().BeTrue(because: $"directory is: {testDirectory}"); + + var fileName = Path.Combine(testDirectory, TestRunner.GenerateResultName(new DataQueryResult() { RowId = 1 })); + var fileName2 = Path.Combine(testDirectory, TestRunner.GenerateResultName(new DataQueryResult() { RowId = 2 })); + var fileName3 = Path.Combine(testDirectory, TestRunner.GenerateResultName(new DataQueryResult() { RowId = 3 })); + + _ = File.Exists(fileName).Should().BeTrue(); + _ = File.Exists(fileName2).Should().BeTrue(); + _ = File.Exists(fileName3).Should().BeTrue(); + } + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnDbSourceWithCapture")] + [TestCategory("ResultCompare")] + public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICall_WithResult_200_ShouldStoreAllRequest_withFileNamingBasedOnDbResult() { + + server.Given( + WireMock.RequestBuilders.Request.Create() + .WithPath("/WeatherForecast") + .WithParam("urlKey", "configKey") + .WithParam("id", new WildcardMatcher(MatchBehaviour.AcceptOnMatch, "*", true)) + .UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "application/json") + .WithBody("Hello, world!") + ); + + Config apiTesterConfig = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + RequestBody = null, + HeaderParam = new List { + new Param("accept", "application/json") + }, + UrlParam = new List { + new Param("urlKey", "configKey"), + new Param("id", "bindingId") + }, + DBConnectionString = _dbConnectionStringForTests, + DBQuery = "select id as bindingId, RecordType as fileRecordType from dbo.sampleTable where id in (1,3)", + DBFields = new List + { + new Param("bindingId", "bindingId"), + new Param("fileRecordType", "fileRecordType"), + }, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.All, + ResultFileNamePattern = "{fileRecordType}-{bindingId}", + ConfigMode = TesterConfigMode.CaptureAndCompare, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + var testRunner = await new ApiTesterRunner(logger) + .RunTests(apiTesterConfig); + + _ = testRunner.Errors.Should().BeEmpty(); + _ = logger.Messages.Count().Should().Be(5); + + _ = logger.Messages[0].Item2.Should().ContainEquivalentOf("Validating database based data source start"); + _ = logger.Messages[1].Item2.Should().ContainEquivalentOf("Found database connection string"); + _ = logger.Messages[2].Item2.Should().ContainEquivalentOf("Found database query and db fields. Attempting to load data from database."); + _ = logger.Messages[3].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=1 200 success Results/request-music-1.json NewFile"); + _ = logger.Messages[4].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=3 200 success Results/request-software-3.json NewFile"); + + + var expectedFilePath = DirectoryServices.AssemblyDirectory; + + var testDirectory = Path.Combine(expectedFilePath, TestConstants.TestOutputDirectory); + _ = Directory.Exists(testDirectory).Should().BeTrue(because: $"directory is: {testDirectory}"); + + var fileName = Path.Combine(testDirectory, "request-music-1.json"); + var fileName3 = Path.Combine(testDirectory, "request-software-3.json"); + + _ = File.Exists(fileName).Should().BeTrue(because: $"Expected file name {fileName}"); + _ = File.Exists(fileName3).Should().BeTrue(because: $"Expected file name {fileName3}"); + + // now lets rerun and see the differences + // clear logger + logger = new TestLogger(); + + apiTesterConfig.DBQuery = "select id as bindingId, RecordType as fileRecordType from dbo.sampleTable"; + + testRunner = await new ApiTesterRunner(logger) + .RunTests(apiTesterConfig); + + _ = testRunner.Errors.Should().BeEmpty(); + _ = logger.Messages.Count().Should().Be(6); + + _ = logger.Messages[0].Item2.Should().ContainEquivalentOf("Validating database based data source start"); + _ = logger.Messages[1].Item2.Should().ContainEquivalentOf("Found database connection string"); + _ = logger.Messages[2].Item2.Should().ContainEquivalentOf("Found database query and db fields. Attempting to load data from database."); + _ = logger.Messages[3].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=1 200 success Results/request-music-1.json Matching"); + _ = logger.Messages[4].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=2 200 success Results/request-software-2.json NewFile"); + _ = logger.Messages[5].Item2.Should().ContainEquivalentOf("/WeatherForecast?urlkey=configKey&id=3 200 success Results/request-software-3.json Matching"); + + _ = Path.Combine(testDirectory, "request-music-1.json"); + _ = Path.Combine(testDirectory, "request-software-2.json"); + _ = Path.Combine(testDirectory, "request-software-3.json"); + } + + + [TestMethod] + [TestCategory("SimpleAPICallBasedOnDbSource")] + [TestCategory("dbcapture")] + public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldAppendIdToUrl() { + + server.Given( + WireMock.RequestBuilders.Request.Create() + .WithPath("/WeatherForecast/1") + + .UsingGet() + ) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithHeader("Content-Type", "text/plain") + .WithBody("Hello, world!") + ); + + Config apiTesterConfig = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast/{bindingId}", + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + UrlParam = null, + DBConnectionString = _dbConnectionStringForTests, + DBQuery = "select top 1 id as bindingId from dbo.sampleTable;", + DBFields = new List + { + new Param("bindingId", "bindingId"), + }, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.None, + ConfigMode = TesterConfigMode.Run, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + var logger = new TestLogger(); + + var testRunner = await new ApiTesterRunner(logger) + .RunTests(apiTesterConfig); + + _ = testRunner.Errors.Should().BeEmpty(); + _ = logger.Messages.Count().Should().Be(4); + + _ = logger.Messages[0].Item2.Should().ContainEquivalentOf("Validating database based data source start"); + _ = logger.Messages[1].Item2.Should().ContainEquivalentOf("Found database connection string"); + _ = logger.Messages[2].Item2.Should().ContainEquivalentOf("Found database query and db fields. Attempting to load data from database."); + _ = logger.Messages[3].Item2.Should().ContainEquivalentOf("/WeatherForecast/1 200 success"); + } + + + //[TestMethod] + //public async Task CreateConfigForSingleAPICallWithUrlParam() + //{ + // _ = new Config() + // { + // UrlBase = "https://localhost:7055", + // CompareUrlBase = string.Empty, + // CompareUrlPath = string.Empty, + // UrlPath = "/WeatherForecast/GetWeatherForecastForLocation", + // UrlParam = new List + // { + // new Param("location","UK") + // }, + // HeaderParam = new List { + // new Param("accept","application/json") + //}, + // RequestBody = null, + // DBConnectionString = null, + // DBQuery = null, + // DBFields = null, + // RequestType = RequestType.GET, + // ResultsStoreOption = StoreResultsOption.None, + // ConfigMode = TesterConfigMode.Run, + // LogLocation = DirectoryServices.AssemblyDirectory + // }; + // Assert.Fail(); + //} + + ////[DataRow(StoreResultsOption.None)] + ////[DataRow(StoreResultsOption.FailuresOnly)] + ////[DataRow(StoreResultsOption.All)] + ////public async Task CreateConfigForDatabaseBasedAPICall(StoreResultsOption storeResultsOption) + ////{ + //[TestMethod] + //public async Task CreateConfigForDatabaseBasedAPICall() + //{ + // StoreResultsOption storeResultsOption = StoreResultsOption.All; + + // string sqlCon = @"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=C:\code\cpoDesign\APITestingRunner\APITestingRunner.Unit.Tests\SampleDb.mdf;Integrated Security=True"; + + + // Config config = new() + // { + // UrlBase = "https://localhost:7055", + // CompareUrlBase = string.Empty, + // CompareUrlPath = string.Empty, + // UrlPath = "/Data", + // UrlParam = new List + //{ + // new Param("urlKey", "test"), + // new Param("id", "sqlId") + //}, + // HeaderParam = new List { + // new Param("accept","application/json") + //}, + // RequestBody = null, + // DBConnectionString = sqlCon, + // DBQuery = "select id as sqlId from dbo.sampleTable;", + // DBFields = new List + //{ + // new Param("sqlId", "sqlId") + //}, + // RequestType = RequestType.GET, + // ResultsStoreOption = storeResultsOption, + // ConfigMode = TesterConfigMode.Run, + // LogLocation = DirectoryServices.AssemblyDirectory + // }; + + + // _ = await IndividualActions.RunTests(config); + //} + + //[TestMethod] + //public async Task CreateConfigForDatabaseBasedAPIComparrisonCall() + //{ + // Config config = new() + // { + // UrlBase = "https://localhost:7055", + // CompareUrlBase = "https://localhost:7055", + // UrlPath = "/Data", + // CompareUrlPath = "/DataV2", + // UrlParam = new List + //{ + // new Param("urlKey", "test"), + // new Param("id", "sqlId") + //}, + // HeaderParam = new List { + // new Param("accept","application/json") + //}, + // RequestBody = null, + // DBConnectionString = "Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True;", + // DBQuery = "select id as sqlId from dbo.sampleTable;", + // DBFields = new List + //{ + // new Param("sqlId", "sqlId") + //}, + // RequestType = RequestType.GET, + // ResultsStoreOption = StoreResultsOption.None, + // ConfigMode = TesterConfigMode.APICompare, + // LogLocation = DirectoryServices.AssemblyDirectory + // }; + + + // await IndividualActions.RunTests(config); + //} + + //[TestMethod] + //public async Task CreateConfigForSingleAPICallWithUrlParamAndBodyModel() + //{ + + // Config config = new() + // { + // UrlBase = "https://localhost:7055", + // CompareUrlBase = string.Empty, + // CompareUrlPath = string.Empty, + // UrlPath = "/datamodel/123456789", + // UrlParam = new List + // { + // new Param("location","UK") + // }, + // HeaderParam = new List { + // new Param("accept","application/json") + //}, + // RequestBody = "{Id={sqlId},StaticData=\"data\"}", + // DBConnectionString = null, + // DBQuery = null, + // DBFields = null, + // RequestType = RequestType.GET, + // ResultsStoreOption = StoreResultsOption.None, + // ConfigMode = TesterConfigMode.Run, + // LogLocation = DirectoryServices.AssemblyDirectory + // }; + + // await IndividualActions.RunTests(config); + //} + } +} \ No newline at end of file diff --git a/APITestingRunner/APITestingRunner.csproj b/APITestingRunner/APITestingRunner.csproj index 213449e..9ecce57 100644 --- a/APITestingRunner/APITestingRunner.csproj +++ b/APITestingRunner/APITestingRunner.csproj @@ -11,6 +11,20 @@ + + + + + + + + + + + + + Always + diff --git a/APITestingRunner/ApiCallResult.cs b/APITestingRunner/ApiCallResult.cs deleted file mode 100644 index 1157656..0000000 --- a/APITestingRunner/ApiCallResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -using System.Net; -using System.Net.Http.Headers; - -namespace APITestingRunner -{ - public record ApiCallResult(HttpStatusCode statusCode, string responseContent, HttpResponseHeaders headers, string url, DataQueryResult? item, - bool IsSuccessStatusCode, List? CompareResults = null) - { - - } -} \ No newline at end of file diff --git a/APITestingRunner/ApiRequest/ApiCallResult.cs b/APITestingRunner/ApiRequest/ApiCallResult.cs new file mode 100644 index 0000000..82de747 --- /dev/null +++ b/APITestingRunner/ApiRequest/ApiCallResult.cs @@ -0,0 +1,26 @@ +// See https://aka.ms/new-console-template for more information + +using APITestingRunner.Database; +using System.Net; + +namespace APITestingRunner.ApiRequest { + + /// + /// Container for an api call result. + /// + /// + /// + /// + /// + /// + /// + /// + public record ApiCallResult(HttpStatusCode statusCode, + string responseContent, + List> headers, + string url, + DataQueryResult? item, + bool IsSuccessStatusCode, + List? CompareResults = null) { + } +} \ No newline at end of file diff --git a/APITestingRunner/ApiRequest/DataRequestConstructor.cs b/APITestingRunner/ApiRequest/DataRequestConstructor.cs new file mode 100644 index 0000000..355fca5 --- /dev/null +++ b/APITestingRunner/ApiRequest/DataRequestConstructor.cs @@ -0,0 +1,73 @@ +// See https://aka.ms/new-console-template for more information + +using APITestingRunner.Database; + +namespace APITestingRunner.ApiRequest { + public class DataRequestConstructor { + private string _baseUrl = string.Empty; + private string _relativeUrl = string.Empty; + + /// + /// Uset to compose url based on dataquery result. Used to create data driven URL + /// + /// Endpoint url|/param> + /// Instance of configuration parameters + /// Data from database used to merge to create a data driven url + /// Instance of object + public DataRequestConstructor ComposeUrlAddressForRequest(string urlPath, Config _config, DataQueryResult? dbData) { + if (!string.IsNullOrWhiteSpace(urlPath)) { + _relativeUrl = urlPath; + } + + + if (_config.UrlParam == null || _config.UrlParam.Count() == 0) { + return this; + } + + bool isFirst = true; + + foreach (Param item in _config.UrlParam) { + if (isFirst) { + isFirst = false; + urlPath += $"?{item.Name}="; + } else { + urlPath += $"&{item.Name}="; + } + + //check if item value is listed in dbfields, if yes we have mapping to value from database otherwise just use value + if (_config.DBFields != null) { + if (_config.DBFields.Any(x => x.value == item.value) && dbData != null) { + //replace value from dbData object + KeyValuePair dbResultFound = dbData.Results.FirstOrDefault(x => x.Key == item.value); + + urlPath += $"{dbResultFound.Value}"; + + } else { + // no match found in parameters + urlPath += $"{item.value}"; + } + } else { + // no match found in parameters + urlPath += $"{item.value}"; + } + } + + _relativeUrl = urlPath; + return this; + } + + public DataRequestConstructor AddBaseUrl(string urlBase) { + _baseUrl = urlBase; + + return this; + } + + public string GetPathAndQuery() { + return _relativeUrl; + } + + public string? GetFullUrl() { + return $"{_baseUrl}{_relativeUrl}"; + } + } +} \ No newline at end of file diff --git a/APITestingRunner/ApiRequest/Param.cs b/APITestingRunner/ApiRequest/Param.cs new file mode 100644 index 0000000..b8b2cdc --- /dev/null +++ b/APITestingRunner/ApiRequest/Param.cs @@ -0,0 +1,3 @@ +// See https://aka.ms/new-console-template for more information + +public record Param(string Name, string value); diff --git a/APITestingRunner/ApiRequest/RequestType.cs b/APITestingRunner/ApiRequest/RequestType.cs new file mode 100644 index 0000000..caaaede --- /dev/null +++ b/APITestingRunner/ApiRequest/RequestType.cs @@ -0,0 +1,11 @@ + +/// +/// Contains request types that can be used to make the api call. +/// +public enum RequestType { + GET = 1, + POST = 2, + PUT = 3, + PATCH = 4, + DELETE = 5, +} diff --git a/APITestingRunner/ApiTesterRunner.cs b/APITestingRunner/ApiTesterRunner.cs new file mode 100644 index 0000000..b1421be --- /dev/null +++ b/APITestingRunner/ApiTesterRunner.cs @@ -0,0 +1,59 @@ +// See https://aka.ms/new-console-template for more information + +//Console.WriteLine("Hello, World!"); + + +//Console.WriteLine(path); +//Console.ReadLine(); + +using Microsoft.Extensions.Logging; + +namespace APITestingRunner { + public class ApiTesterRunner { + private readonly ILogger? _logger; + + public ApiTesterRunner(ILogger logger) { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task CreateConfig(string pathConfigJson, Config config) { + ConfigurationManager configManager = new(); + + _logger.LogInformation($"creating config on path: {pathConfigJson}"); + await configManager.CreateConfig(pathConfigJson, config); + _logger.LogInformation($"creating created"); + return; + + } + public async Task RunTests(string pathConfigJson) { + Console.WriteLine($"Loading config on path: {pathConfigJson}"); + + _ = _logger ?? throw new ArgumentNullException(nameof(_logger)); + + ConfigurationManager configManager = new(); + + Config? configSettings = await configManager.GetConfigAsync(pathConfigJson); + TestRunner testRunner = new(_logger); + await testRunner.ApplyConfig(configSettings); + + try { + testRunner = await testRunner.RunTestsAsync(); + } catch (Exception ex) { + _logger.LogError(ex.Message); + } + + _ = await testRunner.PrintResultsSummary(); + return; + } + + public async Task RunTests(Config config) { + _ = _logger ?? throw new ArgumentNullException("_logger"); + + TestRunner testRunner = new(_logger); + await testRunner.ApplyConfig(config); + + return await testRunner.RunTestsAsync(); + + } + } +} \ No newline at end of file diff --git a/APITestingRunner/Config.cs b/APITestingRunner/Config.cs deleted file mode 100644 index e5c7f87..0000000 --- a/APITestingRunner/Config.cs +++ /dev/null @@ -1,75 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -using static ConfigurationManager; - -public class Config -{ - /// - /// Base path of the url. - /// - public required string UrlBase { get; set; } - /// - /// Alternative compare url in case of option of comparing requests. - /// - public required string CompareUrlBase { get; set; } - - /// - /// Relative path for the client. - /// - public required string UrlPath { get; set; } - - /// - /// Contains a compare url path. Allows user to point to the same api under different name. - /// - public required string CompareUrlPath { get; set; } - - /// - /// Any query parameters api requires. - /// - public required List UrlParam { get; set; } - /// - /// Any headers the api requires - /// - public required List HeaderParam { get; set; } - - /// - /// Request body optional. When present the replace values will apply the same way like for url. - /// - public string? RequestBody { get; set; } - - /// - /// What type of HTTP verb to use for the API call. - /// - public required RequestType RequestType { get; set; } - - /// - /// What is the config for the runner. - /// - public required TesterConfigMode? ConfigMode { get; set; } - - /// - /// What to do with results - /// - public StoreResultsOption ResultsStoreOption { get; set; } - - /// - /// Location where responses will be stored. - /// Can be null and if yes no responses will be stored even if StoreResults is enabled - /// - public string? LogLocation { get; set; } - - /// - /// Database connection string to target your database source. - /// - public string? DBConnectionString { get; set; } - - /// - /// Query to generate the data to use for data generation for api arguments. - /// - public string? DBQuery { get; set; } - - /// - /// Database fields mapping to parameters - /// - public List? DBFields { get; set; } -} diff --git a/APITestingRunner/Configuration/Config.cs b/APITestingRunner/Configuration/Config.cs new file mode 100644 index 0000000..ea41ed6 --- /dev/null +++ b/APITestingRunner/Configuration/Config.cs @@ -0,0 +1,79 @@ +// See https://aka.ms/new-console-template for more information + +using static ConfigurationManager; + +public class Config { + /// + /// Base path of the url. + /// + public required string UrlBase { get; set; } + /// + /// Alternative compare url in case of option of comparing requests. + /// + public required string? CompareUrlBase { get; set; } + + /// + /// Relative path for the client. + /// + public required string UrlPath { get; set; } + + /// + /// Contains a compare url path. Allows user to point to the same api under different name. + /// + public required string? CompareUrlPath { get; set; } + + /// + /// Any query parameters api requires. + /// + public required List UrlParam { get; set; } + /// + /// Any headers the api requires + /// + public required List HeaderParam { get; set; } + + /// + /// Request body optional. When present the replace values will apply the same way like for url. + /// + public string? RequestBody { get; set; } + + /// + /// What type of HTTP verb to use for the API call. + /// + public required RequestType RequestType { get; set; } + + /// + /// What is the config for the runner. + /// + public required TesterConfigMode? ConfigMode { get; set; } + + /// + /// What to do with results + /// + public StoreResultsOption ResultsStoreOption { get; set; } + + /// + /// Location where responses will be stored. + /// Can be null and if yes no responses will be stored even if StoreResults is enabled. + /// + public string? OutputLocation { get; set; } + + /// + /// Database connection string to target your database source. + /// + public string? DBConnectionString { get; set; } + + /// + /// Query to generate the data to use for data generation for api arguments. + /// + public string? DBQuery { get; set; } + + /// + /// Database fields mapping to parameters + /// + public List? DBFields { get; set; } + + /// + /// Result file name pattern used to create a result file when used in combination of result capture. + /// + public string? ResultFileNamePattern { get; set; } +} diff --git a/APITestingRunner/Configuration.cs b/APITestingRunner/Configuration/Configuration.cs similarity index 71% rename from APITestingRunner/Configuration.cs rename to APITestingRunner/Configuration/Configuration.cs index b9d3fc8..12a7170 100644 --- a/APITestingRunner/Configuration.cs +++ b/APITestingRunner/Configuration/Configuration.cs @@ -1,21 +1,20 @@ -// See https://aka.ms/new-console-template for more information -using System.Text.Json; - -public partial class ConfigurationManager -{ - public string? LogLocation { get; internal set; } - - public async Task CreateConfig(string pathConfigJson, Config config) - { - - string objString = JsonSerializer.Serialize(config); - - await File.WriteAllTextAsync(pathConfigJson, objString); - } - - public async Task GetConfigAsync(string path) - { - string fileContent = await File.ReadAllTextAsync(path); - return string.IsNullOrWhiteSpace(fileContent) ? throw new Exception() : JsonSerializer.Deserialize(fileContent); - } -} +// See https://aka.ms/new-console-template for more information +using System.Text.Json; + +public class ConfigurationManager { + + public async Task CreateConfig(string pathConfigJson, Config config) { + string objString = JsonSerializer.Serialize(config); + + if (File.Exists(pathConfigJson)) { + File.Delete(pathConfigJson); + } + + await File.WriteAllTextAsync(pathConfigJson, objString); + } + + public async Task GetConfigAsync(string path) { + string fileContent = await File.ReadAllTextAsync(path); + return string.IsNullOrWhiteSpace(fileContent) ? throw new Exception() : JsonSerializer.Deserialize(fileContent); + } +} diff --git a/APITestingRunner/DataAccess.cs b/APITestingRunner/DataAccess.cs deleted file mode 100644 index fa2acfa..0000000 --- a/APITestingRunner/DataAccess.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Dapper; -using Microsoft.Data.SqlClient; - -namespace APITestingRunner -{ - public class DataAccess - { - private readonly Config config; - - public DataAccess(Config config) - { - this.config = config ?? throw new ArgumentNullException(nameof(config)); - } - - public async Task> FetchDataForRunnerAsync() - { - if (string.IsNullOrWhiteSpace(config.DBConnectionString)) throw new TestRunnerConfigurationErrorsException("Failed to load connection string"); - using SqlConnection connection = new(config.DBConnectionString); - - IEnumerable result = await connection.QueryAsync(config.DBQuery); - - List list = new(); - int i = 0; - foreach (object rows in result) - { - i++; - - DataQueryResult resultItem = new() { RowId = i, Results = new List>() }; - - IDictionary? fields = rows as IDictionary; - - // get the fields from database and match to the object - foreach (ConfigurationManager.Param config in config.DBFields) - { - object sum = fields[config.Name]; - resultItem.Results.Add(new KeyValuePair(config.Name, sum.ToString())); - } - - list.Add(resultItem); - } - - return list; - } - } -} diff --git a/APITestingRunner/DataQueryResult.cs b/APITestingRunner/DataQueryResult.cs deleted file mode 100644 index e327b16..0000000 --- a/APITestingRunner/DataQueryResult.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace APITestingRunner -{ - public class DataQueryResult - { - public int RowId { get; set; } = 0; - public List> Results = new(); - } -} diff --git a/APITestingRunner/DataRequestConstructor.cs b/APITestingRunner/DataRequestConstructor.cs deleted file mode 100644 index 808e177..0000000 --- a/APITestingRunner/DataRequestConstructor.cs +++ /dev/null @@ -1,55 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -namespace APITestingRunner -{ - public class DataRequestConstructor - { - public string? ComposeUrlAddressForRequest(string urlPath, Config _config, DataQueryResult? dbData) - { - if (_config.UrlParam == null || _config.UrlParam.Count() == 0) - { - return urlPath; - } - - bool isFirst = true; - - foreach (ConfigurationManager.Param item in _config.UrlParam) - { - if (isFirst) - { - isFirst = false; - urlPath += $"?{item.Name}="; - } - else - { - urlPath += $"&{item.Name}="; - } - - //check if item value is listed in dbfields, if yes we have mapping to value from database otherwise just use value - if (_config.DBFields != null) - { - if (_config.DBFields.Any(x => x.value == item.value) && dbData != null) - { - //replace value from dbData object - KeyValuePair dbResultFound = dbData.Results.FirstOrDefault(x => x.Key == item.value); - - urlPath += $"{dbResultFound.Value}"; - - } - else - { - // no match found in parameters - urlPath += $"{item.value}"; - } - } - else - { - // no match found in parameters - urlPath += $"{item.value}"; - } - } - - return urlPath; - } - } -} \ No newline at end of file diff --git a/APITestingRunner/Database/DataAccess.cs b/APITestingRunner/Database/DataAccess.cs new file mode 100644 index 0000000..98ac3b4 --- /dev/null +++ b/APITestingRunner/Database/DataAccess.cs @@ -0,0 +1,56 @@ +using APITestingRunner.Excetions; +using Dapper; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; + +namespace APITestingRunner.Database { + public class DataAccess { + private readonly Config _config; + private readonly ILogger _logger; + + public DataAccess(Config config, Microsoft.Extensions.Logging.ILogger logger) { + _config = config ?? throw new ArgumentNullException(nameof(config)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task> FetchDataForRunnerAsync() { + + if (string.IsNullOrWhiteSpace(_config.DBConnectionString)) throw new TestRunnerConfigurationErrorsException("Failed to load connection string"); + + List list = new(); + + try { + _logger.LogDebug($"Attempting to use connection string: {_config.DBConnectionString}"); + + using SqlConnection connection = new(_config.DBConnectionString); + + IEnumerable result = await connection.QueryAsync(_config.DBQuery); + + int i = 0; + foreach (object rows in result) { + i++; + + DataQueryResult resultItem = new() { RowId = i, Results = new List>() }; + + IDictionary? fieldsInResult = rows as IDictionary; + + // get the fields from database and match to the object + if (fieldsInResult is not null && _config.DBFields is not null) { + foreach (Param configItem in _config.DBFields) { + var fieldValue = fieldsInResult[configItem.Name]?.ToString()!; + + resultItem.Results.Add(new KeyValuePair(configItem.Name, fieldValue)); + } + + list.Add(resultItem); + } + } + } catch (Exception ex) { + _logger.LogError(ex.Message); + throw; + } + + return list; + } + } +} diff --git a/APITestingRunner/Database/DataQueryResult.cs b/APITestingRunner/Database/DataQueryResult.cs new file mode 100644 index 0000000..2c244ac --- /dev/null +++ b/APITestingRunner/Database/DataQueryResult.cs @@ -0,0 +1,8 @@ +namespace APITestingRunner.Database +{ + public class DataQueryResult + { + public int RowId { get; set; } = 0; + public List> Results = new(); + } +} diff --git a/APITestingRunner/DirectoryServices.cs b/APITestingRunner/DirectoryServices.cs deleted file mode 100644 index 0de9271..0000000 --- a/APITestingRunner/DirectoryServices.cs +++ /dev/null @@ -1,16 +0,0 @@ -// See https://aka.ms/new-console-template for more information -using System.Reflection; - -public class DirectoryServices -{ - public static string AssemblyDirectory - { - get - { - string codeBase = Assembly.GetExecutingAssembly().CodeBase; - UriBuilder uri = new(codeBase); - string path = Uri.UnescapeDataString(uri.Path); - return Path.GetDirectoryName(path); - } - } -} \ No newline at end of file diff --git a/APITestingRunner/Enums/StoreResultsOption.cs b/APITestingRunner/Enums/StoreResultsOption.cs new file mode 100644 index 0000000..9e82bd4 --- /dev/null +++ b/APITestingRunner/Enums/StoreResultsOption.cs @@ -0,0 +1,19 @@ +/// +/// Options to to store the results for the response +/// +public enum StoreResultsOption { + /// + /// Just run the tests + /// + None = 0, + + /// + /// Record only failures + /// + FailuresOnly = 1, + + /// + /// stores all results + /// + All = 2 +} diff --git a/APITestingRunner/Enums/TesterConfigMode.cs b/APITestingRunner/Enums/TesterConfigMode.cs new file mode 100644 index 0000000..a085061 --- /dev/null +++ b/APITestingRunner/Enums/TesterConfigMode.cs @@ -0,0 +1,23 @@ +// See https://aka.ms/new-console-template for more information + +public enum TesterConfigMode { + /// + /// Runs the tests only and shows result as overview. + /// + Run = 1, + /// + /// Runs the tests and capture the results. + /// + Capture = 2, + /// + /// Calls APIs and store result. If file already exists then it wil also compare output from a api with stored file. + /// + CaptureAndCompare = 3, + + ///// + ///// TODO Implement first + ///Realtime compare. Compares the results of two APIs. + ///// Good for regression testing of APIs. + ///// + //APICompare = 4 +} \ No newline at end of file diff --git a/APITestingRunner/Excetions/TestRunnerConfigurationErrorsException.cs b/APITestingRunner/Excetions/TestRunnerConfigurationErrorsException.cs new file mode 100644 index 0000000..5ed77e2 --- /dev/null +++ b/APITestingRunner/Excetions/TestRunnerConfigurationErrorsException.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; + +namespace APITestingRunner.Excetions +{ + [Serializable] + public class TestRunnerConfigurationErrorsException : Exception + { + public TestRunnerConfigurationErrorsException() + { + } + + public TestRunnerConfigurationErrorsException(string? message) : base(message) + { + } + + public TestRunnerConfigurationErrorsException(string? message, Exception? innerException) : base(message, innerException) + { + } + + protected TestRunnerConfigurationErrorsException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/APITestingRunner/FileOperations.cs b/APITestingRunner/FileOperations.cs deleted file mode 100644 index 06cf5df..0000000 --- a/APITestingRunner/FileOperations.cs +++ /dev/null @@ -1,29 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -using System.Text.Json; - -namespace APITestingRunner -{ - public class FileOperations - { - public void WriteFile(string path, string name, string content, bool overrideFile) { } - public string GetFileContent(string path, string name) - { - throw new NotImplementedException(); - } - - /// - /// Expected file with extension. - /// - /// - /// - /// - /// - internal async Task WriteFile(string path, string fileName, ApiCallResult apiCallResult) - { - string objString = JsonSerializer.Serialize(apiCallResult); - - await File.WriteAllTextAsync(Path.Combine(path, fileName), objString); - } - } -} \ No newline at end of file diff --git a/APITestingRunner/IoOperations/DirectoryServices.cs b/APITestingRunner/IoOperations/DirectoryServices.cs new file mode 100644 index 0000000..1caea5c --- /dev/null +++ b/APITestingRunner/IoOperations/DirectoryServices.cs @@ -0,0 +1,20 @@ +// See https://aka.ms/new-console-template for more information +using System.Reflection; + +public class DirectoryServices { + /// + /// "C:\\code\\cpoDesign\\APITestingRunner\\APITestingRunner.Unit.Tests\\bin\\Debug\\net7.0" + /// + public static string AssemblyDirectory { + + get { +#pragma warning disable SYSLIB0012 // Type or member is obsolete + string codeBase = Assembly.GetExecutingAssembly().CodeBase!; +#pragma warning restore SYSLIB0012 // Type or member is obsolete + + UriBuilder uri = new(codeBase); + string path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path)!; + } + } +} \ No newline at end of file diff --git a/APITestingRunner/IoOperations/FileOperations.cs b/APITestingRunner/IoOperations/FileOperations.cs new file mode 100644 index 0000000..fed99fb --- /dev/null +++ b/APITestingRunner/IoOperations/FileOperations.cs @@ -0,0 +1,30 @@ +// See https://aka.ms/new-console-template for more information + +namespace APITestingRunner.IoOperations { + public class FileOperations { + public void WriteFile(string path, string name, string content, bool overrideFile) { } + public string GetFileContent(string path, string name) { + throw new NotImplementedException(); + } + + /// + /// Expected file with extension. + /// + /// + /// + /// + internal async Task WriteFile(string path, string fileContent) { + + await File.WriteAllTextAsync(path, fileContent); + } + + internal bool ValidateIfFileExists(string fileName) { + return File.Exists(fileName); + } + + public static string GetFileData(string filePath) { + return File.ReadAllText(filePath, encoding: System.Text.Encoding.UTF8).Trim(); + } + + } +} \ No newline at end of file diff --git a/APITestingRunner/IoOperations/SHA256Managed.cs b/APITestingRunner/IoOperations/SHA256Managed.cs new file mode 100644 index 0000000..1166a29 --- /dev/null +++ b/APITestingRunner/IoOperations/SHA256Managed.cs @@ -0,0 +1,6 @@ +// See https://aka.ms/new-console-template for more information + +namespace APITestingRunner.IoOperations { + internal class SHA256Managed { + } +} \ No newline at end of file diff --git a/APITestingRunner/Logger.cs b/APITestingRunner/Logger.cs new file mode 100644 index 0000000..4ff24cb --- /dev/null +++ b/APITestingRunner/Logger.cs @@ -0,0 +1,44 @@ +// See https://aka.ms/new-console-template for more information + +using Microsoft.Extensions.Logging; + +namespace APITestingRunner { + public class Logger : ILogger { + internal static void LogOutput(string logMessage) { + Console.WriteLine(logMessage); + } + + public IDisposable? BeginScope(TState state) where TState : notnull { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) { + throw new NotImplementedException(); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { + Console.ForegroundColor = ConsoleColor.White; + switch (logLevel) { + + case LogLevel.Error: + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine(state.ToString()); + break; + case LogLevel.Information: + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(state.ToString()); + break; + case LogLevel.Debug: + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine(state.ToString()); + break; + + default: + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(state.ToString()); + break; + + } + } + } +} \ No newline at end of file diff --git a/APITestingRunner/Param.cs b/APITestingRunner/Param.cs deleted file mode 100644 index 3e8d252..0000000 --- a/APITestingRunner/Param.cs +++ /dev/null @@ -1,6 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -public partial class ConfigurationManager -{ - public record Param(string Name, string value); -} diff --git a/APITestingRunner/ProcessingFileResult.cs b/APITestingRunner/ProcessingFileResult.cs new file mode 100644 index 0000000..992b469 --- /dev/null +++ b/APITestingRunner/ProcessingFileResult.cs @@ -0,0 +1,14 @@ +// See https://aka.ms/new-console-template for more information + +namespace APITestingRunner { + public class ProcessingFileResult { + public ComparissonStatus ComparissonStatus { get; set; } + public bool DisplayFilePathInLog { get; internal set; } + } + + public enum ComparissonStatus { + NewFile = 1, + Matching = 2, + Different = 3, + } +} \ No newline at end of file diff --git a/APITestingRunner/Program.cs b/APITestingRunner/Program.cs index 683209a..0acca44 100644 --- a/APITestingRunner/Program.cs +++ b/APITestingRunner/Program.cs @@ -1,26 +1,47 @@ // See https://aka.ms/new-console-template for more information -//Console.WriteLine("Hello, World!"); +using Microsoft.Extensions.Logging; +using System.CommandLine; +using System.Reflection; -//Console.WriteLine(path); -//Console.ReadLine(); - -using static ConfigurationManager; - -namespace APITestingRunner -{ +namespace APITestingRunner { /// /// Provides an eval/print loop for command line argument strings. + /// TODO: implement command line binder https://learn.microsoft.com/en-us/dotnet/standard/commandline/model-binding /// - internal static class Program - { + internal static class Program { + private static bool CreateConfig { get; set; } private static bool RunTest { get; set; } private static bool Help { get; set; } + private static readonly Config ApiTesterConfig = new() { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast/{bindingTown}", + RequestBody = null, + HeaderParam = new List { + new Param("accept","application/json") + }, + UrlParam = new List + { + new Param("urlKey", "configKey"), + new Param("id", "bindingId") + }, + DBConnectionString = "set null or enter your connection string", + DBQuery = "select id as bindingId, town as bindingTown from table name", + DBFields = new List{ + new Param("bindingId","bindingId") + }, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.All, + ConfigMode = TesterConfigMode.CaptureAndCompare, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; /// /// Returns a "pretty" string representation of the provided Type; specifically, corrects the naming of generic Types @@ -28,8 +49,7 @@ internal static class Program /// /// The type for which the colloquial name should be created. /// A "pretty" string representation of the provided Type. - public static string ToColloquialString(this Type type) - { + public static string ToColloquialString(this Type type) { return !type.IsGenericType ? type.Name : type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(a => a.ToColloquialString())) + ">"; } @@ -37,309 +57,109 @@ public static string ToColloquialString(this Type type) /// Application entry point /// /// Command line arguments - private static async Task Main(string[] args) - { - // enable ctrl+c - Console.CancelKeyPress += (o, e) => - { - Environment.Exit(1); - }; - - - while (true) - { - Console.Write("> "); - - string input = Console.ReadLine(); - - if (input == "exit") - break; - - string pathConfigJson = $"{DirectoryServices.AssemblyDirectory}\\config.json"; - Console.WriteLine("==========CreateConfigForSingleAPICall============"); - - //CreateConfigForSingleAPICall(pathConfigJson); - - //await IndividualActions.RunTests(pathConfigJson); - - //Console.ReadLine(); - - //Console.WriteLine("==========CreateConfigForSingleAPICallWithUrlParam============"); - - //CreateConfigForSingleAPICallWithUrlParam(pathConfigJson); + private static async Task Main(string[] args) { - //await IndividualActions.RunTests(pathConfigJson); + using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); + ILogger logger = loggerFactory.CreateLogger(); - //Console.ReadLine(); + //#region sample + //var delayOption = new Option + // ("--delay", "An option whose argument is parsed as an int."); - //Console.WriteLine("==========CreateConfigForDatabaseBasedAPICall============"); + //var messageOption = new Option + // ("--message", "An option whose argument is parsed as a string."); - //CreateConfigForDatabaseBasedAPICall(pathConfigJson); + //#endregion - //await IndividualActions.RunTests(pathConfigJson); + var generateConfig = new Option + ("--generateConfig", "An option to generate a new config file with sample data."); - //Console.WriteLine("======================"); + //TODO: add a option for people to provide a custom config path + //var config = new Option + // ("--config", "A path to a custom config file to be used for the runner."); + var run = new Option + ("--run", "Run the tester."); - //Console.ReadLine(); + var version = new Option + ("--version", "Print version of this tool."); - //Console.WriteLine("==========CreateConfigForDatabaseBasedAPICall============"); + var rootCommand = new RootCommand("Parameter binding example"); + //rootCommand.Add(delayOption); + //rootCommand.Add(messageOption); - //CreateConfigForDatabaseBasedAPICall(pathConfigJson, StoreResultsOption.None); + //rootCommand.SetHandler( + // (delayOptionValue, messageOptionValue) => { + // DisplayIntAndString(delayOptionValue, messageOptionValue); + // }, + // delayOption, messageOption); - //await IndividualActions.RunTests(pathConfigJson); + //rootCommand.Add(config); + rootCommand.Add(generateConfig); + rootCommand.Add(run); - //Console.WriteLine("======================"); + string pathConfigJson = $"{DirectoryServices.AssemblyDirectory}\\config.json"; - //Console.ReadLine(); + rootCommand.SetHandler((version) => { + logger.LogInformation(Assembly.GetEntryAssembly().GetName().Version.MajorRevision.ToString()); - //Console.WriteLine("==========CreateConfigForDatabaseBasedAPICall with capture mode============"); + }, version); - //CreateConfigForDatabaseBasedAPICallWithFailures(pathConfigJson, StoreResultsOption.FailuresOnly); - //await IndividualActions.RunTests(pathConfigJson); - //Console.WriteLine("======================"); + //rootCommand.SetHandler(async (generateConfig) => { + // logger.LogInformation($"Started a sample config generation."); - //Console.ReadLine(); + // await new ApiTesterRunner(logger) + // .CreateConfig(pathConfigJson, ApiTesterConfig); - //Console.WriteLine("==========CreateConfigForDatabaseBasedAPIComparrisonCall============"); + // logger.LogInformation($"Config has been generated."); - //CreateConfigForDatabaseBasedAPIComparrisonCall(pathConfigJson, StoreResultsOption.None); + //}); - //await IndividualActions.RunTests(pathConfigJson); + rootCommand.SetHandler(async (run, version, generateConfig) => { - //Console.WriteLine("======================"); + if (run) { + logger.LogInformation($"received a command to start running tests"); + logger.LogInformation($"Validating presence of a config file..."); - Console.ReadLine(); - - Console.WriteLine("==========CreateConfigForDatabaseBasedAPIComparrisonCall============"); - - CreateConfigForSingleAPICallWithUrlParamAndBodyModel(pathConfigJson); - - await IndividualActions.RunTests(pathConfigJson); - - Console.WriteLine("======================"); - } - - - Console.WriteLine("completed run"); - _ = Console.ReadKey(); - } - - private static void CreateConfigForSingleAPICall(string pathConfigJson) - { - Config config = new() - { - UrlBase = "https://localhost:7055/WeatherForecast", - CompareUrlBase = string.Empty, - CompareUrlPath = string.Empty, - UrlPath = "/WeatherForecast", - UrlParam = null, - RequestBody = null, - - HeaderParam = new List { - new Param("accept","application/json") - }, - DBConnectionString = null, - DBQuery = null, - DBFields = null, - RequestType = RequestType.GET, - ResultsStoreOption = StoreResultsOption.None, - ConfigMode = TesterConfigMode.Run, - LogLocation = DirectoryServices.AssemblyDirectory - }; - - _ = IndividualActions.CreateConfig(DirectoryServices.AssemblyDirectory, pathConfigJson, config); - } - - private static void CreateConfigForSingleAPICallWithUrlParam(string pathConfigJson) - { - Config config = new() - { - UrlBase = "https://localhost:7055", - CompareUrlBase = string.Empty, - CompareUrlPath = string.Empty, - UrlPath = "/WeatherForecast/GetWeatherForecastForLocation", - UrlParam = new List - { - new Param("location","UK") - }, - HeaderParam = new List { - new Param("accept","application/json") - }, - RequestBody = null, - DBConnectionString = null, - DBQuery = null, - DBFields = null, - RequestType = RequestType.GET, - ResultsStoreOption = StoreResultsOption.None, - ConfigMode = TesterConfigMode.Run, - LogLocation = DirectoryServices.AssemblyDirectory - }; - - _ = IndividualActions.CreateConfig(DirectoryServices.AssemblyDirectory, pathConfigJson, config); - } + if (File.Exists(pathConfigJson)) { + try { + await new ApiTesterRunner(logger).RunTests(pathConfigJson); + } catch (Exception ex) { + logger.LogInformation($"Failed to run runner Exception.{ex.Message}"); + if (ex.InnerException != null) { + logger.LogInformation($"Inner exception Exception.{ex.InnerException.Message}"); + } + } + } else { + logger.LogInformation($"Failed to find config on path: {pathConfigJson}"); + } - private static void CreateConfigForDatabaseBasedAPICall(string pathConfigJson, StoreResultsOption storeResultsOption = StoreResultsOption.None) - { - Config config = new() - { - UrlBase = "https://localhost:7055", - CompareUrlBase = string.Empty, - CompareUrlPath = string.Empty, - UrlPath = "/Data", - UrlParam = new List - { - new Param("urlKey", "test"), - new Param("id", "sqlId") - }, - HeaderParam = new List { - new Param("accept","application/json") - }, - RequestBody = null, - DBConnectionString = "Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True;", - DBQuery = "select id as sqlId from dbo.sampleTable;", - DBFields = new List - { - new Param("sqlId", "sqlId") - }, - RequestType = RequestType.GET, - ResultsStoreOption = storeResultsOption, - ConfigMode = TesterConfigMode.Run, - LogLocation = DirectoryServices.AssemblyDirectory - }; - - _ = IndividualActions.CreateConfig(DirectoryServices.AssemblyDirectory, pathConfigJson, config); - } - - private static void CreateConfigForDatabaseBasedAPICallWithFailures(string pathConfigJson, StoreResultsOption storeResultsOption = StoreResultsOption.None) - { - Config config = new() - { - UrlBase = "https://localhost:7055", - CompareUrlBase = string.Empty, - CompareUrlPath = string.Empty, - UrlPath = "/WithFailure", - UrlParam = new List - { - new Param("urlKey", "test"), - new Param("id", "sqlId") - }, - HeaderParam = new List { - new Param("accept","application/json") - }, - RequestBody = null, - DBConnectionString = "Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True;", - DBQuery = "select id as sqlId from dbo.sampleTable;", - DBFields = new List - { - new Param("sqlId", "sqlId") - }, - RequestType = RequestType.GET, - ResultsStoreOption = storeResultsOption, - ConfigMode = TesterConfigMode.Capture, - LogLocation = DirectoryServices.AssemblyDirectory - }; - - _ = IndividualActions.CreateConfig(DirectoryServices.AssemblyDirectory, pathConfigJson, config); - } - - private static void CreateConfigForDatabaseBasedAPIComparrisonCall(string pathConfigJson, StoreResultsOption storeResultsOption = StoreResultsOption.None) - { - Config config = new() - { - UrlBase = "https://localhost:7055", - CompareUrlBase = "https://localhost:7055", - UrlPath = "/Data", - CompareUrlPath = "/DataV2", - UrlParam = new List - { - new Param("urlKey", "test"), - new Param("id", "sqlId") - }, - HeaderParam = new List { - new Param("accept","application/json") - }, - RequestBody = null, - DBConnectionString = "Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True;", - DBQuery = "select id as sqlId from dbo.sampleTable;", - DBFields = new List - { - new Param("sqlId", "sqlId") - }, - RequestType = RequestType.GET, - ResultsStoreOption = storeResultsOption, - ConfigMode = TesterConfigMode.APICompare, - LogLocation = DirectoryServices.AssemblyDirectory - }; - - _ = IndividualActions.CreateConfig(DirectoryServices.AssemblyDirectory, pathConfigJson, config); - } - - - private static void CreateConfigForSingleAPICallWithUrlParamAndBodyModel(string pathConfigJson) - { - Config config = new() - { - UrlBase = "https://localhost:7055", - CompareUrlBase = string.Empty, - CompareUrlPath = string.Empty, - UrlPath = "/datamodel/123456789", - UrlParam = new List - { - new Param("location","UK") - }, - HeaderParam = new List { - new Param("accept","application/json") - }, - RequestBody = "{Id={sqlId},StaticData=\"data\"}", - DBConnectionString = null, - DBQuery = null, - DBFields = null, - RequestType = RequestType.GET, - ResultsStoreOption = StoreResultsOption.None, - ConfigMode = TesterConfigMode.Run, - LogLocation = DirectoryServices.AssemblyDirectory - }; - - _ = IndividualActions.CreateConfig(DirectoryServices.AssemblyDirectory, pathConfigJson, config); - } - } - - public class IndividualActions - { - public static async Task CreateConfig(string directory, string pathConfigJson, Config config) - { - ConfigurationManager configManager = new(); - - Console.WriteLine($"Created config on path: {pathConfigJson}"); - await configManager.CreateConfig(pathConfigJson, config); - return; - - } - public static async Task RunTests(string pathConfigJson) - { - Console.WriteLine($"Loading config on path: {pathConfigJson}"); + logger.LogInformation(""); + logger.LogInformation("Completed test run"); + } - ConfigurationManager configManager = new(); + if (generateConfig) { + logger.LogInformation($"Started a sample config generation."); - Config? configSettings = await configManager.GetConfigAsync(pathConfigJson); - TestRunner testRunner = new(); - await testRunner.ApplyConfig(configSettings); + await new ApiTesterRunner(logger) + .CreateConfig(pathConfigJson, ApiTesterConfig); + logger.LogInformation($"Config has been generated."); + return; + } - // execute db data load only has some data in it - if (!string.IsNullOrWhiteSpace(configSettings.DBConnectionString) && !string.IsNullOrWhiteSpace(configSettings.DBQuery) && configSettings.DBFields.Count() > 0) - { - testRunner = await testRunner.GetTestRunnerDbSet(); - } + if (version) { + logger.LogInformation(Assembly.GetEntryAssembly().GetName().Version.MajorRevision.ToString()); + return; + } - testRunner = await testRunner.RunTestsAsync(); + return; + }, run, version, generateConfig); - _ = await testRunner.PrintResults(); - return; + _ = await rootCommand.InvokeAsync(args); } } } \ No newline at end of file diff --git a/APITestingRunner/Properties/launchSettings.json b/APITestingRunner/Properties/launchSettings.json new file mode 100644 index 0000000..5ac16a5 --- /dev/null +++ b/APITestingRunner/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "APITestingRunner": { + "commandName": "Project", + "commandLineArgs": "--run" + } + } +} \ No newline at end of file diff --git a/APITestingRunner/RequestType.cs b/APITestingRunner/RequestType.cs deleted file mode 100644 index 1715666..0000000 --- a/APITestingRunner/RequestType.cs +++ /dev/null @@ -1,15 +0,0 @@ - -public partial class ConfigurationManager -{ - /// - /// Contains request types that can be used to make the api call. - /// - public enum RequestType - { - GET = 1, - POST = 2, - PUT = 3, - PATCH = 4, - DELETE = 5, - } -} diff --git a/APITestingRunner/StoreResultsOption.cs b/APITestingRunner/StoreResultsOption.cs deleted file mode 100644 index cf72a40..0000000 --- a/APITestingRunner/StoreResultsOption.cs +++ /dev/null @@ -1,23 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -public partial class ConfigurationManager -{ - /// - /// Options to to store the results for the response - /// - public enum StoreResultsOption - { - /// - /// Just run the tests - /// - None = 0, - /// - /// stores all results - /// - All = 1, - /// - /// Record only failures - /// - FailuresOnly = 2, - } -} diff --git a/APITestingRunner/TestResultStatus.cs b/APITestingRunner/TestResultStatus.cs index 6f62443..10ca128 100644 --- a/APITestingRunner/TestResultStatus.cs +++ b/APITestingRunner/TestResultStatus.cs @@ -1,10 +1,18 @@ -// See https://aka.ms/new-console-template for more information - -namespace APITestingRunner -{ - public class TestResultStatus - { - public int StatusCode { get; set; } - public int NumberOfResults { get; set; } = 0; - }; +// See https://aka.ms/new-console-template for more information + +namespace APITestingRunner +{ + public class TestResultStatus + { + public int StatusCode { get; set; } + public int NumberOfResults { get; set; } = 0; + }; + + public class TestConstants + { + /// + /// Name of the test directory. + /// + public static string TestOutputDirectory = "Results"; + } } \ No newline at end of file diff --git a/APITestingRunner/TestRunner.cs b/APITestingRunner/TestRunner.cs index 35e1e24..271d3cc 100644 --- a/APITestingRunner/TestRunner.cs +++ b/APITestingRunner/TestRunner.cs @@ -1,318 +1,449 @@ // See https://aka.ms/new-console-template for more information +using APITestingRunner.ApiRequest; +using APITestingRunner.Database; +using APITestingRunner.IoOperations; +using Microsoft.Extensions.Logging; +using System.Diagnostics; using System.Net.Mime; using System.Text; +using System.Text.Json; -namespace APITestingRunner -{ - internal class TestRunner - { - private Config _config; - private IEnumerable? _dbBasedItems = new List(); +namespace APITestingRunner { + public class TestRunner { + private Config? _config; + private readonly IEnumerable? _dbBasedItems = new List(); private readonly List _errors = new(); private readonly List responses = new(); private readonly List _resultsStats = new(); - private HttpClient? compareClient = null; - private string? compareUrl; - - public TestRunner() - { + //private readonly HttpClient? compareClient = null; + //private readonly string? compareUrl; + private readonly ILogger _logger; + + public List Errors => _errors; + + /// + /// Instance of test runner executing the tests. + /// + /// Instance of a logger. + /// Can throw when logger is not provided. + public TestRunner(ILogger logger) { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - internal async Task ApplyConfig(Config? configSettings) - { - if (configSettings == null) - { + internal async Task ApplyConfig(Config? configSettings) { + if (configSettings == null) { _errors.Add("Failed to load configuration"); + } else { + _config = configSettings; } - _config = configSettings; await Task.CompletedTask; } - internal async Task GetTestRunnerDbSet() - { - // connect to database and load information - DataAccess db = new(_config); - _dbBasedItems = await db.FetchDataForRunnerAsync(); - - if (_dbBasedItems != null) - { - Console.WriteLine($"Found {_dbBasedItems.Count()} records for test"); - } - - return this; - } + internal async Task RunTestsAsync() { + _ = _config ?? throw new ArgumentNullException(nameof(_config)); - internal async Task RunTestsAsync() - { // create a request to the api + var handler = new HttpClientHandler(); - HttpClient client = new() - { + handler.ServerCertificateCustomValidationCallback += + (sender, certificate, chain, errors) => { + return true; + }; + + //TODO: conver to HTTPFactory to produce api calls + HttpClient client = new(handler) { BaseAddress = new Uri(_config.UrlBase) }; + PopulateClientHeadersFromConfig(client, _config.HeaderParam); - if (!string.IsNullOrWhiteSpace(_config.CompareUrlBase)) - { - compareClient = new() - { - BaseAddress = new Uri(_config.UrlBase) - }; - } + await MakeApiCall(client); + + return this; + } + + private void PopulateClientHeadersFromConfig(HttpClient client, List headerParam) { + _ = _config ?? throw new ArgumentNullException(nameof(_config)); - if (_config.HeaderParam != null && _config.HeaderParam.Count > 0) - { - foreach (ConfigurationManager.Param item in _config.HeaderParam) - { + if (headerParam != null && headerParam.Count > 0) { + foreach (Param item in _config.HeaderParam) { client.DefaultRequestHeaders.Add(item.Name, item.value); } } + } - if (_dbBasedItems.Count() > 0) - { - //http://localhost:5152/Data?id=1 - foreach (DataQueryResult item in _dbBasedItems) - { - Console.WriteLine($"proceeding with call for record {item.RowId}"); + private async Task MakeApiCall(HttpClient client) { - await MakeApiCorCollectionCall(client, item); - } - } - else - { - //http://localhost:5152/Data?id=1 - await MakeApiCall(client); + var numberOfResults = 0; + foreach (DataQueryResult dataQueryResult in await GetDataToProcessAsync()) { + + await MakeApiForCollectionCall(client, dataQueryResult, numberOfResults++, PopulateRequestBody(_config, dataQueryResult)); } - return this; + return; } - private async Task MakeApiCall(HttpClient client) - { - string? url = string.Empty; - try - { - url = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, null); + public static string PopulateRequestBody(Config config, DataQueryResult dataQueryResult) { + if (config is null) throw new ArgumentNullException(nameof(config)); + if (dataQueryResult is null) throw new ArgumentNullException(nameof(dataQueryResult)); - if (_config.ConfigMode == ConfigurationManager.TesterConfigMode.APICompare) - { - compareUrl = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.CompareUrlPath, _config, null); + return ReplaceValueWithDataSource(config.RequestBody, dataQueryResult); + } + + public static string ReplaceValueWithDataSource(string stringToUpdate, DataQueryResult dataQueryResult) { + if (!string.IsNullOrWhiteSpace(stringToUpdate)) { + // we have a value + var replaceValues = stringToUpdate; + + if (dataQueryResult.Results.Any()) { + foreach (var item in dataQueryResult.Results) { + replaceValues = replaceValues.Replace($"{{{item.Key}}}", item.Value); + } } - } - catch (Exception) - { - _errors.Add($"Error has occurred while composing an url: {url}"); - return; + + return replaceValues; } - await MakeApiCorCollectionCall(client, url); - return; + + return string.Empty; } - private async Task MakeApiCorCollectionCall(HttpClient client, DataQueryResult item) - { - string? url = string.Empty; - try - { - url = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, item); - if (_config.ConfigMode == ConfigurationManager.TesterConfigMode.APICompare) - { - compareUrl = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.CompareUrlPath, _config, item); + /// + /// Gets data for processing. + /// TODO: convert to yield + /// + /// + /// + public async Task> GetDataToProcessAsync() { + _ = _config ?? throw new ArgumentNullException(nameof(_config)); + + _logger.LogInformation("Validating database based data source start"); + /// return data source + if (!string.IsNullOrWhiteSpace(_config.DBConnectionString)) { + + _logger.LogInformation("Found database connection string"); + + if (!string.IsNullOrWhiteSpace(_config.DBQuery) && _config?.DBFields?.Count() > 0) { + + _logger.LogInformation("Found database query and db fields. Attempting to load data from database."); + + return await new DataAccess(_config, _logger).FetchDataForRunnerAsync(); } } - catch (Exception) - { - _errors.Add($"Error has occurred while composing an url: {url}"); + + return new List { new DataQueryResult() { RowId = 0 } }; + } + + /// + /// + /// + /// Client contains base url + /// contains the relative paths + /// + /// + /// + /// + private async Task MakeApiForCollectionCall(HttpClient client, DataQueryResult item, int numberOfResults, string requestBody = "") { + _ = _config ?? throw new ArgumentNullException(nameof(_config)); + + HttpResponseMessage? response = null; + string onScreenMessage = string.Empty; + + var pathAndQuery = string.Empty; + try { + pathAndQuery = new DataRequestConstructor() + .AddBaseUrl(_config.UrlBase) + .ComposeUrlAddressForRequest(_config.UrlPath, _config, item) + .GetPathAndQuery(); + + //if (_config.ConfigMode == ConfigurationManager.TesterConfigMode.APICompare) + //{ + // compareUrl = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.CompareUrlPath, _config, null); + //} + } catch (Exception) { + _errors.Add($"Error has occurred while composing an url: {pathAndQuery}"); return; } - await MakeApiCorCollectionCall(client, url, item); + if (string.IsNullOrWhiteSpace(pathAndQuery)) { + _errors.Add("Failed to compose path and query for API request"); + return; + } + // update variables from url directly on url + pathAndQuery = TestRunner.ReplaceValueWithDataSource(pathAndQuery, item); - return; - } + try { - private async Task MakeApiCorCollectionCall(HttpClient client, string url, DataQueryResult? item = null, string requestBody = "") - { - HttpResponseMessage? response = null; - try - { + switch (_config.RequestType) { + case RequestType.GET: - switch (_config.RequestType) - { - case ConfigurationManager.RequestType.GET: + if (string.IsNullOrWhiteSpace(requestBody)) { + // we are using only data in url query + response = await client.GetAsync(pathAndQuery); + } else { + //TODO: there are people with existing APIs that are using get with providing data as part of request body we need to cater for. + throw new NotImplementedException(); + //// we are sending data in body + //HttpRequestMessage request = new() + //{ + // Method = HttpMethod.Get, + // Content = CreateRequestContent(requestBody) + //}; - HttpRequestMessage request = new() - { - Method = HttpMethod.Get, - RequestUri = new Uri(url), - Content = CreateRequestContent(requestBody) - }; - - response = !string.IsNullOrWhiteSpace(requestBody) ? await client.SendAsync(request) : await client.GetAsync(url); + //response = await client.SendAsync(request); + } break; - case ConfigurationManager.RequestType.POST: + case RequestType.POST: + + var requestContent = CreateRequestContent(requestBody); + Debug.WriteLine($"Capturing requestBody: {await requestContent.ReadAsStringAsync()}"); + response = await client.PostAsync(pathAndQuery, requestContent); - response = await client.PostAsync(url, CreateRequestContent(requestBody)); break; - case ConfigurationManager.RequestType.PUT: - response = await client.PutAsync(url, CreateRequestContent(requestBody)); + case RequestType.PUT: + response = await client.PutAsync(pathAndQuery, CreateRequestContent(requestBody)); break; - case ConfigurationManager.RequestType.PATCH: - response = await client.PatchAsync(url, CreateRequestContent(requestBody)); + case RequestType.PATCH: + response = await client.PatchAsync(pathAndQuery, CreateRequestContent(requestBody)); break; - case ConfigurationManager.RequestType.DELETE: - response = await client.DeleteAsync(url); + case RequestType.DELETE: + response = await client.DeleteAsync(pathAndQuery); break; default: _errors.Add("Unsupported request type"); break; } - if (response != null) - { - string content = await response.Content.ReadAsStringAsync(); - - if (_config.ConfigMode == ConfigurationManager.TesterConfigMode.APICompare) - { - List compareList = new(); + Debug.WriteLine(response); - if (compareClient != null) - { - HttpResponseMessage compareResponse = await compareClient.GetAsync(compareUrl); + if (response != null) { + _resultsStats.Add(new TestResultStatus() { + NumberOfResults = numberOfResults, + StatusCode = (int)response.StatusCode + }); - string responseCompareContent = await response.Content.ReadAsStringAsync(); + onScreenMessage = GenerateResponseMessage(_config.RequestType, pathAndQuery, response); + string content = await response.Content.ReadAsStringAsync(); + var fileName = string.Empty; + var responseHeaders = response.Headers.Select(x => new KeyValuePair(x.Key, x.Value.ToString() ?? string.Empty)).ToList(); + ProcessingFileResult result = null; + switch (_config.ConfigMode) { + case TesterConfigMode.Run: + // no another work necessary here + break; + + case TesterConfigMode.Capture: + fileName = TestRunner.GenerateResultName(item, _config.ResultFileNamePattern); + + result = await ProcessResultCaptureAndCompareIfRequested( + new ApiCallResult(response.StatusCode, content, responseHeaders, pathAndQuery, item, response.IsSuccessStatusCode)); + if (result.DisplayFilePathInLog) { + onScreenMessage += $" {TestConstants.TestOutputDirectory}/{fileName}"; + } + break; + case TesterConfigMode.CaptureAndCompare: + fileName = TestRunner.GenerateResultName(item, _config.ResultFileNamePattern); - // compare status code - if (response.StatusCode == compareResponse.StatusCode) - { - compareList.Add($"Status code SourceAPI: {response.StatusCode} CompareAPI: {response.StatusCode}"); - } - // compare content - if (content != responseCompareContent) - { - compareList.Add("APIs content does not match"); - } + result = await ProcessResultCaptureAndCompareIfRequested(new ApiCallResult(response.StatusCode, content, responseHeaders, pathAndQuery, item, response.IsSuccessStatusCode)); - if (compareList.Count == 0) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Comparing API for {item?.RowId} success"); - } - else - { - Console.Write($"Comparing API for {item?.RowId} Failed"); - foreach (string errorsInComparrison in compareList) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"- {errorsInComparrison}"); - } + if (result.DisplayFilePathInLog) { + onScreenMessage += $" {TestConstants.TestOutputDirectory}/{fileName}"; } + onScreenMessage += $" {Enum.GetName(result.ComparissonStatus)}"; - Console.ForegroundColor = ConsoleColor.White; - - _ = ProcessResultCapture(new ApiCallResult(compareResponse.StatusCode, responseCompareContent, compareResponse.Headers, url, item, compareResponse.IsSuccessStatusCode), true); - } + break; - await ProcessResultCapture(new ApiCallResult(response.StatusCode, content, response.Headers, url, item, response.IsSuccessStatusCode)); - } - else - { - _errors.Add("Failed to find configuration for compare API"); + default: + _errors.Add("This option is currently not supported"); + throw new NotImplementedException(); } + + //TODO:implement + ///case TesterConfigMode.APICompare: + //if (_config.ConfigMode == ConfigurationManager.TesterConfigMode.APICompare) + //{ + // List compareList = new(); + + // if (compareClient != null) + // { + // HttpResponseMessage compareResponse = await compareClient.GetAsync(compareUrl); + + // string responseCompareContent = await response.Content.ReadAsStringAsync(); + + + // // compare status code + // if (response.StatusCode == compareResponse.StatusCode) + // { + // compareList.Add($"Status code SourceAPI: {response.StatusCode} CompareAPI: {response.StatusCode}"); + // } + + // // compare content + // if (content != responseCompareContent) + // { + // compareList.Add("APIs content does not match"); + // } + + // if (compareList.Count == 0) + // { + // Console.ForegroundColor = ConsoleColor.Green; + // Console.WriteLine($"Comparing API for {item?.RowId} success"); + // } + // else + // { + // Console.Write($"Comparing API for {item?.RowId} Failed"); + // foreach (string errorsInComparrison in compareList) + // { + // Console.ForegroundColor = ConsoleColor.Red; + // Console.WriteLine($"- {errorsInComparrison}"); + // } + // } + + // Console.ForegroundColor = ConsoleColor.White; + + // _ = ProcessResultCapture(new ApiCallResult(compareResponse.StatusCode, responseCompareContent, compareResponse.Headers, pathAndQuery, item, compareResponse.IsSuccessStatusCode), true); + // } + + // await ProcessResultCapture(new ApiCallResult(response.StatusCode, content, response.Headers, pathAndQuery, item, response.IsSuccessStatusCode)); + //} + //else + //{ + // _errors.Add("Failed to find configuration for compare API"); + //} } + } catch (Exception ex) { + _errors.Add($"Error has occurred while calling api with url:{client.BaseAddress} with {pathAndQuery} with message:{onScreenMessage} && exception: {ex.Message}"); + } finally { + _logger.LogInformation(onScreenMessage); } - catch (Exception ex) - { - _errors.Add($"Error has occurred while calling api with url:{url} with message: {ex.Message}"); + } + + public string GenerateResponseMessage(RequestType requestType, string relativeUrl, HttpResponseMessage? response) { + var determination = "fail"; + + if (response is not null) { + if (response.IsSuccessStatusCode) determination = "success"; + return $"{requestType} {relativeUrl} {(int)response.StatusCode} {determination}"; } + + return $"{relativeUrl} failed to return"; } - private static StringContent CreateRequestContent(string requestBody) - { + private static StringContent CreateRequestContent(string requestBody) { return new(requestBody, Encoding.UTF8, MediaTypeNames.Application.Json); } - private async Task ProcessResultCapture(ApiCallResult apiCallResult, bool IsCompareFile = false) - { - string response = $"{apiCallResult.statusCode} - {apiCallResult.responseContent}"; + private async Task ProcessResultCaptureAndCompareIfRequested(ApiCallResult apiCallResult) { + _ = _config ?? throw new ArgumentNullException(nameof(_config)); + _ = $"{apiCallResult.statusCode} - {apiCallResult.responseContent}"; - if (_config.ConfigMode == ConfigurationManager.TesterConfigMode.Capture) - { - if (_config.ResultsStoreOption == ConfigurationManager.StoreResultsOption.All || (_config.ResultsStoreOption == ConfigurationManager.StoreResultsOption.FailuresOnly && !apiCallResult.IsSuccessStatusCode)) - { - if (_config.LogLocation != null) - { - await logIntoFileAsync(_config.LogLocation, apiCallResult, IsCompareFile); - } - else - { + ComparissonStatus fileCompareStatus = ComparissonStatus.NewFile; + var result = new ProcessingFileResult { ComparissonStatus = fileCompareStatus }; + + if (_config.ConfigMode == TesterConfigMode.Capture || _config.ConfigMode == TesterConfigMode.CaptureAndCompare) { + if (_config.ResultsStoreOption == StoreResultsOption.All || (_config.ResultsStoreOption == StoreResultsOption.FailuresOnly && !apiCallResult.IsSuccessStatusCode)) { + if (_config.OutputLocation != null) { + result.DisplayFilePathInLog = true; + result.ComparissonStatus = await logIntoFileAsync(_config.OutputLocation, apiCallResult, false); + } else { _errors.Add("No logLocation found"); } } } - if (!IsCompareFile) - { - TestResultStatus? existingResult = _resultsStats.FirstOrDefault(x => x.StatusCode == (int)apiCallResult.statusCode); - if (existingResult == null) - { - _resultsStats.Add(new TestResultStatus { StatusCode = (int)apiCallResult.statusCode, NumberOfResults = 1 }); - } - else - { - existingResult.NumberOfResults++; - } - responses.Add(response); - } + //if (IsCompareFile) { + // TestResultStatus? existingResult = _resultsStats.FirstOrDefault(x => x.StatusCode == (int)apiCallResult.statusCode); + // if (existingResult == null) { + // _resultsStats.Add(new TestResultStatus { StatusCode = (int)apiCallResult.statusCode, NumberOfResults = 1 }); + // } else { + // existingResult.NumberOfResults++; + // } + + // responses.Add(response); + //} + + return result; - Console.WriteLine(response); } - private async Task logIntoFileAsync(string logLocation, ApiCallResult apiCallResult, bool IsCompareFile) - { - string filePrefix = "request"; - try - { - string resultsDirectory = Path.Combine(logLocation, "Results"); - if (!Directory.Exists(resultsDirectory)) - { + /// + /// Validate If File already exists. + /// + /// + /// + /// + /// Boolean result checking if file already exists. + private async Task logIntoFileAsync(string logLocation, ApiCallResult apiCallResult, bool validateIfFilesMatch = false) { + _ = _config ?? throw new ArgumentNullException(nameof(_config)); + try { + string resultsDirectory = Path.Combine(logLocation, TestConstants.TestOutputDirectory); + if (!Directory.Exists(resultsDirectory)) { _ = Directory.CreateDirectory(resultsDirectory); } - string fileName = $"{filePrefix}-{apiCallResult.item?.RowId}"; - if (IsCompareFile) - { - fileName += "Compare"; + var status = ComparissonStatus.NewFile; + var fileName = TestRunner.GenerateResultName(apiCallResult.item, _config.ResultFileNamePattern); + var fileOperations = new FileOperations(); + + var filePath = Path.Combine(resultsDirectory, fileName); + string apiResult = JsonSerializer.Serialize(apiCallResult); + + if (fileOperations.ValidateIfFileExists(filePath)) { + var fileSourceResult = JsonSerializer.Deserialize(FileOperations.GetFileData(filePath)); + + if (fileSourceResult is not null) { + status = DataComparrison.CompareAPiResults(apiCallResult, fileSourceResult); + } + } else { + await fileOperations.WriteFile(filePath, apiResult); } + return status; + } catch (Exception ex) { + Debug.WriteLine(ex); - await new FileOperations().WriteFile(resultsDirectory, $"{fileName}.json", apiCallResult); + _errors.Add("Failed to capture logs into a file"); + throw; } - catch - { + } + + + + public static string GenerateResultName(DataQueryResult? item, string? resultFileNamePattern = null) { + if (item == null) throw new ArgumentNullException(nameof(item)); + + string filePrefix = "request"; + string fileSuffix = ".json"; + + if (string.IsNullOrWhiteSpace(resultFileNamePattern)) { + return $"{filePrefix}-{item?.RowId}{fileSuffix}"; + } else { + + //we have value lets replace it + + foreach (var resultItem in item.Results) { + resultFileNamePattern = resultFileNamePattern.Replace($"{{{resultItem.Key}}}", resultItem.Value); + } + + return $"{filePrefix}-{resultFileNamePattern}{fileSuffix}"; } } - internal async Task PrintResults() - { + internal async Task PrintResultsSummary() { Console.WriteLine("==========Status=========="); - foreach (TestResultStatus item in _resultsStats) - { + foreach (TestResultStatus item in _resultsStats) { Console.WriteLine($"{item.StatusCode} - Count: {item.NumberOfResults}"); } - if (_errors.Count > 0) - { + if (_errors.Count > 0) { Console.WriteLine("==========Errors=========="); - foreach (string error in _errors) - { + foreach (string error in _errors) { Console.WriteLine(error); } } @@ -320,5 +451,42 @@ internal async Task PrintResults() await Task.CompletedTask; return this; } + + private async Task MakeApiCorCollectionCall(HttpClient client, DataQueryResult item) { + _logger.LogInformation($"API base url: {client.BaseAddress}"); + //string? url = string.Empty; + //try + //{ + // url = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.UrlPath, _config, item); + // if (_config.ConfigMode == ConfigurationManager.TesterConfigMode.APICompare) + // { + // compareUrl = new DataRequestConstructor().ComposeUrlAddressForRequest(_config.CompareUrlPath, _config, item); + // } + //} + //catch (Exception) + //{ + // _errors.Add($"Error has occurred while composing an url: {url}"); + // return; + //} + + //await MakeApiForCollectionCall(client, url, item); + + + return; + } + } + public class DataComparrison { + + + public static ComparissonStatus CompareAPiResults(ApiCallResult apiCallResult, ApiCallResult fileSourceResult) { + var status = ComparissonStatus.Different; + + if (apiCallResult.statusCode == fileSourceResult.statusCode) + if (apiCallResult.IsSuccessStatusCode == fileSourceResult.IsSuccessStatusCode) + if (apiCallResult.responseContent == fileSourceResult.responseContent) + status = ComparissonStatus.Matching; + + return status; + } } } \ No newline at end of file diff --git a/APITestingRunner/TestRunnerConfigurationErrorsException.cs b/APITestingRunner/TestRunnerConfigurationErrorsException.cs deleted file mode 100644 index 35e76dd..0000000 --- a/APITestingRunner/TestRunnerConfigurationErrorsException.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Runtime.Serialization; - -namespace APITestingRunner -{ - [Serializable] - public class TestRunnerConfigurationErrorsException : Exception - { - public TestRunnerConfigurationErrorsException() - { - } - - public TestRunnerConfigurationErrorsException(string? message) : base(message) - { - } - - public TestRunnerConfigurationErrorsException(string? message, Exception? innerException) : base(message, innerException) - { - } - - protected TestRunnerConfigurationErrorsException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - } -} \ No newline at end of file diff --git a/APITestingRunner/TesterConfigMode.cs b/APITestingRunner/TesterConfigMode.cs deleted file mode 100644 index 68d4006..0000000 --- a/APITestingRunner/TesterConfigMode.cs +++ /dev/null @@ -1,25 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -public partial class ConfigurationManager -{ - public enum TesterConfigMode - { - /// - /// Runs the tests only and shows result as overview. - /// - Run = 1, - /// - /// Runs the tests and capture the results. - /// - Capture = 2, - /// - /// Calls APIs and compare to a stored file. - /// - FileCompare = 3, - /// - /// Realtime compare. Compares the results of two APIs. - /// Good for regression testing of APIs. - /// - APICompare = 4 - } -} diff --git a/APITestingRunner/config.json b/APITestingRunner/config.json new file mode 100644 index 0000000..cac0e66 --- /dev/null +++ b/APITestingRunner/config.json @@ -0,0 +1,17 @@ +{ + "UrlBase": "https://localhost:7055", + "CompareUrlBase": "", + "UrlPath": "/get/{bindingId}", + "CompareUrlPath": "", + "UrlParam": null, + "HeaderParam": [{ "Name": "accept", "value": "application/json" }], + "RequestBody": null, + "RequestType": 1, + "ConfigMode": 3, + "ResultsStoreOption": 2, + "OutputLocation": "C:\\code\\cpoDesign\\APITestingRunner\\APITestingRunner\\bin\\Release\\net7.0", + "DBConnectionString": "Server=127.0.0.1; Database=testdb; User Id=sa; Password=;TrustServerCertificate=True", + "DBQuery": "select id as bindingId from dbo.sampleTable where id = 1", + "DBFields": [{ "Name": "bindingId", "value": "bindingId" }], + "ResultFileNamePattern": null +} diff --git a/SampleAPI/Controllers/DataController.cs b/SampleAPI/Controllers/DataController.cs index cd5d400..afaf947 100644 --- a/SampleAPI/Controllers/DataController.cs +++ b/SampleAPI/Controllers/DataController.cs @@ -2,27 +2,34 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; -namespace SampleAPI.Controllers -{ +namespace SampleAPI.Controllers { [ApiController] [Route("[controller]")] - public partial class DataController : ControllerBase - { + public partial class DataController : ControllerBase { private readonly ILogger _logger; - public DataController(ILogger logger) - { + public DataController(ILogger logger) { _logger = logger; } + [HttpGet(Name = "get")] - public async Task GetAsync(int id, string urlKey = "") - { - if (string.IsNullOrEmpty(urlKey)) - { + public async Task GetAsync(int id, string urlKey = "") { + if (string.IsNullOrEmpty(urlKey)) { return BadRequest("Provide url key"); } - string connectionString = $"Server=127.0.0.1; Database=test; User Id=sa; Password=;TrustServerCertificate=True"; + string connectionString = $"Server=127.0.0.1; Database=testdb; User Id=sa; Password=;TrustServerCertificate=True"; + + using SqlConnection connection = new(connectionString); + string sql = $"select * from dbo.sampleTable where id={id};"; + SampleData result = await connection.QuerySingleAsync(sql); + return result == null ? NotFound() : Ok(result); + } + + [HttpGet("/get/{id}")] + public async Task GetByIdAsync(int id) { + + string connectionString = $"Server=127.0.0.1; Database=testdb; User Id=sa; Password=;TrustServerCertificate=True"; using SqlConnection connection = new(connectionString); string sql = $"select * from dbo.sampleTable where id={id};"; diff --git a/lut/APITestingRunner/v2/0.pid b/lut/APITestingRunner/v2/0.pid new file mode 100644 index 0000000..799de75 --- /dev/null +++ b/lut/APITestingRunner/v2/0.pid @@ -0,0 +1 @@ +16060 \ No newline at end of file diff --git a/lut/APITestingRunner/v2/0/.lutsyncmode b/lut/APITestingRunner/v2/0/.lutsyncmode new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/lut/APITestingRunner/v2/0/.lutsyncmode @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/readme.md b/readme.md index 3e0dbbd..0fa40d8 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,153 @@ +# Promethean API Test Runner + +TODO: +outstanding items + +command line options +-- generate a config + +1 wiremock add validation for a api call with setup for post with data so it matches a result +1. Create a status report + 1. Add analysis on how many ware successfull and how many not +1. 1. Compatibility +we use UTF-32 to ensure maximum compatibility as the application has to support Chinese traditional and Chinese simplified. Traditional UTF-8 does not support it. + + + +## Purpose of the tool +- Create an API Test, and: + - Review the return. + - Review and the database store (based on MS SQL database). +- Capture (Store) the test results to file. +- Compare with previous request. + +Supported methods + + |Request Types |Supported | + |-- |-- | + |GET |Yes | + |PUT |Yes | + |POST |Yes | + |PATCH |Yes | + |DELETE |Yes | + + + + +Modes for the implementation + +This tool allows you to run the api calls in multiple modes +- FileCompare + + +### FileCompare +we compare only a response from the file + +## Configuration fields + +|Key|Required|Value| +|--|--|--| +|HeaderParam|no| Appends header information to the API requires| +|OutputLocation|Conditional|Location where captured logs are stored. Depends on ConfigMode = Capture or CaptureAndCompare| +|ResultsStoreOption|Yes|Required with possible values None, FailuresOnly, All| +|UrlBase|Yes | Location where to make api call to| +|UrlParam|Yes|Url param allows adding query parameters to url| + +### UrlParam + +#### Static binding +---- +Populate the http request with a value to get a static parameter + +```url +http://localhost:7055/WeatherForecast?urlkey=configKey +``` +the config will look like this: + +```c# +UrlBase = "http://localhost:7055", +UrlPath = "/WeatherForecast", +UrlParam = new List + { + new Param("urlKey", "configKey"), + } +``` + +#### Data driven parameter + +populate id with database + +```url +http://localhost:7055/WeatherForecast?id=15 +``` + +config to create the binding, mark the id from database in your sql query and use dbfields to capture the output + +```c# +UrlBase = "http://localhost:7055", +UrlPath = "/WeatherForecast", +DBConnectionString = "", +UrlParam = new List{ + new Param("urlKey", "configKey"), + new Param("id", "bindingId") +}, +DBQuery = "select id as bindingId from dbo.sampleTable;", +DBFields = new List{ + new Param("bindingId", "bindingId"), + new Param("fieldName", "fieldName") +}, +``` + +this will map to param with this pattern + +```url +http://localhost:7055/WeatherForecast?id={bindingId} +``` + +### ConfigMode types + +|Type |ConfigValue|UseCase| +|-- |-- |--| +|Run |1 |Runs the tests only and shows result as overview.| +|Capture |2 |Runs the tests and capture the results. Process will fail in case the file already exists. | +|CaptureAndCompare |3 |Calls APIs and store result. If file already exists then it wil also compare output with api result.| + +~~|APICompare |4 |Not implemented yet. Realtime compare. Compares the results of two APIs. Good for regression testing of APIs.|~~ + +### ResultsStoreOption + +This has to be used with configuration ConfigMode min level capture + +|Name |Numeric value| purpose| +|-- |-- |--| +|None | 0 |Just run the tests| +|FailuresOnly | 1 |Record only failures| +|All | 2 |Stores all results| + + +if while is being stored, the file name is part of output data +```bash +/WeatherForecast?urlKey=configKey&id=1 404 fail Results/request-1.json +``` + +#### RUN + +takes the configuration and only execute a call against the api +Reports success to the api as + +Example of the test: +```bash +api base url: http://localhost:7071/ + +1. /Data OK success +2. /Data/1 OK success +``` ## Docker SQL test +Note: out of date needs more columns to support test cases + ```bash docker pull mcr.microsoft.com/mssql/server:2022-latest ``` @@ -39,4 +185,3 @@ GO INSERT [dbo].[sampleTable] ([id], [name], [description]) VALUES (4, N'Name4', N'descirption 2') GO ``` -