maester-tests/Maester/Drift/MT1060Drift.tests.ps1

<#
  Test Suite: MT1060Drift.tests.ps1
  Description: This test suite checks for drift in JSON files supplied by the user.
      It compares the current JSON against a baseline JSON and reports any differences.
      It also checks for the existence of baseline and current JSON files in specified drift folders.
  Example Usage:
      Invoke-MtTest -Path "tests/Maester/Drift/MT1060Drift.tests.ps1"
  Setup:
      - If you want to do drift checks, create a folder named "drift" at the root of your project.
      - Inside the "drift" folder, create subfolders for each drift test.
      - Each subfolder should contain:
          - `baseline.json`: The expected JSON structure.
          - `current.json`: The actual JSON structure to compare against the baseline.
          - `settings.json` (optional): A JSON file with settings, currently only supports `ExcludeProperties` to skip certain properties during comparison.
  Author: Stephan van Rooij @svrooij
  Date: 2025-06-26
#>


# This is temporary code to load the correct Compare-MtJsonObject function during development.
# In production, this should be part of the Maester module.
BeforeAll {
    # Ensure the Compare-MtJsonObject function is available
    if (-not (Get-Command -Name Compare-MtJsonObject -ErrorAction SilentlyContinue)) {
        Write-Verbose "Loading Compare-MtJsonObject function from public/maester/drift/Compare-MtJsonObject.ps1"
        . "$PSScriptRoot/../../../powershell/public/maester/drift/Compare-MtJsonObject.ps1"
    }
}

# By default this will not run if either the function Compare-MtJsonObject is not available or if the user has not created the mandatory drift folder structure.
# Using discovery to dynamically add drift folders to the test suite.
# This allows the user to "define" drift tests by creating folders in the "drift" directory.
BeforeDiscovery {

    # Get root directory for drift tests
    # This assumes the drift tests are located in a folder named "drift" at the root of the current location
    # $driftRoot = Join-Path -Path $(Get-Location) -ChildPath "drift"
    $driftRoot = $env:MEASTER_FOLDER_DRIFT
    # Ensure the drift root directory exists
    if ($null -eq $driftRoot -or -not (Test-Path -Path $driftRoot)) {
        return $null
    }

    # Ensure the Compare-MtJsonObject function is available
    if (-not (Get-Command -Name Compare-MtJsonObject -ErrorAction SilentlyContinue)) {
        Write-Warning "Compare-MtJsonObject function missing, not the right version of Maester?"
        return $null
    }

    $driftFolders = Get-ChildItem -Path $driftRoot -Directory
    # if it is not an array but a single folder, convert it to an array
    if (-not ($driftFolders -is [array]) -and $driftFolders -is [System.IO.DirectoryInfo]) {
        $driftFolders = @($driftFolders)

    }

    # No drift folders found, return null, meaning no drift tests will be run
    if ($driftFolders.Count -eq 0) {
        return $null
    }
    Write-Verbose "Found drift folders: $($driftFolders | ForEach-Object { $_.Name })"
}

