Initial commit

This commit is contained in:
Dimitar Milov 2021-04-07 09:40:42 -07:00 committed by Mark Peek
commit b2afcf89bd
22 changed files with 3322 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
**/.vs
**/bin
**/obj
/CloudEvents
/CloudEvents.Sdk

201
LICENSE Normal file
View File

@ -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.

213
README.md Normal file
View File

@ -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.<br/>
If `true`
```powershell
@{'root' = @{'Attributes' = @{'att1' = 'true'}; 'Value' = 'val-1'}}
```
is formatted as
```xml
<root att1="true">val-1</root>
```
If `false`
```powershell
@{'root' = @{'Attributes' = @{'att1' = 'true'}; 'Value' = 'val-1'}}
```
is formatted as
```xml
<root><Attributes><att1>true</att1></Attributes><Value>val-1</Value></root>
```
#### 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 <headers> -Body <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<br/>
`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<br/>
Example:
```xml
<key att='true'>value1</key>
```
is converted to
```powershell
@{'key' = 'value-1'}
```
`AlwaysAttrValue` - Each element is represented as a hashtable with two keys<br/>
'Attributes' - key-value pair of the cml element attributes if any, otherwise null<br/>
'Value' - string value represinting the xml element inner text<br/>
Example:
```xml
```
<key1 att='true'>value1</key1><key2>value2</key2>
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<br/>
Example:
```xml
<key1 att='true'>value1</key1><key2>value2</key2>
```
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
```

96
build.ps1 Normal file
View File

@ -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

View File

@ -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 = @()
}

View File

@ -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 '<much wow="xml"/>' -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 '<root att1="true">val-1</root>'
Otherwise
@{'root' = @{'Attributes' = @{'att1' = 'true'}; 'Value' = 'val-1'}} would be '<root><Attributes><att1>true</att1></Attributes><Value>val-1</Value></root>'
.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:
"<key att='true'>value1</key>" 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:
"<key1 att='true'>value1</key1><key2>value2</key2>" 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:
"<key1 att='true'>value1</key1><key2>value2</key2>" 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

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>CloudEventsPowerShell</RootNamespace>
<AssemblyName>CloudEventsPowerShell</AssemblyName>
<Description>CloudEvents PowerShell SDK.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CloudNative.CloudEvents" Version="1.3.80" />
</ItemGroup>
<ItemGroup>
<None Update="CloudEvents.Sdk.psd1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="CloudEvents.Sdk.psm1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="dataserialization/xml.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -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 = "<key>value</key>"
$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 = "<keys><key>value1</key><key>value2</key><key>value3</key></keys>"
$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 = "<keys><key>value1</key><key>value2</key><key>value3</key></keys>"
$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 = "<root><key1 att1='true'>value-1</key1><key2>value-2</key2></root>"
$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 = "<root><key>value</key><withattr att1='true' att2='false'>value-1</withattr></root>"
$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 = '<UserLoginSessionEvent><key>8570</key><chainId>8570</chainId><createdTime>2021-02-04T08:51:53.723999Z</createdTime><userName>dcui</userName><datacenter><name>vcqaDC</name><datacenter type="Datacenter">datacenter-2</datacenter></datacenter><computeResource><name>cls</name><computeResource type="ClusterComputeResource">domain-c7</computeResource></computeResource><host><name>10.161.140.163</name><host type="HostSystem">host-21</host></host><fullFormattedMessage>User dcui@127.0.0.1 logged in as VMware-client/6.5.0</fullFormattedMessage><ipAddress>127.0.0.1</ipAddress><userAgent>VMware-client/6.4.0</userAgent><locale>en</locale><sessionId>52b910cf-661f-f72d-9f86-fb82113404b7</sessionId></UserLoginSessionEvent>'
$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 = '<UserLoginSessionEvent><key>8570</key><chainId>8570</chainId><createdTime>2021-02-04T08:51:53.723999Z</createdTime><userName>dcui</userName><datacenter><name>vcqaDC</name><datacenter type="Datacenter">datacenter-2</datacenter></datacenter><computeResource><name>cls</name><computeResource type="ClusterComputeResource">domain-c7</computeResource></computeResource><host><name>10.161.140.163</name><host type="HostSystem">host-21</host></host><fullFormattedMessage>User dcui@127.0.0.1 logged in as VMware-client/6.5.0</fullFormattedMessage><ipAddress>127.0.0.1</ipAddress><userAgent>VMware-client/6.4.0</userAgent><locale>en</locale><sessionId>52b910cf-661f-f72d-9f86-fb82113404b7</sessionId></UserLoginSessionEvent>'
$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 = "<key>value</key>"
# 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 = "<keys><key>value1</key><key>value2</key><key>value3</key></keys>"
# 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 = '<UserLoginSessionEvent><datacenter><datacenter>datacenter-2</datacenter></datacenter></UserLoginSessionEvent>'
# 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 = '<UserLoginSessionEvent><computeResource><computeResource><Attributes><type>ClusterComputeResource</type></Attributes><Value>domain-c7</Value></computeResource><name>cls</name></computeResource></UserLoginSessionEvent>'
# 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('<Attributes><type>ClusterComputeResource</type>') | Should -Be $true
$actual.Contains('<Value>domain-c7</Value>') | Should -Be $true
$actual.Contains('<name>cls</name>') | Should -Be $true
$actual.IndexOf('<Attributes>') | Should -BeGreaterThan $actual.IndexOf('<computeResource><computeResource>')
$actual.IndexOf('<Value>') | Should -BeLessThan $actual.IndexOf('</computeResource>')
}
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 = '<UserLoginSessionEvent><computeResource><computeResource type="ClusterComputeResource">domain-c7</computeResource><name>cls</name></computeResource></UserLoginSessionEvent>'
# 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('<UserLoginSessionEvent>') | Should -Be $true
$actual.Contains('<computeResource type="ClusterComputeResource">domain-c7</computeResource>') | Should -Be $true
$actual.Contains('<name>cls</name>') | 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'
}
}
}

View File

@ -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
}

42
test/RunTests.ps1 Normal file
View File

@ -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
}

View File

@ -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))
}
}
}

View File

@ -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
}
}
}

View File

@ -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'

View File

@ -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 = '<much wow="xml"/>'
$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*"
}
}
}

View File

@ -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
}
}
}

View File

@ -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 = '<a>b</a>'
$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</4></3></2></1>'
$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*'
}
}
}

View File

@ -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('<much wow="xml"/>')
$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('<much wow="xml"/>')
$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('<much wow="xml"/>')
$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*"
}
}
}

View File

@ -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 = '<much wow="xml"/>'
$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
## <much wow="xml"/>
$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 = '<much wow="xml"/>'
$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": "<much wow=/"xml/"/>"
## }
$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
}
}
}

View File

@ -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
}
}
}

View File

@ -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 = '<much wow="xml"/>'
$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
}
}
}

View File

@ -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'
}
}
}

View File

@ -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 = "<a>b</a>"
$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 = '<l1><l2><l3><l4>wow</l4></l3></l2></l1>'
$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'
}
}
}