diff --git a/APITestingRunner.Unit.Tests/Plugins/StringPluginTests.cs b/APITestingRunner.Unit.Tests/Plugins/StringPluginTests.cs new file mode 100644 index 0000000..6cc7a6b --- /dev/null +++ b/APITestingRunner.Unit.Tests/Plugins/StringPluginTests.cs @@ -0,0 +1,177 @@ +using APITestingRunner.ApiRequest; +using APITestingRunner.Configuration; +using APITestingRunner.IoOperations; +using APITestingRunner.Plugins; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; + +namespace APITestingRunner.Unit.Tests.Plugins +{ + [TestClass] + public class StringPluginTests + { + private IPlugin? _stringComparisonPlugin = new StringComparisonPlugin(); + + private TestLogger _logger = new(); + + [TestInitialize] + public void Initialize() + { + _stringComparisonPlugin = new StringComparisonPlugin(); + IConfig baseConfig = new Config() + { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast", + UrlParam = null, + RequestBody = null, + HeaderParam = null, + DBConnectionString = null, + DBQuery = null, + DBFields = null, + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.None, + ConfigMode = TesterConfigMode.Run, + OutputLocation = DirectoryServices.AssemblyDirectory, + ResultFileNamePattern = null, + ContentReplacements = null + }; + + _logger = new TestLogger(); + _stringComparisonPlugin.ApplyConfig(ref baseConfig, _logger); + } + + [TestMethod] + public void NoDifferencesReported() + { + var json1 = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}"; + var json2 = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}"; + var apiResult1 = CreateApiResultForJsonResponse(json1); + var apiResult2 = CreateApiResultForJsonResponse(json2); + + _ = _stringComparisonPlugin.ProcessComparison(apiResult1, apiResult2, ComparisonStatus.NewFile); + _ = _logger.Messages.Should().HaveCount(1); + _ = _logger.Messages.First().Item2.Should().Contain("Source and target has same length."); + } + + + [TestMethod] + public void DifferentLength_isReported() + { + var json1 = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}"; + var json2 = "{\"age\":30,\"city\":\"New York\",\"name\":\"John\",\"country\":\"USA\"}"; + var apiResult1 = CreateApiResultForJsonResponse(json1); + var apiResult2 = CreateApiResultForJsonResponse(json2); + + _ = _stringComparisonPlugin.ProcessComparison(apiResult1, apiResult2, ComparisonStatus.NewFile); + var loggerMessages = _logger.Messages.Select(x => x.Item2); + _ = loggerMessages.Should().HaveCountGreaterThan(1); + _ = loggerMessages.Should().ContainMatch("Source is different in length: 42 < 58"); + } + + [TestMethod] + public void DifferentLength_KeyMissingIsReported_FromTarget() + { + var json1 = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}"; + var json2 = "{\"age\":30,\"city\":\"New York\",\"name\":\"John\",\"country\":\"USA\"}"; + + var apiResult1 = CreateApiResultForJsonResponse(json1); + var apiResult2 = CreateApiResultForJsonResponse(json2); + + var loggerMessages = _logger.Messages.Select(x => x.Item2); + _ = _stringComparisonPlugin.ProcessComparison(apiResult1, apiResult2, ComparisonStatus.NewFile); + _ = _logger.Messages.Should().HaveCountGreaterThan(1); + _ = loggerMessages.Should().ContainMatch("Difference at path '.country' for property 'country'"); + _ = loggerMessages.Should().ContainMatch("Missing path in source at path: '.country' for property 'country'"); + } + + [TestMethod] + public void DifferentLength_KeyMissingIsReported_FromSource() + { + var json1 = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\", \"country\":\"USA\"}"; + var json2 = "{\"age\":30,\"city\":\"New York\",\"name\":\"John\"}"; + + var apiResult1 = CreateApiResultForJsonResponse(json1); + var apiResult2 = CreateApiResultForJsonResponse(json2); + + _ = _stringComparisonPlugin.ProcessComparison(apiResult1, apiResult2, ComparisonStatus.NewFile); + + var loggerMessages = _logger.Messages.Select(x => x.Item2); + + _ = loggerMessages.Should().HaveCountGreaterThan(1); + _ = loggerMessages.Should().ContainMatch("Difference at path '.country' for property 'country'"); + _ = loggerMessages.Should().ContainMatch("Missing path in source at path: '.country' for property 'country'"); + } + + [TestMethod] + public void Different_DifferentValueIsReported() + { + var json1 = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}"; + var json2 = "{\"name\":\"John\",\"age\":30,\"city\":\"New Yorks\"}"; + + var apiResult1 = CreateApiResultForJsonResponse(json1); + var apiResult2 = CreateApiResultForJsonResponse(json2); + + _ = _stringComparisonPlugin.ProcessComparison(apiResult1, apiResult2, ComparisonStatus.NewFile); + var loggerMessages = _logger.Messages.Select(x => x.Item2); + _ = loggerMessages.Should().HaveCount(4); + _ = loggerMessages.Should().ContainMatch("Difference at path '' for property 'Root'"); + _ = loggerMessages.Should().ContainMatch("Difference at path '.city' for property 'city'"); + _ = loggerMessages.Should().ContainMatch("DiffValue is: New York <> New Yorks"); + } + [TestMethod] + public void StringPlugin_ArrayValues() + { + var json1 = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\",\"hobbies\":[\"reading\",\"traveling\"]}"; + var json2 = "{\"age\":30,\"city\":\"New York\",\"name\":\"John\",\"hobbies\":[\"reading\",\"cooking\"]}"; + + var apiResult1 = CreateApiResultForJsonResponse(json1); + var apiResult2 = CreateApiResultForJsonResponse(json2); + + _ = _stringComparisonPlugin.ProcessComparison(apiResult1, apiResult2, ComparisonStatus.NewFile); + var loggerMessages = _logger.Messages.Select(x => x.Item2); + _ = loggerMessages.Should().HaveCount(5); + _ = loggerMessages.Should().ContainMatch("Difference at path '.hobbies' for property 'hobbies'"); + _ = loggerMessages.Should().ContainMatch("Difference at path '.hobbies[1]' for property 'hobbies'"); + _ = loggerMessages.Should().ContainMatch("DiffValue is: traveling <> cooking"); + } + + [TestMethod] + public void Different_DeepArrayNestingComparison() + { + var json1 = "{\"name\":\"John\",\"age\":30,\"city\":\"New York\",\"hobbies\":[{\"type\":\"reading\",\"locations\":[{\"name\":\"Library\",\"hours\":9},{\"name\":\"Park\",\"hours\":5}]}]}"; + var json2 = "{\"age\":30,\"city\":\"New York\",\"name\":\"John\",\"hobbies\":[{\"type\":\"reading\",\"locations\":[{\"name\":\"Library\",\"hours\":9},{\"name\":\"Beach\",\"hours\":8}]}]}"; + + var apiResult1 = CreateApiResultForJsonResponse(json1); + var apiResult2 = CreateApiResultForJsonResponse(json2); + + _ = _stringComparisonPlugin.ProcessComparison(apiResult1, apiResult2, ComparisonStatus.NewFile); + _ = _logger.Messages.Should().HaveCount(9); + var loggerMessages = _logger.Messages.Select(x => x.Item2); + _ = loggerMessages.Should().ContainMatch("Difference at path '.hobbies' for property 'hobbies'"); + _ = loggerMessages.Should().ContainMatch("Difference at path '.hobbies[0]' for property 'hobbies'"); + _ = loggerMessages.Should().ContainMatch("Difference at path '.hobbies[0].locations' for property 'locations'"); + _ = loggerMessages.Should().ContainMatch("Difference at path '.hobbies[0].locations[1]' for property 'locations'"); + _ = loggerMessages.Should().ContainMatch("Difference at path '.hobbies[0].locations[1].name' for property 'name'"); + _ = loggerMessages.Should().ContainMatch("DiffValue is: Park <> Beach"); + _ = loggerMessages.Should().ContainMatch("Difference at path '.hobbies[0].locations[1].hours' for property 'hours'"); + } + + + private static ApiCallResult CreateApiResultForJsonResponse(string json1) + { + return new ApiCallResult + { + StatusCode = System.Net.HttpStatusCode.Continue, + ResponseContent = json1, + Headers = new List>(), + Url = string.Empty, + DataQueryResult = new Database.DataQueryResult(), + IsSuccessStatusCode = true, + CompareResults = new List() + }; + } + } +} diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/CustomResponse.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/CustomResponse.cs new file mode 100644 index 0000000..80bbf7a --- /dev/null +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/CustomResponse.cs @@ -0,0 +1,45 @@ +using System.Text; +using WireMock; +using WireMock.ResponseBuilders; +using WireMock.ResponseProviders; +using WireMock.Settings; +using WireMock.Types; +using WireMock.Util; + +namespace APITestingRunner.Unit.Tests.TestRunnerTests +{ + public class CustomResponse : IResponseProvider + { + private static int _count = 0; + public async Task<(IResponseMessage Message, IMapping? Mapping)> ProvideResponseAsync(IMapping mapping, IRequestMessage requestMessage, WireMockServerSettings settings) + { + ResponseMessage response; + if (_count % 2 == 0) + { + response = new ResponseMessage() { StatusCode = 200 }; + SetBody(response, @"{ ""msg"": ""Hello from wiremock!"" }"); + } + else + { + response = new ResponseMessage() { StatusCode = 500 }; + SetBody(response, @"{ ""msg"": ""Hello some error from wiremock!"" }"); + } + + _count++; + (ResponseMessage, IMapping) tuple = (response, null); + return await Task.FromResult(tuple); + } + + + private void SetBody(ResponseMessage response, string body) + { + response.BodyDestination = BodyDestinationFormat.SameAsSource; + response.BodyData = new BodyData + { + Encoding = Encoding.UTF8, + DetectedBodyType = BodyType.String, + BodyAsString = body + }; + } + } +} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/DataComparisonTests.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/DataComparisonTests.cs deleted file mode 100644 index 4db77a7..0000000 --- a/APITestingRunner.Unit.Tests/TestRunnerTests/DataComparisonTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using APITestingRunner.ApiRequest; -using FluentAssertions; - -namespace APITestingRunner.Unit.Tests -{ - - [TestClass] - public class DataComparisonTests - { - - //TODO: Review together tests for any that shoudl be type-safe - [TestMethod] - public void CompareAPiResults_ShouldReturnMatching() - { - var apiResult = new ApiCallResult(System.Net.HttpStatusCode.OK, string.Empty, null, null, null, true, null) { ResponseContent = string.Empty }; - var fileResult = new ApiCallResult(System.Net.HttpStatusCode.OK, string.Empty, null, null, null, true, null) { ResponseContent = string.Empty }; - var expectedResult = ComparissonStatus.Matching; - - var result = DataComparison.CompareAPiResults(apiResult, fileResult); - _ = result.Should().Be(expectedResult); - } - - [TestMethod] - public void CompareAPiResults_ShouldReturnDifferent() - { - var apiResult = new ApiCallResult(System.Net.HttpStatusCode.OK, string.Empty, null, null, null, true, null) { ResponseContent = string.Empty }; - var fileResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "test", null, null, null, true, null) { ResponseContent = "test" }; - var expectedResult = ComparissonStatus.Different; - - - var result = DataComparison.CompareAPiResults(apiResult, fileResult); - _ = result.Should().Be(expectedResult); - } - - [TestMethod] - public void CompareAPiResults_ShouldReturnDifferent_StatusCodeIsDifferent() - { - var apiResult = new ApiCallResult(System.Net.HttpStatusCode.OK, string.Empty, null, null, null, true, null) { ResponseContent = string.Empty }; - var fileResult = new ApiCallResult(System.Net.HttpStatusCode.Accepted, "test", null, null, null, true, null) { ResponseContent = "test" }; - var expectedResult = ComparissonStatus.Different; - - var result = DataComparison.CompareAPiResults(apiResult, fileResult); - _ = result.Should().Be(expectedResult); - } - - [TestMethod] - public void CompareAPiResults_ShouldReturnDifferent_IsSuccessCodeIsDifferent() - { - var apiResult = new ApiCallResult(System.Net.HttpStatusCode.OK, string.Empty, null, null, null, true, null) { ResponseContent = string.Empty }; - var fileResult = new ApiCallResult(System.Net.HttpStatusCode.OK, "test", null, null, null, false, null) { ResponseContent = "test" }; - var expectedResult = ComparissonStatus.Different; - - var result = DataComparison.CompareAPiResults(apiResult, fileResult); - _ = result.Should().Be(expectedResult); - } - } -} \ No newline at end of file diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/PopulateRequestBodyTests.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/PopulateRequestBodyTests.cs index fff6776..1d0396c 100644 --- a/APITestingRunner.Unit.Tests/TestRunnerTests/PopulateRequestBodyTests.cs +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/PopulateRequestBodyTests.cs @@ -47,7 +47,7 @@ public void PopulateRequestBody_ShouldThrowExcpetion_becauseOfNoDataQueryResult( Action action = () => TestRunner.PopulateRequestBody(config, null); - _ = action.Should().Throw().WithMessage("Value cannot be null. (Parameter 'dataQueryResult')"); + _ = action.Should().Throw().WithMessage("Value cannot be null. (Parameter 'DataQueryResult')"); } [TestMethod] diff --git a/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsWithConfigAPIBasedOnDatabaseCallsTests.cs b/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsWithConfigAPIBasedOnDatabaseCallsTests.cs index dc5c198..57b62c5 100644 --- a/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsWithConfigAPIBasedOnDatabaseCallsTests.cs +++ b/APITestingRunner.Unit.Tests/TestRunnerTests/TestRunnerWithOptionsWithConfigAPIBasedOnDatabaseCallsTests.cs @@ -61,7 +61,8 @@ public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICa .UsingGet() ) .RespondWith( - Response.Create() + Response + .Create() .WithStatusCode(200) .WithHeader("Content-Type", "text/plain") .WithBody("Hello, world!") @@ -187,7 +188,6 @@ public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICa [TestCategory("ResultCompare")] public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICall_WithResult_200_ShouldStoreAllRequest_withFileNamingBasedOnDbResult() { - server.Given( WireMock.RequestBuilders.Request.Create() .WithPath("/WeatherForecast") @@ -199,7 +199,7 @@ public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICa Response.Create() .WithStatusCode(200) .WithHeader("Content-Type", "application/json") - .WithBody("Hello, world!") + .WithBody("{\"name\":\"John\",\"age\":30,\"city\":\"New York\",\"hobbies\":[{\"type\":\"reading\",\"locations\":[{\"name\":\"Library\",\"hours\":9},{\"name\":\"Park\",\"hours\":5}]}]}") ); Config apiTesterConfig = new() @@ -265,15 +265,16 @@ public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldMakeAnAPICa .RunTests(apiTesterConfig); _ = testRunner.Errors.Should().BeEmpty(); - _ = logger.Messages.Count.Should().Be(7); + var loggerMessages = logger.Messages.Select(x => x.Item2); + _ = loggerMessages.Should().HaveCount(17); - _ = 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"); - _ = logger.Messages[6].Item2.Should().Contain("Total process took:"); + _ = loggerMessages.Should().ContainEquivalentOf("Validating database based data source start"); + _ = loggerMessages.Should().ContainEquivalentOf("Found database connection string"); + _ = loggerMessages.Should().ContainEquivalentOf("Found database query and db fields. Attempting to load data from database."); + _ = loggerMessages.Should().ContainEquivalentOf("GET /WeatherForecast?urlKey=configKey&id=1 200 success Results/request-music-1.json Matching"); + + _ = loggerMessages.Should().ContainEquivalentOf("GET /WeatherForecast?urlKey=configKey&id=2 200 success Results/request-software-2.json NewFile"); + _ = loggerMessages.Should().ContainEquivalentOf("GET /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"); @@ -335,137 +336,76 @@ public async Task ValidateImplementationFor_SingleAPICallAsync_ShouldAppendIdToU _ = logger.Messages[4].Item2.Should().Contain("Total process took:"); } - //[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); - //} + [TestMethod] + [TestCategory("SimpleAPICallBasedOnDbSource")] + [TestCategory("dbcapture")] + public async Task ValidateImplementationFor_WireMockTest() + { + //note https://github.com/WireMock-Net/WireMock.Net/wiki/Scenarios-and-States + server.Given( + WireMock.RequestBuilders.Request.Create() + .WithPath("/WeatherForecast/1") + + .UsingGet() + ) + .RespondWith(new CustomResponse()); + + Config apiTesterConfig = new() + { + UrlBase = "http://localhost:7055", + CompareUrlBase = string.Empty, + CompareUrlPath = string.Empty, + UrlPath = "/WeatherForecast/{bindingId}", + RequestBody = null, + HeaderParam = [ + new Param("accept","application/json") + ], + UrlParam = null!, + DBConnectionString = _dbConnectionStringForTests, + DBQuery = "select top 1 id as bindingId from dbo.sampleTable;", + DBFields = [ + new Param("bindingId", "bindingId"), + ], + RequestType = RequestType.GET, + ResultsStoreOption = StoreResultsOption.All, + ConfigMode = TesterConfigMode.CaptureAndCompare, + OutputLocation = DirectoryServices.AssemblyDirectory, + }; + + TestLogger logger = new(); + + 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/1 200 success"); + _ = logger.Messages[4].Item2.Should().Contain("Total process took:"); + + logger = new(); + testRunner = await new ApiTesterRunner(logger) + .RunTests(apiTesterConfig); + + _ = testRunner.Errors.Should().BeEmpty(); + _ = logger.Messages.Count.Should().Be(13); + var messages = logger.Messages.Select(x => x.Item2).ToList(); + + _ = messages.Should().ContainEquivalentOf("Validating database based data source start"); + _ = messages.Should().ContainEquivalentOf("Found database connection string"); + _ = messages.Should().ContainEquivalentOf("Found database query and db fields. Attempting to load data from database."); + _ = messages.Should().ContainEquivalentOf("Processing comparison using:ContentReplacements"); + _ = messages.Should().ContainEquivalentOf("Processing comparison using:ContentReplacements with result Matching"); + _ = messages.Should().ContainEquivalentOf("Processing comparison using:StringPlugin"); + _ = messages.Should().ContainEquivalentOf("Source is different in length: 33 < 44"); + _ = messages.Should().ContainEquivalentOf("Difference at path '' for property 'Root'"); + _ = messages.Should().ContainEquivalentOf("Difference at path '.msg' for property 'msg'"); + _ = messages.Should().ContainEquivalentOf("DiffValue is: Hello from wiremock! <> Hello some error from wiremock!"); + _ = messages.Should().ContainEquivalentOf("Processing comparison using:StringPlugin with result Different"); + _ = messages.Should().ContainEquivalentOf("GET /WeatherForecast/1 500 fail Results/request-1.json Different"); + } } } \ No newline at end of file diff --git a/APITestingRunner.sln b/APITestingRunner.sln index 44a6307..12a00ef 100644 --- a/APITestingRunner.sln +++ b/APITestingRunner.sln @@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitignore = .gitignore + GitVersion.yml = GitVersion.yml readme.md = readme.md EndProjectSection EndProject diff --git a/APITestingRunner/APITestingRunner.csproj b/APITestingRunner/APITestingRunner.csproj index 9f2ca3a..0c44f56 100644 --- a/APITestingRunner/APITestingRunner.csproj +++ b/APITestingRunner/APITestingRunner.csproj @@ -1,4 +1,4 @@ - + Exe @@ -18,10 +18,12 @@ + + diff --git a/APITestingRunner/ApiRequest/ApiCallResult.cs b/APITestingRunner/ApiRequest/ApiCallResult.cs index f693518..02837e8 100644 --- a/APITestingRunner/ApiRequest/ApiCallResult.cs +++ b/APITestingRunner/ApiRequest/ApiCallResult.cs @@ -4,25 +4,26 @@ 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) { - public required string ResponseContent { get; set; } +{ + + /// + /// Container for an api call result. + /// + /// + /// + /// + /// + /// + /// + /// + public record ApiCallResult + { + public HttpStatusCode StatusCode { get; set; } + public string ResponseContent { get; set; } + public List>? Headers { get; set; } + public string? Url { get; set; } + public DataQueryResult? DataQueryResult { get; set; } + public bool IsSuccessStatusCode { get; set; } + public List? CompareResults { get; set; } } } \ No newline at end of file diff --git a/APITestingRunner/ApiTesterRunner.cs b/APITestingRunner/ApiTesterRunner.cs index b8499fa..a829d4a 100644 --- a/APITestingRunner/ApiTesterRunner.cs +++ b/APITestingRunner/ApiTesterRunner.cs @@ -19,7 +19,9 @@ public class ApiTesterRunner(ILogger logger) private readonly IList PluginList = new List(){ - new ContentReplacements() + new ContentReplacements(), + new StringComparisonPlugin(), + //new APICallResultComparison(), }; @@ -41,7 +43,7 @@ public async Task RunTests(string pathConfigJson) _ = _logger ?? throw new ArgumentNullException(nameof(_logger)); var configSettings = await ConfigurationManager.GetConfigAsync(pathConfigJson); - var testRunner = await this.RunTestWithPlugins(configSettings); + var testRunner = await RunTestWithPlugins(configSettings); _ = await testRunner.PrintResultsSummary(); return; @@ -64,7 +66,7 @@ private async Task RunTestWithPlugins(Config config) stopwatch.Start(); testRunner.ApplyConfig(config); - testRunner.RegisterPlugin(this.PluginList); + testRunner.RegisterPlugin(PluginList); _ = await testRunner.RunTestsAsync(); stopwatch.Stop(); _logger.LogInformation($"Total process took: {stopwatch.Elapsed:mm\\:ss\\.ff}"); diff --git a/APITestingRunner/DataComparison.cs b/APITestingRunner/DataComparison.cs deleted file mode 100644 index 36f18b7..0000000 --- a/APITestingRunner/DataComparison.cs +++ /dev/null @@ -1,23 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -using APITestingRunner.ApiRequest; - -namespace APITestingRunner -{ - public class DataComparison - { - public static ComparissonStatus CompareAPiResults(ApiCallResult apiCallResult, ApiCallResult fileSourceResult) - { - var status = ComparissonStatus.Different; - - if ((apiCallResult.StatusCode == fileSourceResult.StatusCode) - && (apiCallResult.IsSuccessStatusCode == fileSourceResult.IsSuccessStatusCode) - && (apiCallResult.ResponseContent == fileSourceResult.ResponseContent)) - { - status = ComparissonStatus.Matching; - } - - return status; - } - } -} \ No newline at end of file diff --git a/APITestingRunner/Plugins/ContentReplacements.cs b/APITestingRunner/Plugins/ContentReplacements.cs index 7f62721..199a3a5 100644 --- a/APITestingRunner/Plugins/ContentReplacements.cs +++ b/APITestingRunner/Plugins/ContentReplacements.cs @@ -1,4 +1,5 @@ -using APITestingRunner.Configuration; +using APITestingRunner.ApiRequest; +using APITestingRunner.Configuration; using Microsoft.Extensions.Logging; namespace APITestingRunner.Plugins @@ -16,7 +17,6 @@ public class ContentReplacements() : IPlugin string IPlugin.Name => "ContentReplacements"; - //TODO: Review this pattern - need reason not to put this into the constructor - which improves the code tightness public void ApplyConfig(ref IConfig config, ILogger logger) { @@ -48,13 +48,16 @@ private string ProcessValueWithFilters(bool filterConfigurationByStoreInFile, st private List ApplySavedFilter(bool filterConfigurationByStoreInFile) { - if (_config.ContentReplacements == null) - return []; - - if (!filterConfigurationByStoreInFile) - return _config.ContentReplacements.ToList(); + return _config.ContentReplacements == null + ? (List)([]) + : !filterConfigurationByStoreInFile + ? _config.ContentReplacements.ToList() + : _config.ContentReplacements.Where(x => x.StoreInFile).ToList(); + } - return _config.ContentReplacements.Where(x => x.StoreInFile).ToList(); + public ComparisonStatus ProcessComparison(ApiCallResult apiCallResult, ApiCallResult fileSourceResult, ComparisonStatus comparisonStatus) + { + return comparisonStatus; } } } diff --git a/APITestingRunner/Plugins/IPlugin.cs b/APITestingRunner/Plugins/IPlugin.cs index fe4f98d..1b33eb4 100644 --- a/APITestingRunner/Plugins/IPlugin.cs +++ b/APITestingRunner/Plugins/IPlugin.cs @@ -1,4 +1,5 @@ -using APITestingRunner.Configuration; +using APITestingRunner.ApiRequest; +using APITestingRunner.Configuration; using Microsoft.Extensions.Logging; namespace APITestingRunner.Plugins @@ -9,7 +10,12 @@ public interface IPlugin /// Name of the plugin /// string Name { get; } + + /// + /// Description of the plugin. + /// string Description { get; } + void ApplyConfig(ref IConfig config, ILogger logger); /// @@ -19,6 +25,15 @@ public interface IPlugin /// Returns a processed string. string ProcessBeforeSave(string apiResponseString); + /// + /// Compare api call results implementation + /// + /// + /// + /// Current status of comparing files. + /// ComparisonStatus. + ComparisonStatus ProcessComparison(ApiCallResult apiCallResult, ApiCallResult fileSourceResult, ComparisonStatus comparisonStatus); + /// /// Processes validation string. /// diff --git a/APITestingRunner/Plugins/StringComparisonPlugin.cs b/APITestingRunner/Plugins/StringComparisonPlugin.cs new file mode 100644 index 0000000..3abf3dc --- /dev/null +++ b/APITestingRunner/Plugins/StringComparisonPlugin.cs @@ -0,0 +1,150 @@ +using APITestingRunner.ApiRequest; +using APITestingRunner.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace APITestingRunner.Plugins +{ + public class StringComparisonPlugin : IPlugin + { + public string Name => "StringPlugin"; + + public string Description => "Compare string into greater detail"; + + private ILogger? _logger; + private IConfig? _config; + private ComparisonStatus _comparisonStatus; + + public void ApplyConfig(ref IConfig config, ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _config = config ?? throw new ArgumentNullException(nameof(config)); + } + + public string ProcessBeforeSave(string apiResponseString) + { + return apiResponseString; + } + + public string ProcessValidation(string value) + { + return value; + } + + public ComparisonStatus ProcessComparison(ApiCallResult apiCallResult, ApiCallResult fileSourceResult, ComparisonStatus comparisonStatus) + { + _comparisonStatus = comparisonStatus; + if ((apiCallResult.StatusCode == fileSourceResult.StatusCode) + && (apiCallResult.IsSuccessStatusCode == fileSourceResult.IsSuccessStatusCode) + && (apiCallResult.ResponseContent == fileSourceResult.ResponseContent)) + { + _comparisonStatus = ComparisonStatus.Matching; + } + + if (apiCallResult.ResponseContent == null || fileSourceResult.ResponseContent == null) return _comparisonStatus; + GetJsonDifferences(apiCallResult.ResponseContent, fileSourceResult.ResponseContent); + return _comparisonStatus; + } + private void GetJsonDifferences(string json1, string json2) + { + if (json1 == null && json2 == null) + { + return; + } + else if (json1 == null && json2 != null) + { + return; + } + else if (json1 != null && json2 == null) + { + return; + } + + if (json1.Length > json2.Length) + { + _comparisonStatus = ComparisonStatus.Different; + _logger.LogInformation($"Source is different in length: {json1.Length} > {json2.Length}"); + } + else if (json1.Length < json2.Length) + { + _comparisonStatus = ComparisonStatus.Different; + _logger.LogInformation($"Source is different in length: {json1.Length} < {json2.Length}"); + } + else + { + _logger.LogInformation("Source and target has same length."); + } + + var token1 = JToken.Parse(json1); + var token2 = JToken.Parse(json2); + FindDifferences(token1, token2, string.Empty, "Root"); + } + + private void FindDifferences(JToken token1, JToken token2, string path, string propertyName) + { + if (!JToken.DeepEquals(token1, token2)) + { + _logger.LogInformation($"Difference at path '{path}' for property '{propertyName}'"); + + var diffValue = string.Empty; + var diffValue2 = string.Empty; + + if (token1 != null && token2 != null) + { + if (token1.Type == JTokenType.String || token2.Type == JTokenType.String) + { + if (token1.Type == JTokenType.String) + { + diffValue = ((JValue)token1).Value?.ToString(); + } + + if (token2.Type == JTokenType.String) + { + diffValue2 = ((JValue)token2).Value?.ToString(); + } + + _comparisonStatus = ComparisonStatus.Different; + _logger.LogInformation($"DiffValue is: {diffValue} <> {diffValue2}"); + } + } + } + + if (token1 == null || token2 == null) + { + _logger.LogInformation($"Missing path in source at path: '{path}' for property '{propertyName}'"); + } + else if (token1.Type == JTokenType.Object) + { + var props1 = (JObject)token1; + var props2 = (JObject)token2; + + var allPropertyNames = props1.Properties().Select(p => p.Name).Union(props2.Properties().Select(p => p.Name)); + + foreach (var propName in allPropertyNames) + { + FindDifferences( + props1.GetValue(propName, StringComparison.OrdinalIgnoreCase), + props2.GetValue(propName, StringComparison.OrdinalIgnoreCase), + $"{path}.{propName}", + propName + ); + } + } + else if (token1.Type == JTokenType.Array) + { + var array1 = (JArray)token1; + var array2 = (JArray)token2; + + var maxLength = Math.Max(array1.Count, array2.Count); + + for (var i = 0; i < maxLength; i++) + { + var element1 = i < array1.Count ? array1[i] : null; + var element2 = i < array2.Count ? array2[i] : null; + + FindDifferences(element1, element2, $"{path}[{i}]", propertyName); + } + } + } + } +} \ No newline at end of file diff --git a/APITestingRunner/ProcessingFileResult.cs b/APITestingRunner/ProcessingFileResult.cs index db3ecd5..33f5e52 100644 --- a/APITestingRunner/ProcessingFileResult.cs +++ b/APITestingRunner/ProcessingFileResult.cs @@ -4,14 +4,15 @@ namespace APITestingRunner { public class ProcessingFileResult { - public ComparissonStatus ComparissonStatus { get; set; } + public ComparisonStatus ComparissonStatus { get; set; } public bool DisplayFilePathInLog { get; internal set; } } - public enum ComparissonStatus + public enum ComparisonStatus { NewFile = 1, Matching = 2, Different = 3, + NotApplicable = 4, } } \ No newline at end of file diff --git a/APITestingRunner/TestRunner.cs b/APITestingRunner/TestRunner.cs index 27cd175..0176daf 100644 --- a/APITestingRunner/TestRunner.cs +++ b/APITestingRunner/TestRunner.cs @@ -286,7 +286,7 @@ private async Task MakeApiForCollectionCall(HttpClient client, DataQueryResult i fileName = TestRunner.GenerateResultName(item, _config.ResultFileNamePattern); result = await ProcessResultCaptureAndCompareIfRequested( - new ApiCallResult(response.StatusCode, content, responseHeaders, pathAndQuery, item, response.IsSuccessStatusCode) { ResponseContent = content }); + new ApiCallResult() { StatusCode = response.StatusCode, Headers = responseHeaders, Url = pathAndQuery, DataQueryResult = item, IsSuccessStatusCode = response.IsSuccessStatusCode, ResponseContent = content }); if (result.DisplayFilePathInLog) { onScreenMessage += $" {TestConstants.TestOutputDirectory}/{fileName}"; @@ -297,7 +297,7 @@ private async Task MakeApiForCollectionCall(HttpClient client, DataQueryResult i fileName = TestRunner.GenerateResultName(item, _config.ResultFileNamePattern); - result = await ProcessResultCaptureAndCompareIfRequested(new ApiCallResult(response.StatusCode, content, responseHeaders, pathAndQuery, item, response.IsSuccessStatusCode) { ResponseContent = content }); + result = await ProcessResultCaptureAndCompareIfRequested(new ApiCallResult { StatusCode = response.StatusCode, Headers = responseHeaders, Url = pathAndQuery, DataQueryResult = item, IsSuccessStatusCode = response.IsSuccessStatusCode, ResponseContent = content }); if (result.DisplayFilePathInLog) { @@ -428,7 +428,7 @@ private async Task ProcessResultCaptureAndCompareIfRequest //TODO: Review what this is for? _ = $"{apiCallResult.StatusCode} - {apiCallResult.ResponseContent}"; - var fileCompareStatus = ComparissonStatus.NewFile; + var fileCompareStatus = ComparisonStatus.NewFile; var result = new ProcessingFileResult { ComparissonStatus = fileCompareStatus }; if (_config.ConfigMode is TesterConfigMode.Capture or TesterConfigMode.CaptureAndCompare) @@ -470,7 +470,7 @@ private async Task ProcessResultCaptureAndCompareIfRequest /// /// /// Boolean result checking if file already exists. - private async Task LogIntoFileAsync(string logLocation, ApiCallResult apiCallResult) + private async Task LogIntoFileAsync(string logLocation, ApiCallResult apiCallResult) { ArgumentNullException.ThrowIfNull(_config); @@ -482,8 +482,8 @@ private async Task LogIntoFileAsync(string logLocation, ApiCa _ = Directory.CreateDirectory(resultsDirectory); } - var status = ComparissonStatus.NewFile; - var fileName = GenerateResultName(apiCallResult.Item, _config.ResultFileNamePattern); + var status = ComparisonStatus.NewFile; + var fileName = GenerateResultName(apiCallResult.DataQueryResult, _config.ResultFileNamePattern); var fileOperations = new FileOperations(); var filePath = Path.Combine(resultsDirectory, fileName); @@ -495,38 +495,41 @@ private async Task LogIntoFileAsync(string logLocation, ApiCa apiCallResult.ResponseContent = plugin.ProcessBeforeSave(apiCallResult.ResponseContent); } } - - var apiResult = JsonSerializer.Serialize(apiCallResult); - + + var apiResult = JsonSerializer.Serialize(apiCallResult); + if (FileOperations.ValidateIfFileExists(filePath)) { var compareFileData = FileOperations.GetFileData(filePath); - if (plugins != null) - { - foreach (var plugin in plugins) - { - compareFileData = plugin.ProcessValidation(compareFileData); - } - } - var fileSourceResult = JsonSerializer.Deserialize(compareFileData); if (fileSourceResult is not null) - { - status = DataComparison.CompareAPiResults(apiCallResult, fileSourceResult); + { + // because the file is loaded lets make it same. then plugin can decided to make it different + status = ComparisonStatus.Matching; + + if (plugins != null) + { + foreach (var plugin in plugins) + { + _logger.LogInformation($"Processing comparison using:{plugin.Name}"); + status = plugin.ProcessComparison(fileSourceResult, apiCallResult, status); + _logger.LogInformation($"Processing comparison using:{plugin.Name} with result {status}"); + } + } } } else { await FileOperations.WriteFile(filePath, apiResult); - } + } + return status; } catch (Exception ex) { Debug.WriteLine(ex); - Errors.Add("Failed to capture logs into a file"); throw; } diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..b2e5e83 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1 @@ +next-version: 1.0.0 \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..827f823 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,97 @@ +## depends on: https://marketplace.visualstudio.com/items?itemName=gittools.gittools + +name: $(date:yyyyMMdd)$(rev:.r)-$(Build.SourceBranchName)-$(GitVersion.SemVer) + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + contentVersion: $(GitVersion.AssemblySemVer) + parameters.semVer.value: $(GitVersion.AssemblySemVer) + +steps: + # Needed for GitVersion (fetchDepth field is critical) + - checkout: self + displayName: Git Checkout + fetchDepth: 0 + persistCredentials: true + + - task: gitversion/setup@0 + displayName: GitVersion Setup + inputs: + versionSpec: 5.x + updateAssemblyInfo: true + - task: gitversion/execute@0 + displayName: GitVersion Execute + + - task: DotNetCoreCLI@2 + displayName: Restore packages to all projects + inputs: + command: 'restore' + projects: '**/*.csproj' + feedsToUse: 'select' + noCache: true + + - script: echo current version is $(GitVersion.SemVer) + displayName: 'Display calculated version $(GitVersion.SemVer)' + + # - task: DotNetCoreCLI@2 + # inputs: + # command: 'build' + # publishWebProjects: false + # projects: '**/*APITestingRunner.csproj' + # arguments: '-c "Release" /p:OutputPath="$(build.artifactStagingDirectory)"' + + - task: DotNetCoreCLI@2 + inputs: + command: 'build' + publishWebProjects: false + projects: '**/*APITestingRunner.csproj' + arguments: '--output "$(build.artifactStagingDirectory)" --runtime win-x64 --configuration Release -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true' + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'drop' + publishLocation: 'Container' + + + + # - task: VSBuild@1 + # inputs: + # solution: '$(solution)' + # msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"' + # platform: '$(buildPlatform)' + # configuration: '$(buildConfiguration)' + +# - task: VSTest@2 +# inputs: +# platform: '$(buildPlatform)' +# configuration: '$(buildConfiguration)' + + + - task: PowerShell@2 + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) + displayName: 'Get latest SHA commmit from repo' + inputs: + targetType: 'inline' + script: | + $commits = Invoke-RestMethod -Method GET -Uri "https://api.github.com/repos/cpoDesign/APITestingRunner/commits" + $sha = $commits[0].sha + Write-Host "##vso[task.setvariable variable=sha;]$sha" + + - task: GitHubRelease@1 + condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) + inputs: + gitHubConnection: 'github.com_cpoDesign' + repositoryName: 'cpoDesign/APITestingRunner' + action: 'create' + target: '$(sha)' + tagSource: 'userSpecifiedTag' + tag: '$(GitVersion.SemVer)' + title: 'v$(GitVersion.SemVer)' + assets: '$(Build.ArtifactStagingDirectory)' + addChangeLog: true \ No newline at end of file