diff --git a/.cspell.yaml b/.cspell.yaml index d690ae1..1f5e6b1 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -80,6 +80,7 @@ words: - fileassert - FileAssert - filepart + - selftest - testname - TMPL - triaging diff --git a/.reviewmark.yaml b/.reviewmark.yaml index f82c8e9..33a40c9 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -23,21 +23,68 @@ evidence-source: # Each review-set groups requirements, source, and tests for a coherent software unit # so that an AI-assisted review can verify consistency across the full evidence chain. reviews: + # System review - end-to-end behavior and design overview + - id: FileAssert-System + title: Review of FileAssert System + paths: + - "docs/reqstream/fileassert-system.yaml" + - "docs/design/introduction.md" + - "docs/design/system.md" + - "test/**/IntegrationTests.cs" + + # Subsystem reviews - one per subsystem + - id: FileAssert-Cli + title: Review of FileAssert Cli Subsystem + paths: + - "docs/reqstream/subsystem-cli.yaml" + - "docs/design/cli.md" + - "test/**/CliSubsystemTests.cs" + + - id: FileAssert-Configuration + title: Review of FileAssert Configuration Subsystem + paths: + - "docs/reqstream/subsystem-configuration.yaml" + - "docs/design/configuration.md" + - "test/**/ConfigurationSubsystemTests.cs" + + - id: FileAssert-Modeling + title: Review of FileAssert Modeling Subsystem + paths: + - "docs/reqstream/subsystem-modeling.yaml" + - "docs/design/modeling.md" + - "test/**/ModelingSubsystemTests.cs" + + - id: FileAssert-Utilities + title: Review of FileAssert Utilities Subsystem + paths: + - "docs/reqstream/subsystem-utilities.yaml" + - "docs/design/utilities.md" + - "test/**/UtilitiesSubsystemTests.cs" + + - id: FileAssert-SelfTest + title: Review of FileAssert SelfTest Subsystem + paths: + - "docs/reqstream/subsystem-selftest.yaml" + - "docs/design/selftest.md" + - "test/**/SelfTestSubsystemTests.cs" + # Software unit reviews - one per class - id: FileAssert-Context title: Review of FileAssert Context Unit paths: - "docs/reqstream/unit-context.yaml" + - "docs/design/context.md" - "src/**/Context.cs" - "test/**/ContextTests.cs" + - "test/**/ContextNewPropertiesTests.cs" - id: FileAssert-Program title: Review of FileAssert Program Unit paths: - "docs/reqstream/unit-program.yaml" + - "docs/design/program.md" - "src/**/Program.cs" - "test/**/ProgramTests.cs" - - "test/**/IntegrationTests.cs" - "test/**/Runner.cs" - "test/**/AssemblyInfo.cs" @@ -45,6 +92,7 @@ reviews: title: Review of FileAssert Validation Unit paths: - "docs/reqstream/unit-validation.yaml" + - "docs/design/validation.md" - "src/**/Validation.cs" - "test/**/ValidationTests.cs" @@ -52,6 +100,7 @@ reviews: title: Review of FileAssert PathHelpers Unit paths: - "docs/reqstream/unit-path-helpers.yaml" + - "docs/design/path-helpers.md" - "src/**/PathHelpers.cs" - "test/**/PathHelpersTests.cs" @@ -61,7 +110,6 @@ reviews: - "docs/reqstream/unit-file-assert-rule.yaml" - "docs/design/file-assert-rule.md" - "src/**/FileAssertRule.cs" - - "src/**/FileAssertData.cs" - "test/**/FileAssertRuleTests.cs" - id: FileAssert-FileAssertFile @@ -87,7 +135,12 @@ reviews: - "docs/design/file-assert-config.md" - "src/**/FileAssertConfig.cs" - "test/**/FileAssertConfigTests.cs" - - "test/**/ContextNewPropertiesTests.cs" + + - id: FileAssert-FileAssertData + title: Review of FileAssert FileAssertData Unit + paths: + - "docs/design/file-assert-data.md" + - "src/**/FileAssertData.cs" # Platform and OTS dependency reviews - id: Platform-Support diff --git a/docs/design/cli.md b/docs/design/cli.md new file mode 100644 index 0000000..751f0ca --- /dev/null +++ b/docs/design/cli.md @@ -0,0 +1,40 @@ +# Cli Subsystem Design + +## Overview + +The Cli subsystem is responsible for translating the raw command-line argument array into a +structured, immutable context object that the rest of the tool uses for output, configuration, +and execution decisions. + +## Subsystem Contents + +| Unit | File | Responsibility | +| :-------- | :------------ | :-------------------------------------------------------- | +| `Context` | `Context.cs` | Parses arguments and owns all I/O operations. | + +## Subsystem Responsibilities + +- Parse all supported flags (`--version`, `--help`, `--silent`, `--validate`, `--log`, + `--results`, `--config`) and positional filter arguments. +- Reject unknown or malformed arguments with a descriptive `ArgumentException`. +- Open and manage a log file when `--log` is specified. +- Write output to stdout and the log file; write errors to stderr and the log file. +- Expose an exit code that reflects whether any errors have been reported. + +## Interactions with Other Subsystems + +| Consumer | Usage | +| :---------------- | :------------------------------------------------------------------- | +| Program | Creates a `Context` and passes it to all downstream operations. | +| Configuration | Receives a `Context` to report errors and write progress output. | +| Modeling | Receives a `Context` to write error messages for assertion failures. | +| SelfTest | Receives a `Context` to write validation results and errors. | + +## Design Decisions + +- **Immutable context object**: Properties are set once via `private init` accessors, preventing + accidental mutation after the context is created. +- **Internal ArgumentParser helper**: Argument parsing is encapsulated in a private nested + class, keeping the public `Context` interface focused on output and state rather than parsing. +- **AutoFlush log writer**: The log file stream is opened with `AutoFlush = true` so that log + entries are written to disk immediately, even if the process terminates unexpectedly. diff --git a/docs/design/configuration.md b/docs/design/configuration.md new file mode 100644 index 0000000..10b7d8d --- /dev/null +++ b/docs/design/configuration.md @@ -0,0 +1,58 @@ +# Configuration Subsystem Design + +## Overview + +The Configuration subsystem is responsible for reading the YAML test-suite configuration file +and constructing the domain object hierarchy that drives test execution. It owns the data +transfer objects used during deserialization and the top-level configuration class that loads +and runs the tests. + +## Subsystem Contents + +| Unit | File | Responsibility | +| :----------------- | :--------------------- | :---------------------------------------------------------- | +| `FileAssertConfig` | `FileAssertConfig.cs` | Loads the YAML file and runs the filtered test suite. | +| `FileAssertData` | `FileAssertData.cs` | Data transfer objects for YAML deserialization. | + +## Subsystem Responsibilities + +- Read and deserialize a YAML configuration file using YamlDotNet. +- Tolerate unknown YAML properties for forward compatibility. +- Construct the full `FileAssertTest → FileAssertFile → FileAssertRule` hierarchy from the + deserialized data. +- Resolve the base directory for glob patterns from the configuration file path. +- Filter tests by name or tag before execution. + +## Interactions with Other Subsystems + +| Dependency | Usage | +| :---------- | :-------------------------------------------------------------------------- | +| Cli | Receives a `Context` to report errors and write progress output. | +| Modeling | Delegates test construction to `FileAssertTest.Create` and execution to | +| | `FileAssertTest.Run`. | + +## YAML Configuration Format + +The top-level YAML structure is: + +```yaml +tests: + - name: "Test Name" + tags: + - tag1 + files: + - pattern: "**/*.cs" + min: 1 + rules: + - contains: "Copyright" +``` + +## Design Decisions + +- **Separation of data and domain objects**: The `FileAssertData` classes are pure data holders + with no logic. The Modeling subsystem owns the domain objects built from them. +- **Forward-compatible deserialization**: `IgnoreUnmatchedProperties()` allows configuration + files to contain keys introduced in later tool versions without causing parse failures. +- **Base directory from config path**: Resolving glob patterns relative to the configuration + file location is more intuitive than the working directory, especially when the tool is + invoked from a build script in a different directory. diff --git a/docs/design/context.md b/docs/design/context.md new file mode 100644 index 0000000..555a6b3 --- /dev/null +++ b/docs/design/context.md @@ -0,0 +1,62 @@ +# Context Design + +## Overview + +`Context` is the command-line argument parser and I/O owner for FileAssert. It translates the +raw `string[]` argument array into named properties, manages the optional log file stream, and +provides a unified interface for writing output and errors throughout the tool's execution. + +## Class Structure + +### Properties + +| Property | Type | Description | +| :--------------------- | :---------------------- | :------------------------------------------------------------ | +| `Version` | `bool` | Set when `--version` or `-v` is present. | +| `Help` | `bool` | Set when `--help`, `-h`, or `-?` is present. | +| `Silent` | `bool` | Set when `--silent` is present. | +| `Validate` | `bool` | Set when `--validate` is present. | +| `ResultsFile` | `string?` | Path provided via `--results`, or null. | +| `ConfigFile` | `string` | Path provided via `--config`; defaults to `.fileassert.yaml`. | +| `IsConfigFileExplicit` | `bool` | True when `--config` was explicitly specified. | +| `Filters` | `IReadOnlyList` | Positional arguments treated as test name or tag filters. | +| `ExitCode` | `int` | Returns `1` if any errors have been reported; otherwise `0`. | + +### Factory Method + +```csharp +public static Context Create(string[] args) +``` + +Delegates argument parsing to the private `ArgumentParser` nested class. Opens a log file if +`--log` was specified. Returns the fully initialized `Context` instance. + +### Output Methods + +```csharp +public void WriteLine(string message) +public void WriteError(string message) +``` + +`WriteLine` writes to stdout and the log file (unless `--silent` suppresses console output). +`WriteError` sets the internal error flag, writes to stderr in red (unless silent), and writes +to the log file. + +### Argument Parsing + +The private nested class `ArgumentParser` processes each argument in order: + +- Flag arguments (starting with `--` or `-`) are matched by a `switch` statement. +- Arguments requiring a value (`--log`, `--results`, `--config`) consume the next element + from the argument array and throw `ArgumentException` if no value follows. +- Unknown flag arguments (starting with `-`) throw `ArgumentException`. +- All other arguments are accumulated in the `Filters` list. + +## Design Decisions + +- **Sealed with IDisposable**: The class is sealed to prevent inheritance of internal state, and + implements `IDisposable` to ensure the log file stream is always closed. +- **Factory method**: The `Create` factory method is `public` so tests and the self-validation + tests can construct a context directly without invoking `Main`. +- **Error flag over exception**: `WriteError` sets a flag rather than throwing, so the tool + completes all assertions before reporting a final failure via the exit code. diff --git a/docs/design/definition.yaml b/docs/design/definition.yaml index ed31baa..4556fcf 100644 --- a/docs/design/definition.yaml +++ b/docs/design/definition.yaml @@ -5,10 +5,21 @@ resource-path: input-files: - docs/design/title.txt - docs/design/introduction.md - - docs/design/file-assert-rule.md - - docs/design/file-assert-file.md - - docs/design/file-assert-test.md + - docs/design/system.md + - docs/design/program.md + - docs/design/cli.md + - docs/design/context.md + - docs/design/configuration.md - docs/design/file-assert-config.md + - docs/design/file-assert-data.md + - docs/design/modeling.md + - docs/design/file-assert-test.md + - docs/design/file-assert-file.md + - docs/design/file-assert-rule.md + - docs/design/utilities.md + - docs/design/path-helpers.md + - docs/design/selftest.md + - docs/design/validation.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/design/file-assert-data.md b/docs/design/file-assert-data.md new file mode 100644 index 0000000..66f331b --- /dev/null +++ b/docs/design/file-assert-data.md @@ -0,0 +1,59 @@ +# FileAssertData Design + +## Overview + +`FileAssertData` is the set of YAML data transfer objects (DTOs) used by YamlDotNet to +deserialize the FileAssert configuration file. Each class maps directly to a YAML structure +and is intentionally free of business logic. Domain objects are constructed from these DTOs +by the Modeling subsystem. + +## Class Structure + +### FileAssertRuleData + +Represents a single content validation rule within a file assertion. + +| Property | YAML alias | Type | Description | +| :--------- | :---------- | :-------- | :---------------------------------------------- | +| `Contains` | `contains` | `string?` | Substring that file content must contain. | +| `Matches` | `matches` | `string?` | Regular expression the file content must match. | + +Exactly one property shall be set per rule. The `FileAssertRule.Create` factory enforces this. + +### FileAssertFileData + +Represents a file pattern assertion within a test. + +| Property | YAML alias | Type | Description | +| :-------- | :--------- | :--------------------------- | :----------------------------------------------------------- | +| `Pattern` | `pattern` | `string?` | Glob pattern used to locate files. | +| `Min` | `min` | `int?` | Minimum number of matching files; null means no lower bound. | +| `Max` | `max` | `int?` | Maximum number of matching files; null means no upper bound. | +| `Rules` | `rules` | `List?` | Content rules applied to each matched file. | + +### FileAssertTestData + +Represents a named test within the configuration. + +| Property | YAML alias | Type | Description | +| :------- | :--------- | :--------------------------- | :-------------------------------------------- | +| `Name` | `name` | `string?` | Human-readable name for the test. | +| `Tags` | `tags` | `List?` | Tags used for command-line filter selection. | +| `Files` | `files` | `List?` | File assertions belonging to this test. | + +### FileAssertConfigData + +Represents the top-level configuration document. + +| Property | YAML alias | Type | Description | +| :------- | :--------- | :--------------------------- | :-------------------------------------------- | +| `Tests` | `tests` | `List?` | Tests defined in this configuration file. | + +## Design Decisions + +- **Nullable reference type properties**: All properties are nullable to correctly represent + absent YAML keys without throwing during deserialization. +- **No validation logic in DTOs**: Validation and construction of domain objects is the + responsibility of the factory methods in the Modeling subsystem, keeping DTOs simple. +- **YamlMember aliases**: Explicit `[YamlMember(Alias = "...")]` attributes tie each property + to its YAML key, decoupling C# naming conventions from the YAML schema. diff --git a/docs/design/modeling.md b/docs/design/modeling.md new file mode 100644 index 0000000..8532951 --- /dev/null +++ b/docs/design/modeling.md @@ -0,0 +1,52 @@ +# Modeling Subsystem Design + +## Overview + +The Modeling subsystem contains the domain objects that represent a FileAssert test suite at +runtime. It transforms the data transfer objects produced by the Configuration subsystem into +executable domain objects and drives the assertion logic. + +## Subsystem Contents + +| Unit | File | Responsibility | +| :---------------- | :------------------ | :-------------------------------------------------------------- | +| `FileAssertTest` | `FileAssertTest.cs` | Named test with file assertions and tag-based filter matching. | +| `FileAssertFile` | `FileAssertFile.cs` | Glob pattern matcher with count constraints and content rules. | +| `FileAssertRule` | `FileAssertRule.cs` | Abstract content validation rule hierarchy. | + +## Subsystem Responsibilities + +- Construct domain objects from Configuration DTOs via static factory methods. +- Validate required fields (test name, file pattern, rule type) during construction. +- Execute file glob matching using `Microsoft.Extensions.FileSystemGlobbing`. +- Enforce minimum and maximum file count constraints. +- Apply content rules to matched file text. +- Report assertion failures via the `Context` from the Cli subsystem. + +## Object Hierarchy + +```text +FileAssertTest +└── FileAssertFile (one or more) + └── FileAssertRule (zero or more) + ├── FileAssertContainsRule + └── FileAssertMatchesRule +``` + +## Interactions with Other Subsystems + +| Dependency | Usage | +| :------------ | :------------------------------------------------------- | +| Cli | Receives `Context` to report assertion failures. | +| Configuration | Accepts DTO types for test, file, and rule construction. | + +## Design Decisions + +- **Factory methods over constructors**: Each domain class provides an `internal static Create` + method that validates the DTO and constructs the domain object, keeping constructors private. +- **Error accumulation**: Failures are reported via `context.WriteError` rather than exceptions, + so all assertions run to completion and all failures are visible in a single pass. +- **Glob via FileSystemGlobbing**: Uses the `Microsoft.Extensions.FileSystemGlobbing` library for + cross-platform glob pattern evaluation consistent with the rest of the .NET ecosystem. +- **Compiled regex with timeout**: The `FileAssertMatchesRule` compiles its regex at construction + time and applies a ten-second evaluation timeout to guard against catastrophic backtracking. diff --git a/docs/design/path-helpers.md b/docs/design/path-helpers.md new file mode 100644 index 0000000..4ca47a9 --- /dev/null +++ b/docs/design/path-helpers.md @@ -0,0 +1,38 @@ +# PathHelpers Design + +## Overview + +`PathHelpers` is a static utility class that provides a safe path-combination method. It +protects callers against path-traversal attacks by rejecting relative paths that contain `..` +or that are rooted (absolute) paths. + +## Class Structure + +### SafePathCombine Method + +```csharp +internal static string SafePathCombine(string basePath, string relativePath) +``` + +Combines `basePath` and `relativePath` safely, ensuring the resulting path remains within +the base directory. + +**Validation steps:** + +1. Reject null inputs via `ArgumentNullException.ThrowIfNull`. +2. Reject `relativePath` values that contain `..` (path traversal). +3. Reject `relativePath` values that are rooted (absolute paths). +4. Combine the paths with `Path.Combine`. +5. Compute the full (canonical) paths of both base and combined paths. +6. Use `Path.GetRelativePath` to verify the combined path is still under the base; reject if + it escapes the base directory. + +## Design Decisions + +- **Two-phase validation**: The pre-combine check (steps 2–3) catches obvious traversal + attempts. The post-combine check (steps 5–6) adds defense-in-depth against edge cases that + bypass the initial checks on exotic file systems or path formats. +- **ArgumentException on invalid input**: Callers receive a specific `ArgumentException` + identifying `relativePath` as the problematic parameter, making debugging straightforward. +- **No logging or error accumulation**: `SafePathCombine` is a pure utility method that throws + on invalid input; it does not interact with the `Context` or any output mechanism. diff --git a/docs/design/program.md b/docs/design/program.md new file mode 100644 index 0000000..beaee55 --- /dev/null +++ b/docs/design/program.md @@ -0,0 +1,73 @@ +# Program Design + +## Overview + +`Program` is the entry point for the FileAssert tool. It owns the `Main` method, constructs a +`Context` from command-line arguments, dispatches to the appropriate handler based on context +flags, and returns the final exit code. It also exposes a `Version` property used by both the +version display path and the self-validation header. + +## Class Structure + +### Version Property + +```csharp +public static string Version { get; } +``` + +Reads the informational version from the executing assembly's +`AssemblyInformationalVersionAttribute`. Falls back to the assembly version, then to `"0.0.0"`. + +### Main Method + +```csharp +private static int Main(string[] args) +``` + +Creates a `Context`, calls `Run`, and returns `context.ExitCode`. Catches `ArgumentException` +and `InvalidOperationException` to print expected error messages and return exit code `1`. +Unexpected exceptions are re-thrown after printing the message so that the runtime generates +an event-log entry. + +### Run Method + +```csharp +public static void Run(Context context) +``` + +Inspects context flags in the following priority order: + +| Priority | Condition | Action | +| :------- | :-------------------- | :------------------------------------------ | +| 1 | `context.Version` | Print version string; return. | +| 2 | `context.Help` | Print banner and usage; return. | +| 3 | `context.Validate` | Print banner; delegate to `Validation.Run`. | +| 4 | Default | Print banner; delegate to `RunToolLogic`. | + +### RunToolLogic Method + +```csharp +private static void RunToolLogic(Context context) +``` + +Checks whether the configuration file exists. When the default configuration file is absent, it +prints guidance without setting an error. When an explicit configuration file is absent, it calls +`context.WriteError` to signal failure. When the file exists, it calls +`FileAssertConfig.ReadFromFile` and then `config.Run`. + +## Interactions with Other Units + +| Dependency | Usage | +| :------------------ | :------------------------------------------------------------ | +| `Context` | Created by `Context.Create`; owns all I/O and exit code. | +| `Validation` | Invoked by `Run` when `--validate` is set. | +| `FileAssertConfig` | Loaded from file and executed by `RunToolLogic`. | + +## Design Decisions + +- **Public `Run` method**: `Run` is `public` so that unit tests and the self-validation tests + can invoke it directly without starting a new process. +- **Exception hierarchy**: `ArgumentException` and `InvalidOperationException` are caught as + expected error conditions; all other exceptions propagate to generate crash reports. +- **Version from assembly attribute**: Using `AssemblyInformationalVersionAttribute` allows the + CI pipeline to inject the exact package version (including pre-release labels) at build time. diff --git a/docs/design/selftest.md b/docs/design/selftest.md new file mode 100644 index 0000000..bad81cb --- /dev/null +++ b/docs/design/selftest.md @@ -0,0 +1,39 @@ +# SelfTest Subsystem Design + +## Overview + +The SelfTest subsystem provides built-in self-validation functionality that verifies the core +behavior of the tool at run time. It is invoked via the `--validate` command-line flag and +produces structured test results that can be written to a TRX or JUnit XML file. + +## Subsystem Contents + +| Unit | File | Responsibility | +| :----------- | :-------------- | :------------------------------------------------------- | +| `Validation` | `Validation.cs` | Runs built-in self-validation tests and reports results. | + +## Subsystem Responsibilities + +- Execute a set of built-in test cases that exercise core tool functionality. +- Collect and summarize test outcomes (passed, failed). +- Optionally serialize results to TRX or JUnit XML format. +- Report a system information header before running tests. + +## Interactions with Other Subsystems + +| Dependency | Usage | +| :---------- | :----------------------------------------------------------------- | +| Cli | Receives a `Context` to write output and collect errors. | +| Utilities | Uses `PathHelpers.SafePathCombine` to create temporary file paths. | +| Program | References `Program.Version` for the system information header. | +| | Calls `Program.Run` and `Context.Create` to exercise the tool. | + +## Design Decisions + +- **Self-contained tests**: Each built-in test creates its own `Context` with a temporary log + file, runs `Program.Run`, and inspects the output. No external test framework is required at + run time. +- **Results serialization**: TRX format is used when the results file has a `.trx` extension; + JUnit XML is used for `.xml`. Unsupported extensions are reported as errors. +- **Temporary directory cleanup**: Each test uses a disposable `TemporaryDirectory` helper that + deletes its directory on disposal, preventing accumulation of temporary files. diff --git a/docs/design/system.md b/docs/design/system.md new file mode 100644 index 0000000..9912ed9 --- /dev/null +++ b/docs/design/system.md @@ -0,0 +1,68 @@ +# FileAssert System Design + +## Overview + +FileAssert is a .NET command-line tool for asserting file properties using YAML-defined test +suites. It is packaged as a .NET global tool and invoked as `fileassert`. The tool accepts a +configuration file, evaluates glob patterns against the file system, and reports failures when +files do not meet the declared constraints. + +## System-Level Responsibilities + +| Responsibility | Description | +| :---------------------- | :------------------------------------------------------------------------ | +| Argument parsing | Accept and validate command-line flags and positional filter arguments. | +| Configuration loading | Read and deserialize a YAML test-suite configuration file. | +| Test execution | Run selected tests, evaluating file patterns and content rules. | +| Output and logging | Report results to stdout/stderr and optionally to a log file. | +| Self-validation | Verify core functionality at run time via built-in tests. | +| Results serialization | Write test outcome records to TRX or JUnit XML format. | + +## Software Item Hierarchy + +The system is decomposed into one top-level unit (Program) and five subsystems, each containing +one or more units: + +| Item | Level | Units contained | +| :------------ | :-------- | :-------------------------------------------------------- | +| FileAssert | System | — | +| Program | Unit | — | +| Cli | Subsystem | Context | +| Configuration | Subsystem | FileAssertConfig, FileAssertData | +| Modeling | Subsystem | FileAssertTest, FileAssertFile, FileAssertRule | +| Utilities | Subsystem | PathHelpers | +| SelfTest | Subsystem | Validation | + +## Execution Flow + +The following sequence describes the normal execution path: + +1. `Program.Main` creates a `Context` instance by parsing command-line arguments. +2. `Program.Run` inspects context flags in priority order: + a. `--version` — prints the version string and exits. + b. `--help` — prints usage information and exits. + c. `--validate` — delegates to `Validation.Run` for self-validation and exits. + d. Default — delegates to `Program.RunToolLogic`. +3. `RunToolLogic` checks for the configuration file. If absent, it prints guidance (default + path) or an error (explicit path) and exits. +4. `FileAssertConfig.ReadFromFile` deserializes the YAML configuration into a hierarchy of + `FileAssertTest`, `FileAssertFile`, and `FileAssertRule` instances. +5. `FileAssertConfig.Run` filters the test list against `context.Filters` and executes each + matching test. +6. Each `FileAssertTest.Run` iterates its `FileAssertFile` list. +7. Each `FileAssertFile.Run` discovers files via a glob matcher, validates count constraints, + and applies content rules. +8. Content rules (`FileAssertContainsRule`, `FileAssertMatchesRule`) call + `context.WriteError` on failure. +9. After all tests complete, `context.ExitCode` reflects whether any errors occurred. + +## Design Decisions + +- **Single-assembly tool**: All logic is compiled into one assembly and published as a .NET + global tool, simplifying installation and avoiding DLL management. +- **YAML configuration**: YAML is human-readable, widely supported, and natively handled by + YamlDotNet. The `IgnoreUnmatchedProperties` setting provides forward compatibility. +- **Internal visibility**: All classes except test-facing members are `internal`, limiting the + public API surface to what is strictly necessary. +- **Error accumulation**: Failures are accumulated via `Context.WriteError` rather than + exceptions, so all assertions in a run are reported in a single pass. diff --git a/docs/design/utilities.md b/docs/design/utilities.md new file mode 100644 index 0000000..9f2f7e1 --- /dev/null +++ b/docs/design/utilities.md @@ -0,0 +1,31 @@ +# Utilities Subsystem Design + +## Overview + +The Utilities subsystem provides shared helper functionality used by other subsystems. It +contains security-sensitive or otherwise reusable operations that do not belong to any specific +domain subsystem. + +## Subsystem Contents + +| Unit | File | Responsibility | +| :------------ | :--------------- | :------------------------------------------------------------ | +| `PathHelpers` | `PathHelpers.cs` | Safe path-combination utility with path-traversal protection. | + +## Subsystem Responsibilities + +- Provide path utilities that safely combine paths while preventing path-traversal attacks. +- Reject relative paths containing `..` or absolute paths when a relative path is expected. + +## Interactions with Other Subsystems + +| Consumer | Usage | +| :-------- | :----------------------------------------------------------------------- | +| SelfTest | Uses `PathHelpers.SafePathCombine` when creating temporary log files. | + +## Design Decisions + +- **Static class**: `PathHelpers` is a static utility class with no instance state, suitable + for use anywhere in the codebase without injection. +- **Defense-in-depth validation**: Path safety is validated both before and after combining + paths, guarding against edge cases that might bypass the initial checks. diff --git a/docs/design/validation.md b/docs/design/validation.md new file mode 100644 index 0000000..b776bd6 --- /dev/null +++ b/docs/design/validation.md @@ -0,0 +1,67 @@ +# Validation Design + +## Overview + +`Validation` is a static class that implements the self-validation test runner for FileAssert. +It runs a series of built-in tests that exercise the tool's core functionality, prints a +structured report to the context output, and optionally writes results to a TRX or JUnit XML +file. + +## Class Structure + +### Run Method + +```csharp +public static void Run(Context context) +``` + +Entry point for self-validation. Executes the following steps: + +1. Prints a system information table (tool version, machine name, OS, .NET runtime, timestamp). +2. Creates a `TestResults` collection. +3. Runs each built-in test, adding results to the collection. +4. Prints a pass/fail summary. +5. Writes the results file if `context.ResultsFile` is set. + +### Built-in Tests + +| Test Name | Description | +| :-------------------------- | :---------------------------------------------------------------- | +| `FileAssert_VersionDisplay` | Runs `--version`; verifies log contains a version string. | +| `FileAssert_HelpDisplay` | Runs `--help`; verifies log contains `"Usage:"` and `"Options:"`. | + +Each test: + +1. Creates a temporary directory. +2. Builds a `Context` with `--silent` and `--log` pointing to a file in that directory. +3. Calls `Program.Run`. +4. Reads the log file and asserts its contents. +5. Records the outcome in the `TestResults` collection. + +### Results Serialization + +```csharp +private static void WriteResultsFile(Context context, TestResults testResults) +``` + +Writes the collected results to the file specified by `context.ResultsFile`: + +- `.trx` extension → TRX format via `TrxSerializer.Serialize`. +- `.xml` extension → JUnit XML format via `JUnitSerializer.Serialize`. +- Other extensions → error written to context. + +### TemporaryDirectory Helper + +A private nested `IDisposable` class that creates a unique temporary directory on construction +and deletes it recursively on disposal. Uses `PathHelpers.SafePathCombine` to build the +directory path under `Path.GetTempPath()`. + +## Design Decisions + +- **Generic exception catch in test methods**: Each built-in test wraps its body in a + `try/catch (Exception)` to record any unexpected exception as a test failure rather than + crashing the self-validation run. +- **Separation of pass/fail summary**: The pass/fail counts are printed only after all tests + complete, so the summary reflects the full run. +- **`Program.Run` as the test target**: Using the public `Run` method rather than the private + `Main` method allows tests to capture the log output without spawning a subprocess. diff --git a/docs/reqstream/fileassert-system.yaml b/docs/reqstream/fileassert-system.yaml new file mode 100644 index 0000000..f7caeb9 --- /dev/null +++ b/docs/reqstream/fileassert-system.yaml @@ -0,0 +1,49 @@ +--- +# Software System Requirements for FileAssert +# +# These requirements describe the observable, end-to-end behavior of the FileAssert +# tool as experienced by users. They are verified by integration tests that exercise +# the tool as a complete, deployed unit. + +sections: + - title: FileAssert System Requirements + requirements: + - id: FileAssert-System-CoreExecution + title: | + The FileAssert tool shall load a YAML configuration file and run file assertions, + returning a zero exit code when all assertions pass. + justification: | + The primary purpose of the tool is to assert properties of files on the file system + using YAML-defined test suites. Verifying this core behavior at the system level + confirms that all subsystems integrate correctly to produce the expected outcome. + tests: + - IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero + + - id: FileAssert-System-FailureDetection + title: | + The FileAssert tool shall return a non-zero exit code when one or more file + assertions fail. + justification: | + CI/CD pipelines and scripts depend on the exit code to detect assertion failures. + A non-zero exit code when an assertion fails is the primary mechanism for the tool + to signal that files do not meet the declared constraints. + tests: + - IntegrationTest_ValidConfig_FailingAssertions_ReturnsNonZero + + - id: FileAssert-System-SilentOutput + title: The FileAssert tool shall suppress console output when invoked with the --silent flag. + justification: | + Automated pipelines that parse or redirect tool output must be able to run the tool + without cluttering their own output. The --silent flag provides this capability + while still allowing exit code and log file output to carry result information. + tests: + - IntegrationTest_SilentFlag_SuppressesOutput + + - id: FileAssert-System-LogFile + title: The FileAssert tool shall write all output to a log file when invoked with the --log flag. + justification: | + Persistent log files allow post-hoc inspection of tool output in CI/CD environments + where console output may be discarded. The log file must capture all messages that + would otherwise appear on the console. + tests: + - IntegrationTest_LogFlag_WritesOutputToFile diff --git a/docs/reqstream/subsystem-cli.yaml b/docs/reqstream/subsystem-cli.yaml new file mode 100644 index 0000000..03c91bd --- /dev/null +++ b/docs/reqstream/subsystem-cli.yaml @@ -0,0 +1,28 @@ +--- +# Software Subsystem Requirements for the Cli Subsystem +# +# The Cli subsystem is responsible for translating the raw command-line argument +# array into a structured, immutable context object and providing a unified +# interface for output and logging throughout the tool's execution. + +sections: + - title: Cli Subsystem Requirements + requirements: + - id: FileAssert-CliSubsystem-ArgumentParsing + title: The Cli subsystem shall parse command-line arguments and expose them as structured properties. + justification: | + A dedicated argument-parsing subsystem provides a single point of truth for all + command-line interface logic, keeping the rest of the tool free of raw argument + handling. It also enables consistent unit and integration testing of argument + behavior independently of business logic. + tests: + - CliSubsystem_CreateContext_ParsesAllSupportedFlags + + - id: FileAssert-CliSubsystem-OutputPipeline + title: The Cli subsystem shall route output and errors to the console and optional log file. + justification: | + A unified output pipeline ensures that all messages, whether informational or + error-level, are consistently written to both the console and any active log file. + This provides a reliable audit trail for automated environments and CI/CD pipelines. + tests: + - CliSubsystem_OutputPipeline_WritesMessagesToLogFile diff --git a/docs/reqstream/subsystem-configuration.yaml b/docs/reqstream/subsystem-configuration.yaml new file mode 100644 index 0000000..d73dea5 --- /dev/null +++ b/docs/reqstream/subsystem-configuration.yaml @@ -0,0 +1,30 @@ +--- +# Software Subsystem Requirements for the Configuration Subsystem +# +# The Configuration subsystem is responsible for reading the YAML test-suite +# configuration file and constructing the domain object hierarchy that drives +# test execution. It owns the data transfer objects used during deserialization +# and the top-level configuration class. + +sections: + - title: Configuration Subsystem Requirements + requirements: + - id: FileAssert-ConfigurationSubsystem-LoadAndBuild + title: The Configuration subsystem shall load a YAML configuration file and build a complete test hierarchy. + justification: | + The Configuration subsystem must correctly translate a YAML configuration document + into the full hierarchy of domain objects (tests, file assertions, rules) so that + the Modeling subsystem can execute them. Verifying this end-to-end pipeline at the + subsystem level ensures that the deserialization and domain-object construction + stages interact correctly. + tests: + - ConfigurationSubsystem_LoadYaml_BuildsCompleteTestHierarchy + + - id: FileAssert-ConfigurationSubsystem-FilterExecution + title: The Configuration subsystem shall execute only tests that match the provided name or tag filters. + justification: | + Selective test execution based on names and tags is a core feature of the + Configuration subsystem. Testing this capability at the subsystem level verifies + that filtering works correctly across the full load-and-run pipeline. + tests: + - ConfigurationSubsystem_RunWithFilter_ExecutesOnlyMatchingTests diff --git a/docs/reqstream/subsystem-modeling.yaml b/docs/reqstream/subsystem-modeling.yaml new file mode 100644 index 0000000..789861e --- /dev/null +++ b/docs/reqstream/subsystem-modeling.yaml @@ -0,0 +1,27 @@ +--- +# Software Subsystem Requirements for the Modeling Subsystem +# +# The Modeling subsystem contains the domain objects that represent a FileAssert +# test suite at runtime. It transforms Configuration DTOs into executable domain +# objects and drives the assertion logic through the test-file-rule hierarchy. + +sections: + - title: Modeling Subsystem Requirements + requirements: + - id: FileAssert-ModelingSubsystem-ExecutionChain + title: The Modeling subsystem shall execute the full test-file-rule chain and pass when all constraints are met. + justification: | + The Modeling subsystem integrates three unit types (FileAssertTest, FileAssertFile, + FileAssertRule) into a single execution chain. Verifying the chain end-to-end at the + subsystem level ensures that inter-unit delegation and data flow work correctly. + tests: + - ModelingSubsystem_ExecuteChain_PassesWhenAllConstraintsMet + + - id: FileAssert-ModelingSubsystem-FailureReporting + title: The Modeling subsystem shall report failures through the context when assertions are violated. + justification: | + Assertion failures must propagate from the innermost rule through the file and test + layers to the context, setting the error exit code. Testing this at the subsystem + level verifies that the full error-reporting pipeline within the subsystem works. + tests: + - ModelingSubsystem_ExecuteChain_ReportsFailuresThroughContext diff --git a/docs/reqstream/subsystem-selftest.yaml b/docs/reqstream/subsystem-selftest.yaml new file mode 100644 index 0000000..b2497e4 --- /dev/null +++ b/docs/reqstream/subsystem-selftest.yaml @@ -0,0 +1,19 @@ +--- +# Software Subsystem Requirements for the SelfTest Subsystem +# +# The SelfTest subsystem provides built-in self-validation functionality that +# verifies the core behavior of the tool at run time. It is invoked via the +# --validate command-line flag and produces structured test results. + +sections: + - title: SelfTest Subsystem Requirements + requirements: + - id: FileAssert-SelfTestSubsystem-ValidationPipeline + title: The SelfTest subsystem shall run built-in validation tests and produce a structured summary. + justification: | + The SelfTest subsystem integrates the Validation unit with the Cli and Utilities + subsystems to produce a complete self-validation report. Testing the full pipeline + at the subsystem level verifies that test execution, result collection, and summary + output all work together correctly. + tests: + - SelfTestSubsystem_Run_ExecutesBuiltInTestsAndProducesSummary diff --git a/docs/reqstream/subsystem-utilities.yaml b/docs/reqstream/subsystem-utilities.yaml new file mode 100644 index 0000000..08c3218 --- /dev/null +++ b/docs/reqstream/subsystem-utilities.yaml @@ -0,0 +1,19 @@ +--- +# Software Subsystem Requirements for the Utilities Subsystem +# +# The Utilities subsystem provides shared helper functionality used by other +# subsystems. It contains security-sensitive operations that do not belong to +# any specific domain subsystem. + +sections: + - title: Utilities Subsystem Requirements + requirements: + - id: FileAssert-UtilitiesSubsystem-SafePathOperations + title: The Utilities subsystem shall provide safe path operations that prevent path traversal. + justification: | + Security-sensitive path operations must be isolated in a dedicated subsystem so + that they can be verified in isolation and reused across the codebase without + duplicating path-safety logic. Testing at the subsystem level confirms that the + utility functions are accessible to and correctly used by dependent subsystems. + tests: + - UtilitiesSubsystem_SafePathCombine_PreventsPathTraversalToFileSystem diff --git a/requirements.yaml b/requirements.yaml index d526362..7bafaa9 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -1,6 +1,12 @@ --- # Root requirements file - includes all subsystem, platform, and OTS requirements includes: + - docs/reqstream/fileassert-system.yaml + - docs/reqstream/subsystem-cli.yaml + - docs/reqstream/subsystem-configuration.yaml + - docs/reqstream/subsystem-modeling.yaml + - docs/reqstream/subsystem-utilities.yaml + - docs/reqstream/subsystem-selftest.yaml - docs/reqstream/unit-context.yaml - docs/reqstream/unit-program.yaml - docs/reqstream/unit-validation.yaml diff --git a/src/DemaConsulting.FileAssert/Context.cs b/src/DemaConsulting.FileAssert/Cli/Context.cs similarity index 99% rename from src/DemaConsulting.FileAssert/Context.cs rename to src/DemaConsulting.FileAssert/Cli/Context.cs index 8e29de3..505fa81 100644 --- a/src/DemaConsulting.FileAssert/Context.cs +++ b/src/DemaConsulting.FileAssert/Cli/Context.cs @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert; +namespace DemaConsulting.FileAssert.Cli; /// /// Context class that handles command-line arguments and program output. diff --git a/src/DemaConsulting.FileAssert/FileAssertConfig.cs b/src/DemaConsulting.FileAssert/Configuration/FileAssertConfig.cs similarity index 97% rename from src/DemaConsulting.FileAssert/FileAssertConfig.cs rename to src/DemaConsulting.FileAssert/Configuration/FileAssertConfig.cs index 88db305..4b7bc45 100644 --- a/src/DemaConsulting.FileAssert/FileAssertConfig.cs +++ b/src/DemaConsulting.FileAssert/Configuration/FileAssertConfig.cs @@ -18,9 +18,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Modeling; using YamlDotNet.Serialization; -namespace DemaConsulting.FileAssert; +namespace DemaConsulting.FileAssert.Configuration; /// /// Top-level configuration that loads a YAML file and runs its defined tests. diff --git a/src/DemaConsulting.FileAssert/FileAssertData.cs b/src/DemaConsulting.FileAssert/Configuration/FileAssertData.cs similarity index 98% rename from src/DemaConsulting.FileAssert/FileAssertData.cs rename to src/DemaConsulting.FileAssert/Configuration/FileAssertData.cs index 1e2cb51..abc884c 100644 --- a/src/DemaConsulting.FileAssert/FileAssertData.cs +++ b/src/DemaConsulting.FileAssert/Configuration/FileAssertData.cs @@ -20,7 +20,7 @@ using YamlDotNet.Serialization; -namespace DemaConsulting.FileAssert; +namespace DemaConsulting.FileAssert.Configuration; /// /// YAML data transfer object representing a single file assertion rule. diff --git a/src/DemaConsulting.FileAssert/FileAssertFile.cs b/src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs similarity index 97% rename from src/DemaConsulting.FileAssert/FileAssertFile.cs rename to src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs index 9406d83..2589ba6 100644 --- a/src/DemaConsulting.FileAssert/FileAssertFile.cs +++ b/src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs @@ -18,10 +18,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; -namespace DemaConsulting.FileAssert; +namespace DemaConsulting.FileAssert.Modeling; /// /// Represents a glob file pattern with optional count constraints and content rules. diff --git a/src/DemaConsulting.FileAssert/FileAssertRule.cs b/src/DemaConsulting.FileAssert/Modeling/FileAssertRule.cs similarity index 98% rename from src/DemaConsulting.FileAssert/FileAssertRule.cs rename to src/DemaConsulting.FileAssert/Modeling/FileAssertRule.cs index ef62467..99bc97a 100644 --- a/src/DemaConsulting.FileAssert/FileAssertRule.cs +++ b/src/DemaConsulting.FileAssert/Modeling/FileAssertRule.cs @@ -19,8 +19,10 @@ // SOFTWARE. using System.Text.RegularExpressions; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; -namespace DemaConsulting.FileAssert; +namespace DemaConsulting.FileAssert.Modeling; /// /// Abstract base class representing a content validation rule applied to file content. diff --git a/src/DemaConsulting.FileAssert/FileAssertTest.cs b/src/DemaConsulting.FileAssert/Modeling/FileAssertTest.cs similarity index 97% rename from src/DemaConsulting.FileAssert/FileAssertTest.cs rename to src/DemaConsulting.FileAssert/Modeling/FileAssertTest.cs index 9b82683..ea45894 100644 --- a/src/DemaConsulting.FileAssert/FileAssertTest.cs +++ b/src/DemaConsulting.FileAssert/Modeling/FileAssertTest.cs @@ -18,7 +18,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; + +namespace DemaConsulting.FileAssert.Modeling; /// /// Represents a named test containing file assertions that can be filtered by name or tag. diff --git a/src/DemaConsulting.FileAssert/Program.cs b/src/DemaConsulting.FileAssert/Program.cs index ec15d91..a898e84 100644 --- a/src/DemaConsulting.FileAssert/Program.cs +++ b/src/DemaConsulting.FileAssert/Program.cs @@ -19,6 +19,9 @@ // SOFTWARE. using System.Reflection; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; +using DemaConsulting.FileAssert.SelfTest; namespace DemaConsulting.FileAssert; diff --git a/src/DemaConsulting.FileAssert/Validation.cs b/src/DemaConsulting.FileAssert/SelfTest/Validation.cs similarity index 99% rename from src/DemaConsulting.FileAssert/Validation.cs rename to src/DemaConsulting.FileAssert/SelfTest/Validation.cs index 3538d0f..fe7c6ed 100644 --- a/src/DemaConsulting.FileAssert/Validation.cs +++ b/src/DemaConsulting.FileAssert/SelfTest/Validation.cs @@ -19,9 +19,11 @@ // SOFTWARE. using System.Runtime.InteropServices; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Utilities; using DemaConsulting.TestResults.IO; -namespace DemaConsulting.FileAssert; +namespace DemaConsulting.FileAssert.SelfTest; /// /// Provides self-validation functionality for FileAssert. diff --git a/src/DemaConsulting.FileAssert/PathHelpers.cs b/src/DemaConsulting.FileAssert/Utilities/PathHelpers.cs similarity index 98% rename from src/DemaConsulting.FileAssert/PathHelpers.cs rename to src/DemaConsulting.FileAssert/Utilities/PathHelpers.cs index 9300a7b..bda596e 100644 --- a/src/DemaConsulting.FileAssert/PathHelpers.cs +++ b/src/DemaConsulting.FileAssert/Utilities/PathHelpers.cs @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert; +namespace DemaConsulting.FileAssert.Utilities; /// /// Helper utilities for safe path operations. diff --git a/test/DemaConsulting.FileAssert.Tests/Cli/CliSubsystemTests.cs b/test/DemaConsulting.FileAssert.Tests/Cli/CliSubsystemTests.cs new file mode 100644 index 0000000..c0c3321 --- /dev/null +++ b/test/DemaConsulting.FileAssert.Tests/Cli/CliSubsystemTests.cs @@ -0,0 +1,97 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.FileAssert.Cli; + +namespace DemaConsulting.FileAssert.Tests.Cli; + +/// +/// Subsystem tests for the Cli subsystem. +/// +[TestClass] +public class CliSubsystemTests +{ + /// + /// Verifies that the Cli subsystem correctly parses all supported flags + /// into structured properties on a single context. + /// + [TestMethod] + public void CliSubsystem_CreateContext_ParsesAllSupportedFlags() + { + // Arrange - build an argument list with all supported flags + var tempDir = Directory.CreateTempSubdirectory("fileassert_cli_"); + try + { + var logPath = Path.Combine(tempDir.FullName, "out.log"); + + // Act - create a context with the silent, validate, and log flags + using (var context = Context.Create( + [ + "--silent", + "--validate", + "--log", logPath + ])) + { + // Assert - all flags are reflected in the context properties + Assert.IsTrue(context.Silent); + Assert.IsTrue(context.Validate); + Assert.IsFalse(context.Version); + Assert.IsFalse(context.Help); + Assert.AreEqual(".fileassert.yaml", context.ConfigFile); + Assert.AreEqual(0, context.ExitCode); + } + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Verifies that the Cli subsystem routes both informational and error messages + /// through the log file when a log path is specified. + /// + [TestMethod] + public void CliSubsystem_OutputPipeline_WritesMessagesToLogFile() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_cli_"); + try + { + var logPath = Path.Combine(tempDir.FullName, "out.log"); + + // Act - create a silent context with logging, write messages, dispose to flush + using (var context = Context.Create(["--silent", "--log", logPath])) + { + context.WriteLine("informational message"); + context.WriteError("error message"); + } + + // Assert - both messages appear in the log file + var logContent = File.ReadAllText(logPath); + Assert.IsTrue(logContent.Contains("informational message")); + Assert.IsTrue(logContent.Contains("error message")); + } + finally + { + tempDir.Delete(recursive: true); + } + } +} diff --git a/test/DemaConsulting.FileAssert.Tests/ContextNewPropertiesTests.cs b/test/DemaConsulting.FileAssert.Tests/Cli/ContextNewPropertiesTests.cs similarity index 98% rename from test/DemaConsulting.FileAssert.Tests/ContextNewPropertiesTests.cs rename to test/DemaConsulting.FileAssert.Tests/Cli/ContextNewPropertiesTests.cs index 12bc2e1..e07409f 100644 --- a/test/DemaConsulting.FileAssert.Tests/ContextNewPropertiesTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Cli/ContextNewPropertiesTests.cs @@ -18,7 +18,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert.Tests; +using DemaConsulting.FileAssert.Cli; + +namespace DemaConsulting.FileAssert.Tests.Cli; /// /// Unit tests for the new ConfigFile, Filters, and --config features of . diff --git a/test/DemaConsulting.FileAssert.Tests/ContextTests.cs b/test/DemaConsulting.FileAssert.Tests/Cli/ContextTests.cs similarity index 99% rename from test/DemaConsulting.FileAssert.Tests/ContextTests.cs rename to test/DemaConsulting.FileAssert.Tests/Cli/ContextTests.cs index 6b5392e..5059865 100644 --- a/test/DemaConsulting.FileAssert.Tests/ContextTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Cli/ContextTests.cs @@ -18,7 +18,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert.Tests; +using DemaConsulting.FileAssert.Cli; + +namespace DemaConsulting.FileAssert.Tests.Cli; /// /// Unit tests for the Context class. diff --git a/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationSubsystemTests.cs b/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationSubsystemTests.cs new file mode 100644 index 0000000..43e03ae --- /dev/null +++ b/test/DemaConsulting.FileAssert.Tests/Configuration/ConfigurationSubsystemTests.cs @@ -0,0 +1,118 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; + +namespace DemaConsulting.FileAssert.Tests.Configuration; + +/// +/// Subsystem tests for the Configuration subsystem. +/// +[TestClass] +public class ConfigurationSubsystemTests +{ + /// + /// Verifies that the Configuration subsystem loads a YAML file and builds the + /// complete test hierarchy (tests → files → rules) correctly. + /// + [TestMethod] + public void ConfigurationSubsystem_LoadYaml_BuildsCompleteTestHierarchy() + { + // Arrange - write a YAML configuration with nested test, file, and rule entries + var tempDir = Directory.CreateTempSubdirectory("fileassert_config_"); + try + { + var configPath = Path.Combine(tempDir.FullName, "config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "License Check" + tags: + - license + files: + - pattern: "**/*.txt" + min: 1 + rules: + - contains: "Copyright" + """); + + // Act + var config = FileAssertConfig.ReadFromFile(configPath); + + // Assert - the full hierarchy is correctly constructed + Assert.AreEqual(1, config.Tests.Count); + var test = config.Tests[0]; + Assert.AreEqual("License Check", test.Name); + Assert.AreEqual(1, test.Tags.Count); + Assert.AreEqual("license", test.Tags[0]); + Assert.AreEqual(1, test.Files.Count); + var file = test.Files[0]; + Assert.AreEqual("**/*.txt", file.Pattern); + Assert.AreEqual(1, file.Min); + Assert.AreEqual(1, file.Rules.Count); + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Verifies that the Configuration subsystem executes only tests that match + /// the provided filters when running a configuration with multiple tests. + /// + [TestMethod] + public void ConfigurationSubsystem_RunWithFilter_ExecutesOnlyMatchingTests() + { + // Arrange - two tests in config; only one file exists so only that test should pass + var tempDir = Directory.CreateTempSubdirectory("fileassert_config_"); + try + { + var configPath = Path.Combine(tempDir.FullName, "config.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "Alpha" + files: + - pattern: "alpha.txt" + min: 1 + - name: "Beta" + files: + - pattern: "beta.txt" + min: 1 + """); + + // Create only alpha.txt so the Alpha test passes and Beta would fail + File.WriteAllText(Path.Combine(tempDir.FullName, "alpha.txt"), "content"); + + var config = FileAssertConfig.ReadFromFile(configPath); + using var context = Context.Create(["--silent"]); + + // Act - run with the "Alpha" filter only + config.Run(context, ["Alpha"]); + + // Assert - no errors because only Alpha ran (and alpha.txt exists) + Assert.AreEqual(0, context.ExitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } +} diff --git a/test/DemaConsulting.FileAssert.Tests/FileAssertConfigTests.cs b/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs similarity index 97% rename from test/DemaConsulting.FileAssert.Tests/FileAssertConfigTests.cs rename to test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs index 7e0f314..e31de17 100644 --- a/test/DemaConsulting.FileAssert.Tests/FileAssertConfigTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Configuration/FileAssertConfigTests.cs @@ -18,7 +18,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert.Tests; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; +using DemaConsulting.FileAssert.Modeling; + +namespace DemaConsulting.FileAssert.Tests.Configuration; /// /// Unit tests for the class. diff --git a/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs b/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs index 03d0783..f20522f 100644 --- a/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/IntegrationTests.cs @@ -18,6 +18,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using DemaConsulting.FileAssert.Utilities; + namespace DemaConsulting.FileAssert.Tests; /// @@ -245,4 +247,88 @@ public void IntegrationTest_UnknownArgument_ReturnsError() Assert.AreNotEqual(0, exitCode); Assert.Contains("Error", output); } + + /// + /// Test that a valid configuration file causes the tool to run assertions and succeed. + /// + [TestMethod] + public void IntegrationTest_ValidConfig_PassingAssertions_ReturnsZero() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); + try + { + // Create a file that satisfies the assertion + File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "Copyright (c) DEMA Consulting"); + + // Write a config that asserts the file exists and contains the expected text + var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "License Check" + files: + - pattern: "*.txt" + min: 1 + rules: + - contains: "Copyright" + """); + + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--config", + configPath); + + // Assert + Assert.AreEqual(0, exitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Test that a configuration file with a failing assertion causes the tool to return non-zero. + /// + [TestMethod] + public void IntegrationTest_ValidConfig_FailingAssertions_ReturnsNonZero() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_integration_"); + try + { + // Create a file that does NOT satisfy the assertion + File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "no license here"); + + // Write a config that asserts the file contains text it does not contain + var configPath = Path.Combine(tempDir.FullName, ".fileassert.yaml"); + File.WriteAllText(configPath, """ + tests: + - name: "License Check" + files: + - pattern: "*.txt" + rules: + - contains: "Copyright" + """); + + // Act + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--silent", + "--config", + configPath); + + // Assert - non-zero exit code indicates assertion failure + Assert.AreNotEqual(0, exitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } } diff --git a/test/DemaConsulting.FileAssert.Tests/FileAssertFileTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs similarity index 97% rename from test/DemaConsulting.FileAssert.Tests/FileAssertFileTests.cs rename to test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs index b80a0f6..5512131 100644 --- a/test/DemaConsulting.FileAssert.Tests/FileAssertFileTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertFileTests.cs @@ -18,7 +18,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert.Tests; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; +using DemaConsulting.FileAssert.Modeling; + +namespace DemaConsulting.FileAssert.Tests.Modeling; /// /// Unit tests for the class. diff --git a/test/DemaConsulting.FileAssert.Tests/FileAssertRuleTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertRuleTests.cs similarity index 96% rename from test/DemaConsulting.FileAssert.Tests/FileAssertRuleTests.cs rename to test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertRuleTests.cs index dfaff39..f5910f9 100644 --- a/test/DemaConsulting.FileAssert.Tests/FileAssertRuleTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertRuleTests.cs @@ -18,7 +18,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert.Tests; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; +using DemaConsulting.FileAssert.Modeling; + +namespace DemaConsulting.FileAssert.Tests.Modeling; /// /// Unit tests for and its derived rule classes. diff --git a/test/DemaConsulting.FileAssert.Tests/FileAssertTestTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs similarity index 97% rename from test/DemaConsulting.FileAssert.Tests/FileAssertTestTests.cs rename to test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs index 2bf8c3a..3e02aa4 100644 --- a/test/DemaConsulting.FileAssert.Tests/FileAssertTestTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/FileAssertTestTests.cs @@ -18,7 +18,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert.Tests; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; +using DemaConsulting.FileAssert.Modeling; + +namespace DemaConsulting.FileAssert.Tests.Modeling; /// /// Unit tests for the class. diff --git a/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingSubsystemTests.cs b/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingSubsystemTests.cs new file mode 100644 index 0000000..5f8c0cb --- /dev/null +++ b/test/DemaConsulting.FileAssert.Tests/Modeling/ModelingSubsystemTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.Configuration; +using DemaConsulting.FileAssert.Modeling; + +namespace DemaConsulting.FileAssert.Tests.Modeling; + +/// +/// Subsystem tests for the Modeling subsystem. +/// +[TestClass] +public class ModelingSubsystemTests +{ + /// + /// Verifies that the Modeling subsystem executes the full test → file → rule + /// chain without errors when all constraints are satisfied. + /// + [TestMethod] + public void ModelingSubsystem_ExecuteChain_PassesWhenAllConstraintsMet() + { + // Arrange - create a real file with content that satisfies all rules + var tempDir = Directory.CreateTempSubdirectory("fileassert_modeling_"); + try + { + File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "Copyright (c) DEMA Consulting"); + + var testData = new FileAssertTestData + { + Name = "License Check", + Files = + [ + new FileAssertFileData + { + Pattern = "*.txt", + Min = 1, + Rules = + [ + new FileAssertRuleData { Contains = "Copyright" } + ] + } + ] + }; + + var test = FileAssertTest.Create(testData); + using var context = Context.Create(["--silent"]); + + // Act + test.Run(context, tempDir.FullName); + + // Assert - no errors reported + Assert.AreEqual(0, context.ExitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } + + /// + /// Verifies that the Modeling subsystem reports failures through the context + /// when a content rule is not satisfied. + /// + [TestMethod] + public void ModelingSubsystem_ExecuteChain_ReportsFailuresThroughContext() + { + // Arrange - create a file that does NOT contain the required text + var tempDir = Directory.CreateTempSubdirectory("fileassert_modeling_"); + try + { + File.WriteAllText(Path.Combine(tempDir.FullName, "sample.txt"), "no license header here"); + + var testData = new FileAssertTestData + { + Name = "License Check", + Files = + [ + new FileAssertFileData + { + Pattern = "*.txt", + Rules = + [ + new FileAssertRuleData { Contains = "Copyright" } + ] + } + ] + }; + + var test = FileAssertTest.Create(testData); + using var context = Context.Create(["--silent"]); + + // Act + test.Run(context, tempDir.FullName); + + // Assert - an error was reported and the exit code is non-zero + Assert.AreEqual(1, context.ExitCode); + } + finally + { + tempDir.Delete(recursive: true); + } + } +} diff --git a/test/DemaConsulting.FileAssert.Tests/ProgramTests.cs b/test/DemaConsulting.FileAssert.Tests/ProgramTests.cs index 58713d2..7ef7e14 100644 --- a/test/DemaConsulting.FileAssert.Tests/ProgramTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/ProgramTests.cs @@ -18,6 +18,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using DemaConsulting.FileAssert.Cli; + namespace DemaConsulting.FileAssert.Tests; /// diff --git a/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestSubsystemTests.cs b/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestSubsystemTests.cs new file mode 100644 index 0000000..2edacf6 --- /dev/null +++ b/test/DemaConsulting.FileAssert.Tests/SelfTest/SelfTestSubsystemTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.SelfTest; + +namespace DemaConsulting.FileAssert.Tests.SelfTest; + +/// +/// Subsystem tests for the SelfTest subsystem. +/// +[TestClass] +public class SelfTestSubsystemTests +{ + /// + /// Verifies that the SelfTest subsystem runs all built-in tests and produces + /// a summary that includes pass and fail counts. + /// + [TestMethod] + public void SelfTestSubsystem_Run_ExecutesBuiltInTestsAndProducesSummary() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_selftest_"); + try + { + var logPath = Path.Combine(tempDir.FullName, "validation.log"); + int exitCode; + + using (var context = Context.Create(["--silent", "--log", logPath])) + { + // Act + Validation.Run(context); + + // Capture exit code before disposal + exitCode = context.ExitCode; + } + + // Assert - context is disposed above so the log file is fully flushed and closed + Assert.AreEqual(0, exitCode); + + var logContent = File.ReadAllText(logPath); + Assert.IsTrue(logContent.Contains("Total Tests:"), "Log should contain 'Total Tests:'"); + Assert.IsTrue(logContent.Contains("Passed:"), "Log should contain 'Passed:'"); + Assert.IsTrue(logContent.Contains("Failed:"), "Log should contain 'Failed:'"); + } + finally + { + tempDir.Delete(recursive: true); + } + } +} diff --git a/test/DemaConsulting.FileAssert.Tests/ValidationTests.cs b/test/DemaConsulting.FileAssert.Tests/SelfTest/ValidationTests.cs similarity index 97% rename from test/DemaConsulting.FileAssert.Tests/ValidationTests.cs rename to test/DemaConsulting.FileAssert.Tests/SelfTest/ValidationTests.cs index e36f2af..fa63a74 100644 --- a/test/DemaConsulting.FileAssert.Tests/ValidationTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/SelfTest/ValidationTests.cs @@ -18,7 +18,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert.Tests; +using DemaConsulting.FileAssert.Cli; +using DemaConsulting.FileAssert.SelfTest; + +namespace DemaConsulting.FileAssert.Tests.SelfTest; /// /// Unit tests for the Validation class. diff --git a/test/DemaConsulting.FileAssert.Tests/PathHelpersTests.cs b/test/DemaConsulting.FileAssert.Tests/Utilities/PathHelpersTests.cs similarity index 98% rename from test/DemaConsulting.FileAssert.Tests/PathHelpersTests.cs rename to test/DemaConsulting.FileAssert.Tests/Utilities/PathHelpersTests.cs index 322c7b6..e323915 100644 --- a/test/DemaConsulting.FileAssert.Tests/PathHelpersTests.cs +++ b/test/DemaConsulting.FileAssert.Tests/Utilities/PathHelpersTests.cs @@ -18,7 +18,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -namespace DemaConsulting.FileAssert.Tests; +using DemaConsulting.FileAssert.Utilities; + +namespace DemaConsulting.FileAssert.Tests.Utilities; /// /// Tests for the PathHelpers class. diff --git a/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesSubsystemTests.cs b/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesSubsystemTests.cs new file mode 100644 index 0000000..eafb798 --- /dev/null +++ b/test/DemaConsulting.FileAssert.Tests/Utilities/UtilitiesSubsystemTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.FileAssert.Utilities; + +namespace DemaConsulting.FileAssert.Tests.Utilities; + +/// +/// Subsystem tests for the Utilities subsystem. +/// +[TestClass] +public class UtilitiesSubsystemTests +{ + /// + /// Verifies that the Utilities subsystem's safe path combination prevents + /// path traversal when used against the real file system. + /// + [TestMethod] + public void UtilitiesSubsystem_SafePathCombine_PreventsPathTraversalToFileSystem() + { + // Arrange + var tempDir = Directory.CreateTempSubdirectory("fileassert_util_"); + try + { + // Act & Assert - a traversal attempt is rejected with ArgumentException + Assert.Throws( + () => PathHelpers.SafePathCombine(tempDir.FullName, "../escape.txt")); + + // Act & Assert - a valid relative path within the base is accepted + var combined = PathHelpers.SafePathCombine(tempDir.FullName, "nested/file.txt"); + Assert.IsTrue(combined.StartsWith(tempDir.FullName, StringComparison.OrdinalIgnoreCase)); + } + finally + { + tempDir.Delete(recursive: true); + } + } +}