commit b2afcf89bd0498c711c3efcdea6bdf1a73c51cfb Author: Dimitar Milov Date: Wed Apr 7 09:40:42 2021 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c51270b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +**/.vs +**/bin +**/obj +/CloudEvents +/CloudEvents.Sdk \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4259733 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021-Present The CloudEvents Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..865d8b2 --- /dev/null +++ b/README.md @@ -0,0 +1,213 @@ +# PowerShell 7.0 SDK for CloudEvents based on [.NET SDK for CloudEvents](https://github.com/cloudevents/sdk-csharp) + +## Status + +Supported CloudEvents versions: +- v1.0 + +Supported Protocols: +- HTTP + +# **CloudEvents.Sdk** Module +The module contains functions to +- Create CloudEvent objects +- Add data to a CloudEvent object +- Read data from a CloudEvent object +- Convert an CloudEvent object to an HTTP Message +- Convert an HTTP Message to an CloudEvent object + +## Producer +### Create a CloudEvent object +```powershell +$cloudEvent = New-CloudEvent -Type 'com.example.object.deleted.v2' -Source 'mailto:cncf-wg-serverless@lists.cncf.io' -Id '6e8bc430-9c3a-11d9-9669-0800200c9a66' -Time (Get-Date) +``` + +### Add **JSON Data** to a CloudEvent object +```powershell +$cloudEvent | Add-CloudEventJsonData -Data @{ + 'key1' = 'value1' + 'key2' = @{ + 'key3' = 'value3' + } +} +``` + +### Add **XML Data** to a CloudEvent object +```powershell +$cloudEvent | Add-CloudEventXmlData -Data @{ + 'key1' = @{ + 'key2' = 'value' + } +} ` +-AttributesKeysInElementAttributes $true +``` +`AttributesKeysInElementAttributes` specifies how to format the XML. If `true` and the input Data hashtable has pairs of 'Attributes', 'Value' keys creates XML element with attributes, otherwise each key is formatted as XML element.
+If `true` +```powershell + @{'root' = @{'Attributes' = @{'att1' = 'true'}; 'Value' = 'val-1'}} +``` +is formatted as +```xml +val-1 +``` +If `false` +```powershell +@{'root' = @{'Attributes' = @{'att1' = 'true'}; 'Value' = 'val-1'}} +``` +is formatted as +```xml +trueval-1 +``` + +#### Add Custom Format Data to a CloudEvent object +```powershell +$cloudEvent | Add-CloudEventData -DataContentType 'application/text' -Data 'wow' +``` + +### Convert a CloudEvent object to an HTTP message in **Binary** or **Structured** content mode +```powershell +$cloudEventBinaryHttpMessage = $cloudEvent | ConvertTo-HttpMessage -ContentMode Binary +$cloudEventStructuredHttpMessage = $cloudEvent | ConvertTo-HttpMessage -ContentMode Structured +``` + +### Send CloudEvent object to HTTP server +```powershell +Invoke-WebRequest -Method POST -Uri 'http://my.cloudevents.server/' -Headers $cloudEventBinaryHttpMessage.Headers -Body $cloudEventBinaryHttpMessage.Body +``` + +## Consumer +### Convert an HTTP message to a CloudEvent object +```powershell +$cloudEvent = ConvertFrom-HttpMessage -Headers -Body +``` + +### Read CloudEvent **JSON Data** as a **PowerShell Hashtable** +```powershell +$hashtableData = Read-CloudEventJsonData -CloudEvent $cloudEvent +``` + +### Read CloudEvent **XML Data** as a **PowerShell Hashtable** +```powershell +$hashtableData = Read-CloudEventXmlData -CloudEvent $cloudEvent -ConvertMode SkipAttributes +``` +The `ConvertMode` parameter specifies how the XML to be represented in the result hashtable
+`SkipAttributes` - Skips attributes of the XML elements. XmlElement is represented as a Key-Value pair where key is the xml element name, and the value is the xml element inner text
+Example: +```xml +value1 +``` +is converted to +```powershell +@{'key' = 'value-1'} +``` +`AlwaysAttrValue` - Each element is represented as a hashtable with two keys
+ 'Attributes' - key-value pair of the cml element attributes if any, otherwise null
+ 'Value' - string value represinting the xml element inner text
+Example: +```xml +``` +value1value2 +is converted to +```powershell +@{ + 'key1' = @{ + 'Attributes' = @{ + 'att' = 'true' + } + 'Value' = 'value1' + } + 'key2' = @{ + 'Attributes' = $null + 'Value' = 'value2' + } +} +``` +`AttrValueWhenAttributes` - Uses `SkipAttributes` for xml elements without attributes and `AlwaysAttrValue` for xml elements with attributes
+Example: +```xml +value1value2 +``` +is converted to +```powershell +@{ + 'key1' = @{ + 'Attributes' = @{ + 'att' = 'true' + } + 'Value' = 'value1' + } + 'key2' = 'value2' +} +``` + +### Read CloudEvent Custom Format **Data** as a **byte[]** +```powershell +$bytes = Read-CloudEventData -CloudEvent $cloudEvent +``` + +# Build the **CloudEvents.Sdk** Module + +The `build.ps1` script +- Creates the CloudEvents PowerShell Module in a `CloudEvents` directory. +- Runs functions unit tests +- Runs local integrations tests +- Creates a catalog file for the CloudEvents Module + +### Prerequisites +- [PowerShell 7.0](https://github.com/PowerShell/PowerShell/releases/tag/v7.0.4) +- [Pester 5.1.1](https://www.powershellgallery.com/packages/Pester/5.1.1) +- [dotnet SDK](https://dotnet.microsoft.com/download/dotnet/5.0) + +```powershell +> ./build.ps1 +[9:52:42 AM] INFO: Publish CloudEvents.Sdk Module to 'C:\git-repos\cloudevents\cloudevents-sdk-powershell\CloudEvents.Sdk' +Microsoft (R) Build Engine version 16.8.3+39993bd9d for .NET +Copyright (C) Microsoft Corporation. All rights reserved. + + Determining projects to restore... + All projects are up-to-date for restore. + CloudEventsPowerShell -> C:\git-repos\cloudevents\cloudevents-sdk-powershell\src\CloudEventsPowerShell\bin\Release\netstandard2.0\CloudEventsPowerShell.dll + CloudEventsPowerShell -> C:\git-repos\cloudevents\cloudevents-sdk-powershell\CloudEvents.Sdk\ +[9:52:44 AM] INFO: Run unit tests + +Starting discovery in 9 files. +Discovery finished in 294ms. +[+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\Add-CloudEventData.Tests.ps1 1.01s (184ms|656ms) +[+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\Add-CloudEventJsonData.Tests.ps1 329ms (39ms|279ms) [+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\Add-CloudEventXmlData.Tests.ps1 336ms (58ms|267ms) [+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\ConvertFrom-HttpMessage.Tests.ps1 557ms (203ms|337ms) [+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\ConvertTo-HttpMessage.Tests.ps1 508ms (132ms|361ms) [+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\New-CloudEvent.Tests.ps1 275ms (22ms|243ms) +[+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\Read-CloudEventData.Tests.ps1 257ms (10ms|236ms) +[+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\Read-CloudEventJsonData.Tests.ps1 308ms (40ms|257ms) +[+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\unit\Read-CloudEventXmlData.Tests.ps1 310ms (53ms|246ms) +Tests completed in 3.94s +Tests Passed: 28, Failed: 0, Skipped: 0 NotRun: 0 +[9:52:49 AM] INFO: Run integration tests + +Starting discovery in 1 files. +Discovery finished in 176ms. +[+] C:\git-repos\cloudevents\cloudevents-sdk-powershell\test\integration\HttpIntegration.Tests.ps1 2.54s (1.77s|617ms) +Tests completed in 2.56s +Tests Passed: 5, Failed: 0, Skipped: 0 NotRun: 0 +``` + +# Install **CloudEvents.Sdk** Module + +```powershell +$vmwareArtifactoryRepo = 'https://build-artifactory.eng.vmware.com/artifactory/api/nuget/powercli-nuget-local/' + +Register-PSRepository -Name 'Artifactory' -SourceLocation $vmwareArtifactoryRepo -PublishLocation $vmwareArtifactoryRepo -InstallationPolicy Trusted + +Install-Module CloudEvents.Sdk -Repository Artifactory +Import-Module CloudEvents.Sdk +Get-Command -Module CloudEvents.Sdk + +ommandType Name Version Source +----------- ---- ------- ------ +Function Add-CloudEventData 0.1.2 CloudEvents.Sdk +Function Add-CloudEventJsonData 0.1.2 CloudEvents.Sdk +Function Add-CloudEventXmlData 0.1.2 CloudEvents.Sdk +Function ConvertFrom-HttpMessage 0.1.2 CloudEvents.Sdk +Function ConvertTo-HttpMessage 0.1.2 CloudEvents.Sdk +Function New-CloudEvent 0.1.2 CloudEvents.Sdk +Function Read-CloudEventData 0.1.2 CloudEvents.Sdk +Function Read-CloudEventJsonData 0.1.2 CloudEvents.Sdk +Function Read-CloudEventXmlData 0.1.2 CloudEvents.Sdk +``` diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..87370b8 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,96 @@ +param( + [Parameter()] + [string] + $OutputDir +) + +$moduleName = 'CloudEvents.Sdk' + +#region Input +if (-not $OutputDir) { + $OutputDir = $PSScriptRoot +} + +$OutputDir = Join-Path $OutputDir $moduleName + +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir | Out-Null +} +#endregion + +#region Helper Funcitons +function Write-InfoLog($message) { + $dt = (Get-Date).ToLongTimeString() + Write-Host "[$dt] INFO: $message" -ForegroundColor Green +} + +function Write-ErrorLog($message) { + $dt = (Get-Date).ToLongTimeString() + Write-Host "[$dt] ERROR: $message" -ForegroundColor Red +} + +function Test-BuildToolsAreAvailable { + $dotnetSdk = Get-Command 'dotnet' + if (-not $dotnetSdk) { + throw "'dotnet' sdk is not available" + } +} + +function Start-Tests { +param( + [Parameter()] + [ValidateSet('unit', 'integration', 'all')] + [string] + $TestsType +) + $pesterModule = Get-Module Pester -List + if ($pesterModule -eq $null) { + Write-ErrorLog "Pester Module is not available" + } else { + # Run Tests in external process because it will load build output binaries + Write-InfoLog "Run $TestsType tests" + $usePowerShell = (Get-Process -Id $pid).ProcessName + + $testLauncherScript = Join-Path (Join-Path $PSScriptRoot 'test') 'RunTests.ps1' + $CloudEventsModulePath = Join-Path $OutputDir "$moduleName.psd1" + $testProcessArguments = "-Command $testLauncherScript -CloudEventsModulePath '$CloudEventsModulePath' -TestsType '$TestsType' -EnableProcessExit" + + # Process Exit Code is 0 if all tests pass, otherwise it equals the number of failed tests + $testProcess = Start-Process ` + -FilePath $usePowerShell ` + -ArgumentList $testProcessArguments ` + -PassThru ` + -NoNewWindow + + $testProcess | Wait-Process + } +} +#endregion + +$dotnetProjectName = 'CloudEventsPowerShell' +$dotnetProjectPath = Join-Path (Join-Path (Join-Path $PSScriptRoot 'src') $dotnetProjectName) "$dotnetProjectName.csproj" + +# 1. Test dotnet command is available +Test-BuildToolsAreAvailable + +# 2. Publish CloudEvents Module +Write-InfoLog "Publish CloudEvents.Sdk Module to '$OutputDir'" +dotnet publish -c Release -o $OutputDir $dotnetProjectPath + +# 3. Cleanup Unnecessary Outputs +Get-ChildItem "$dotnetProjectName*" -Path $OutputDir | Remove-Item -Confirm:$false + +# 4. Run Unit Tests +Start-Tests -TestsType 'unit' + +# 5. Run Integration Tests +Start-Tests -TestsType 'integration' + +# 6. Prepare Module for Publishing +$dirItem = Get-Item $OutputDir +$catalogFilePath = Join-path $OutputDir ($dirItem.Name + ".cat") +if (Test-Path $catalogFilePath) { + # Delete previous catalog file + Remove-Item $catalogFilePath -Confirm:$false +} +New-FileCatalog -Path $OutputDir -CatalogFilePath $catalogFilePath | Out-Null diff --git a/src/CloudEventsPowerShell/CloudEvents.Sdk.psd1 b/src/CloudEventsPowerShell/CloudEvents.Sdk.psd1 new file mode 100644 index 0000000..783b80d --- /dev/null +++ b/src/CloudEventsPowerShell/CloudEvents.Sdk.psd1 @@ -0,0 +1,80 @@ + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'CloudEvents.Sdk.psm1' + +# Version number of this module. +ModuleVersion = '0.1.4' + +# Supported PSEditions +CompatiblePSEditions = @('Core') + +# ID used to uniquely identify this module +GUID = 'd0d7d392-0eab-40a8-8a3f-78ba41ef2f02' + +# Author of this module +Author = 'dmilov' + +# Company or vendor of this module +CompanyName = 'The CloudEvents Authors' + +# Copyright statement for this module +Copyright = '(c) The CloudEvents Authors + +# Description of the functionality provided by this module +Description = 'PowerShell CloudEvents SDK' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.0' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +RequiredAssemblies = @('CloudNative.CloudEvents.dll') + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @( +'New-CloudEvent', 'Add-CloudEventData', 'Add-CloudEventJsonData', 'Add-CloudEventXmlData', 'Read-CloudEventData', 'Read-CloudEventJsonData', 'Read-CloudEventXmlData', # CloudEvent Object Functions +'ConvertTo-HttpMessage', 'ConvertFrom-HttpMessage' # Http Binding Functions +) + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +} + diff --git a/src/CloudEventsPowerShell/CloudEvents.Sdk.psm1 b/src/CloudEventsPowerShell/CloudEvents.Sdk.psm1 new file mode 100644 index 0000000..1b959d9 --- /dev/null +++ b/src/CloudEventsPowerShell/CloudEvents.Sdk.psm1 @@ -0,0 +1,780 @@ +$xmlDataSerilizationLibPath = Join-Path (Join-Path $PSScriptRoot 'dataserialization') 'xml.ps1' +. $xmlDataSerilizationLibPath + +function New-CloudEvent { +<# + .SYNOPSIS + This function creates a new cloud event. + + + .DESCRIPTION + This function creates a new cloud event object with the provided parameters. + The result cloud event object has no data. Use Add-CloudEvent* functions to + add data to the cloud event object. + + .PARAMETER Type + Specifies the 'type' attribute of the cloud event. + + .PARAMETER Source + Specifies the 'source' attribute of the cloud event. + + .PARAMETER Id + Specifies the 'id' attribute of the cloud event. + + .PARAMETER Time + Specifies the 'time' attribute of the cloud event. + + .EXAMPLE + New-CloudEvent -Type 'com.example.object.deleted.v2' -Source 'mailto:cncf-wg-serverless@lists.cncf.io' -Id '6e8bc430-9c3a-11d9-9669-0800200c9a66' -Time (Get-Date) + + Creates a cloud event with Type, Source, Id, and Time +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Type, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Uri] + $Source, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Id, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [DateTime] + $Time +) + +PROCESS { + $cloudEvent = New-Object ` + -TypeName 'CloudNative.CloudEvents.CloudEvent' ` + -ArgumentList @( + $Type, + $Source, + $Id, + $Time, + @()) + + Write-Output $cloudEvent +} +} + +#region Add Data Functions +function Add-CloudEventData { +<# + .SYNOPSIS + This function adds data to a cloud event. + + .DESCRIPTION + This function adds data to a cloud event object with the provided parameters. + + .PARAMETER CloudEvent + Specifies the cloud event object to add data to. + + .PARAMETER Data + Specifies the data object that is added to the cloud event 'data' attribute. + + .PARAMETER DataContentType + Specifies the 'datacontenttype' attribute of the cloud event. + + + .EXAMPLE + $cloudEvent = New-CloudEvent -Type 'com.example.object.deleted.v2' -Source 'mailto:cncf-wg-serverless@lists.cncf.io' -Id '6e8bc430-9c3a-11d9-9669-0800200c9a66' -Time (Get-Date) + $cloudEvent | Add-CloudEventData -Data '' -DataContentType 'application/xml' + + Adds xml data to the cloud event +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [CloudNative.CloudEvents.CloudEvent] + $CloudEvent, + + [Parameter(Mandatory = $true, + ValueFromPipeline = $false)] + [ValidateNotNullOrEmpty()] + [object] + $Data, + + # CloudEvent 'datacontenttype' attribute. Content type of the 'data' attribute value. + # This attribute enables the data attribute to carry any type of content, whereby + # format and encoding might differ from that of the chosen event format. + [Parameter(Mandatory = $false, + ValueFromPipeline = $false)] + [string] + $DataContentType) + +PROCESS { + + # https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype + $contentType = New-Object ` + -TypeName 'System.Net.Mime.ContentType' ` + -ArgumentList ($DataContentType) + + $cloudEvent.Data = $Data + $cloudEvent.DataContentType = $dataContentType + + Write-Output $CloudEvent +} + +} + +function Add-CloudEventJsonData { +<# + .SYNOPSIS + This function adds JSON format data to a cloud event. + + .DESCRIPTION + This function converts a PowerShell hashtable to JSON format data and adds it to a cloud event. + + .PARAMETER CloudEvent + Specifies the cloud event object to add data to. + + .PARAMETER Data + Specifies the PowerShell hashtable object that is added as JSON to the cloud event 'data' attribute. + The 'datacontenttype' attribute is set to 'applicaiton/json' + + + .EXAMPLE + $cloudEvent = New-CloudEvent -Type 'com.example.object.deleted.v2' -Source 'mailto:cncf-wg-serverless@lists.cncf.io' -Id '6e8bc430-9c3a-11d9-9669-0800200c9a66' -Time (Get-Date) + $cloudEvent | Add-CloudEventJsonData -Data @{ 'key1' = 'value1'; 'key2' = 'value2'; } + + Adds JSON data to the cloud event +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [CloudNative.CloudEvents.CloudEvent] + $CloudEvent, + + [Parameter(Mandatory = $true, + ValueFromPipeline = $false)] + [ValidateNotNull()] + [Hashtable] + $Data, + + [Parameter(Mandatory = $false, + ValueFromPipeline = $false)] + [int] + $Depth = 3) + +PROCESS { + + # DataContentType is set to 'application/json' + # https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype + $dataContentType = New-Object ` + -TypeName 'System.Net.Mime.ContentType' ` + -ArgumentList ([System.Net.Mime.MediaTypeNames+Application]::Json) + + $cloudEvent.DataContentType = $dataContentType + $cloudEvent.Data = ConvertTo-Json -InputObject $Data -Depth $Depth + + Write-Output $CloudEvent +} + +} + +function Add-CloudEventXmlData { +<# + .SYNOPSIS + This function adds XML format data to a cloud event. + + .DESCRIPTION + This function converts a PowerShell hashtable to XML format data and adds it to a cloud event. + + .PARAMETER CloudEvent + Specifies the cloud event object to add data to. + + .PARAMETER Data + Specifies the PowerShell hashtable object that is added as XML to the cloud event 'data' attribute. + The 'datacontenttype' attribute is set to 'applicaiton/xml' + + .PARAMETER AttributesKeysInElementAttributes + Specifies how to format the XML. If specified and the input Data hashtable has pairs of 'Attributes', 'Value' keys + creates XML element with attributes, otherwise each key is formatted as XML element. + If true + @{'root' = @{'Attributes' = @{'att1' = 'true'}; 'Value' = 'val-1'}} would be 'val-1' + Otherwise + @{'root' = @{'Attributes' = @{'att1' = 'true'}; 'Value' = 'val-1'}} would be 'trueval-1' + + + .EXAMPLE + $cloudEvent = New-CloudEvent -Type 'com.example.object.deleted.v2' -Source 'mailto:cncf-wg-serverless@lists.cncf.io' -Id '6e8bc430-9c3a-11d9-9669-0800200c9a66' -Time (Get-Date) + $cloudEvent | Add-CloudEventXmlData -Data @{ 'key1' = 'value1'; 'key2' = 'value2'; } -AttributesKeysInElementAttributes $true + + Adds XML data to the cloud event +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [CloudNative.CloudEvents.CloudEvent] + $CloudEvent, + + [Parameter(Mandatory = $true, + ValueFromPipeline = $false)] + [ValidateNotNull()] + [Hashtable] + $Data, + + [Parameter(Mandatory = $true)] + [bool] + $AttributesKeysInElementAttributes) + +PROCESS { + + # DataContentType is set to 'application/xml' + $dataContentType = New-Object ` + -TypeName 'System.Net.Mime.ContentType' ` + -ArgumentList ([System.Net.Mime.MediaTypeNames+Application]::Xml) + + $cloudEvent.DataContentType = $dataContentType + $cloudEvent.Data = ConvertTo-CEDataXml -InputObject $Data -AttributesKeysInElementAttributes $AttributesKeysInElementAttributes + + Write-Output $CloudEvent +} + +} +#endregion Add Data Functions + +#region Read Data Functions +function Read-CloudEventData { +<# + .SYNOPSIS + This function gets the data from a cloud event. + + .DESCRIPTION + This function gets the data as-is from a cloud event. It is equiualent of accessing the Data property of a CloudEvent object + + .PARAMETER CloudEvent + Specifies the cloud event object to get data from. + + .EXAMPLE + $cloudEvent = ConvertFrom-HttpMessage -Headers $httpResponse.Headers -Body $httpResponse.Content + $cloudEvent | Read-CloudEventData + + Reads data from a cloud event received on the http response +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [CloudNative.CloudEvents.CloudEvent] + $CloudEvent +) + +PROCESS { + Write-Output $CloudEvent.Data +} + +} + +function Read-CloudEventJsonData { +<# + .SYNOPSIS + This function gets JSON fromat data from a cloud event as a PowerShell hashtable. + + .DESCRIPTION + This function gets the data from a cloud event and converts it to a PowerShell hashtable. + If the cloud event datacontenttype is not 'application/json' nothing is returned. + + .PARAMETER CloudEvent + Specifies the cloud event object to get data from. + + .PARAMETER Depth + Specifies how many levels of contained objects are included in the JSON representation. The default value is 3. + + .EXAMPLE + $cloudEvent = ConvertFrom-HttpMessage -Headers $httpResponse.Headers -Body $httpResponse.Content + $hashtable = $cloudEvent | Read-CloudEventJsonData + + Reads JSON data as a hashtable from a cloud event received on the http response +#> + + +<# + .DESCRIPTION + Returns PowerShell hashtable that represents the CloudEvent Json Data + if the data content type is 'application/json', otherwise otherwise non-terminating error and no result +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [CloudNative.CloudEvents.CloudEvent] + $CloudEvent, + + [Parameter(Mandatory = $false, + ValueFromPipeline = $false)] + [int] + $Depth = 3 +) + +PROCESS { + + # DataContentType is expected to be 'application/json' + # https://github.com/cloudevents/spec/blob/master/spec.md#datacontenttype + $dataContentType = New-Object ` + -TypeName 'System.Net.Mime.ContentType' ` + -ArgumentList ([System.Net.Mime.MediaTypeNames+Application]::Json) + + if ($CloudEvent.DataContentType -eq $dataContentType -or ` + ($CloudEvent.DataContentType -eq $null -and ` # Datacontent Type is Optional, if it is not specified we assume it is JSON as per https://github.com/cloudevents/spec/blob/v1.0.1/spec.md#datacontenttype + $cloudEvent.Data -is [Newtonsoft.Json.Linq.JObject])) { + + $data = $cloudEvent.Data + + if ($cloudEvent.Data -is [byte[]]) { + $data = [System.Text.Encoding]::UTF8.GetString($data) + } + + $result = $data.ToString() | ConvertFrom-Json -AsHashtable -Depth $Depth + + Write-Output $result + } else { + Write-Error "Cloud Event '$($cloudEvent.Id)' has no json data" + } +} + +} + +function Read-CloudEventXmlData { +<# + .SYNOPSIS + This function gets XML fromat data from a cloud event as a PowerShell hashtable. + + .DESCRIPTION + This function gets the data from a cloud event and converts it to a PowerShell hashtable. + If the cloud event datacontenttype is not 'application/xml' nothing is returned. + + .PARAMETER CloudEvent + Specifies the cloud event object to get data from. + + .PARAMETER ConvertMode + Specifies the how to convert the xml data to a hashtable + 'SkipAttributes' - Skips attributes of the XML elements. XmlElement is represented as a + Key-Value pair where key is the xml element name, and the value is the xml element inner text + + Example: + "value1" is converted to + @{'key' = 'value-1'} + + 'AlwaysAttrValue' - Each element is represented as a hashtable with two keys + 'Attributes' - key-value pair of the cml element attributes if any, otherwise null + 'Value' - string value represinting the xml element inner text + + Example: + "value1value2" is converted to + @{ + 'key1' = @{ + 'Attributes' = @{ + 'att' = 'true' + } + 'Value' = 'value1' + } + 'key2' = @{ + 'Attributes' = $null + 'Value' = 'value2' + } + + } + 'AttrValueWhenAttributes' - Uses 'SkipAttributes' for xml elements without attributes and + 'AlwaysAttrValue' for xml elements with attributes + Example: + "value1value2" is converted to + @{ + 'key1' = @{ + 'Attributes' = @{ + 'att' = 'true' + } + 'Value' = 'value1' + } + 'key2' = 'value2' + } + + .EXAMPLE + $cloudEvent = ConvertFrom-HttpMessage -Headers $httpResponse.Headers -Body $httpResponse.Content + $hashtable = $cloudEvent | Read-CloudEventXmlData -ConvertMode AttrValueWhenAttributes + + Reads XML data as a hashtable from a cloud event received on the http response +#> + + +<# + .DESCRIPTION + Returns PowerShell hashtable that represents the CloudEvent Xml Data + if the data content type is 'application/xml', otherwise non-terminating error and no result +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] + [CloudNative.CloudEvents.CloudEvent] + $CloudEvent, + + [Parameter(Mandatory = $true)] + [ValidateSet("SkipAttributes", "AlwaysAttrValue", "AttrValueWhenAttributes")] + [string] + $ConvertMode +) + +PROCESS { + + # DataContentType is expected to be 'application/xml' + $dataContentType = New-Object ` + -TypeName 'System.Net.Mime.ContentType' ` + -ArgumentList ([System.Net.Mime.MediaTypeNames+Application]::Xml) + + if ($CloudEvent.DataContentType -eq $dataContentType) { + + $data = $cloudEvent.Data + + if ($cloudEvent.Data -is [byte[]]) { + $data = [System.Text.Encoding]::UTF8.GetString($data) + } + + $result = $data.ToString() | ConvertFrom-CEDataXml -ConvertMode $ConvertMode + + Write-Output $result + } else { + Write-Error "Cloud Event '$($cloudEvent.Id)' has no xml data" + } +} + +} +#endregion Read Data Functions + +#region HTTP Protocol Binding Conversion Functions +function ConvertTo-HttpMessage { +<# + .SYNOPSIS + This function converts a cloud event object to a Http Message. + + .DESCRIPTION + This function converts a cloud event object to a PSObject with Headers and Body properties. + The 'Headers' propery is a hashtable that can pe provided to the 'Headers' parameter of the Inveok-WebRequest cmdlet. + The 'Body' propery is byte[] that can pe provided to the 'Body' parameter of the Inveok-WebRequest cmdlet. + + .PARAMETER CloudEvent + Specifies the cloud event object to convert. + + .PARAMETER ContentMode + Specifies the cloud event content mode. Structured and Binary content modes are supporterd. + + .EXAMPLE + $cloudEvent = New-CloudEvent -Type 'com.example.object.deleted.v2' -Source 'mailto:cncf-wg-serverless@lists.cncf.io' -Id '6e8bc430-9c3a-11d9-9669-0800200c9a66' -Time (Get-Date) + $cloudEvent | Add-CloudEventJsonData -Data @{ 'key1' = 'value1'; 'key2' = 'value2'; } + + $cloudEvent | ConvertTo-HttpMessage -ContentMode Binary + + Converts a cloud event object to Headers and Body formatted in Binary content mode. + + .EXAMPLE + $cloudEvent = New-CloudEvent -Type 'com.example.object.deleted.v2' -Source 'mailto:cncf-wg-serverless@lists.cncf.io' -Id '6e8bc430-9c3a-11d9-9669-0800200c9a66' -Time (Get-Date) + $cloudEvent | Add-CloudEventJsonData -Data @{ 'key1' = 'value1'; 'key2' = 'value2'; } + + $cloudEvent | ConvertTo-HttpMessage -ContentMode Structured + + Converts a cloud event object to Headers and Body formatted in Structured content mode. + + .EXAMPLE + $httpMessage = New-CloudEvent -Type 'com.example.object.deleted.v2' -Source 'mailto:cncf-wg-serverless@lists.cncf.io' -Id '6e8bc430-9c3a-11d9-9669-0800200c9a66' -Time (Get-Date) | ` + Add-CloudEventJsonData -Data @{ 'key1' = 'value1'; 'key2' = 'value2'; } | ` + ConvertTo-HttpMessage -ContentMode Structured + + Invoke-WebRequest -Uri 'http://localhost:52673/' -Headers $httpMessage.Headers -Body $httpMessage.Body + + Sends a cloud event http requests to a server +#> + +[CmdletBinding()] +param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNull()] + [CloudNative.CloudEvents.CloudEvent] + $CloudEvent, + + [Parameter( + Mandatory = $true, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [CloudNative.CloudEvents.ContentMode] + $ContentMode) + +PROCESS { + # Output Object + $result = New-Object -TypeName PSCustomObject + + $cloudEventFormatter = New-Object 'CloudNative.CloudEvents.JsonEventFormatter' + + $HttpHeaderPrefix = "ce-"; + $SpecVersionHttpHeader1 = $HttpHeaderPrefix + "cloudEventsVersion"; + $SpecVersionHttpHeader2 = $HttpHeaderPrefix + "specversion"; + + $headers = @{} + + # Build HTTP headers + foreach ($attribute in $cloudEvent.GetAttributes()) { + if (-not $attribute.Key.Equals([CloudNative.CloudEvents.CloudEventAttributes]::DataAttributeName($cloudEvent.SpecVersion)) -and ` + -not $attribute.Key.Equals([CloudNative.CloudEvents.CloudEventAttributes]::DataContentTypeAttributeName($cloudEvent.SpecVersion))) { + if ($attribute.Value -is [string]) { + $headers.Add(($HttpHeaderPrefix + $attribute.Key), $attribute.Value.ToString()) + } + elseif ($attribute.Value -is [DateTime]) { + $headers.Add(($HttpHeaderPrefix + $attribute.Key), $attribute.Value.ToString("u")) + } + elseif ($attribute.Value -is [Uri] -or $attribute.Value -is [int]) { + $headers.Add(($HttpHeaderPrefix + $attribute.Key), $attribute.Value.ToString()) + } + else + { + $headers.Add(($HttpHeaderPrefix + $attribute.Key), + [System.Text.Encoding]::UTF8.GetString($cloudEventFormatter.EncodeAttribute($cloudEvent.SpecVersion, $attribute.Key, + $attribute.Value, + $cloudEvent.Extensions.Values))); + } + } + } + + # Add Headers property to the output object + $result | Add-Member -MemberType NoteProperty -Name 'Headers' -Value $headers + + # Process Structured Mode + # Structured Mode supports non-batching JSON format only + # https://github.com/cloudevents/spec/blob/v1.0.1/http-protocol-binding.md#14-event-formats + if ($ContentMode -eq [CloudNative.CloudEvents.ContentMode]::Structured) { + # Format Body as byte[] + $contentType = $null + + # CloudEventFormatter is instance of 'CloudNative.CloudEvents.JsonEventFormatter' from the + # .NET CloudEvents SDK for the purpose of fomatting structured mode + $buffer = $cloudEventFormatter.EncodeStructuredEvent($cloudEvent, [ref] $contentType) + $result | Add-Member -MemberType NoteProperty -Name 'Body' -Value $buffer + $result.Headers.Add('Content-Type', $contentType) + } + + # Process Binary Mode + if ($ContentMode -eq [CloudNative.CloudEvents.ContentMode]::Binary) { + $bodyData = $null + + if ($cloudEvent.DataContentType -ne $null) { + $result.Headers.Add('Content-Type', $cloudEvent.DataContentType) + } + + if ($cloudEvent.Data -is [byte[]]) { + $bodyData = $cloudEvent.Data + } + elseif ($cloudEvent.Data -is [string]) { + $bodyData = [System.Text.Encoding]::UTF8.GetBytes($cloudEvent.Data.ToString()) + } + elseif ($cloudEvent.Data -is [IO.Stream]) { + $buffer = New-Object 'byte[]' -ArgumentList 1024 + + $ms = New-Object 'IO.MemoryStream' + + try { + $read = 0 + while (($read = $cloudEvent.Data.Read($buffer, 0, 1024)) -gt 0) + { + $ms.Write($buffer, 0, $read); + } + $bodyData = $ms.ToArray() + } finally { + $ms.Dispose() + } + + } else { + $bodyData = $cloudEventFormatter.EncodeAttribute($cloudEvent.SpecVersion, + [CloudNative.CloudEvents.CloudEventAttributes]::DataAttributeName($cloudEvent.SpecVersion), + $cloudEvent.Data, $cloudEvent.Extensions.Values) + } + + # Add Body property to the output object + $result | Add-Member -MemberType NoteProperty -Name 'Body' -Value $bodyData + } + + Write-Output $result +} +} + +function ConvertFrom-HttpMessage { +<# + .SYNOPSIS + This function converts a Http Message to a cloud event object + + .DESCRIPTION + This function converts a Http Message (Headers and Body) to a cloud event object. + Result of Invoke-WebRequest that contains a cloud event can be passed as input to this + function binding the the 'Headers' and 'Content' properties to the 'Headers' and 'Body' paramters. + + .PARAMETER Headers + Specifies the Http Headers as a PowerShell hashtable. + + .PARAMETER Body + Specifies the Http body as string or byte[]. + + .EXAMPLE + $httpReponse = Invoke-WebRequest -Uri 'http://localhost:52673/' -Headers $httpMessage.Headers -Body $httpMessage.Body + $cloudEvent = ConvertFrom-HttpMessage -Headers $httpResponse.Headers -Body $httpResponse.Content + + Converts a http response to a cloud event object +#> + +[CmdletBinding()] +param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNull()] + [hashtable] + $Headers, + + [Parameter( + Mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNull()] + $Body) + +PROCESS { + $HttpHeaderPrefix = "ce-"; + $SpecVersionHttpHeader1 = $HttpHeaderPrefix + "cloudEventsVersion"; + $SpecVersionHttpHeader2 = $HttpHeaderPrefix + "specversion"; + + $result = $null + + # Always Convert Body to byte[] + # Conversion works with byte[] while + # body can be string in HTTP responses + # for text content type + if ($Body -is [string]) { + $Body = [System.Text.Encoding]::UTF8.GetBytes($Body) + } + + if ($Headers['Content-Type'] -ne $null) { + $ContentType = $Headers['Content-Type'] + if ($ContentType -is [array]) { + # Get the first content-type value + $ContentType = $ContentType[0] + } + + if ($ContentType.StartsWith([CloudNative.CloudEvents.CloudEvent]::MediaType, + [StringComparison]::InvariantCultureIgnoreCase)) { + + # Handle Structured Mode + $ctParts = $ContentType.Split(';') + if ($ctParts[0].Trim().StartsWith(([CloudNative.CloudEvents.CloudEvent]::MediaType) + ([CloudNative.CloudEvents.JsonEventFormatter]::MediaTypeSuffix), + [StringComparison]::InvariantCultureIgnoreCase)) { + + # Structured Mode supports non-batching JSON format only + # https://github.com/cloudevents/spec/blob/v1.0.1/http-protocol-binding.md#14-event-formats + + # .NET SDK 'CloudNative.CloudEvents.JsonEventFormatter' type is used + # to decode the Structured Mode CloudEvents + + $json = [System.Text.Encoding]::UTF8.GetString($Body) + $jObject = [Newtonsoft.Json.Linq.JObject]::Parse($json) + $formatter = New-Object 'CloudNative.CloudEvents.JsonEventFormatter' + $result = $formatter.DecodeJObject($jObject, $null) + + $result.Data = $result.Data + } else { + # Throw error for unsupported encoding + throw "Unsupported CloudEvents encoding" + } + } else { + # Handle Binary Mode + $version = [CloudNative.CloudEvents.CloudEventsSpecVersion]::Default + if ($Headers.Contains($SpecVersionHttpHeader1)) { + $version = [CloudNative.CloudEvents.CloudEventsSpecVersion]::V0_1 + } + + if ($Headers.Contains($SpecVersionHttpHeader2)) { + if ($Headers[$SpecVersionHttpHeader2][0] -eq "0.2") { + $version = [CloudNative.CloudEvents.CloudEventsSpecVersion]::V0_2 + } elseif ($Headers[$SpecVersionHttpHeader2][0] -eq "0.3") { + $version = [CloudNative.CloudEvents.CloudEventsSpecVersion]::V0_3 + } + } + + $cloudEvent = New-Object ` + -TypeName 'CloudNative.CloudEvents.CloudEvent' ` + -ArgumentList @($version, $null); + + $attributes = $cloudEvent.GetAttributes(); + + # Get attributes from HTTP Headers + foreach ($httpHeader in $Headers.GetEnumerator()) { + if ($httpHeader.Key.Equals($SpecVersionHttpHeader1, [StringComparison]::InvariantCultureIgnoreCase) -or ` + $httpHeader.Key.Equals($SpecVersionHttpHeader2, [StringComparison]::InvariantCultureIgnoreCase)) { + continue + } + + if ($httpHeader.Key.StartsWith($HttpHeaderPrefix, [StringComparison]::InvariantCultureIgnoreCase)) { + $headerValue = $httpHeader.Value + if ($headerValue -is [array]) { + # Get the first object + $headerValue = $headerValue[0] + } + $name = $httpHeader.Key.Substring(3); + + # Abolished structures in headers in 1.0 + if ($version -ne [CloudNative.CloudEvents.CloudEventsSpecVersion]::V0_1 -and ` + $headerValue -ne $null -and ` + $headerValue.StartsWith('"') -and ` + $headerValue.EndsWith('"') -or ` + $headerValue.StartsWith("'") -and $headerValue.EndsWith("'") -or ` + $headerValue.StartsWith("{") -and $headerValue.EndsWith("}") -or ` + $headerValue.StartsWith("[") -and $headerValue.EndsWith("]")) { + + $jsonFormatter = New-Object 'CloudNative.CloudEvents.JsonEventFormatter' + + $attributes[$name] = $jsonFormatter.DecodeAttribute($version, $name, + [System.Text.Encoding]::UTF8.GetBytes($headerValue), $null); + } else { + $attributes[$name] = $headerValue + } + } + } + + if ($Headers['Content-Type'] -ne $null -and $Headers['Content-Type'][0] -is [string]) { + $cloudEvent.DataContentType = New-Object 'System.Net.Mime.ContentType' -ArgumentList @($Headers['Content-Type'][0]) + } + + # Get Data from HTTP Body + $cloudEvent.Data = $Body + + $result = $cloudEvent + } + } + + Write-Output $result +} +} +#endregion HTTP Protocol Binding Conversion Functions \ No newline at end of file diff --git a/src/CloudEventsPowerShell/CloudEventsPowerShell.csproj b/src/CloudEventsPowerShell/CloudEventsPowerShell.csproj new file mode 100644 index 0000000..9235420 --- /dev/null +++ b/src/CloudEventsPowerShell/CloudEventsPowerShell.csproj @@ -0,0 +1,26 @@ + + + + CloudEventsPowerShell + CloudEventsPowerShell + CloudEvents PowerShell SDK. + netstandard2.0 + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + \ No newline at end of file diff --git a/src/CloudEventsPowerShell/dataserialization/xml.Tests.ps1 b/src/CloudEventsPowerShell/dataserialization/xml.Tests.ps1 new file mode 100644 index 0000000..df51aad --- /dev/null +++ b/src/CloudEventsPowerShell/dataserialization/xml.Tests.ps1 @@ -0,0 +1,363 @@ +BeforeAll { + . $PSCommandPath.Replace('.Tests.ps1', '.ps1') +} + +Describe "CloudEvent Xml Serializers Unit Tests" { + Context "ConvertFrom-CEDataXml" { + It "Should convert single element XML text to a hashtable" { + # Arrange + $inputXml = "value" + $expected = @{'key' = 'value'} + + # Act + $actual = ConvertFrom-CEDataXml -InputString $inputXml -ConvertMode SkipAttributes + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.Keys.Count | Should -Be $expected.Keys.Count + $actual.key | Should -Be $expected.key + } + + It "Should convert XML with array of nodes to a hashtable" { + # Arrange + $inputXml = "value1value2value3" + $expected = @{'keys' = @{'key' = @('value1', 'value2', 'value3')}} + + # Act + $actual = ConvertFrom-CEDataXml -InputString $inputXml -ConvertMode SkipAttributes + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.keys.key[0] | Should -Be $expected.keys.key[0] + $actual.keys.key[1] | Should -Be $expected.keys.key[1] + $actual.keys.key[2] | Should -Be $expected.keys.key[2] + } + + It "Should convert XML with array of nodes to a hashtable and ConvertMode 'AlwaysAttrValue'" { + # Arrange + $inputXml = "value1value2value3" + $expected = @{ + 'keys' = @{ + 'Attributes' = $null + 'Value' = @{ + 'key' = @{ + 'Attributes' = $null + 'Value' = @( + @{ + 'Attributes' = $null + 'Value' = 'value1' + }, + @{ + 'Attributes' = $null + 'Value' = 'value2' + }, + @{ + 'Attributes' = $null + 'Value' = 'value3' + } + ) + } + } + } + + } + + # Act + $actual = ConvertFrom-CEDataXml -InputString $inputXml -ConvertMode AlwaysAttrValue + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.keys.Attributes | Should -Be $null + $actual.keys.Value.key.Value[0].Value | Should -Be $expected.keys.Value.key.Value[0].Value + $actual.keys.Value.key.Attributes | Should -Be $null + $actual.keys.Value.key.Value[0].Attributes | Should -Be $null + $actual.keys.Value.key.Value[1].Value | Should -Be $expected.keys.Value.key.Value[1].Value + $actual.keys.Value.key.Value[2].Value | Should -Be $expected.keys.Value.key.Value[2].Value + } + + It "Should convert single element XML with ConvertMode = 'AlwaysAttrValue'" { + # Arrange + $inputXml = "value-1value-2" + $expected = @{ + 'root' = @{ + 'Attributes' = $null + 'Value' = @{ + 'key1' = @{ + 'Attributes' = @{ + 'att1' = 'true' + } + 'Value' = 'value-1' + } + 'key2' = @{ + 'Attributes' = $null + 'Value' = 'value-2' + } + } + } + } + + # Act + $actual = ConvertFrom-CEDataXml -InputString $inputXml -ConvertMode AlwaysAttrValue + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.root.Value.key1.Attributes.att1 | Should -Be $expected.root.Value.key1.Attributes.att1 + $actual.root.Value.key1.Value | Should -Be $expected.root.Value.key1.Value + $actual.root.Value.key2.Attributes | Should -Be $expected.root.Value.key2.Attributes + $actual.root.Value.key2.Value | Should -Be $expected.root.Value.key2.Value + } + + It "Should convert elements with attributes with ConvertMode = 'AttrValueWhenAttributes'" { + # Arrange + $inputXml = "valuevalue-1" + $expected = @{ + 'root' = @{ + 'key' = 'value' + 'withattr' = @{ + 'Attributes' = @{ + 'att1' = 'true' + 'att2' = 'false' + } + 'Value' = 'value-1' + } + } + } + + # Act + $actual = ConvertFrom-CEDataXml -InputString $inputXml -ConvertMode AttrValueWhenAttributes + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.root.key | Should -Be $expected.root.key + $actual.root.withattr.Attributes.att1 | Should -Be $expected.root.withattr.Attributes.att1 + $actual.root.withattr.Attributes.att2 | Should -Be $expected.root.withattr.Attributes.att2 + $actual.root.withattr.Value | Should -Be $expected.root.withattr.Value + } + + It "Should convert XML with nested elements to a hashtable skipping attribute properties" { + # Arrange + $inputXml = '857085702021-02-04T08:51:53.723999ZdcuivcqaDCdatacenter-2clsdomain-c710.161.140.163host-21User dcui@127.0.0.1 logged in as VMware-client/6.5.0127.0.0.1VMware-client/6.4.0en52b910cf-661f-f72d-9f86-fb82113404b7' + $expected = @{ + 'UserLoginSessionEvent' = @{ + 'key' = '8570' + 'createdTime' = '2021-02-04T08:51:53.723999Z' + 'userName' = 'dcui' + 'datacenter' = @{ + 'name' = 'vcqaDC' + 'datacenter' = 'datacenter-2' + } + 'computeResource' = @{ + 'name' = 'cls' + 'computeResource' = 'domain-c7' + } + 'host' = @{ + 'name' = '10.161.140.163' + 'host' = 'host-21' + } + 'fullFormattedMessage' = 'User dcui@127.0.0.1 logged in as VMware-client/6.5.0' + 'ipAddress' = '127.0.0.1' + 'userAgent' = 'VMware-client/6.4.0' + 'locale' = 'en' + 'sessionId' = '52b910cf-661f-f72d-9f86-fb82113404b7' + } + } + + # Act + $actual = ConvertFrom-CEDataXml -InputString $inputXml -ConvertMode SkipAttributes + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.UserLoginSessionEvent -is [hashtable] | Should -Be $true + $actual.UserLoginSessionEvent.key | Should -Be $expected.UserLoginSessionEvent.key + $actual.UserLoginSessionEvent.createdTime | Should -Be $expected.UserLoginSessionEvent.createdTime + $actual.UserLoginSessionEvent.userName | Should -Be $expected.UserLoginSessionEvent.userName + $actual.UserLoginSessionEvent.datacenter -is [hashtable] | Should -Be $true + $actual.UserLoginSessionEvent.datacenter.name | Should -Be $expected.UserLoginSessionEvent.datacenter.name + $actual.UserLoginSessionEvent.datacenter.datacenter | Should -Be $expected.UserLoginSessionEvent.datacenter.datacenter + $actual.UserLoginSessionEvent.host -is [hashtable] | Should -Be $true + $actual.UserLoginSessionEvent.host.name | Should -Be $expected.UserLoginSessionEvent.host.name + $actual.UserLoginSessionEvent.host.host | Should -Be $expected.UserLoginSessionEvent.host.host + $actual.UserLoginSessionEvent.fullFormattedMessage | Should -Be $expected.UserLoginSessionEvent.fullFormattedMessage + } + + It "Should convert XML with nested elements to a hashtable with ConvertMode = 'AttrValueWhenAttributes'" { + # Arrange + $inputXml = '857085702021-02-04T08:51:53.723999ZdcuivcqaDCdatacenter-2clsdomain-c710.161.140.163host-21User dcui@127.0.0.1 logged in as VMware-client/6.5.0127.0.0.1VMware-client/6.4.0en52b910cf-661f-f72d-9f86-fb82113404b7' + $expected = @{ + 'UserLoginSessionEvent' = @{ + 'key' = '8570' + 'createdTime' = '2021-02-04T08:51:53.723999Z' + 'userName' = 'dcui' + 'datacenter' = @{ + 'name' = 'vcqaDC' + 'datacenter' = @{ + 'Attributes' = @{ + 'type' = 'Datacenter' + } + 'Value' = 'datacenter-2' + } + } + 'computeResource' = @{ + 'name' = 'cls' + 'computeResource' = @{ + 'Attributes' = @{ + 'type' = 'ClusterComputeResource' + } + 'Value' = 'domain-c7' + } + } + 'host' = @{ + 'name' = '10.161.140.163' + 'host' = @{ + 'Attributes' = @{ + 'type' = 'HostSystem' + } + 'Value' = 'host-21' + } + } + 'fullFormattedMessage' = 'User dcui@127.0.0.1 logged in as VMware-client/6.5.0' + 'ipAddress' = '127.0.0.1' + 'userAgent' = 'VMware-client/6.4.0' + 'locale' = 'en' + 'sessionId' = '52b910cf-661f-f72d-9f86-fb82113404b7' + } + } + + # Act + $actual = ConvertFrom-CEDataXml -InputString $inputXml -ConvertMode AttrValueWhenAttributes + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.UserLoginSessionEvent -is [hashtable] | Should -Be $true + $actual.UserLoginSessionEvent.key | Should -Be $expected.UserLoginSessionEvent.key + $actual.UserLoginSessionEvent.createdTime | Should -Be $expected.UserLoginSessionEvent.createdTime + $actual.UserLoginSessionEvent.userName | Should -Be $expected.UserLoginSessionEvent.userName + $actual.UserLoginSessionEvent.datacenter -is [hashtable] | Should -Be $true + $actual.UserLoginSessionEvent.datacenter.name | Should -Be $expected.UserLoginSessionEvent.datacenter.name + $actual.UserLoginSessionEvent.datacenter.datacenter.Attributes.type | Should -Be $expected.UserLoginSessionEvent.datacenter.datacenter.Attributes.type + $actual.UserLoginSessionEvent.datacenter.datacenter.Value | Should -Be $expected.UserLoginSessionEvent.datacenter.datacenter.Value + $actual.UserLoginSessionEvent.computeResource.computeResource.Attributes.type | Should -Be $expected.UserLoginSessionEvent.computeResource.computeResource.Attributes.type + $actual.UserLoginSessionEvent.computeResource.computeResource.Value | Should -Be $expected.UserLoginSessionEvent.computeResource.computeResource.Value + $actual.UserLoginSessionEvent.host.host.Attributes.type | Should -Be $expected.UserLoginSessionEvent.host.host.Attributes.type + $actual.UserLoginSessionEvent.host.host.Value | Should -Be $expected.UserLoginSessionEvent.host.host.Value + } + } + + Context "ConvertTo-CEDataXml" { + It "Should convert single hashtable to XML" { + # Arrange + $inputHashtable = @{'key' = 'value'} + $expected = "value" + + # Act + $actual = ConvertTo-CEDataXml -InputObject $inputHashtable -AttributesKeysInElementAttributes $false + + # Assert + $actual | Should -Be $expected + } + + It "Should convert hashtable with array values to XML " { + # Arrange + $inputHashtable = @{'keys' = @{'key' = @('value1', 'value2', 'value3')}} + $expected = "value1value2value3" + + # Act + $actual = ConvertTo-CEDataXml -InputObject $inputHashtable -AttributesKeysInElementAttributes $false + + # Assert + $actual | Should -Be $expected + } + + It "Should convert hashtable with hashtable values to XML " { + # Arrange + $inputHashtable = @{ + 'UserLoginSessionEvent' = @{ + 'datacenter' = @{ + 'datacenter' = 'datacenter-2' + } + } + } + $expected = 'datacenter-2' + + # Act + $actual = ConvertTo-CEDataXml -InputObject $inputHashtable -AttributesKeysInElementAttributes $false + + # Assert + $actual | Should -Be $expected + } + + It "Should convert hashtable with Attributes keys to XML elements without attributes" { + # Arrange + $inputHashtable = @{ + 'UserLoginSessionEvent' = @{ + 'computeResource' = @{ + 'name' = 'cls' + 'computeResource' = @{ + 'Attributes' = @{ + 'type' = 'ClusterComputeResource' + } + 'Value' = 'domain-c7' + } + } + } + } + $expected = 'ClusterComputeResourcedomain-c7cls' + + # Act + $actual = ConvertTo-CEDataXml -InputObject $inputHashtable -AttributesKeysInElementAttributes $false + + # Assert + ## We can not expected the xml elements to be ordered as in the expected string, + ## so test Xml ignoring the elements order + $actual.Contains('ClusterComputeResource') | Should -Be $true + $actual.Contains('domain-c7') | Should -Be $true + $actual.Contains('cls') | Should -Be $true + $actual.IndexOf('') | Should -BeGreaterThan $actual.IndexOf('') + $actual.IndexOf('') | Should -BeLessThan $actual.IndexOf('') + } + + + It "Should convert hashtable with Attributes keys to XML elements with attributes" { + # Arrange + $inputHashtable = @{ + 'UserLoginSessionEvent' = @{ + 'computeResource' = @{ + 'name' = 'cls' + 'computeResource' = @{ + 'Attributes' = @{ + 'type' = 'ClusterComputeResource' + } + 'Value' = 'domain-c7' + } + } + } + } + $expected = 'domain-c7cls' + + # Act + $actual = ConvertTo-CEDataXml -InputObject $inputHashtable -AttributesKeysInElementAttributes $true + + # Assert + ## We can not expected the xml elements to be ordered as in the expected string, + ## so test Xml ignoring the elements order + $actual.StartsWith('') | Should -Be $true + $actual.Contains('domain-c7') | Should -Be $true + $actual.Contains('cls') | Should -Be $true + } + + It "Should throw when input hashtable has more than one root" { + # Arrange + $inputHashtable = @{'key1' = 'val1'; 'key2' = 'val2'} + + # Act & Assert + { ConvertTo-CEDataXml -InputObject $inputHashtable -AttributesKeysInElementAttributes $false } | Should -Throw 'Input Hashtable must have single root key' + } + } +} \ No newline at end of file diff --git a/src/CloudEventsPowerShell/dataserialization/xml.ps1 b/src/CloudEventsPowerShell/dataserialization/xml.ps1 new file mode 100644 index 0000000..a4022b5 --- /dev/null +++ b/src/CloudEventsPowerShell/dataserialization/xml.ps1 @@ -0,0 +1,187 @@ +$SKIPATTR = "SkipAttributes" +$ALWAYSATTRVALUE = "AlwaysAttrValue" +$ATTRVALUEFORELEMENTSWITHATTR = "AttrValueWhenAttributes" +function ConvertFrom-XmlPropertyValue { +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $false)] + $InputObject, + + [Parameter(Mandatory = $true)] + [ValidateSet("SkipAttributes", "AlwaysAttrValue", "AttrValueWhenAttributes")] + [string] + $ConvertMode +) + + $value = $InputObject + $Attributes = $null + + if ($InputObject -is [Xml.XmlElement]) { + $hasAttributes = (($InputObject | Get-Member -MemberType Properties) | Where-Object {$_.Name -eq '#text'}) -ne $null + if ($hasAttributes) { + $Attributes = @{} + $arrProperties = $InputObject | Get-Member -MemberType Properties + foreach ($p in $arrProperties) { + if ($p.Name -eq '#text') { + $value = $InputObject.'#text' + } else { + $Attributes[$p.Name] = $InputObject.$($p.Name) + } + } + + } else { + $value = ConvertFrom-CEDataXml -InputXmlElement $InputObject -ConvertMode $ConvertMode + } + } + + if ($InputObject -is [array]) { + $value = @() + foreach ($obj in $InputObject) { + $value += ConvertFrom-XmlPropertyValue -InputObject $obj -ConvertMode $ConvertMode + } + } + + if (($ConvertMode -eq $SKIPATTR) -or + ($Attributes -eq $null -and $ConvertMode -eq $ATTRVALUEFORELEMENTSWITHATTR)) { + + $value + } + + if (($ConvertMode -eq $ALWAYSATTRVALUE) -or + ($Attributes -ne $null -and $ConvertMode -eq $ATTRVALUEFORELEMENTSWITHATTR)) { + @{ + "Attributes" = $Attributes + "Value" = $value + } + } +} + +function ConvertFrom-CEDataXml { +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true, + ParameterSetName="Text")] + [ValidateNotNull()] + [string] + $InputString, + + [Parameter(Mandatory = $true, + ValueFromPipeline = $false, + ParameterSetName="XmlElement")] + [ValidateNotNull()] + [Xml.XmlElement] + $InputXmlElement, + + [Parameter(Mandatory = $true)] + [ValidateSet("SkipAttributes", "AlwaysAttrValue", "AttrValueWhenAttributes")] + [string] + $ConvertMode +) + $result = $null + if ($InputString -ne $null) { + $xmlDocument = [xml]$InputString + } + if ($InputXmlElement -ne $null) { + $xmlDocument = $InputXmlElement + } + + if ($xmlDocument -ne $null) { + $xmlProperties = $xmlDocument | Get-Member -MemberType Properties + + $result = @{} + + + foreach ($property in $xmlProperties) { + $propertyName = $property.Name + $value = ConvertFrom-XmlPropertyValue -InputObject $xmlDocument.$propertyName -ConvertMode $ConvertMode + $result[$propertyName] = $value + } + } + + $result +} + +function New-XmlElement { +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $false)] + [ValidateNotNull()] + $DictionaryEntry, + + [Parameter(Mandatory = $true, + ValueFromPipeline = $false)] + $XmlDocument, + + [Parameter(Mandatory = $false)] + [Switch] + $AttributesKeysInElementAttributes + +) + $result = $XmlDocument.CreateElement($DictionaryEntry.Name) + + $value = "" + + if ($DictionaryEntry.Value -is [hashtable]) { + if($DictionaryEntry.Value.Keys.Count -eq 2 -and ` + $DictionaryEntry.Value['Attributes'] -is [hashtable] -and ` + $DictionaryEntry.Value['Value'] -ne $null -and ` + $AttributesKeysInElementAttributes) { + foreach ($attKv in $DictionaryEntry.Value['Attributes'].GetEnumerator()) { + $result.SetAttribute($attKv.Name, $attKv.Value) + } + $result.InnerText = $DictionaryEntry.Value['Value'].ToString() + + } else { + foreach ($nameValue in $DictionaryEntry.Value.GetEnumerator()) { + $xmlElement = New-XmlElement -DictionaryEntry $nameValue -XmlDocument $XmlDocument -AttributesKeysInElementAttributes:$AttributesKeysInElementAttributes + $xmlElement | Foreach-Object { + $result.AppendChild($_) | Out-Null + } + } + } + } elseif ($DictionaryEntry.Value -is [array]) { + $result = @() + foreach ($item in $DictionaryEntry.Value) { + $result += (New-XmlElement ` + -DictionaryEntry ` + (New-Object System.Collections.DictionaryEntry ` + -ArgumentList @($DictionaryEntry.Name, $item)) ` + -XmlDocument $XmlDocument ` + -AttributesKeysInElementAttributes:$AttributesKeysInElementAttributes) + } + } else { + $value = $DictionaryEntry.Value.ToString() + $result.InnerText = $value + } + + $result +} + +function ConvertTo-CEDataXml { +param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateNotNull()] + [Hashtable] + $InputObject, + + [Parameter(Mandatory = $true)] + [bool] + $AttributesKeysInElementAttributes +) + if ($InputObject.Keys.Count -ne 1) { + throw "Input Hashtable must have single root key" + } + + [xml]$resultDocument = New-Object System.Xml.XmlDocument + + foreach ($nameValue in $InputObject.GetEnumerator()) { + $element = New-XmlElement -DictionaryEntry $nameValue -XmlDocument $resultDocument -AttributesKeysInElementAttributes:$AttributesKeysInElementAttributes + + $element | Foreach-Object { + $resultDocument.AppendChild($_) | Out-Null + } + } + + $resultDocument.OuterXml +} \ No newline at end of file diff --git a/test/RunTests.ps1 b/test/RunTests.ps1 new file mode 100644 index 0000000..0d9cac7 --- /dev/null +++ b/test/RunTests.ps1 @@ -0,0 +1,42 @@ +param( + [Parameter()] + [ValidateScript({Test-Path $_})] + [string] + $CloudEventsModulePath, + + [Parameter()] + [ValidateSet('unit', 'integration', 'all')] + [string] + $TestsType, + + [Parameter()] + [Switch] + $EnableProcessExit +) + +Import-Module $CloudEventsModulePath + +if ($TestsType -eq 'unit' -or $TestsType -eq 'all') { + $pesterContainer = New-PesterContainer -Path (Join-Path $PSScriptRoot 'unit') + $pesterConfiguration = [PesterConfiguration]::Default + + $pesterConfiguration.Run.Path = (Join-Path $PSScriptRoot 'unit') + $pesterConfiguration.Run.Container = $pesterContainer + + Invoke-Pester -Configuration $pesterConfiguration +} + +if ($TestsType -eq 'integration' -or $TestsType -eq 'all') { + + $testsData = @{ + CloudEventsModulePath = $CloudEventsModulePath + } + + $pesterContainer = New-PesterContainer -Path (Join-Path $PSScriptRoot 'integration') -Data $testsData + $pesterConfiguration = [PesterConfiguration]::Default + + $pesterConfiguration.Run.Path = (Join-Path $PSScriptRoot 'integration') + $pesterConfiguration.Run.Container = $pesterContainer + + Invoke-Pester -Configuration $pesterConfiguration +} diff --git a/test/integration/HttpIntegration.Tests.ps1 b/test/integration/HttpIntegration.Tests.ps1 new file mode 100644 index 0000000..18f9f7f --- /dev/null +++ b/test/integration/HttpIntegration.Tests.ps1 @@ -0,0 +1,275 @@ +param( + [Parameter()] + [ValidateScript({Test-Path $_})] + [string] + $CloudEventsModulePath) + +Describe "Client-Server Integration Tests" { + Context "Send And Receive CloudEvents over Http" { + BeforeAll { + $testServerUrl = 'http://localhost:52673/' + + $serverProcess = $null + + . (Join-Path $PSScriptRoot 'ProtocolConstants.ps1') + + # Starts CloudEvent Test Server + $usePowerShell = (Get-Process -Id $pid).ProcessName + $serverScript = Join-Path $PSScriptRoot 'HttpServer.ps1' + $serverProcessArguments = "-Command $serverScript -CloudEventsModulePath '$CloudEventsModulePath' -ServerUrl '$testServerUrl'" + + $serverProcess = Start-Process ` + -FilePath $usePowerShell ` + -ArgumentList $serverProcessArguments ` + -PassThru ` + -NoNewWindow + } + + AfterAll { + # Requests Stop CloudEvent Test Server + $serverStopRequest = ` + New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type $script:ServerStopType ` + -Source $script:ClientSource | ` + ConvertTo-HttpMessage ` + -ContentMode Structured + + Invoke-WebRequest ` + -Uri $testServerUrl ` + -Headers $serverStopRequest.Headers ` + -Body $serverStopRequest.Body | Out-Null + + if ($serverProcess -ne $null -and ` + -not $serverProcess.HasExited) { + $serverProcess | Wait-Process + } + } + + It 'Echo binary content mode cloud events' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Type $script:EchoBinaryType ` + -Source $script:ClientSource ` + -Id 'integration-test-1' ` + -Time (Get-Date) | ` + Add-CloudEventJsonData -Data @{ + 'a1' = 'b' + 'a2' = 'c' + 'a3' = 'd' + } + + # Act + + ## Convert CloudEvent to HTTP Message + $httpRequest = ConvertTo-HttpMessage ` + -CloudEvent $cloudEvent ` + -ContentMode Binary + + ## Invoke WebRequest with the HTTP Message + $httpResponse = Invoke-WebRequest ` + -Uri $testServerUrl ` + -Headers $httpRequest.Headers ` + -Body $httpRequest.Body + + ## Convert HTTP Response to CloudEvent + $resultCloudEvent = ConvertFrom-HttpMessage ` + -Headers $httpResponse.Headers ` + -Body $httpResponse.Content + + # Assert + + ## Assert echoed CloudEvent + $resultCloudEvent | Should -Not -Be $null + $resultCloudEvent.Source | Should -Be $script:ServerSource + $resultCloudEvent.Type | Should -Be $script:EchoBinaryType + $resultCloudEvent.Id | Should -Be $cloudEvent.Id + $resultCloudEvent.Time | Should -BeGreaterThan $cloudEvent.Time + + ## Assert Result CloudEvent Data + ## Read Data as Json + $resultData = $resultCloudEvent | Read-CloudEventJsonData + $resultData.a1 | Should -Be 'b' + $resultData.a2 | Should -Be 'c' + $resultData.a3 | Should -Be 'd' + } + + It 'Echo binary content mode cloud events with XML data' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Type $script:EchoBinaryType ` + -Source $script:ClientSource ` + -Id 'integration-test-2' ` + -Time (Get-Date) | ` + Add-CloudEventXmlData -Data @{ + 'a1' = @{ + 'a2' = 'c' + 'a3' = 'd' + } + } ` + -AttributesKeysInElementAttributes $false + + # Act + + ## Convert CloudEvent to HTTP Message + $httpRequest = ConvertTo-HttpMessage ` + -CloudEvent $cloudEvent ` + -ContentMode Binary + + ## Invoke WebRequest with the HTTP Message + $httpResponse = Invoke-WebRequest ` + -Uri $testServerUrl ` + -Headers $httpRequest.Headers ` + -Body $httpRequest.Body + + ## Convert HTTP Response to CloudEvent + $resultCloudEvent = ConvertFrom-HttpMessage ` + -Headers $httpResponse.Headers ` + -Body $httpResponse.Content + + # Assert + + ## Assert echoed CloudEvent + $resultCloudEvent | Should -Not -Be $null + $resultCloudEvent.Source | Should -Be $script:ServerSource + $resultCloudEvent.Type | Should -Be $script:EchoBinaryType + $resultCloudEvent.Id | Should -Be $cloudEvent.Id + $resultCloudEvent.Time | Should -BeGreaterThan $cloudEvent.Time + + ## Assert Result CloudEvent Data + ## Read Data as Xml + $resultData = $resultCloudEvent | Read-CloudEventXmlData -ConvertMode 'SkipAttributes' + $resultData -is [hashtable] | Should -Be $true + $resultData.a1 -is [hashtable] | Should -Be $true + $resultData.a1.a2 | Should -Be 'c' + $resultData.a1.a3 | Should -Be 'd' + } + + It 'Echo structured content mode cloud events' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Type $script:EchoStructuredType ` + -Source $script:ClientSource ` + -Id 'integration-test-3' ` + -Time (Get-Date) | ` + Add-CloudEventJsonData -Data @{ + 'b1' = 'd' + 'b2' = 'e' + 'b3' = 'f' + } + + # Act + + ## Convert CloudEvent to HTTP Message + $httpRequest = ConvertTo-HttpMessage ` + -CloudEvent $cloudEvent ` + -ContentMode Structured + + ## Invoke WebRequest with the HTTP Message + $httpResponse = Invoke-WebRequest ` + -Uri $testServerUrl ` + -Headers $httpRequest.Headers ` + -Body $httpRequest.Body + + ## Convert HTTP Response to CloudEvent + $resultCloudEvent = ConvertFrom-HttpMessage ` + -Headers $httpResponse.Headers ` + -Body $httpResponse.Content + + # Assert + + ## Assert echoed CloudEvent + $resultCloudEvent | Should -Not -Be $null + $resultCloudEvent.Source | Should -Be $script:ServerSource + $resultCloudEvent.Type | Should -Be $script:EchoStructuredType + $resultCloudEvent.Id | Should -Be $cloudEvent.Id + $resultCloudEvent.Time | Should -BeGreaterThan $cloudEvent.Time + + ## Assert Result CloudEvent Data + ## Read Data as Json + $resultData = $resultCloudEvent | Read-CloudEventJsonData + $resultData.b1 | Should -Be 'd' + $resultData.b2 | Should -Be 'e' + $resultData.b3 | Should -Be 'f' + } + + It 'Echo structured content mode cloud events with XML data' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Type $script:EchoStructuredType ` + -Source $script:ClientSource ` + -Id 'integration-test-4' ` + -Time (Get-Date) | ` + Add-CloudEventXmlData -Data @{ + 'b1' = @{ + 'b2' = 'e' + 'b3' = 'f' + } + } ` + -AttributesKeysInElementAttributes $false + + # Act + + ## Convert CloudEvent to HTTP Message + $httpRequest = ConvertTo-HttpMessage ` + -CloudEvent $cloudEvent ` + -ContentMode Structured + + ## Invoke WebRequest with the HTTP Message + $httpResponse = Invoke-WebRequest ` + -Uri $testServerUrl ` + -Headers $httpRequest.Headers ` + -Body $httpRequest.Body + + ## Convert HTTP Response to CloudEvent + $resultCloudEvent = ConvertFrom-HttpMessage ` + -Headers $httpResponse.Headers ` + -Body $httpResponse.Content + + # Assert + + ## Assert echoed CloudEvent + $resultCloudEvent | Should -Not -Be $null + $resultCloudEvent.Source | Should -Be $script:ServerSource + $resultCloudEvent.Type | Should -Be $script:EchoStructuredType + $resultCloudEvent.Id | Should -Be $cloudEvent.Id + $resultCloudEvent.Time | Should -BeGreaterThan $cloudEvent.Time + + ## Assert Result CloudEvent Data + ## Read Data as Xml + $resultData = $resultCloudEvent | Read-CloudEventXmlData -ConvertMode 'SkipAttributes' + $resultData -is [hashtable] | Should -Be $true + $resultData.b1 -is [hashtable] | Should -Be $true + $resultData.b1.b2 | Should -Be 'e' + $resultData.b1.b3 | Should -Be 'f' + } + + It 'Send cloud event expecting no result' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Type 'no-content' ` + -Source $script:ClientSource ` + -Id 'integration-test-5' ` + -Time (Get-Date) | ` + Add-CloudEventData ` + -Data 'This is text data' ` + -DataContentType 'application/text' + + # Act + + ## Convert CloudEvent to HTTP Message + $httpRequest = ConvertTo-HttpMessage ` + -CloudEvent $cloudEvent ` + -ContentMode Structured + + ## Invoke WebRequest with the HTTP Message + $httpResponse = Invoke-WebRequest ` + -Uri $testServerUrl ` + -Headers $httpRequest.Headers ` + -Body $httpRequest.Body + + # Assert + $httpResponse.StatusCode | Should -Be ([int]([System.Net.HttpStatusCode]::NoContent)) + } + } +} \ No newline at end of file diff --git a/test/integration/HttpServer.ps1 b/test/integration/HttpServer.ps1 new file mode 100644 index 0000000..ced4987 --- /dev/null +++ b/test/integration/HttpServer.ps1 @@ -0,0 +1,146 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateScript({Test-Path $_})] + [string] + $CloudEventsModulePath, + + [Parameter( + Mandatory = $true, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNull()] + [string] + $ServerUrl +) + +. (Join-Path $PSScriptRoot 'ProtocolConstants.ps1') + +# Import SDK Module +Import-Module $CloudEventsModulePath + +function Start-HttpCloudEventListener { +<# + .DESCRIPTION + Starts a HTTP CloudEvent Listener on specified Url +#> + +[CmdletBinding()] +param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNull()] + [string] + $Url, + + [Parameter( + Mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [scriptblock] + $Handler +) + + $listener = New-Object -Type 'System.Net.HttpListener' + $listener.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Anonymous + $listener.Prefixes.Add($Url) + + try { + $listener.Start() + + $context = $listener.GetContext() + + # Read Input Stream + $buffer = New-Object 'byte[]' -ArgumentList 1024 + $ms = New-Object 'IO.MemoryStream' + $read = 0 + while (($read = $context.Request.InputStream.Read($buffer, 0, 1024)) -gt 0) { + $ms.Write($buffer, 0, $read); + } + $bodyData = $ms.ToArray() + $ms.Dispose() + + # Read Headers + $headers = @{} + for($i =0; $i -lt $context.Request.Headers.Count; $i++) { + $headers[$context.Request.Headers.GetKey($i)] = $context.Request.Headers.GetValues($i) + } + + $cloudEvent = ConvertFrom-HttpMessage -Headers $headers -Body $bodyData + + if ( $cloudEvent -ne $null ) { + $Handler.Invoke($cloudEvent, $context.Response) + $context.Response.Close(); + } else { + $context.Response.StatusCode = [int]([System.Net.HttpStatusCode]::BadRequest) + $context.Response.Close(); + } + + } catch { + Write-Error $_ + $context.Response.StatusCode = [int]([System.Net.HttpStatusCode]::InternalServerError) + $context.Response.Close(); + } finally { + $listener.Stop() + } +} + +$global:serverStopRequested = $false +while ( -not $global:serverStopRequested ) { + Start-HttpCloudEventListener -Url $ServerUrl -Handler { + $requestCloudEvent = $args[0] + $response = $args[1] + + # When CloudEvent Type is 'echo-structured' or 'echo-binary' the Server responds + # with CloudEvent in corresponding content mode + if ( $requestCloudEvent.Type -eq $script:EchoBinaryType -or ` + $requestCloudEvent.Type -eq $script:EchoStructuredType ) { + + # Create Cloud Event for the response + $cloudEvent = New-CloudEvent ` + -Type $requestCloudEvent.Type ` + -Source $script:ServerSource ` + -Time (Get-Date) ` + -Id $requestCloudEvent.Id + + # Add Data to the new Cloud Event + $requestCloudEventJsonData = $requestCloudEvent | Read-CloudEventJsonData + $requestCloudEventXmlData = $requestCloudEvent | Read-CloudEventXmlData -ConvertMode 'SkipAttributes' + if ($requestCloudEventJsonData) { + $cloudEvent = $cloudEvent | Add-CloudEventJsonData ` + -Data $requestCloudEventJsonData + } elseif ($requestCloudEventXmlData) { + $cloudEvent = $cloudEvent | Add-CloudEventXmlData ` + -Data $requestCloudEventXmlData ` + -AttributesKeysInElementAttributes $false + } else { + $requestCloudEventData = $requestCloudEvent | Read-CloudEventData + $cloudEvent = $cloudEvent | Add-CloudEventData ` + -Data $requestCloudEventData ` + -DataContentType $requestCloudEvent.DataContentType + } + + # Convert Cloud Event to HTTP Response + $contentMode = $requestCloudEvent.Type.TrimStart('echo-') + $httpMessage = $cloudEvent | ConvertTo-HttpMessage -ContentMode $contentMode + + $response.Headers = New-Object 'System.Net.WebHeaderCollection' + foreach ($keyValue in $httpMessage.Headers.GetEnumerator()) { + $response.Headers.Add($keyValue.Key, $keyValue.Value) + } + $response.ContentLength64 = $httpMessage.Body.Length + $response.OutputStream.Write($httpMessage.Body, 0, $httpMessage.Body.Length) + $response.StatusCode = [int]([System.Net.HttpStatusCode]::OK) + + } else { + # No Content in all other cases + $response.StatusCode = [int]([System.Net.HttpStatusCode]::NoContent) + } + + if ( $requestCloudEvent.Type -eq $script:ServerStopType ) { + # Server Stop is requested + $global:serverStopRequested = $true + } + } +} \ No newline at end of file diff --git a/test/integration/ProtocolConstants.ps1 b/test/integration/ProtocolConstants.ps1 new file mode 100644 index 0000000..839fa8f --- /dev/null +++ b/test/integration/ProtocolConstants.ps1 @@ -0,0 +1,5 @@ +New-Variable -Option Constant -Scope 'script' -Name 'ClientSource' -Value 'ps:test:client' +New-Variable -Option Constant -Scope 'script' -Name 'ServerSource' -Value 'ps:test:server' +New-Variable -Option Constant -Scope 'script' -Name 'EchoBinaryType' -Value 'echo-binary' +New-Variable -Option Constant -Scope 'script' -Name 'EchoStructuredType' -Value 'echo-structured' +New-Variable -Option Constant -Scope 'script' -Name 'ServerStopType' -Value 'server-stop' \ No newline at end of file diff --git a/test/unit/Add-CloudEventData.Tests.ps1 b/test/unit/Add-CloudEventData.Tests.ps1 new file mode 100644 index 0000000..18c4180 --- /dev/null +++ b/test/unit/Add-CloudEventData.Tests.ps1 @@ -0,0 +1,103 @@ +Describe "Add-CloudEventData Function Tests" { + Context "Adds Data" { + It 'Adds byte[] data' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $expectedData = [Text.Encoding]::UTF8.GetBytes("test") + $expectedDataContentType = 'application/octet-stream' + + + # Act + $actual = $cloudEvent | ` + Add-CloudEventData ` + -Data $expectedData ` + -DataContentType $expectedDataContentType + + # Assert + $actual | Should -Not -Be $null + $actual.DataContentType.ToString() | Should -Be $expectedDataContentType + $actual.Data | Should -Be $expectedData + } + + It 'Adds xml text data' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $expectedData = '' + $expectedDataContentType = 'application/xml' + + + # Act + $actual = $cloudEvent | ` + Add-CloudEventData ` + -Data $expectedData ` + -DataContentType $expectedDataContentType + + # Assert + $actual | Should -Not -Be $null + $actual.DataContentType.ToString() | Should -Be $expectedDataContentType + $actual.Data | Should -Be $expectedData + } + } + + + Context "Errors on invalid data content type" { + It 'Throws error on invalid MIME content type' { + # Arrange + $invalidContentType = 'invalid' + + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + # Act & Assert + { Add-CloudEventData ` + -CloudEvent $cloudEvent ` + -Data '1' ` + -DataContentType $invalidContentType } | ` + Should -Throw "*The specified content type is invalid*" + } + + It 'Throws error on empty content type' { + # Arrange + $invalidContentType = [string]::Empty + + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + # Act & Assert + { Add-CloudEventData ` + -CloudEvent $cloudEvent ` + -Data '1' ` + -DataContentType $invalidContentType } | ` + Should -Throw "*The parameter 'contentType' cannot be an empty string*" + } + + It 'Throws error on null content type' { + # Arrange + $invalidContentType = $null + + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + # Act & Assert + { Add-CloudEventData ` + -CloudEvent $cloudEvent ` + -Data '1' ` + -DataContentType $invalidContentType } | ` + Should -Throw "*The parameter 'contentType' cannot be an empty string*" + } + } +} \ No newline at end of file diff --git a/test/unit/Add-CloudEventJsonData.Tests.ps1 b/test/unit/Add-CloudEventJsonData.Tests.ps1 new file mode 100644 index 0000000..1eb940a --- /dev/null +++ b/test/unit/Add-CloudEventJsonData.Tests.ps1 @@ -0,0 +1,61 @@ +Describe "Add-CloudEventJsonData Function Tests" { + Context "Adds Json Data" { + It 'Adds json data with depth 1' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $expectedJson = '{ + "a": "b" +}' + + $htData = @{'a' = 'b'} + + # Act + $actual = $cloudEvent | Add-CloudEventJsonData -Data $htData + + # Assert + $actual | Should -Not -Be $null + $actual.DataContentType.ToString() | Should -Be 'application/json' + $actual.Data | Should -Be $expectedJson + } + + It 'Adds json data with depth 4' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $expectedJson = '{ + "1": { + "2": { + "3": { + "4": "wow" + } + } + } +}' + + $htData = @{ + '1' = @{ + '2' = @{ + '3' = @{ + '4' = 'wow' + } + } + } + } + + # Act + $actual = $cloudEvent | Add-CloudEventJsonData -Data $htData -Depth 4 + + # Assert + $actual | Should -Not -Be $null + $actual.DataContentType.ToString() | Should -Be 'application/json' + $actual.Data | Should -Be $expectedJson + } + } +} \ No newline at end of file diff --git a/test/unit/Add-CloudEventXmlData.Tests.ps1 b/test/unit/Add-CloudEventXmlData.Tests.ps1 new file mode 100644 index 0000000..6d189e1 --- /dev/null +++ b/test/unit/Add-CloudEventXmlData.Tests.ps1 @@ -0,0 +1,68 @@ +Describe "Add-CloudEventXmlData Function Tests" { + Context "Adds Xml Data" { + It 'Adds xml data with depth 1' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $expectedXml = 'b' + + $htData = @{'a' = 'b'} + + # Act + $actual = $cloudEvent | Add-CloudEventXmlData -Data $htData -AttributesKeysInElementAttributes $false + + # Assert + $actual | Should -Not -Be $null + $actual.DataContentType.ToString() | Should -Be 'application/xml' + $actual.Data | Should -Be $expectedXml + } + + It 'Adds xml data with depth 4' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $expectedXml = '<1><2><3><4>wow' + + $htData = @{ + '1' = @{ + '2' = @{ + '3' = @{ + '4' = 'wow' + } + } + } + } + + # Act + $actual = $cloudEvent | Add-CloudEventXmlData -Data $htData -AttributesKeysInElementAttributes $false + + # Assert + $actual | Should -Not -Be $null + $actual.DataContentType.ToString() | Should -Be 'application/xml' + $actual.Data | Should -Be $expectedXml + } + + It 'Should throw when no single root key hashtable is passed to the Add-CloudEventXmlData Data parameter' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $htData = @{ + '1' = '2' + '3' = '4' + } + + # Act & Assert + { $cloudEvent | Add-CloudEventXmlData -Data $htData -AttributesKeysInElementAttributes $false} | ` + Should -Throw '*Input Hashtable must have single root key*' + } + } +} \ No newline at end of file diff --git a/test/unit/ConvertFrom-HttpMessage.Tests.ps1 b/test/unit/ConvertFrom-HttpMessage.Tests.ps1 new file mode 100644 index 0000000..cb76ae4 --- /dev/null +++ b/test/unit/ConvertFrom-HttpMessage.Tests.ps1 @@ -0,0 +1,244 @@ +Describe "ConvertFrom-HttpMessage Function Tests" { + BeforeAll { + $expectedSpecVersion = '1.0' + $expectedStructuredContentType = 'application/cloudevents+json' + } + + Context "Converts CloudEvent in Binary Content Mode" { + It 'Converts a CloudEvent with all properties and json format data' { + + # Arrange + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedId = 'test-id-1' + $expectedTime = Get-Date ` + -Year 2021 ` + -Month 1 ` + -Day 18 ` + -Hour 12 ` + -Minute 30 ` + -Second 23 ` + -MilliSecond 134 + $expectedDataContentType = 'application/json' + + $headers = @{ + 'Content-Type' = @($expectedDataContentType, 'charset=utf-8') + 'ce-specversion' = $expectedSpecVersion + 'ce-type' = $expectedType + 'ce-time' = $expectedTime.ToString("u") + 'ce-id' = $expectedId + 'ce-source' = $expectedSource + } + + $body =[Text.Encoding]::UTF8.GetBytes('{ + "l10": { + "l2": { + "l3": "wow" + } + }, + "l11": "mhm" +}') + + # Act + $actual = ConvertFrom-HttpMessage ` + -Headers $headers ` + -Body $body + + # Assert + $actual | Should -Not -Be $null + $actual.Type | Should -Be $expectedType + $actual.Source | Should -Be $expectedSource + $actual.Id | Should -Be $expectedId + $actual.Time.Year | Should -Be $expectedTime.Year + $actual.Time.Month | Should -Be $expectedTime.Month + $actual.Time.Day | Should -Be $expectedTime.Day + $actual.Time.Hours | Should -Be $expectedTime.Hours + $actual.Time.Minutes | Should -Be $expectedTime.Minutes + $actual.Time.Seconds | Should -Be $expectedTime.Seconds + $actual.Time.MilliSeconds | Should -Be $expectedTime.MilliSeconds + $actual.DataContentType | Should -Be $expectedDataContentType + + ## Assert Data + $actualHTData = $actual | Read-CloudEventJsonData -Depth 3 + + $actualHTData | Should -Not -Be $null + $actualHTData.l10.l2.l3 | Should -Be 'wow' + $actualHTData.l11 | Should -Be 'mhm' + + } + + It 'Converts a CloudEvent with required properties and application/xml format data' { + # Arrange + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedDataContentType = 'application/xml' + $expectedData = [Text.Encoding]::UTF8.GetBytes('') + + $headers = @{ + 'Content-Type' = @($expectedDataContentType, 'charset=utf-8') + 'ce-specversion' = $expectedSpecVersion + 'ce-type' = $expectedType + 'ce-source' = $expectedSource + } + + $body = $expectedData + + # Act + $actual = ConvertFrom-HttpMessage ` + -Headers $headers ` + -Body $body + + # Assert + $actual | Should -Not -Be $null + $actual.Type | Should -Be $expectedType + $actual.Source | Should -Be $expectedSource + $actual.DataContentType | Should -Be $expectedDataContentType + $actual.Data | Should -Be $expectedData + + ## Assert Data obtained by Read-CloudEventData + $actualData = $actual | Read-CloudEventData + + $actualData | Should -Be $expectedData + } + } + + Context "Converts CloudEvent in Structured Content Mode" { + It 'Converts a CloudEvent with all properties and json format data' { + # Arrange + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedId = 'test-id-1' + $expectedTime = Get-Date ` + -Year 2021 ` + -Month 1 ` + -Day 18 ` + -Hour 12 ` + -Minute 30 ` + -Second 23 ` + -MilliSecond 134 + $expectedDataContentType = 'application/json' + + $headers = @{ + 'Content-Type' = $expectedStructuredContentType + } + + $eventData = @{ + 'l10' = @{ + 'l2' = @{ + 'l3' = 'wow' + } + } + 'l11' = 'mhm' +} + + $structuredJsonBody = @{ + 'specversion' = $expectedSpecVersion + 'type' = $expectedType + 'time' = $expectedTime.ToString("u") + 'id' = $expectedId + 'source' = $expectedSource + 'datacontenttype' = $expectedDataContentType + } + + $structuredJsonBody['data_base64'] = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(($eventData | ConvertTo-Json -Depth 3))) + + $body = [Text.Encoding]::UTF8.GetBytes(($structuredJsonBody | ConvertTo-Json)) + + # Act + $actual = ConvertFrom-HttpMessage ` + -Headers $headers ` + -Body $body + + # Assert + $actual | Should -Not -Be $null + $actual.Type | Should -Be $expectedType + $actual.Source | Should -Be $expectedSource + $actual.Id | Should -Be $expectedId + $actual.Time.Year | Should -Be $expectedTime.Year + $actual.Time.Month | Should -Be $expectedTime.Month + $actual.Time.Day | Should -Be $expectedTime.Day + $actual.Time.Hours | Should -Be $expectedTime.Hours + $actual.Time.Minutes | Should -Be $expectedTime.Minutes + $actual.Time.Seconds | Should -Be $expectedTime.Seconds + $actual.Time.MilliSeconds | Should -Be $expectedTime.MilliSeconds + $actual.DataContentType | Should -Be $expectedDataContentType + + ## Assert Data + $actualHTData = $actual | Read-CloudEventJsonData -Depth 3 + + $actualHTData | Should -Not -Be $null + $actualHTData -is [hashtable] | Should -Be $true + $actualHTData.l10.l2.l3 | Should -Be 'wow' + $actualHTData.l11 | Should -Be 'mhm' + } + + It 'Converts a CloudEvent with required properties and application/xml format data' { + # Arrange + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedDataContentType = 'application/xml' + $expectedData = [Text.Encoding]::UTF8.GetBytes('') + + $headers = @{ + 'Content-Type' = $expectedStructuredContentType + } + + $structuredJsonBody = @{ + 'specversion' = $expectedSpecVersion + 'type' = $expectedType + 'source' = $expectedSource + 'datacontenttype' = $expectedDataContentType + 'data' = $expectedData + } + + $body = [Text.Encoding]::UTF8.GetBytes(($structuredJsonBody | ConvertTo-Json)) + + # Act + $actual = ConvertFrom-HttpMessage ` + -Headers $headers ` + -Body $body + + # Assert + $actual | Should -Not -Be $null + $actual.Type | Should -Be $expectedType + $actual.Source | Should -Be $expectedSource + $actual.DataContentType | Should -Be $expectedDataContentType + $actual.Data | Should -Be $expectedData + + ## Assert Data obtained by Read-CloudEventData + $actualData = $actual | Read-CloudEventData + + $actualData | Should -Be $expectedData + } + + It 'Throws error when CloudEvent encoding is not non-batching JSON' { + # Arrange + $unsupportedContentFormat = 'application/cloudevents-batch+json' + + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedDataContentType = 'application/xml' + $expectedData = [Text.Encoding]::UTF8.GetBytes('') + + $headers = @{ + 'Content-Type' = $unsupportedContentFormat + } + + $structuredJsonBody = @{ + 'specversion' = $expectedSpecVersion + 'type' = $expectedType + 'source' = $expectedSource + 'datacontenttype' = $expectedDataContentType + 'data' = $expectedData + } + + $body = [Text.Encoding]::UTF8.GetBytes(($structuredJsonBody | ConvertTo-Json)) + + # Act & Assert + {ConvertFrom-HttpMessage ` + -Headers $headers ` + -Body $body } | ` + Should -Throw "*Unsupported CloudEvents encoding*" + } + } +} diff --git a/test/unit/ConvertTo-HttpMessage.Tests.ps1 b/test/unit/ConvertTo-HttpMessage.Tests.ps1 new file mode 100644 index 0000000..c57f42e --- /dev/null +++ b/test/unit/ConvertTo-HttpMessage.Tests.ps1 @@ -0,0 +1,223 @@ +Describe "ConvertTo-HttpMessage Function Tests" { + BeforeAll { + $expectedSpecVersion = '1.0' + $expectedStructuredContentType = 'application/cloudevents+json' + } + + Context "Converts CloudEvent in Binary Content Mode" { + It 'Converts a CloudEvent with all properties and json format data' { + + # Arrange + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedId = 'test-id-1' + $expectedTime = Get-Date -Year 2021 -Month 1 -Day 18 -Hour 12 -Minute 30 -Second 0 + $expectedDataContentType = 'application/json' + + $cloudEvent = New-CloudEvent ` + -Type $expectedType ` + -Source $expectedSource ` + -Id $expectedId ` + -Time $expectedTime + + $expectedData = @{ 'key1' = 'value2'; 'key3' = 'value4' } + $cloudEvent = Add-CloudEventJsonData ` + -CloudEvent $cloudEvent ` + -Data $expectedData + + + # Act + $actual = $cloudEvent | ConvertTo-HttpMessage -ContentMode Binary + + # Assert + $actual | Should -Not -Be $null + $actual.Headers | Should -Not -Be $null + $actual.Body | Should -Not -Be $null + + ## Assert Headers + $actual.Headers['Content-Type'] | Should -Be $expectedDataContentType + $actual.Headers['ce-source'] | Should -Be $expectedSource + $actual.Headers['ce-specversion'] | Should -Be $expectedSpecVersion + $actual.Headers['ce-type'] | Should -Be $expectedType + $actual.Headers['ce-time'] | Should -Be '2021-01-18 12:30:00Z' + $actual.Headers['ce-id'] | Should -Be $expectedId + + ## Assert Body + + ## Expected Body is + ## { + ## "key1": "value2", + ## "key3": "value4" + ## } + + + $actualDecodedBody = [Text.Encoding]::UTF8.GetString($actual.Body) | ConvertFrom-Json -AsHashtable + $actualDecodedBody.Keys.Count | Should -Be 2 + $actualDecodedBody.key1 | Should -Be $expectedData.key1 + $actualDecodedBody.key3 | Should -Be $expectedData.key3 + } + + It 'Converts a CloudEvent with required properties and application/xml format data' { + # Arrange + $expectedType = 'test' + $expectedId = 'test-id-1' + $expectedSource = 'urn:test' + $expectedDataContentType = 'application/xml' + + $cloudEvent = New-CloudEvent ` + -Id $expectedId ` + -Type $expectedType ` + -Source $expectedSource + + $expectedData = '' + $cloudEvent = Add-CloudEventData ` + -CloudEvent $cloudEvent ` + -Data $expectedData ` + -DataContentType $expectedDataContentType + + # Act + $actual = $cloudEvent | ConvertTo-HttpMessage -ContentMode Binary + + # Assert + $actual | Should -Not -Be $null + $actual.Headers | Should -Not -Be $null + $actual.Body | Should -Not -Be $null + + ## Assert Headers + $actual.Headers['Content-Type'] | Should -Be $expectedDataContentType + $actual.Headers['ce-source'] | Should -Be $expectedSource + $actual.Headers['ce-specversion'] | Should -Be $expectedSpecVersion + $actual.Headers['ce-type'] | Should -Be $expectedType + $actual.Headers['ce-id'] | Should -Be $expectedId + + ## Assert Body + + ## Expected Body is + ## + $actualDecodedBody =[Text.Encoding]::UTF8.GetString($actual.Body) + $actualDecodedBody | Should -Be $expectedData + } + } + + Context "Converts CloudEvent in Structured Content Mode" { + It 'Converts a CloudEvent with all properties and json format data' { + # Arrange + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedId = 'test-id-1' + $expectedTime = Get-Date -Year 2021 -Month 1 -Day 18 -Hour 12 -Minute 30 -Second 0 + $expectedDataContentType = 'application/json' + + $cloudEvent = New-CloudEvent ` + -Type $expectedType ` + -Source $expectedSource ` + -Id $expectedId ` + -Time $expectedTime + + $expectedData = @{ 'key1' = 'value2'; 'key3' = 'value4' } + $cloudEvent = Add-CloudEventJsonData ` + -CloudEvent $cloudEvent ` + -Data $expectedData + + + # Act + $actual = $cloudEvent | ConvertTo-HttpMessage -ContentMode Structured + + # Assert + $actual | Should -Not -Be $null + $actual.Headers | Should -Not -Be $null + $actual.Body | Should -Not -Be $null + + ## Assert Headers + $headerContentTypes = $actual.Headers['Content-Type'].ToString().Split(';') + $headerContentTypes[0].Trim() | Should -Be $expectedStructuredContentType + $headerContentTypes[1].Trim() | Should -Be 'charset=utf-8' + + $actual.Headers['ce-source'] | Should -Be $expectedSource + $actual.Headers['ce-specversion'] | Should -Be $expectedSpecVersion + $actual.Headers['ce-type'] | Should -Be $expectedType + $actual.Headers['ce-time'] | Should -Be '2021-01-18 12:30:00Z' + $actual.Headers['ce-id'] | Should -Be $expectedId + + ## Assert Body + + ## Expected Body is + ## { + ## "specversion": "1.0", + ## "type": "test", + ## "source": "urn:test", + ## "id": "test-id-1", + ## "time": "2021-01-18T12:30:00.9785466+02:00", + ## "datacontenttype": "application/json", + ## "data": "{ + ## "key1": "value2", + ## "key3": "value4" + ## }" + ## } + $actualDecodedBody = [Text.Encoding]::UTF8.GetString($actual.Body) | ConvertFrom-Json -Depth 1 + + $actualDecodedBody.specversion | Should -Be $expectedSpecVersion + $actualDecodedBody.type | Should -Be $expectedType + $actualDecodedBody.source | Should -Be $expectedSource + Get-Date $actualDecodedBody.time | Should -Be $expectedTime + $actualDecodedBody.datacontenttype | Should -Be $expectedDataContentType + $actualDecodedData = $actualDecodedBody.data | ConvertFrom-Json -AsHashtable + $actualDecodedData.Keys.Count | Should -Be 2 + $actualDecodedData.key1 | Should -Be $expectedData.key1 + $actualDecodedData.key3 | Should -Be $expectedData.key3 + } + + It 'Converts a CloudEvent with required properties and application/xml format data' { + # Arrange + $expectedId = ([Guid]::NewGuid().ToString()) + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedDataContentType = 'application/xml' + + $cloudEvent = New-CloudEvent ` + -Id $expectedId ` + -Type $expectedType ` + -Source $expectedSource + + $expectedData = '' + $cloudEvent = Add-CloudEventData ` + -CloudEvent $cloudEvent ` + -Data $expectedData ` + -DataContentType $expectedDataContentType + + # Act + $actual = $cloudEvent | ConvertTo-HttpMessage -ContentMode Structured + + # Assert + $actual | Should -Not -Be $null + $actual.Headers | Should -Not -Be $null + $actual.Body | Should -Not -Be $null + + ## Assert Headers + $headerContentTypes = $actual.Headers['Content-Type'].ToString().Split(';') + $headerContentTypes[0].Trim() | Should -Be $expectedStructuredContentType + $headerContentTypes[1].Trim() | Should -Be 'charset=utf-8' + $actual.Headers['ce-source'] | Should -Be $expectedSource + $actual.Headers['ce-specversion'] | Should -Be $expectedSpecVersion + $actual.Headers['ce-type'] | Should -Be $expectedType + $actual.Headers['ce-id'] | Should -Be $expectedId + + ## Assert Body + + ## Expected Body is + ## { + ## "specversion": "1.0", + ## "type": "test", + ## "source": "urn:test", + ## "datacontenttype": "application/xml", + ## "data": "" + ## } + $actualDecodedBody = [Text.Encoding]::UTF8.GetString($actual.Body) | ConvertFrom-Json -Depth 1 + $actualDecodedBody.specversion | Should -Be $expectedSpecVersion + $actualDecodedBody.type | Should -Be $expectedType + $actualDecodedBody.source | Should -Be $expectedSource + $actualDecodedBody.datacontenttype | Should -Be $expectedDataContentType + $actualDecodedBody.data | Should -Be $expectedData + } + } +} diff --git a/test/unit/New-CloudEvent.Tests.ps1 b/test/unit/New-CloudEvent.Tests.ps1 new file mode 100644 index 0000000..4bc27d1 --- /dev/null +++ b/test/unit/New-CloudEvent.Tests.ps1 @@ -0,0 +1,44 @@ +Describe "New-CloudEvent Function Tests" { + Context "Create CloudEvent Object" { + It 'Create CloudEvent with required parameters only' { + # Arrange + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedId = ([Guid]::NewGuid().ToString()) + + # Act + $actual = New-CloudEvent ` + -Id $expectedId ` + -Type $expectedType ` + -Source $expectedSource + + # Assert + $actual | Should -Not -Be $null + $actual.Type | Should -Be $expectedType + $actual.Source | Should -Be $expectedSource + $actual.Id | Should -Be $expectedId + } + + It 'Create CloudEvent with all possible parameters' { + # Arrange + $expectedType = 'test' + $expectedSource = 'urn:test' + $expectedId = 'test-id-1' + $expectedTime = Get-Date -Year 2021 -Month 1 -Day 18 -Hour 12 -Minute 30 -Second 0 + + # Act + $actual = New-CloudEvent ` + -Type $expectedType ` + -Source $expectedSource ` + -Id $expectedId ` + -Time $expectedTime + + # Assert + $actual | Should -Not -Be $null + $actual.Type | Should -Be $expectedType + $actual.Source | Should -Be $expectedSource + $actual.Id | Should -Be $expectedId + $actual.Time | Should -Be $expectedTime + } + } +} \ No newline at end of file diff --git a/test/unit/Read-CloudEventData.Tests.ps1 b/test/unit/Read-CloudEventData.Tests.ps1 new file mode 100644 index 0000000..2641f70 --- /dev/null +++ b/test/unit/Read-CloudEventData.Tests.ps1 @@ -0,0 +1,24 @@ +Describe "Read-CloudEventData Function Tests" { + Context "Extracts Data from CloudEvent" { + It 'Reads xml text data' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + + $expectedData = '' + $expectedDataContentType = 'text/xml' + + $cloudEvent = $cloudEvent | Add-CloudEventData -Data $expectedData -DataContentType $expectedDataContentType + + # Act + $actual = $cloudEvent | Read-CloudEventData + + # Assert + $actual | Should -Not -Be $null + $actual | Should -Be $expectedData + } + } +} \ No newline at end of file diff --git a/test/unit/Read-CloudEventJsonData.Tests.ps1 b/test/unit/Read-CloudEventJsonData.Tests.ps1 new file mode 100644 index 0000000..9d6d5d2 --- /dev/null +++ b/test/unit/Read-CloudEventJsonData.Tests.ps1 @@ -0,0 +1,67 @@ +Describe "Read-CloudEventJsonData Function Tests" { + Context "Extracts Json Data from CloudEvent" { + It 'Extracts hashtable from CloudEvent with json data' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $expectedHtData = @{'a' = 'b'} + + $cloudEvent = $cloudEvent | Add-CloudEventJsonData -Data $expectedHtData + + # Act + $actual = $cloudEvent | Read-CloudEventJsonData + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.a | Should -Be 'b' + } + + It 'Expects error when CloudEvent data is not json' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $cloudEvent = $cloudEvent | Add-CloudEventData -Data "test" -DataContentType 'application/text' + $pre + + # Act + { $cloudEvent | Read-CloudEventJsonData -ErrorAction Stop } | ` + Should -Throw "*Cloud Event '$($cloudEvent.Id)' has no json data*" + + } + + It 'Extracts hashtable from CloudEvent with json data with depth 4' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $expectedHtData = @{ + 'l1' = @{ + 'l2' = @{ + 'l3' = @{ + 'l4' = 'wow' + } + } + } + } + + $cloudEvent = $cloudEvent | Add-CloudEventJsonData -Data $expectedHtData -Depth 4 + + # Act + $actual = $cloudEvent | Read-CloudEventJsonData -Depth 4 + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.l1.l2.l3.l4 | Should -Be 'wow' + } + } +} \ No newline at end of file diff --git a/test/unit/Read-CloudEventXmlData.Tests.ps1 b/test/unit/Read-CloudEventXmlData.Tests.ps1 new file mode 100644 index 0000000..f74ddde --- /dev/null +++ b/test/unit/Read-CloudEventXmlData.Tests.ps1 @@ -0,0 +1,69 @@ +Describe "Read-CloudEventXmlData Function Tests" { + Context "Extracts Xml Data from CloudEvent" { + It 'Extracts hashtable from CloudEvent with xml data' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $xmlData = "b" + $expectedHtData = @{'a' = 'b'} + + $cloudEvent = $cloudEvent | Add-CloudEventData -Data $xmlData -DataContentType 'application/xml' + + # Act + $actual = $cloudEvent | Read-CloudEventXmlData -ConvertMode 'SkipAttributes' + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.a | Should -Be 'b' + } + + It 'Expects error when CloudEvent data is not xml' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $cloudEvent = $cloudEvent | Add-CloudEventData -Data "test" -DataContentType 'application/text' + $pre + + # Act + { $cloudEvent | Read-CloudEventXmlData -ConvertMode 'SkipAttributes' -ErrorAction Stop } | ` + Should -Throw "*Cloud Event '$($cloudEvent.Id)' has no xml data*" + + } + + It 'Extracts hashtable from CloudEvent with xml data with depth 4' { + # Arrange + $cloudEvent = New-CloudEvent ` + -Id ([Guid]::NewGuid()) ` + -Type test ` + -Source 'urn:test' + + $xmlData = 'wow' + $expectedHtData = @{ + 'l1' = @{ + 'l2' = @{ + 'l3' = @{ + 'l4' = 'wow' + } + } + } + } + + $cloudEvent = $cloudEvent | Add-CloudEventData -Data $xmlData -DataContentType 'application/xml' + + # Act + $actual = $cloudEvent | Read-CloudEventXmlData -ConvertMode 'SkipAttributes' + + # Assert + $actual | Should -Not -Be $null + $actual -is [hashtable] | Should -Be $true + $actual.l1.l2.l3.l4 | Should -Be 'wow' + } + } +} \ No newline at end of file