Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/createrelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,47 @@ jobs:
name: "Deploy to Staging"

steps:
- uses: actions/checkout@v4

- name: Install scheduled tasks
shell: pwsh
env:
SCHEDULED_TASK_USER: ${{ secrets.SCHEDULED_TASK_USER }}
SCHEDULED_TASK_PASSWORD: ${{ secrets.SCHEDULED_TASK_PASSWORD }}
run: |
$taskScriptDir = 'C:\txnproc\transactionprocessing\powershell'
$scheduledTasksRoot = Join-Path $env:GITHUB_WORKSPACE 'ScheduledTasks'

New-Item -ItemType Directory -Path $taskScriptDir -Force | Out-Null

Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.ps1 |
Where-Object { $_.FullName -notmatch '\\Tests\\' } |
Copy-Item -Destination $taskScriptDir -Force

Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.xml |
Where-Object { $_.FullName -notmatch '\\Tests\\' } |
ForEach-Object {
[xml]$taskDefinition = Get-Content -Path $_.FullName -Raw
$taskName = [string]$taskDefinition.Task.RegistrationInfo.URI

if ([string]::IsNullOrWhiteSpace($taskName)) {
$taskName = $_.BaseName
}

$registerParams = @{
Xml = $taskDefinition.OuterXml
TaskName = $taskName.TrimStart('\')
TaskPath = '\'
Force = $true
}

if ($env:SCHEDULED_TASK_USER -and $env:SCHEDULED_TASK_PASSWORD) {
$registerParams.User = $env:SCHEDULED_TASK_USER
$registerParams.Password = $env:SCHEDULED_TASK_PASSWORD
}

Register-ScheduledTask @registerParams | Out-Null
}

- name: Download the artifact (Merchant Pos)
uses: actions/download-artifact@v4.1.8
Expand Down Expand Up @@ -131,6 +172,47 @@ jobs:
name: "Deploy to Production"

steps:
- uses: actions/checkout@v4

- name: Install scheduled tasks
shell: pwsh
env:
SCHEDULED_TASK_USER: ${{ secrets.SCHEDULED_TASK_USER }}
SCHEDULED_TASK_PASSWORD: ${{ secrets.SCHEDULED_TASK_PASSWORD }}
run: |
$taskScriptDir = 'C:\txnproc\transactionprocessing\powershell'
$scheduledTasksRoot = Join-Path $env:GITHUB_WORKSPACE 'ScheduledTasks'

New-Item -ItemType Directory -Path $taskScriptDir -Force | Out-Null

Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.ps1 |
Where-Object { $_.FullName -notmatch '\\Tests\\' } |
Copy-Item -Destination $taskScriptDir -Force

Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.xml |
Where-Object { $_.FullName -notmatch '\\Tests\\' } |
ForEach-Object {
[xml]$taskDefinition = Get-Content -Path $_.FullName -Raw
$taskName = [string]$taskDefinition.Task.RegistrationInfo.URI

if ([string]::IsNullOrWhiteSpace($taskName)) {
$taskName = $_.BaseName
}

$registerParams = @{
Xml = $taskDefinition.OuterXml
TaskName = $taskName.TrimStart('\')
TaskPath = '\'
Force = $true
}

if ($env:SCHEDULED_TASK_USER -and $env:SCHEDULED_TASK_PASSWORD) {
$registerParams.User = $env:SCHEDULED_TASK_USER
$registerParams.Password = $env:SCHEDULED_TASK_PASSWORD
}

Register-ScheduledTask @registerParams | Out-Null
}

- name: Remove existing Windows service (Merchant Pos)
run: |
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/powershell_nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ jobs:
- name: Run Pester Tests
shell: pwsh
run: |
Invoke-Pester -Path ./ScheduledTasks/SettlementProcessing/Scripts/Tests -CI
Invoke-Pester -Path @(
'./ScheduledTasks/Tests'
'./ScheduledTasks/ReplayParkedQueue/Tests'
'./ScheduledTasks/SettlementProcessing/Tests'
) -CI
6 changes: 5 additions & 1 deletion .github/workflows/pullrequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@ jobs:
- name: Run Pester Tests
shell: pwsh
run: |
Invoke-Pester -Path ./ScheduledTasks/SettlementProcessing/Tests -CI
Invoke-Pester -Path @(
'./ScheduledTasks/Tests'
'./ScheduledTasks/ReplayParkedQueue/Tests'
'./ScheduledTasks/SettlementProcessing/Tests'
) -CI
8 changes: 8 additions & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="transactionprocessing" value="https://f.feedz.io/transactionprocessing/nugets/nuget/index.json" />
</packageSources>
</configuration>
File renamed without changes.
File renamed without changes.
20 changes: 11 additions & 9 deletions ScheduledTasks/ReplayParkedQueue/ReplayParkedQueue.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@
[Parameter(Mandatory = $true)]
[string]$BaseUrl,

[Parameter]
[string]$Username,

[Parameter]
[string]$Password,
[Parameter()]
[System.Management.Automation.PSCredential]$Credential,

[string]$LogDirectory = "C:\home\txnproc\trace",
[int]$LogRetentionDays = 7
Expand Down Expand Up @@ -63,10 +60,15 @@ Write-Trace "INFO" "Script started. BaseUrl=$BaseUrl"
# =========================
# Auth header
# =========================
$AuthHeader = @{
Authorization = "Basic " +
$AuthHeader = @{}
if ($null -ne $Credential) {
$networkCredential = $Credential.GetNetworkCredential()
$username = $networkCredential.UserName
$password = $networkCredential.Password

$AuthHeader.Authorization = "Basic " +
[Convert]::ToBase64String(
[Text.Encoding]::ASCII.GetBytes("$Username`:$Password")
[Text.Encoding]::ASCII.GetBytes("$username`:$password")
)
}

Expand Down Expand Up @@ -140,4 +142,4 @@ foreach ($sub in $subscriptions) {
#}
}

Write-Trace "INFO" "Script completed"
Write-Trace "INFO" "Script completed"
139 changes: 139 additions & 0 deletions ScheduledTasks/ReplayParkedQueue/Tests/ReplayParkedQueue.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
Describe 'ReplayParkedQueue.ps1' {
It 'replays parked messages for subscriptions with parked counts and encodes $all streams' {
$scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'ReplayParkedQueue.ps1')).Path
$logDirectory = Join-Path $TestDrive 'replay-logs-1'
$securePassword = [System.Security.SecureString]::new()
foreach ($code in 115, 101, 99, 114, 101, 116) {
$securePassword.AppendChar([char]$code)
}
$credential = [pscredential]::new('alice', $securePassword)
$subscriptions = @(
[pscustomobject]@{
eventStreamId = 'stream-1'
groupName = 'group-1'
},
[pscustomobject]@{
eventStreamId = '$all'
groupName = 'group-2'
}
)
$captured = [pscustomobject]@{
SubscriptionCalls = 0
InfoUris = [System.Collections.Generic.List[string]]::new()
ReplayUris = [System.Collections.Generic.List[string]]::new()
}

Mock Invoke-RestMethod {
$captured.SubscriptionCalls++
$subscriptions
} -ParameterFilter {
$Method -eq 'GET' -and
$Uri -eq 'https://queue.example/subscriptions'
}

Mock Invoke-RestMethod {
$captured.InfoUris.Add($Uri) | Out-Null
@{ parkedMessageCount = 2 }
} -ParameterFilter {
$Method -eq 'GET' -and
$Uri -eq 'https://queue.example/subscriptions/stream-1/group-1/info'
}

Mock Invoke-RestMethod {
$captured.InfoUris.Add($Uri) | Out-Null
@{ parkedMessageCount = 0 }
} -ParameterFilter {
$Method -eq 'GET' -and
$Uri -eq 'https://queue.example/subscriptions/%24all/group-2/info'
}

Mock Invoke-RestMethod {
$captured.ReplayUris.Add($Uri) | Out-Null
} -ParameterFilter {
$Method -eq 'POST' -and
$Uri -eq 'https://queue.example/subscriptions/stream-1/group-1/replayParked?from=0'
}

& $scriptUnderTest 'https://queue.example' -Credential $credential -LogDirectory $logDirectory

$captured.SubscriptionCalls | Should -Be 1
$captured.InfoUris.Count | Should -Be 2
$captured.InfoUris | Should -Be @(
'https://queue.example/subscriptions/stream-1/group-1/info',
'https://queue.example/subscriptions/%24all/group-2/info'
)
$captured.ReplayUris | Should -Be @(
'https://queue.example/subscriptions/stream-1/group-1/replayParked?from=0'
)
}

It 'does not post a replay when no subscription has parked messages' {
$scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'ReplayParkedQueue.ps1')).Path
$logDirectory = Join-Path $TestDrive 'replay-logs-2'
$securePassword = [System.Security.SecureString]::new()
foreach ($code in 115, 101, 99, 114, 101, 116) {
$securePassword.AppendChar([char]$code)
}
$credential = [pscredential]::new('alice', $securePassword)
$subscriptions = @(
[pscustomobject]@{
eventStreamId = 'stream-1'
groupName = 'group-1'
}
)
$captured = [pscustomobject]@{
SubscriptionCalls = 0
InfoCalls = 0
ReplayCalls = 0
}

Mock Invoke-RestMethod {
$captured.SubscriptionCalls++
$subscriptions
} -ParameterFilter {
$Method -eq 'GET' -and
$Uri -eq 'https://queue.example/subscriptions'
}

Mock Invoke-RestMethod {
$captured.InfoCalls++
@{ parkedMessageCount = 0 }
} -ParameterFilter {
$Method -eq 'GET' -and
$Uri -eq 'https://queue.example/subscriptions/stream-1/group-1/info'
}

Mock Invoke-RestMethod {
$captured.ReplayCalls++
} -ParameterFilter {
$Method -eq 'POST' -and
$Uri -like 'https://queue.example/subscriptions/*/*/replayParked?from=0'
}

& $scriptUnderTest 'https://queue.example' -Credential $credential -LogDirectory $logDirectory

$captured.SubscriptionCalls | Should -Be 1
$captured.InfoCalls | Should -Be 1
$captured.ReplayCalls | Should -Be 0
}

