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