diff --git a/HubbersHelper.psd1 b/HubbersHelper.psd1 index 353444b..06e26d2 100644 --- a/HubbersHelper.psd1 +++ b/HubbersHelper.psd1 @@ -54,7 +54,7 @@ Copyright = '(c) rulasg. All rights reserved.' # RequiredModules = @() # Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() +RequiredModules = @(@{ModuleName="InvokeHelper"; ModuleVersion="1.2.4"}) # Script files (.ps1) that are run in the caller's environment prior to importing this module. # ScriptsToProcess = @() diff --git a/Test/helper/module.helper.ps1 b/Test/helper/module.helper.ps1 new file mode 100644 index 0000000..7fd1786 --- /dev/null +++ b/Test/helper/module.helper.ps1 @@ -0,0 +1,171 @@ +# Helper for module variables + +function Find-ModuleRootPath{ + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline,Position = 0)] + [string]$Path + ) + + $path = Convert-Path -Path $Path + + while (-not [string]::IsNullOrWhiteSpace($Path)){ + $psd1 = Get-ChildItem -Path $Path -Filter *.psd1 | Select-Object -First 1 + + if ($psd1 | Test-Path) { + + if($psd1.BaseName -eq "Test"){ + #foudn testing module. Continue + $path = $path | Split-Path -Parent + continue + } + + # foudn module + return $path + } + # folder without psd1 file + $path = $path | Split-Path -Parent + } + + # Path is null. Reached driver root. Module not found + return $null +} + +$MODULE_ROOT_PATH = $PSScriptRoot | Find-ModuleRootPath +$MODULE_NAME = (Get-ChildItem -Path $MODULE_ROOT_PATH -Filter *.psd1 | Select-Object -First 1).BaseName + +# Helper for module variables + + +$VALID_FOLDER_NAMES = @('Include', 'Private', 'Public', 'Root', 'TestInclude', 'TestPrivate', 'TestPublic', 'TestRoot', 'Tools', 'DevContainer', 'WorkFlows', 'GitHub', 'Helper', 'Config', 'TestHelper', 'TestConfig') + +class ValidFolderNames : System.Management.Automation.IValidateSetValuesGenerator { + [String[]] GetValidValues() { + return $script:VALID_FOLDER_NAMES + } +} + +function Get-Ps1FullPath{ + [CmdletBinding()] + param( + [Parameter(Mandatory,Position = 0)][string]$Name, + [Parameter(Position = 1)][ValidateSet([ValidFolderNames])][string]$FolderName, + [Parameter(Position = 0)][string]$ModuleRootPath + ) + + # If folderName is not empty + if($FolderName -ne $null){ + $folder = Get-ModuleFolder -FolderName $FolderName -ModuleRootPath $ModuleRootPath + $path = $folder | Join-Path -ChildPath $Name + } else { + $path = $Name + } + + # Check if file exists + if(-Not (Test-Path $path)){ + throw "File $path not found" + } + + # Get Path item + $item = Get-item -Path $path + + return $item +} +function Get-ModuleRootPath{ + [CmdletBinding()] + param( + [Parameter(Position = 0)][string]$ModuleRootPath + ) + + # if ModuleRootPath is not provided, default to local module path + if([string]::IsNullOrWhiteSpace($ModuleRootPath)){ + $ModuleRootPath = $MODULE_ROOT_PATH + } + + # Convert to full path + $ModuleRootPath = Convert-Path -Path $ModuleRootPath + + return $ModuleRootPath +} + +function Get-ModuleName{ + [CmdletBinding()] + param( + [Parameter(Position = 0)] [string]$ModuleRootPath + ) + + $ModuleRootPath = Get-ModuleRootPath -ModuleRootPath $ModuleRootPath + + $MODULE_NAME = (Get-ChildItem -Path $MODULE_ROOT_PATH -Filter *.psd1 | Select-Object -First 1).BaseName + + + return $MODULE_NAME +} + +function Get-ModuleFolder{ + [CmdletBinding()] + param( + [Parameter(Mandatory,Position = 0)][ValidateSet([ValidFolderNames])][string]$FolderName, + [Parameter(Position = 1)][string]$ModuleRootPath + ) + + $ModuleRootPath = Get-ModuleRootPath -ModuleRootPath $ModuleRootPath + + # TestRootPath + $testRootPath = $ModuleRootPath | Join-Path -ChildPath "Test" + + switch ($FolderName){ + 'Public'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "public" + } + 'Private'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "private" + } + 'Include'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "include" + } + 'TestInclude'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "include" + } + 'TestPrivate'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "private" + } + 'TestPublic'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "public" + } + 'Root'{ + $moduleFolder = $ModuleRootPath + } + 'TestRoot'{ + $moduleFolder = $testRootPath + } + 'Tools'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "tools" + } + 'DevContainer'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath ".devcontainer" + } + 'WorkFlows'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath ".github/workflows" + } + 'GitHub'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath ".github" + } + 'Helper'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "helper" + } + 'Config'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "config" + } + 'TestHelper'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "helper" + } + 'TestConfig'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "config" + } + default{ + throw "Folder [$FolderName] is unknown" + } + } + return $moduleFolder +} Export-ModuleMember -Function Get-ModuleFolder diff --git a/Test/include/database.mock.ps1 b/Test/include/database.mock.ps1 new file mode 100644 index 0000000..85ff32c --- /dev/null +++ b/Test/include/database.mock.ps1 @@ -0,0 +1,41 @@ +# DATABASE MOCK +# +# This file is used to mock the database path and the database file +# for the tests. It creates a mock database path and a mock database file +# and sets the database path to the mock database path. +# +# THIS INCLUDE REQURED module.helper.ps1 +if(-not $MODULE_NAME){ throw "Missing MODULE_NAME varaible initialization. Check for module.helerp.ps1 file." } + +$DB_INVOKE_GET_ROOT_PATH_CMD = "Invoke-$($MODULE_NAME)GetDbRootPath" +$MOCK_DATABASE_PATH = "test_database_path" + +function Mock_Database([switch]$ResetDatabase){ + + MockCallToString $DB_INVOKE_GET_ROOT_PATH_CMD -OutString $MOCK_DATABASE_PATH + + $dbstore = Invoke-MyCommand -Command $DB_INVOKE_GET_ROOT_PATH_CMD + Assert-AreEqual -Expected $MOCK_DATABASE_PATH -Presented $dbstore + + if($ResetDatabase){ + Reset-DatabaseStore + } + +} + +function Get-Mock_DatabaseStore{ + $dbstore = Invoke-MyCommand -Command $DB_INVOKE_GET_ROOT_PATH_CMD + return $dbstore +} + +function Reset-DatabaseStore{ + [CmdletBinding()] + param() + + $databaseRoot = Invoke-MyCommand -Command $DB_INVOKE_GET_ROOT_PATH_CMD + + Remove-Item -Path $databaseRoot -Recurse -Force -ErrorAction SilentlyContinue + + New-Item -Path $databaseRoot -ItemType Directory + +} diff --git a/Test/include/invokeCommand.mock.ps1 b/Test/include/invokeCommand.mock.ps1 new file mode 100644 index 0000000..6af5c68 --- /dev/null +++ b/Test/include/invokeCommand.mock.ps1 @@ -0,0 +1,253 @@ + +# INVOKE COMMAND MOCK +# +# This includes help commands to mock invokes in a test module +# +# THIS INCLUDE REQURED module.helper.ps1 +if(-not $MODULE_NAME){ throw "Missing MODULE_NAME varaible initialization. Check for module.helerp.ps1 file." } +if(-not $MODULE_ROOT_PATH){ throw "Missing MODULE_ROOT_PATH varaible initialization. Check for module.helerp.ps1 file." } + + +$testRootPath = $MODULE_ROOT_PATH | Join-Path -ChildPath 'Test' +$MOCK_PATH = $testRootPath | Join-Path -ChildPath 'private' -AdditionalChildPath 'mocks' + +$MODULE_INVOKATION_TAG = "$($MODULE_NAME)Module" +$MODULE_INVOKATION_TAG_MOCK = "$($MODULE_INVOKATION_TAG)_Mock" + +function Set-InvokeCommandMock{ + [CmdletBinding()] + param( + [Parameter(Mandatory,Position=0)][string]$Alias, + [Parameter(Mandatory,Position=1)][string]$Command + ) + + InvokeHelper\Set-InvokeCommandAlias -Alias $Alias -Command $Command -Tag $MODULE_INVOKATION_TAG_MOCK +} + +function Reset-InvokeCommandMock{ + [CmdletBinding()] + param() + + # Remove all mocks + InvokeHelper\Reset-InvokeCommandAlias -Tag $MODULE_INVOKATION_TAG_MOCK + + # Disable all dependecies of the library + Disable-InvokeCommandAlias -Tag $MODULE_INVOKATION_TAG + + # Clear Enviroment variables used + Get-Variable -scope Global -Name "$($MODULE_INVOKATION_TAG_MOCK)_*" | Remove-Variable -Force -Scope Global + +} Export-ModuleMember -Function Reset-InvokeCommandMock + +function Enable-InvokeCommandAliasModule{ + [CmdletBinding()] + param() + + Enable-InvokeCommandAlias -Tag $MODULE_INVOKATION_TAG +} Export-ModuleMember -Function Enable-InvokeCommandAliasModule + +function MockCall{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $filename + ) + + Assert-MockFileNotfound $fileName + + Set-InvokeCommandMock -Alias $command -Command "Get-MockFileContent -filename $filename" +} + +function MockCallAsync{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $filename + ) + + Assert-MockFileNotfound $fileName + + $moduleTest = $PSScriptRoot | Split-Path -Parent | Convert-Path + + Set-InvokeCommandMock -Alias $command -Command "Import-Module $moduleTest ; Get-MockFileContent -filename $filename" +} + +function MockCallJson{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $filename, + [Parameter()][switch] $AsHashtable + + ) + + Assert-MockFileNotfound $fileName + + Set-InvokeCommandMock -Alias $command -Command "Get-MockFileContentJson -filename $filename -AsHashtable:$AsHashtable" +} + +function MockCallJsonAsync{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $filename + + ) + + Assert-MockFileNotfound $fileName + + $moduleTest = $PSScriptRoot | Split-Path -Parent | Convert-Path + + Set-InvokeCommandMock -Alias $command -Command "Import-Module $moduleTest ; Get-MockFileContentJson -filename $filename" +} + +function Get-MockFileFullPath{ + param( + [parameter(Mandatory,Position=0)][string] $fileName + ) + + $filePath = $MOCK_PATH | Join-Path -ChildPath $fileName + + return $filePath +} Export-ModuleMember -Function Get-MockFileFullPath + +function Get-MockFileContent{ + param( + [parameter(Mandatory,Position=0)][string] $fileName + ) + + Assert-MockFileNotfound $FileName + + $filePath = Get-MockFileFullPath -fileName $fileName + + $content = Get-Content -Path $filePath | Out-String + + return $content +} Export-ModuleMember -Function Get-MockFileContent + +function Get-MockFileContentJson{ + param( + [parameter(Mandatory,Position=0)][string] $fileName, + [Parameter()][switch] $AsHashtable + ) + + Assert-MockFileNotfound $FileName + + $content = Get-MockFileContent -fileName $filename | ConvertFrom-Json -AsHashtable:$AsHashtable -Depth 10 + + return $content +} Export-ModuleMember -Function Get-MockFileContentJson + +function MockCallToString{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $OutString + ) + + $outputstring = 'echo "{output}"' + $outputstring = $outputstring -replace "{output}", $OutString + + Set-InvokeCommandMock -Alias $command -Command $outputstring +} + + +function MockCallToObject{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][object] $OutObject + ) + + $random = [System.Guid]::NewGuid().ToString() + $varName = "$MODULE_INVOKATION_TAG_MOCK" + "_$random" + + Set-Variable -Name $varName -Value $OutObject -Scope Global + + Set-InvokeCommandMock -Alias $command -Command "(Get-Variable -Name $varName -Scope Global).Value" +} + +function MockCallToNull{ + param( + [Parameter(Position=0)][string] $command + ) + + Set-InvokeCommandMock -Alias $command -Command 'return $null' +} + +function MockCallThrow{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $ExceptionMessage + + ) + + $mockCommand = 'throw "{message}"' + $mockCommand = $mockCommand -replace "{message}", $exceptionMessage + + Set-InvokeCommandMock -Alias $command -Command $mockCommand +} + +function MockCallExpression{ + param( + [Parameter(Position=0)][string] $command, + [Parameter(Position=1)][string] $expression + ) + + $mockCommand = @' + Invoke-Expression -Command '{expression}' +'@ + $mockCommand = $mockCommand -replace "{expression}", $expression + + Set-InvokeCommandMock -Alias $command -Command $expression +} + + +function Save-InvokeAsMockFile{ + param( + [Parameter(Mandatory=$true)] [string]$Command, + [Parameter(Mandatory=$true)] [string]$FileName, + [Parameter(Mandatory=$false)] [switch]$Force + ) + + $filePath = Get-MockFileFullPath -fileName $fileName + + $result = Invoke-Expression -Command $Command + + $json = $result | ConvertTo-Json -Depth 100 + + $json | Out-File -FilePath $filePath + + Write-Host $FileName +} Export-ModuleMember -Function Save-InvokeAsMockFile + +function Save-InvokeAsMockFileJson{ + param( + [Parameter(Mandatory=$true)] [string]$Command, + [Parameter(Mandatory=$true)] [string]$FileName + ) + + $filePath = Get-MockFileFullPath -fileName $fileName + + $result = Invoke-Expression -Command $Command + + $result | Out-File -FilePath $filePath + + Write-Host $FileName +} Export-ModuleMember -Function Save-InvokeAsMockFileJson + +function Assert-MockFileNotfound{ + param( + [Parameter(Mandatory=$true,Position=0)] [string]$FileName + ) + + $filePath = Get-MockFileFullPath -fileName $fileName + + if(-Not (Test-Path -Path $filePath)){ + throw "File not found: $fileName" + } + + # Throw if $file.name and the $filename parameter have different case + # We need to check this to avoid test bugs for mock files not found on linux that the FS is case sensitive + $file = Get-ChildItem -Path $MOCK_PATH | Where-Object { $_.Name.ToLower() -eq $fileName.ToLower() } + if($file.name -cne $fileName){ + Write-host "Wait-Debugger - File not found or wrong case - $($file.name)" + Wait-Debugger + throw "File not found or wrong case name. Expected[ $filename ] - Found[$( $file.name )]" + } +} + diff --git a/Test/private/mocks/hubbers.json b/Test/private/mocks/hubbers.json new file mode 100644 index 0000000..e0667b9 --- /dev/null +++ b/Test/private/mocks/hubbers.json @@ -0,0 +1,151 @@ +{ + "user3": { + "manager": "user0", + "employment_type": "employee", + "github_login": "user3", + "msft_alias": "user3MSFT", + "name": "User Name 1", + "email": "user3@github.com", + "title": "Chief Operating Officer", + "cost_center": "Strategic Operations", + "country": "United States of America", + "state": "Connecticut" + }, + "user1": { + "manager": "user0", + "employment_type": "employee", + "github_login": "user1", + "msft_alias": "user1MSFT", + "name": "User Name 1", + "email": "user1@github.com", + "title": "Chief Operating Officer", + "cost_center": "Strategic Operations", + "country": "United States of America", + "state": "Connecticut" + }, + "user2": { + "manager": "user0", + "employment_type": "microsoft_employee", + "github_login": "user2", + "msft_alias": "user2MSFT", + "name": "User Name 2", + "email": "user2@github.com", + "title": "Chief Revenue Officer", + "cost_center": "Office of the CEO", + "country": "United States of America", + "state": "Maryland" + }, + "user0": { + "manager": "user0", + "employment_type": "microsoft_employee", + "github_login": "user0", + "msft_alias": "user0MSFT", + "name": "User Name 0", + "email": "user0@github.com", + "title": "CEO", + "cost_center": "Office of the CEO", + "country": "United States of America\"", + "state": "Washington" + }, + "user4": { + "manager": "user2", + "employment_type": "employee", + "github_login": "user4", + "msft_alias": "user4MSFT", + "name": "User Name 4", + "email": "user4@github.com", + "title": "Chief Product Officer", + "cost_center": "Product Management", + "country": "United States of America", + "state": "North Carolina" + }, + "user5": { + "manager": "user1", + "employment_type": "microsoft_employee", + "github_login": "user5", + "msft_alias": "user5MSFT", + "name": "User Name 5", + "email": "user5@github.com", + "title": "VP, Advisor to the CEO", + "cost_center": "Office of the CEO", + "country": "United States of America" + }, + "user6": { + "manager": "user3", + "employment_type": "employee", + "github_login": "user6", + "msft_alias": "user6MSFT", + "name": "User Name 6", + "email": "user6@github.com", + "title": "VP, COS to the CEO", + "cost_center": "Office of the CEO", + "country": "United States of America", + "state": "North Carolina" + }, + "user7": { + "manager": "user1", + "employment_type": "microsoft_employee", + "github_login": "user7", + "msft_alias": "user7MSFT", + "name": "User Name 7", + "email": "user7@github.com", + "title": "Chief Legal Officer", + "cost_center": "Legal", + "country": "United States of America" + }, + "user8": { + "manager": "user2", + "employment_type": "employee", + "github_login": "user8", + "msft_alias": "user8MSFT", + "name": "User Name 8", + "email": "user8@github.com", + "title": "CISO", + "cost_center": "Security", + "country": "United States of America", + "state": "Virginia" + }, + "user9": { + "manager": "user3", + "employment_type": "microsoft_employee", + "github_login": "user9", + "name": "Jay Parikh", + "user9MSFT": "jayparikh@github.com", + "title": "User Name 9", + "cost_center": "user9 of the CEO", + "country": "United States of America" + }, + "user10": { + "manager": "user2", + "employment_type": "microsoft_employee", + "github_login": "user10", + "msft_alias": "user10MSFT", + "name": "User Name 10", + "email": "user10@github.com", + "title": "Chief People Officer", + "cost_center": "Office of the CEO", + "country": "United States of America" + }, + "user11": { + "manager": "user1", + "employment_type": "microsoft_employee", + "github_login": "user11", + "msft_alias": "user11MSFT", + "name": "User Name 11", + "email": "user11h@github.com", + "title": "Chief Technology Officer", + "cost_center": "Office of the CEO", + "country": "United States of America" + }, + "user12": { + "manager": "user3", + "employment_type": "microsoft_employee", + "github_login": "user12", + "msft_alias": "user12MSFT", + "name": "User Name 12", + "email": "user12@github.com", + "title": "Chief Financial Officer", + "cost_center": "Office of the CEO", + "country": "United States of America" + } +} \ No newline at end of file diff --git a/Test/public/SampleFunctionTests.ps1 b/Test/public/SampleFunctionTests.ps1 deleted file mode 100644 index 49f32da..0000000 --- a/Test/public/SampleFunctionTests.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -$TESTED_MODULE_PATH = $PSScriptRoot | split-path -Parent | split-path -Parent - -function Test_GetPublicString{ - - $sampleString = "this is a sample string" - - $result = Get-PublicString -Param1 $sampleString - - Assert-AreEqual -Expected ("Public string [{0}]" -f $samplestring) -presented $result -Comment "Sample test failed" - -} - -function Test_GetPrivateString { - - $testedModulePath = $TESTED_MODULE_PATH | Join-Path -ChildPath "HubbersHelper.psd1" - $testedModule = Import-Module -Name $testedModulePath -Force -PassThru - - $sampleString = "this is a sample string" - - $result = & $testedModule { - $sampleString = "this is a sample string" - Get-PrivateString -Param1 $sampleString - } - - Assert-AreEqual -Expected ("Private string [{0}]" -f $samplestring) -presented $result -Comment "Sample test failed" - -} - -Export-ModuleMember -Function Test_* diff --git a/Test/public/buildTree.test.ps1 b/Test/public/buildTree.test.ps1 index 7e5c1bc..c86292e 100644 --- a/Test/public/buildTree.test.ps1 +++ b/Test/public/buildTree.test.ps1 @@ -1,11 +1,43 @@ -function Test_Build-Tree{ - - $hubbers,$tree = Build-Tree +function Test_Load_HubbersList{ - Assert-AreEqual -Expected ashtom -Presented $hubbers.rulasg.manager.manager.manager.manager.github_login + Reset-InvokeCommandMock + Mock_Database + + $filePath = Get-MockFileFullPath -fileName "hubbers.json" + + $result = Import-HubbersList -Path $filePath + + Assert-AreEqual -Expected user0 -Presented $result.HubbersList.user12.manager.manager.github_login # total employeed - Assert-Count -Expected 4410 -Presented $hubbers - Assert-AreEqual -Expected 4410 -Presented $tree.totalEmployees + Assert-AreEqual -Expected 13 -Presented $result.totalHubbers + Assert-AreEqual -Expected 13 -Presented $result.HubbersList.count +} + +function Test_GetHubbersList{ + Reset-InvokeCommandMock + Mock_Database + + $filePath = Get-MockFileFullPath -fileName "hubbers.json" + + $result = Import-HubbersList -Path $filePath + + $result = Get-HubbersList + + Assert-AreEqual -Expected user0 -Presented $result.user12.manager.manager.github_login +} + +function Test_GetHubbersTree{ + Reset-InvokeCommandMock + Mock_Database + + $filePath = Get-MockFileFullPath -fileName "hubbers.json" + + $result = Import-HubbersList -Path $filePath + + $result = Get-HubbersTree + + Assert-AreEqual -Expected user0 -Presented $result.github_login + Assert-AreEqual -Expected user12 -Presented $result.reports.user3.reports.user12.github_login +} -} \ No newline at end of file diff --git a/Test/public/gethubber.test.ps1 b/Test/public/gethubber.test.ps1 new file mode 100644 index 0000000..f5b9dec --- /dev/null +++ b/Test/public/gethubber.test.ps1 @@ -0,0 +1,16 @@ +function Test_Gethubber{ + + Reset-InvokeCommandMock + Mock_Database + $filePath = Get-MockFileFullPath -fileName "hubbers.json" + $result = Import-HubbersList -Path $filePath + + $user = "user3" + $manager = "user0" + + $result = Get-Hubber -Handle $user + + Assert-AreEqual -Expected $user -Presented $result.github_login + Assert-AreEqual -Expected $manager -Presented $result.manager.github_login + +} \ No newline at end of file diff --git a/helper/invokeCommand.helper.ps1 b/helper/invokeCommand.helper.ps1 new file mode 100644 index 0000000..6781ef1 --- /dev/null +++ b/helper/invokeCommand.helper.ps1 @@ -0,0 +1,35 @@ + +# SET MY INVOKE COMMAND ALIAS +# +# Allows calling constitely InvokeHelper with the module tag +# Need to define a variable called $MODULE_INVOKATION_TAG +# + +$moduleRootPath = $PSScriptRoot | Split-Path -Parent +$MODULE_NAME = (Get-ChildItem -Path $moduleRootPath -Filter *.psd1 | Select-Object -First 1).BaseName +$MODULE_INVOKATION_TAG = "$($MODULE_NAME)Module" + +function Set-MyInvokeCommandAlias{ + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory,Position=0)][string]$Alias, + [Parameter(Mandatory,Position=1)][string]$Command + ) + + if ($PSCmdlet.ShouldProcess("InvokeCommandAliasList", ("Add Command Alias [{0}] = [{1}]" -f $Alias, $Command))) { + InvokeHelper\Set-InvokeCommandAlias -Alias $Alias -Command $Command -Tag $MODULE_INVOKATION_TAG + } +} + +function Invoke-MyCommand{ + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline,Position=0)][string]$Command, + [Parameter(Position=1)][hashtable]$Parameters + ) + + Write-Debug "[invoke] $Command" $Parameters + + return InvokeHelper\Invoke-MyCommand -Command $Command -Parameters $Parameters +} + diff --git a/helper/module.helper.ps1 b/helper/module.helper.ps1 new file mode 100644 index 0000000..7fd1786 --- /dev/null +++ b/helper/module.helper.ps1 @@ -0,0 +1,171 @@ +# Helper for module variables + +function Find-ModuleRootPath{ + [CmdletBinding()] + param( + [Parameter(Mandatory,ValueFromPipeline,Position = 0)] + [string]$Path + ) + + $path = Convert-Path -Path $Path + + while (-not [string]::IsNullOrWhiteSpace($Path)){ + $psd1 = Get-ChildItem -Path $Path -Filter *.psd1 | Select-Object -First 1 + + if ($psd1 | Test-Path) { + + if($psd1.BaseName -eq "Test"){ + #foudn testing module. Continue + $path = $path | Split-Path -Parent + continue + } + + # foudn module + return $path + } + # folder without psd1 file + $path = $path | Split-Path -Parent + } + + # Path is null. Reached driver root. Module not found + return $null +} + +$MODULE_ROOT_PATH = $PSScriptRoot | Find-ModuleRootPath +$MODULE_NAME = (Get-ChildItem -Path $MODULE_ROOT_PATH -Filter *.psd1 | Select-Object -First 1).BaseName + +# Helper for module variables + + +$VALID_FOLDER_NAMES = @('Include', 'Private', 'Public', 'Root', 'TestInclude', 'TestPrivate', 'TestPublic', 'TestRoot', 'Tools', 'DevContainer', 'WorkFlows', 'GitHub', 'Helper', 'Config', 'TestHelper', 'TestConfig') + +class ValidFolderNames : System.Management.Automation.IValidateSetValuesGenerator { + [String[]] GetValidValues() { + return $script:VALID_FOLDER_NAMES + } +} + +function Get-Ps1FullPath{ + [CmdletBinding()] + param( + [Parameter(Mandatory,Position = 0)][string]$Name, + [Parameter(Position = 1)][ValidateSet([ValidFolderNames])][string]$FolderName, + [Parameter(Position = 0)][string]$ModuleRootPath + ) + + # If folderName is not empty + if($FolderName -ne $null){ + $folder = Get-ModuleFolder -FolderName $FolderName -ModuleRootPath $ModuleRootPath + $path = $folder | Join-Path -ChildPath $Name + } else { + $path = $Name + } + + # Check if file exists + if(-Not (Test-Path $path)){ + throw "File $path not found" + } + + # Get Path item + $item = Get-item -Path $path + + return $item +} +function Get-ModuleRootPath{ + [CmdletBinding()] + param( + [Parameter(Position = 0)][string]$ModuleRootPath + ) + + # if ModuleRootPath is not provided, default to local module path + if([string]::IsNullOrWhiteSpace($ModuleRootPath)){ + $ModuleRootPath = $MODULE_ROOT_PATH + } + + # Convert to full path + $ModuleRootPath = Convert-Path -Path $ModuleRootPath + + return $ModuleRootPath +} + +function Get-ModuleName{ + [CmdletBinding()] + param( + [Parameter(Position = 0)] [string]$ModuleRootPath + ) + + $ModuleRootPath = Get-ModuleRootPath -ModuleRootPath $ModuleRootPath + + $MODULE_NAME = (Get-ChildItem -Path $MODULE_ROOT_PATH -Filter *.psd1 | Select-Object -First 1).BaseName + + + return $MODULE_NAME +} + +function Get-ModuleFolder{ + [CmdletBinding()] + param( + [Parameter(Mandatory,Position = 0)][ValidateSet([ValidFolderNames])][string]$FolderName, + [Parameter(Position = 1)][string]$ModuleRootPath + ) + + $ModuleRootPath = Get-ModuleRootPath -ModuleRootPath $ModuleRootPath + + # TestRootPath + $testRootPath = $ModuleRootPath | Join-Path -ChildPath "Test" + + switch ($FolderName){ + 'Public'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "public" + } + 'Private'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "private" + } + 'Include'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "include" + } + 'TestInclude'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "include" + } + 'TestPrivate'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "private" + } + 'TestPublic'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "public" + } + 'Root'{ + $moduleFolder = $ModuleRootPath + } + 'TestRoot'{ + $moduleFolder = $testRootPath + } + 'Tools'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "tools" + } + 'DevContainer'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath ".devcontainer" + } + 'WorkFlows'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath ".github/workflows" + } + 'GitHub'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath ".github" + } + 'Helper'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "helper" + } + 'Config'{ + $moduleFolder = $ModuleRootPath | Join-Path -ChildPath "config" + } + 'TestHelper'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "helper" + } + 'TestConfig'{ + $moduleFolder = $testRootPath | Join-Path -ChildPath "config" + } + default{ + throw "Folder [$FolderName] is unknown" + } + } + return $moduleFolder +} Export-ModuleMember -Function Get-ModuleFolder diff --git a/include/MyWrite.ps1 b/include/MyWrite.ps1 new file mode 100644 index 0000000..24d039c --- /dev/null +++ b/include/MyWrite.ps1 @@ -0,0 +1,78 @@ + +$ERROR_COLOR = "Red" +$WARNING_COLOR = "Yellow" +$OUTPUT_COLOR = "DarkCyan" +$DEBUG_COLOR = "DarkGray" + +function Write-MyError { + param( + [Parameter(Mandatory, ValueFromPipeline)][string]$Message + ) + # Write-Host "Error: $message" -ForegroundColor $ERROR_COLOR + Write-ToConsole "Error: $message" -Color $ERROR_COLOR +} + +function Write-MyWarning { + param( + [Parameter(Mandatory, ValueFromPipeline)][string]$Message + ) + # Write-Host "Error: $message" -ForegroundColor $WARNING_COLOR + Write-ToConsole $message -Color $WARNING_COLOR +} + +function Write-MyVerbose { + param( + [Parameter(ValueFromPipeline)][string]$Message + ) + Write-Verbose -Message $message +} + +function Write-MyHost { + param( + [Parameter(ValueFromPipeline)][string]$Message, + #NoNewLine + [Parameter()][switch]$NoNewLine + ) + # Write-Host $message -ForegroundColor $OUTPUT_COLOR + Write-ToConsole $message -Color $OUTPUT_COLOR -NoNewLine:$NoNewLine +} + +function Write-Debug { + param( + [Parameter(Position = 0)][string]$section, + [Parameter(Position = 1, ValueFromPipeline)][string]$Message, + [Parameter(Position = 2)][object]$Object + ) + + $flag = $env:ProjectHelper_DEBUG + + # Enable debug + if ([string]::IsNullOrWhiteSpace( $flag )) { + return + } + + $trace = ($flag -like '*all*') -or ( $section -like "*$flag*") + + # Write-Host $message -ForegroundColor $DEBUG_COLOR + if ($trace) { + + if ($Object) { + $objJson = $Object | ConvertTo-Json -Depth 10 -ErrorAction SilentlyContinue + $message = $message + " - " + $objJson + } + + $message = "[DEBUG][$section] " + $message + Write-ToConsole $message -Color $DEBUG_COLOR + } +} + +function Write-ToConsole { + param( + [Parameter(ValueFromPipeline)][string]$Color, + [Parameter(ValueFromPipeline, Position = 0)][string]$Message, + [Parameter()][switch]$NoNewLine + + ) + Microsoft.PowerShell.Utility\Write-Host $message -ForegroundColor $Color -NoNewLine:$NoNewLine +} + diff --git a/include/databaseV2.ps1 b/include/databaseV2.ps1 new file mode 100644 index 0000000..8f74a6b --- /dev/null +++ b/include/databaseV2.ps1 @@ -0,0 +1,172 @@ +# DATABASE V2 +# +# Database driver to store the cache +# +# Include design description +# This is the function ps1. This file is the same for all modules. +# Create a public psq with variables, Set-MyInvokeCommandAlias call and Invoke public function. +# Invoke function will call back `GetDatabaseRootPath` to use production root path +# Mock this Invoke function with Set-MyInvokeCommandAlias to set the Store elsewhere +# This ps1 has function `GetDatabaseFile` that will call `Invoke-MyCommand -Command $DB_INVOKE_GET_ROOT_PATH_ALIAS` +# to use the store path, mocked or not, to create the final store file name. +# All functions of this ps1 will depend on `GetDatabaseFile` for functionality. +# + +$MODULE_ROOT_PATH = $PSScriptRoot | Split-Path -Parent +$MODULE_NAME = (Get-ChildItem -Path $MODULE_ROOT_PATH -Filter *.psd1 | Select-Object -First 1).BaseName +$DATABASE_ROOT = [System.Environment]::GetFolderPath('UserProfile') | Join-Path -ChildPath ".helpers" -AdditionalChildPath $MODULE_NAME, "databaseCache" + +$DB_INVOKE_GET_ROOT_PATH_ALIAS = "$($MODULE_NAME)GetDbRootPath" + +$function = "Invoke-$($MODULE_NAME)GetDbRootPath" +if(-not (Test-Path -Path function:$function)){ + + # PUBLIC FUNCTION + function Invoke-MyModuleGetDbRootPath{ + [CmdletBinding()] + param() + + $databaseRoot = GetDatabaseRootPath + return $databaseRoot + + } + Rename-Item -path Function:Invoke-MyModuleGetDbRootPath -NewName $function + Export-ModuleMember -Function $function + Set-MyInvokeCommandAlias -Alias $DB_INVOKE_GET_ROOT_PATH_ALIAS -Command $function +} + +# Extra functions not needed by INCLUDE DATABASE V2 +$function = "Reset-$($MODULE_NAME)DatabaseStore" +if(-not (Test-Path -Path function:$function)){ + function Reset-MyModuleDatabaseStore{ + [CmdletBinding()] + param() + + $databaseRoot = Invoke-MyCommand -Command $DB_INVOKE_GET_ROOT_PATH_ALIAS + + Remove-Item -Path $databaseRoot -Recurse -Force -ErrorAction SilentlyContinue + + New-Item -Path $databaseRoot -ItemType Directory + + } + + Rename-Item -path Function:Reset-MyModuleDatabaseStore -NewName $function + Export-ModuleMember -Function $function +} + +# PRIVATE FUNCTIONS +function GetDatabaseRootPath { + [CmdletBinding()] + param() + + $databaseRoot = $DATABASE_ROOT + return $databaseRoot +} + +function GetDatabaseFile{ + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)][string]$Key, + [Parameter(Position = 1)][ValidateSet("JSON","XML","TXT")][string]$DBFormat = "JSON" + ) + + $databaseRoot = Invoke-MyCommand -Command $DB_INVOKE_GET_ROOT_PATH_ALIAS + + if(-not (Test-Path -Path $databaseRoot)){ + New-Item -Path $databaseRoot -ItemType Directory -Force | Out-Null + } + + $ext = GetFileExtension -DbFormat $DBFormat + + $path = $databaseRoot | Join-Path -ChildPath "$Key$ext" + + return $path +} + +function GetFileExtension{ + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$DbFormat + ) + + switch ($DbFormat.ToUpper()){ + "JSON" { $ret = ".json" ; Break } + "XML" { $ret = ".xml" ; Break } + "TXT" { $ret = ".txt" ; Break } + default { throw "Unsupported database format $DbFormat" } + } + return $ret +} + +function Get-DatabaseKey{ + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)][string]$Key, + [Parameter(Position = 1)][ValidateSet("JSON","XML","TXT")][string]$DBFormat = "JSON" + ) + + if(-Not (Test-DatabaseKey $Key -DBFormat $DBFormat)){ + return $null + } + + $path = GetDatabaseFile $Key -DBFormat $DBFormat + + switch ($DBFormat) { + "JSON" { $ret = Get-Content $path | ConvertFrom-Json ; Break } + "XML" { $ret = Import-Clixml -Path $path ; Break } + "TXT" { $ret = Get-Content $path ; Break } + default { throw "Unsupported database format $DbFormat" } + } + + return $ret +} + +function Reset-DatabaseKey{ + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)][string]$Key, + [Parameter(Position = 1)][ValidateSet("JSON","XML","TXT")][string]$DBFormat = "JSON" + ) + $path = GetDatabaseFile -Key $Key -DBFormat $DBFormat + Remove-Item -Path $path -Force -ErrorAction SilentlyContinue + return +} + +function Save-DatabaseKey{ + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)][string]$Key, + [Parameter(Mandatory, Position = 2)][Object]$Value, + [Parameter(Position = 3)][ValidateSet("JSON","XML","TXT")][string]$DbFormat = "JSON" + ) + + $path = GetDatabaseFile -Key $Key -DBFormat $DbFormat + + switch ($DbFormat) { + "JSON" { $Value | ConvertTo-Json -Depth 10 | Set-Content $path -Encoding UTF8 -Force ; Break } + "XML" { $Value | Export-Clixml -Path $path -Force ; Break } + "TXT" { $Value | Set-Content -Path $path -Encoding UTF8 -Force ; Break } + default { throw "Unsupported database format $DbFormat" + } + } +} + +function Test-DatabaseKey{ + [CmdletBinding()] + param( + [Parameter(Mandatory, Position = 0)][string]$Key, + [Parameter(Position = 1)][ValidateSet("JSON","XML","TXT")][string]$DBFormat = "JSON" + ) + + $path = GetDatabaseFile -Key $Key -DBFormat $DBFormat + + # Key file not exists + if(-Not (Test-Path $path)){ + return $false + } + + # TODO: Return $false if cache has expired + + return $true +} + diff --git a/public/buildTree.ps1 b/public/buildTree.ps1 index 1402c61..f73c6e5 100644 --- a/public/buildTree.ps1 +++ b/public/buildTree.ps1 @@ -1,21 +1,49 @@ -function Build-Tree { + +function Import-HubbersList { [cmdletbinding()] param ( - + #path to hubber list + [Parameter(Mandatory, Position = 1)][string]$Path ) - $hubbers = Get-Hubber -AsHashtable + try{ + $hubbersRawList = Get-Content $Path -Raw | ConvertFrom-Json -AsHashtable + } catch { + Write-Error -Message "Failed to read or parse JSON file: $_" + return $null + } + + # Calculate the tree + $result = Build-HubbersTree -Hubbers $hubbersRawList + + # Save to local cache + Save-HubbersListDb -Hubbers $result.HubbersList + Save-HubbersTreeDb -Hubbers $result.HubbersTree + + return $result + +} Export-ModuleMember -Function Import-HubbersList + + +function Build-HubbersTree { + [cmdletbinding()] + param ( + [Parameter(mandatory)][hashtable]$Hubbers + ) $ceo = $hubbers.Values | Where-Object { $_.manager -eq $_.github_login } $tree = Build-Node $hubbers $ceo - $global:hubbers = $hubbers - $global:tree = $tree + $global:ResultHubbersBuild = @{ + totalHubbers = $hubbers.count + "HubbersList" = $hubbers + "HubbersTree" = $tree + } - return $hubbers, $tree + return $global:ResultHubbersBuild -} Export-ModuleMember -Function Build-Tree +} function Build-Node { @@ -26,7 +54,9 @@ function Build-Node { try { - Write-Host "." -NoNewline + if ($null -eq $script:count -or ($script:count % 10) -eq 0) { + Write-Host "." -NoNewline + } # Stop after X nodes if (! (Test-Continue)) { @@ -38,21 +68,22 @@ function Build-Node { ## Manager # Set to null the manager for the CEO where manager == him self - $node.manager = ($node.manager -ne $nlogin) ? $hubbers.$($node.manager) : $null + $node.manager = ($node.manager -eq $nlogin) ? $null : $hubbers.$($node.manager) + $node.level = $node.manager ? $node.manager.level + 1 : 0 - ## Employees + ## Reports - $employeesList = $hubbers.Values | Where-Object { ($_.manager -eq $nlogin) -and ($_.github_login -ne $nlogin) } + $reportsList = $hubbers.Values | Where-Object { ($_.manager -eq $nlogin) -and ($_.github_login -ne $nlogin) } - $Node.totalEmployees = 0 + $Node.totalReports = 0 # Recurse employ - if ($employeesList.count -ne 0) { + if ($reportsList.count -ne 0) { - $Node.employees = @{} + $Node.reports = @{} # recurse call - foreach ($employee in $employeesList) { + foreach ($employee in $reportsList) { $elogin = $employee.github_login @@ -62,15 +93,15 @@ function Build-Node { continue } - $Node.employees.$elogin = Build-Node $hubbers $employee + $Node.reports.$elogin = Build-Node $hubbers $employee } - # Count the number of employees under this node - foreach ($employee in $Node.employees.Values) { - if ($null -ne $employee.totalEmployees) { - $Node.totalEmployees += $employee.totalEmployees + # Count the number of reports under this node + foreach ($employee in $Node.reports.Values) { + if ($null -ne $employee.totalReports) { + $Node.totalReports += $employee.totalReports } - $Node.totalEmployees++ + $Node.totalReports++ } } diff --git a/public/exporthubbers.ps1 b/public/exporthubbers.ps1 new file mode 100644 index 0000000..fce3f1f --- /dev/null +++ b/public/exporthubbers.ps1 @@ -0,0 +1,33 @@ +function Save-HubbersListDb { + param( + [Parameter(Mandatory)][object]$Hubbers + ) + + Save-DatabaseKey -Key HubbersList -Value $Hubbers -DBFormat XML + +} + +function Save-HubbersTreeDb { + param( + [Parameter(Mandatory)][object]$Hubbers + ) + + Save-DatabaseKey -Key HubbersTree -Value $Hubbers -DBFormat XML + +} + +function Get-HubbersList { + param() + + $hubbers = Get-DatabaseKey -Key HubbersList -DBFormat XML + + return $hubbers +} Export-ModuleMember -Function Get-HubbersList + +function Get-HubbersTree { + param() + + $tree = Get-DatabaseKey -Key HubbersTree -DBFormat XML + + return $tree +} Export-ModuleMember -Function Get-HubbersTree \ No newline at end of file diff --git a/public/getHubber.ps1 b/public/getHubber.ps1 index a86645b..f1d12eb 100644 --- a/public/getHubber.ps1 +++ b/public/getHubber.ps1 @@ -1,3 +1,4 @@ + function Get-Hubber{ [CmdletBinding()] param ( @@ -5,36 +6,10 @@ function Get-Hubber{ [Parameter()][switch]$AsHashtable ) - $hubbers = Get-Content -Path "~/hubbers.json" | ConvertFrom-Json -Depth 10 -AsHashtable - - if(! [string]::IsNullOrWhiteSpace($Handle)){ - return [pscustomobject] $hubbers.$Handle - } - - if($AsHashtable){ - return $hubbers - } else { - return $hubbers.Values - } -} Export-ModuleMember -Function Get-Hubber - -function Get-HubberByCountry { - param ( - [string]$Country - ) - - $path = Get-HubberJsonPath - - $hubbers = Get-Content -Path $path | ConvertFrom-Json -Depth 10 -AsHashtable + $hubbers = Get-HubbersList - $ret = $hubbers.Values | Where-Object { $_.country -eq $Country } + $ret = $hubbers.$Handle return $ret -} Export-ModuleMember -Function Get-HubberByCountry - -function Get-HubberJsonPath() { - - # Downloaded from https://thehub.github.com/assets/org-data.json - return "~/hubbers.json" -} \ No newline at end of file +} Export-ModuleMember -Function Get-Hubber \ No newline at end of file diff --git a/public/samplePublicFunction.ps1 b/public/samplePublicFunction.ps1 deleted file mode 100644 index ad678be..0000000 --- a/public/samplePublicFunction.ps1 +++ /dev/null @@ -1,8 +0,0 @@ - -function Get-PublicString { - param ( - [Parameter()][string]$Param1 - ) - - return ("Public string [{0}]" -f $param1) -} Export-ModuleMember -Function Get-PublicString