diff --git a/docs/changelogs/da0116b2f3cf4c028bfec714089c4cd7.cle b/docs/changelogs/da0116b2f3cf4c028bfec714089c4cd7.cle new file mode 100644 index 0000000..393bc2e --- /dev/null +++ b/docs/changelogs/da0116b2f3cf4c028bfec714089c4cd7.cle @@ -0,0 +1,9 @@ +{ + "Id": "da0116b2-f3cf-4c02-8bfe-c714089c4cd7", + "IssueId": "", + "Prefix": "Changed", + "Tag": "", + "Message": "Refactord domain model and added \u0016TestCase reference to TestStep", + "CreatedAt": "2026-05-05T19:24:26.4154813+00:00", + "CreatedBy": "thomasduft" +} \ No newline at end of file diff --git a/src/tapir.Cli/Commands/RunCommand.cs b/src/tapir.Cli/Commands/RunCommand.cs index e21c0f3..14bab02 100644 --- a/src/tapir.Cli/Commands/RunCommand.cs +++ b/src/tapir.Cli/Commands/RunCommand.cs @@ -143,7 +143,7 @@ CancellationToken cancellationToken foreach (var table in testCase.Tables) { var instructions = table.Steps - .Select(step => TestStepInstruction.FromTestStep(step, testCase.Variables, testCase.File)) + .Select(TestStepInstruction.FromTestStep) .ToList(); var executionResult = await _testCaseExecutor.ExecuteAsync( diff --git a/src/tapir.Cli/Domain/Core/TestCase.cs b/src/tapir.Cli/Domain/Core/TestCase.cs index 7539140..079e9e7 100644 --- a/src/tapir.Cli/Domain/Core/TestCase.cs +++ b/src/tapir.Cli/Domain/Core/TestCase.cs @@ -43,7 +43,6 @@ CancellationToken cancellationToken ) { var parser = new TestCaseParser(file); - return await parser.ToTestCaseAsync(cancellationToken); } diff --git a/src/tapir.Cli/Domain/Core/TestStep.cs b/src/tapir.Cli/Domain/Core/TestStep.cs index 7f0ac2f..a8f71fd 100644 --- a/src/tapir.Cli/Domain/Core/TestStep.cs +++ b/src/tapir.Cli/Domain/Core/TestStep.cs @@ -8,8 +8,5 @@ internal class TestStep public string ExpectedResult { get; set; } = string.Empty; public string ActualResult { get; set; } = string.Empty; public bool IsSuccess { get; set; } = false; - - // TODO: TestSteps should contain a reference to the TestCase they belong to - // (i.e. `public TestCase TestCase { get; set; }`) so we can easily access the TestCase - // variables and domain when executing the TestStep. + public TestCase TestCase { get; set; } = new(); } diff --git a/src/tapir.Cli/Domain/Core/TestStepInstruction.cs b/src/tapir.Cli/Domain/Core/TestStepInstruction.cs index 9d18019..547c223 100644 --- a/src/tapir.Cli/Domain/Core/TestStepInstruction.cs +++ b/src/tapir.Cli/Domain/Core/TestStepInstruction.cs @@ -13,7 +13,6 @@ internal class TestStepInstruction public string JsonPath { get; set; } = string.Empty; public string Method { get; set; } = "GET"; public string Endpoint { get; set; } = string.Empty; - public string TestCaseFile { get; set; } = string.Empty; /// /// The domain to send the request to. Will override the global domain and be prepended to the endpoint. @@ -28,22 +27,17 @@ public TestStepInstruction(TestStep step) _step = step; } - // TODO: needs to be refactored so the test case file is not passed here, but rather access via the `TestStep.TestCase.File`-property public static TestStepInstruction FromTestStep( - TestStep step, - Dictionary variables, - string testCaseFile = "" + TestStep step ) { var testData = ParseTestData(step.TestData); if (testData.Count == 0) throw new InvalidDataException($"No TestData found for Test Step {step.Id}"); - TestStepInstruction instruction = new(step) - { - TestCaseFile = testCaseFile - }; + TestStepInstruction instruction = new(step); + var variables = step.TestCase.Variables; foreach (var parameter in testData) { switch (parameter.Key) @@ -83,6 +77,16 @@ public static TestStepInstruction FromTestStep( return instruction; } + public static TestStepInstruction FromTestStep( + TestStep step, + Dictionary variables + ) + { + step.TestCase.WithVariables(variables); + + return FromTestStep(step); + } + private static string ReplaceVariables( string value, Dictionary variables diff --git a/src/tapir.Cli/Domain/TestCaseContentFileResolver.cs b/src/tapir.Cli/Domain/TestCaseContentFileResolver.cs index aea9fda..17f23c2 100644 --- a/src/tapir.Cli/Domain/TestCaseContentFileResolver.cs +++ b/src/tapir.Cli/Domain/TestCaseContentFileResolver.cs @@ -9,7 +9,7 @@ internal static class TestCaseContentFileResolver return null; } - foreach (var candidate in GetCandidatePaths(instruction.File, instruction.TestCaseFile)) + foreach (var candidate in GetCandidatePaths(instruction.File, instruction.TestStep.TestCase.File)) { if (File.Exists(candidate)) { @@ -28,7 +28,7 @@ public static string LocateExistingFile(TestStepInstruction instruction) ); } - internal static IEnumerable GetCandidatePaths(string filePath, string testCaseFile) + internal static IEnumerable GetCandidatePaths(string filePath, string? testCaseFile) { if (string.IsNullOrWhiteSpace(filePath)) { diff --git a/src/tapir.Cli/Domain/TestCaseParser.cs b/src/tapir.Cli/Domain/TestCaseParser.cs index c46a330..4548cc0 100644 --- a/src/tapir.Cli/Domain/TestCaseParser.cs +++ b/src/tapir.Cli/Domain/TestCaseParser.cs @@ -37,6 +37,8 @@ internal async Task ToTestCaseAsync(CancellationToken cancellationToke File = _file }; + AssignTestCaseToSteps(testCase); + if (!string.IsNullOrEmpty(domain)) { testCase.WithDomain(domain); @@ -60,7 +62,7 @@ internal async Task ToTestCaseAsync(CancellationToken cancellationToke return (testCaseId, testCaseTitle); } - private string? FindTag(string[] lines, string tag) + private static string? FindTag(string[] lines, string tag) { var line = lines.FirstOrDefault(l => l.StartsWith($"- **{tag}**:")); if (line == null) return null; @@ -74,7 +76,7 @@ internal async Task ToTestCaseAsync(CancellationToken cancellationToke /// Finds a tag value that may contain colons (e.g. URLs like https://localhost:5001). /// Returns everything after the first colon on the matching line. /// - private string? FindTagValue(string[] lines, string tag) + private static string? FindTagValue(string[] lines, string tag) { var line = lines.FirstOrDefault(l => l.StartsWith($"- **{tag}**:")); if (line == null) return null; @@ -83,7 +85,7 @@ internal async Task ToTestCaseAsync(CancellationToken cancellationToke return colonIndex >= 0 ? line.Substring(colonIndex + 1).Trim() : null; } - private IEnumerable GetTables(string markdownContent) + private static IEnumerable
GetTables(string markdownContent) { var tables = new List
(); var table = new MarkdownTable(markdownContent); @@ -106,21 +108,11 @@ private IEnumerable
GetTables(string markdownContent) return tables; } - private string? GetLinkedFile(string file, string? link) + private static void AssignTestCaseToSteps(TestCase testCase) { - if (string.IsNullOrWhiteSpace(link)) return null; - if (!link.Contains('(')) return null; - - // Format: [The administrator must be authenticated](TC-001-Login.md) - string pattern = @"\(([^)]+)\)"; - Match match = Regex.Match(link, pattern); - - if (match.Success) + foreach (var step in testCase.Tables.SelectMany(t => t.Steps)) { - return Path.Combine(Path.GetDirectoryName(file)!, match.Groups[1].Value); + step.TestCase = testCase; } - - // Return null if no match is found - return null; } } diff --git a/src/tapir.Cli/Domain/Validation/TestCaseValidator.cs b/src/tapir.Cli/Domain/Validation/TestCaseValidator.cs index d905757..3ab1590 100644 --- a/src/tapir.Cli/Domain/Validation/TestCaseValidator.cs +++ b/src/tapir.Cli/Domain/Validation/TestCaseValidator.cs @@ -47,6 +47,7 @@ CancellationToken cancellationToken result.AddError("Status", $"Status must be either '{Constants.TestCaseStatus.Passed}', '{Constants.TestCaseStatus.Failed}' or '{Constants.TestCaseStatus.Unknown}'."); } + // Validates each test step using the appropriate validator based on the action. foreach (var step in testCase.Tables.SelectMany(t => t.Steps)) { if (string.IsNullOrWhiteSpace(step.TestData)) @@ -54,7 +55,7 @@ CancellationToken cancellationToken try { - var instruction = TestStepInstruction.FromTestStep(step, testCase.Variables, testCase.File); + var instruction = TestStepInstruction.FromTestStep(step); var validationErrors = await _validators .FirstOrDefault(v => v.Name == instruction.Action)! .ValidateAsync(instruction, cancellationToken); @@ -94,7 +95,7 @@ CancellationToken cancellationToken try { var instructions = addContentSteps - .Select(step => TestStepInstruction.FromTestStep(step, testCase.Variables, testCase.File)) + .Select(TestStepInstruction.FromTestStep) .Where(i => i.Action == Constants.Actions.AddContent) .ToList(); diff --git a/src/tapir.Tests/Unit/HttpRequestMessageBuilderTests.cs b/src/tapir.Tests/Unit/HttpRequestMessageBuilderTests.cs index 01b96f6..94e214f 100644 --- a/src/tapir.Tests/Unit/HttpRequestMessageBuilderTests.cs +++ b/src/tapir.Tests/Unit/HttpRequestMessageBuilderTests.cs @@ -395,11 +395,16 @@ public async Task BuildAsync_WithRelativeFileContent_ReadsContentFromTestCaseDir { var instructions = new List { - new TestStepInstruction(new TestStep()) + new TestStepInstruction(new TestStep + { + TestCase = new TestCase + { + File = testCaseFile + } + }) { Action = Constants.Actions.AddContent, - File = "payload.json", - TestCaseFile = testCaseFile + File = "payload.json" }, new TestStepInstruction(new TestStep()) { diff --git a/src/tapir.Tests/Unit/HttpResponseMessageValidatorTests.cs b/src/tapir.Tests/Unit/HttpResponseMessageValidatorTests.cs index 552c350..17ab70f 100644 --- a/src/tapir.Tests/Unit/HttpResponseMessageValidatorTests.cs +++ b/src/tapir.Tests/Unit/HttpResponseMessageValidatorTests.cs @@ -250,7 +250,10 @@ public async Task ValidateAsync_WithVerifyContentFileRelativeToTestCase_ShouldRe file: "expected.json" ); verifyInstruction.ContentType = Constants.ContentTypes.Json; - verifyInstruction.TestCaseFile = testCaseFile; + verifyInstruction.TestStep.TestCase = new TestCase + { + File = testCaseFile + }; var instructions = new[] { sendInstruction, verifyInstruction }; var response = new HttpResponseMessage(HttpStatusCode.OK); diff --git a/src/tapir.Tests/Unit/TestCaseContentFileResolverTests.cs b/src/tapir.Tests/Unit/TestCaseContentFileResolverTests.cs index b2b4031..b065a0f 100644 --- a/src/tapir.Tests/Unit/TestCaseContentFileResolverTests.cs +++ b/src/tapir.Tests/Unit/TestCaseContentFileResolverTests.cs @@ -14,7 +14,7 @@ public async Task TryLocateExistingFile_WithRelativePath_ResolvesAgainstTestCase try { var instruction = CreateInstruction(Constants.Actions.VerifyContent, file: "expected.json"); - instruction.TestCaseFile = fixture.TestCaseFile; + instruction.TestStep.TestCase.File = fixture.TestCaseFile; var resolvedPath = TestCaseContentFileResolver.TryLocateExistingFile(instruction); @@ -39,7 +39,7 @@ public async Task AddContentActionValidator_WithRelativePath_UsesTestCaseDirecto file: "payload.json", contentType: Constants.ContentTypes.Json ); - instruction.TestCaseFile = fixture.TestCaseFile; + instruction.TestStep.TestCase.File = fixture.TestCaseFile; var results = (await validator.ValidateAsync(instruction, CancellationToken.None)).ToList(); @@ -64,7 +64,7 @@ public async Task VerifyContentActionValidator_WithRelativePath_UsesTestCaseDire file: "expected.json", contentType: Constants.ContentTypes.Json ); - instruction.TestCaseFile = fixture.TestCaseFile; + instruction.TestStep.TestCase.File = fixture.TestCaseFile; var results = (await validator.ValidateAsync(instruction, CancellationToken.None)).ToList(); @@ -83,7 +83,7 @@ private static TestStepInstruction CreateInstruction( string contentType = "application/json" ) { - return new TestStepInstruction(new TestStep { Id = 1 }) + return new TestStepInstruction(new TestStep { Id = 1, TestCase = new TestCase() }) { Action = action, File = file, diff --git a/src/tapir.Tests/Unit/TestCaseParserTests.cs b/src/tapir.Tests/Unit/TestCaseParserTests.cs index ab30cbb..528c8d8 100644 --- a/src/tapir.Tests/Unit/TestCaseParserTests.cs +++ b/src/tapir.Tests/Unit/TestCaseParserTests.cs @@ -71,6 +71,7 @@ Tests whether all users can be retrieved and Alice's ID can be extracted so that Assert.Equal("Call users api", firstTableSteps[0].Description); Assert.Equal("Verify response code", firstTableSteps[1].Description); Assert.Equal("Inspect content", firstTableSteps[2].Description); + Assert.All(firstTableSteps, step => Assert.Same(testCase, step.TestCase)); // Verify second table has 3 steps var secondTable = tables[1]; @@ -79,6 +80,7 @@ Tests whether all users can be retrieved and Alice's ID can be extracted so that Assert.Equal("Get Alice Details", secondTableSteps[0].Description); Assert.Equal("Verify response code", secondTableSteps[1].Description); Assert.Equal("Inspect content", secondTableSteps[2].Description); + Assert.All(secondTableSteps, step => Assert.Same(testCase, step.TestCase)); // Verify total steps across all tables is 6 var totalSteps = tables.SelectMany(t => t.Steps).Count(); diff --git a/src/tapir.Tests/Unit/TestStepInstructionTests.cs b/src/tapir.Tests/Unit/TestStepInstructionTests.cs index f4ac0dc..d7eefca 100644 --- a/src/tapir.Tests/Unit/TestStepInstructionTests.cs +++ b/src/tapir.Tests/Unit/TestStepInstructionTests.cs @@ -23,7 +23,6 @@ public void Constructor_WithTestStep_InitializesPropertiesWithDefaultValues() Assert.Equal(string.Empty, instruction.JsonPath); Assert.Equal("GET", instruction.Method); Assert.Equal(string.Empty, instruction.Endpoint); - Assert.Equal(string.Empty, instruction.TestCaseFile); Assert.Equal(string.Empty, instruction.Domain); Assert.Equal(Constants.ContentTypes.Json, instruction.ContentType); Assert.Equal(step, instruction.TestStep); @@ -235,22 +234,27 @@ public void FromTestStep_WithContentTypeParameter_ParsesContentTypeCorrectly() } [Fact] - public void FromTestStep_WithTestCaseFile_SetsTestCaseFile() + public void FromTestStep_WithStepTestCase_KeepsStepReference() { // Arrange + var testCaseFile = "/tmp/TC-Users-001.md"; + var testCase = new TestCase + { + File = testCaseFile + }; var step = new TestStep { Id = 1, - TestData = "Action=AddContent File=data.json" + TestData = "Action=AddContent File=data.json", + TestCase = testCase }; var variables = new Dictionary(); - var testCaseFile = "/tmp/TC-Users-001.md"; // Act - var instruction = TestStepInstruction.FromTestStep(step, variables, testCaseFile); + var instruction = TestStepInstruction.FromTestStep(step, variables); // Assert - Assert.Equal(testCaseFile, instruction.TestCaseFile); + Assert.Equal(testCaseFile, instruction.TestStep.TestCase.File); } [Fact] diff --git a/todo.md b/todo.md index 4aa9f54..8251e24 100644 --- a/todo.md +++ b/todo.md @@ -1,7 +1,5 @@ # Todo -- [ ] address TODO refactorings - - e.g. TestCase references in TestStep - [ ] add Variables support in Content files - [ ] Use ILogger through ctor injection instead of static Log class - make use of LoggerFactory while setting up the DI container @@ -19,3 +17,5 @@ - [x] improved docs / man - [x] support colored Console Logger based on log-levels with Serilog instead of custom ConsoleHelper - https://github.com/serilog/serilog-sinks-console/issues/35#issuecomment-2577943657 +- [x] address TODO refactorings + - e.g. TestCase references in TestStep