Compare commits

...
Author SHA1 Message Date
james.maesandClaude Sonnet 5.5 fa0282fbcc wip: native-auth pin script, tests and qualification notes
Recovered from an uncommitted worktree during the 2026-11-07 cleanup. Based on the 2026-11-01 release-qualification branch, so dev/ docs and scripts may be stale relative to main; review before opening a PR.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-07 16:40:46 -06:00
7 changed files with 464 additions and 2 deletions

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
+110
View File
@@ -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."
+19
View File
@@ -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())
+28
View File
@@ -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.
+79
View File
@@ -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]);
}
}