Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/changelogs/da0116b2f3cf4c028bfec714089c4cd7.cle
Original file line number Diff line number Diff line change
@@ -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"
}
2 changes: 1 addition & 1 deletion src/tapir.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 0 additions & 1 deletion src/tapir.Cli/Domain/Core/TestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ CancellationToken cancellationToken
)
{
var parser = new TestCaseParser(file);

return await parser.ToTestCaseAsync(cancellationToken);
}

Expand Down
5 changes: 1 addition & 4 deletions src/tapir.Cli/Domain/Core/TestStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
22 changes: 13 additions & 9 deletions src/tapir.Cli/Domain/Core/TestStepInstruction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// The domain to send the request to. Will override the global domain and be prepended to the endpoint.
Expand All @@ -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<string, string> 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)
Expand Down Expand Up @@ -83,6 +77,16 @@ public static TestStepInstruction FromTestStep(
return instruction;
}

public static TestStepInstruction FromTestStep(
TestStep step,
Dictionary<string, string> variables
)
{
step.TestCase.WithVariables(variables);

return FromTestStep(step);
}

private static string ReplaceVariables(
string value,
Dictionary<string, string> variables
Expand Down
4 changes: 2 additions & 2 deletions src/tapir.Cli/Domain/TestCaseContentFileResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand All @@ -28,7 +28,7 @@ public static string LocateExistingFile(TestStepInstruction instruction)
);
}

internal static IEnumerable<string> GetCandidatePaths(string filePath, string testCaseFile)
internal static IEnumerable<string> GetCandidatePaths(string filePath, string? testCaseFile)
{
if (string.IsNullOrWhiteSpace(filePath))
{
Expand Down
24 changes: 8 additions & 16 deletions src/tapir.Cli/Domain/TestCaseParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ internal async Task<TestCase> ToTestCaseAsync(CancellationToken cancellationToke
File = _file
};

AssignTestCaseToSteps(testCase);

if (!string.IsNullOrEmpty(domain))
{
testCase.WithDomain(domain);
Expand All @@ -60,7 +62,7 @@ internal async Task<TestCase> 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;
Expand All @@ -74,7 +76,7 @@ internal async Task<TestCase> 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.
/// </summary>
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;
Expand All @@ -83,7 +85,7 @@ internal async Task<TestCase> ToTestCaseAsync(CancellationToken cancellationToke
return colonIndex >= 0 ? line.Substring(colonIndex + 1).Trim() : null;
}

private IEnumerable<Table> GetTables(string markdownContent)
private static IEnumerable<Table> GetTables(string markdownContent)
{
var tables = new List<Table>();
var table = new MarkdownTable(markdownContent);
Expand All @@ -106,21 +108,11 @@ private IEnumerable<Table> 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;
}
}
5 changes: 3 additions & 2 deletions src/tapir.Cli/Domain/Validation/TestCaseValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ 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))
continue;

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);
Expand Down Expand Up @@ -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();

Expand Down
11 changes: 8 additions & 3 deletions src/tapir.Tests/Unit/HttpRequestMessageBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,16 @@ public async Task BuildAsync_WithRelativeFileContent_ReadsContentFromTestCaseDir
{
var instructions = new List<TestStepInstruction>
{
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())
{
Expand Down
5 changes: 4 additions & 1 deletion src/tapir.Tests/Unit/HttpResponseMessageValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions src/tapir.Tests/Unit/TestCaseContentFileResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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();

Expand All @@ -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();

Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/tapir.Tests/Unit/TestCaseParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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();
Expand Down
16 changes: 10 additions & 6 deletions src/tapir.Tests/Unit/TestStepInstructionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<string, string>();
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]
Expand Down
4 changes: 2 additions & 2 deletions todo.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Loading