Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa0282fbcc
|
No files matched your search
+1
-1
@@ -475,7 +475,7 @@ try
|
||||
$clientResult = Get-Content (Join-Path $reports 'result.json') -Raw | ConvertFrom-Json
|
||||
$serverResult = Get-Content (Join-Path $reports 'server-result.json') -Raw | ConvertFrom-Json
|
||||
$s3Result = Get-Content (Join-Path $reports 's3-results.json') -Raw | ConvertFrom-Json
|
||||
if (!$clientResult.success -or !$serverResult.success -or !$s3Result.passed -or $clientResult.jobs -ne $Workload -or @($s3Result.checks).Count -ne 65)
|
||||
if (!$clientResult.success -or !$serverResult.success -or !$s3Result.passed -or $clientResult.jobs -ne $Workload -or @($s3Result.checks).Count -ne 77)
|
||||
{
|
||||
throw 'Result invariants failed.'
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ Core source archive. Java and .NET run inside Docker; neither host SDK is requir
|
||||
|
||||
## Start, test and stop
|
||||
|
||||
From the repository root, the default command runs 65 S3 checks and 40 network
|
||||
From the repository root, the default command runs 77 S3 checks and 40 network
|
||||
jobs, saves protected reports, and removes its containers and synthetic volumes:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#Requires -Version 7.4
|
||||
# Copyright 2026 Kingsrook, LLC
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retargets the opt-in native-auth Docker fixture to a new immutable source tuple.
|
||||
.DESCRIPTION
|
||||
The pinned core commit, its archive checksum, the Maven development revision, and the pinned
|
||||
client commit and checksum appear in several files. Editing them by hand drifts. This script
|
||||
reads the current values from Invoke-NativeAuth.ps1, replaces every literal occurrence inside
|
||||
dev/ only, and fails unless each old value was found and no old value remains. It needs no
|
||||
network, Docker or Maven, and never fetches or verifies an archive: the caller supplies the
|
||||
SHA-256 from the immutable source. Run NativeAuthPins.Tests.ps1 afterwards.
|
||||
.EXAMPLE
|
||||
pwsh ./dev/Set-NativeAuthPins.ps1 -CoreCommit <40 hex> -CoreArchiveSha256 <64 hex> -CoreVersionBase 4.1.0
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory)][ValidatePattern('^[0-9a-f]{40}$')][string] $CoreCommit,
|
||||
[Parameter(Mandatory)][ValidatePattern('^[0-9a-f]{64}$')][string] $CoreArchiveSha256,
|
||||
[Parameter(Mandatory)][ValidatePattern('^\d+\.\d+\.\d+$')][string] $CoreVersionBase,
|
||||
[ValidatePattern('^[0-9a-f]{40}$')][string] $ClientCommit,
|
||||
[ValidatePattern('^[0-9a-f]{64}$')][string] $ClientArchiveSha256
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if([bool]$ClientCommit -ne [bool]$ClientArchiveSha256)
|
||||
{
|
||||
throw 'Supply the client commit and its archive SHA-256 together, or neither.'
|
||||
}
|
||||
|
||||
$dev = $PSScriptRoot
|
||||
$pinScript = Join-Path $dev 'Invoke-NativeAuth.ps1'
|
||||
$serverDockerfile = Join-Path $dev 'native-auth/Dockerfile.server'
|
||||
$clientDockerfile = Join-Path $dev 'native-auth/Dockerfile.client'
|
||||
|
||||
|
||||
# Returns the single capture of a pattern in a file or throws; the file is the pin's source of truth.
|
||||
function Get-Pin
|
||||
{
|
||||
param([string] $Path, [string] $Pattern)
|
||||
|
||||
$found = [regex]::Matches((Get-Content -LiteralPath $Path -Raw), $Pattern)
|
||||
if($found.Count -lt 1)
|
||||
{
|
||||
throw "Pin not found in $([IO.Path]::GetFileName($Path))."
|
||||
}
|
||||
return $found[0].Groups[1].Value
|
||||
}
|
||||
|
||||
|
||||
$oldCore = Get-Pin $pinScript "\`$coreSource = '([0-9a-f]{40})'"
|
||||
$oldClient = Get-Pin $pinScript "\`$clientSource = '([0-9a-f]{40})'"
|
||||
$oldCoreSum = Get-Pin $serverDockerfile 'ADD --checksum=sha256:([0-9a-f]{64}) https://github.com/QRun-IO/qqq/archive/'
|
||||
$oldClientSum = Get-Pin $clientDockerfile 'ADD --checksum=sha256:([0-9a-f]{64}) https://github.com/QRun-IO/qqq-print-agent/archive/'
|
||||
$oldRevision = Get-Pin $serverDockerfile '-Drevision=(\S+)'
|
||||
$newRevision = "$CoreVersionBase-native-dev-$($CoreCommit.Substring(0, 8))"
|
||||
|
||||
$replacements = [ordered]@{
|
||||
$oldCore = $CoreCommit
|
||||
$oldCoreSum = $CoreArchiveSha256
|
||||
$oldRevision = $newRevision
|
||||
}
|
||||
if($ClientCommit)
|
||||
{
|
||||
$replacements[$oldClient] = $ClientCommit
|
||||
$replacements[$oldClientSum] = $ClientArchiveSha256
|
||||
}
|
||||
|
||||
$files = Get-ChildItem -LiteralPath $dev -Recurse -File -Include '*.ps1', '*.cs', '*.md', '*.yaml', '*.sh', '*.java', 'Dockerfile.*' |
|
||||
Where-Object { $_.FullName -ne $PSCommandPath -and $_.Name -ne 'NativeAuthPins.Tests.ps1' }
|
||||
$seen = @{}
|
||||
foreach($file in $files)
|
||||
{
|
||||
$text = [IO.File]::ReadAllText($file.FullName)
|
||||
$updated = $text
|
||||
foreach($pair in $replacements.GetEnumerator())
|
||||
{
|
||||
if($updated.Contains($pair.Key))
|
||||
{
|
||||
$seen[$pair.Key] = $true
|
||||
$updated = $updated.Replace($pair.Key, $pair.Value)
|
||||
}
|
||||
}
|
||||
if(-not [string]::Equals($text, $updated, [StringComparison]::Ordinal) -and $PSCmdlet.ShouldProcess($file.FullName, 'Retarget pins'))
|
||||
{
|
||||
[IO.File]::WriteAllText($file.FullName, $updated, [Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
}
|
||||
|
||||
foreach($old in $replacements.Keys)
|
||||
{
|
||||
if(-not $seen.ContainsKey($old))
|
||||
{
|
||||
throw "A current pin was never found in dev/: $($old.Substring(0, 8))..."
|
||||
}
|
||||
}
|
||||
|
||||
# The Maven development revision is also written next to -Dqqq.version in the server build.
|
||||
if(-not $WhatIfPreference)
|
||||
{
|
||||
$serverText = [IO.File]::ReadAllText($serverDockerfile)
|
||||
if(([regex]::Matches($serverText, [regex]::Escape($newRevision))).Count -ne 2)
|
||||
{
|
||||
throw 'Expected the new development revision exactly twice in Dockerfile.server.'
|
||||
}
|
||||
}
|
||||
Write-Output "Retargeted to core $($CoreCommit.Substring(0, 8)); run dev/tests/NativeAuthPins.Tests.ps1 and the fixture."
|
||||
@@ -78,6 +78,11 @@ public final class S3PrintingQualification {
|
||||
s3.putObject(PutObjectRequest.builder().bucket(otherBucket).key("canary.txt").build(), RequestBody.fromString("synthetic separate bucket"));
|
||||
PrintingService a = service(instance, "fixture-tenant-a");
|
||||
PrintingService b = service(instance, "fixture-tenant-b");
|
||||
for(PrintingService tenantService : List.of(a, b)) {
|
||||
JSONObject defaults = tenantService.getRetentionSettings();
|
||||
check(defaults.getInt("days") == 365 && defaults.getBoolean("enabled"), "fresh tenant defaults to enabled 365-day retention");
|
||||
check(bucket.equals(defaults.getString("bucketName")) && !otherBucket.equals(defaults.getString("bucketName")), "administration reports the dedicated fixture bucket, not the canary bucket");
|
||||
}
|
||||
String deviceA = enroll(a);
|
||||
String deviceB = enroll(b);
|
||||
byte[] pdf = PrintingTestDocuments.pdf();
|
||||
@@ -128,6 +133,20 @@ public final class S3PrintingQualification {
|
||||
check(s3.getObjectAsBytes(GetObjectRequest.builder().bucket(otherBucket).key("canary.txt").build()).asUtf8String().equals("synthetic separate bucket"), "separate bucket canary preserved");
|
||||
check(a.runRetention() == 0, "repeated cleanup is idempotent");
|
||||
check(b.runRetention() == 1, "tenant B cleanup independently removes its one eligible document");
|
||||
String adjustedReference = reference(zplJob);
|
||||
execute("UPDATE printing_job SET completed_at=? WHERE id=?", Instant.now().minusSeconds(40L * 86400).toString(), zplJob);
|
||||
check(a.getRetentionSettings().getLong("eligibleBacklog") == 0, "a 40-day-old document is not eligible under the 365-day default");
|
||||
a.saveRetentionSettings(30, false, 200);
|
||||
check(a.runRetention() == 0, "administrator-disabled retention removes nothing");
|
||||
check(s3.headObject(HeadObjectRequest.builder().bucket(bucket).key(objectKey(adjustedReference)).build()).contentLength() == zpl.length, "disabled retention leaves the object in S3");
|
||||
a.saveRetentionSettings(30, true, 1);
|
||||
JSONObject adjusted = a.getRetentionSettings();
|
||||
check(adjusted.getInt("days") == 30 && adjusted.getInt("batchSize") == 1 && adjusted.getLong("eligibleBacklog") == 1, "administrator 30-day setting and batch bound are persisted and reported");
|
||||
check(b.getRetentionSettings().getInt("days") == 365, "tenant A administrator change does not alter tenant B");
|
||||
check(a.runRetention() == 1, "administrator-adjusted retention removes exactly the one now-eligible document");
|
||||
missing(objectKey(adjustedReference));
|
||||
a.saveRetentionSettings(365, true, 200);
|
||||
check(a.getRetentionSettings().getInt("days") == 365, "administrator restores the 365-day default");
|
||||
check(s3.listMultipartUploads(ListMultipartUploadsRequest.builder().bucket(bucket).build()).uploads().isEmpty(), "no incomplete multipart uploads remain");
|
||||
check(QContext.getQSession() == null, "storage restores original absent QQQ session");
|
||||
JSONObject result = new JSONObject().put("passed", true).put("timestamp", Instant.now().toString())
|
||||
|
||||
@@ -81,6 +81,34 @@ pairing, job, print operation or fabricated user session. Released RC.20 lacks
|
||||
the native-session grant; this experiment must stay opt-in until an appropriate
|
||||
immutable upstream tuple is published and separately adopted.
|
||||
|
||||
## Retargeting to a new immutable tuple
|
||||
|
||||
The pins live in several files. Do not edit them by hand. Obtain the immutable
|
||||
core commit, the SHA-256 of its GitHub source archive and its Maven base version
|
||||
from the release owner, then run:
|
||||
|
||||
```powershell
|
||||
pwsh ./dev/Set-NativeAuthPins.ps1 -CoreCommit <40 hex> -CoreArchiveSha256 <64 hex> -CoreVersionBase <x.y.z>
|
||||
Invoke-Pester ./dev/tests/NativeAuthPins.Tests.ps1, ./dev/tests/NativeAuth.Tests.ps1
|
||||
```
|
||||
|
||||
The script needs no network and fails unless every current pin was found and
|
||||
replaced; the Pester drift test then proves all copies agree. The fixture still
|
||||
requires the upstream API to report exactly the version in
|
||||
`client/NativeAuthDiscovery.cs` (currently 1.9.0) with `nativeSessionLogin`. If a
|
||||
new tuple reports another API version the full run fails at discovery; change
|
||||
that gate and its twelve fake-HTTP cases deliberately, with the reason, rather
|
||||
than loosening them. Never replace shared Maven artifacts: the build installs
|
||||
only into the image's `/candidate-maven`.
|
||||
|
||||
## What this profile does not establish
|
||||
|
||||
Fixture passes are separate from three open qualifications and must be reported
|
||||
separately: the installed Windows browser sign-in with the connector's private
|
||||
URI callback, long-lived (months) unattended grant renewal against a real
|
||||
provider, and the final Echo cohort including scanner/RF devices. This profile
|
||||
tests none of them.
|
||||
|
||||
## Recorded local qualification
|
||||
|
||||
On 2026-11-01 the full disposable profile passed all eight checks and exited zero.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#Requires -Version 7.4
|
||||
# Copyright 2026 Kingsrook, LLC
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# No Docker, network or provider calls; only checks that every copy of the pinned source tuple agrees.
|
||||
BeforeAll {
|
||||
$script:dev = Join-Path $PSScriptRoot '..'
|
||||
|
||||
|
||||
# Reads one file under dev/ as text.
|
||||
function Read-DevFile
|
||||
{
|
||||
param([string] $RelativePath)
|
||||
|
||||
return Get-Content -LiteralPath (Join-Path $script:dev $RelativePath) -Raw
|
||||
}
|
||||
|
||||
|
||||
# Returns every capture group 1 match of a pattern.
|
||||
function Get-Captures
|
||||
{
|
||||
param([string] $Text, [string] $Pattern)
|
||||
|
||||
return @([regex]::Matches($Text, $Pattern) | ForEach-Object { $_.Groups[1].Value })
|
||||
}
|
||||
|
||||
|
||||
$script:coreSources = @(
|
||||
Get-Captures (Read-DevFile 'Invoke-NativeAuth.ps1') "coreSource = '([0-9a-f]{40})'"
|
||||
Get-Captures (Read-DevFile 'native-auth/client/Program.cs') 'CoreSource = "([0-9a-f]{40})"'
|
||||
Get-Captures (Read-DevFile 'native-auth/Dockerfile.server') 'qqq/archive/([0-9a-f]{40})\.tar\.gz'
|
||||
Get-Captures (Read-DevFile 'native-auth/Dockerfile.server') 'NATIVE_CORE_SOURCE=([0-9a-f]{40})'
|
||||
Get-Captures (Read-DevFile 'tests/NativeAuth.Tests.ps1') "coreSource = '([0-9a-f]{40})'"
|
||||
)
|
||||
$script:clientSources = @(
|
||||
Get-Captures (Read-DevFile 'Invoke-NativeAuth.ps1') "clientSource = '([0-9a-f]{40})'"
|
||||
Get-Captures (Read-DevFile 'native-auth/client/Program.cs') 'ClientSource = "([0-9a-f]{40})"'
|
||||
Get-Captures (Read-DevFile 'native-auth/Dockerfile.client') 'qqq-print-agent/archive/([0-9a-f]{40})\.tar\.gz'
|
||||
Get-Captures (Read-DevFile 'Dockerfile.client') 'qqq-print-agent/archive/([0-9a-f]{40})\.tar\.gz'
|
||||
Get-Captures (Read-DevFile 'tests/NativeAuth.Tests.ps1') "clientSource = '([0-9a-f]{40})'"
|
||||
)
|
||||
}
|
||||
|
||||
Describe 'Native-auth fixture source pins' {
|
||||
It 'uses one core commit everywhere, and every location is present' {
|
||||
$script:coreSources.Count | Should -Be 5
|
||||
@($script:coreSources | Select-Object -Unique).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'uses one client commit everywhere, matching the default workload pin' {
|
||||
$script:clientSources.Count | Should -Be 5
|
||||
@($script:clientSources | Select-Object -Unique).Count | Should -Be 1
|
||||
}
|
||||
|
||||
It 'names the Maven development revision after the core commit, in both server build steps' {
|
||||
$text = Read-DevFile 'native-auth/Dockerfile.server'
|
||||
$revisions = Get-Captures $text '-D(?:revision|qqq\.version)=(\d+\.\d+\.\d+-native-dev-[0-9a-f]{8})'
|
||||
$revisions.Count | Should -Be 2
|
||||
@($revisions | Select-Object -Unique).Count | Should -Be 1
|
||||
$revisions[0] | Should -BeLike "*-native-dev-$($script:coreSources[0].Substring(0, 8))"
|
||||
}
|
||||
|
||||
It 'checks both source archives with a SHA-256, never an unchecked download' {
|
||||
Get-Captures (Read-DevFile 'native-auth/Dockerfile.server') 'ADD --checksum=sha256:([0-9a-f]{64}) https://github.com/QRun-IO/qqq/archive/' | Should -HaveCount 1
|
||||
Get-Captures (Read-DevFile 'native-auth/Dockerfile.client') 'ADD --checksum=sha256:([0-9a-f]{64}) https://github.com/QRun-IO/qqq-print-agent/archive/' | Should -HaveCount 1
|
||||
}
|
||||
|
||||
It 'pins base images by digest' {
|
||||
foreach($file in 'native-auth/Dockerfile.server', 'native-auth/Dockerfile.client')
|
||||
{
|
||||
$images = Get-Captures (Read-DevFile $file) '(?m)^FROM (\S+)'
|
||||
$images.Count | Should -BeGreaterThan 0
|
||||
foreach($image in $images)
|
||||
{
|
||||
$image | Should -Match '@sha256:[0-9a-f]{64}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.printing;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
import com.kingsrook.qqq.middleware.javalin.QApplicationLauncher;
|
||||
import com.kingsrook.qqq.middleware.javalin.QApplicationLauncherConfig;
|
||||
import io.javalin.Javalin;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Whether a job created through the normal native QQQ process path wakes an
|
||||
** already-connected realtime channel at once. The existing realtime tests
|
||||
** create jobs by calling PrintingService.submit directly; none drives the
|
||||
** native test-print process that an operator uses. The server holds no
|
||||
** periodic reconciliation timer, so a push either arrives from the commit
|
||||
** notification or not until the next client message. Synthetic data only: no
|
||||
** physical printer exists and nothing is dispatched.
|
||||
*******************************************************************************/
|
||||
public class PrintingRealtimeNativeSubmissionTest
|
||||
{
|
||||
private static final long PUSH_BUDGET_MILLIS = 3000;
|
||||
|
||||
@TempDir
|
||||
Path directory;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The native process resolves its service from the running QQQ instance.
|
||||
** Its commit must reach the channel served by the host's own routes.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void nativeProcessSubmissionPushesToConnectedChannelWithoutAnyClientMessage() throws Exception
|
||||
{
|
||||
String jdbc = "jdbc:h2:file:" + directory.resolve("native-push").toString().replace('\\', '/');
|
||||
PrintingLocalHost host = new PrintingLocalHost(new PrintingLocalSettings(jdbc, "sa", "", "demo", 0, "synthetic-management-token-long-enough", "operator", "synthetic-realtime-password-1234"));
|
||||
AtomicReference<Javalin> app = new AtomicReference<>();
|
||||
QApplicationLauncher launcher = QApplicationLauncher.run(host, new QApplicationLauncherConfig().withRegisterShutdownHook(false).withServerCustomizer(server ->
|
||||
{
|
||||
host.configure(server, false);
|
||||
server.withJavalinConfigurationCustomizer(app::set);
|
||||
}));
|
||||
try
|
||||
{
|
||||
PrintingService service = launcher.getQInstance().getQBits().values().stream().map(value -> value.getConfig()).filter(PrintingQBitConfig.class::isInstance)
|
||||
.map(PrintingQBitConfig.class::cast).findFirst().orElseThrow().getService();
|
||||
String base = "http://127.0.0.1:" + app.get().port();
|
||||
JSONObject enrolled = service.enrollForUser(service.createEnrollment("default", "Native submission fixture").put("deviceName", "Native submission fixture").put("agentVersion", "test"), "operator");
|
||||
String device = enrolled.getString("deviceId");
|
||||
service.inventory(device, new JSONArray().put(new JSONObject().put("printerId", "queue").put("name", "Synthetic queue").put("status", "READY").put("formats", new JSONArray().put("ZPL"))));
|
||||
service.enablePrinter(device, "queue", true);
|
||||
|
||||
try(HttpClient http = HttpClient.newHttpClient(); PrintingSocketFixture channel = PrintingSocketFixture.connect(http, base, login(http, base), device, enrolled.getString("deviceToken")))
|
||||
{
|
||||
drainUntilQuiet(channel);
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// run the operator's real "print a test label" process under //
|
||||
// the host's own QInstance, exactly as the native UI does, //
|
||||
// then wait for the push with no HELLO, PING or other message //
|
||||
// from the client that could stand in for a reconciliation. //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QContext.init(launcher.getQInstance(), new QSession().withUser(new QUser().withIdReference("operator")).withPermissions("printing_admin").withSecurityKeyValue("printing_tenant", "demo"));
|
||||
try
|
||||
{
|
||||
String printerRowId = printerRowId(device);
|
||||
long started = System.nanoTime();
|
||||
var result = new RunProcessAction().execute(new RunProcessInput().withProcessName("printingTestZpl")
|
||||
.withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP).withValue("deviceId", device).withValue("printerId", printerRowId));
|
||||
assertFalse(result.getException().isPresent(), () -> "Native process failed: " + result.getException());
|
||||
assertEquals(1, service.queuedJobs(device).length(), "the native process must have queued exactly one durable job");
|
||||
|
||||
JSONObject pushed = channel.take("JOB_AVAILABLE");
|
||||
long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
|
||||
assertTrue(pushed.has("payload"));
|
||||
assertTrue(elapsedMillis < PUSH_BUDGET_MILLIS, "native process commit reached the channel only after " + elapsedMillis + " ms");
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
launcher.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Characterization, not a requirement: a second PrintingService over the
|
||||
** same database that was not given the host's bus commits durably but
|
||||
** cannot wake the host's channel. PrintingRealtimeTest.sharedBusRoundTrip
|
||||
** shows the supported arrangement (withRealtimeBus). Keep or delete this
|
||||
** according to the contract the owners choose; it reproduces the only
|
||||
** cross-instance wake hazard found in source.
|
||||
***************************************************************************/
|
||||
@Test
|
||||
void serviceInstanceWithoutTheHostBusCommitsButDoesNotWakeTheChannel() throws Exception
|
||||
{
|
||||
String jdbc = "jdbc:h2:file:" + directory.resolve("split-bus").toString().replace('\\', '/');
|
||||
PrintingLocalHost host = new PrintingLocalHost(new PrintingLocalSettings(jdbc, "sa", "", "demo", 0, "synthetic-management-token-long-enough", "operator", "synthetic-realtime-password-1234"));
|
||||
AtomicReference<Javalin> app = new AtomicReference<>();
|
||||
QApplicationLauncher launcher = QApplicationLauncher.run(host, new QApplicationLauncherConfig().withRegisterShutdownHook(false).withServerCustomizer(server ->
|
||||
{
|
||||
host.configure(server, false);
|
||||
server.withJavalinConfigurationCustomizer(app::set);
|
||||
}));
|
||||
try
|
||||
{
|
||||
PrintingService service = launcher.getQInstance().getQBits().values().stream().map(value -> value.getConfig()).filter(PrintingQBitConfig.class::isInstance)
|
||||
.map(PrintingQBitConfig.class::cast).findFirst().orElseThrow().getService();
|
||||
PrintingService split = new PrintingService(new DriverDataSource(jdbc, "sa", ""), "demo");
|
||||
String base = "http://127.0.0.1:" + app.get().port();
|
||||
JSONObject enrolled = service.enrollForUser(service.createEnrollment("default", "Split bus fixture").put("deviceName", "Split bus fixture").put("agentVersion", "test"), "operator");
|
||||
String device = enrolled.getString("deviceId");
|
||||
service.inventory(device, new JSONArray().put(new JSONObject().put("printerId", "queue").put("name", "Synthetic queue").put("status", "READY").put("formats", new JSONArray().put("ZPL"))));
|
||||
service.enablePrinter(device, "queue", true);
|
||||
|
||||
try(HttpClient http = HttpClient.newHttpClient(); PrintingSocketFixture channel = PrintingSocketFixture.connect(http, base, login(http, base), device, enrolled.getString("deviceToken")))
|
||||
{
|
||||
drainUntilQuiet(channel);
|
||||
split.submit(new JSONObject().put("deviceId", device).put("printerId", "queue").put("title", "Synthetic split bus").put("format", "ZPL")
|
||||
.put("contentBase64", Base64.getEncoder().encodeToString("^XA^XZ".getBytes(java.nio.charset.StandardCharsets.US_ASCII))).put("idempotencyKey", "split-bus"));
|
||||
assertEquals(1, service.queuedJobs(device).length(), "the commit is durable and visible to the host");
|
||||
assertNull(channel.received.poll(1500, TimeUnit.MILLISECONDS), "a service without the host bus is expected not to wake the channel");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
launcher.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Discard connection-time frames so the measured push is the only one.
|
||||
***************************************************************************/
|
||||
private static void drainUntilQuiet(PrintingSocketFixture channel) throws Exception
|
||||
{
|
||||
while(channel.received.poll(400, TimeUnit.MILLISECONDS) != null)
|
||||
{
|
||||
// expected: frames from the initial durable snapshot are discarded here
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Resolve the printer row the process form would have posted.
|
||||
***************************************************************************/
|
||||
private static String printerRowId(String device) throws Exception
|
||||
{
|
||||
QueryInput query = new QueryInput();
|
||||
query.setTableName(PrintingTables.PRINTER);
|
||||
for(QRecord row : new QueryAction().execute(query).getRecords())
|
||||
{
|
||||
if(device.equals(row.getValueString("device_id")))
|
||||
{
|
||||
return (row.getValueString("id"));
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Synthetic printer row was not found");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Real native password login; the session exists only for this test.
|
||||
***************************************************************************/
|
||||
private static String login(HttpClient client, String base) throws Exception
|
||||
{
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/qqq/v1/manageSession")).header("Origin", base)
|
||||
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString("operator:synthetic-realtime-password-1234".getBytes(java.nio.charset.StandardCharsets.UTF_8)))
|
||||
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString("{}")).build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
assertEquals(200, response.statusCode());
|
||||
return (response.headers().allValues("set-cookie").stream().filter(value -> value.startsWith("sessionUUID=")).findFirst().orElseThrow().split(";", 2)[0]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user