# $driftFolders is coming from BeforeDiscovery.
Describe "Maester/Drift" -ForEach $driftFolders {
    # BeforeAll is run once for each drift folder, allowing us to set up the context for each drift test.
    BeforeAll {
        # Capture the drift folder context for each test
        # This allows us to access the drift folder in each test
        $driftFolder = $_
        $baselinePath = Join-Path -Path $driftFolder.FullName -ChildPath "baseline.json"
        $hasBaseline = Test-Path -Path $baselinePath -ErrorAction SilentlyContinue

        # Initialize variables to avoid linting errors
        $script:baselineData = $null
        $script:currentData = $null
        $script:settingsObject = $null

        if ($hasBaseline) {
            $script:baselineData = Get-Content -Path $baselinePath -Raw | ConvertFrom-Json -Depth 100
        }

        $driftCurrentPath = Join-Path -Path $driftFolder.FullName -ChildPath "current.json"
        $hasCurrent = Test-Path -Path $driftCurrentPath -ErrorAction SilentlyContinue
        if ($hasCurrent) {
            $script:currentData = Get-Content -Path $driftCurrentPath -Raw | ConvertFrom-Json -Depth 100
        }

        # Detect and parse settings.json for all settings
        $settingsPath = Join-Path -Path $driftFolder.FullName -ChildPath "settings.json"
        if (Test-Path -Path $settingsPath -ErrorAction SilentlyContinue) {
            try {
                $script:settingsObject = Get-Content -Path $settingsPath -Raw | ConvertFrom-Json
            } catch {
                Write-Warning "Could not parse settings.json in $($driftFolder.Name): $($_.Exception.Message)"
                $script:settingsObject = $null
            }
        }

        # Preload the differences if both the current data and baseline data are available
        if ($hasBaseline -and $hasCurrent) {
            try {
                # Use the recursive comparison function to find all differences, passing settingsObject
                $script:driftIssues = Compare-MtJsonObject -Baseline $baselineData -Current $currentData -Settings $settingsObject
            } catch {
                # If an error occurs during comparison, capture it as an issue
                $script:driftIssues = @([MtPropertyDifference]::new("", "N/A", "N/A", "An error occurred while comparing JSON objects: $($_.Exception.Message)", "ComparisonError"))

            }
        }
    }

    # MT1060.1: Validate that the baseline JSON file is valid, if you're using drift checks. you probably want it to fail if the baseline file is not valid or missing.
    # The ID of this test will be `MT1060.{folderName}.1` and has a tag of `MT1060`, `MT1060.1`, `MT1060.{folderName}`, and `MT1060.{folderName}.1`.
    It "MT1060.<_.Name>.1: Drift baseline in '<_.Name>' is valid JSON" -Tag "MT1060","MT1060.1","MT1060.$($_.Name)","MT1060.$($_.Name).1" {
        Add-MtTestResultDetail -Description "The ``baseline.json`` file should be valid JSON."
        $hasBaseline | Should -BeTrue -Because "the baseline file should exist for drift checks"
        $baselineData | Should -Not -BeNullOrEmpty -Because "the baseline file should contain valid JSON data"
    }

    # MT1060.2: Validate that the current JSON file is valid, if you're using drift checks. you probably want it to fail if the current file is not valid or missing.
    # The ID of this test will be `MT1060.{folderName}.2` and has a tag of `MT1060`, `MT1060.2`, `MT1060.{folderName}`, and `MT1060.{folderName}.2`.
    It "MT1060.<_.Name>.2: Drift current in '<_.Name>' is valid JSON" -Tag "MT1060","MT1060.2","MT1060.$($_.Name)","MT1060.$($_.Name).2" {
        Add-MtTestResultDetail -Description "The ``current.json`` file should be valid JSON, how else can we compare it?"
        $hasCurrent | Should -BeTrue -Because "the current file should exist for drift checks"
        $currentData | Should -Not -BeNullOrEmpty -Because "the current file should contain valid JSON data"
    }

    # MT1060.3: Validate that there are missing properties between baseline and current JSON files, skipping if either file is missing.
    # The ID of this test will be `MT1060.{folderName}.3` and has a tag of `MT1060`, `MT1060.3`, `MT1060.{folderName}`, and `MT1060.{folderName}.3`.
    It "MT1060.<_.Name>.3: Drift current in '<_.Name>' has no missing properties" -Tag "MT1060","MT1060.3","MT1060.$($_.Name)","MT1060.$($_.Name).3" -Skip:(($hasBaseline -eq $false) -or ($hasCurrent -eq $false)) {
        $description = "The ``current.json`` file should not have any missing properties compared to the ``baseline.json`` file."

        $missingProperties = $script:driftIssues | Where-Object { $_.Reason -eq "MissingProperty" } |
            Select-Object -ExpandProperty PropertyName -Unique

        if ($missingProperties.Count -gt 0) {
            # If there are missing properties, format them for the test result
            $formattedMissing = "The following properties are in the baseline but missing in ``current.json``: `n`n"
            $missingProperties | ForEach-Object {
                $formattedMissing += "- ``$_```n"
            }

            $formattedMissing += "`n"
            $formattedMissing += "Files compared in folder: ``$($driftFolder.FullName)```n"

            Add-MtTestResultDetail -Result $formattedMissing -Description $description
        } else {
            Add-MtTestResultDetail -Result "No missing properties found in current.json." -Description $description
        }
        $missingProperties | Should -BeNullOrEmpty -Because "there should be no missing properties in current.json"
    }

    # MT1060.4: Validate that there are no drift issues between baseline and current JSON files, skipping if either file is missing.
    # The ID of this test will be `MT1060.{folderName}.4` and has a tag of `MT1060`, `MT1060.4`, `MT1060.{folderName}`, and `MT1060.{folderName}.4`.
    It "MT1060.<_.Name>.4: Drift all values in '<_.Name>' match" -Tag "MT1060","MT1060.4","MT1060.$($_.Name)","MT1060.$($_.Name).4" -Skip:(($hasBaseline -eq $false) -or ($hasCurrent -eq $false)) {
        $description = "The ``current.json`` file should not drift from the ``baseline.json`` file."

        $propertyIssues = $script:driftIssues | Where-Object { $_.Reason -ne "MissingProperty" }

        # Format issues into a more readable format if there are any
        if ($propertyIssues.Count -gt 0) {
            # Convert issues to a more readable format for error messages
            $formattedIssues = "| Property | Reason | Expected Value | Actual Value | Description |" + "`n"
            $formattedIssues += "|----------|---------|----------------|--------------|-------------|" + "`n"
            $propertyIssues | ForEach-Object {
                $formattedIssues += "| ``$($_.PropertyName)`` | $($_.Reason) | ``$($_.ExpectedValue)`` | ``$($_.ActualValue)`` | $($_.Description) |`n"
            }
            $script:driftIssues | ForEach-Object {

            }
            $formattedIssues += "`n"
            $formattedIssues += "Files compared in folder: ``$($driftFolder.FullName)```n"

            Add-MtTestResultDetail -Result $formattedIssues -Description $description
        }
        else {
            Add-MtTestResultDetail -Result "No issues found in current.json." -Description $description
        }

        # Report all issues at once
        $propertyIssues.Count | Should -Be 0 -Because "there should be no differences between baseline and current JSON files"
    }
}