Fix dashboard static assets and add client e2e scripts
This commit is contained in:
@@ -74,6 +74,53 @@ The test output includes session id, worker process id, command status,
|
||||
HRESULT/status diagnostics, event sequence and handles, close status, and worker
|
||||
stdout/stderr lines emitted during the run.
|
||||
|
||||
## Client E2E Scripts
|
||||
|
||||
`scripts/discover-testmachine-tags.ps1` queries the ZB Galaxy Repository for the
|
||||
deployed runtime references used by the live client e2e scripts. It reads
|
||||
`TestMachine_001` through `TestMachine_020` and the expected attributes:
|
||||
|
||||
- `ProtectedValue`
|
||||
- `TestChangingInt`
|
||||
- `TestBoolArray`
|
||||
- `TestIntArray`
|
||||
- `TestDateTimeArray`
|
||||
- `TestStringArray`
|
||||
|
||||
The discovery output includes the exact `fullTagReference`, data type, array
|
||||
dimension, and security classification. The array attributes are expected to be
|
||||
dimension 50. `ProtectedValue` has security classification 2 and requires
|
||||
secured write semantics; the current client CLI e2e runner subscribes to it but
|
||||
does not attempt a normal `Write`.
|
||||
|
||||
Run discovery directly when validating the Galaxy Repository inputs:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/discover-testmachine-tags.ps1 -Json
|
||||
```
|
||||
|
||||
`scripts/run-client-e2e-tests.ps1` drives the .NET, Go, Rust, Python, and Java
|
||||
client CLIs through a live gateway session. For each client it opens one
|
||||
session, registers, adds and advises every discovered test tag, reads a bounded
|
||||
event stream, then closes the session in a `finally` path. The script writes a
|
||||
JSON report under `artifacts/e2e/`.
|
||||
|
||||
Build the gateway and worker, start the gateway, and provide a valid API key
|
||||
before running the client e2e script:
|
||||
|
||||
```powershell
|
||||
$env:MXGATEWAY_API_KEY = "<api-key>"
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1
|
||||
```
|
||||
|
||||
Useful runner options:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Clients dotnet,python -MachineStart 1 -MachineEnd 2
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipStream
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Endpoint localhost:5000 -ApiKeyEnv MXGATEWAY_API_KEY
|
||||
```
|
||||
|
||||
## Focused Commands
|
||||
|
||||
Run the cross-language smoke matrix tests after changing the documented client
|
||||
|
||||
@@ -296,7 +296,8 @@ Do not use MudBlazor or other Blazor UI component libraries for v1.
|
||||
|
||||
Dashboard access should require API-key-backed dashboard authentication with
|
||||
`admin` scope when enabled. For local development, anonymous localhost access
|
||||
may exist only behind an explicit configuration option that defaults to false.
|
||||
is enabled by default through `Dashboard:AllowAnonymousLocalhost`; the bypass is
|
||||
limited to loopback requests.
|
||||
|
||||
## Later Revisit Items
|
||||
|
||||
|
||||
@@ -275,8 +275,9 @@ The implementation path is:
|
||||
5. Dashboard pages require that cookie.
|
||||
6. Logout clears the cookie.
|
||||
|
||||
For local development, allow an explicit `Dashboard:AllowAnonymousLocalhost`
|
||||
option. It must default to false.
|
||||
For local development, `Dashboard:AllowAnonymousLocalhost` defaults to `true`.
|
||||
The bypass applies only to loopback requests; remote dashboard requests still
|
||||
use the API-key-backed cookie flow.
|
||||
|
||||
`DashboardAuthenticator` keeps API-key validation outside UI components. It
|
||||
formats the submitted key as a bearer authorization header for
|
||||
@@ -296,7 +297,7 @@ Suggested configuration:
|
||||
"Enabled": true,
|
||||
"PathBase": "/dashboard",
|
||||
"RequireAdminScope": true,
|
||||
"AllowAnonymousLocalhost": false,
|
||||
"AllowAnonymousLocalhost": true,
|
||||
"SnapshotIntervalMilliseconds": 1000,
|
||||
"RecentFaultLimit": 100,
|
||||
"RecentSessionLimit": 200,
|
||||
|
||||
@@ -682,7 +682,7 @@ SameSite, and scoped with the `__Host-MxGatewayDashboard` name. Logout clears
|
||||
that cookie. Login and logout posts use anti-forgery validation, and dashboard
|
||||
API keys are not accepted in query strings. `Dashboard:AllowAnonymousLocalhost`
|
||||
allows only loopback requests to bypass the dashboard cookie requirement and
|
||||
defaults to `false`.
|
||||
defaults to `true`.
|
||||
|
||||
Recommended scopes:
|
||||
|
||||
@@ -861,7 +861,7 @@ Suggested configuration shape:
|
||||
"Enabled": true,
|
||||
"PathBase": "/dashboard",
|
||||
"RequireAdminScope": true,
|
||||
"AllowAnonymousLocalhost": false,
|
||||
"AllowAnonymousLocalhost": true,
|
||||
"SnapshotIntervalMilliseconds": 1000,
|
||||
"RecentFaultLimit": 100,
|
||||
"RecentSessionLimit": 200,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$MachineStart = 1,
|
||||
[int]$MachineEnd = 20,
|
||||
[string[]]$Attributes = @(
|
||||
"ProtectedValue",
|
||||
"TestChangingInt",
|
||||
"TestBoolArray",
|
||||
"TestIntArray",
|
||||
"TestDateTimeArray",
|
||||
"TestStringArray"
|
||||
),
|
||||
[string]$SqlServer = "localhost",
|
||||
[string]$Database = "ZB",
|
||||
[string]$SqlcmdPath = "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\130\Tools\Binn\SQLCMD.EXE",
|
||||
[switch]$Json
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if ($MachineStart -lt 1 -or $MachineEnd -lt $MachineStart) {
|
||||
throw "MachineStart must be at least 1 and MachineEnd must be greater than or equal to MachineStart."
|
||||
}
|
||||
|
||||
$Attributes = @($Attributes | ForEach-Object {
|
||||
$_ -split ","
|
||||
} | ForEach-Object {
|
||||
$_.Trim()
|
||||
} | Where-Object {
|
||||
-not [string]::IsNullOrWhiteSpace($_)
|
||||
})
|
||||
|
||||
if ($Attributes.Count -eq 0) {
|
||||
throw "At least one attribute name is required."
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $SqlcmdPath)) {
|
||||
throw "sqlcmd was not found at '$SqlcmdPath'. Pass -SqlcmdPath or update docs/toolchain-links.md."
|
||||
}
|
||||
|
||||
$attributeList = ($Attributes | ForEach-Object {
|
||||
"'" + $_.Replace("'", "''") + "'"
|
||||
}) -join ","
|
||||
|
||||
$query = @"
|
||||
SET NOCOUNT ON;
|
||||
;WITH deployed_package_chain AS (
|
||||
SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth
|
||||
FROM gobject g
|
||||
INNER JOIN package p ON p.package_id = g.deployed_package_id
|
||||
WHERE g.is_template = 0 AND g.deployed_package_id <> 0
|
||||
UNION ALL
|
||||
SELECT dpc.gobject_id, p.package_id, p.derived_from_package_id, dpc.depth + 1
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN package p ON p.package_id = dpc.derived_from_package_id
|
||||
WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10
|
||||
), ranked_dynamic AS (
|
||||
SELECT
|
||||
dpc.gobject_id,
|
||||
g.tag_name,
|
||||
da.attribute_name,
|
||||
g.tag_name + '.' + da.attribute_name
|
||||
+ CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END AS full_tag_reference,
|
||||
da.mx_data_type,
|
||||
dt.description AS data_type_name,
|
||||
da.is_array,
|
||||
CASE WHEN da.is_array = 1
|
||||
THEN CONVERT(int, CONVERT(varbinary(2),
|
||||
SUBSTRING(da.mx_value, 15, 2) + SUBSTRING(da.mx_value, 13, 2), 2))
|
||||
ELSE NULL
|
||||
END AS array_dimension,
|
||||
da.security_classification,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY dpc.gobject_id, da.attribute_name
|
||||
ORDER BY dpc.depth
|
||||
) AS rn
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN dynamic_attribute da ON da.package_id = dpc.package_id
|
||||
INNER JOIN gobject g ON g.gobject_id = dpc.gobject_id
|
||||
LEFT JOIN data_type dt ON dt.mx_data_type = da.mx_data_type
|
||||
WHERE g.tag_name LIKE 'TestMachine[_][0-9][0-9][0-9]'
|
||||
AND TRY_CONVERT(int, RIGHT(g.tag_name, 3)) BETWEEN $MachineStart AND $MachineEnd
|
||||
AND da.attribute_name IN ($attributeList)
|
||||
)
|
||||
SELECT tag_name, attribute_name, full_tag_reference, mx_data_type, data_type_name,
|
||||
is_array, COALESCE(CONVERT(varchar(12), array_dimension), '') AS array_dimension,
|
||||
security_classification
|
||||
FROM ranked_dynamic
|
||||
WHERE rn = 1
|
||||
ORDER BY tag_name, attribute_name;
|
||||
"@
|
||||
|
||||
$output = & $SqlcmdPath -S $SqlServer -d $Database -E -W -h -1 -s "|" -Q $query
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Galaxy Repository tag discovery failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
$rows = foreach ($line in $output) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $line -split "\|", 8
|
||||
if ($parts.Count -ne 8) {
|
||||
continue
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
tagName = $parts[0]
|
||||
attributeName = $parts[1]
|
||||
fullTagReference = $parts[2]
|
||||
mxDataType = [int]$parts[3]
|
||||
dataTypeName = $parts[4]
|
||||
isArray = $parts[5] -eq "1"
|
||||
arrayDimension = if ([string]::IsNullOrWhiteSpace($parts[6])) { $null } else { [int]$parts[6] }
|
||||
securityClassification = [int]$parts[7]
|
||||
}
|
||||
}
|
||||
|
||||
if ($Json) {
|
||||
$rows | ConvertTo-Json -Depth 4
|
||||
return
|
||||
}
|
||||
|
||||
$rows | Format-Table -AutoSize
|
||||
@@ -0,0 +1,530 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$Clients = @("dotnet", "go", "rust", "python", "java"),
|
||||
[int]$MachineStart = 1,
|
||||
[int]$MachineEnd = 20,
|
||||
[string[]]$Attributes = @(
|
||||
"ProtectedValue",
|
||||
"TestChangingInt",
|
||||
"TestBoolArray",
|
||||
"TestIntArray",
|
||||
"TestDateTimeArray",
|
||||
"TestStringArray"
|
||||
),
|
||||
[string]$Endpoint = "localhost:5000",
|
||||
[string]$ApiKeyEnv = "MXGATEWAY_API_KEY",
|
||||
[string]$SqlServer = "localhost",
|
||||
[string]$Database = "ZB",
|
||||
[int]$EventLimit = 5,
|
||||
[switch]$SkipStream,
|
||||
[switch]$DryRun,
|
||||
[string]$ReportPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$discoveryScript = Join-Path $PSScriptRoot "discover-testmachine-tags.ps1"
|
||||
$validClients = @("dotnet", "go", "rust", "python", "java")
|
||||
$Clients = @($Clients | ForEach-Object {
|
||||
$_ -split ","
|
||||
} | ForEach-Object {
|
||||
$_.Trim().ToLowerInvariant()
|
||||
} | Where-Object {
|
||||
-not [string]::IsNullOrWhiteSpace($_)
|
||||
})
|
||||
$Attributes = @($Attributes | ForEach-Object {
|
||||
$_ -split ","
|
||||
} | ForEach-Object {
|
||||
$_.Trim()
|
||||
} | Where-Object {
|
||||
-not [string]::IsNullOrWhiteSpace($_)
|
||||
})
|
||||
|
||||
if ($Clients.Count -eq 0) {
|
||||
throw "At least one client is required."
|
||||
}
|
||||
|
||||
if ($Attributes.Count -eq 0) {
|
||||
throw "At least one attribute is required."
|
||||
}
|
||||
|
||||
foreach ($client in $Clients) {
|
||||
if ($validClients -notcontains $client) {
|
||||
throw "Unsupported client '$client'. Supported clients: $($validClients -join ', ')."
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ReportPath)) {
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$ReportPath = Join-Path $repoRoot "artifacts/e2e/testmachine-client-e2e-$timestamp.json"
|
||||
}
|
||||
|
||||
function ConvertTo-HttpEndpoint {
|
||||
param([string]$Value)
|
||||
|
||||
if ($Value.StartsWith("http://", [StringComparison]::OrdinalIgnoreCase) -or
|
||||
$Value.StartsWith("https://", [StringComparison]::OrdinalIgnoreCase)) {
|
||||
return $Value
|
||||
}
|
||||
|
||||
return "http://$Value"
|
||||
}
|
||||
|
||||
function ConvertTo-HostEndpoint {
|
||||
param([string]$Value)
|
||||
|
||||
$hostValue = $Value
|
||||
if ($hostValue.StartsWith("http://", [StringComparison]::OrdinalIgnoreCase)) {
|
||||
$hostValue = $hostValue.Substring(7)
|
||||
} elseif ($hostValue.StartsWith("https://", [StringComparison]::OrdinalIgnoreCase)) {
|
||||
$hostValue = $hostValue.Substring(8)
|
||||
}
|
||||
|
||||
return $hostValue.TrimEnd("/")
|
||||
}
|
||||
|
||||
function Join-CommandLine {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string[]]$Arguments
|
||||
)
|
||||
|
||||
$parts = @($FilePath) + $Arguments
|
||||
return ($parts | ForEach-Object { ConvertTo-NativeArgument -Value $_ }) -join " "
|
||||
}
|
||||
|
||||
function ConvertTo-NativeArgument {
|
||||
param([string]$Value)
|
||||
|
||||
if ($Value -notmatch '[\s"]') {
|
||||
return $Value
|
||||
}
|
||||
|
||||
return '"' + $Value.Replace('\', '\\').Replace('"', '\"') + '"'
|
||||
}
|
||||
|
||||
function Invoke-NativeCommand {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string[]]$Arguments,
|
||||
[string]$WorkingDirectory,
|
||||
[hashtable]$Environment = @{}
|
||||
)
|
||||
|
||||
$process = [System.Diagnostics.Process]::new()
|
||||
$process.StartInfo.FileName = $FilePath
|
||||
$process.StartInfo.Arguments = ($Arguments | ForEach-Object {
|
||||
ConvertTo-NativeArgument -Value $_
|
||||
}) -join " "
|
||||
$process.StartInfo.WorkingDirectory = $WorkingDirectory
|
||||
$process.StartInfo.RedirectStandardOutput = $true
|
||||
$process.StartInfo.RedirectStandardError = $true
|
||||
$process.StartInfo.UseShellExecute = $false
|
||||
foreach ($entry in $Environment.GetEnumerator()) {
|
||||
$process.StartInfo.Environment[$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
|
||||
$commandLine = Join-CommandLine -FilePath $FilePath -Arguments $Arguments
|
||||
if ($DryRun) {
|
||||
Write-Host "[dry-run] $commandLine"
|
||||
return [pscustomobject]@{
|
||||
commandLine = $commandLine
|
||||
exitCode = 0
|
||||
stdout = "{}"
|
||||
stderr = ""
|
||||
}
|
||||
}
|
||||
|
||||
[void]$process.Start()
|
||||
$stdout = $process.StandardOutput.ReadToEnd()
|
||||
$stderr = $process.StandardError.ReadToEnd()
|
||||
$process.WaitForExit()
|
||||
|
||||
$result = [pscustomobject]@{
|
||||
commandLine = $commandLine
|
||||
exitCode = $process.ExitCode
|
||||
stdout = $stdout
|
||||
stderr = $stderr
|
||||
}
|
||||
|
||||
if ($result.exitCode -ne 0) {
|
||||
throw "Command failed with exit code $($result.exitCode): $commandLine`n$stderr`n$stdout"
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Read-JsonObject {
|
||||
param([string]$Text)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Text)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
return $Text | ConvertFrom-Json
|
||||
} catch {
|
||||
$jsonLines = @()
|
||||
foreach ($line in ($Text -split "`r?`n")) {
|
||||
$trimmed = $line.Trim()
|
||||
if ($trimmed.StartsWith("{") -and $trimmed.EndsWith("}")) {
|
||||
$jsonLines += ($trimmed | ConvertFrom-Json)
|
||||
}
|
||||
}
|
||||
return $jsonLines
|
||||
}
|
||||
}
|
||||
|
||||
function Get-OpenSessionId {
|
||||
param(
|
||||
[string]$Client,
|
||||
[object]$Json
|
||||
)
|
||||
|
||||
switch ($Client) {
|
||||
"dotnet" { return $Json.sessionId }
|
||||
"go" { return $Json.reply.sessionId }
|
||||
"rust" { return $Json.sessionId }
|
||||
"python" { return $Json.sessionId }
|
||||
"java" { return $Json.reply.sessionId }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ServerHandle {
|
||||
param(
|
||||
[string]$Client,
|
||||
[object]$Json
|
||||
)
|
||||
|
||||
switch ($Client) {
|
||||
"dotnet" { return [int]$Json.register.serverHandle }
|
||||
"go" { return [int]$Json.reply.register.serverHandle }
|
||||
"rust" { return [int]$Json.serverHandle }
|
||||
"python" { return [int]$Json.serverHandle }
|
||||
"java" { return [int]$Json.reply.register.serverHandle }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ItemHandle {
|
||||
param(
|
||||
[string]$Client,
|
||||
[object]$Json
|
||||
)
|
||||
|
||||
switch ($Client) {
|
||||
"dotnet" { return [int]$Json.addItem.itemHandle }
|
||||
"go" { return [int]$Json.reply.addItem.itemHandle }
|
||||
"rust" { return [int]$Json.itemHandle }
|
||||
"python" { return [int]$Json.itemHandle }
|
||||
"java" { return [int]$Json.reply.addItem.itemHandle }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-StreamEventCount {
|
||||
param(
|
||||
[string]$Client,
|
||||
[object]$Json
|
||||
)
|
||||
|
||||
switch ($Client) {
|
||||
"dotnet" { return @($Json.events).Count }
|
||||
"go" { return @($Json).Count }
|
||||
"rust" { return [int]$Json.eventCount }
|
||||
"python" { return @($Json.events).Count }
|
||||
"java" { return @($Json).Count }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ClientCommand {
|
||||
param(
|
||||
[string]$Client,
|
||||
[string]$Operation,
|
||||
[hashtable]$Values
|
||||
)
|
||||
|
||||
$httpEndpoint = ConvertTo-HttpEndpoint -Value $Endpoint
|
||||
$hostEndpoint = ConvertTo-HostEndpoint -Value $Endpoint
|
||||
$clientName = "mxgw-$Client-e2e"
|
||||
|
||||
switch ($Client) {
|
||||
"dotnet" {
|
||||
$arguments = @(
|
||||
"run", "--project", "clients/dotnet/MxGateway.Client.Cli", "--",
|
||||
$Operation,
|
||||
"--endpoint", $httpEndpoint,
|
||||
"--api-key-env", $ApiKeyEnv,
|
||||
"--timeout", "60s",
|
||||
"--json"
|
||||
)
|
||||
if ($Operation -eq "open-session") {
|
||||
$arguments += @("--client-name", $clientName)
|
||||
} elseif ($Operation -eq "register") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--client-name", $clientName)
|
||||
} elseif ($Operation -eq "add-item") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item", $Values.item)
|
||||
} elseif ($Operation -eq "advise") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)")
|
||||
} elseif ($Operation -eq "stream-events") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit")
|
||||
} elseif ($Operation -eq "close-session") {
|
||||
$arguments += @("--session-id", $Values.sessionId)
|
||||
}
|
||||
return [pscustomobject]@{ file = "dotnet"; args = $arguments; cwd = $repoRoot; env = @{} }
|
||||
}
|
||||
"go" {
|
||||
$arguments = @(
|
||||
"run", "./cmd/mxgw-go", $Operation,
|
||||
"-endpoint", $hostEndpoint,
|
||||
"-api-key-env", $ApiKeyEnv,
|
||||
"-plaintext",
|
||||
"-json"
|
||||
)
|
||||
if ($Operation -eq "open-session") {
|
||||
$arguments += @("-client-session-name", $clientName)
|
||||
} elseif ($Operation -eq "register") {
|
||||
$arguments += @("-session-id", $Values.sessionId, "-client-name", $clientName)
|
||||
} elseif ($Operation -eq "add-item") {
|
||||
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-item", $Values.item)
|
||||
} elseif ($Operation -eq "advise") {
|
||||
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-item-handle", "$($Values.itemHandle)")
|
||||
} elseif ($Operation -eq "stream-events") {
|
||||
$arguments += @("-session-id", $Values.sessionId, "-limit", "$EventLimit")
|
||||
} elseif ($Operation -eq "close-session") {
|
||||
$arguments += @("-session-id", $Values.sessionId)
|
||||
}
|
||||
return [pscustomobject]@{ file = "go"; args = $arguments; cwd = (Join-Path $repoRoot "clients/go"); env = @{} }
|
||||
}
|
||||
"rust" {
|
||||
$arguments = @(
|
||||
"run", "-p", "mxgw-cli", "--", $Operation,
|
||||
"--endpoint", $httpEndpoint,
|
||||
"--api-key-env", $ApiKeyEnv,
|
||||
"--json"
|
||||
)
|
||||
if ($Operation -eq "open-session") {
|
||||
$arguments += @("--client-name", $clientName)
|
||||
} elseif ($Operation -eq "register") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--client-name", $clientName)
|
||||
} elseif ($Operation -eq "add-item") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item", $Values.item)
|
||||
} elseif ($Operation -eq "advise") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)")
|
||||
} elseif ($Operation -eq "stream-events") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit")
|
||||
} elseif ($Operation -eq "close-session") {
|
||||
$arguments += @("--session-id", $Values.sessionId)
|
||||
}
|
||||
return [pscustomobject]@{ file = "cargo"; args = $arguments; cwd = (Join-Path $repoRoot "clients/rust"); env = @{} }
|
||||
}
|
||||
"python" {
|
||||
$arguments = @(
|
||||
"-m", "mxgateway_cli", $Operation,
|
||||
"--endpoint", $hostEndpoint,
|
||||
"--api-key-env", $ApiKeyEnv,
|
||||
"--plaintext",
|
||||
"--json"
|
||||
)
|
||||
if ($Operation -eq "open-session") {
|
||||
$arguments += @("--client-name", $clientName)
|
||||
} elseif ($Operation -eq "register") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--client-name", $clientName)
|
||||
} elseif ($Operation -eq "add-item") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item", $Values.item)
|
||||
} elseif ($Operation -eq "advise") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)")
|
||||
} elseif ($Operation -eq "stream-events") {
|
||||
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit", "--timeout", "15")
|
||||
} elseif ($Operation -eq "close-session") {
|
||||
$arguments += @("--session-id", $Values.sessionId)
|
||||
}
|
||||
$env = @{
|
||||
PYTHONPATH = Join-Path $repoRoot "clients/python/src"
|
||||
}
|
||||
return [pscustomobject]@{ file = "py"; args = @("-3.12") + $arguments; cwd = (Join-Path $repoRoot "clients/python"); env = $env }
|
||||
}
|
||||
"java" {
|
||||
$cliArgs = @(
|
||||
$Operation,
|
||||
"--endpoint", $hostEndpoint,
|
||||
"--api-key-env", $ApiKeyEnv,
|
||||
"--plaintext",
|
||||
"--json"
|
||||
)
|
||||
if ($Operation -eq "open-session") {
|
||||
$cliArgs += @("--client-session-name", $clientName)
|
||||
} elseif ($Operation -eq "register") {
|
||||
$cliArgs += @("--session-id", $Values.sessionId, "--client-name", $clientName)
|
||||
} elseif ($Operation -eq "add-item") {
|
||||
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item", $Values.item)
|
||||
} elseif ($Operation -eq "advise") {
|
||||
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)")
|
||||
} elseif ($Operation -eq "stream-events") {
|
||||
$cliArgs += @("--session-id", $Values.sessionId, "--limit", "$EventLimit")
|
||||
} elseif ($Operation -eq "close-session") {
|
||||
$cliArgs += @("--session-id", $Values.sessionId)
|
||||
}
|
||||
$arguments = @("--quiet", ":mxgateway-cli:run", "--args=$($cliArgs -join ' ')")
|
||||
return [pscustomobject]@{ file = "gradle"; args = $arguments; cwd = (Join-Path $repoRoot "clients/java"); env = @{} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ClientOperation {
|
||||
param(
|
||||
[string]$Client,
|
||||
[string]$Operation,
|
||||
[hashtable]$Values = @{}
|
||||
)
|
||||
|
||||
$command = Get-ClientCommand -Client $Client -Operation $Operation -Values $Values
|
||||
$result = Invoke-NativeCommand `
|
||||
-FilePath $command.file `
|
||||
-Arguments $command.args `
|
||||
-WorkingDirectory $command.cwd `
|
||||
-Environment $command.env
|
||||
if ($DryRun) {
|
||||
switch ($Operation) {
|
||||
"open-session" { return [pscustomobject]@{ sessionId = "dry-run-session-$Client"; reply = [pscustomobject]@{ sessionId = "dry-run-session-$Client" } } }
|
||||
"register" { return [pscustomobject]@{ serverHandle = 1; register = [pscustomobject]@{ serverHandle = 1 }; reply = [pscustomobject]@{ register = [pscustomobject]@{ serverHandle = 1 } } } }
|
||||
"add-item" { return [pscustomobject]@{ itemHandle = 1; addItem = [pscustomobject]@{ itemHandle = 1 }; reply = [pscustomobject]@{ addItem = [pscustomobject]@{ itemHandle = 1 } } } }
|
||||
"stream-events" { return [pscustomobject]@{ eventCount = 1; events = @([pscustomobject]@{ workerSequence = 1 }) } }
|
||||
default { return [pscustomobject]@{ ok = $true; reply = [pscustomobject]@{} } }
|
||||
}
|
||||
}
|
||||
return Read-JsonObject -Text $result.stdout
|
||||
}
|
||||
|
||||
$discoveryJson = & $discoveryScript `
|
||||
-MachineStart $MachineStart `
|
||||
-MachineEnd $MachineEnd `
|
||||
-Attributes $Attributes `
|
||||
-SqlServer $SqlServer `
|
||||
-Database $Database `
|
||||
-Json
|
||||
$convertedTags = $discoveryJson | ConvertFrom-Json
|
||||
$tags = @($convertedTags)
|
||||
if ($tags.Count -eq 1 -and $tags[0] -is [System.Array]) {
|
||||
$tags = @($tags[0])
|
||||
}
|
||||
$expectedTagCount = (($MachineEnd - $MachineStart + 1) * $Attributes.Count)
|
||||
|
||||
if ($tags.Count -ne $expectedTagCount) {
|
||||
$found = $tags | Group-Object -Property tagName | ForEach-Object {
|
||||
"$($_.Name)=$($_.Count)"
|
||||
}
|
||||
throw "Expected $expectedTagCount discovered test tags, found $($tags.Count): $($found -join ', ')"
|
||||
}
|
||||
|
||||
$run = [ordered]@{
|
||||
schemaVersion = 1
|
||||
endpoint = $Endpoint
|
||||
apiKeyEnv = $ApiKeyEnv
|
||||
machineStart = $MachineStart
|
||||
machineEnd = $MachineEnd
|
||||
attributes = $Attributes
|
||||
eventLimit = $EventLimit
|
||||
skipStream = [bool]$SkipStream
|
||||
startedAt = (Get-Date).ToUniversalTime().ToString("O")
|
||||
discoveredTags = $tags
|
||||
clients = @()
|
||||
}
|
||||
|
||||
$hadFailure = $false
|
||||
|
||||
foreach ($client in $Clients) {
|
||||
Write-Host "Running $client client e2e flow against $($tags.Count) discovered tags."
|
||||
$sessionId = $null
|
||||
$serverHandle = $null
|
||||
$clientResult = [ordered]@{
|
||||
language = $client
|
||||
sessionId = $null
|
||||
serverHandle = $null
|
||||
addedItems = @()
|
||||
eventCount = 0
|
||||
closed = $false
|
||||
error = $null
|
||||
}
|
||||
|
||||
try {
|
||||
$openJson = Invoke-ClientOperation -Client $client -Operation "open-session"
|
||||
$sessionId = Get-OpenSessionId -Client $client -Json $openJson
|
||||
if ([string]::IsNullOrWhiteSpace($sessionId)) {
|
||||
throw "The $client open-session command did not return a session id."
|
||||
}
|
||||
$clientResult.sessionId = $sessionId
|
||||
|
||||
$registerJson = Invoke-ClientOperation -Client $client -Operation "register" -Values @{
|
||||
sessionId = $sessionId
|
||||
}
|
||||
$serverHandle = Get-ServerHandle -Client $client -Json $registerJson
|
||||
$clientResult.serverHandle = $serverHandle
|
||||
|
||||
foreach ($tag in $tags) {
|
||||
$addJson = Invoke-ClientOperation -Client $client -Operation "add-item" -Values @{
|
||||
sessionId = $sessionId
|
||||
serverHandle = $serverHandle
|
||||
item = $tag.fullTagReference
|
||||
}
|
||||
$itemHandle = Get-ItemHandle -Client $client -Json $addJson
|
||||
Invoke-ClientOperation -Client $client -Operation "advise" -Values @{
|
||||
sessionId = $sessionId
|
||||
serverHandle = $serverHandle
|
||||
itemHandle = $itemHandle
|
||||
} | Out-Null
|
||||
|
||||
$clientResult.addedItems += [ordered]@{
|
||||
tagName = $tag.tagName
|
||||
attributeName = $tag.attributeName
|
||||
fullTagReference = $tag.fullTagReference
|
||||
itemHandle = $itemHandle
|
||||
protectedWriteRequired = $tag.attributeName -eq "ProtectedValue"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipStream) {
|
||||
$streamJson = Invoke-ClientOperation -Client $client -Operation "stream-events" -Values @{
|
||||
sessionId = $sessionId
|
||||
}
|
||||
$clientResult.eventCount = Get-StreamEventCount -Client $client -Json $streamJson
|
||||
if ($clientResult.eventCount -lt 1) {
|
||||
throw "The $client stream-events command returned no events."
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
$hadFailure = $true
|
||||
$clientResult.error = $_.Exception.Message
|
||||
Write-Warning "$client e2e flow failed: $($clientResult.error)"
|
||||
} finally {
|
||||
if (-not [string]::IsNullOrWhiteSpace($sessionId)) {
|
||||
try {
|
||||
Invoke-ClientOperation -Client $client -Operation "close-session" -Values @{
|
||||
sessionId = $sessionId
|
||||
} | Out-Null
|
||||
$clientResult.closed = $true
|
||||
} catch {
|
||||
$hadFailure = $true
|
||||
$clientResult.error = "$($clientResult.error) close-session failed: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$run.clients += $clientResult
|
||||
}
|
||||
|
||||
$run.completedAt = (Get-Date).ToUniversalTime().ToString("O")
|
||||
$run.success = -not $hadFailure
|
||||
|
||||
if (-not $DryRun) {
|
||||
$reportDirectory = Split-Path -Parent $ReportPath
|
||||
if (-not [string]::IsNullOrWhiteSpace($reportDirectory)) {
|
||||
New-Item -ItemType Directory -Path $reportDirectory -Force | Out-Null
|
||||
}
|
||||
$run | ConvertTo-Json -Depth 8 | Set-Content -Path $ReportPath -Encoding UTF8
|
||||
Write-Host "Wrote e2e report to $ReportPath"
|
||||
}
|
||||
|
||||
if ($hadFailure) {
|
||||
exit 1
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public sealed class DashboardOptions
|
||||
|
||||
public bool RequireAdminScope { get; init; } = true;
|
||||
|
||||
public bool AllowAnonymousLocalhost { get; init; }
|
||||
public bool AllowAnonymousLocalhost { get; init; } = true;
|
||||
|
||||
public int SnapshotIntervalMilliseconds { get; init; } = 1_000;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<body class="dashboard-body">
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
<script src="/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="/_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/"
|
||||
@page "/dashboard/"
|
||||
@inherits DashboardPageBase
|
||||
|
||||
<PageTitle>MXAccess Gateway Dashboard</PageTitle>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/events"
|
||||
@page "/dashboard/events"
|
||||
@inherits DashboardPageBase
|
||||
|
||||
<PageTitle>Dashboard Events</PageTitle>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/sessions/{SessionId}"
|
||||
@page "/dashboard/sessions/{SessionId}"
|
||||
@inherits DashboardPageBase
|
||||
|
||||
<PageTitle>Dashboard Session</PageTitle>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/sessions"
|
||||
@page "/dashboard/sessions"
|
||||
@inherits DashboardPageBase
|
||||
|
||||
<PageTitle>Dashboard Sessions</PageTitle>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/settings"
|
||||
@page "/dashboard/settings"
|
||||
@inherits DashboardPageBase
|
||||
|
||||
<PageTitle>Dashboard Settings</PageTitle>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/workers"
|
||||
@page "/dashboard/workers"
|
||||
@inherits DashboardPageBase
|
||||
|
||||
<PageTitle>Dashboard Workers</PageTitle>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
||||
using MxGateway.Contracts;
|
||||
using MxGateway.Server.Configuration;
|
||||
using MxGateway.Server.Dashboard;
|
||||
@@ -13,6 +14,8 @@ namespace MxGateway.Server;
|
||||
|
||||
public static class GatewayApplication
|
||||
{
|
||||
private const string StaticAssetsManifestFileName = "MxGateway.Server.staticwebassets.endpoints.json";
|
||||
|
||||
public static WebApplication Build(string[] args)
|
||||
{
|
||||
WebApplicationBuilder builder = CreateBuilder(args);
|
||||
@@ -30,7 +33,12 @@ public static class GatewayApplication
|
||||
|
||||
public static WebApplicationBuilder CreateBuilder(string[] args)
|
||||
{
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions
|
||||
{
|
||||
Args = args,
|
||||
ContentRootPath = ResolveContentRootPath(),
|
||||
});
|
||||
StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.Services.AddGatewayConfiguration();
|
||||
builder.Services.AddSqliteAuthStore();
|
||||
@@ -47,8 +55,65 @@ public static class GatewayApplication
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static string ResolveContentRootPath()
|
||||
{
|
||||
string? configuredContentRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_CONTENTROOT");
|
||||
if (!string.IsNullOrWhiteSpace(configuredContentRootPath)
|
||||
&& IsServerContentRoot(configuredContentRootPath))
|
||||
{
|
||||
return configuredContentRootPath;
|
||||
}
|
||||
|
||||
string currentDirectory = Environment.CurrentDirectory;
|
||||
if (IsServerContentRoot(currentDirectory))
|
||||
{
|
||||
return currentDirectory;
|
||||
}
|
||||
|
||||
string baseDirectory = AppContext.BaseDirectory;
|
||||
if (IsServerContentRoot(baseDirectory))
|
||||
{
|
||||
return baseDirectory;
|
||||
}
|
||||
|
||||
string? discoveredContentRootPath = DiscoverServerContentRoot(currentDirectory)
|
||||
?? DiscoverServerContentRoot(baseDirectory);
|
||||
|
||||
return discoveredContentRootPath ?? baseDirectory;
|
||||
}
|
||||
|
||||
private static string? DiscoverServerContentRoot(string startPath)
|
||||
{
|
||||
DirectoryInfo? directory = new(startPath);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (IsServerContentRoot(directory.FullName))
|
||||
{
|
||||
return directory.FullName;
|
||||
}
|
||||
|
||||
string serverProjectPath = Path.Combine(directory.FullName, "src", "MxGateway.Server");
|
||||
if (IsServerContentRoot(serverProjectPath))
|
||||
{
|
||||
return serverProjectPath;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsServerContentRoot(string path)
|
||||
{
|
||||
return File.Exists(Path.Combine(path, "appsettings.json"))
|
||||
&& Directory.Exists(Path.Combine(path, "wwwroot"));
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapGatewayEndpoints(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
endpoints.MapStaticAssets(ResolveStaticAssetsManifestPath());
|
||||
|
||||
endpoints.MapGet("/", () => Results.Redirect("/health/live"));
|
||||
|
||||
endpoints.MapGet(
|
||||
@@ -64,4 +129,11 @@ public static class GatewayApplication
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static string ResolveStaticAssetsManifestPath()
|
||||
{
|
||||
string manifestPath = Path.Combine(AppContext.BaseDirectory, StaticAssetsManifestFileName);
|
||||
|
||||
return File.Exists(manifestPath) ? manifestPath : StaticAssetsManifestFileName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"Enabled": true,
|
||||
"PathBase": "/dashboard",
|
||||
"RequireAdminScope": true,
|
||||
"AllowAnonymousLocalhost": false,
|
||||
"AllowAnonymousLocalhost": true,
|
||||
"SnapshotIntervalMilliseconds": 1000,
|
||||
"RecentFaultLimit": 100,
|
||||
"RecentSessionLimit": 200,
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class GatewayOptionsTests
|
||||
Assert.True(options.Dashboard.Enabled);
|
||||
Assert.Equal("/dashboard", options.Dashboard.PathBase);
|
||||
Assert.True(options.Dashboard.RequireAdminScope);
|
||||
Assert.False(options.Dashboard.AllowAnonymousLocalhost);
|
||||
Assert.True(options.Dashboard.AllowAnonymousLocalhost);
|
||||
Assert.Equal(1_000, options.Dashboard.SnapshotIntervalMilliseconds);
|
||||
Assert.Equal(100, options.Dashboard.RecentFaultLimit);
|
||||
Assert.Equal(200, options.Dashboard.RecentSessionLimit);
|
||||
|
||||
Reference in New Issue
Block a user