It 'does not add an Authorization header when no credential is supplied' {
$scriptUnderTest = (Resolve-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'ReplayParkedQueue.ps1')).Path
$logDirectory = Join-Path $TestDrive 'replay-logs-3'
$captured = [pscustomobject]@{
Headers = $null
}

Mock Invoke-RestMethod {
$captured.Headers = $Headers
@()
} -ParameterFilter {
$Method -eq 'GET' -and
$Uri -eq 'https://queue.example/subscriptions'
}

& $scriptUnderTest 'https://queue.example' -LogDirectory $logDirectory

$captured.Headers.ContainsKey('Authorization') | Should -BeFalse
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
Describe 'Invoke-DailySettlementProcessing.ps1' {
It 'gets merchants and posts a settlement for each merchant returned' {
$scriptUnderTest = (Resolve-Path (Join-Path (Join-Path $PSScriptRoot '..') 'Invoke-DailySettlementProcessing.ps1')).Path
$postedUris = [System.Collections.Generic.List[string]]::new()
$settlementDate = (Get-Date).ToString('yyyy-MM-dd')
$scriptUnderTest = (Resolve-Path (Join-Path (Join-Path $PSScriptRoot '..') 'Invoke-DailySettlementProcessing.ps1')).Path
$captured = [pscustomobject]@{
TokenCalls = 0
MerchantCalls = 0
}

Mock Invoke-RestMethod {
$captured.TokenCalls++
@{
access_token = 'access-token'
}
Expand All @@ -18,6 +23,7 @@ Describe 'Invoke-DailySettlementProcessing.ps1' {
}

Mock Invoke-RestMethod {
$captured.MerchantCalls++
@{
merchants = @(
@{ merchantId = 'merchant-1' },
Expand Down Expand Up @@ -45,17 +51,61 @@ Describe 'Invoke-DailySettlementProcessing.ps1' {
-ClientId 'client-id' `
-ClientSecret 'client-secret'

Assert-MockCalled Invoke-RestMethod -Times 1 -ParameterFilter {
$Method -eq 'Post' -and $Uri -eq 'https://security.example/connect/token'
}

Assert-MockCalled Invoke-RestMethod -Times 1 -ParameterFilter {
$Method -eq 'Get' -and $Uri -eq 'https://api.example/api/estates/estate-123/merchants'
}

$captured.TokenCalls | Should -Be 1
$captured.MerchantCalls | Should -Be 1
$postedUris | Should -Be @(
"https://api.example/api/estates/estate-123/settlements/$settlementDate/merchants/merchant-1",
"https://api.example/api/estates/estate-123/settlements/$settlementDate/merchants/merchant-2"
)
}

It 'does not post settlements when no merchants are returned' {
$scriptUnderTest = (Resolve-Path (Join-Path (Join-Path $PSScriptRoot '..') 'Invoke-DailySettlementProcessing.ps1')).Path
$captured = [pscustomobject]@{
TokenCalls = 0
MerchantCalls = 0
SettlementCalls = 0
}

Mock Invoke-RestMethod {
$captured.TokenCalls++
@{
access_token = 'access-token'
}
} -ParameterFilter {
$Method -eq 'Post' -and
$Uri -eq 'https://security.example/connect/token' -and
$ContentType -eq 'application/x-www-form-urlencoded' -and
$Body.grant_type -eq 'client_credentials' -and
$Body.client_id -eq 'client-id' -and
$Body.client_secret -eq 'client-secret'
}

Mock Invoke-RestMethod {
$captured.MerchantCalls++
@()
} -ParameterFilter {
$Method -eq 'Get' -and
$Uri -eq 'https://api.example/api/estates/estate-123/merchants' -and
$Headers.Authorization -eq 'Bearer access-token'
}

Mock Invoke-RestMethod {
$captured.SettlementCalls++
} -ParameterFilter {
$Method -eq 'Post' -and
$Uri -like 'https://api.example/api/estates/estate-123/settlements/*/merchants/*'
}

& $scriptUnderTest `
-EstateId 'estate-123' `
-BaseUrl 'https://api.example/' `
-SecurityServiceUrl 'https://security.example/' `
-ClientId 'client-id' `
-ClientSecret 'client-secret'

$captured.TokenCalls | Should -Be 1
$captured.MerchantCalls | Should -Be 1
$captured.SettlementCalls | Should -Be 0
}
}
Loading
Loading