From 721f366dc813d0c549916f4506e2807ef7ddc741 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:09:40 +0000 Subject: [PATCH 01/11] Initial plan From b67092168c1a151d705dfde371b6fab09865cfb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:40:22 +0000 Subject: [PATCH 02/11] Add Stryker.NET mutation testing configuration and CI workflow Agent-Logs-Url: https://github.com/Kros-sk/TeaPie/sessions/06690327-20a3-4c5b-a653-d6c40201601d Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- .config/dotnet-tools.json | 13 +++++++ .github/workflows/mutation-testing.yml | 40 +++++++++++++++++++++ .gitignore | 3 ++ docs/mutation-testing.md | 50 ++++++++++++++++++++++++++ stryker-config.json | 13 +++++++ 5 files changed, 119 insertions(+) create mode 100644 .config/dotnet-tools.json create mode 100644 .github/workflows/mutation-testing.yml create mode 100644 docs/mutation-testing.md create mode 100644 stryker-config.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..3acf9630 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-stryker": { + "version": "4.14.0", + "commands": [ + "dotnet-stryker" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.github/workflows/mutation-testing.yml b/.github/workflows/mutation-testing.yml new file mode 100644 index 00000000..3e494bd1 --- /dev/null +++ b/.github/workflows/mutation-testing.yml @@ -0,0 +1,40 @@ +name: Mutation Testing + +on: + schedule: + - cron: '0 3 * * 1' # Every Monday at 03:00 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + mutation-testing: + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.x' + + - name: Restore tools + run: dotnet tool restore + + - name: Install dependencies + run: dotnet restore + + - name: Run Stryker mutation testing + run: dotnet stryker + + - name: Upload mutation report + if: always() + uses: actions/upload-artifact@v4 + with: + name: mutation-report + path: '**/StrykerOutput/**/reports/**' + retention-days: 14 diff --git a/.gitignore b/.gitignore index 83018201..61256c51 100644 --- a/.gitignore +++ b/.gitignore @@ -401,6 +401,9 @@ FodyWeavers.xsd **/.teapie/cache/ **/.teapie/reports/ +# Stryker mutation testing +**/StrykerOutput/ + # User-defined *launchSettings*.json docs/_site diff --git a/docs/mutation-testing.md b/docs/mutation-testing.md new file mode 100644 index 00000000..d08f69fd --- /dev/null +++ b/docs/mutation-testing.md @@ -0,0 +1,50 @@ +# Mutation Testing + +TeaPie uses [Stryker.NET](https://stryker-mutator.io/docs/stryker-net/introduction/) for mutation testing to verify the effectiveness of the test suite. + +## Overview + +Mutation testing systematically introduces small changes (*mutants*) into the source code and checks whether the existing tests detect them. A killed mutant means the tests caught the change; a surviving mutant may indicate a gap in test assertions. + +## Quick Start + +```bash +# Restore the local tool +dotnet tool restore + +# Run mutation testing +dotnet stryker +``` + +The HTML report is generated in the `StrykerOutput/` directory. + +## Configuration + +Stryker is configured via [`stryker-config.json`](../stryker-config.json) in the repository root. Key settings: + +| Setting | Value | Description | +|---------|-------|-------------| +| `project` | `TeaPie.csproj` | Source project under test | +| `test-projects` | `TeaPie.Tests.csproj` | Test project | +| `reporters` | html, progress, cleartext | Output formats | +| `thresholds.high` | 80 | Score above this is green | +| `thresholds.low` | 60 | Score below this is red | + +## CI Integration + +Mutation testing runs as a **scheduled weekly workflow** (`.github/workflows/mutation-testing.yml`) every Monday at 03:00 UTC. It can also be triggered manually via `workflow_dispatch`. The HTML report is uploaded as a build artifact. + +## Baseline Score + +The initial baseline mutation score is **37.83%**, established with Stryker.NET 4.14.0. + +| Metric | Count | +|--------|-------| +| Killed | 787 | +| Survived | 707 | +| Timeout | 6 | +| No Coverage | 596 | +| Compile Error | 145 | +| Ignored | 677 | + +The relatively low score is expected for an initial baseline. Many surviving mutants are in code paths not yet covered by tests (No Coverage) or in areas like console rendering and pipeline orchestration that are harder to unit test. The score will improve as tests are strengthened over time. diff --git a/stryker-config.json b/stryker-config.json new file mode 100644 index 00000000..3a3dbab9 --- /dev/null +++ b/stryker-config.json @@ -0,0 +1,13 @@ +{ + "stryker-config": { + "project": "TeaPie.csproj", + "test-projects": ["./tests/TeaPie.Tests/TeaPie.Tests.csproj"], + "solution": "./TeaPie.sln", + "reporters": ["html", "progress", "cleartext"], + "thresholds": { + "high": 80, + "low": 60, + "break": 0 + } + } +} From c0ce66cab1bab7b539d8fc69c9cdd3d034d10608 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:16:54 +0000 Subject: [PATCH 03/11] Set realistic mutation score thresholds based on 37.83% baseline Agent-Logs-Url: https://github.com/Kros-sk/TeaPie/sessions/f673be2b-f9b9-4ad8-b339-ee0bff0181c6 Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- docs/mutation-testing.md | 15 +++++++++++++-- stryker-config.json | 6 +++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/mutation-testing.md b/docs/mutation-testing.md index d08f69fd..789ecc09 100644 --- a/docs/mutation-testing.md +++ b/docs/mutation-testing.md @@ -27,8 +27,9 @@ Stryker is configured via [`stryker-config.json`](../stryker-config.json) in the | `project` | `TeaPie.csproj` | Source project under test | | `test-projects` | `TeaPie.Tests.csproj` | Test project | | `reporters` | html, progress, cleartext | Output formats | -| `thresholds.high` | 80 | Score above this is green | -| `thresholds.low` | 60 | Score below this is red | +| `thresholds.high` | 60 | Score above this is green | +| `thresholds.low` | 40 | Score below this is red | +| `thresholds.break` | 35 | Score below this fails the build | ## CI Integration @@ -48,3 +49,13 @@ The initial baseline mutation score is **37.83%**, established with Stryker.NET | Ignored | 677 | The relatively low score is expected for an initial baseline. Many surviving mutants are in code paths not yet covered by tests (No Coverage) or in areas like console rendering and pipeline orchestration that are harder to unit test. The score will improve as tests are strengthened over time. + +## Thresholds + +The thresholds are calibrated against the baseline score of 37.83%: + +- **`break` (35%)** — Just below the baseline. If the mutation score drops below this, Stryker exits with a non-zero code and the CI workflow fails. This prevents significant regression. +- **`low` (40%)** — Slightly above the baseline. Scores below this are reported as red/warning, signaling a near-term improvement target. +- **`high` (60%)** — Longer-term goal for a healthy mutation score. Scores above this are reported as green. + +These thresholds should be raised as the test suite improves and the mutation score increases. diff --git a/stryker-config.json b/stryker-config.json index 3a3dbab9..3e7f3c93 100644 --- a/stryker-config.json +++ b/stryker-config.json @@ -5,9 +5,9 @@ "solution": "./TeaPie.sln", "reporters": ["html", "progress", "cleartext"], "thresholds": { - "high": 80, - "low": 60, - "break": 0 + "high": 60, + "low": 40, + "break": 35 } } } From df6d4742ec4475a21661b3faf8b51e59bd766fde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:22:43 +0000 Subject: [PATCH 04/11] Set honest mutation score thresholds with industry context Break threshold raised to 37 (at baseline - no regression allowed), low set to 50 (mid-term target), high set to 80 (industry good). Documentation updated with honest assessment and score ranges. Agent-Logs-Url: https://github.com/Kros-sk/TeaPie/sessions/aa8c45ed-90bf-4506-b48f-74d67cd27895 Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- docs/mutation-testing.md | 25 +++++++++++++++++-------- stryker-config.json | 6 +++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/mutation-testing.md b/docs/mutation-testing.md index 789ecc09..754e33e0 100644 --- a/docs/mutation-testing.md +++ b/docs/mutation-testing.md @@ -27,9 +27,9 @@ Stryker is configured via [`stryker-config.json`](../stryker-config.json) in the | `project` | `TeaPie.csproj` | Source project under test | | `test-projects` | `TeaPie.Tests.csproj` | Test project | | `reporters` | html, progress, cleartext | Output formats | -| `thresholds.high` | 60 | Score above this is green | -| `thresholds.low` | 40 | Score below this is red | -| `thresholds.break` | 35 | Score below this fails the build | +| `thresholds.high` | 80 | Score above this is green | +| `thresholds.low` | 50 | Score below this is red | +| `thresholds.break` | 37 | Score below this fails the build | ## CI Integration @@ -48,14 +48,23 @@ The initial baseline mutation score is **37.83%**, established with Stryker.NET | Compile Error | 145 | | Ignored | 677 | -The relatively low score is expected for an initial baseline. Many surviving mutants are in code paths not yet covered by tests (No Coverage) or in areas like console rendering and pipeline orchestration that are harder to unit test. The score will improve as tests are strengthened over time. +A score of ~38% is **low**. In industry practice, mutation scores typically fall into these ranges: + +| Range | Assessment | +|-------|------------| +| **80%+** | Good — strong test suite that catches most regressions | +| **60–79%** | Acceptable — reasonable safety net, room to improve | +| **40–59%** | Below average — significant gaps in test assertions | +| **Below 40%** | Low — most mutations survive undetected | + +The main contributors to the low score are the high **No Coverage** count (596 mutants in untested code) and the large number of **Survived** mutants (707) in areas like console rendering, pipeline orchestration, and file I/O that lack assertions. Improving coverage of core logic and adding targeted assertions will have the biggest impact. ## Thresholds -The thresholds are calibrated against the baseline score of 37.83%: +The thresholds are set to prevent regression and drive improvement: -- **`break` (35%)** — Just below the baseline. If the mutation score drops below this, Stryker exits with a non-zero code and the CI workflow fails. This prevents significant regression. -- **`low` (40%)** — Slightly above the baseline. Scores below this are reported as red/warning, signaling a near-term improvement target. -- **`high` (60%)** — Longer-term goal for a healthy mutation score. Scores above this are reported as green. +- **`break` (37%)** — At the baseline. The score is already low, so any regression fails the CI build immediately. This ensures the mutation score can only go up. +- **`low` (50%)** — Mid-term improvement target. Reaching this means the test suite catches at least half of all mutations, a meaningful milestone. +- **`high` (80%)** — Long-term goal aligned with industry standards for a good mutation score. Scores above this are reported as green. These thresholds should be raised as the test suite improves and the mutation score increases. diff --git a/stryker-config.json b/stryker-config.json index 3e7f3c93..394833a4 100644 --- a/stryker-config.json +++ b/stryker-config.json @@ -5,9 +5,9 @@ "solution": "./TeaPie.sln", "reporters": ["html", "progress", "cleartext"], "thresholds": { - "high": 60, - "low": 40, - "break": 35 + "high": 80, + "low": 50, + "break": 37 } } } From e14ad6ba944468c5e587040b4a6245cbef1a4686 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:51:04 +0000 Subject: [PATCH 05/11] Add unit tests for 13 untested source files Add test files following existing conventions ({ClassName}Should.cs pattern, xUnit with FluentAssertions, file-scoped namespaces): - PascalCaseHumanizerShould: SplitPascalCase with various inputs - StringExtensionsShould: TrimSuffix edge cases - ConstantsShould: Verify all constant values - VariableNameValidatorShould: Valid/invalid variable name validation - FunctionNameValidatorShould: Valid/invalid function name validation - HeaderNameValidatorShould: HTTP header name/value validation - JsonBodyResolverShould: JSON body resolution with JPath - XmlBodyResolverShould: XML body resolution with XPath - RequestVariableDescriptionShould: Record behavior - TimerShould: All 3 Execute overloads - ExitCodeShould: Enum value verification - VariableNameViolationExceptionShould: Exception constructors - FunctionNameViolationExceptionShould: Exception constructors Total: 83 new passing tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- tests/TeaPie.Tests/ConstantsShould.cs | 70 +++++++++++++++++++ tests/TeaPie.Tests/ExitCodeShould.cs | 22 ++++++ .../Functions/FunctionNameValidatorShould.cs | 59 ++++++++++++++++ .../FunctionNameViolationExceptionShould.cs | 30 ++++++++ .../Http/Headers/HeaderNameValidatorShould.cs | 46 ++++++++++++ tests/TeaPie.Tests/Logging/TimerShould.cs | 47 +++++++++++++ .../TeaPie.Tests/PascalCaseHumanizerShould.cs | 23 ++++++ tests/TeaPie.Tests/StringExtensionsShould.cs | 44 ++++++++++++ .../Variables/JsonBodyResolverShould.cs | 48 +++++++++++++ .../RequestVariableDescriptionShould.cs | 32 +++++++++ .../Variables/VariableNameValidatorShould.cs | 61 ++++++++++++++++ .../VariableNameViolationExceptionShould.cs | 30 ++++++++ .../Variables/XmlBodyResolverShould.cs | 47 +++++++++++++ 13 files changed, 559 insertions(+) create mode 100644 tests/TeaPie.Tests/ConstantsShould.cs create mode 100644 tests/TeaPie.Tests/ExitCodeShould.cs create mode 100644 tests/TeaPie.Tests/Functions/FunctionNameValidatorShould.cs create mode 100644 tests/TeaPie.Tests/Functions/FunctionNameViolationExceptionShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/HeaderNameValidatorShould.cs create mode 100644 tests/TeaPie.Tests/Logging/TimerShould.cs create mode 100644 tests/TeaPie.Tests/PascalCaseHumanizerShould.cs create mode 100644 tests/TeaPie.Tests/StringExtensionsShould.cs create mode 100644 tests/TeaPie.Tests/Variables/JsonBodyResolverShould.cs create mode 100644 tests/TeaPie.Tests/Variables/RequestVariableDescriptionShould.cs create mode 100644 tests/TeaPie.Tests/Variables/VariableNameValidatorShould.cs create mode 100644 tests/TeaPie.Tests/Variables/VariableNameViolationExceptionShould.cs create mode 100644 tests/TeaPie.Tests/Variables/XmlBodyResolverShould.cs diff --git a/tests/TeaPie.Tests/ConstantsShould.cs b/tests/TeaPie.Tests/ConstantsShould.cs new file mode 100644 index 00000000..171e1bfa --- /dev/null +++ b/tests/TeaPie.Tests/ConstantsShould.cs @@ -0,0 +1,70 @@ +using FluentAssertions; + +namespace TeaPie.Tests; + +public class ConstantsShould +{ + [Fact] + public void HaveCorrectRequestFileExtension() => + Constants.RequestFileExtension.Should().Be(".http"); + + [Fact] + public void HaveCorrectScriptFileExtension() => + Constants.ScriptFileExtension.Should().Be(".csx"); + + [Fact] + public void HaveCorrectPreRequestSuffix() => + Constants.PreRequestSuffix.Should().Be("-init"); + + [Fact] + public void HaveCorrectRequestSuffix() => + Constants.RequestSuffix.Should().Be("-req"); + + [Fact] + public void HaveCorrectPostResponseSuffix() => + Constants.PostResponseSuffix.Should().Be("-test"); + + [Fact] + public void HaveCorrectDefaultEnvironmentFileName() => + Constants.DefaultEnvironmentFileName.Should().Be("env"); + + [Fact] + public void HaveCorrectEnvironmentFileExtension() => + Constants.EnvironmentFileExtension.Should().Be(".json"); + + [Fact] + public void HaveCorrectDefaultEnvironmentName() => + Constants.DefaultEnvironmentName.Should().Be("$shared"); + + [Fact] + public void HaveCorrectPascalCasePattern() => + Constants.PascalCasePattern.Should().Be("([A-Z][a-z]*|[a-z]+)"); + + [Fact] + public void HaveCorrectApplicationName() => + Constants.ApplicationName.Should().Be("TeaPie"); + + [Fact] + public void HaveCorrectDefaultInitializationScriptName() => + Constants.DefaultInitializationScriptName.Should().Be("init"); + + [Fact] + public void HaveCorrectTeaPieFolderName() => + Constants.TeaPieFolderName.Should().Be(".teapie"); + + [Fact] + public void HaveCorrectUnixEndOfLine() => + Constants.UnixEndOfLine.Should().Be("\n"); + + [Fact] + public void HaveCorrectWindowsEndOfLine() => + Constants.WindowsEndOfLine.Should().Be("\r\n"); + + [Fact] + public void HaveCorrectSecretVariableTag() => + Constants.SecretVariableTag.Should().Be("secret"); + + [Fact] + public void HaveCorrectNoCacheVariableTag() => + Constants.NoCacheVariableTag.Should().Be("no-cache"); +} diff --git a/tests/TeaPie.Tests/ExitCodeShould.cs b/tests/TeaPie.Tests/ExitCodeShould.cs new file mode 100644 index 00000000..d46e7651 --- /dev/null +++ b/tests/TeaPie.Tests/ExitCodeShould.cs @@ -0,0 +1,22 @@ +using FluentAssertions; + +namespace TeaPie.Tests; + +public class ExitCodeShould +{ + [Fact] + public void HaveSuccessEqualToZero() => + ((int)ExitCode.Success).Should().Be(0); + + [Fact] + public void HaveGeneralErrorEqualToOne() => + ((int)ExitCode.GeneralError).Should().Be(1); + + [Fact] + public void HaveTestsFailedEqualToTwo() => + ((int)ExitCode.TestsFailed).Should().Be(2); + + [Fact] + public void HaveCanceledEqualTo130() => + ((int)ExitCode.Canceled).Should().Be(130); +} diff --git a/tests/TeaPie.Tests/Functions/FunctionNameValidatorShould.cs b/tests/TeaPie.Tests/Functions/FunctionNameValidatorShould.cs new file mode 100644 index 00000000..a83c866d --- /dev/null +++ b/tests/TeaPie.Tests/Functions/FunctionNameValidatorShould.cs @@ -0,0 +1,59 @@ +using FluentAssertions; +using TeaPie.Functions; + +namespace TeaPie.Tests.Functions; + +public class FunctionNameValidatorShould +{ + [Theory] + [InlineData("$myFunction")] + [InlineData("$func-123")] + [InlineData("$a")] + public void NotThrowForValidName(string name) + { + var act = () => FunctionNameValidator.Resolve(name); + act.Should().NotThrow(); + } + + [Fact] + public void ThrowForNullName() + { + var act = () => FunctionNameValidator.Resolve(null); + act.Should().Throw(); + } + + [Fact] + public void ThrowForEmptyString() + { + var act = () => FunctionNameValidator.Resolve(string.Empty); + act.Should().Throw(); + } + + [Fact] + public void ThrowForWhitespaceOnly() + { + var act = () => FunctionNameValidator.Resolve(" "); + act.Should().Throw(); + } + + [Fact] + public void ThrowForNameWithoutDollarPrefix() + { + var act = () => FunctionNameValidator.Resolve("myFunc"); + act.Should().Throw(); + } + + [Fact] + public void ThrowForNameWithSpaces() + { + var act = () => FunctionNameValidator.Resolve("$my func"); + act.Should().Throw(); + } + + [Fact] + public void ThrowForNameWithAngleBrackets() + { + var act = () => FunctionNameValidator.Resolve("$my"); + act.Should().Throw(); + } +} diff --git a/tests/TeaPie.Tests/Functions/FunctionNameViolationExceptionShould.cs b/tests/TeaPie.Tests/Functions/FunctionNameViolationExceptionShould.cs new file mode 100644 index 00000000..df638ee6 --- /dev/null +++ b/tests/TeaPie.Tests/Functions/FunctionNameViolationExceptionShould.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using TeaPie.Functions; + +namespace TeaPie.Tests.Functions; + +public class FunctionNameViolationExceptionShould +{ + [Fact] + public void BeCreatedWithDefaultConstructor() + { + var exception = new FunctionNameViolationException(); + exception.Should().NotBeNull(); + } + + [Fact] + public void SetMessageWithMessageConstructor() + { + var exception = new FunctionNameViolationException("test message"); + exception.Message.Should().Be("test message"); + } + + [Fact] + public void SetMessageAndInnerException() + { + var inner = new InvalidOperationException("inner"); + var exception = new FunctionNameViolationException("outer", inner); + exception.Message.Should().Be("outer"); + exception.InnerException.Should().BeSameAs(inner); + } +} diff --git a/tests/TeaPie.Tests/Http/Headers/HeaderNameValidatorShould.cs b/tests/TeaPie.Tests/Http/Headers/HeaderNameValidatorShould.cs new file mode 100644 index 00000000..070425e5 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/HeaderNameValidatorShould.cs @@ -0,0 +1,46 @@ +using System.Data; +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class HeaderNameValidatorShould +{ + [Theory] + [InlineData("Content-Type")] + [InlineData("X-Custom-Header")] + [InlineData("Accept")] + public void NotThrowForValidHeaderName(string headerName) + { + var act = () => HeaderNameValidator.CheckName(headerName); + act.Should().NotThrow(); + } + + [Fact] + public void ThrowForEmptyHeaderName() + { + var act = () => HeaderNameValidator.CheckName(string.Empty); + act.Should().Throw(); + } + + [Fact] + public void ThrowForHeaderNameWithSpaces() + { + var act = () => HeaderNameValidator.CheckName("Content Type"); + act.Should().Throw(); + } + + [Fact] + public void NotThrowForValidHeaderValue() + { + var act = () => HeaderNameValidator.CheckValue("application/json"); + act.Should().NotThrow(); + } + + [Fact] + public void NotThrowForCheckHeaderWithValidNameAndValue() + { + var act = () => HeaderNameValidator.CheckHeader("Content-Type", "application/json"); + act.Should().NotThrow(); + } +} diff --git a/tests/TeaPie.Tests/Logging/TimerShould.cs b/tests/TeaPie.Tests/Logging/TimerShould.cs new file mode 100644 index 00000000..e09b6c47 --- /dev/null +++ b/tests/TeaPie.Tests/Logging/TimerShould.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using Timer = TeaPie.Logging.Timer; + +namespace TeaPie.Tests.Logging; + +public class TimerShould +{ + [Fact] + public async Task InvokeLogCallbackAndReturnResultForAsyncExecute() + { + long loggedMs = -1; + + var result = await Timer.Execute( + async () => { await Task.Delay(10); return 42; }, + ms => loggedMs = ms); + + result.Should().Be(42); + loggedMs.Should().BeGreaterThanOrEqualTo(0); + } + + [Fact] + public void InvokeLogCallbackAndReturnResultForSyncExecute() + { + long loggedMs = -1; + + var result = Timer.Execute( + () => 42, + ms => loggedMs = ms); + + result.Should().Be(42); + loggedMs.Should().BeGreaterThanOrEqualTo(0); + } + + [Fact] + public void InvokeActionAndLogCallbackForVoidExecute() + { + long loggedMs = -1; + var actionInvoked = false; + + Timer.Execute( + () => actionInvoked = true, + ms => loggedMs = ms); + + actionInvoked.Should().BeTrue(); + loggedMs.Should().BeGreaterThanOrEqualTo(0); + } +} diff --git a/tests/TeaPie.Tests/PascalCaseHumanizerShould.cs b/tests/TeaPie.Tests/PascalCaseHumanizerShould.cs new file mode 100644 index 00000000..a549eda3 --- /dev/null +++ b/tests/TeaPie.Tests/PascalCaseHumanizerShould.cs @@ -0,0 +1,23 @@ +using FluentAssertions; + +namespace TeaPie.Tests; + +public class PascalCaseHumanizerShould +{ + [Theory] + [InlineData("HelloWorld", "Hello World")] + [InlineData("Hello", "Hello")] + [InlineData("ThisIsATest", "This Is A Test")] + [InlineData("hello", "hello")] + [InlineData("ABC", "A B C")] + public void SplitPascalCaseCorrectly(string input, string expected) + { + input.SplitPascalCase().Should().Be(expected); + } + + [Fact] + public void ReturnEmptyStringForEmptyInput() + { + string.Empty.SplitPascalCase().Should().Be(string.Empty); + } +} diff --git a/tests/TeaPie.Tests/StringExtensionsShould.cs b/tests/TeaPie.Tests/StringExtensionsShould.cs new file mode 100644 index 00000000..9bb78e98 --- /dev/null +++ b/tests/TeaPie.Tests/StringExtensionsShould.cs @@ -0,0 +1,44 @@ +using FluentAssertions; + +namespace TeaPie.Tests; + +public class StringExtensionsShould +{ + [Fact] + public void RemoveMatchingSuffix() + { + "HelloWorld".TrimSuffix("World").Should().Be("Hello"); + } + + [Fact] + public void ReturnSameStringWhenSuffixDoesNotMatch() + { + "HelloWorld".TrimSuffix("Planet").Should().Be("HelloWorld"); + } + + [Fact] + public void ReturnOriginalStringWhenSuffixIsNull() + { + "HelloWorld".TrimSuffix(null!).Should().Be("HelloWorld"); + } + + [Fact] + public void ReturnOriginalStringWhenSuffixIsEmpty() + { + "HelloWorld".TrimSuffix(string.Empty).Should().Be("HelloWorld"); + } + + [Fact] + public void ThrowArgumentNullExceptionWhenTextIsNull() + { + string text = null!; + var act = () => text.TrimSuffix("suffix"); + act.Should().Throw(); + } + + [Fact] + public void ReturnEmptyStringWhenTextEqualsSuffix() + { + "Hello".TrimSuffix("Hello").Should().Be(string.Empty); + } +} diff --git a/tests/TeaPie.Tests/Variables/JsonBodyResolverShould.cs b/tests/TeaPie.Tests/Variables/JsonBodyResolverShould.cs new file mode 100644 index 00000000..dbd34594 --- /dev/null +++ b/tests/TeaPie.Tests/Variables/JsonBodyResolverShould.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using TeaPie.Variables; + +namespace TeaPie.Tests.Variables; + +public class JsonBodyResolverShould +{ + private readonly JsonBodyResolver _resolver = new(); + + [Fact] + public void CanResolveApplicationJson() + { + _resolver.CanResolve("application/json").Should().BeTrue(); + } + + [Fact] + public void CanResolveApplicationJsonCaseInsensitive() + { + _resolver.CanResolve("APPLICATION/JSON").Should().BeTrue(); + } + + [Fact] + public void NotResolveTextXml() + { + _resolver.CanResolve("text/xml").Should().BeFalse(); + } + + [Fact] + public void ResolveSimpleProperty() + { + var body = """{"name":"John"}"""; + _resolver.Resolve(body, "name").Should().Be("John"); + } + + [Fact] + public void ReturnDefaultWhenTokenNotFound() + { + var body = """{"name":"John"}"""; + _resolver.Resolve(body, "age", "N/A").Should().Be("N/A"); + } + + [Fact] + public void ResolveNestedValueWithJPath() + { + var body = """{"user":{"name":"John"}}"""; + _resolver.Resolve(body, "user.name").Should().Be("John"); + } +} diff --git a/tests/TeaPie.Tests/Variables/RequestVariableDescriptionShould.cs b/tests/TeaPie.Tests/Variables/RequestVariableDescriptionShould.cs new file mode 100644 index 00000000..5191c884 --- /dev/null +++ b/tests/TeaPie.Tests/Variables/RequestVariableDescriptionShould.cs @@ -0,0 +1,32 @@ +using FluentAssertions; +using TeaPie.Variables; + +namespace TeaPie.Tests.Variables; + +public class RequestVariableDescriptionShould +{ + [Fact] + public void ReturnDotSeparatedValuesFromToString() + { + var description = new RequestVariableDescription("req1", "response", "body", "$.name"); + description.ToString().Should().Be("req1.response.body.$.name"); + } + + [Fact] + public void SupportRecordEquality() + { + var a = new RequestVariableDescription("req1", "response", "body", "$.name"); + var b = new RequestVariableDescription("req1", "response", "body", "$.name"); + a.Should().Be(b); + } + + [Fact] + public void ExposeProperties() + { + var description = new RequestVariableDescription("req1", "response", "body", "$.name"); + description.Name.Should().Be("req1"); + description.Type.Should().Be("response"); + description.Content.Should().Be("body"); + description.Query.Should().Be("$.name"); + } +} diff --git a/tests/TeaPie.Tests/Variables/VariableNameValidatorShould.cs b/tests/TeaPie.Tests/Variables/VariableNameValidatorShould.cs new file mode 100644 index 00000000..cca6d927 --- /dev/null +++ b/tests/TeaPie.Tests/Variables/VariableNameValidatorShould.cs @@ -0,0 +1,61 @@ +using FluentAssertions; +using TeaPie.Variables; + +namespace TeaPie.Tests.Variables; + +public class VariableNameValidatorShould +{ + [Theory] + [InlineData("myVariable")] + [InlineData("my-variable")] + [InlineData("var_123")] + [InlineData("a")] + [InlineData("my.var")] + public void NotThrowForValidName(string name) + { + var act = () => VariableNameValidator.Resolve(name); + act.Should().NotThrow(); + } + + [Fact] + public void ThrowForNullName() + { + var act = () => VariableNameValidator.Resolve(null); + act.Should().Throw(); + } + + [Fact] + public void ThrowForEmptyString() + { + var act = () => VariableNameValidator.Resolve(string.Empty); + act.Should().Throw(); + } + + [Fact] + public void ThrowForWhitespaceOnly() + { + var act = () => VariableNameValidator.Resolve(" "); + act.Should().Throw(); + } + + [Fact] + public void ThrowForNameWithAngleBrackets() + { + var act = () => VariableNameValidator.Resolve("my"); + act.Should().Throw(); + } + + [Fact] + public void ThrowForNameStartingWithDollarSign() + { + var act = () => VariableNameValidator.Resolve("$myVar"); + act.Should().Throw(); + } + + [Fact] + public void ThrowForNameWithSpaces() + { + var act = () => VariableNameValidator.Resolve("my var"); + act.Should().Throw(); + } +} diff --git a/tests/TeaPie.Tests/Variables/VariableNameViolationExceptionShould.cs b/tests/TeaPie.Tests/Variables/VariableNameViolationExceptionShould.cs new file mode 100644 index 00000000..667f5afe --- /dev/null +++ b/tests/TeaPie.Tests/Variables/VariableNameViolationExceptionShould.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using TeaPie.Variables; + +namespace TeaPie.Tests.Variables; + +public class VariableNameViolationExceptionShould +{ + [Fact] + public void BeCreatedWithDefaultConstructor() + { + var exception = new VariableNameViolationException(); + exception.Should().NotBeNull(); + } + + [Fact] + public void SetMessageWithMessageConstructor() + { + var exception = new VariableNameViolationException("test message"); + exception.Message.Should().Be("test message"); + } + + [Fact] + public void SetMessageAndInnerException() + { + var inner = new InvalidOperationException("inner"); + var exception = new VariableNameViolationException("outer", inner); + exception.Message.Should().Be("outer"); + exception.InnerException.Should().BeSameAs(inner); + } +} diff --git a/tests/TeaPie.Tests/Variables/XmlBodyResolverShould.cs b/tests/TeaPie.Tests/Variables/XmlBodyResolverShould.cs new file mode 100644 index 00000000..e63d09e3 --- /dev/null +++ b/tests/TeaPie.Tests/Variables/XmlBodyResolverShould.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using TeaPie.Variables; + +namespace TeaPie.Tests.Variables; + +public class XmlBodyResolverShould +{ + private readonly XmlBodyResolver _resolver = new(); + + [Fact] + public void CanResolveApplicationXml() + { + _resolver.CanResolve("application/xml").Should().BeTrue(); + } + + [Fact] + public void CanResolveTextXml() + { + _resolver.CanResolve("text/xml").Should().BeTrue(); + } + + [Fact] + public void CanResolveTextXmlCaseInsensitive() + { + _resolver.CanResolve("TEXT/XML").Should().BeTrue(); + } + + [Fact] + public void NotResolveApplicationJson() + { + _resolver.CanResolve("application/json").Should().BeFalse(); + } + + [Fact] + public void ResolveValueForXPathQuery() + { + var body = "John"; + _resolver.Resolve(body, "/root/name").Should().Be("John"); + } + + [Fact] + public void ReturnDefaultWhenNodeNotFound() + { + var body = "John"; + _resolver.Resolve(body, "/root/age", "N/A").Should().Be("N/A"); + } +} From 6709c41e2bb52fa28ded1f74fc9df6f9cc757d07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:00:36 +0000 Subject: [PATCH 06/11] Add unit tests for HTTP parsers, Testing summaries, StructureExploration types, and ScriptReference Add 15 new test files with 77 tests covering: - HTTP parsing: MethodAndUriParser, HeaderParser, BodyParser, CommentLineParser, EmptyLineParser - Testing summaries: TestResultsSummary, CollectionTestResultsSummary, TestCaseTestResultsSummary - Structure exploration: File, Folder, InternalFile, ExternalFile, Script, TestCase - Scripts: ScriptReference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- .../Http/Parsing/BodyParserShould.cs | 46 +++++ .../Http/Parsing/CommentLineParserShould.cs | 68 +++++++ .../Http/Parsing/EmptyLineParserShould.cs | 60 ++++++ .../Http/Parsing/HeaderParserShould.cs | 62 +++++++ .../Http/Parsing/MethodAndUriParserShould.cs | 119 ++++++++++++ .../Scripts/ScriptReferenceShould.cs | 33 ++++ .../ExternalFileShould.cs | 23 +++ .../StructureExploration/FileShould.cs | 54 ++++++ .../StructureExploration/FolderShould.cs | 34 ++++ .../InternalFileShould.cs | 41 +++++ .../StructureExploration/ScriptShould.cs | 18 ++ .../StructureExploration/TestCaseShould.cs | 54 ++++++ .../CollectionTestResultsSummaryShould.cs | 81 +++++++++ .../TestCaseTestResultsSummaryShould.cs | 27 +++ .../Testing/TestResultsSummaryShould.cs | 172 ++++++++++++++++++ 15 files changed, 892 insertions(+) create mode 100644 tests/TeaPie.Tests/Http/Parsing/BodyParserShould.cs create mode 100644 tests/TeaPie.Tests/Http/Parsing/CommentLineParserShould.cs create mode 100644 tests/TeaPie.Tests/Http/Parsing/EmptyLineParserShould.cs create mode 100644 tests/TeaPie.Tests/Http/Parsing/HeaderParserShould.cs create mode 100644 tests/TeaPie.Tests/Http/Parsing/MethodAndUriParserShould.cs create mode 100644 tests/TeaPie.Tests/Scripts/ScriptReferenceShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/ExternalFileShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/FileShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/FolderShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/InternalFileShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/ScriptShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/TestCaseShould.cs create mode 100644 tests/TeaPie.Tests/Testing/CollectionTestResultsSummaryShould.cs create mode 100644 tests/TeaPie.Tests/Testing/TestCaseTestResultsSummaryShould.cs create mode 100644 tests/TeaPie.Tests/Testing/TestResultsSummaryShould.cs diff --git a/tests/TeaPie.Tests/Http/Parsing/BodyParserShould.cs b/tests/TeaPie.Tests/Http/Parsing/BodyParserShould.cs new file mode 100644 index 00000000..92602316 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Parsing/BodyParserShould.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using TeaPie.Http.Parsing; + +namespace TeaPie.Tests.Http.Parsing; + +public class BodyParserShould +{ + private readonly BodyParser _parser = new(); + + private static HttpParsingContext CreateContext() + { + using var requestMessage = new HttpRequestMessage(); + return new HttpParsingContext(requestMessage.Headers); + } + + [Fact] + public void ReturnTrueWhenIsBodyIsTrue() + { + var context = CreateContext(); + context.IsBody = true; + + _parser.CanParse("any line", context).Should().BeTrue(); + } + + [Fact] + public void ReturnFalseWhenIsBodyIsFalse() + { + var context = CreateContext(); + + _parser.CanParse("any line", context).Should().BeFalse(); + } + + [Fact] + public void AppendLineToBodyBuilder() + { + var context = CreateContext(); + context.IsBody = true; + + _parser.Parse("line one", context); + _parser.Parse("line two", context); + + var body = context.BodyBuilder.ToString(); + body.Should().Contain("line one"); + body.Should().Contain("line two"); + } +} diff --git a/tests/TeaPie.Tests/Http/Parsing/CommentLineParserShould.cs b/tests/TeaPie.Tests/Http/Parsing/CommentLineParserShould.cs new file mode 100644 index 00000000..bc34134b --- /dev/null +++ b/tests/TeaPie.Tests/Http/Parsing/CommentLineParserShould.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using TeaPie.Http.Parsing; + +namespace TeaPie.Tests.Http.Parsing; + +public class CommentLineParserShould +{ + private readonly CommentLineParser _parser = new(); + + private static HttpParsingContext CreateContext() + { + using var requestMessage = new HttpRequestMessage(); + return new HttpParsingContext(requestMessage.Headers); + } + + [Fact] + public void ReturnTrueForHashComment() + { + var context = CreateContext(); + + _parser.CanParse("# this is a comment", context).Should().BeTrue(); + } + + [Fact] + public void ReturnTrueForSlashComment() + { + var context = CreateContext(); + + _parser.CanParse("// this is a comment", context).Should().BeTrue(); + } + + [Fact] + public void ReturnFalseForNonCommentLine() + { + var context = CreateContext(); + + _parser.CanParse("GET https://example.com", context).Should().BeFalse(); + } + + [Fact] + public void ReturnFalseWhenIsBodyIsTrue() + { + var context = CreateContext(); + context.IsBody = true; + + _parser.CanParse("# comment", context).Should().BeFalse(); + } + + [Fact] + public void ExtractRequestNameFromNameDirective() + { + var context = CreateContext(); + + _parser.Parse("# @name MyRequest", context); + + context.RequestName.Should().Be("MyRequest"); + } + + [Fact] + public void NotSetNameForPlainComment() + { + var context = CreateContext(); + + _parser.Parse("# just a plain comment", context); + + context.RequestName.Should().BeEmpty(); + } +} diff --git a/tests/TeaPie.Tests/Http/Parsing/EmptyLineParserShould.cs b/tests/TeaPie.Tests/Http/Parsing/EmptyLineParserShould.cs new file mode 100644 index 00000000..b2f6beb7 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Parsing/EmptyLineParserShould.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using TeaPie.Http.Parsing; + +namespace TeaPie.Tests.Http.Parsing; + +public class EmptyLineParserShould +{ + private readonly EmptyLineParser _parser = new(); + + private static HttpParsingContext CreateContext() + { + using var requestMessage = new HttpRequestMessage(); + return new HttpParsingContext(requestMessage.Headers); + } + + [Fact] + public void ReturnTrueForEmptyString() + { + var context = CreateContext(); + + _parser.CanParse("", context).Should().BeTrue(); + } + + [Fact] + public void ReturnTrueForWhitespaceOnly() + { + var context = CreateContext(); + + _parser.CanParse(" ", context).Should().BeTrue(); + } + + [Fact] + public void ReturnFalseForNonEmptyLine() + { + var context = CreateContext(); + + _parser.CanParse("GET https://example.com", context).Should().BeFalse(); + } + + [Fact] + public void SetIsBodyWhenMethodAndUriResolved() + { + var context = CreateContext(); + context.IsMethodAndUriResolved = true; + + _parser.Parse("", context); + + context.IsBody.Should().BeTrue(); + } + + [Fact] + public void NotSetIsBodyWhenMethodAndUriNotResolved() + { + var context = CreateContext(); + + _parser.Parse("", context); + + context.IsBody.Should().BeFalse(); + } +} diff --git a/tests/TeaPie.Tests/Http/Parsing/HeaderParserShould.cs b/tests/TeaPie.Tests/Http/Parsing/HeaderParserShould.cs new file mode 100644 index 00000000..71fc766b --- /dev/null +++ b/tests/TeaPie.Tests/Http/Parsing/HeaderParserShould.cs @@ -0,0 +1,62 @@ +using System.Data; +using FluentAssertions; +using TeaPie.Http.Parsing; + +namespace TeaPie.Tests.Http.Parsing; + +public class HeaderParserShould +{ + private readonly HeaderParser _parser = new(); + + private static HttpParsingContext CreateContext() + { + using var requestMessage = new HttpRequestMessage(); + return new HttpParsingContext(requestMessage.Headers); + } + + [Fact] + public void ReturnTrueForValidHeaderLine() + { + var context = CreateContext(); + + _parser.CanParse("Content-Type: application/json", context).Should().BeTrue(); + } + + [Fact] + public void ReturnFalseWhenIsBodyIsTrue() + { + var context = CreateContext(); + context.IsBody = true; + + _parser.CanParse("Content-Type: application/json", context).Should().BeFalse(); + } + + [Fact] + public void ReturnFalseForLineWithoutColon() + { + var context = CreateContext(); + + _parser.CanParse("NoColonHere", context).Should().BeFalse(); + } + + [Fact] + public void AddHeaderToContext() + { + var context = CreateContext(); + + _parser.Parse("X-Custom-Header: my-value", context); + + context.Headers.Should().ContainKey("X-Custom-Header"); + context.Headers["X-Custom-Header"].Should().Be("my-value"); + } + + [Fact] + public void ThrowForInvalidHeaderName() + { + var context = CreateContext(); + + var act = () => _parser.Parse("Invalid Header Name: value", context); + + act.Should().Throw(); + } +} diff --git a/tests/TeaPie.Tests/Http/Parsing/MethodAndUriParserShould.cs b/tests/TeaPie.Tests/Http/Parsing/MethodAndUriParserShould.cs new file mode 100644 index 00000000..53e28f2b --- /dev/null +++ b/tests/TeaPie.Tests/Http/Parsing/MethodAndUriParserShould.cs @@ -0,0 +1,119 @@ +using FluentAssertions; +using TeaPie.Http.Parsing; + +namespace TeaPie.Tests.Http.Parsing; + +public class MethodAndUriParserShould +{ + private readonly MethodAndUriParser _parser = new(); + + private static HttpParsingContext CreateContext() + { + using var requestMessage = new HttpRequestMessage(); + return new HttpParsingContext(requestMessage.Headers); + } + + [Fact] + public void ReturnTrueForValidMethodAndUriLine() + { + var context = CreateContext(); + + _parser.CanParse("GET https://example.com", context).Should().BeTrue(); + } + + [Fact] + public void ReturnFalseWhenMethodAndUriAlreadyResolved() + { + var context = CreateContext(); + context.IsMethodAndUriResolved = true; + + _parser.CanParse("GET https://example.com", context).Should().BeFalse(); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void ReturnFalseForEmptyOrWhitespaceLine(string line) + { + var context = CreateContext(); + + _parser.CanParse(line, context).Should().BeFalse(); + } + + [Fact] + public void ReturnFalseForCommentLine() + { + var context = CreateContext(); + + _parser.CanParse("# this is a comment", context).Should().BeFalse(); + } + + [Fact] + public void ReturnFalseForAltCommentLine() + { + var context = CreateContext(); + + _parser.CanParse("// this is a comment", context).Should().BeFalse(); + } + + [Fact] + public void ReturnFalseWhenIsBodyIsTrue() + { + var context = CreateContext(); + context.IsBody = true; + + _parser.CanParse("GET https://example.com", context).Should().BeFalse(); + } + + [Fact] + public void ParseGetMethodAndUriCorrectly() + { + var context = CreateContext(); + + _parser.Parse("GET https://example.com/api", context); + + context.Method.Should().Be(HttpMethod.Get); + context.RequestUri.Should().Be("https://example.com/api"); + } + + [Fact] + public void ParsePostMethodAndUriCorrectly() + { + var context = CreateContext(); + + _parser.Parse("POST https://example.com/api/items", context); + + context.Method.Should().Be(HttpMethod.Post); + context.RequestUri.Should().Be("https://example.com/api/items"); + } + + [Fact] + public void ThrowForSingleWordLine() + { + var context = CreateContext(); + + var act = () => _parser.Parse("GETWITHNOURL", context); + + act.Should().Throw(); + } + + [Fact] + public void ThrowForUnsupportedMethod() + { + var context = CreateContext(); + + var act = () => _parser.Parse("UNKNOWN https://example.com", context); + + act.Should().Throw().WithMessage("*UNKNOWN*"); + } + + [Fact] + public void SetIsMethodAndUriResolvedAfterParsing() + { + var context = CreateContext(); + + _parser.Parse("GET https://example.com", context); + + context.IsMethodAndUriResolved.Should().BeTrue(); + } +} diff --git a/tests/TeaPie.Tests/Scripts/ScriptReferenceShould.cs b/tests/TeaPie.Tests/Scripts/ScriptReferenceShould.cs new file mode 100644 index 00000000..3487be87 --- /dev/null +++ b/tests/TeaPie.Tests/Scripts/ScriptReferenceShould.cs @@ -0,0 +1,33 @@ +using FluentAssertions; +using TeaPie.Scripts; + +namespace TeaPie.Tests.Scripts; + +public class ScriptReferenceShould +{ + [Fact] + public void SetPropertiesCorrectly() + { + var reference = new ScriptReference("/real/path.csx", "/temp/path.csx"); + + reference.RealPath.Should().Be("/real/path.csx"); + reference.TempPath.Should().Be("/temp/path.csx"); + } + + [Fact] + public void DefaultIsExternalToFalse() + { + var reference = new ScriptReference("/real/path.csx", "/temp/path.csx"); + + reference.IsExternal.Should().BeFalse(); + } + + [Fact] + public void SupportRecordEquality() + { + var ref1 = new ScriptReference("/real/path.csx", "/temp/path.csx", true); + var ref2 = new ScriptReference("/real/path.csx", "/temp/path.csx", true); + + ref1.Should().Be(ref2); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/ExternalFileShould.cs b/tests/TeaPie.Tests/StructureExploration/ExternalFileShould.cs new file mode 100644 index 00000000..22684e19 --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/ExternalFileShould.cs @@ -0,0 +1,23 @@ +using FluentAssertions; +using TeaPie.StructureExploration; + +namespace TeaPie.Tests.StructureExploration; + +public class ExternalFileShould +{ + [Fact] + public void SetPathCorrectly() + { + var file = new ExternalFile("/external/path/script.csx"); + + file.Path.Should().Be("/external/path/script.csx"); + } + + [Fact] + public void ExtractNameCorrectly() + { + var file = new ExternalFile("/external/path/script.csx"); + + file.Name.Should().Be("script.csx"); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/FileShould.cs b/tests/TeaPie.Tests/StructureExploration/FileShould.cs new file mode 100644 index 00000000..058cbee5 --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/FileShould.cs @@ -0,0 +1,54 @@ +using FluentAssertions; +using StructureFile = TeaPie.StructureExploration.File; + +namespace TeaPie.Tests.StructureExploration; + +public class FileShould +{ + [Theory] + [InlineData("/home/user/test.http", "test.http")] + [InlineData("folder/subfolder/file.csx", "file.csx")] + public void ExtractFileNameFromPath(string path, string expectedName) + { + var file = new StructureFile(path); + + file.Name.Should().Be(expectedName); + } + + [Fact] + public void ReturnPathWhenRelativePathIsEmpty() + { + var file = new StructureFile("/home/user/test.http"); + + file.GetDisplayPath().Should().Be("/home/user/test.http"); + } + + [Fact] + public void ReturnRelativePathWhenNotEmpty() + { + var file = new StructureFile("/home/user/test.http", "user/test.http"); + + file.GetDisplayPath().Should().Be("user/test.http"); + } + + [Fact] + public void ReturnTrueWhenFileIsUnderRoot() + { + StructureFile.BelongsTo("/home/user/project/test.http", "/home/user/project") + .Should().BeTrue(); + } + + [Fact] + public void ReturnFalseWhenFileIsNotUnderRoot() + { + StructureFile.BelongsTo("/other/path/test.http", "/home/user/project") + .Should().BeFalse(); + } + + [Fact] + public void TrimWhitespaceInBelongsTo() + { + StructureFile.BelongsTo(" /home/user/project/test.http", " /home/user/project") + .Should().BeTrue(); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/FolderShould.cs b/tests/TeaPie.Tests/StructureExploration/FolderShould.cs new file mode 100644 index 00000000..f81d1f03 --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/FolderShould.cs @@ -0,0 +1,34 @@ +using FluentAssertions; +using TeaPie.StructureExploration; + +namespace TeaPie.Tests.StructureExploration; + +public class FolderShould +{ + [Fact] + public void SetPropertiesCorrectly() + { + var folder = new Folder("/home/user/project", "project", "project"); + + folder.Path.Should().Be("/home/user/project"); + folder.RelativePath.Should().Be("project"); + folder.Name.Should().Be("project"); + } + + [Fact] + public void SupportRecordEquality() + { + var folder1 = new Folder("/path", "rel", "name"); + var folder2 = new Folder("/path", "rel", "name"); + + folder1.Should().Be(folder2); + } + + [Fact] + public void AllowNullParentFolder() + { + var folder = new Folder("/path", "rel", "name", null); + + folder.ParentFolder.Should().BeNull(); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/InternalFileShould.cs b/tests/TeaPie.Tests/StructureExploration/InternalFileShould.cs new file mode 100644 index 00000000..45e8efb2 --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/InternalFileShould.cs @@ -0,0 +1,41 @@ +using FluentAssertions; +using TeaPie.StructureExploration; + +namespace TeaPie.Tests.StructureExploration; + +public class InternalFileShould +{ + [Fact] + public void SetPropertiesCorrectlyViaCreate() + { + var folder = new Folder("/home/user/project", "project", "project"); + + var file = InternalFile.Create("/home/user/project/test-req.http", folder); + + file.Path.Should().Be("/home/user/project/test-req.http"); + file.Name.Should().Be("test-req.http"); + file.ParentFolder.Should().Be(folder); + } + + [Fact] + public void ComputeRelativePathFromFolderAndFileName() + { + var folder = new Folder("/home/user/project/sub", "project/sub", "sub"); + + var file = InternalFile.Create("/home/user/project/sub/request.http", folder); + + file.RelativePath.Should().Be(System.IO.Path.Combine("project/sub", "request.http")); + } + + [Fact] + public void SetParentFolderCorrectly() + { + var parent = new Folder("/root", "root", "root"); + var folder = new Folder("/root/child", "root/child", "child", parent); + + var file = InternalFile.Create("/root/child/file.http", folder); + + file.ParentFolder.Should().Be(folder); + file.ParentFolder.ParentFolder.Should().Be(parent); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/ScriptShould.cs b/tests/TeaPie.Tests/StructureExploration/ScriptShould.cs new file mode 100644 index 00000000..9f5e34b4 --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/ScriptShould.cs @@ -0,0 +1,18 @@ +using FluentAssertions; +using TeaPie.StructureExploration; +using StructureFile = TeaPie.StructureExploration.File; + +namespace TeaPie.Tests.StructureExploration; + +public class ScriptShould +{ + [Fact] + public void SetFilePropertyCorrectly() + { + var file = new StructureFile("/home/user/script.csx", "script.csx"); + var script = new Script(file); + + script.File.Should().Be(file); + script.File.Name.Should().Be("script.csx"); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/TestCaseShould.cs b/tests/TeaPie.Tests/StructureExploration/TestCaseShould.cs new file mode 100644 index 00000000..1429d95c --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/TestCaseShould.cs @@ -0,0 +1,54 @@ +using FluentAssertions; +using TeaPie.StructureExploration; + +namespace TeaPie.Tests.StructureExploration; + +public class TestCaseShould +{ + private static InternalFile CreateRequestFile(string fileName = "MyTest-req.http") + { + var folder = new Folder("/home/user/project", "project", "project"); + return InternalFile.Create($"/home/user/project/{fileName}", folder); + } + + [Fact] + public void TrimRequestSuffixAndExtensionFromName() + { + var requestFile = CreateRequestFile("MyTest-req.http"); + + var testCase = new TestCase(requestFile); + + testCase.Name.Should().Be("MyTest"); + } + + [Fact] + public void SetParentFolderFromRequestFile() + { + var requestFile = CreateRequestFile(); + + var testCase = new TestCase(requestFile); + + testCase.ParentFolder.Should().Be(requestFile.ParentFolder); + } + + [Fact] + public void SetRequestsFile() + { + var requestFile = CreateRequestFile(); + + var testCase = new TestCase(requestFile); + + testCase.RequestsFile.Should().Be(requestFile); + } + + [Fact] + public void HaveEmptyScriptCollectionsInitially() + { + var requestFile = CreateRequestFile(); + + var testCase = new TestCase(requestFile); + + testCase.PreRequestScripts.Should().BeEmpty(); + testCase.PostResponseScripts.Should().BeEmpty(); + } +} diff --git a/tests/TeaPie.Tests/Testing/CollectionTestResultsSummaryShould.cs b/tests/TeaPie.Tests/Testing/CollectionTestResultsSummaryShould.cs new file mode 100644 index 00000000..df10fdaf --- /dev/null +++ b/tests/TeaPie.Tests/Testing/CollectionTestResultsSummaryShould.cs @@ -0,0 +1,81 @@ +using FluentAssertions; +using TeaPie.Testing; + +namespace TeaPie.Tests.Testing; + +public class CollectionTestResultsSummaryShould +{ + [Fact] + public void SetNameFromConstructor() + { + var summary = new CollectionTestResultsSummary("MyCollection"); + + summary.Name.Should().Be("MyCollection"); + } + + [Fact] + public void CreateTestCaseEntryAndAddPassedResult() + { + var summary = new CollectionTestResultsSummary("col"); + var passed = new TestResult.Passed(100) { TestName = "test1", TestCasePath = "" }; + + summary.AddPassedTest("TestCaseA", passed); + + summary.TestCases.Should().ContainKey("TestCaseA"); + summary.TestCases["TestCaseA"].PassedTests.Should().ContainSingle().Which.Should().Be(passed); + summary.NumberOfPassedTests.Should().Be(1); + } + + [Fact] + public void CreateTestCaseEntryAndAddFailedResult() + { + var summary = new CollectionTestResultsSummary("col"); + var failed = new TestResult.Failed(200, "error", null) { TestName = "test2", TestCasePath = "" }; + + summary.AddFailedTest("TestCaseB", failed); + + summary.TestCases.Should().ContainKey("TestCaseB"); + summary.TestCases["TestCaseB"].FailedTests.Should().ContainSingle().Which.Should().Be(failed); + summary.NumberOfFailedTests.Should().Be(1); + } + + [Fact] + public void CreateTestCaseEntryAndAddSkippedResult() + { + var summary = new CollectionTestResultsSummary("col"); + var skipped = new TestResult.NotRun() { TestName = "test3", TestCasePath = "" }; + + summary.AddSkippedTest("TestCaseC", skipped); + + summary.TestCases.Should().ContainKey("TestCaseC"); + summary.TestCases["TestCaseC"].SkippedTests.Should().ContainSingle().Which.Should().Be(skipped); + summary.NumberOfSkippedTests.Should().Be(1); + } + + [Fact] + public void AddMultipleResultsToSameTestCase() + { + var summary = new CollectionTestResultsSummary("col"); + var passed1 = new TestResult.Passed(100) { TestName = "t1", TestCasePath = "" }; + var passed2 = new TestResult.Passed(200) { TestName = "t2", TestCasePath = "" }; + + summary.AddPassedTest("SameCase", passed1); + summary.AddPassedTest("SameCase", passed2); + + summary.TestCases.Should().HaveCount(1); + summary.TestCases["SameCase"].PassedTests.Should().HaveCount(2); + summary.NumberOfPassedTests.Should().Be(2); + } + + [Fact] + public void ContainExpectedTestCaseEntries() + { + var summary = new CollectionTestResultsSummary("col"); + summary.AddPassedTest("CaseA", new TestResult.Passed(10) { TestName = "t1", TestCasePath = "" }); + summary.AddFailedTest("CaseB", new TestResult.Failed(20, "err", null) { TestName = "t2", TestCasePath = "" }); + summary.AddSkippedTest("CaseC", new TestResult.NotRun() { TestName = "t3", TestCasePath = "" }); + + summary.TestCases.Should().HaveCount(3); + summary.TestCases.Keys.Should().Contain(["CaseA", "CaseB", "CaseC"]); + } +} diff --git a/tests/TeaPie.Tests/Testing/TestCaseTestResultsSummaryShould.cs b/tests/TeaPie.Tests/Testing/TestCaseTestResultsSummaryShould.cs new file mode 100644 index 00000000..3d5c75f3 --- /dev/null +++ b/tests/TeaPie.Tests/Testing/TestCaseTestResultsSummaryShould.cs @@ -0,0 +1,27 @@ +using FluentAssertions; +using TeaPie.Testing; + +namespace TeaPie.Tests.Testing; + +public class TestCaseTestResultsSummaryShould +{ + [Fact] + public void SetNameFromConstructor() + { + var summary = new TestCaseTestResultsSummary("MyTestCase"); + + summary.Name.Should().Be("MyTestCase"); + } + + [Fact] + public void AddPassedTestViaInheritedBehavior() + { + var summary = new TestCaseTestResultsSummary("tc"); + var passed = new TestResult.Passed(150) { TestName = "test1", TestCasePath = "" }; + + summary.AddPassedTest(passed); + + summary.NumberOfPassedTests.Should().Be(1); + summary.PassedTests.Should().ContainSingle().Which.Should().Be(passed); + } +} diff --git a/tests/TeaPie.Tests/Testing/TestResultsSummaryShould.cs b/tests/TeaPie.Tests/Testing/TestResultsSummaryShould.cs new file mode 100644 index 00000000..17b5654c --- /dev/null +++ b/tests/TeaPie.Tests/Testing/TestResultsSummaryShould.cs @@ -0,0 +1,172 @@ +using FluentAssertions; +using TeaPie.Testing; + +namespace TeaPie.Tests.Testing; + +public class TestResultsSummaryShould +{ + [Fact] + public void SetTimestampOnStart() + { + var summary = new TestResultsSummary(); + var before = DateTime.Now; + + summary.Start(); + + summary.Timestamp.Should().BeOnOrAfter(before); + summary.Timestamp.Should().BeOnOrBefore(DateTime.Now); + } + + [Fact] + public void IncrementSkippedCountAndAddToCollections() + { + var summary = new TestResultsSummary(); + var skipped = new TestResult.NotRun() { TestName = "skipped1", TestCasePath = "" }; + + summary.AddSkippedTest(skipped); + + summary.NumberOfSkippedTests.Should().Be(1); + summary.SkippedTests.Should().ContainSingle().Which.Should().Be(skipped); + summary.TestResults.Should().ContainSingle().Which.Should().Be(skipped); + } + + [Fact] + public void IncrementPassedCountAndAddDurationAndCollections() + { + var summary = new TestResultsSummary(); + var passed = new TestResult.Passed(100) { TestName = "passed1", TestCasePath = "" }; + + summary.AddPassedTest(passed); + + summary.NumberOfPassedTests.Should().Be(1); + summary.TimeElapsedDuringTesting.Should().Be(100); + summary.PassedTests.Should().ContainSingle().Which.Should().Be(passed); + summary.TestResults.Should().ContainSingle().Which.Should().Be(passed); + } + + [Fact] + public void IncrementFailedCountAndAddDurationAndCollections() + { + var summary = new TestResultsSummary(); + var failed = new TestResult.Failed(200, "error", null) { TestName = "failed1", TestCasePath = "" }; + + summary.AddFailedTest(failed); + + summary.NumberOfFailedTests.Should().Be(1); + summary.TimeElapsedDuringTesting.Should().Be(200); + summary.FailedTests.Should().ContainSingle().Which.Should().Be(failed); + summary.TestResults.Should().ContainSingle().Which.Should().Be(failed); + } + + [Fact] + public void ReturnSumOfAllTestsForNumberOfTests() + { + var summary = new TestResultsSummary(); + summary.AddSkippedTest(new TestResult.NotRun() { TestName = "s1", TestCasePath = "" }); + summary.AddPassedTest(new TestResult.Passed(10) { TestName = "p1", TestCasePath = "" }); + summary.AddFailedTest(new TestResult.Failed(20, "err", null) { TestName = "f1", TestCasePath = "" }); + + summary.NumberOfTests.Should().Be(3); + } + + [Fact] + public void ReturnPassedPlusFailedForNumberOfExecutedTests() + { + var summary = new TestResultsSummary(); + summary.AddSkippedTest(new TestResult.NotRun() { TestName = "s1", TestCasePath = "" }); + summary.AddPassedTest(new TestResult.Passed(10) { TestName = "p1", TestCasePath = "" }); + summary.AddFailedTest(new TestResult.Failed(20, "err", null) { TestName = "f1", TestCasePath = "" }); + + summary.NumberOfExecutedTests.Should().Be(2); + } + + [Fact] + public void ReturnTrueForAllTestsPassedWhenOnlyPassedTests() + { + var summary = new TestResultsSummary(); + summary.AddPassedTest(new TestResult.Passed(10) { TestName = "p1", TestCasePath = "" }); + summary.AddPassedTest(new TestResult.Passed(20) { TestName = "p2", TestCasePath = "" }); + + summary.AllTestsPassed.Should().BeTrue(); + } + + [Fact] + public void ReturnFalseForAllTestsPassedWhenThereAreFailedTests() + { + var summary = new TestResultsSummary(); + summary.AddPassedTest(new TestResult.Passed(10) { TestName = "p1", TestCasePath = "" }); + summary.AddFailedTest(new TestResult.Failed(20, "err", null) { TestName = "f1", TestCasePath = "" }); + + summary.AllTestsPassed.Should().BeFalse(); + } + + [Fact] + public void ReturnTrueForHasSkippedTestsWhenSkippedGreaterThanZero() + { + var summary = new TestResultsSummary(); + summary.AddSkippedTest(new TestResult.NotRun() { TestName = "s1", TestCasePath = "" }); + + summary.HasSkippedTests.Should().BeTrue(); + } + + [Fact] + public void ReturnFalseForHasSkippedTestsWhenZeroSkipped() + { + var summary = new TestResultsSummary(); + summary.AddPassedTest(new TestResult.Passed(10) { TestName = "p1", TestCasePath = "" }); + + summary.HasSkippedTests.Should().BeFalse(); + } + + [Fact] + public void CalculatePercentageOfPassedTestsCorrectly() + { + var summary = new TestResultsSummary(); + summary.AddPassedTest(new TestResult.Passed(10) { TestName = "p1", TestCasePath = "" }); + summary.AddFailedTest(new TestResult.Failed(20, "err", null) { TestName = "f1", TestCasePath = "" }); + + summary.PercentageOfPassedTests.Should().Be(50.0); + } + + [Fact] + public void CalculatePercentageOfFailedTestsCorrectly() + { + var summary = new TestResultsSummary(); + summary.AddPassedTest(new TestResult.Passed(10) { TestName = "p1", TestCasePath = "" }); + summary.AddFailedTest(new TestResult.Failed(20, "err", null) { TestName = "f1", TestCasePath = "" }); + + summary.PercentageOfFailedTests.Should().Be(50.0); + } + + [Fact] + public void CalculatePercentageOfSkippedTestsCorrectly() + { + var summary = new TestResultsSummary(); + summary.AddSkippedTest(new TestResult.NotRun() { TestName = "s1", TestCasePath = "" }); + summary.AddPassedTest(new TestResult.Passed(10) { TestName = "p1", TestCasePath = "" }); + summary.AddFailedTest(new TestResult.Failed(20, "err", null) { TestName = "f1", TestCasePath = "" }); + + summary.PercentageOfSkippedTests.Should().BeApproximately(33.33, 0.01); + } + + [Fact] + public void ReturnZeroPercentagesWhenNoTests() + { + var summary = new TestResultsSummary(); + + summary.PercentageOfPassedTests.Should().Be(0.0); + summary.PercentageOfFailedTests.Should().Be(0.0); + summary.PercentageOfSkippedTests.Should().Be(0.0); + } + + [Fact] + public void SumPassedAndFailedDurationsForTimeElapsed() + { + var summary = new TestResultsSummary(); + summary.AddPassedTest(new TestResult.Passed(100) { TestName = "p1", TestCasePath = "" }); + summary.AddPassedTest(new TestResult.Passed(200) { TestName = "p2", TestCasePath = "" }); + summary.AddFailedTest(new TestResult.Failed(50, "err", null) { TestName = "f1", TestCasePath = "" }); + + summary.TimeElapsedDuringTesting.Should().Be(350); + } +} From 06ccb70acee5b614586c6c412a8cd23c125e9a7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:12:01 +0000 Subject: [PATCH 07/11] Add unit tests for ApplicationContextOptions, Builder, and directive pattern builders - ApplicationContextOptionsBuilderShould: 8 tests covering defaults, setters, fluent chaining, null handling - ApplicationContextOptionsShould: 5 tests covering constructors, null defaults, property mutability - BaseDirectivePatternBuilderShould: 8 tests covering pattern building, params, prefix, auto-naming - HttpDirectivePatternBuilderShould: 4 tests covering factory, prefix, params, sample directive matching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- .../ApplicationContextOptionsBuilderShould.cs | 67 +++++++++++++++ .../ApplicationContextOptionsShould.cs | 57 +++++++++++++ .../BaseDirectivePatternBuilderShould.cs | 83 +++++++++++++++++++ .../HttpDirectivePatternBuilderShould.cs | 43 ++++++++++ 4 files changed, 250 insertions(+) create mode 100644 tests/TeaPie.Tests/ApplicationContextOptionsBuilderShould.cs create mode 100644 tests/TeaPie.Tests/ApplicationContextOptionsShould.cs create mode 100644 tests/TeaPie.Tests/Http/Parsing/BaseDirectivePatternBuilderShould.cs create mode 100644 tests/TeaPie.Tests/Http/Parsing/HttpDirectivePatternBuilderShould.cs diff --git a/tests/TeaPie.Tests/ApplicationContextOptionsBuilderShould.cs b/tests/TeaPie.Tests/ApplicationContextOptionsBuilderShould.cs new file mode 100644 index 00000000..65053fd3 --- /dev/null +++ b/tests/TeaPie.Tests/ApplicationContextOptionsBuilderShould.cs @@ -0,0 +1,67 @@ +using FluentAssertions; + +namespace TeaPie.Tests; + +public class ApplicationContextOptionsBuilderShould +{ + [Fact] + public void Build_ReturnsDefaultOptions_WhenNothingSet() + { + var options = new ApplicationContextOptionsBuilder().Build(); + + options.TempFolderPath.Should().Be(Constants.SystemTemporaryFolderPath); + options.Environment.Should().Be(string.Empty); + options.EnvironmentFilePath.Should().Be(string.Empty); + options.ReportFilePath.Should().Be(string.Empty); + options.InitializationScriptPath.Should().Be(string.Empty); + options.CacheVariables.Should().BeTrue(); + } + + [Fact] + public void SetEnvironment_SetsEnvironment() => + new ApplicationContextOptionsBuilder() + .SetEnvironment("dev") + .Build() + .Environment.Should().Be("dev"); + + [Fact] + public void SetReportFilePath_SetsReportFilePath() => + new ApplicationContextOptionsBuilder() + .SetReportFilePath("report.xml") + .Build() + .ReportFilePath.Should().Be("report.xml"); + + [Fact] + public void SetVariablesCaching_SetsCachingFlag() => + new ApplicationContextOptionsBuilder() + .SetVariablesCaching(false) + .Build() + .CacheVariables.Should().BeFalse(); + + [Fact] + public void EachSetter_ReturnsBuilder_ForFluentChaining() + { + var builder = new ApplicationContextOptionsBuilder(); + builder.SetEnvironment("dev").Should().BeSameAs(builder); + } + + [Fact] + public void SetTempFolderPath_WithNull_UsesEmptyString() => + new ApplicationContextOptionsBuilder() + .SetTempFolderPath(null) + .Build() + .TempFolderPath.Should().Be(string.Empty); + + [Fact] + public void SetEnvironment_WithNull_UsesEmptyString() => + new ApplicationContextOptionsBuilder() + .SetEnvironment(null) + .Build() + .Environment.Should().Be(string.Empty); + + [Fact] + public void Build_WithNoTempPath_UsesConstantsSystemTemporaryFolderPath() => + new ApplicationContextOptionsBuilder() + .Build() + .TempFolderPath.Should().Be(Constants.SystemTemporaryFolderPath); +} diff --git a/tests/TeaPie.Tests/ApplicationContextOptionsShould.cs b/tests/TeaPie.Tests/ApplicationContextOptionsShould.cs new file mode 100644 index 00000000..8c750f16 --- /dev/null +++ b/tests/TeaPie.Tests/ApplicationContextOptionsShould.cs @@ -0,0 +1,57 @@ +using FluentAssertions; + +namespace TeaPie.Tests; + +public class ApplicationContextOptionsShould +{ + [Fact] + public void DefaultConstructor_SetsEmptyStringsAndCacheVariablesTrue() + { + var options = new ApplicationContextOptions(); + + options.TempFolderPath.Should().Be(string.Empty); + options.Environment.Should().Be(string.Empty); + options.EnvironmentFilePath.Should().Be(string.Empty); + options.ReportFilePath.Should().Be(string.Empty); + options.InitializationScriptPath.Should().Be(string.Empty); + options.CacheVariables.Should().BeTrue(); + } + + [Fact] + public void Constructor_WithValues_SetsPropertiesCorrectly() + { + var options = new ApplicationContextOptions( + "/temp", "prod", "env.json", "report.xml", "init.csx", false); + + options.TempFolderPath.Should().Be("/temp"); + options.Environment.Should().Be("prod"); + options.EnvironmentFilePath.Should().Be("env.json"); + options.ReportFilePath.Should().Be("report.xml"); + options.InitializationScriptPath.Should().Be("init.csx"); + options.CacheVariables.Should().BeFalse(); + } + + [Fact] + public void NullParameters_DefaultToEmptyStrings() + { + var options = new ApplicationContextOptions(null, null, null, null, null); + + options.TempFolderPath.Should().Be(string.Empty); + options.Environment.Should().Be(string.Empty); + options.EnvironmentFilePath.Should().Be(string.Empty); + options.ReportFilePath.Should().Be(string.Empty); + options.InitializationScriptPath.Should().Be(string.Empty); + } + + [Fact] + public void CacheVariables_DefaultsToTrue() => + new ApplicationContextOptions().CacheVariables.Should().BeTrue(); + + [Fact] + public void Properties_AreSettable() + { + var options = new ApplicationContextOptions(); + options.TempFolderPath = "new"; + options.TempFolderPath.Should().Be("new"); + } +} diff --git a/tests/TeaPie.Tests/Http/Parsing/BaseDirectivePatternBuilderShould.cs b/tests/TeaPie.Tests/Http/Parsing/BaseDirectivePatternBuilderShould.cs new file mode 100644 index 00000000..71250181 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Parsing/BaseDirectivePatternBuilderShould.cs @@ -0,0 +1,83 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using TeaPie.Http.Parsing; + +namespace TeaPie.Tests.Http.Parsing; + +public class BaseDirectivePatternBuilderShould +{ + [Fact] + public void Build_WithNoParams_ProducesPatternMatchingDirective() + { + var pattern = new BaseDirectivePatternBuilder("MY-DIRECTIVE").Build(); + Regex.IsMatch("## MY-DIRECTIVE", pattern).Should().BeTrue(); + } + + [Fact] + public void Build_WithStringParameter_ProducesPatternWithStringCapture() + { + var pattern = new BaseDirectivePatternBuilder("MY-DIRECTIVE") + .AddStringParameter("Value") + .Build(); + + Regex.IsMatch("## MY-DIRECTIVE: someValue", pattern).Should().BeTrue(); + } + + [Fact] + public void Build_WithBooleanParameter_ProducesPatternWithBoolCapture() + { + var pattern = new BaseDirectivePatternBuilder("MY-DIRECTIVE") + .AddBooleanParameter("Flag") + .Build(); + + Regex.IsMatch("## MY-DIRECTIVE: true", pattern).Should().BeTrue(); + } + + [Fact] + public void Build_WithNumberParameter_ProducesPatternWithNumberCapture() + { + var pattern = new BaseDirectivePatternBuilder("MY-DIRECTIVE") + .AddNumberParameter("Count") + .Build(); + + Regex.IsMatch("## MY-DIRECTIVE: 42", pattern).Should().BeTrue(); + } + + [Fact] + public void Build_WithPrefix_AddsPrefixToDirectiveName() + { + var pattern = new BaseDirectivePatternBuilder("NAME", "PRE-").Build(); + Regex.IsMatch("## PRE-NAME", pattern).Should().BeTrue(); + } + + [Fact] + public void AddParameter_WithNullName_AutoGeneratesName() + { + var pattern = new BaseDirectivePatternBuilder("MY-DIRECTIVE") + .AddStringParameter() + .Build(); + + pattern.Should().Contain("Parameter1"); + } + + [Fact] + public void MultipleParameters_JoinedWithSeparator() + { + var pattern = new BaseDirectivePatternBuilder("MY-DIRECTIVE") + .AddStringParameter("P1") + .AddStringParameter("P2") + .Build(); + + Regex.IsMatch("## MY-DIRECTIVE: val1; val2", pattern).Should().BeTrue(); + } + + [Fact] + public void Verify_PatternMatches_ActualDirectiveLine() + { + var pattern = new BaseDirectivePatternBuilder("AUTH-PROVIDER") + .AddStringParameter("Provider") + .Build(); + + Regex.IsMatch("## AUTH-PROVIDER: OAuth2", pattern).Should().BeTrue(); + } +} diff --git a/tests/TeaPie.Tests/Http/Parsing/HttpDirectivePatternBuilderShould.cs b/tests/TeaPie.Tests/Http/Parsing/HttpDirectivePatternBuilderShould.cs new file mode 100644 index 00000000..de5401cc --- /dev/null +++ b/tests/TeaPie.Tests/Http/Parsing/HttpDirectivePatternBuilderShould.cs @@ -0,0 +1,43 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using TeaPie.Http.Parsing; + +namespace TeaPie.Tests.Http.Parsing; + +public class HttpDirectivePatternBuilderShould +{ + [Fact] + public void Create_ReturnsNewBuilder() => + HttpDirectivePatternBuilder.Create("TEST").Should().NotBeNull(); + + [Fact] + public void WithPrefix_SetsPrefix() + { + var pattern = HttpDirectivePatternBuilder.Create("NAME") + .WithPrefix("PRE-") + .Build(); + + Regex.IsMatch("## PRE-NAME", pattern).Should().BeTrue(); + } + + [Fact] + public void Build_WithPrefixAndParameters_ProducesCorrectPattern() + { + var pattern = HttpDirectivePatternBuilder.Create("NAME") + .WithPrefix("PRE-") + .AddStringParameter("Param") + .Build(); + + Regex.IsMatch("## PRE-NAME: someValue", pattern).Should().BeTrue(); + } + + [Fact] + public void Verify_BuiltPattern_MatchesSampleDirectiveLine() + { + var pattern = HttpDirectivePatternBuilder.Create("AUTH-PROVIDER") + .AddStringParameter("Provider") + .Build(); + + Regex.IsMatch("## AUTH-PROVIDER: OAuth2", pattern).Should().BeTrue(); + } +} From f7de46a4a8ecae3cf0964dc40beb61c7c52d6efc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:16:05 +0000 Subject: [PATCH 08/11] Add unit tests for HTTP header handler classes Add test files for DefaultHeaderHandler, AuthorizationHeaderHandler, HostHeaderHandler, UserAgentHeaderHandler, DateHeaderHandler, ConnectionHeaderHandler, ContentTypeHeaderHandler, ContentEncodingHeaderHandler, ContentLanguageHeaderHandler, and ContentDispositionHeaderHandler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- .../AuthorizationHeaderHandlerShould.cs | 51 +++++++++++++++++++ .../Headers/ConnectionHeaderHandlerShould.cs | 51 +++++++++++++++++++ .../ContentDispositionHeaderHandlerShould.cs | 39 ++++++++++++++ .../ContentEncodingHeaderHandlerShould.cs | 38 ++++++++++++++ .../ContentLanguageHeaderHandlerShould.cs | 38 ++++++++++++++ .../Headers/ContentTypeHeaderHandlerShould.cs | 43 ++++++++++++++++ .../Http/Headers/DateHeaderHandlerShould.cs | 46 +++++++++++++++++ .../Headers/DefaultHeaderHandlerShould.cs | 48 +++++++++++++++++ .../Http/Headers/HostHeaderHandlerShould.cs | 44 ++++++++++++++++ .../Headers/UserAgentHeaderHandlerShould.cs | 36 +++++++++++++ 10 files changed, 434 insertions(+) create mode 100644 tests/TeaPie.Tests/Http/Headers/AuthorizationHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/ConnectionHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/ContentDispositionHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/ContentEncodingHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/ContentLanguageHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/ContentTypeHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/DateHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/HostHeaderHandlerShould.cs create mode 100644 tests/TeaPie.Tests/Http/Headers/UserAgentHeaderHandlerShould.cs diff --git a/tests/TeaPie.Tests/Http/Headers/AuthorizationHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/AuthorizationHeaderHandlerShould.cs new file mode 100644 index 00000000..773e976b --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/AuthorizationHeaderHandlerShould.cs @@ -0,0 +1,51 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class AuthorizationHeaderHandlerShould +{ + private readonly AuthorizationHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_WithBearerToken_SetsAuthorizationCorrectly() + { + var request = new HttpRequestMessage(); + + _handler.SetHeader("Bearer token123", request); + + request.Headers.Authorization.Should().NotBeNull(); + request.Headers.Authorization!.Scheme.Should().Be("Bearer"); + request.Headers.Authorization.Parameter.Should().Be("token123"); + } + + [Fact] + public void SetHeader_WithSingleWord_Throws() + { + var request = new HttpRequestMessage(); + + var act = () => _handler.SetHeader("BearerOnly", request); + + act.Should().Throw(); + } + + [Fact] + public void GetHeader_ReturnsAuthorizationString() + { + var request = new HttpRequestMessage(); + _handler.SetHeader("Bearer token123", request); + + _handler.GetHeader(request).Should().Be("Bearer token123"); + } + + [Fact] + public void GetHeader_ReturnsEmpty_WhenNotSet() + { + var request = new HttpRequestMessage(); + + _handler.GetHeader(request).Should().BeEmpty(); + } + + [Fact] + public void HeaderName_IsAuthorization() => _handler.HeaderName.Should().Be("Authorization"); +} diff --git a/tests/TeaPie.Tests/Http/Headers/ConnectionHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/ConnectionHeaderHandlerShould.cs new file mode 100644 index 00000000..96258448 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/ConnectionHeaderHandlerShould.cs @@ -0,0 +1,51 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class ConnectionHeaderHandlerShould +{ + private readonly ConnectionHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_WithKeepAlive_SetsHeader() + { + var request = new HttpRequestMessage(); + + _handler.SetHeader("keep-alive", request); + + request.Headers.TryGetValues("Connection", out var values).Should().BeTrue(); + values.Should().Contain("keep-alive"); + } + + [Fact] + public void SetHeader_WithClose_SetsConnectionClose() + { + var request = new HttpRequestMessage(); + + _handler.SetHeader("close", request); + + request.Headers.ConnectionClose.Should().BeTrue(); + } + + [Fact] + public void SetHeader_WithKeepAliveAndClose_HandlesBoth() + { + var request = new HttpRequestMessage(); + + _handler.SetHeader("keep-alive, close", request); + + request.Headers.ConnectionClose.Should().BeTrue(); + _handler.GetHeader(request).Should().Contain("keep-alive"); + _handler.GetHeader(request).Should().Contain("close"); + } + + [Fact] + public void GetHeader_IncludesClose_WhenConnectionCloseSet() + { + var request = new HttpRequestMessage(); + request.Headers.ConnectionClose = true; + + _handler.GetHeader(request).Should().Contain("close"); + } +} diff --git a/tests/TeaPie.Tests/Http/Headers/ContentDispositionHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/ContentDispositionHeaderHandlerShould.cs new file mode 100644 index 00000000..4e6b5b94 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/ContentDispositionHeaderHandlerShould.cs @@ -0,0 +1,39 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class ContentDispositionHeaderHandlerShould +{ + private readonly ContentDispositionHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_SetsContentDisposition() + { + var request = new HttpRequestMessage { Content = new StringContent("body") }; + + _handler.SetHeader("attachment", request); + + request.Content.Headers.ContentDisposition.Should().NotBeNull(); + request.Content.Headers.ContentDisposition!.DispositionType.Should().Be("attachment"); + } + + [Fact] + public void GetHeader_ReturnsContentDisposition() + { + var request = new HttpRequestMessage { Content = new StringContent("body") }; + _handler.SetHeader("attachment", request); + + _handler.GetHeader(request).Should().Contain("attachment"); + } + + [Fact] + public void SetHeader_Throws_WhenContentIsNull() + { + var request = new HttpRequestMessage(); + + var act = () => _handler.SetHeader("attachment", request); + + act.Should().Throw(); + } +} diff --git a/tests/TeaPie.Tests/Http/Headers/ContentEncodingHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/ContentEncodingHeaderHandlerShould.cs new file mode 100644 index 00000000..cfe714aa --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/ContentEncodingHeaderHandlerShould.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class ContentEncodingHeaderHandlerShould +{ + private readonly ContentEncodingHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_AddsEncoding() + { + var request = new HttpRequestMessage { Content = new StringContent("body") }; + + _handler.SetHeader("gzip", request); + + request.Content.Headers.ContentEncoding.Should().Contain("gzip"); + } + + [Fact] + public void GetHeader_ReturnsEncoding() + { + var request = new HttpRequestMessage { Content = new StringContent("body") }; + request.Content.Headers.ContentEncoding.Add("gzip"); + + _handler.GetHeader(request).Should().Be("gzip"); + } + + [Fact] + public void SetHeader_Throws_WhenContentIsNull() + { + var request = new HttpRequestMessage(); + + var act = () => _handler.SetHeader("gzip", request); + + act.Should().Throw(); + } +} diff --git a/tests/TeaPie.Tests/Http/Headers/ContentLanguageHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/ContentLanguageHeaderHandlerShould.cs new file mode 100644 index 00000000..07d937e6 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/ContentLanguageHeaderHandlerShould.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class ContentLanguageHeaderHandlerShould +{ + private readonly ContentLanguageHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_AddsLanguage() + { + var request = new HttpRequestMessage { Content = new StringContent("body") }; + + _handler.SetHeader("en-US", request); + + request.Content.Headers.ContentLanguage.Should().Contain("en-US"); + } + + [Fact] + public void GetHeader_ReturnsLanguage() + { + var request = new HttpRequestMessage { Content = new StringContent("body") }; + request.Content.Headers.ContentLanguage.Add("en-US"); + + _handler.GetHeader(request).Should().Be("en-US"); + } + + [Fact] + public void SetHeader_Throws_WhenContentIsNull() + { + var request = new HttpRequestMessage(); + + var act = () => _handler.SetHeader("en-US", request); + + act.Should().Throw(); + } +} diff --git a/tests/TeaPie.Tests/Http/Headers/ContentTypeHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/ContentTypeHeaderHandlerShould.cs new file mode 100644 index 00000000..3f910f42 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/ContentTypeHeaderHandlerShould.cs @@ -0,0 +1,43 @@ +using FluentAssertions; +using System.Net.Http.Headers; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class ContentTypeHeaderHandlerShould +{ + private readonly ContentTypeHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_SetsContentType() + { + var request = new HttpRequestMessage { Content = new StringContent("body") }; + + _handler.SetHeader("application/json", request); + + request.Content.Headers.ContentType.Should().NotBeNull(); + request.Content.Headers.ContentType!.MediaType.Should().Be("application/json"); + } + + [Fact] + public void GetHeader_ReturnsContentType() + { + var request = new HttpRequestMessage { Content = new StringContent("body") }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + _handler.GetHeader(request).Should().Be("application/json"); + } + + [Fact] + public void SetHeader_Throws_WhenContentIsNull() + { + var request = new HttpRequestMessage(); + + var act = () => _handler.SetHeader("application/json", request); + + act.Should().Throw(); + } + + [Fact] + public void HeaderName_IsContentType() => _handler.HeaderName.Should().Be("Content-Type"); +} diff --git a/tests/TeaPie.Tests/Http/Headers/DateHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/DateHeaderHandlerShould.cs new file mode 100644 index 00000000..780e2488 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/DateHeaderHandlerShould.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class DateHeaderHandlerShould +{ + private readonly DateHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_WithValidDate_SetsHeader() + { + var request = new HttpRequestMessage(); + + _handler.SetHeader("2024-01-15T10:30:00Z", request); + + request.Headers.Date.Should().NotBeNull(); + } + + [Fact] + public void SetHeader_WithInvalidDate_Throws() + { + var request = new HttpRequestMessage(); + + var act = () => _handler.SetHeader("not-a-date", request); + + act.Should().Throw(); + } + + [Fact] + public void GetHeader_ReturnsFormattedDate() + { + var request = new HttpRequestMessage(); + _handler.SetHeader("2024-01-15T10:30:00Z", request); + + _handler.GetHeader(request).Should().NotBeEmpty(); + } + + [Fact] + public void GetHeader_ReturnsEmpty_WhenNotSet() + { + var request = new HttpRequestMessage(); + + _handler.GetHeader(request).Should().BeEmpty(); + } +} diff --git a/tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs new file mode 100644 index 00000000..9e471018 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class DefaultHeaderHandlerShould +{ + private readonly DefaultHeaderHandler _handler = new("X-Custom"); + + [Fact] + public void SetHeader_AddsHeaderToRequest() + { + var request = new HttpRequestMessage(); + + _handler.SetHeader("value1", request); + + request.Headers.GetValues("X-Custom").Should().ContainSingle("value1"); + } + + [Fact] + public void GetHeader_ReturnsHeaderFromRequest() + { + var request = new HttpRequestMessage(); + request.Headers.TryAddWithoutValidation("X-Custom", "value1"); + + _handler.GetHeader(request).Should().Be("value1"); + } + + [Fact] + public void GetHeader_ReturnsEmptyString_WhenHeaderNotSet() + { + var request = new HttpRequestMessage(); + + _handler.GetHeader(request).Should().BeEmpty(); + } + + [Fact] + public void GetHeader_FromResponse_Works() + { + var response = new HttpResponseMessage(); + response.Headers.TryAddWithoutValidation("X-Custom", "responseValue"); + + _handler.GetHeader(response).Should().Be("responseValue"); + } + + [Fact] + public void HeaderName_PropertyIsSet() => _handler.HeaderName.Should().Be("X-Custom"); +} diff --git a/tests/TeaPie.Tests/Http/Headers/HostHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/HostHeaderHandlerShould.cs new file mode 100644 index 00000000..eb190189 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/HostHeaderHandlerShould.cs @@ -0,0 +1,44 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class HostHeaderHandlerShould +{ + private readonly HostHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_SetsHost() + { + var request = new HttpRequestMessage(); + + _handler.SetHeader("example.com", request); + + request.Headers.Host.Should().Be("example.com"); + } + + [Fact] + public void GetHeader_ReturnsHost() + { + var request = new HttpRequestMessage(); + _handler.SetHeader("example.com", request); + + _handler.GetHeader(request).Should().Be("example.com"); + } + + [Fact] + public void GetHeader_ReturnsEmpty_WhenNotSet() + { + var request = new HttpRequestMessage(); + + _handler.GetHeader(request).Should().BeEmpty(); + } + + [Fact] + public void GetHeader_FromResponse_ReturnsEmpty() + { + var response = new HttpResponseMessage(); + + _handler.GetHeader(response).Should().BeEmpty(); + } +} diff --git a/tests/TeaPie.Tests/Http/Headers/UserAgentHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/UserAgentHeaderHandlerShould.cs new file mode 100644 index 00000000..1041623c --- /dev/null +++ b/tests/TeaPie.Tests/Http/Headers/UserAgentHeaderHandlerShould.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using TeaPie.Http.Headers; + +namespace TeaPie.Tests.Http.Headers; + +public class UserAgentHeaderHandlerShould +{ + private readonly UserAgentHeaderHandler _handler = new(); + + [Fact] + public void SetHeader_AddsUserAgent() + { + var request = new HttpRequestMessage(); + + _handler.SetHeader("MyApp/1.0", request); + + _handler.GetHeader(request).Should().Contain("MyApp/1.0"); + } + + [Fact] + public void GetHeader_ReturnsUserAgentString() + { + var request = new HttpRequestMessage(); + _handler.SetHeader("MyApp/1.0", request); + + _handler.GetHeader(request).Should().NotBeEmpty(); + } + + [Fact] + public void GetHeader_FromResponse_ReturnsEmpty() + { + var response = new HttpResponseMessage(); + + _handler.GetHeader(response).Should().BeEmpty(); + } +} From 44bb0cae7ffbe7f06059434c75fa7e53292cb2d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:18:50 +0000 Subject: [PATCH 09/11] Add unit tests for StructureExploration, Http/Auth, Http/Retrying, and Testing Add 8 test files with 54 tests covering: - ExternalFilesRegistry: register, get, overwrite, unregistered key - CollectionStructure: constructors, folders, test cases, env file, init script - NoAuthProvider: authenticate completes without modifying request - AuthConstants: constant value verification - AuthDirectives: prefix, names, regex pattern matching - RetryingConstants: default values and backoff type - RetryingDirectives: full names and regex pattern matching - TestDirectives: prefix, full names, and regex pattern matching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- .../Authentication/AuthConstantsShould.cs | 19 ++ .../Authentication/AuthDirectivesShould.cs | 33 ++++ .../Authentication/NoAuthProviderShould.cs | 31 ++++ .../Http/Retrying/RetryingConstantsShould.cs | 38 ++++ .../Http/Retrying/RetryingDirectivesShould.cs | 77 ++++++++ .../CollectionStructureShould.cs | 168 ++++++++++++++++++ .../ExternalFilesRegistryShould.cs | 55 ++++++ .../Testing/TestDirectivesShould.cs | 53 ++++++ 8 files changed, 474 insertions(+) create mode 100644 tests/TeaPie.Tests/Http/Authentication/AuthConstantsShould.cs create mode 100644 tests/TeaPie.Tests/Http/Authentication/AuthDirectivesShould.cs create mode 100644 tests/TeaPie.Tests/Http/Authentication/NoAuthProviderShould.cs create mode 100644 tests/TeaPie.Tests/Http/Retrying/RetryingConstantsShould.cs create mode 100644 tests/TeaPie.Tests/Http/Retrying/RetryingDirectivesShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/CollectionStructureShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/ExternalFilesRegistryShould.cs create mode 100644 tests/TeaPie.Tests/Testing/TestDirectivesShould.cs diff --git a/tests/TeaPie.Tests/Http/Authentication/AuthConstantsShould.cs b/tests/TeaPie.Tests/Http/Authentication/AuthConstantsShould.cs new file mode 100644 index 00000000..3eadb871 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Authentication/AuthConstantsShould.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using TeaPie.Http.Auth; + +namespace TeaPie.Tests.Http.Authentication; + +public class AuthConstantsShould +{ + [Fact] + public void NoAuthKey_IsNone() + { + AuthConstants.NoAuthKey.Should().Be("None"); + } + + [Fact] + public void OAuth2Key_IsOAuth2() + { + AuthConstants.OAuth2Key.Should().Be("OAuth2"); + } +} diff --git a/tests/TeaPie.Tests/Http/Authentication/AuthDirectivesShould.cs b/tests/TeaPie.Tests/Http/Authentication/AuthDirectivesShould.cs new file mode 100644 index 00000000..f6679e4b --- /dev/null +++ b/tests/TeaPie.Tests/Http/Authentication/AuthDirectivesShould.cs @@ -0,0 +1,33 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using TeaPie.Http.Auth; + +namespace TeaPie.Tests.Http.Authentication; + +public class AuthDirectivesShould +{ + [Fact] + public void AuthDirectivePrefix_IsAUTH() + { + AuthDirectives.AuthDirectivePrefix.Should().Be("AUTH-"); + } + + [Fact] + public void AuthProviderDirectiveName_IsPROVIDER() + { + AuthDirectives.AuthProviderDirectiveName.Should().Be("PROVIDER"); + } + + [Fact] + public void AuthProviderDirectiveFullName_IsAUTH_PROVIDER() + { + AuthDirectives.AuthProviderDirectiveFullName.Should().Be("AUTH-PROVIDER"); + } + + [Fact] + public void AuthProviderDirectivePattern_MatchesSampleDirective() + { + Regex.IsMatch("## AUTH-PROVIDER: OAuth2", AuthDirectives.AuthProviderSelectorDirectivePattern) + .Should().BeTrue(); + } +} diff --git a/tests/TeaPie.Tests/Http/Authentication/NoAuthProviderShould.cs b/tests/TeaPie.Tests/Http/Authentication/NoAuthProviderShould.cs new file mode 100644 index 00000000..df163da9 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Authentication/NoAuthProviderShould.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using TeaPie.Http.Auth; + +namespace TeaPie.Tests.Http.Authentication; + +public class NoAuthProviderShould +{ + [Fact] + public async Task Authenticate_CompletesWithoutThrowing() + { + var provider = new NoAuthProvider(); + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); + + var act = () => provider.Authenticate(request, CancellationToken.None); + + await act.Should().NotThrowAsync(); + } + + [Fact] + public async Task Authenticate_DoesNotModifyRequest() + { + var provider = new NoAuthProvider(); + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com"); + request.Headers.Add("X-Custom", "value"); + + await provider.Authenticate(request, CancellationToken.None); + + request.Headers.GetValues("X-Custom").Should().ContainSingle().Which.Should().Be("value"); + request.Headers.Authorization.Should().BeNull(); + } +} diff --git a/tests/TeaPie.Tests/Http/Retrying/RetryingConstantsShould.cs b/tests/TeaPie.Tests/Http/Retrying/RetryingConstantsShould.cs new file mode 100644 index 00000000..c90a5d3f --- /dev/null +++ b/tests/TeaPie.Tests/Http/Retrying/RetryingConstantsShould.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using Polly; +using TeaPie.Http.Retrying; + +namespace TeaPie.Tests.Http.Retrying; + +public class RetryingConstantsShould +{ + [Fact] + public void DefaultName_IsRetry() + { + RetryingConstants.DefaultName.Should().Be("Retry"); + } + + [Fact] + public void DefaultRetryCount_Is3() + { + RetryingConstants.DefaultRetryCount.Should().Be(3); + } + + [Fact] + public void MaxRetryCount_IsIntMaxValue() + { + RetryingConstants.MaxRetryCount.Should().Be(int.MaxValue); + } + + [Fact] + public void DefaultBackoffType_IsConstant() + { + RetryingConstants.DefaultBackoffType.Should().Be(DelayBackoffType.Constant); + } + + [Fact] + public void DefaultBaseDelay_Is2Seconds() + { + RetryingConstants.DefaultBaseDelay.Should().Be(TimeSpan.FromSeconds(2)); + } +} diff --git a/tests/TeaPie.Tests/Http/Retrying/RetryingDirectivesShould.cs b/tests/TeaPie.Tests/Http/Retrying/RetryingDirectivesShould.cs new file mode 100644 index 00000000..5f6519c9 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Retrying/RetryingDirectivesShould.cs @@ -0,0 +1,77 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using TeaPie.Http.Retrying; + +namespace TeaPie.Tests.Http.Retrying; + +public class RetryingDirectivesShould +{ + [Fact] + public void RetryDirectivePrefix_IsRETRY() + { + RetryingDirectives.RetryDirectivePrefix.Should().Be("RETRY-"); + } + + [Fact] + public void RetryStrategyDirectiveFullName_IsRETRY_STRATEGY() + { + RetryingDirectives.RetryStrategyDirectiveFullName.Should().Be("RETRY-STRATEGY"); + } + + [Fact] + public void RetryUntilStatusCodesDirectiveFullName_IsRETRY_UNTIL_STATUS() + { + RetryingDirectives.RetryUntilStatusCodesDirectiveFullName.Should().Be("RETRY-UNTIL-STATUS"); + } + + [Fact] + public void RetryMaxAttemptsDirectiveFullName_IsRETRY_MAX_ATTEMPTS() + { + RetryingDirectives.RetryMaxAttemptsDirectiveFullName.Should().Be("RETRY-MAX-ATTEMPTS"); + } + + [Fact] + public void RetryBackoffTypeDirectiveFullName_IsRETRY_BACKOFF_TYPE() + { + RetryingDirectives.RetryBackoffTypeDirectiveFullName.Should().Be("RETRY-BACKOFF-TYPE"); + } + + [Fact] + public void RetryMaxDelayDirectiveFullName_IsRETRY_MAX_DELAY() + { + RetryingDirectives.RetryMaxDelayDirectiveFullName.Should().Be("RETRY-MAX-DELAY"); + } + + [Fact] + public void RetryUntilTestPassDirectiveFullName_IsRETRY_UNTIL_TEST_PASS() + { + RetryingDirectives.RetryUntilTestPassDirectiveFullName.Should().Be("RETRY-UNTIL-TEST-PASS"); + } + + [Theory] + [InlineData("## RETRY-STRATEGY: MyStrategy")] + [InlineData("## RETRY-STRATEGY: Default")] + public void RetryStrategyPattern_MatchesSampleDirectiveLine(string line) + { + Regex.IsMatch(line, RetryingDirectives.RetryStrategySelectorDirectivePattern) + .Should().BeTrue(); + } + + [Theory] + [InlineData("## RETRY-MAX-ATTEMPTS: 5")] + [InlineData("## RETRY-MAX-ATTEMPTS: 10")] + public void RetryMaxAttemptsPattern_MatchesSampleDirectiveLine(string line) + { + Regex.IsMatch(line, RetryingDirectives.RetryMaxAttemptsDirectivePattern) + .Should().BeTrue(); + } + + [Theory] + [InlineData("## RETRY-UNTIL-STATUS: [200, 201]")] + [InlineData("## RETRY-UNTIL-STATUS: [500]")] + public void RetryUntilStatusCodesPattern_MatchesSampleDirectiveLine(string line) + { + Regex.IsMatch(line, RetryingDirectives.RetryUntilStatusCodesDirectivePattern) + .Should().BeTrue(); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/CollectionStructureShould.cs b/tests/TeaPie.Tests/StructureExploration/CollectionStructureShould.cs new file mode 100644 index 00000000..cac145c3 --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/CollectionStructureShould.cs @@ -0,0 +1,168 @@ +using FluentAssertions; +using TeaPie.StructureExploration; +using StructureFile = TeaPie.StructureExploration.File; + +namespace TeaPie.Tests.StructureExploration; + +public class CollectionStructureShould +{ + [Fact] + public void Constructor_WithoutRoot_HasNullRoot() + { + var structure = new CollectionStructure(); + + structure.Root.Should().BeNull(); + } + + [Fact] + public void Constructor_WithRoot_SetsRootAndAddsFolder() + { + var root = new Folder("/root", "root", "root"); + + var structure = new CollectionStructure(root); + + structure.Root.Should().Be(root); + structure.Folders.Should().Contain(root); + } + + [Fact] + public void TryAddFolder_AddsFolder() + { + var structure = new CollectionStructure(); + var folder = new Folder("/folder", "folder", "folder"); + + structure.TryAddFolder(folder).Should().BeTrue(); + structure.TryGetFolder("/folder", out var result).Should().BeTrue(); + result.Should().Be(folder); + } + + [Fact] + public void TryAddFolder_ReturnsFalse_ForDuplicate() + { + var structure = new CollectionStructure(); + var folder = new Folder("/folder", "folder", "folder"); + + structure.TryAddFolder(folder); + + structure.TryAddFolder(folder).Should().BeFalse(); + } + + [Fact] + public void TryGetFolder_ReturnsTrue_ForExistingFolder() + { + var structure = new CollectionStructure(); + var folder = new Folder("/folder", "folder", "folder"); + structure.TryAddFolder(folder); + + structure.TryGetFolder("/folder", out var result).Should().BeTrue(); + result.Should().Be(folder); + } + + [Fact] + public void TryGetFolder_ReturnsFalse_ForNonExisting() + { + var structure = new CollectionStructure(); + + structure.TryGetFolder("/nonexistent", out _).Should().BeFalse(); + } + + [Fact] + public void TryAddTestCase_AddsTestCaseAndAutoAddsParentFolder() + { + var structure = new CollectionStructure(); + var parentFolder = new Folder("/parent", "parent", "parent"); + var requestFile = new InternalFile("/parent/req.http", "parent/req.http", parentFolder); + var testCase = new TestCase(requestFile); + + structure.TryAddTestCase(testCase).Should().BeTrue(); + structure.TestCases.Should().Contain(testCase); + structure.TryGetFolder("/parent", out _).Should().BeTrue(); + } + + [Fact] + public void TryAddTestCase_ReturnsFalse_ForDuplicate() + { + var structure = new CollectionStructure(); + var parentFolder = new Folder("/parent", "parent", "parent"); + var requestFile = new InternalFile("/parent/req.http", "parent/req.http", parentFolder); + var testCase = new TestCase(requestFile); + + structure.TryAddTestCase(testCase); + + structure.TryAddTestCase(testCase).Should().BeFalse(); + } + + [Fact] + public void SetEnvironmentFile_SetsFile() + { + var structure = new CollectionStructure(); + + structure.SetEnvironmentFile(new StructureFile("env.json")); + + structure.EnvironmentFile.Should().NotBeNull(); + } + + [Fact] + public void SetEnvironmentFile_Throws_WhenNull() + { + var structure = new CollectionStructure(); + + var act = () => structure.SetEnvironmentFile(null); + + act.Should().Throw(); + } + + [Fact] + public void HasEnvironmentFile_ReturnsTrue_WhenSet() + { + var structure = new CollectionStructure(); + structure.SetEnvironmentFile(new StructureFile("env.json")); + + structure.HasEnvironmentFile.Should().BeTrue(); + } + + [Fact] + public void HasEnvironmentFile_ReturnsFalse_WhenNotSet() + { + var structure = new CollectionStructure(); + + structure.HasEnvironmentFile.Should().BeFalse(); + } + + [Fact] + public void SetInitializationScript_SetsScript() + { + var structure = new CollectionStructure(); + + structure.SetInitializationScript(new Script(new StructureFile("init.csx"))); + + structure.InitializationScript.Should().NotBeNull(); + } + + [Fact] + public void SetInitializationScript_Throws_WhenNull() + { + var structure = new CollectionStructure(); + + var act = () => structure.SetInitializationScript(null); + + act.Should().Throw(); + } + + [Fact] + public void HasInitializationScript_ReturnsTrue_WhenSet() + { + var structure = new CollectionStructure(); + structure.SetInitializationScript(new Script(new StructureFile("init.csx"))); + + structure.HasInitializationScript.Should().BeTrue(); + } + + [Fact] + public void HasInitializationScript_ReturnsFalse_WhenNotSet() + { + var structure = new CollectionStructure(); + + structure.HasInitializationScript.Should().BeFalse(); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/ExternalFilesRegistryShould.cs b/tests/TeaPie.Tests/StructureExploration/ExternalFilesRegistryShould.cs new file mode 100644 index 00000000..d925ce82 --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/ExternalFilesRegistryShould.cs @@ -0,0 +1,55 @@ +using FluentAssertions; +using TeaPie.StructureExploration; + +namespace TeaPie.Tests.StructureExploration; + +public class ExternalFilesRegistryShould +{ + private readonly ExternalFilesRegistry _registry = new(); + + [Fact] + public void Register_And_Get_ReturnsTheElement() + { + var file = new ExternalFile("/path/to/file.txt"); + + _registry.Register("myFile", file); + + _registry.Get("myFile").Should().Be(file); + } + + [Fact] + public void IsRegistered_ReturnsTrue_AfterRegistration() + { + var file = new ExternalFile("/path/to/file.txt"); + + _registry.Register("myFile", file); + + _registry.IsRegistered("myFile").Should().BeTrue(); + } + + [Fact] + public void IsRegistered_ReturnsFalse_BeforeRegistration() + { + _registry.IsRegistered("nonExistent").Should().BeFalse(); + } + + [Fact] + public void Get_Throws_ForUnregisteredName() + { + var act = () => _registry.Get("unknown"); + + act.Should().Throw(); + } + + [Fact] + public void Register_Overwrites_ExistingWithSameName() + { + var first = new ExternalFile("/path/first.txt"); + var second = new ExternalFile("/path/second.txt"); + + _registry.Register("key", first); + _registry.Register("key", second); + + _registry.Get("key").Should().Be(second); + } +} diff --git a/tests/TeaPie.Tests/Testing/TestDirectivesShould.cs b/tests/TeaPie.Tests/Testing/TestDirectivesShould.cs new file mode 100644 index 00000000..ddb13e2c --- /dev/null +++ b/tests/TeaPie.Tests/Testing/TestDirectivesShould.cs @@ -0,0 +1,53 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using TeaPie.Testing; + +namespace TeaPie.Tests.Testing; + +public class TestDirectivesShould +{ + [Fact] + public void TestDirectivePrefix_IsTEST() + { + TestDirectives.TestDirectivePrefix.Should().Be("TEST-"); + } + + [Fact] + public void TestExpectStatusCodesDirectiveFullName_IsTEST_EXPECT_STATUS() + { + TestDirectives.TestExpectStatusCodesDirectiveFullName.Should().Be("TEST-EXPECT-STATUS"); + } + + [Fact] + public void TestHasBodyDirectiveFullName_IsTEST_HAS_BODY() + { + TestDirectives.TestHasBodyDirectiveFullName.Should().Be("TEST-HAS-BODY"); + } + + [Fact] + public void TestHasHeaderDirectiveFullName_IsTEST_HAS_HEADER() + { + TestDirectives.TestHasHeaderDirectiveFullName.Should().Be("TEST-HAS-HEADER"); + } + + [Fact] + public void TestExpectStatusCodesPattern_MatchesSampleDirectiveLine() + { + Regex.IsMatch("## TEST-EXPECT-STATUS: [200, 201]", TestDirectives.TestExpectStatusCodesDirectivePattern) + .Should().BeTrue(); + } + + [Fact] + public void TestHasBodyPattern_MatchesSampleDirectiveLine() + { + Regex.IsMatch("## TEST-HAS-BODY: true", TestDirectives.TestHasBodyDirectivePattern) + .Should().BeTrue(); + } + + [Fact] + public void TestHasHeaderPattern_MatchesSampleDirectiveLine() + { + Regex.IsMatch("## TEST-HAS-HEADER: Content-Type", TestDirectives.TestHasHeaderDirectivePattern) + .Should().BeTrue(); + } +} From e25f02b4c22e227e9f7fa2e7abfea4e4738ec6d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:28:42 +0000 Subject: [PATCH 10/11] Add tests for paths, contexts, retry parsers, functions, JSON, environments (batch 4: 77 tests) Agent-Logs-Url: https://github.com/Kros-sk/TeaPie/sessions/fac89cfe-d408-4f06-a17d-75ebe01fbef8 Co-authored-by: Burgyn <5930822+Burgyn@users.noreply.github.com> --- .../Environments/EnvironmentShould.cs | 42 +++++++ .../DefaultFunctionsRegistratorShould.cs | 69 ++++++++++++ .../TeaPie.Tests/Functions/FunctionShould.cs | 85 ++++++++++++++ .../Http/Parsing/HttpParsingContextShould.cs | 76 +++++++++++++ .../Http/RequestExecutionContextShould.cs | 66 +++++++++++ ...icitPropertiesDirectiveLineParserShould.cs | 73 ++++++++++++ .../RetryStrategyDirectiveLineParserShould.cs | 42 +++++++ ...tilStatusCodesDirectiveLineParserShould.cs | 43 ++++++++ .../CaseInsensitiveExpandoObjectShould.cs | 77 +++++++++++++ .../Paths/PathExtensionsShould.cs | 104 ++++++++++++++++++ .../Paths/PathProviderShould.cs | 102 +++++++++++++++++ .../Paths/RelativePathResolverShould.cs | 37 +++++++ .../Testing/TestDescriptionShould.cs | 47 ++++++++ 13 files changed, 863 insertions(+) create mode 100644 tests/TeaPie.Tests/Environments/EnvironmentShould.cs create mode 100644 tests/TeaPie.Tests/Functions/DefaultFunctionsRegistratorShould.cs create mode 100644 tests/TeaPie.Tests/Functions/FunctionShould.cs create mode 100644 tests/TeaPie.Tests/Http/Parsing/HttpParsingContextShould.cs create mode 100644 tests/TeaPie.Tests/Http/RequestExecutionContextShould.cs create mode 100644 tests/TeaPie.Tests/Http/Retrying/RetryExplicitPropertiesDirectiveLineParserShould.cs create mode 100644 tests/TeaPie.Tests/Http/Retrying/RetryStrategyDirectiveLineParserShould.cs create mode 100644 tests/TeaPie.Tests/Http/Retrying/RetryUntilStatusCodesDirectiveLineParserShould.cs create mode 100644 tests/TeaPie.Tests/Json/CaseInsensitiveExpandoObjectShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/Paths/PathExtensionsShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/Paths/PathProviderShould.cs create mode 100644 tests/TeaPie.Tests/StructureExploration/Paths/RelativePathResolverShould.cs create mode 100644 tests/TeaPie.Tests/Testing/TestDescriptionShould.cs diff --git a/tests/TeaPie.Tests/Environments/EnvironmentShould.cs b/tests/TeaPie.Tests/Environments/EnvironmentShould.cs new file mode 100644 index 00000000..babf602d --- /dev/null +++ b/tests/TeaPie.Tests/Environments/EnvironmentShould.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using TeaPie.Variables; +using Environment = TeaPie.Environments.Environment; + +namespace TeaPie.Tests.Environments; + +public class EnvironmentShould +{ + [Fact] + public void SetNameFromConstructor() + { + var env = new Environment("dev", new Dictionary()); + + env.Name.Should().Be("dev"); + } + + [Fact] + public void SetVariablesFromConstructor() + { + var vars = new Dictionary { { "key", "value" } }; + var env = new Environment("dev", vars); + + env.Variables.Should().ContainKey("key"); + } + + [Fact] + public void ApplySetsAllVariablesOnTargetCollection() + { + var vars = new Dictionary + { + { "baseUrl", "http://localhost" }, + { "port", 8080 } + }; + var env = new Environment("dev", vars); + var collection = new VariablesCollection(); + + env.Apply(collection); + + collection.Get("baseUrl").Should().Be("http://localhost"); + collection.Get("port").Should().Be(8080); + } +} diff --git a/tests/TeaPie.Tests/Functions/DefaultFunctionsRegistratorShould.cs b/tests/TeaPie.Tests/Functions/DefaultFunctionsRegistratorShould.cs new file mode 100644 index 00000000..b11362ae --- /dev/null +++ b/tests/TeaPie.Tests/Functions/DefaultFunctionsRegistratorShould.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using TeaPie.Functions; + +namespace TeaPie.Tests.Functions; + +public class DefaultFunctionsRegistratorShould +{ + [Fact] + public void RegisterNowFunction() + { + FunctionsCollection collection = []; + + DefaultFunctionsRegistrator.Register(collection); + + collection.Contains("$now", 1).Should().BeTrue(); + } + + [Fact] + public void RegisterGuidFunction() + { + FunctionsCollection collection = []; + + DefaultFunctionsRegistrator.Register(collection); + + collection.Contains("$guid", 0).Should().BeTrue(); + } + + [Fact] + public void RegisterRandFunction() + { + FunctionsCollection collection = []; + + DefaultFunctionsRegistrator.Register(collection); + + collection.Contains("$rand", 0).Should().BeTrue(); + } + + [Fact] + public void RegisterRandomIntFunction() + { + FunctionsCollection collection = []; + + DefaultFunctionsRegistrator.Register(collection); + + collection.Contains("$randomInt", 2).Should().BeTrue(); + } + + [Fact] + public void NowFunctionReturnsNonEmptyString() + { + FunctionsCollection collection = []; + DefaultFunctionsRegistrator.Register(collection); + + var result = collection.Execute("$now", "o"); + + result.Should().NotBeNullOrEmpty(); + } + + [Fact] + public void GuidFunctionReturnsValidGuid() + { + FunctionsCollection collection = []; + DefaultFunctionsRegistrator.Register(collection); + + var result = collection.Execute("$guid"); + + result.Should().NotBe(Guid.Empty); + } +} diff --git a/tests/TeaPie.Tests/Functions/FunctionShould.cs b/tests/TeaPie.Tests/Functions/FunctionShould.cs new file mode 100644 index 00000000..37cafcb1 --- /dev/null +++ b/tests/TeaPie.Tests/Functions/FunctionShould.cs @@ -0,0 +1,85 @@ +using FluentAssertions; +using TeaPie.Functions; + +namespace TeaPie.Tests.Functions; + +public class FunctionShould +{ + [Fact] + public void InvokeNoArgFunctionAndReturnResult() + { + var func = new Function("$test", () => 42); + + var result = func.InvokeFunction(); + + result.Should().Be(42); + } + + [Fact] + public void SetNamePropertyFromConstructor() + { + var func = new Function("$myFunc", () => 0); + + func.Name.Should().Be("$myFunc"); + } + + [Fact] + public void InvokeSingleArgFunctionWithConversion() + { + var func = new Function("$double", x => x * 2); + + var result = func.InvokeFunction(5); + + result.Should().Be(10); + } + + [Fact] + public void ThrowWhenSingleArgFunctionCalledWithNoArgs() + { + var func = new Function("$double", x => x * 2); + + var act = () => func.InvokeFunction(Array.Empty()); + + act.Should().Throw(); + } + + [Fact] + public void ThrowWhenSingleArgFunctionCalledWithNullArgs() + { + var func = new Function("$double", x => x * 2); + + var act = () => func.InvokeFunction(null); + + act.Should().Throw(); + } + + [Fact] + public void InvokeTwoArgFunctionCorrectly() + { + var func = new Function("$add", (a, b) => a + b); + + var result = func.InvokeFunction(3, 7); + + result.Should().Be(10); + } + + [Fact] + public void ThrowWhenTwoArgFunctionCalledWithLessThanTwoArgs() + { + var func = new Function("$add", (a, b) => a + b); + + var act = () => func.InvokeFunction(1); + + act.Should().Throw(); + } + + [Fact] + public void ThrowWhenTwoArgFunctionCalledWithNullArgs() + { + var func = new Function("$add", (a, b) => a + b); + + var act = () => func.InvokeFunction(null); + + act.Should().Throw(); + } +} diff --git a/tests/TeaPie.Tests/Http/Parsing/HttpParsingContextShould.cs b/tests/TeaPie.Tests/Http/Parsing/HttpParsingContextShould.cs new file mode 100644 index 00000000..9b6a7f78 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Parsing/HttpParsingContextShould.cs @@ -0,0 +1,76 @@ +using FluentAssertions; +using TeaPie.Http.Parsing; +using TeaPie.Testing; + +namespace TeaPie.Tests.Http.Parsing; + +public class HttpParsingContextShould +{ + private static HttpParsingContext CreateContext() + { + using var msg = new HttpRequestMessage(); + return new HttpParsingContext(msg.Headers); + } + + [Fact] + public void HaveEmptyInitialProperties() + { + var context = CreateContext(); + + context.RequestName.Should().BeEmpty(); + context.RequestUri.Should().BeEmpty(); + context.RetryStrategyName.Should().BeEmpty(); + context.AuthProviderName.Should().BeEmpty(); + } + + [Fact] + public void AddHeaderToHeadersDictionary() + { + var context = CreateContext(); + + context.AddHeader("Content-Type", "application/json"); + + context.Headers.Should().ContainKey("Content-Type"); + context.Headers["Content-Type"].Should().Be("application/json"); + } + + [Fact] + public void AddMultipleHeaders() + { + var context = CreateContext(); + + context.AddHeader("Accept", "text/plain"); + context.AddHeader("Authorization", "Bearer token"); + + context.Headers.Should().ContainKey("Accept"); + context.Headers.Should().ContainKey("Authorization"); + } + + [Fact] + public void RegisterTestAddsToTestsList() + { + var context = CreateContext(); + var testDesc = new TestDescription("TEST", new Dictionary()); + + context.RegiterTest(testDesc); + + context.Tests.Should().ContainSingle(); + } + + [Fact] + public void HaveNonNullBodyBuilder() + { + var context = CreateContext(); + + context.BodyBuilder.Should().NotBeNull(); + } + + [Fact] + public void HaveDefaultFalseForBoolProperties() + { + var context = CreateContext(); + + context.IsBody.Should().BeFalse(); + context.IsMethodAndUriResolved.Should().BeFalse(); + } +} diff --git a/tests/TeaPie.Tests/Http/RequestExecutionContextShould.cs b/tests/TeaPie.Tests/Http/RequestExecutionContextShould.cs new file mode 100644 index 00000000..81322bc2 --- /dev/null +++ b/tests/TeaPie.Tests/Http/RequestExecutionContextShould.cs @@ -0,0 +1,66 @@ +using FluentAssertions; +using TeaPie.Http; +using TeaPie.StructureExploration; + +namespace TeaPie.Tests.Http; + +public class RequestExecutionContextShould +{ + private static InternalFile CreateFile() + { + var folder = new Folder("/path", "relative", "name"); + return new InternalFile("/path/test-req.http", "relative/test-req.http", folder); + } + + [Fact] + public void SetRequestFileFromConstructor() + { + var file = CreateFile(); + using var context = new RequestExecutionContext(file); + + context.RequestFile.Should().BeSameAs(file); + } + + [Fact] + public void HaveEmptyNameByDefault() + { + var file = CreateFile(); + using var context = new RequestExecutionContext(file); + + context.Name.Should().BeEmpty(); + } + + [Fact] + public void HaveNullTestCaseExecutionContextByDefault() + { + var file = CreateFile(); + using var context = new RequestExecutionContext(file); + + context.TestCaseExecutionContext.Should().BeNull(); + } + + [Fact] + public void AllowSettingProperties() + { + var file = CreateFile(); + using var context = new RequestExecutionContext(file); + + context.Name = "MyRequest"; + context.RawContent = "GET http://example.com"; + + context.Name.Should().Be("MyRequest"); + context.RawContent.Should().Be("GET http://example.com"); + } + + [Fact] + public void SetRawContentToNullOnDispose() + { + var file = CreateFile(); + var context = new RequestExecutionContext(file); + context.RawContent = "some content"; + + context.Dispose(); + + context.RawContent.Should().BeNull(); + } +} diff --git a/tests/TeaPie.Tests/Http/Retrying/RetryExplicitPropertiesDirectiveLineParserShould.cs b/tests/TeaPie.Tests/Http/Retrying/RetryExplicitPropertiesDirectiveLineParserShould.cs new file mode 100644 index 00000000..da81bf1c --- /dev/null +++ b/tests/TeaPie.Tests/Http/Retrying/RetryExplicitPropertiesDirectiveLineParserShould.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using Polly; +using TeaPie.Http.Parsing; +using TeaPie.Http.Retrying; + +namespace TeaPie.Tests.Http.Retrying; + +public class RetryExplicitPropertiesDirectiveLineParserShould +{ + private readonly RetryExplicitPropertiesDirectiveLineParser _parser = new(); + + private static HttpParsingContext CreateContext() + { + using var msg = new HttpRequestMessage(); + return new HttpParsingContext(msg.Headers); + } + + [Fact] + public void ReturnTrueForMaxAttemptsDirective() + { + var context = CreateContext(); + + _parser.CanParse("## RETRY-MAX-ATTEMPTS: 5", context).Should().BeTrue(); + } + + [Fact] + public void ReturnTrueForBackoffTypeDirective() + { + var context = CreateContext(); + + _parser.CanParse("## RETRY-BACKOFF-TYPE: Linear", context).Should().BeTrue(); + } + + [Fact] + public void ReturnFalseForUnrelatedDirective() + { + var context = CreateContext(); + + _parser.CanParse("## UNRELATED-DIRECTIVE: value", context).Should().BeFalse(); + } + + [Fact] + public void SetMaxRetryAttemptsFromMaxAttemptsDirective() + { + var context = CreateContext(); + + _parser.Parse("## RETRY-MAX-ATTEMPTS: 5", context); + + context.ExplicitRetryStrategy.Should().NotBeNull(); + context.ExplicitRetryStrategy!.MaxRetryAttempts.Should().Be(5); + } + + [Fact] + public void SetBackoffTypeFromBackoffTypeDirective() + { + var context = CreateContext(); + + _parser.Parse("## RETRY-BACKOFF-TYPE: Linear", context); + + context.ExplicitRetryStrategy.Should().NotBeNull(); + context.ExplicitRetryStrategy!.BackoffType.Should().Be(DelayBackoffType.Linear); + } + + [Fact] + public void ThrowForUnparseableLine() + { + var context = CreateContext(); + + var act = () => _parser.Parse("## INVALID-LINE: something", context); + + act.Should().Throw(); + } +} diff --git a/tests/TeaPie.Tests/Http/Retrying/RetryStrategyDirectiveLineParserShould.cs b/tests/TeaPie.Tests/Http/Retrying/RetryStrategyDirectiveLineParserShould.cs new file mode 100644 index 00000000..1b569e0e --- /dev/null +++ b/tests/TeaPie.Tests/Http/Retrying/RetryStrategyDirectiveLineParserShould.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using TeaPie.Http.Parsing; +using TeaPie.Http.Retrying; + +namespace TeaPie.Tests.Http.Retrying; + +public class RetryStrategyDirectiveLineParserShould +{ + private readonly RetryStrategyDirectiveLineParser _parser = new(); + + private static HttpParsingContext CreateContext() + { + using var msg = new HttpRequestMessage(); + return new HttpParsingContext(msg.Headers); + } + + [Fact] + public void ReturnTrueForMatchingRetryStrategyDirective() + { + var context = CreateContext(); + + _parser.CanParse("## RETRY-STRATEGY: MyStrategy", context).Should().BeTrue(); + } + + [Fact] + public void ReturnFalseForNonMatchingLine() + { + var context = CreateContext(); + + _parser.CanParse("## SOMETHING-ELSE: value", context).Should().BeFalse(); + } + + [Fact] + public void ExtractStrategyNameIntoContext() + { + var context = CreateContext(); + + _parser.Parse("## RETRY-STRATEGY: MyStrategy", context); + + context.RetryStrategyName.Should().Be("MyStrategy"); + } +} diff --git a/tests/TeaPie.Tests/Http/Retrying/RetryUntilStatusCodesDirectiveLineParserShould.cs b/tests/TeaPie.Tests/Http/Retrying/RetryUntilStatusCodesDirectiveLineParserShould.cs new file mode 100644 index 00000000..9f6ac684 --- /dev/null +++ b/tests/TeaPie.Tests/Http/Retrying/RetryUntilStatusCodesDirectiveLineParserShould.cs @@ -0,0 +1,43 @@ +using FluentAssertions; +using TeaPie.Http.Parsing; + +namespace TeaPie.Tests.Http.Retrying; + +public class RetryUntilStatusCodesDirectiveLineParserShould +{ + private readonly RetryUntilStatusCodesLineParser _parser = new(); + + private static HttpParsingContext CreateContext() + { + using var msg = new HttpRequestMessage(); + return new HttpParsingContext(msg.Headers); + } + + [Fact] + public void ReturnTrueForMatchingLine() + { + var context = CreateContext(); + + _parser.CanParse("## RETRY-UNTIL-STATUS: [200, 201]", context).Should().BeTrue(); + } + + [Fact] + public void ReturnFalseForNonMatchingLine() + { + var context = CreateContext(); + + _parser.CanParse("## SOMETHING-ELSE: value", context).Should().BeFalse(); + } + + [Fact] + public void ExtractStatusCodesIntoContext() + { + var context = CreateContext(); + + _parser.Parse("## RETRY-UNTIL-STATUS: [200, 201]", context); + + context.RetryUntilStatusCodes.Should().HaveCount(2); + context.RetryUntilStatusCodes.Should().Contain(System.Net.HttpStatusCode.OK); + context.RetryUntilStatusCodes.Should().Contain(System.Net.HttpStatusCode.Created); + } +} diff --git a/tests/TeaPie.Tests/Json/CaseInsensitiveExpandoObjectShould.cs b/tests/TeaPie.Tests/Json/CaseInsensitiveExpandoObjectShould.cs new file mode 100644 index 00000000..50eaaa8f --- /dev/null +++ b/tests/TeaPie.Tests/Json/CaseInsensitiveExpandoObjectShould.cs @@ -0,0 +1,77 @@ +using FluentAssertions; +using Newtonsoft.Json.Linq; +using TeaPie.Json; + +namespace TeaPie.Tests.Json; + +public class CaseInsensitiveExpandoObjectShould +{ + [Fact] + public void ReturnValueForMatchingKey() + { + var dict = new Dictionary { { "Name", "John" } }; + dynamic obj = new CaseInsensitiveExpandoObject(dict); + + string name = obj.Name; + + name.Should().Be("John"); + } + + [Fact] + public void ReturnValueCaseInsensitively() + { + var dict = new Dictionary { { "Name", "John" } }; + dynamic obj = new CaseInsensitiveExpandoObject(dict); + + string name = obj.name; + + name.Should().Be("John"); + } + + [Fact] + public void SetValueViaDynamicMember() + { + var dict = new Dictionary { { "Name", "John" } }; + dynamic obj = new CaseInsensitiveExpandoObject(dict); + + obj.Name = "Jane"; + string name = obj.Name; + + name.Should().Be("Jane"); + } + + [Fact] + public void ReturnKeysFromGetDynamicMemberNames() + { + var dict = new Dictionary { { "Key1", "A" }, { "Key2", "B" } }; + var obj = new CaseInsensitiveExpandoObject(dict); + + var members = obj.GetDynamicMemberNames(); + + members.Should().Contain("Key1").And.Contain("Key2"); + } + + [Fact] + public void WrapJObjectAsCaseInsensitiveExpandoObject() + { + var jObj = JObject.FromObject(new { inner = "value" }); + var dict = new Dictionary { { "nested", jObj } }; + dynamic obj = new CaseInsensitiveExpandoObject(dict); + + object nested = obj.nested; + + nested.Should().BeOfType(); + } + + [Fact] + public void WrapJArrayAsList() + { + var jArr = new JArray(1, 2, 3); + var dict = new Dictionary { { "items", jArr } }; + dynamic obj = new CaseInsensitiveExpandoObject(dict); + + object items = obj.items; + + items.Should().BeOfType>(); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/Paths/PathExtensionsShould.cs b/tests/TeaPie.Tests/StructureExploration/Paths/PathExtensionsShould.cs new file mode 100644 index 00000000..2534b8ec --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/Paths/PathExtensionsShould.cs @@ -0,0 +1,104 @@ +using FluentAssertions; +using TeaPie.StructureExploration.Paths; + +namespace TeaPie.Tests.StructureExploration.Paths; + +public class PathExtensionsShould +{ + [Fact] + public void NormalizeSeparatorsToCurrentPlatform() + { + var path = "some/path\\to\\file"; + + var result = path.NormalizeSeparators(); + + result.Should().NotContain(Path.DirectorySeparatorChar == '/' ? "\\" : "/"); + } + + [Fact] + public void TrimSlashAtTheEndRemovesTrailingSeparator() + { + var path = $"some{Path.DirectorySeparatorChar}path{Path.DirectorySeparatorChar}"; + + var result = path.TrimSlashAtTheEnd(); + + result.Should().NotEndWith(Path.DirectorySeparatorChar.ToString()); + } + + [Fact] + public void TrimSlashInTheBeginningRemovesLeadingSeparator() + { + var path = $"{Path.DirectorySeparatorChar}some{Path.DirectorySeparatorChar}path"; + + var result = path.TrimSlashInTheBeginning(); + + result.Should().NotStartWith(Path.DirectorySeparatorChar.ToString()); + } + + [Fact] + public void TrimSlashesRemovesBothLeadingAndTrailingSeparators() + { + var path = $"{Path.DirectorySeparatorChar}some{Path.DirectorySeparatorChar}path{Path.DirectorySeparatorChar}"; + + var result = path.TrimSlashes(); + + result.Should().NotStartWith(Path.DirectorySeparatorChar.ToString()); + result.Should().NotEndWith(Path.DirectorySeparatorChar.ToString()); + } + + [Theory] + [InlineData("\"some/path\"", "some/path")] + [InlineData("\"quoted\"", "quoted")] + [InlineData("noquotes", "noquotes")] + public void TrimQuotesRemovesSurroundingQuotes(string input, string expected) + { + var result = input.TrimQuotes(); + + result.Should().Be(expected); + } + + [Fact] + public void NormalizePathTrimsAndRemovesQuotesAndNormalizesAndTrimsEndSlash() + { + var path = " \"some/path/to/dir/\" "; + + var result = path.NormalizePath(); + + result.Should().NotStartWith(" "); + result.Should().NotEndWith(Path.DirectorySeparatorChar.ToString()); + result.Should().NotContain("\""); + } + + [Fact] + public void TrimRootPathReturnsRelativePathWhenFullPathStartsWithRoot() + { + var rootPath = Path.Combine(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar), "root"); + var fullPath = Path.Combine(rootPath, "sub", "file.txt"); + + var result = fullPath.TrimRootPath(rootPath); + + result.Should().Be(Path.Combine("sub", "file.txt")); + } + + [Fact] + public void TrimRootPathKeepsRootFolderNameWhenRequested() + { + var rootPath = Path.Combine(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar), "root"); + var fullPath = Path.Combine(rootPath, "sub", "file.txt"); + + var result = fullPath.TrimRootPath(rootPath, keepRootFolder: true); + + result.Should().StartWith("root"); + } + + [Fact] + public void TrimRootPathReturnsOriginalWhenPathDoesNotStartWithRoot() + { + var rootPath = Path.Combine(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar), "root"); + var fullPath = Path.Combine(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar), "other", "file.txt"); + + var result = fullPath.TrimRootPath(rootPath); + + result.Should().Be(fullPath); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/Paths/PathProviderShould.cs b/tests/TeaPie.Tests/StructureExploration/Paths/PathProviderShould.cs new file mode 100644 index 00000000..4eacae6a --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/Paths/PathProviderShould.cs @@ -0,0 +1,102 @@ +using FluentAssertions; +using TeaPie.StructureExploration.Paths; + +namespace TeaPie.Tests.StructureExploration.Paths; + +public class PathProviderShould +{ + [Fact] + public void HaveEmptyInitialPaths() + { + var provider = new PathProvider(); + + provider.RootPath.Should().BeEmpty(); + provider.TempRootPath.Should().BeEmpty(); + provider.TeaPieFolderPath.Should().BeEmpty(); + } + + [Fact] + public void SetRootPathAndTempRootPathOnUpdatePaths() + { + var provider = new PathProvider(); + var rootPath = Path.Combine("some", "root", "path"); + var tempRootPath = Path.Combine("some", "temp", "root"); + + provider.UpdatePaths(rootPath, tempRootPath); + + provider.RootPath.Should().Be(rootPath); + provider.TempRootPath.Should().Be(tempRootPath); + } + + [Fact] + public void UseTempRootPathAsTeaPieFolderPathWhenTeaPieFolderPathIsEmpty() + { + var provider = new PathProvider(); + var rootPath = Path.Combine("some", "root", "path"); + var tempRootPath = Path.Combine("some", "temp", "root"); + + provider.UpdatePaths(rootPath, tempRootPath); + + provider.TeaPieFolderPath.Should().Be(tempRootPath); + } + + [Fact] + public void UseProvidedTeaPieFolderPathWhenNotEmpty() + { + var provider = new PathProvider(); + var rootPath = Path.Combine("some", "root", "path"); + var tempRootPath = Path.Combine("some", "temp", "root"); + var teaPieFolderPath = Path.Combine("custom", "teapie", "folder"); + + provider.UpdatePaths(rootPath, tempRootPath, teaPieFolderPath); + + provider.TeaPieFolderPath.Should().Be(teaPieFolderPath); + } + + [Theory] + [InlineData("my-collection-req", "my-collection")] + [InlineData("my-collection", "my-collection")] + [InlineData("simple", "simple")] + public void DeriveStructureNameFromRootPath(string fileName, string expectedStructureName) + { + var provider = new PathProvider(); + var rootPath = Path.Combine("some", "root", fileName + ".http"); + + provider.UpdatePaths(rootPath, "temp"); + + provider.StructureName.Should().Be(expectedStructureName); + } + + [Fact] + public void ComputeCacheFolderPathFromTeaPieFolderPath() + { + var provider = new PathProvider(); + var teaPieFolderPath = Path.Combine("some", "teapie"); + + provider.UpdatePaths("root", "temp", teaPieFolderPath); + + provider.CacheFolderPath.Should().Be(Path.Combine(teaPieFolderPath, "cache")); + } + + [Fact] + public void ComputeReportsFolderPathFromTeaPieFolderPath() + { + var provider = new PathProvider(); + var teaPieFolderPath = Path.Combine("some", "teapie"); + + provider.UpdatePaths("root", "temp", teaPieFolderPath); + + provider.ReportsFolderPath.Should().Be(Path.Combine(teaPieFolderPath, "reports")); + } + + [Fact] + public void ComputeVariablesFilePathEndingWithVariablesJson() + { + var provider = new PathProvider(); + var teaPieFolderPath = Path.Combine("some", "teapie"); + + provider.UpdatePaths("root", "temp", teaPieFolderPath); + + provider.VariablesFilePath.Should().EndWith("variables.json"); + } +} diff --git a/tests/TeaPie.Tests/StructureExploration/Paths/RelativePathResolverShould.cs b/tests/TeaPie.Tests/StructureExploration/Paths/RelativePathResolverShould.cs new file mode 100644 index 00000000..5f9e1886 --- /dev/null +++ b/tests/TeaPie.Tests/StructureExploration/Paths/RelativePathResolverShould.cs @@ -0,0 +1,37 @@ +using FluentAssertions; +using TeaPie.StructureExploration.Paths; + +namespace TeaPie.Tests.StructureExploration.Paths; + +public class RelativePathResolverShould +{ + private readonly RelativePathResolver _resolver = new(); + + [Theory] + [InlineData("sub/file.txt")] + [InlineData("file.txt")] + [InlineData("a/b/c.json")] + public void ReturnTrueForRelativePath(string path) + { + _resolver.CanResolve(path).Should().BeTrue(); + } + + [Theory] + [InlineData("/home/user/file.txt")] + [InlineData("/absolute/path")] + public void ReturnFalseForAbsolutePath(string path) + { + _resolver.CanResolve(path).Should().BeFalse(); + } + + [Fact] + public void CombineRelativePathWithBasePath() + { + var basePath = Path.Combine(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar), "base"); + var relativePath = Path.Combine("sub", "file.txt"); + + var result = _resolver.ResolvePath(relativePath, basePath); + + result.Should().Be(Path.GetFullPath(Path.Combine(basePath, relativePath))); + } +} diff --git a/tests/TeaPie.Tests/Testing/TestDescriptionShould.cs b/tests/TeaPie.Tests/Testing/TestDescriptionShould.cs new file mode 100644 index 00000000..8dd3118a --- /dev/null +++ b/tests/TeaPie.Tests/Testing/TestDescriptionShould.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using TeaPie.Http; +using TeaPie.StructureExploration; +using TeaPie.Testing; + +namespace TeaPie.Tests.Testing; + +public class TestDescriptionShould +{ + [Fact] + public void SetDirectiveFromConstructor() + { + var description = new TestDescription("TEST", new Dictionary()); + + description.Directive.Should().Be("TEST"); + } + + [Fact] + public void SetParametersFromConstructor() + { + var parameters = new Dictionary { { "key", "value" } }; + var description = new TestDescription("TEST", parameters); + + description.Parameters.Should().ContainKey("key"); + } + + [Fact] + public void HaveNullRequestExecutionContextInitially() + { + var description = new TestDescription("TEST", new Dictionary()); + + description.RequestExecutionContext.Should().BeNull(); + } + + [Fact] + public void SetRequestExecutionContextViaMethod() + { + var description = new TestDescription("TEST", new Dictionary()); + var folder = new Folder("/path", "relative", "name"); + var file = new InternalFile("/path/test.http", "test.http", folder); + var context = new RequestExecutionContext(file); + + description.SetRequestExecutionContext(context); + + description.RequestExecutionContext.Should().BeSameAs(context); + } +} From 5cc6d7be2a961025fa4cf44a645315914adc8bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=88o?= Date: Wed, 1 Apr 2026 06:49:57 +0200 Subject: [PATCH 11/11] Fix incorrect FluentAssertions ContainSingle overload in DefaultHeaderHandlerShould ContainSingle(string) treats the argument as a failure reason message, not the expected value. Use ContainSingle().Which.Should().Be() to actually assert the value. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs b/tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs index 9e471018..3dc91fee 100644 --- a/tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs +++ b/tests/TeaPie.Tests/Http/Headers/DefaultHeaderHandlerShould.cs @@ -14,7 +14,7 @@ public void SetHeader_AddsHeaderToRequest() _handler.SetHeader("value1", request); - request.Headers.GetValues("X-Custom").Should().ContainSingle("value1"); + request.Headers.GetValues("X-Custom").Should().ContainSingle().Which.Should().Be("value1"); } [Fact]