diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..20a3196
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+# Copyright 2026 Kingsrook, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+.git
+**/bin
+**/obj
+target
+.local
+.venv
diff --git a/.github/scripts/check-java-compiler.py b/.github/scripts/check-java-compiler.py
new file mode 100644
index 0000000..a6143f3
--- /dev/null
+++ b/.github/scripts/check-java-compiler.py
@@ -0,0 +1,120 @@
+# Copyright (C) 2026 Kingsrook, LLC
+# SPDX-License-Identifier: Apache-2.0
+"""Exercise the repository's actual compiler configuration on isolated probes."""
+
+import argparse
+import copy
+import json
+import logging
+import os
+import subprocess
+import tempfile
+import xml.etree.ElementTree as ET
+from dataclasses import dataclass
+from pathlib import Path
+
+NS = "{http://maven.apache.org/POM/4.0.0}"
+LOG = logging.getLogger(__name__)
+PROBES: dict[str, tuple[str, str | None]] = {
+ "positive": ('public String value() { return "present"; }', None),
+ "nullaway": ("public String value() { return null; }", "[NullAway]"),
+ "errorprone": ("public void value() { new IllegalArgumentException(); }", "[DeadException]"),
+ "warnings": ('public long value() { return java.util.Date.parse("2000-01-01"); }', "warnings found and -Werror specified"),
+}
+
+
+@dataclass(frozen=True)
+class GuardContext:
+ """Everything shared by all probes: repository root, evidence folder, probe POM and Maven command."""
+
+ root: Path
+ output: Path
+ pom: ET.ElementTree
+ command: list[str]
+
+
+@dataclass(frozen=True)
+class Probe:
+ """One isolated compilation: source root, name, the member source and the diagnostic it must produce (None means it must compile)."""
+
+ phase: str
+ name: str
+ source: str
+ expected: str | None
+
+
+def probe_pom(source: ET.Element) -> ET.ElementTree:
+ """Copy compiler configuration, including executions, without changing its flags."""
+ compiler = source.find(f"{NS}build/{NS}plugins/{NS}plugin[{NS}artifactId='maven-compiler-plugin']")
+ if compiler is None:
+ raise ValueError("Repository must declare its compiler plugin")
+ project = ET.Element(f"{NS}project")
+ for name, value in (("modelVersion", "4.0.0"), ("groupId", "com.kingsrook.qbits"), ("artifactId", "compiler-configuration-probe"), ("version", "1")):
+ ET.SubElement(project, f"{NS}{name}").text = value
+ properties = source.find(f"{NS}properties")
+ if properties is not None:
+ project.append(copy.deepcopy(properties))
+ plugins = ET.SubElement(ET.SubElement(project, f"{NS}build"), f"{NS}plugins")
+ plugins.append(copy.deepcopy(compiler))
+ return ET.ElementTree(project)
+
+
+def run_probe(context: GuardContext, probe: Probe) -> dict[str, str | bool]:
+ """Require the intended diagnostic, not merely an unrelated Maven failure."""
+ folder = context.output / f"{probe.phase}-{probe.name}"
+ package = folder / "src" / probe.phase / "java" / "com" / "kingsrook" / "qbits" / "printing"
+ package.mkdir(parents=True)
+ (package / "Probe.java").write_text(f"// Generated compiler guard fixture; not application source.\npackage com.kingsrook.qbits.printing;\npublic final class Probe {{ {probe.source} }}\n", encoding="utf-8")
+ context.pom.write(folder / "pom.xml", encoding="utf-8", xml_declaration=True)
+ result = subprocess.run( # noqa: S603 -- argument list (no shell) of the repository Maven wrapper or the operator-supplied executable plus fixed Maven goals
+ [*context.command, "-f", str(folder / "pom.xml"), "test-compile"],
+ cwd=context.root,
+ capture_output=True,
+ text=True,
+ timeout=180,
+ check=False,
+ )
+ log = result.stdout + result.stderr
+ (folder / "compiler.log").write_text(log, encoding="utf-8")
+ passed = result.returncode == 0 if probe.expected is None else result.returncode != 0 and probe.expected in log
+ if probe.name == "warnings":
+ passed = passed and "[deprecation]" in log
+ if not passed:
+ raise RuntimeError(f"Compiler guard failed: {probe.phase}/{probe.name}; inspect {folder / 'compiler.log'}")
+ return {"sourceRoot": probe.phase, "probe": probe.name, "expectedRejection": probe.expected is not None, "passed": True}
+
+
+def main() -> None:
+ """Run positive and negative main/test probes without installing any artifact."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--maven", help="Optional exact Maven executable; otherwise use the repository wrapper")
+ parser.add_argument("--settings", type=Path)
+ parser.add_argument("--local-repository", type=Path)
+ parser.add_argument("--offline", action="store_true")
+ args = parser.parse_args()
+ root = Path(__file__).resolve().parents[2]
+ command = [args.maven] if args.maven else ([str(root / "mvnw.cmd")] if os.name == "nt" else ["bash", str(root / "mvnw")])
+ command += ["-B", "-ntp"]
+ if args.offline:
+ command.append("-o")
+ if args.settings:
+ command += ["-s", str(args.settings.resolve())]
+ if args.local_repository:
+ command.append(f"-Dmaven.repo.local={args.local_repository.resolve()}")
+ output_root = root / "target" / "compiler-guard"
+ output_root.mkdir(parents=True, exist_ok=True)
+ output = Path(tempfile.mkdtemp(prefix="run-", dir=output_root))
+ ET.register_namespace("", NS[1:-1])
+ pom = probe_pom(ET.parse(root / "pom.xml").getroot()) # noqa: S314 -- parses this repository's own checked-in pom.xml, never external input
+ context = GuardContext(root, output, pom, command)
+ results: list[dict[str, str | bool]] = []
+ for phase in ("main", "test"):
+ for name, (source, expected) in PROBES.items():
+ results.append(run_probe(context, Probe(phase, name, source, expected)))
+ (output / "results.json").write_text(json.dumps(results, indent=3) + "\n", encoding="utf-8")
+ LOG.info("Compiler guard passed", extra={"probeCount": len(results), "evidenceDirectory": str(output)})
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ main()
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c9cf17e..052c164 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: QRun-IO/qqq-print-protocol
- ref: a26c18c90c7d6102bbe26eb50cbb90fcd1bd46e3
+ ref: 42c0d249f81395f00408ec0f92638dcbbf5deb03
path: qqq-print-protocol
persist-credentials: false
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
@@ -59,6 +59,8 @@ jobs:
python3 .github/scripts/check-source-headers.py
- name: Checkstyle, unit tests and HTTP contract tests
run: bash ./mvnw --batch-mode --no-transfer-progress -Pui-demo verify
+ - name: Prove main and test compiler safety gates reject invalid source
+ run: python3 .github/scripts/check-java-compiler.py
- name: Prepare JAR, POM and checksums
shell: bash
run: |
@@ -84,6 +86,7 @@ jobs:
path: |
qbit-printing/target/surefire-reports/
qbit-printing/target/checkstyle-result.xml
+ qbit-printing/target/compiler-guard/
if-no-files-found: ignore
release-security:
diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml
index cde4a9e..839f180 100644
--- a/.github/workflows/development.yml
+++ b/.github/workflows/development.yml
@@ -50,6 +50,48 @@ jobs:
$configuration.TestRegistry.Enabled = $false
Invoke-Pester -Configuration $configuration
+ clients:
+ name: development-clients (${{ matrix.fixture }})
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - fixture: workload
+ dockerfile: dev/Dockerfile.client
+ - fixture: native-auth
+ dockerfile: dev/native-auth/Dockerfile.client
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Build strict fixtures and run fake-HTTP discovery checks
+ env:
+ DOCKERFILE: ${{ matrix.dockerfile }}
+ run: docker build --target build --file "$DOCKERFILE" .
+
+ servers:
+ name: development-servers (${{ matrix.fixture }})
+ runs-on: ubuntu-24.04
+ timeout-minutes: 40
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - fixture: workload
+ dockerfile: dev/Dockerfile.server
+ - fixture: native-auth
+ dockerfile: dev/native-auth/Dockerfile.server
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Build strict Maven project and compile the fixture Java without starting it
+ env:
+ DOCKERFILE: ${{ matrix.dockerfile }}
+ run: docker build --target build --file "$DOCKERFILE" .
+
docker:
name: development-docker
runs-on: ubuntu-24.04
diff --git a/.gitignore b/.gitignore
index 3f84bc3..00c2725 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,7 @@ Thumbs.db
node_modules/
.ruff_cache/
__pycache__/
+
+# Isolated .NET developer fixture output.
+dev/**/bin/
+dev/**/obj/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ecc0deb..6f93c81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,14 @@ metadata, not by comparing versions. An agent that does not advertise
`printer-media-v1` stays a three-capability peer and cannot claim a job carrying a
media selection, whatever extension version it discovers.
+- Built-in test prints add 4 x 1 and 4 x 2 inch labels (203/300/600 dpi) beside the unchanged
+ 4 x 6 path: compact PDF and ZPL layouts, and for short PDF labels an explicit actual-size
+ job paper (`paperWidthMm`/`paperHeightMm`, a complete positive pair up to 2000 mm, never
+ combined with a media profile). 4 x 6 and page profiles keep using the queue default.
+- Strict source standards: Error Prone 2.51.0 and NullAway run as errors with `-Werror` on
+ main and test sources, with honest `@Nullable` contracts, strict source headers and
+ development-fixture style gates. No authorization, retained-source, replay or queue
+ behavior was changed by this work.
- Realtime shutdown now waits, with a bound, for channel workers that are already
running before the host's durable state is released. Previously the executor was
only told to stop, so a late drain could query a stopped file database and
diff --git a/README.md b/README.md
index e72ba0a..112f0ad 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Java/.NET WebSocket jobs and S3 checks without an installed connector or printer
## Build and verify
-Install Java 21. Clone this repository and `qqq-print-protocol` as siblings. This source pins protocol commit `f3cbc02e9b0fb4fdfefaa59d976c04df747cc589`; use that commit for reproducible compatibility tests. Use the checked-in wrapper (Maven 3.9.11).
+Install Java 21. Clone this repository and `qqq-print-protocol` as siblings. This source pins protocol commit `42c0d249f81395f00408ec0f92638dcbbf5deb03` (the integrated contract under Gitea PR 2; the same ref CI checks out); use that commit for reproducible compatibility tests. Use the checked-in wrapper (Maven 3.9.11).
```powershell
.\mvnw.cmd verify
@@ -149,7 +149,7 @@ For durable QQQ jobs register `PrintEnqueueEffectTarget` as an `IDEMPOTENT_EXTER
## Deliberate first-version limits
-The implemented path targets an explicit device/printer. Logical destination bindings, cancellation, reprint workflow, automatic reconciliation, clustered throughput optimization, retention and alerts are follow-ups. Documents are durably stored as base64 in the database for this first path; object-storage integration is deferred. Custom paper dimensions are rejected; configure the Windows queue. Received/submitting jobs are not automatically offered again. The queue records SPOOLED honestly and does not assert physical delivery.
+The implemented path targets an explicit device/printer. Logical destination bindings, cancellation, reprint workflow, automatic reconciliation, clustered throughput optimization, retention and alerts are follow-ups. Documents are durably stored as base64 in the database for this first path; object-storage integration is deferred. Job paper (`paperWidthMm` and `paperHeightMm`, both positive, at most 2000, never with `mediaProfile`) is accepted; the built-in 4 x 1 and 4 x 2 in PDF test labels use it (101.6 x 25.4 / 50.8 mm) so Windows does not feed 4 x 6 stock. Only a client from the same unreleased 0.4 cohort applies it; older clients reject the job. Other jobs follow the Windows queue default. Received/submitting jobs are not automatically offered again. The queue records SPOOLED honestly and does not assert physical delivery.
The protocol demo and synthetic native UI preview are separate local hosts. Attaching the QBit to a deployed application and its real authentication/facility policy remains a host integration task. PostgreSQL behavior, production deployment, upgrades, cancellation/reprint and broad printer-driver coverage are not certified by the initial H2/laser tests.
diff --git a/dev/.editorconfig b/dev/.editorconfig
new file mode 100644
index 0000000..8221d91
--- /dev/null
+++ b/dev/.editorconfig
@@ -0,0 +1,206 @@
+# KofTwentyTwo .NET code style (standards: K22-CODE-CS): the Kingsrook code style
+# (https://github.com/QRun-IO/qqq/blob/develop/CODE_STYLE.md) applied to C#.
+# Copy to the repository root. Severities of "warning" fail the build because the
+# companion Directory.Build.props sets TreatWarningsAsErrors and EnforceCodeStyleInBuild,
+# so local `dotnet build` and CI enforce exactly the same rules. The resharper_* keys
+# apply the Kingsrook blank-line rules in Rider and ReSharper, which .NET's own
+# formatter cannot express.
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+# Kingsrook: always 3 spaces, never tabs.
+indent_size = 3
+tab_width = 3
+
+# YAML stays at 2: the YAML ecosystem (GitHub Actions, Dependabot) assumes it.
+[*.{yml,yaml}]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+# Windows-only script formats require CRLF.
+[*.{cmd,bat}]
+end_of_line = crlf
+
+[*.cs]
+# All fixture sources are repository-owned Apache-2.0 code.
+file_header_template = Copyright 2026 Kingsrook, LLC\nSPDX-License-Identifier: Apache-2.0
+dotnet_diagnostic.IDE0073.severity = warning
+#### Formatting (IDE0055 makes every rule below a build error) ####
+dotnet_diagnostic.IDE0055.severity = warning
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_space_after_cast = false
+# Kingsrook: no space between a control keyword and its parenthesis: if(x), for(...), catch(...).
+csharp_space_after_keywords_in_control_flow_statements = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_after_comma = true
+csharp_space_before_comma = false
+csharp_space_after_semicolon_in_for_statement = true
+csharp_preserve_single_line_statements = false
+csharp_preserve_single_line_blocks = false
+
+#### Kingsrook blank lines and layout (Rider / ReSharper; .NET formatter cannot express) ####
+resharper_blank_lines_around_method = 3
+resharper_blank_lines_around_property = 3
+resharper_blank_lines_around_auto_property = 1
+resharper_blank_lines_around_type = 3
+resharper_blank_lines_after_using_list = 2
+resharper_keep_blank_lines_in_code = 1
+resharper_keep_blank_lines_in_declarations = 3
+resharper_brace_style = next_line
+resharper_indent_size = 3
+resharper_space_after_keywords_in_control_flow_statements = false
+resharper_max_line_length = 999
+resharper_wrap_before_first_method_call = true
+resharper_wrap_chained_method_calls = chop_if_long
+
+#### Usings and namespaces ####
+# IDE0005 needs GenerateDocumentationFile=true to run during build (set in Directory.Build.props).
+dotnet_diagnostic.IDE0005.severity = warning
+csharp_using_directive_placement = outside_namespace:warning
+csharp_style_namespace_declarations = file_scoped:warning
+dotnet_diagnostic.IDE0161.severity = warning
+
+#### var: only when the type is apparent from the right-hand side ####
+csharp_style_var_for_built_in_types = false:warning
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = false:warning
+dotnet_diagnostic.IDE0007.severity = warning
+dotnet_diagnostic.IDE0008.severity = warning
+
+#### Braces, modifiers, readonly ####
+csharp_prefer_braces = true:warning
+dotnet_diagnostic.IDE0011.severity = warning
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+dotnet_diagnostic.IDE0040.severity = warning
+dotnet_style_readonly_field = true:warning
+dotnet_diagnostic.IDE0044.severity = warning
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:warning
+dotnet_diagnostic.IDE0036.severity = warning
+
+#### this. qualification: never ####
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+dotnet_diagnostic.IDE0003.severity = warning
+
+#### Language keywords over framework type names ####
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+dotnet_diagnostic.IDE0049.severity = warning
+
+#### Dead code and redundancy ####
+dotnet_diagnostic.IDE0051.severity = warning # unused private member
+dotnet_diagnostic.IDE0052.severity = warning # unread private member
+dotnet_diagnostic.IDE0059.severity = warning # unnecessary assignment
+dotnet_diagnostic.IDE0060.severity = warning # unused parameter
+dotnet_diagnostic.IDE0035.severity = warning # unreachable code
+dotnet_diagnostic.IDE0004.severity = warning # unnecessary cast
+dotnet_diagnostic.IDE0100.severity = warning # unnecessary equality operator
+dotnet_diagnostic.IDE0080.severity = warning # unnecessary suppression operator
+dotnet_diagnostic.IDE0079.severity = warning # unnecessary suppression
+dotnet_code_quality_unused_parameters = all
+
+#### Modern language features (suggestions: shown in IDE, not enforced) ####
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_prefer_switch_expression = true:suggestion
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+csharp_style_expression_bodied_methods = when_on_single_line:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+
+#### Naming ####
+dotnet_naming_rule.interfaces_begin_with_i.symbols = interfaces
+dotnet_naming_rule.interfaces_begin_with_i.style = i_prefix_pascal_case
+dotnet_naming_rule.interfaces_begin_with_i.severity = warning
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_style.i_prefix_pascal_case.required_prefix = I
+dotnet_naming_style.i_prefix_pascal_case.capitalization = pascal_case
+
+dotnet_naming_rule.type_parameters_begin_with_t.symbols = type_parameters
+dotnet_naming_rule.type_parameters_begin_with_t.style = t_prefix_pascal_case
+dotnet_naming_rule.type_parameters_begin_with_t.severity = warning
+dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter
+dotnet_naming_style.t_prefix_pascal_case.required_prefix = T
+dotnet_naming_style.t_prefix_pascal_case.capitalization = pascal_case
+
+dotnet_naming_rule.types_and_members_are_pascal_case.symbols = types_and_members
+dotnet_naming_rule.types_and_members_are_pascal_case.style = pascal_case
+dotnet_naming_rule.types_and_members_are_pascal_case.severity = warning
+dotnet_naming_symbols.types_and_members.applicable_kinds = namespace, class, struct, enum, delegate, property, method, event
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_rule.private_fields_are_underscore_camel.symbols = private_fields
+dotnet_naming_rule.private_fields_are_underscore_camel.style = underscore_camel_case
+dotnet_naming_rule.private_fields_are_underscore_camel.severity = warning
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private, private_protected
+dotnet_naming_symbols.private_fields.required_modifiers =
+dotnet_naming_style.underscore_camel_case.required_prefix = _
+dotnet_naming_style.underscore_camel_case.capitalization = camel_case
+
+dotnet_naming_rule.private_static_fields_are_s_camel.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_are_s_camel.style = s_camel_case
+dotnet_naming_rule.private_static_fields_are_s_camel.severity = warning
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, private_protected
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+dotnet_naming_style.s_camel_case.required_prefix = s_
+dotnet_naming_style.s_camel_case.capitalization = camel_case
+
+dotnet_naming_rule.constants_are_pascal_case.symbols = constants
+dotnet_naming_rule.constants_are_pascal_case.style = pascal_case
+dotnet_naming_rule.constants_are_pascal_case.severity = warning
+dotnet_naming_symbols.constants.applicable_kinds = field, local
+dotnet_naming_symbols.constants.required_modifiers = const
+
+#### Analyzer adjustments (each one justified) ####
+# CA1716: flags members named like VB keywords (e.g. ILog.Error). KofTwentyTwo is C#-only.
+dotnet_diagnostic.CA1716.severity = none
+# MA0004 (ConfigureAwait): app code runs on UI threads where ConfigureAwait(false) is
+# wrong by default; libraries opt back in with a [*.cs] override in their own folder.
+dotnet_diagnostic.MA0004.severity = none
+# MA0049 (type name matches namespace): conflicts with the AppName/AppName.Core layout.
+dotnet_diagnostic.MA0049.severity = none
+
+[**/{Generated,obj}/**.cs]
+generated_code = true
+
+[{tests,test}/**.cs]
+# xUnit's Method_Scenario_Expectation names use underscores by convention.
+dotnet_diagnostic.CA1707.severity = none
+# Inline arrays keep test data at the assertion; each test runs once.
+dotnet_diagnostic.CA1861.severity = none
+# Test projects are not documented API.
+dotnet_diagnostic.CS1591.severity = none
+# MA0048 (one type per file): a test file groups the test classes and fakes of one feature.
+dotnet_diagnostic.MA0048.severity = none
diff --git a/dev/.gitattributes b/dev/.gitattributes
new file mode 100644
index 0000000..16c31e7
--- /dev/null
+++ b/dev/.gitattributes
@@ -0,0 +1,5 @@
+# Copyright 2026 Kingsrook, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+# Keep the SDK formatter identical on Windows and Linux checkouts.
+*.cs text eol=lf
diff --git a/dev/Directory.Build.props b/dev/Directory.Build.props
new file mode 100644
index 0000000..1de3bbc
--- /dev/null
+++ b/dev/Directory.Build.props
@@ -0,0 +1,30 @@
+
+
+
+ enable
+ enable
+ latest
+ false
+ true
+ true
+ latest-recommended
+ true
+ true
+ true
+ true
+ all
+ low
+ true
+ /core/Qqq.PrintAgent.Core.dll
+
+
+ true
+ true
+
+
+
+
+
+
+
+
diff --git a/dev/Directory.Packages.props b/dev/Directory.Packages.props
new file mode 100644
index 0000000..69ede84
--- /dev/null
+++ b/dev/Directory.Packages.props
@@ -0,0 +1,10 @@
+
+
+
+ true
+ true
+
+
+
+
+
diff --git a/dev/Dockerfile.client b/dev/Dockerfile.client
index b2ac6bd..2251e19 100644
--- a/dev/Dockerfile.client
+++ b/dev/Dockerfile.client
@@ -7,9 +7,11 @@ ADD --checksum=sha256:82fcdbf7240760939a63b219aeb15a2333b3a8a02b52307a8a328500fc
RUN mkdir /client && tar -xzf /tmp/client.tar.gz -C /client --strip-components=1 && rm /tmp/client.tar.gz
RUN dotnet build /client/src/Qqq.PrintAgent.Core/Qqq.PrintAgent.Core.csproj -c Release -p:Platform=x64 -p:Version=0.3.1-rc.1 -p:RestoreLockedMode=true --nologo
RUN mkdir /core && cp /client/src/Qqq.PrintAgent.Core/bin/x64/Release/net10.0/Qqq.PrintAgent.Core.dll /core/
-WORKDIR /fixture
+COPY dev/Directory.Build.props dev/Directory.Packages.props dev/.editorconfig dev/global.json /fixture/dev/
+WORKDIR /fixture/dev/client
COPY dev/client .
-RUN dotnet build -c Release --nologo -o /out
+RUN dotnet build -c Release -p:RestoreLockedMode=true --nologo -o /out
+RUN dotnet format --verify-no-changes --severity warn --no-restore
RUN dotnet /out/Workload.dll --check-discovery
# SDK image is reused deliberately: one pinned image, no hidden host SDK dependency.
FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:de4fe90a98845d75910b9769662465c84de1a934665c431279d1382b2bddc02b
diff --git a/dev/Dockerfile.server b/dev/Dockerfile.server
index 6a1ced3..d27344b 100644
--- a/dev/Dockerfile.server
+++ b/dev/Dockerfile.server
@@ -7,10 +7,11 @@ COPY pom.xml mvnw ./
COPY .mvn .mvn
COPY checkstyle checkstyle
COPY src src
+COPY dev/java dev/java
+COPY dev/native-auth/java dev/native-auth/java
COPY LICENSE NOTICE ./
RUN chmod +x mvnw && ./mvnw -B -Pui-demo -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/runtime
-COPY dev/java /fixture-src
-RUN mkdir -p /fixture-classes && javac -proc:none -cp 'target/classes:target/runtime/*' -d /fixture-classes /fixture-src/*.java
+RUN mkdir -p /fixture-classes && javac -proc:none -cp 'target/classes:target/runtime/*' -d /fixture-classes dev/java/*.java
FROM eclipse-temurin:21-jdk-jammy@sha256:742daacb9316be4b6fc60258f6e4ac2a11882f6c597373c92290a57bfad0d2e0
ARG SOURCE_COMMIT=unknown
diff --git a/dev/README.md b/dev/README.md
index 6cb00d4..cefc678 100644
--- a/dev/README.md
+++ b/dev/README.md
@@ -128,6 +128,26 @@ It is opt-in, Test-only, and does not change the default released QQQ dependency
The development workflow runs the Pester and PowerShell analysis checks on
Windows and Ubuntu for every pull request and main-branch push. Ubuntu also runs
-the real 40-job/S3 Docker smoke. The longer experimental native-auth profile is
+the real 40-job/S3 Docker smoke. Both client fixture images also have build-only
+jobs on every pull request, including the optional native client; these jobs run
+only fake-HTTP discovery checks and do not start a provider.
+
+Maven Checkstyle checks all four Java fixture sources in `dev/java` and
+`dev/native-auth/java` during `validate`. They are deliberately **not** Maven
+compiler sources: the ordinary fixture uses the released QQQ tuple, while the
+native host compiles only inside its explicitly selected candidate image. Both
+server Dockerfiles copy both roots before Maven runs so neither image can skip
+this style coverage.
+
+The ten C# fixture files share `dev/.editorconfig`, strict build properties,
+central Meziantou 3.0.296, and committed package locks. The existing pinned SDK
+image contains .NET SDK 10.0.403, matching `dev/global.json`. Both image builds
+perform locked, vulnerability-audited restore, warnings-as-errors compilation,
+formatter verification, and their nine or 12 fake-HTTP discovery cases. Core is
+still built from the exact public source archive above; no stubs or runtime
+package dependency were added. For a local build, supply the real pinned Core
+DLL through `-p:CoreAssemblyPath=/absolute/path/Qqq.PrintAgent.Core.dll`.
+
+The longer experimental native-auth profile is
an explicit manual workflow option; it is not silently substituted for the
released dependency baseline.
diff --git a/dev/client/DiscoveryChecks.cs b/dev/client/DiscoveryChecks.cs
index 8fa80ed..21b2ba2 100644
--- a/dev/client/DiscoveryChecks.cs
+++ b/dev/client/DiscoveryChecks.cs
@@ -1,12 +1,24 @@
// Copyright 2026 Kingsrook, LLC
-
// SPDX-License-Identifier: Apache-2.0
using System.Net;
using System.Text.Json;
using Qqq.PrintAgent.Core.Authentication;
+namespace Printing.Development;
+
+
+/// Exercises fixture compatibility decisions using credential-free fake HTTP responses.
internal static class DiscoveryChecks
{
+ private static readonly string[] s_features = ["version", "metaData", "processes", "processCancel", "extensions", "widgets"];
+
+
+
+ private static readonly string[] s_channels = ["api"];
+
+
+
+ /// Verify supported stable versions and reject malformed or incompatible versions.
internal static async Task RunAsync()
{
(string Version, bool Accepted)[] cases =
@@ -25,8 +37,14 @@ internal static class DiscoveryChecks
await WorkloadDiscovery.DiscoverRealtimeAsync(client, CancellationToken.None);
accepted = true;
}
- catch(QqqSessionException error) when(error.Failure == QqqSessionException.FailureKind.ConfigurationRequired) { }
- catch(InvalidDataException) { }
+ catch(QqqSessionException error) when(error.Failure == QqqSessionException.FailureKind.ConfigurationRequired)
+ {
+ // Expected for incompatible versions: the case stays rejected and the comparison below judges it.
+ }
+ catch(InvalidDataException)
+ {
+ // Expected for malformed versions: the case stays rejected and the comparison below judges it.
+ }
if(accepted != expected || handler.Requests != 1)
{
throw new InvalidDataException($"Discovery compatibility case {version} failed.");
@@ -35,14 +53,29 @@ internal static class DiscoveryChecks
Console.WriteLine($"Discovery compatibility checks passed: {cases.Length}.");
}
+
+
+ /// Supply one version response and reject any premature credential transmission.
private sealed class DiscoveryHandler(string version) : HttpMessageHandler
{
- internal int Requests { get; private set; }
+ internal int Requests
+ {
+ get;
+
+
+ private set;
+ }
+
+
+
+ /// Return only the synthetic discovery response and count the request.
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- if(request.Method != HttpMethod.Get || request.RequestUri?.AbsolutePath != "/qqq/v1/version"
- || request.Headers.Contains("Cookie") || request.Headers.Contains("Authorization"))
+ if(request.Method != HttpMethod.Get
+ || !string.Equals(request.RequestUri?.AbsolutePath, "/qqq/v1/version", StringComparison.Ordinal)
+ || request.Headers.Contains("Cookie")
+ || request.Headers.Contains("Authorization"))
{
throw new InvalidOperationException("Discovery must precede authentication.");
}
@@ -50,8 +83,8 @@ internal static class DiscoveryChecks
string json = JsonSerializer.Serialize(new
{
apiVersion = "1.8.0",
- features = new[] { "version", "metaData", "processes", "processCancel", "extensions", "widgets" },
- clientChannels = new[] { "api" },
+ features = s_features,
+ clientChannels = s_channels,
extensions = new[] { new { name = "printing", version, pathPrefix = "/qbit/printing" } }
});
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });
diff --git a/dev/client/Program.cs b/dev/client/Program.cs
index ef446f4..c3c4864 100644
--- a/dev/client/Program.cs
+++ b/dev/client/Program.cs
@@ -1,7 +1,5 @@
// Copyright 2026 Kingsrook, LLC
-
// SPDX-License-Identifier: Apache-2.0
-using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Net.WebSockets;
@@ -10,140 +8,618 @@ using System.Text.Json;
using System.Threading.Channels;
using Qqq.PrintAgent.Core.Authentication;
using Qqq.PrintAgent.Core.Printing;
-internal static class Program {
- static int Completed; static readonly byte[] Expected=Encoding.ASCII.GetBytes("^XA^FO20,20^FDSYNTHETIC NOT PRINTED^FS"+new string(' ',980)+"^XZ");
- static async Task Main(string[] args) {
- if(args is ["--check-discovery"]) { await DiscoveryChecks.RunAsync(); return 0; }
- string folder=Path.GetFullPath(args[0]); string stage="input";
- using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(18));
- List lanes=[]; long peak=0; var timer=Stopwatch.StartNew();
- try {
- using JsonDocument input=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(folder,"input.json")));
- stage="native_login_and_channels";
- bool idle=input.RootElement.GetProperty("idle").GetBoolean(); int perLane=input.RootElement.GetProperty("jobsPerLane").GetInt32(); int total=perLane*8; int index=0;
- foreach(var value in input.RootElement.GetProperty("lanes").EnumerateArray()) {
- Uri origin=new(value.GetProperty("origin").GetString()!);
- Require(origin.IsLoopback && origin.Port is not (18081 or 18086),"disposable origin");
- var client=new QqqSessionClient(origin,allowLoopbackHttp:true);
- await WorkloadDiscovery.DiscoverRealtimeAsync(client,timeout.Token);
- await client.GetAuthenticationMetadataAsync(timeout.Token);
- await client.SignInWithPasswordAsync(value.GetProperty("username").GetString()!,value.GetProperty("password").GetString()!,timeout.Token);
- await client.BindDeviceAsync(new(value.GetProperty("deviceId").GetString()!,value.GetProperty("deviceToken").GetString()!),cancellationToken:timeout.Token);
- var binding=await client.GetSessionIdentityAsync(timeout.Token);
- Require(binding.TenantId==value.GetProperty("tenant").GetString(),"authoritative tenant");
- var connection=await Connection.Open(client,timeout.Token);
- lanes.Add(new(client,connection,Path.Combine(folder,"lane-"+index++),perLane,folder));
+namespace Printing.Development;
+
+
+
+/// Runs real Core network workers against synthetic Java services without a physical printer.
+internal static class Program
+{
+ private static readonly JsonSerializerOptions s_reportOptions = new() { WriteIndented = true };
+
+
+
+ private static readonly string[] s_capabilities = ["realtime-v1", "REFRESH_INVENTORY", "CANCEL_SPOOL"];
+
+
+
+ private static int s_completed;
+
+
+
+ private static readonly byte[] s_expected = Encoding.ASCII.GetBytes("^XA^FO20,20^FDSYNTHETIC NOT PRINTED^FS" + new string(' ', 980) + "^XZ");
+
+
+
+ /// Run the bounded workload or fake-HTTP checks and always signal completion to the launcher.
+ private static async Task Main(string[] args)
+ {
+ if(args is ["--check-discovery"])
+ {
+ await DiscoveryChecks.RunAsync();
+ return 0;
+ }
+ string folder = Path.GetFullPath(args[0]);
+ string stage = "input";
+ using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(18));
+ List lanes = [];
+ long peak = 0;
+ var timer = Stopwatch.StartNew();
+ try
+ {
+ using var input = JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(folder, "input.json")));
+ stage = "native_login_and_channels";
+ bool idle = input.RootElement.GetProperty("idle").GetBoolean();
+ int perLane = input.RootElement.GetProperty("jobsPerLane").GetInt32();
+ int total = perLane * 8;
+ await OpenLanesAsync(input.RootElement, lanes, folder, perLane, timeout.Token);
+ bool denied = await RejectForeignProofAsync(input.RootElement, timeout.Token);
+ stage = "workload";
+ peak = await RunLanesAsync(lanes, folder, total, timeout.Token);
+ stage = "logout";
+ foreach(Lane lane in lanes)
+ {
+ await lane.Client.SignOutAsync(timeout.Token);
+ }
+ await WriteSuccessAsync(folder, lanes, total, idle, denied, peak, timer.Elapsed.TotalSeconds);
+ Console.WriteLine("PASS real Core/Java network lifecycles; fake printer only");
+ return 0;
+ }
+ catch(Exception error)
+ {
+ await File.WriteAllTextAsync(Path.Combine(folder, "result.json"), JsonSerializer.Serialize(new
+ {
+ success = false,
+ stage,
+ error = error.GetType().Name,
+ reason = error is InvalidDataException ? error.Message : null,
+ completed = lanes.Sum(l => l.Printer.Count)
+ }));
+ Console.Error.WriteLine("FAIL " + stage + ": " + error.GetType().Name);
+ return 1;
+ }
+ finally
+ {
+ foreach(Lane lane in lanes)
+ {
+ await lane.DisposeAsync();
+ }
+ await File.WriteAllTextAsync(Path.Combine(folder, "done"), "done");
+ }
}
- Require(lanes.Count==8,"eight lanes");
- // Authoritative native proof check: tenant A cannot bind a real tenant B device.
- var foreign=input.RootElement.GetProperty("lanes")[4]; bool denied=false;
- using(var outsider=new QqqSessionClient(new Uri(input.RootElement.GetProperty("lanes")[0].GetProperty("origin").GetString()!),allowLoopbackHttp:true)) {
- await outsider.DiscoverAsync(timeout.Token);await outsider.GetAuthenticationMetadataAsync(timeout.Token);
- await outsider.SignInWithPasswordAsync("operator",input.RootElement.GetProperty("lanes")[0].GetProperty("password").GetString()!,timeout.Token);
- try {await outsider.BindDeviceAsync(new(foreign.GetProperty("deviceId").GetString()!,foreign.GetProperty("deviceToken").GetString()!),cancellationToken:timeout.Token);}catch(QqqSessionException){denied=true;}
- Require(denied,"cross tenant proof refused");await outsider.SignOutAsync(timeout.Token);
+
+
+
+ /// Authenticate and bind eight synthetic lanes before admitting fixture work.
+ private static async Task OpenLanesAsync(JsonElement input, List lanes, string folder, int perLane, CancellationToken token)
+ {
+ int index = 0;
+ foreach(JsonElement value in input.GetProperty("lanes").EnumerateArray())
+ {
+ Uri origin = new(value.GetProperty("origin").GetString()!);
+ Require(origin.IsLoopback && origin.Port is not (18081 or 18086), "disposable origin");
+ var client = new QqqSessionClient(origin, allowLoopbackHttp: true);
+ await WorkloadDiscovery.DiscoverRealtimeAsync(client, token);
+ await client.GetAuthenticationMetadataAsync(token);
+ await client.SignInWithPasswordAsync(value.GetProperty("username").GetString()!, value.GetProperty("password").GetString()!, token);
+ await client.BindDeviceAsync(new(value.GetProperty("deviceId").GetString()!, value.GetProperty("deviceToken").GetString()!), cancellationToken: token);
+ QqqSessionBinding binding = await client.GetSessionIdentityAsync(token);
+ Require(string.Equals(binding.TenantId, value.GetProperty("tenant").GetString(), StringComparison.Ordinal), "authoritative tenant");
+ Connection connection = await Connection.Open(client, token);
+ lanes.Add(new(client, connection, Path.Combine(folder, "lane-" + index++), perLane, folder));
+ }
+ Require(lanes.Count == 8, "eight lanes");
}
- stage="workload";
- var tasks=lanes.Select(lane=>lane.Run(timeout.Token)).ToArray();
- await File.WriteAllTextAsync(Path.Combine(folder,"ready"),"ready",timeout.Token);
- Task all=Task.WhenAll(tasks);
- while(!all.IsCompleted) {
- peak=Math.Max(peak,Process.GetCurrentProcess().WorkingSet64);
- await Task.WhenAny(all,Task.Delay(1000,timeout.Token));timeout.Token.ThrowIfCancellationRequested();
+
+
+
+ /// Verify that authenticated tenant A cannot adopt tenant B's device proof.
+ private static async Task RejectForeignProofAsync(JsonElement input, CancellationToken token)
+ {
+ // Authoritative native proof check: tenant A cannot bind a real tenant B device.
+ JsonElement foreign = input.GetProperty("lanes")[4];
+ bool denied = false;
+ using(var outsider = new QqqSessionClient(new Uri(input.GetProperty("lanes")[0].GetProperty("origin").GetString()!), allowLoopbackHttp: true))
+ {
+ await outsider.DiscoverAsync(token);
+ await outsider.GetAuthenticationMetadataAsync(token);
+ await outsider.SignInWithPasswordAsync("operator", input.GetProperty("lanes")[0].GetProperty("password").GetString()!, token);
+ try
+ {
+ await outsider.BindDeviceAsync(new(foreign.GetProperty("deviceId").GetString()!, foreign.GetProperty("deviceToken").GetString()!), cancellationToken: token);
+ }
+ catch(QqqSessionException) { denied = true; }
+ Require(denied, "cross tenant proof refused");
+ await outsider.SignOutAsync(token);
+ }
+ return denied;
+ }
+
+
+
+ /// Write only aggregate network, accounting, and fake-printer evidence.
+ private static async Task WriteSuccessAsync(string folder, List lanes, int total, bool idle, bool denied, long peak, double elapsedSeconds)
+ {
+ double[] latencies = lanes.SelectMany(l => l.Latencies).Order().ToArray();
+ await File.WriteAllTextAsync(Path.Combine(folder, "result.json"), JsonSerializer.Serialize(new
+ {
+ success = true,
+ jobs = total,
+ idle,
+ tenants = 2,
+ devices = 8,
+ elapsedSeconds = elapsedSeconds,
+ jobsPerSecond = total / elapsedSeconds,
+ peakClientWorkingSetBytes = peak,
+ p50EnqueueToAckMillis = latencies[total / 2],
+ p95EnqueueToAckMillis = latencies[(int)(total * 0.95)],
+ p99EnqueueToAckMillis = latencies[(int)(total * 0.99)],
+ realCoreWorker = true,
+ fakePrinterBackend = true,
+ fixtureJournal = true,
+ serverReceiptsDurable = true,
+ crossTenantProofRefused = denied,
+ submissionReplays = idle ? 0 : 400,
+ receiptReplays = idle ? 0 : 400,
+ notificationToClaimMillis = lanes.SelectMany(l => l.ClaimLatency).ToArray(),
+ notificationToDownloadCompletedMillis = lanes.SelectMany(l => l.DownloadLatency).ToArray(),
+ periodicJobPolling = false,
+ transport = "HTTP/WS loopback; no TLS qualification",
+ realPrinterCalls = 0,
+ runtime = Environment.Version.ToString(),
+ coreVersion = typeof(PrintWorker).Assembly.GetName().Version?.ToString()
+ }, s_reportOptions));
+ }
+
+
+
+ /// Drain only event-triggered work and measure the peak client working set.
+ private static async Task RunLanesAsync(List lanes, string folder, int total, CancellationToken token)
+ {
+ long peak = 0;
+ Task[] tasks = lanes.Select(lane => lane.Run(token)).ToArray();
+ await File.WriteAllTextAsync(Path.Combine(folder, "ready"), "ready", token);
+ var all = Task.WhenAll(tasks);
+ while(!all.IsCompleted)
+ {
+ peak = Math.Max(peak, Process.GetCurrentProcess().WorkingSet64);
+ await Task.WhenAny(all, Task.Delay(1000, token));
+ token.ThrowIfCancellationRequested();
+ }
+ await all;
+ Require(lanes.Sum(l => l.Printer.Count) == total, "exact fake submission count");
+ Require(lanes.All(l => l.Journal.Pending().Count == 0), "no unacknowledged receipts");
+ return peak;
+ }
+
+
+
+ /// Stop the fixture when an invariant fails, retaining a fixed diagnostic reason.
+ private static void Require(bool ok, string reason)
+ {
+ if(!ok)
+ {
+ throw new InvalidDataException(reason);
+ }
+ }
+
+
+
+ /// Own one authenticated worker, its synthetic journal, and notification latency measurements.
+ private sealed class Lane : IAsyncDisposable
+ {
+ public QqqSessionClient Client
+ {
+ get;
+ }
+
+
+
+ public Connection Connection
+ {
+ get;
+ }
+
+
+
+ public FakePrinter Printer { get; } = new();
+
+
+
+ public Journal Journal
+ {
+ get;
+ }
+
+
+
+ public List Latencies { get; } = [];
+
+
+
+ private readonly PrintApiClient _api;
+
+
+
+ private readonly PrintWorker _worker;
+
+
+
+ private readonly int _target;
+
+
+
+ private readonly string _root;
+
+
+
+ private long _wakeAt;
+
+
+
+ public List ClaimLatency { get; } = [];
+
+
+
+ public List DownloadLatency { get; } = [];
+
+
+
+ /// Own one authenticated worker, its synthetic journal, and notification latency measurements.
+ public Lane(QqqSessionClient client, Connection connection, string folder, int targetJobs, string rootDirectory)
+ {
+ Client = client;
+ Connection = connection;
+ _target = targetJobs;
+ _root = rootDirectory;
+ Printer.OnDownload = () => DownloadLatency.Add(Stopwatch.GetElapsedTime(_wakeAt).TotalMilliseconds);
+ Directory.CreateDirectory(folder);
+ Journal = new(folder);
+ _api = PrintApiClient.ForSession(client).WithRealtime(connection.Channel, connection.Settings);
+ _worker = new PrintWorker(_api, Printer, Journal, options: new PrintWorkerOptions
+ {
+ Settings = connection.Settings,
+ StagingDirectory = Path.Combine(folder, "staging"),
+ SynchronizeInventory = false,
+ RecoverIncomplete = false,
+ ActiveJobChanged = job =>
+ {
+ if(job is not null)
+ {
+ ClaimLatency.Add(Stopwatch.GetElapsedTime(_wakeAt).TotalMilliseconds);
+ }
+ }
+ });
+ }
+
+
+
+ /// Drain notified jobs and await durable receipts without periodic job polling.
+ public async Task Run(CancellationToken token)
+ {
+ using var stop = CancellationTokenSource.CreateLinkedTokenSource(token);
+ Task ping = Liveness(stop.Token);
+ try
+ {
+ while(Printer.Count < _target)
+ {
+ _wakeAt = await Connection.Wakes.Reader.ReadAsync(token);
+ while(Printer.Count < _target && await _worker.RunOnceAsync(token))
+ {
+ Require(string.Equals(Journal.Last.Event.Status, "SPOOLED", StringComparison.Ordinal), "synthetic job terminal status");
+ Latencies.Add((DateTimeOffset.UtcNow - DateTimeOffset.Parse(Journal.LastJob!.CreatedAt, CultureInfo.InvariantCulture)).TotalMilliseconds);
+ await File.WriteAllTextAsync(Path.Combine(_root, "completed-" + Interlocked.Increment(ref s_completed)), "acknowledged", token);
+ if(Printer.Count % 25 == 0)
+ {
+ await _api.EventAsync(Journal.Last.JobId, Journal.Last.Event, token);
+ }
+ }
+ }
+ }
+ finally
+ {
+ await stop.CancelAsync();
+ try
+ {
+ await ping;
+ }
+ catch(OperationCanceledException)
+ {
+ // Cancellation ends only the owned liveness task.
+ }
+ }
+ }
+
+
+
+ /// Send transport liveness frames without querying for jobs.
+ private async Task Liveness(CancellationToken token)
+ {
+ using var tick = new PeriodicTimer(TimeSpan.FromSeconds(Connection.Settings.HeartbeatIntervalSeconds));
+ while(await tick.WaitForNextTickAsync(token))
+ {
+ await Connection.Channel.SendAsync("PING", new
+ {
+ }, token: token);
+ }
+ }
+
+
+
+ /// Stop owned background work and dispose this fixture lane or channel.
+ public async ValueTask DisposeAsync()
+ {
+ _api.Dispose();
+ await Connection.DisposeAsync();
+ Client.Dispose();
+ Journal.Dispose();
+ }
+ }
+
+
+
+ /// Verify payload bytes and reject duplicate actions without calling a spooler.
+ private sealed class FakePrinter : IFilePrinterBackend
+ {
+ private readonly HashSet _seen = new(StringComparer.Ordinal);
+
+
+
+ public int Count => _seen.Count;
+
+
+
+ public Action? OnDownload
+ {
+ get; set;
+ }
+
+
+
+ /// Expose only the fixed synthetic ZPL destination.
+ public IReadOnlyList Discover() => [new("synthetic", "No physical printer", "fixture", "none", "READY", ["ZPL"])];
+
+
+
+ /// Record download completion and validate the staged synthetic payload.
+ public async Task PrintFileAsync(PrintJob job, string path, CancellationToken token)
+ {
+ OnDownload?.Invoke();
+ return await PrintAsync(job, await File.ReadAllBytesAsync(path, token), token);
+ }
+
+
+
+ /// Check exact bytes and allow each synthetic job to be submitted once.
+ public Task PrintAsync(PrintJob job, byte[] content, CancellationToken token)
+ {
+ Require(content.AsSpan().SequenceEqual(s_expected), "downloaded bytes exact");
+ Require(_seen.Add(job.JobId), "no duplicate physical action");
+ return Task.FromResult("synthetic-" + Count);
+ }
+ }
+ // Fixture journal is deliberately separate from the installed Windows SQLite journal.
+
+
+
+ /// Track fixture-only receipt state separately from the installed SQLite journal.
+ private sealed class Journal : IJobJournal, IDisposable
+ {
+ private readonly Dictionary _states = new(StringComparer.Ordinal);
+
+
+
+ private readonly Dictionary _pending = new(StringComparer.Ordinal);
+
+
+
+ private readonly StreamWriter _log;
+
+
+
+ public PrintJob? LastJob
+ {
+ get;
+
+
+
+ private set;
+ }
+
+
+
+ public (string JobId, PrintEvent Event) Last
+ {
+ get;
+
+
+
+ private set;
+ }
+
+
+
+ /// Track fixture-only receipt state separately from the installed SQLite journal.
+ public Journal(string folder)
+ {
+ _log = new StreamWriter(new FileStream(Path.Combine(folder, "synthetic-receipts.jsonl"), FileMode.CreateNew, FileAccess.Write, FileShare.Read)) { AutoFlush = true };
+ }
+
+
+
+ /// Retain accepted job metadata for latency and status assertions.
+ public void TrackJob(PrintJob job, string? printer)
+ {
+ LastJob = job;
+ }
+
+
+
+ /// Return the last recorded state for the synthetic job.
+ public string? GetState(string id) => _states.GetValueOrDefault(id);
+
+
+
+ /// Flush a receipt before adding it to the pending set.
+ public void Record(string id, PrintEvent evt)
+ {
+ _log.WriteLine(JsonSerializer.Serialize(new
+ {
+ jobId = id,
+ @event = evt
+ }));
+ _states[id] = evt.Status;
+ _pending.Add(evt.EventId, (id, evt));
+ Last = (id, evt);
+ }
+
+
+
+ /// Snapshot unacknowledged synthetic receipts for the real worker.
+ public IReadOnlyList<(string JobId, PrintEvent Event)> Pending() => _pending.Values.ToList();
+
+
+
+ /// Remove only the specifically acknowledged receipt.
+ public void Acknowledge(string id)
+ {
+ _pending.Remove(id);
+ }
+
+
+
+ /// Declare no recovered jobs because each fixture journal starts fresh.
+ public IReadOnlyList<(string JobId, string State)> Incomplete() => [];
+
+
+
+ /// Close the fixture receipt log.
+ public void Dispose() => _log.Dispose();
+ }
+
+
+
+ /// Own the realtime reader, bounded notifications, and negotiated settings.
+ private sealed class Connection : IAsyncDisposable
+ {
+ public required PrintingRealtimeChannel Channel
+ {
+ get; init;
+ }
+
+
+
+ public required Task Reader
+ {
+ get; init;
+ }
+
+
+
+ public required CancellationTokenSource Lifetime
+ {
+ get; init;
+ }
+
+
+
+ public required Channel Messages
+ {
+ get; init;
+ }
+
+
+
+ public required Channel Wakes
+ {
+ get; init;
+ }
+
+
+
+ public RealtimeSettings Settings
+ {
+ get;
+
+
+
+ private set;
+ } = new();
+
+
+
+ /// Open an authenticated channel and acknowledge its initial authoritative state.
+ public static async Task Open(QqqSessionClient client, CancellationToken token)
+ {
+ var lifetime = CancellationTokenSource.CreateLinkedTokenSource(token);
+ WebSocket socket = await client.ConnectPrintingAsync(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(60), token);
+ var channel = new PrintingRealtimeChannel(socket, new RealtimeSettings());
+ var messages = System.Threading.Channels.Channel.CreateBounded(8);
+ var wakes = System.Threading.Channels.Channel.CreateBounded(new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropWrite });
+ Task reader = channel.RunAsync(e =>
+ {
+ if(string.Equals(e.Type, "JOB_AVAILABLE", StringComparison.Ordinal))
+ {
+ wakes.Writer.TryWrite(Stopwatch.GetTimestamp());
+ return;
+ }
+ if(e.Type is "SETTINGS" or "DESIRED_STATE")
+ {
+ long revision = e.Payload.GetProperty("revision").GetInt64();
+ if(revision != 0)
+ {
+ throw new InvalidDataException("Unexpected policy change");
+ }
+ return;
+ }
+ if(e.Type is "PONG" or "ACK")
+ {
+ return;
+ }
+ if(!messages.Writer.TryWrite(e))
+ {
+ throw new IOException("Fixture envelope inbox full");
+ }
+ }, lifetime.Token);
+ _ = reader.ContinueWith(t => { wakes.Writer.TryComplete(t.Exception); messages.Writer.TryComplete(t.Exception); }, TaskScheduler.Default);
+ var result = new Connection { Channel = channel, Reader = reader, Lifetime = lifetime, Messages = messages, Wakes = wakes };
+ await channel.SendAsync("HELLO", new
+ {
+ agentVersion = PrintApiClient.AgentVersion,
+ capabilities = s_capabilities,
+ lastAppliedRevision = 0,
+ lastSettingsRevision = 0,
+ limits = RealtimeSettings.ClientLimits
+ }, token: token);
+ RealtimeEnvelope welcome = await messages.Reader.ReadAsync(token);
+ Require(string.Equals(welcome.Type, "WELCOME", StringComparison.Ordinal), "welcome");
+ result.Settings = welcome.Payload.GetProperty("settings").Deserialize(PrintApiClient.JsonOptions)!;
+ result.Settings.Validate();
+ channel.UpdateSettings(result.Settings);
+ await channel.SendAsync("STATE_APPLIED", new
+ {
+ lastAppliedRevision = welcome.Payload.GetProperty("desiredState").GetProperty("revision").GetInt64(),
+ lastSettingsRevision = result.Settings.Revision
+ }, acknowledged: true, token: token);
+ return result;
+ }
+
+
+
+ /// Stop owned background work and dispose this fixture lane or channel.
+ public async ValueTask DisposeAsync()
+ {
+ await Lifetime.CancelAsync();
+ try
+ {
+ await Reader;
+ }
+ catch(Exception e) when(e is OperationCanceledException or IOException or WebSocketException)
+ {
+ // Cancelling the owned reader normally ends it with one of these; disposal must still finish.
+ }
+ Channel.Dispose();
+ Lifetime.Dispose();
+ }
}
- await all;
- Require(lanes.Sum(l=>l.Printer.Count)==total,"exact fake submission count");
- Require(lanes.All(l=>l.Journal.Pending().Count==0),"no unacknowledged receipts");
- stage="logout";foreach(var lane in lanes)await lane.Client.SignOutAsync(timeout.Token);
- double[] latencies=lanes.SelectMany(l=>l.Latencies).Order().ToArray();
- await File.WriteAllTextAsync(Path.Combine(folder,"result.json"),JsonSerializer.Serialize(new {
- success=true,jobs=total,idle,tenants=2,devices=8,elapsedSeconds=timer.Elapsed.TotalSeconds,jobsPerSecond=total/timer.Elapsed.TotalSeconds,
- peakClientWorkingSetBytes=peak,p50EnqueueToAckMillis=latencies[total/2],p95EnqueueToAckMillis=latencies[(int)(total*0.95)],p99EnqueueToAckMillis=latencies[(int)(total*0.99)],
- realCoreWorker=true,fakePrinterBackend=true,fixtureJournal=true,serverReceiptsDurable=true,crossTenantProofRefused=denied,
- submissionReplays=idle?0:400,receiptReplays=idle?0:400,notificationToClaimMillis=lanes.SelectMany(l=>l.ClaimLatency).ToArray(),notificationToDownloadCompletedMillis=lanes.SelectMany(l=>l.DownloadLatency).ToArray(),periodicJobPolling=false,transport="HTTP/WS loopback; no TLS qualification",realPrinterCalls=0,
- runtime=Environment.Version.ToString(),coreVersion=typeof(PrintWorker).Assembly.GetName().Version?.ToString()
- },new JsonSerializerOptions{WriteIndented=true}));
- Console.WriteLine("PASS real Core/Java network lifecycles; fake printer only");return 0;
- } catch(Exception error) {
- await File.WriteAllTextAsync(Path.Combine(folder,"result.json"),JsonSerializer.Serialize(new{success=false,stage,error=error.GetType().Name,reason=error is InvalidDataException?error.Message:null,completed=lanes.Sum(l=>l.Printer.Count)}));
- Console.Error.WriteLine("FAIL "+stage+": "+error.GetType().Name);return 1;
- } finally {
- foreach(var lane in lanes)await lane.DisposeAsync();
- await File.WriteAllTextAsync(Path.Combine(folder,"done"),"done");
- }
- }
- static void Require(bool ok,string reason){if(!ok)throw new InvalidDataException(reason);}
- sealed class Lane: IAsyncDisposable {
- public QqqSessionClient Client{get;} public Connection Connection{get;}
- public FakePrinter Printer{get;}=new(); public Journal Journal{get;} public List Latencies{get;}=[];
- readonly PrintApiClient api;readonly PrintWorker worker; readonly int target; readonly string root; long wakeAt; public List ClaimLatency{get;}=[]; public List DownloadLatency{get;}=[];
- public Lane(QqqSessionClient client,Connection connection,string folder,int target,string root) {
- Client=client;Connection=connection;this.target=target;this.root=root;Printer.OnDownload=()=>DownloadLatency.Add(Stopwatch.GetElapsedTime(wakeAt).TotalMilliseconds);Directory.CreateDirectory(folder);Journal=new(folder);
- api=PrintApiClient.ForSession(client).WithRealtime(connection.Channel,connection.Settings);
- worker=new PrintWorker(api,Printer,Journal,options:new PrintWorkerOptions{Settings=connection.Settings,StagingDirectory=Path.Combine(folder,"staging"),SynchronizeInventory=false,RecoverIncomplete=false,ActiveJobChanged=job=>{if(job is not null)ClaimLatency.Add(Stopwatch.GetElapsedTime(wakeAt).TotalMilliseconds);}});
- }
- public async Task Run(CancellationToken token) {
- using var stop=CancellationTokenSource.CreateLinkedTokenSource(token);
- Task ping=Liveness(stop.Token);
- try {
- while(Printer.Count seen=new(StringComparer.Ordinal);public int Count=>seen.Count; public Action? OnDownload{get;set;}
- public IReadOnlyList Discover()=>[new("synthetic","No physical printer","fixture","none","READY",["ZPL"])];
- public async Task PrintFileAsync(PrintJob job,string path,CancellationToken token) {OnDownload?.Invoke();return await PrintAsync(job,await File.ReadAllBytesAsync(path,token),token);}
- public Task PrintAsync(PrintJob job,byte[] content,CancellationToken token) {
- Require(content.AsSpan().SequenceEqual(Expected),"downloaded bytes exact");Require(seen.Add(job.JobId),"no duplicate physical action");return Task.FromResult("synthetic-"+Count);
- }
- }
- // Fixture journal is deliberately separate from the installed Windows SQLite journal.
- sealed class Journal: IJobJournal,IDisposable {
- readonly Dictionary states=new(StringComparer.Ordinal);readonly Dictionary pending=new(StringComparer.Ordinal);readonly StreamWriter log;
- public PrintJob? LastJob{get;private set;}public (string JobId,PrintEvent Event) Last{get;private set;}
- public Journal(string folder){log=new StreamWriter(new FileStream(Path.Combine(folder,"synthetic-receipts.jsonl"),FileMode.CreateNew,FileAccess.Write,FileShare.Read)){AutoFlush=true};}
- public void TrackJob(PrintJob job,string? printer){LastJob=job;}
- public string? GetState(string id)=>states.GetValueOrDefault(id);
- public void Record(string id,PrintEvent evt){log.WriteLine(JsonSerializer.Serialize(new{jobId=id,@event=evt}));states[id]=evt.Status;pending.Add(evt.EventId,(id,evt));Last=(id,evt);}
- public IReadOnlyList<(string JobId,PrintEvent Event)> Pending()=>pending.Values.ToList();
- public void Acknowledge(string id){pending.Remove(id);}
- public IReadOnlyList<(string JobId,string State)> Incomplete()=>[];
- public void Dispose()=>log.Dispose();
- }
- sealed class Connection:IAsyncDisposable {
- public required PrintingRealtimeChannel Channel{get;init;}public required Task Reader{get;init;}public required CancellationTokenSource Lifetime{get;init;}
- public required Channel Messages{get;init;}public required Channel Wakes{get;init;}public RealtimeSettings Settings{get;private set;}=new();
- public static async Task Open(QqqSessionClient client,CancellationToken token) {
- var lifetime=CancellationTokenSource.CreateLinkedTokenSource(token);WebSocket socket=await client.ConnectPrintingAsync(TimeSpan.FromSeconds(20),TimeSpan.FromSeconds(60),token);
- var channel=new PrintingRealtimeChannel(socket,new RealtimeSettings());var messages=System.Threading.Channels.Channel.CreateBounded(8);
- var wakes=System.Threading.Channels.Channel.CreateBounded(new BoundedChannelOptions(1){FullMode=BoundedChannelFullMode.DropWrite});
- Task reader=channel.RunAsync(e=>{
- if(e.Type=="JOB_AVAILABLE"){wakes.Writer.TryWrite(Stopwatch.GetTimestamp());return;}
- if(e.Type is "SETTINGS" or "DESIRED_STATE") {long revision=e.Payload.GetProperty("revision").GetInt64();if(revision!=0)throw new InvalidDataException("Unexpected policy change");return;}
- if(e.Type is "PONG" or "ACK")return;
- if(!messages.Writer.TryWrite(e))throw new IOException("Fixture envelope inbox full");
- },lifetime.Token);
- _=reader.ContinueWith(t=>{wakes.Writer.TryComplete(t.Exception);messages.Writer.TryComplete(t.Exception);},TaskScheduler.Default);
- var result=new Connection{Channel=channel,Reader=reader,Lifetime=lifetime,Messages=messages,Wakes=wakes};
- await channel.SendAsync("HELLO",new{agentVersion=PrintApiClient.AgentVersion,capabilities=new[]{"realtime-v1","REFRESH_INVENTORY","CANCEL_SPOOL"},lastAppliedRevision=0,lastSettingsRevision=0,limits=RealtimeSettings.ClientLimits},token:token);
- RealtimeEnvelope welcome=await messages.Reader.ReadAsync(token);Require(welcome.Type=="WELCOME","welcome");
- result.Settings=welcome.Payload.GetProperty("settings").Deserialize(PrintApiClient.JsonOptions)!;result.Settings.Validate();channel.UpdateSettings(result.Settings);
- await channel.SendAsync("STATE_APPLIED",new{lastAppliedRevision=welcome.Payload.GetProperty("desiredState").GetProperty("revision").GetInt64(),lastSettingsRevision=result.Settings.Revision},acknowledged:true,token:token);return result;
- }
- public async ValueTask DisposeAsync(){await Lifetime.CancelAsync();try{await Reader;}catch(Exception e)when(e is OperationCanceledException or IOException or WebSocketException){}Channel.Dispose();Lifetime.Dispose();}
- }
}
diff --git a/dev/client/Workload.csproj b/dev/client/Workload.csproj
index 9a2c168..73fd9f5 100644
--- a/dev/client/Workload.csproj
+++ b/dev/client/Workload.csproj
@@ -2,14 +2,14 @@
Copyright 2026 Kingsrook, LLC
SPDX-License-Identifier: Apache-2.0
-->
-
-
- Exenet10.0
- enableenable
- true
-
-
- /core/Qqq.PrintAgent.Core.dll
-
+
+ Exe
+ net10.0
+
+
+
+ $(CoreAssemblyPath)
+
+
diff --git a/dev/client/WorkloadDiscovery.cs b/dev/client/WorkloadDiscovery.cs
index b70169d..859afde 100644
--- a/dev/client/WorkloadDiscovery.cs
+++ b/dev/client/WorkloadDiscovery.cs
@@ -1,10 +1,14 @@
// Copyright 2026 Kingsrook, LLC
-
// SPDX-License-Identifier: Apache-2.0
using Qqq.PrintAgent.Core.Authentication;
+namespace Printing.Development;
+
+
+/// Applies production Core discovery rules before any fixture credentials are sent.
internal static class WorkloadDiscovery
{
+ /// Require a compatible realtime extension after strict production discovery validation.
internal static async Task DiscoverRealtimeAsync(QqqSessionClient client, CancellationToken cancellationToken)
{
QqqApiDiscovery discovery = await client.DiscoverAsync(cancellationToken);
diff --git a/dev/client/packages.lock.json b/dev/client/packages.lock.json
new file mode 100644
index 0000000..d635df9
--- /dev/null
+++ b/dev/client/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Meziantou.Analyzer": {
+ "type": "Direct",
+ "requested": "[3.0.296, )",
+ "resolved": "3.0.296",
+ "contentHash": "1Yp/6OK/Ev66hE2YLe9UvIgtJrKOX/xvoGDe0UbdtuG2ZxUoDoyONdEn+HWCDuy02Mu8SmglHEd/9SGPrM1qYg=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dev/global.json b/dev/global.json
new file mode 100644
index 0000000..c8a6201
--- /dev/null
+++ b/dev/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "10.0.403",
+ "rollForward": "latestPatch"
+ }
+}
diff --git a/dev/java/DevHost.java b/dev/java/DevHost.java
index 4d7677d..2918306 100644
--- a/dev/java/DevHost.java
+++ b/dev/java/DevHost.java
@@ -19,8 +19,11 @@
*/
// SPDX-License-Identifier: Apache-2.0
+
package com.kingsrook.qbits.printing;
+
+import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -41,17 +44,30 @@ import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;
-/** Docker-only developer host. Compose publishes its port to host loopback only. */
+
+/***************************************************************************
+ ** Docker-only developer host. Compose publishes its port to host loopback only.
+ ***************************************************************************/
public final class DevHost extends PrintingLocalHost
{
private static final String TABLE = "printing_documents";
+
+
+ /***************************************************************************
+ ** Configure the isolated developer database and synthetic account from its private environment.
+ ***************************************************************************/
private DevHost() throws Exception
{
super(new PrintingLocalSettings("jdbc:h2:file:/data/printing", "sa", required("QQQ_DATABASE_PASSWORD"), "development",
Integer.parseInt(required("QQQ_HTTP_PORT")), required("QQQ_MANAGEMENT_TOKEN"), "developer", required("QQQ_ADMIN_PASSWORD")));
}
+
+
+ /***************************************************************************
+ ** Wait for owned storage and launch the loopback-published developer host.
+ ***************************************************************************/
public static void main(String[] args) throws Exception
{
waitStorage();
@@ -73,6 +89,11 @@ public final class DevHost extends PrintingLocalHost
}));
}
+
+
+ /***************************************************************************
+ ** Add the isolated MinIO-backed document table and make sure its bucket exists.
+ ***************************************************************************/
@Override
public QInstance defineQInstance() throws QException
{
@@ -87,19 +108,17 @@ public final class DevHost extends PrintingLocalHost
.withBackendDetails(new S3TableBackendDetails().withCardinality(Cardinality.ONE).withFileNameFieldName("id").withContentsFieldName("content"))
.withPrimaryKeyField("id").withField(new QFieldMetaData("id", QFieldType.STRING)).withField(new QFieldMetaData("content", QFieldType.BLOB)));
var storage = S3ClientFactory.getClient(backend);
+ try
{
- try
+ storage.headBucket(HeadBucketRequest.builder().bucket("printing-development").build());
+ }
+ catch(S3Exception missing)
+ {
+ if(missing.statusCode() != 404)
{
- storage.headBucket(HeadBucketRequest.builder().bucket("printing-development").build());
- }
- catch(S3Exception missing)
- {
- if(missing.statusCode() != 404)
- {
- throw missing;
- }
- storage.createBucket(CreateBucketRequest.builder().bucket("printing-development").build());
+ throw missing;
}
+ storage.createBucket(CreateBucketRequest.builder().bucket("printing-development").build());
}
PrintingQBitConfig config = instance.getQBits().values().stream().map(value -> value.getConfig())
.filter(PrintingQBitConfig.class::isInstance).map(PrintingQBitConfig.class::cast).findFirst().orElseThrow();
@@ -108,6 +127,11 @@ public final class DevHost extends PrintingLocalHost
return instance;
}
+
+
+ /***************************************************************************
+ ** Bound startup retries to the owned MinIO health endpoint.
+ ***************************************************************************/
private static void waitStorage() throws Exception
{
try(HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build())
@@ -123,7 +147,7 @@ public final class DevHost extends PrintingLocalHost
return;
}
}
- catch(java.io.IOException notReady)
+ catch(IOException notReady)
{
// Only the fixture startup window retries; application work is never retried here.
}
@@ -133,6 +157,11 @@ public final class DevHost extends PrintingLocalHost
}
}
+
+
+ /***************************************************************************
+ ** Read a required fixture setting without exposing its value.
+ ***************************************************************************/
private static String required(String name)
{
String value = System.getenv(name);
diff --git a/dev/java/S3PrintingQualification.java b/dev/java/S3PrintingQualification.java
index 8ebd807..e9d55a7 100644
--- a/dev/java/S3PrintingQualification.java
+++ b/dev/java/S3PrintingQualification.java
@@ -19,202 +19,423 @@
*/
// SPDX-License-Identifier: Apache-2.0
-import com.kingsrook.qbits.printing.*;
-import com.kingsrook.qqq.backend.core.context.QContext;
-import com.kingsrook.qqq.backend.core.model.metadata.*;
-import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
-import com.kingsrook.qqq.backend.core.model.metadata.fields.*;
-import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
-import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.*;
-import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality;
-import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3ClientFactory;
+
+
import java.nio.charset.StandardCharsets;
-import java.nio.file.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.security.MessageDigest;
-import java.sql.*;
import java.time.Instant;
-import java.util.*;
-import org.json.*;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import com.kingsrook.qbits.printing.DriverDataSource;
+import com.kingsrook.qbits.printing.PrintingException;
+import com.kingsrook.qbits.printing.PrintingService;
+import com.kingsrook.qbits.printing.PrintingTestDocuments;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality;
+import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3BackendMetaData;
+import com.kingsrook.qqq.backend.module.filesystem.s3.model.metadata.S3TableBackendDetails;
+import com.kingsrook.qqq.backend.module.filesystem.s3.utils.S3ClientFactory;
+import org.json.JSONArray;
+import org.json.JSONObject;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.services.s3.model.*;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
+import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
+import software.amazon.awssdk.services.s3.model.ListMultipartUploadsRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+import software.amazon.awssdk.services.s3.model.S3Object;
-/** Non-listening synthetic harness. Never routes work to a Windows device. */
-public final class S3PrintingQualification {
- private static final String TABLE = "printing_documents";
- private static final JSONArray CHECKS = new JSONArray();
- private static final JSONArray DOCUMENTS = new JSONArray();
- private static final Map OBJECT_KEYS = new HashMap<>();
- private static DriverDataSource database;
- private static S3Client s3;
- private static String bucket;
- private static String otherBucket;
- public static void main(String[] args) throws Exception {
- Path output = Path.of(args[0]).toAbsolutePath();
- Files.createDirectories(output);
- String run = args[2];
- bucket = "printing-fixture-" + run;
- otherBucket = "printing-canary-" + run;
- S3BackendMetaData backend = new S3BackendMetaData().withName("printing_s3_" + run)
- .withBucketName(bucket).withEndpoint(args[1]).withPathStyleAccess(true).withRegion("us-east-1")
- .withAccessKey(System.getenv("MINIO_ROOT_USER")).withSecretKey(System.getenv("MINIO_ROOT_PASSWORD"));
- backend.setConnectTimeoutMillis(3000);
- backend.setSocketTimeoutMillis(15000);
- QInstance instance = new QInstance();
- // Required metadata only: this harness opens no HTTP endpoint or user session.
- instance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withName("fixture").withType(QAuthenticationType.MOCK));
- instance.addBackend(backend);
- instance.addTable(new QTableMetaData().withName(TABLE).withBackendName(backend.getName())
- .withBackendDetails(new S3TableBackendDetails().withCardinality(Cardinality.ONE)
- .withFileNameFieldName("id").withContentsFieldName("content")).withPrimaryKeyField("id")
- .withField(new QFieldMetaData("id", QFieldType.STRING)).withField(new QFieldMetaData("content", QFieldType.BLOB)));
- database = new DriverDataSource("jdbc:h2:file:" + output.resolve("synthetic-data").toString().replace('\\', '/'), "sa", "");
- try {
- s3 = S3ClientFactory.getClient(backend);
- s3.createBucket(CreateBucketRequest.builder().bucket(bucket).build());
- s3.createBucket(CreateBucketRequest.builder().bucket(otherBucket).build());
- s3.putObject(PutObjectRequest.builder().bucket(bucket).key("unrelated/canary.txt").build(), RequestBody.fromString("synthetic unrelated object"));
- 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");
- String deviceA = enroll(a);
- String deviceB = enroll(b);
- byte[] pdf = PrintingTestDocuments.pdf();
- byte[] zpl = "^XA^FO20,20^ADN,30,18^FDSYNTHETIC STORAGE TEST ONLY^FS^XZ".getBytes(StandardCharsets.US_ASCII);
- byte[] large = new byte[6 * 1024 * 1024 + 71];
- Arrays.fill(large, (byte)'A');
- System.arraycopy("^XA^FX".getBytes(StandardCharsets.US_ASCII), 0, large, 0, 6);
- System.arraycopy("^FS^XZ".getBytes(StandardCharsets.US_ASCII), 0, large, large.length - 6, 6);
- String pdfJob = submitCheck(a, deviceA, "small-pdf", "PDF", pdf, false);
- finish(a, deviceA, pdfJob, "SPOOLED");
- String zplJob = submitCheck(a, deviceA, "small-zpl", "ZPL", zpl, false);
- finish(a, deviceA, zplJob, "SPOOLED");
- String largeJob = submitCheck(a, deviceA, "multipart-zpl", "ZPL", large, true);
- finish(a, deviceA, largeJob, "SPOOLED");
- String foreignJob = submitCheck(b, deviceB, "foreign-zpl", "ZPL", zpl, false);
- finish(b, deviceB, foreignJob, "SPOOLED");
- String foreignRef = reference(foreignJob);
- String ownRef = reference(pdfJob);
- check(!ownRef.split("/")[1].equals(foreignRef.split("/")[1]), "tenant prefixes differ");
- expectStatus(404, () -> a.content(deviceA, foreignJob), "foreign tenant job download denied");
- expectStatus(404, () -> a.content(deviceB, foreignJob), "foreign tenant device rejected");
- execute("UPDATE printing_job SET content_reference=? WHERE id=?", foreignRef, pdfJob);
- expectStatus(409, () -> a.content(deviceA, pdfJob), "foreign stored reference rejected");
- execute("UPDATE printing_job SET content_reference=? WHERE id=?", ownRef, pdfJob);
- check(Arrays.equals(zpl, b.content(deviceB, foreignJob)), "foreign object unchanged after rejection");
- String failed = submitCheck(a, deviceA, "protected-failed", "ZPL", zpl, false);
- finish(a, deviceA, failed, "FAILED");
- String unknown = submitCheck(a, deviceA, "protected-unknown", "ZPL", zpl, false);
- finish(a, deviceA, unknown, "OUTCOME_UNKNOWN");
- String active = a.submit(request(deviceA, "protected-queued", "ZPL", zpl)).getString("jobId");
- String old = Instant.now().minusSeconds(400L * 86400).toString();
- for(String id : List.of(pdfJob, largeJob, foreignJob, failed, unknown, active)) {
- execute("UPDATE printing_job SET created_at=?,completed_at=? WHERE id=?", old, old, id);
+/***************************************************************************
+ ** Non-listening synthetic harness. Never routes work to a Windows device.
+ ***************************************************************************/
+public final class S3PrintingQualification
+{
+ private static final String TABLE = "printing_documents";
+ private static final JSONArray CHECKS = new JSONArray();
+ private static final JSONArray DOCUMENTS = new JSONArray();
+ private static final Map OBJECT_KEYS = new HashMap<>();
+ private static DriverDataSource database;
+ private static S3Client s3;
+ private static String bucket;
+ private static String otherBucket;
+
+
+
+ /***************************************************************************
+ ** Exercise real storage operations against dedicated synthetic buckets without dispatching to a printer.
+ ***************************************************************************/
+ public static void main(String[] args) throws Exception
+ {
+ Path output = Path.of(args[0]).toAbsolutePath();
+ Files.createDirectories(output);
+ String run = args[2];
+ bucket = "printing-fixture-" + run;
+ otherBucket = "printing-canary-" + run;
+ S3BackendMetaData backend = new S3BackendMetaData().withName("printing_s3_" + run)
+ .withBucketName(bucket).withEndpoint(args[1]).withPathStyleAccess(true).withRegion("us-east-1")
+ .withAccessKey(System.getenv("MINIO_ROOT_USER")).withSecretKey(System.getenv("MINIO_ROOT_PASSWORD"));
+ backend.setConnectTimeoutMillis(3000);
+ backend.setSocketTimeoutMillis(15000);
+ QInstance instance = new QInstance();
+ // Required metadata only: this harness opens no HTTP endpoint or user session.
+ instance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withName("fixture").withType(QAuthenticationType.MOCK));
+ instance.addBackend(backend);
+ instance.addTable(new QTableMetaData().withName(TABLE).withBackendName(backend.getName())
+ .withBackendDetails(new S3TableBackendDetails().withCardinality(Cardinality.ONE)
+ .withFileNameFieldName("id").withContentsFieldName("content")).withPrimaryKeyField("id")
+ .withField(new QFieldMetaData("id", QFieldType.STRING)).withField(new QFieldMetaData("content", QFieldType.BLOB)));
+ database = new DriverDataSource("jdbc:h2:file:" + output.resolve("synthetic-data").toString().replace('\\', '/'), "sa", "");
+ try
+ {
+ s3 = S3ClientFactory.getClient(backend);
+ s3.createBucket(CreateBucketRequest.builder().bucket(bucket).build());
+ s3.createBucket(CreateBucketRequest.builder().bucket(otherBucket).build());
+ s3.putObject(PutObjectRequest.builder().bucket(bucket).key("unrelated/canary.txt").build(), RequestBody.fromString("synthetic unrelated object"));
+ 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");
+ String deviceA = enroll(a);
+ String deviceB = enroll(b);
+ byte[] pdf = PrintingTestDocuments.pdf();
+ byte[] zpl = "^XA^FO20,20^ADN,30,18^FDSYNTHETIC STORAGE TEST ONLY^FS^XZ".getBytes(StandardCharsets.US_ASCII);
+ byte[] large = new byte[6 * 1024 * 1024 + 71];
+ Arrays.fill(large, (byte) 'A');
+ System.arraycopy("^XA^FX".getBytes(StandardCharsets.US_ASCII), 0, large, 0, 6);
+ System.arraycopy("^FS^XZ".getBytes(StandardCharsets.US_ASCII), 0, large, large.length - 6, 6);
+ String pdfJob = submitCheck(a, deviceA, "small-pdf", "PDF", pdf, false);
+ finish(a, deviceA, pdfJob, "SPOOLED");
+ String zplJob = submitCheck(a, deviceA, "small-zpl", "ZPL", zpl, false);
+ finish(a, deviceA, zplJob, "SPOOLED");
+ String largeJob = submitCheck(a, deviceA, "multipart-zpl", "ZPL", large, true);
+ finish(a, deviceA, largeJob, "SPOOLED");
+ String foreignJob = submitCheck(b, deviceB, "foreign-zpl", "ZPL", zpl, false);
+ finish(b, deviceB, foreignJob, "SPOOLED");
+ String foreignRef = reference(foreignJob);
+ String ownRef = reference(pdfJob);
+ check(!ownRef.split("/")[1].equals(foreignRef.split("/")[1]), "tenant prefixes differ");
+ expectStatus(404, () -> a.content(deviceA, foreignJob), "foreign tenant job download denied");
+ expectStatus(404, () -> a.content(deviceB, foreignJob), "foreign tenant device rejected");
+ execute("UPDATE printing_job SET content_reference=? WHERE id=?", foreignRef, pdfJob);
+ expectStatus(409, () -> a.content(deviceA, pdfJob), "foreign stored reference rejected");
+ execute("UPDATE printing_job SET content_reference=? WHERE id=?", ownRef, pdfJob);
+ check(Arrays.equals(zpl, b.content(deviceB, foreignJob)), "foreign object unchanged after rejection");
+ String failed = submitCheck(a, deviceA, "protected-failed", "ZPL", zpl, false);
+ finish(a, deviceA, failed, "FAILED");
+ String unknown = submitCheck(a, deviceA, "protected-unknown", "ZPL", zpl, false);
+ finish(a, deviceA, unknown, "OUTCOME_UNKNOWN");
+ String active = a.submit(request(deviceA, "protected-queued", "ZPL", zpl)).getString("jobId");
+ String old = Instant.now().minusSeconds(400L * 86400).toString();
+ for(String id : List.of(pdfJob, largeJob, foreignJob, failed, unknown, active))
+ {
+ execute("UPDATE printing_job SET created_at=?,completed_at=? WHERE id=?", old, old, id);
+ }
+ int before = objectCount();
+ check(a.runRetention() == 2, "tenant A retention removes exactly two aged successful documents");
+ check(objectCount() == before - 2, "object count reflects exact two removals");
+ missing(OBJECT_KEYS.get(ownRef));
+ missing(OBJECT_KEYS.get(reference(largeJob)));
+ expectStatus(410, () -> a.content(deviceA, pdfJob), "expired document returns 410");
+ check(a.submit(request(deviceA, "small-pdf", "PDF", pdf)).getString("jobId").equals(pdfJob), "idempotent replay survives retention without reupload");
+ check(objectCount() == before - 2, "post-retention replay creates no object");
+ for(String id : List.of(zplJob, foreignJob, failed, unknown, active))
+ {
+ check(s3.headObject(HeadObjectRequest.builder().bucket(bucket).key(objectKey(reference(id))).build()).contentLength() == zpl.length,
+ "protected object retained: " + status(id));
+ }
+ check(s3.getObjectAsBytes(GetObjectRequest.builder().bucket(bucket).key("unrelated/canary.txt").build()).asUtf8String().equals("synthetic unrelated object"), "unrelated same-bucket canary preserved");
+ 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");
+ 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())
+ .put("checks", CHECKS).put("documents", DOCUMENTS).put("runtimeJarCount", 204)
+ .put("appRevision", System.getenv("PRINTING_SOURCE_COMMIT"))
+ .put("h2Version", org.h2.engine.Constants.VERSION).put("retentionDays", a.getRetentionSettings().getInt("days"))
+ .put("transport", "loopback HTTP S3-compatible only").put("hardwarePrinting", false)
+ .put("qualifies", "QQQ StorageAction / AWS SDK v2 S3 data-plane behavior against disposable MinIO")
+ .put("doesNotQualify", new JSONArray(List.of("AWS IAM", "remote TLS", "production bucket ownership", "Object Lock/version lifecycle", "remote deployment", "hardware printing")));
+ Files.writeString(output.resolve("results.json"), result.toString(2) + "\n");
+ System.out.println("PASS " + CHECKS.length() + " checks; " + DOCUMENTS.length() + " synthetic document round trips");
}
- int before = objectCount();
- check(a.runRetention() == 2, "tenant A retention removes exactly two aged successful documents");
- check(objectCount() == before - 2, "object count reflects exact two removals");
- missing(OBJECT_KEYS.get(ownRef));
- missing(OBJECT_KEYS.get(reference(largeJob)));
- expectStatus(410, () -> a.content(deviceA, pdfJob), "expired document returns 410");
- check(a.submit(request(deviceA, "small-pdf", "PDF", pdf)).getString("jobId").equals(pdfJob), "idempotent replay survives retention without reupload");
- check(objectCount() == before - 2, "post-retention replay creates no object");
- for(String id : List.of(zplJob, foreignJob, failed, unknown, active)) {
- check(s3.headObject(HeadObjectRequest.builder().bucket(bucket).key(objectKey(reference(id))).build()).contentLength() == zpl.length,
- "protected object retained: " + status(id));
+ finally
+ {
+ Files.writeString(output.resolve("completed-checks.json"), new JSONObject().put("checks", CHECKS).put("documents", DOCUMENTS).toString(2) + "\n");
+ S3ClientFactory.clearCache();
+ QContext.clear();
}
- check(s3.getObjectAsBytes(GetObjectRequest.builder().bucket(bucket).key("unrelated/canary.txt").build()).asUtf8String().equals("synthetic unrelated object"), "unrelated same-bucket canary preserved");
- 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");
- 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())
- .put("checks", CHECKS).put("documents", DOCUMENTS).put("runtimeJarCount", 204)
- .put("appRevision", System.getenv("PRINTING_SOURCE_COMMIT"))
- .put("h2Version", org.h2.engine.Constants.VERSION).put("retentionDays", a.getRetentionSettings().getInt("days"))
- .put("transport", "loopback HTTP S3-compatible only").put("hardwarePrinting", false)
- .put("qualifies", "QQQ StorageAction / AWS SDK v2 S3 data-plane behavior against disposable MinIO")
- .put("doesNotQualify", new JSONArray(List.of("AWS IAM", "remote TLS", "production bucket ownership", "Object Lock/version lifecycle", "remote deployment", "hardware printing")));
- Files.writeString(output.resolve("results.json"), result.toString(2) + "\n");
- System.out.println("PASS " + CHECKS.length() + " checks; " + DOCUMENTS.length() + " synthetic document round trips");
- } finally {
- Files.writeString(output.resolve("completed-checks.json"), new JSONObject().put("checks", CHECKS).put("documents", DOCUMENTS).toString(2) + "\n");
- S3ClientFactory.clearCache();
- QContext.clear();
- }
- }
- private static PrintingService service(QInstance instance, String tenant) throws Exception {
- PrintingService service = new PrintingService(database, tenant);
- service.initialize(); service.configureStorage(instance, TABLE); return service;
- }
- private static String enroll(PrintingService service) throws Exception {
- String id = service.enroll(service.createEnrollment("fixture", "Synthetic station")
- .put("deviceName", "Synthetic station").put("agentVersion", "isolated-storage-fixture")).getString("deviceId");
- service.inventory(id, new JSONArray().put(new JSONObject().put("printerId", "synthetic-never-dispatched")
- .put("name", "Synthetic storage-only destination").put("status", "READY").put("formats", new JSONArray(List.of("PDF", "ZPL")))));
- service.enablePrinter(id, "synthetic-never-dispatched", true); return id;
- }
- private static JSONObject request(String device, String key, String format, byte[] bytes) {
- return new JSONObject().put("deviceId", device).put("printerId", "synthetic-never-dispatched").put("title", "Synthetic storage qualification")
- .put("format", format).put("contentBase64", Base64.getEncoder().encodeToString(bytes)).put("idempotencyKey", key);
- }
- private static String submitCheck(PrintingService service, String device, String key, String format, byte[] bytes, boolean multipart) throws Exception {
- JSONObject request = request(device, key, format, bytes);
- String id = service.submit(request).getString("jobId");
- int count = objectCount();
- check(id.equals(service.submit(request).getString("jobId")) && count == objectCount(), key + " stable replay creates no object");
- check(service.claim(device).getJSONObject("job").getString("jobId").equals(id), key + " claimed through production service");
- byte[] actual = service.content(device, id);
- check(Arrays.equals(bytes, actual) && sha(bytes).equals(sha(actual)), key + " download bytes and SHA256 match");
- String ref = reference(id);
- String actualKey = objectKey(ref);
- HeadObjectResponse head = s3.headObject(HeadObjectRequest.builder().bucket(bucket).key(actualKey).build());
- check(head.contentLength() == bytes.length, key + " object length exact");
- check(head.contentType().equals(format.equals("PDF") ? "application/pdf" : "application/vnd.zebra-zpl"), key + " content type correct");
- check(head.eTag().contains("-2") == multipart, key + " expected single/multipart object ETag");
- try(var c = database.getConnection(); var statement = c.prepareStatement("SELECT content_base64,content_storage_table FROM printing_job WHERE id=?")) {
- statement.setString(1, id); try(var rows = statement.executeQuery()) { check(rows.next() && rows.getString(1) == null && TABLE.equals(rows.getString(2)), key + " payload external, no inline database copy"); }
- }
- DOCUMENTS.put(new JSONObject().put("case", key).put("format", format).put("bytes", bytes.length).put("sha256", sha(bytes)).put("multipart", multipart).put("etag", head.eTag())
- .put("objectKeyEqualsStoredReference", actualKey.equals(ref)).put("objectKeyPrefixBeforeReference", actualKey.substring(0, actualKey.length() - ref.length())));
- return id;
- }
- private static void finish(PrintingService service, String device, String id, String status) throws Exception {
- for(String phase : List.of("RECEIVED", "SUBMITTING", status)) service.event(device, id, new JSONObject().put("eventId", UUID.randomUUID().toString()).put("status", phase).put("message", "Synthetic receipt; no physical device or spooler used"));
- }
- private static String sha(byte[] bytes) throws Exception { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); }
- private static int objectCount() { return s3.listObjectsV2(ListObjectsV2Request.builder().bucket(bucket).build()).contents().size(); }
- private static String objectKey(String reference) {
- String known = OBJECT_KEYS.get(reference);
- if(known != null) return known;
- List matches = s3.listObjectsV2(ListObjectsV2Request.builder().bucket(bucket).build()).contents().stream()
- .map(S3Object::key).filter(key -> key.endsWith(reference)).toList();
- if(matches.size() != 1) throw new AssertionError("Expected exactly one synthetic object for stored reference");
- String key = matches.getFirst();
- String prefix = key.substring(0, key.length() - reference.length());
- if(!prefix.isEmpty() && !prefix.matches("\\\\+")) throw new AssertionError("Unexpected physical S3 key prefix");
- OBJECT_KEYS.put(reference, key);
- return key;
- }
- private static String reference(String id) throws Exception { return field(id, "content_reference"); }
- private static String status(String id) throws Exception { return field(id, "status"); }
- private static String field(String id, String field) throws Exception {
- try(var c = database.getConnection(); var statement = c.prepareStatement("SELECT " + field + " FROM printing_job WHERE id=?")) {
- statement.setString(1,id); try(var rows=statement.executeQuery()) { if(!rows.next()) throw new AssertionError("Missing synthetic job"); return rows.getString(1); }
- }
- }
- private static void execute(String sql, String... args) throws Exception {
- try(var c = database.getConnection(); var statement = c.prepareStatement(sql)) { for(int i=0; i matches = s3.listObjectsV2(ListObjectsV2Request.builder().bucket(bucket).build()).contents().stream()
+ .map(S3Object::key).filter(key -> key.endsWith(reference)).toList();
+ if(matches.size() != 1)
+ {
+ throw new AssertionError("Expected exactly one synthetic object for stored reference");
+ }
+ String key = matches.getFirst();
+ String prefix = key.substring(0, key.length() - reference.length());
+ if(!prefix.isEmpty() && !prefix.matches("\\\\+"))
+ {
+ throw new AssertionError("Unexpected physical S3 key prefix");
+ }
+ OBJECT_KEYS.put(reference, key);
+ return key;
+ }
+
+
+
+ /***************************************************************************
+ ** Read the accepted external reference for a synthetic job.
+ ***************************************************************************/
+ private static String reference(String id) throws Exception
+ {
+ return field(id, "content_reference");
+ }
+
+
+
+ /***************************************************************************
+ ** Read the lifecycle state of a synthetic job.
+ ***************************************************************************/
+ private static String status(String id) throws Exception
+ {
+ return field(id, "status");
+ }
+
+
+
+ /***************************************************************************
+ ** Read a fixture-selected field from a synthetic job.
+ ***************************************************************************/
+ private static String field(String id, String field) throws Exception
+ {
+ try(var c = database.getConnection(); var statement = c.prepareStatement("SELECT " + field + " FROM printing_job WHERE id=?"))
+ {
+ statement.setString(1, id);
+ try(var rows = statement.executeQuery())
+ {
+ if(!rows.next())
+ {
+ throw new AssertionError("Missing synthetic job");
+ }
+ return rows.getString(1);
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ ** Apply a bounded synthetic fixture adjustment with bound values.
+ ***************************************************************************/
+ private static void execute(String sql, String... args) throws Exception
+ {
+ try(var c = database.getConnection(); var statement = c.prepareStatement(sql))
+ {
+ for(int i = 0; i < args.length; i++)
+ {
+ statement.setString(i + 1, args[i]);
+ }
+ statement.executeUpdate();
+ }
+ }
+
+
+
+ /***************************************************************************
+ ** Record a successful assertion or stop qualification immediately.
+ ***************************************************************************/
+ private static void check(boolean pass, String label)
+ {
+ if(!pass)
+ {
+ throw new AssertionError(label);
+ }
+ CHECKS.put(label);
+ }
+
+
+
+ /***************************************************************************
+ ** Verify an eligible object is absent after retention.
+ ***************************************************************************/
+ private static void missing(String ref)
+ {
+ try
+ {
+ s3.headObject(HeadObjectRequest.builder().bucket(bucket).key(ref).build());
+ throw new AssertionError("Expected removed object");
+ }
+ catch(S3Exception exception)
+ {
+ check(exception.statusCode() == 404, "eligible object absent from S3 after cleanup");
+ }
+ }
+
+
+
+ /***************************************************************************
+ ** Operation that can surface a checked storage or service failure.
+ ***************************************************************************/
+ @FunctionalInterface
+ private interface Checked
+ {
+ /***************************************************************************
+ ** Execute an operation whose expected HTTP-domain failure is checked by the caller.
+ ***************************************************************************/
+ void run() throws Exception;
+ }
+
+
+
+ /***************************************************************************
+ ** Require the exact domain rejection while allowing unrelated failures to surface.
+ ***************************************************************************/
+ private static void expectStatus(int status, Checked operation, String label) throws Exception
+ {
+ try
+ {
+ operation.run();
+ throw new AssertionError(label);
+ }
+ catch(PrintingException exception)
+ {
+ check(exception.getStatus() == status, label);
+ }
+ }
}
diff --git a/dev/java/WorkloadFixture.java b/dev/java/WorkloadFixture.java
index 63bf17f..69277cd 100644
--- a/dev/java/WorkloadFixture.java
+++ b/dev/java/WorkloadFixture.java
@@ -19,94 +19,229 @@
*/
// SPDX-License-Identifier: Apache-2.0
+
package com.kingsrook.qbits.printing;
-import java.nio.file.*;
+
+
import java.nio.charset.StandardCharsets;
-import java.sql.*;
-import java.time.*;
-import java.util.*;
-import java.util.concurrent.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
-import com.kingsrook.qqq.middleware.javalin.*;
+import com.kingsrook.qqq.middleware.javalin.QApplicationLauncher;
+import com.kingsrook.qqq.middleware.javalin.QApplicationLauncherConfig;
import io.javalin.Javalin;
-import org.json.*;
-/** Fixture only: real checkout host, synthetic SDK producer, no physical printer. */
-public final class WorkloadFixture {
- public static void main(String[] args) throws Exception {
- Path dir=Path.of(args[0]).toAbsolutePath();
- if(!dir.getFileName().toString().startsWith("run-"))throw new IllegalArgumentException("Owned run directory required");
- String jdbc="jdbc:h2:file:"+dir.resolve("synthetic").toString().replace('\\','/')+";DB_CLOSE_DELAY=-1";
- String password="synthetic-"+UUID.randomUUID();
- List launches=new ArrayList<>(); List services=new ArrayList<>();
- List devices=new ArrayList<>(); JSONArray inputs=new JSONArray();
- boolean idle=args.length>1 && args[1].equals("idle"); int total=idle?40:10000; long started=System.nanoTime(); int exit=1;
- try {
- for(int tenant=0;tenant<2;tenant++) {
- PrintingLocalHost host=new PrintingLocalHost(new PrintingLocalSettings(jdbc,"sa","","tenant-"+tenant,0,"fixture-only-management-token-123456","operator",password));
- AtomicReference app=new AtomicReference<>();
- QApplicationLauncher launch=QApplicationLauncher.run(host,new QApplicationLauncherConfig().withRegisterShutdownHook(false).withServerCustomizer(server->{host.configure(server,false);server.withJavalinConfigurationCustomizer(app::set);}));
- launches.add(launch);
- PrintingService service=launch.getQInstance().getQBits().values().stream().map(v->v.getConfig()).filter(PrintingQBitConfig.class::isInstance).map(PrintingQBitConfig.class::cast).findFirst().orElseThrow().getService();
- services.add(service);
- for(int lane=0;lane<4;lane++) {
- JSONObject enrolled=service.enrollForUser(service.createEnrollment("default","Synthetic lane").put("deviceName","Synthetic lane").put("agentVersion","qualification"),"operator");
- String device=enrolled.getString("deviceId");devices.add(device);
- service.inventory(device,new JSONArray().put(new JSONObject().put("printerId","synthetic").put("name","No physical printer").put("status","READY").put("formats",new JSONArray().put("ZPL"))));service.enablePrinter(device,"synthetic",true);
- inputs.put(new JSONObject().put("origin","http://127.0.0.1:"+app.get().port()+"/").put("username","operator").put("password",password).put("tenant","tenant-"+tenant).put("deviceId",device).put("deviceToken",enrolled.getString("deviceToken")));
- }
- }
- // Only synthetic credentials inside the already protected run directory.
- Files.writeString(dir.resolve("input.json"),new JSONObject().put("lanes",inputs).put("idle",idle).put("jobsPerLane",total/8).toString());
- await(dir.resolve("ready"),Duration.ofMinutes(2));
- byte[] payload=("^XA^FO20,20^FDSYNTHETIC NOT PRINTED^FS"+" ".repeat(980)+"^XZ").getBytes(StandardCharsets.US_ASCII);
- String encoded=Base64.getEncoder().encodeToString(payload);
- long producing=System.nanoTime();
- if(idle) { for(int job=0;job> futures=new ArrayList<>();
- for(int lane=0;lane<8;lane++) { final int index=lane;
- futures.add(executor.submit(()->{try{
- PrintingService service=services.get(index/4);
- for(int job=0;job<1250;job++) {
- JSONObject request=new JSONObject().put("deviceId",devices.get(index)).put("printerId","synthetic").put("title","Synthetic workload "+job).put("format","ZPL").put("contentBase64",encoded).put("idempotencyKey","lane-"+index+"-"+job);
- JSONObject submitted=service.submit(request);
- if(job%25==0 && !submitted.getString("jobId").equals(service.submit(request).getString("jobId")))throw new IllegalStateException("Replay duplicated job");
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+
+/***************************************************************************
+ ** Fixture only: real checkout host, synthetic SDK producer, no physical printer.
+ ***************************************************************************/
+public final class WorkloadFixture
+{
+ /***************************************************************************
+ ** Drive two synthetic tenants through real network workers and verify durable receipts and accounting.
+ ***************************************************************************/
+ public static void main(String[] args) throws Exception
+ {
+ Path dir = Path.of(args[0]).toAbsolutePath();
+ if(!dir.getFileName().toString().startsWith("run-"))
+ {
+ throw new IllegalArgumentException("Owned run directory required");
}
- }catch(Exception e){throw new RuntimeException(e);}}));
- }
- for(var future:futures)future.get(15,TimeUnit.MINUTES);
+ String jdbc = "jdbc:h2:file:" + dir.resolve("synthetic").toString().replace('\\', '/') + ";DB_CLOSE_DELAY=-1";
+ String password = "synthetic-" + UUID.randomUUID();
+ List launches = new ArrayList<>();
+ List services = new ArrayList<>();
+ List devices = new ArrayList<>();
+ JSONArray inputs = new JSONArray();
+ boolean idle = args.length > 1 && args[1].equals("idle");
+ int total = idle ? 40 : 10000;
+ long started = System.nanoTime();
+ int exit = 1;
+ try
+ {
+ for(int tenant = 0; tenant < 2; tenant++)
+ {
+ PrintingLocalHost host = new PrintingLocalHost(new PrintingLocalSettings(jdbc, "sa", "", "tenant-" + tenant, 0, "fixture-only-management-token-123456", "operator", password));
+ AtomicReference app = new AtomicReference<>();
+ QApplicationLauncher launch = QApplicationLauncher.run(host, new QApplicationLauncherConfig().withRegisterShutdownHook(false).withServerCustomizer(server ->
+ {
+ host.configure(server, false);
+ server.withJavalinConfigurationCustomizer(app::set);
+ }));
+ launches.add(launch);
+ PrintingService service = launch.getQInstance().getQBits().values().stream().map(v -> v.getConfig()).filter(PrintingQBitConfig.class::isInstance).map(PrintingQBitConfig.class::cast).findFirst().orElseThrow().getService();
+ services.add(service);
+ for(int lane = 0; lane < 4; lane++)
+ {
+ JSONObject enrolled = service.enrollForUser(service.createEnrollment("default", "Synthetic lane").put("deviceName", "Synthetic lane").put("agentVersion", "qualification"), "operator");
+ String device = enrolled.getString("deviceId");
+ devices.add(device);
+ service.inventory(device, new JSONArray().put(new JSONObject().put("printerId", "synthetic").put("name", "No physical printer").put("status", "READY").put("formats", new JSONArray().put("ZPL"))));
+ service.enablePrinter(device, "synthetic", true);
+ inputs.put(new JSONObject().put("origin", "http://127.0.0.1:" + app.get().port() + "/").put("username", "operator").put("password", password).put("tenant", "tenant-" + tenant).put("deviceId", device).put("deviceToken", enrolled.getString("deviceToken")));
+ }
+ }
+ // Only synthetic credentials inside the already protected run directory.
+ Files.writeString(dir.resolve("input.json"), new JSONObject().put("lanes", inputs).put("idle", idle).put("jobsPerLane", total / 8).toString());
+ await(dir.resolve("ready"), Duration.ofMinutes(2));
+ byte[] payload = ("^XA^FO20,20^FDSYNTHETIC NOT PRINTED^FS" + " ".repeat(980) + "^XZ").getBytes(StandardCharsets.US_ASCII);
+ String encoded = Base64.getEncoder().encodeToString(payload);
+ long producing = System.nanoTime();
+ if(idle)
+ {
+ for(int job = 0; job < total; job++)
+ {
+ int lane = job % 8;
+ PrintingService service = services.get(lane / 4);
+ JSONObject request = new JSONObject().put("deviceId", devices.get(lane)).put("printerId", "synthetic").put("title", "Synthetic idle workload").put("format", "ZPL").put("contentBase64", encoded).put("idempotencyKey", "idle-" + job);
+ service.submit(request);
+ await(dir.resolve("completed-" + (job + 1)), Duration.ofSeconds(30));
+ Thread.sleep(200);
+ }
+ }
+ else
+ {
+ try(var executor = Executors.newFixedThreadPool(8))
+ {
+ List> futures = new ArrayList<>();
+ for(int lane = 0; lane < 8; lane++)
+ {
+ final int index = lane;
+ futures.add(executor.submit(() ->
+ {
+ try
+ {
+ PrintingService service = services.get(index / 4);
+ for(int job = 0; job < 1250; job++)
+ {
+ JSONObject request = new JSONObject().put("deviceId", devices.get(index)).put("printerId", "synthetic").put("title", "Synthetic workload " + job).put("format", "ZPL").put("contentBase64", encoded).put("idempotencyKey", "lane-" + index + "-" + job);
+ JSONObject submitted = service.submit(request);
+ if(job % 25 == 0 && !submitted.getString("jobId").equals(service.submit(request).getString("jobId")))
+ {
+ throw new IllegalStateException("Replay duplicated job");
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+ for(var future : futures)
+ {
+ future.get(15, TimeUnit.MINUTES);
+ }
+ }
+ }
+ double producerSeconds = (System.nanoTime() - producing) / 1e9;
+ Files.writeString(dir.resolve("producer-done"), "done");
+ await(dir.resolve("done"), Duration.ofMinutes(15));
+ JSONObject client = new JSONObject(Files.readString(dir.resolve("result.json")));
+ if(!client.getBoolean("success"))
+ {
+ throw new IllegalStateException("Client reported failure");
+ }
+ JSONObject report = new JSONObject().put("success", true).put("jobs", total).put("receipts", total * 3).put("idle", idle).put("producerSeconds", producerSeconds).put("elapsedSeconds", (System.nanoTime() - started) / 1e9).put("realPrinterCalls", 0);
+ try(Connection connection = DriverManager.getConnection(jdbc, "sa", ""); Statement query = connection.createStatement())
+ {
+ require(query, "SELECT COUNT(*) FROM printing_job", total);
+ require(query, "SELECT COUNT(*) FROM printing_job WHERE status='SPOOLED'", total);
+ require(query, "SELECT COUNT(*) FROM printing_event", total * 3);
+ require(query, "SELECT COUNT(*) FROM printing_document_orphan", 0);
+ for(int tenant = 0; tenant < 2; tenant++)
+ {
+ require(query, "SELECT COUNT(*) FROM printing_job WHERE tenant_id='tenant-" + tenant + "'", total / 2);
+ require(query, "SELECT retained_bytes FROM printing_resource_usage WHERE tenant_id='tenant-" + tenant + "' AND device_id=''", (total / 2L) * payload.length);
+ }
+ JSONArray cross = new JSONArray();
+ for(int tenant = 0; tenant < 2; tenant++)
+ {
+ String foreign = services.get(1 - tenant).jobsPage(1, null).getJSONArray("jobs").getJSONObject(0).getString("jobId");
+ try
+ {
+ services.get(tenant).getJob(foreign);
+ throw new IllegalStateException("Foreign job visible");
+ }
+ catch(PrintingException denied)
+ {
+ if(denied.getStatus() != 404)
+ {
+ throw denied;
+ }
+ }
+ cross.put(true);
+ }
+ report.put("tenantIsolation", cross).put("retainedAccountingVerified", true).put("bytesPerDocument", payload.length);
+ }
+ Files.writeString(dir.resolve("server-result.json"), report.toString(2));
+ exit = 0;
+ }
+ catch(Throwable failure)
+ {
+ Files.writeString(dir.resolve("server-error.json"), new JSONObject().put("error", failure.getClass().getName()).put("cause", failure.getCause() == null ? "" : failure.getCause().getClass().getName()).toString());
+ }
+ finally
+ {
+ for(int i = launches.size() - 1; i >= 0; i--)
+ {
+ launches.get(i).stop();
+ }
+ try(Connection connection = DriverManager.getConnection(jdbc, "sa", ""); Statement query = connection.createStatement())
+ {
+ query.execute("SHUTDOWN");
+ }
+ Files.writeString(dir.resolve("stopped.json"), new JSONObject().put("normalStop", true).put("exitCode", exit).toString());
+ }
+ System.exit(exit);
}
- double producerSeconds=(System.nanoTime()-producing)/1e9;
- Files.writeString(dir.resolve("producer-done"),"done");
- await(dir.resolve("done"),Duration.ofMinutes(15));
- JSONObject client=new JSONObject(Files.readString(dir.resolve("result.json")));
- if(!client.getBoolean("success"))throw new IllegalStateException("Client reported failure");
- JSONObject report=new JSONObject().put("success",true).put("jobs",total).put("receipts",total*3).put("idle",idle).put("producerSeconds",producerSeconds).put("elapsedSeconds",(System.nanoTime()-started)/1e9).put("realPrinterCalls",0);
- try(Connection connection=DriverManager.getConnection(jdbc,"sa","");Statement query=connection.createStatement()) {
- require(query,"SELECT COUNT(*) FROM printing_job",total);
- require(query,"SELECT COUNT(*) FROM printing_job WHERE status='SPOOLED'",total);
- require(query,"SELECT COUNT(*) FROM printing_event",total*3);
- require(query,"SELECT COUNT(*) FROM printing_document_orphan",0);
- for(int tenant=0;tenant<2;tenant++) {
- require(query,"SELECT COUNT(*) FROM printing_job WHERE tenant_id='tenant-"+tenant+"'",total/2);
- require(query,"SELECT retained_bytes FROM printing_resource_usage WHERE tenant_id='tenant-"+tenant+"' AND device_id=''",(total/2L)*payload.length);
- }
- JSONArray cross=new JSONArray();for(int tenant=0;tenant<2;tenant++) {
- String foreign=services.get(1-tenant).jobsPage(1,null).getJSONArray("jobs").getJSONObject(0).getString("jobId");
- try {services.get(tenant).getJob(foreign);throw new IllegalStateException("Foreign job visible");}catch(PrintingException denied){if(denied.getStatus()!=404)throw denied;}
- cross.put(true);
- }
- report.put("tenantIsolation",cross).put("retainedAccountingVerified",true).put("bytesPerDocument",payload.length);
+
+
+
+ /***************************************************************************
+ ** Wait for a private coordination marker with a monotonic deadline.
+ ***************************************************************************/
+ static void await(Path file, Duration limit) throws Exception
+ {
+ long deadline = System.nanoTime() + limit.toNanos();
+ while(!Files.exists(file))
+ {
+ if(System.nanoTime() > deadline)
+ {
+ throw new TimeoutException("Fixture deadline");
+ }
+ Thread.sleep(100);
+ }
+ }
+
+
+
+ /***************************************************************************
+ ** Verify a synthetic database aggregate without emitting row values.
+ ***************************************************************************/
+ static void require(Statement query, String sql, long expected) throws Exception
+ {
+ try(ResultSet rows = query.executeQuery(sql))
+ {
+ if(!rows.next() || rows.getLong(1) != expected)
+ {
+ throw new IllegalStateException("Synthetic count/accounting mismatch");
+ }
+ }
}
- Files.writeString(dir.resolve("server-result.json"),report.toString(2));exit=0;
- } catch(Throwable failure) {
- Files.writeString(dir.resolve("server-error.json"),new JSONObject().put("error",failure.getClass().getName()).put("cause",failure.getCause()==null?"":failure.getCause().getClass().getName()).toString());
- } finally {
- for(int i=launches.size()-1;i>=0;i--)launches.get(i).stop();
- try(Connection connection=DriverManager.getConnection(jdbc,"sa","");Statement query=connection.createStatement()){query.execute("SHUTDOWN");}
- Files.writeString(dir.resolve("stopped.json"),new JSONObject().put("normalStop",true).put("exitCode",exit).toString());
- }
- System.exit(exit);
- }
- static void await(Path file,Duration limit)throws Exception {long deadline=System.nanoTime()+limit.toNanos();while(!Files.exists(file)){if(System.nanoTime()>deadline)throw new TimeoutException("Fixture deadline");Thread.sleep(100);}}
- static void require(Statement query,String sql,long expected)throws Exception {try(ResultSet rows=query.executeQuery(sql)){if(!rows.next()||rows.getLong(1)!=expected)throw new IllegalStateException("Synthetic count/accounting mismatch");}}
}
diff --git a/dev/native-auth/Dockerfile.client b/dev/native-auth/Dockerfile.client
index ab03c27..dc827ec 100644
--- a/dev/native-auth/Dockerfile.client
+++ b/dev/native-auth/Dockerfile.client
@@ -7,10 +7,12 @@ ADD --checksum=sha256:82fcdbf7240760939a63b219aeb15a2333b3a8a02b52307a8a328500fc
RUN mkdir /client && tar -xzf /tmp/client.tar.gz -C /client --strip-components=1 && rm /tmp/client.tar.gz
RUN dotnet build /client/src/Qqq.PrintAgent.Core/Qqq.PrintAgent.Core.csproj -c Release -p:Platform=x64 -p:Version=0.3.1-rc.1 -p:RestoreLockedMode=true --nologo
RUN mkdir /core && cp /client/src/Qqq.PrintAgent.Core/bin/x64/Release/net10.0/Qqq.PrintAgent.Core.dll /core/
+COPY dev/Directory.Build.props dev/Directory.Packages.props dev/.editorconfig dev/global.json /fixture/dev/
WORKDIR /fixture/dev/native-auth/client
COPY dev/native-auth/client .
COPY dev/client/WorkloadDiscovery.cs /fixture/dev/client/WorkloadDiscovery.cs
-RUN dotnet build -c Release --nologo -o /out
+RUN dotnet build -c Release -p:RestoreLockedMode=true --nologo -o /out
+RUN dotnet format --verify-no-changes --severity warn --no-restore
RUN dotnet /out/NativeAuth.dll --check-discovery
FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:de4fe90a98845d75910b9769662465c84de1a934665c431279d1382b2bddc02b
diff --git a/dev/native-auth/Dockerfile.server b/dev/native-auth/Dockerfile.server
index f68c627..01bed82 100644
--- a/dev/native-auth/Dockerfile.server
+++ b/dev/native-auth/Dockerfile.server
@@ -12,10 +12,11 @@ COPY pom.xml mvnw ./
COPY .mvn .mvn
COPY checkstyle checkstyle
COPY src src
+COPY dev/java dev/java
+COPY dev/native-auth/java dev/native-auth/java
COPY LICENSE NOTICE ./
RUN chmod +x mvnw && ./mvnw -B -Dmaven.repo.local=/candidate-maven -Dqqq.version=4.1.0-native-dev-ecf0f550 -Dmaven.test.skip=true package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/runtime
-COPY dev/native-auth/java /fixture-src
-RUN mkdir /fixture-classes && javac -proc:none -cp 'target/classes:target/runtime/*' -d /fixture-classes /fixture-src/*.java
+RUN mkdir /fixture-classes && javac -proc:none -cp 'target/classes:target/runtime/*' -d /fixture-classes dev/native-auth/java/*.java
FROM eclipse-temurin:21-jdk-jammy@sha256:742daacb9316be4b6fc60258f6e4ac2a11882f6c597373c92290a57bfad0d2e0
WORKDIR /app
diff --git a/dev/native-auth/client/CallbackReceiver.cs b/dev/native-auth/client/CallbackReceiver.cs
index 0aa7138..2713378 100644
--- a/dev/native-auth/client/CallbackReceiver.cs
+++ b/dev/native-auth/client/CallbackReceiver.cs
@@ -1,18 +1,27 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-
using System.Net;
using System.Threading.Channels;
-
namespace Printing.NativeAuth;
+
+
/// Receives the actual HTTPS-proxied callback only inside this test network.
internal sealed class CallbackReceiver : IDisposable
{
private readonly HttpListener _listener = new();
+
+
+
private readonly Channel _callbacks = Channel.CreateBounded(1);
+
+
+
private readonly Uri _callback;
+
+
+ /// Receive callbacks only inside the owned native-auth container.
internal CallbackReceiver(Uri callback)
{
_callback = callback;
@@ -21,6 +30,9 @@ internal sealed class CallbackReceiver : IDisposable
_ = ReceiveAsync();
}
+
+
+ /// Accept only the configured callback path and publish one bounded result.
private async Task ReceiveAsync()
{
try
@@ -29,10 +41,12 @@ internal sealed class CallbackReceiver : IDisposable
{
HttpListenerContext context = await _listener.GetContextAsync();
Uri? request = context.Request.Url;
- bool valid = request is not null && request.AbsolutePath == _callback.AbsolutePath
- && context.Request.Headers["Host"] == _callback.Authority
- && context.Request.Headers["X-Forwarded-Proto"] == "https"
- && context.Request.HttpMethod == "GET" && request.Query.Length is > 0 and < 16384;
+ bool valid = request is not null
+ && string.Equals(request.AbsolutePath, _callback.AbsolutePath, StringComparison.Ordinal)
+ && string.Equals(context.Request.Headers["Host"], _callback.Authority, StringComparison.Ordinal)
+ && string.Equals(context.Request.Headers["X-Forwarded-Proto"], "https", StringComparison.Ordinal)
+ && string.Equals(context.Request.HttpMethod, "GET", StringComparison.Ordinal)
+ && request.Query.Length is > 0 and < 16384;
if(valid)
{
var actual = new UriBuilder(_callback) { Query = request!.Query.TrimStart('?') };
@@ -49,6 +63,13 @@ internal sealed class CallbackReceiver : IDisposable
}
}
+
+
+ /// Await the actual provider callback under the caller deadline.
internal ValueTask ReadAsync(CancellationToken token) => _callbacks.Reader.ReadAsync(token);
+
+
+
+ /// Close the owned callback listener.
public void Dispose() => _listener.Close();
}
diff --git a/dev/native-auth/client/FixtureHttp.cs b/dev/native-auth/client/FixtureHttp.cs
index 38c7ee9..7b21350 100644
--- a/dev/native-auth/client/FixtureHttp.cs
+++ b/dev/native-auth/client/FixtureHttp.cs
@@ -1,26 +1,69 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Nodes;
-
namespace Printing.NativeAuth;
+
+
/// Small bounded fixture transport; no redirects, proxy or response-body diagnostics.
internal sealed class FixtureHttp : IDisposable
{
+ /// Carry only the parsed bounded response and optional redirect location.
internal sealed record Reply(HttpStatusCode Status, JsonObject? Json, Uri? Location);
+
+
+
+ /// Record fixed classifications without credentials, URLs, or response bodies.
internal sealed record TraceEntry(string Kind, int Status, bool HasQuery, string? LocationKind, bool LocationHasQuery);
+
+
+
internal static List Trace { get; } = [];
- internal static string? LastRequestKind { get; private set; }
- internal static int? LastStatus { get; private set; }
+
+
+
+ internal static string? LastRequestKind
+ {
+ get;
+
+
+
+ private set;
+ }
+
+
+
+ internal static int? LastStatus
+ {
+ get;
+
+
+
+ private set;
+ }
+
+
+
private readonly HttpClient _client;
+
+
+
private readonly CookieContainer _cookies = new();
+
+
+
private readonly Uri _origin;
+
+
+
private readonly string? _adminToken;
+
+
+ /// Configure bounded same-origin HTTP with redirects and proxies disabled.
internal FixtureHttp(Uri origin, bool cookies = false, string? adminToken = null)
{
_origin = origin;
@@ -32,9 +75,12 @@ internal sealed class FixtureHttp : IDisposable
};
}
+
+
+ /// Send one origin-confined request and retain bounded sanitized diagnostics.
internal async Task SendAsync(string method, Uri uri, JsonObject? body, CancellationToken cancellationToken)
{
- if(uri.Scheme != "https" || uri.GetLeftPart(UriPartial.Authority) != _origin.GetLeftPart(UriPartial.Authority))
+ if(!string.Equals(uri.Scheme, "https", StringComparison.Ordinal) || !string.Equals(uri.GetLeftPart(UriPartial.Authority), _origin.GetLeftPart(UriPartial.Authority), StringComparison.Ordinal))
{
throw new InvalidOperationException("A fixture request left its configured HTTPS origin.");
}
@@ -44,7 +90,10 @@ internal sealed class FixtureHttp : IDisposable
LastStatus = null;
if(_adminToken is not null)
{
- if(!uri.AbsolutePath.StartsWith("/api/v3/", StringComparison.Ordinal)) { throw new InvalidOperationException("Administrative credential scope refused."); }
+ if(!uri.AbsolutePath.StartsWith("/api/v3/", StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("Administrative credential scope refused.");
+ }
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _adminToken);
}
if(body is not null)
@@ -52,36 +101,57 @@ internal sealed class FixtureHttp : IDisposable
request.Content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
request.Headers.Add("Origin", _origin.GetLeftPart(UriPartial.Authority));
request.Headers.Referrer = _origin;
- Cookie? csrf = _cookies.GetCookies(_origin).Cast().SingleOrDefault(cookie => cookie.Name == "authentik_csrf");
- csrf ??= _cookies.GetCookies(_origin).Cast().SingleOrDefault(cookie => cookie.Name == "csrftoken");
- if(csrf is not null) { request.Headers.Add("X-authentik-CSRF", csrf.Value); }
+ Cookie? csrf = _cookies.GetCookies(_origin).Cast().SingleOrDefault(cookie => string.Equals(cookie.Name, "authentik_csrf", StringComparison.Ordinal));
+ csrf ??= _cookies.GetCookies(_origin).Cast().SingleOrDefault(cookie => string.Equals(cookie.Name, "csrftoken", StringComparison.Ordinal));
+ if(csrf is not null)
+ {
+ request.Headers.Add("X-authentik-CSRF", csrf.Value);
+ }
}
using HttpResponseMessage response = await _client.SendAsync(request, cancellationToken);
LastStatus = (int)response.StatusCode;
Uri? location = response.Headers.Location is null ? null : new Uri(_origin, response.Headers.Location);
- if(Trace.Count == 32) { Trace.RemoveAt(0); }
+ if(Trace.Count == 32)
+ {
+ Trace.RemoveAt(0);
+ }
Trace.Add(new TraceEntry(LastRequestKind, (int)response.StatusCode, uri.Query.Length != 0,
location is null ? null : Classify(location, "GET"), location?.Query.Length > 0));
string content = await response.Content.ReadAsStringAsync(cancellationToken);
- JsonObject? json = response.Content.Headers.ContentType?.MediaType == "application/json" && content.Length != 0
+ JsonObject? json = string.Equals(response.Content.Headers.ContentType?.MediaType, "application/json", StringComparison.Ordinal) && content.Length != 0
? JsonNode.Parse(content)?.AsObject() : null;
return new Reply(response.StatusCode, json, response.Headers.Location);
}
+
+
+ /// Send one origin-confined request and retain bounded sanitized diagnostics.
internal Task SendAsync(string method, string path, JsonObject? body, CancellationToken cancellationToken)
=> SendAsync(method, new Uri(_origin, path), body, cancellationToken);
- private static string Classify(Uri uri, string method) => uri.Host == "callback.printing.internal" ? "callback"
+
+
+ /// Map an endpoint to a fixed label without exposing its path or query.
+ private static string Classify(Uri uri, string method) => string.Equals(uri.Host, "callback.printing.internal", StringComparison.Ordinal) ? "callback"
: uri.AbsolutePath.StartsWith("/application/o/authorize", StringComparison.Ordinal) ? "provider-authorization"
: uri.AbsolutePath.StartsWith("/if/flow/", StringComparison.Ordinal) ? "provider-flow-page"
: uri.AbsolutePath.StartsWith("/api/v3/flows/executor/", StringComparison.Ordinal) ? "provider-flow-executor-" + method
: "fixture-api";
+
+
+ /// Require the expected status and a parsed JSON object.
internal static JsonObject RequireJson(Reply reply, HttpStatusCode expected)
{
- if(reply.Status != expected || reply.Json is null) { throw new InvalidOperationException("Unexpected fixture API status or document shape."); }
+ if(reply.Status != expected || reply.Json is null)
+ {
+ throw new InvalidOperationException("Unexpected fixture API status or document shape.");
+ }
return reply.Json;
}
+
+
+ /// Dispose the owned HTTP client and handler.
public void Dispose() => _client.Dispose();
}
diff --git a/dev/native-auth/client/NativeAuth.csproj b/dev/native-auth/client/NativeAuth.csproj
index 4e8ac0d..93aa3ae 100644
--- a/dev/native-auth/client/NativeAuth.csproj
+++ b/dev/native-auth/client/NativeAuth.csproj
@@ -2,17 +2,15 @@
Copyright 2026 Kingsrook, LLC
SPDX-License-Identifier: Apache-2.0
-->
-
-
- Exe
- net10.0
- enable
- enable
- true
-
-
- /core/Qqq.PrintAgent.Core.dll
-
-
+
+ Exe
+ net10.0
+
+
+
+ $(CoreAssemblyPath)
+
+
+
diff --git a/dev/native-auth/client/NativeAuthDiscovery.cs b/dev/native-auth/client/NativeAuthDiscovery.cs
index 839e5f7..cd3793d 100644
--- a/dev/native-auth/client/NativeAuthDiscovery.cs
+++ b/dev/native-auth/client/NativeAuthDiscovery.cs
@@ -2,11 +2,12 @@
* Copyright 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
-
+using Printing.Development;
using Qqq.PrintAgent.Core.Authentication;
-
namespace Printing.NativeAuth;
+
+
/// Requires the pinned experimental native grant before provider authentication.
internal static class NativeAuthDiscovery
{
@@ -14,7 +15,7 @@ internal static class NativeAuthDiscovery
internal static async Task DiscoverAsync(QqqSessionClient client, CancellationToken token)
{
QqqApiDiscovery discovery = await WorkloadDiscovery.DiscoverRealtimeAsync(client, token);
- if(discovery.ApiVersion != "1.9.0" || !discovery.Features.Contains("nativeSessionLogin", StringComparer.Ordinal))
+ if(!string.Equals(discovery.ApiVersion, "1.9.0", StringComparison.Ordinal) || !discovery.Features.Contains("nativeSessionLogin", StringComparer.Ordinal))
{
throw new InvalidOperationException("The actual server did not advertise the pinned native-session API.");
}
diff --git a/dev/native-auth/client/NativeAuthDiscoveryChecks.cs b/dev/native-auth/client/NativeAuthDiscoveryChecks.cs
index 57a2d5f..abee957 100644
--- a/dev/native-auth/client/NativeAuthDiscoveryChecks.cs
+++ b/dev/native-auth/client/NativeAuthDiscoveryChecks.cs
@@ -2,17 +2,22 @@
* Copyright 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
-
using System.Net;
using System.Text.Json;
using Qqq.PrintAgent.Core.Authentication;
-
namespace Printing.NativeAuth;
+
+
/// Exercises the real native probe gate against bounded, credential-free HTTP responses.
internal static class NativeAuthDiscoveryChecks
{
/// Accepts compatible printing releases while preserving the exact upstream grant requirement.
+ private static readonly string[] s_channels = ["api"];
+
+
+
+ /// Check the native grant and compatible extension using fake discovery responses.
internal static async Task RunAsync()
{
(string PrintingVersion, string ApiVersion, bool NativeGrant, bool Accepted)[] cases =
@@ -42,12 +47,15 @@ internal static class NativeAuthDiscoveryChecks
}
catch(QqqSessionException error) when(error.Failure == QqqSessionException.FailureKind.ConfigurationRequired)
{
+ // Expected for incompatible discovery: the case stays rejected and the comparison below judges it.
}
catch(InvalidDataException)
{
+ // Expected for malformed discovery values: the case stays rejected and the comparison below judges it.
}
catch(InvalidOperationException)
{
+ // Expected when native-session discovery is absent: the case stays rejected and judged below.
}
if(accepted != expected || handler.Requests != 1)
{
@@ -63,15 +71,24 @@ internal static class NativeAuthDiscoveryChecks
private sealed class DiscoveryHandler(string printingVersion, string apiVersion, bool nativeGrant) : HttpMessageHandler
{
/// Ensures rejected metadata still traversed the public HTTP discovery boundary.
- internal int Requests { get; private set; }
+ internal int Requests
+ {
+ get;
+
+
+
+ private set;
+ }
/// Refuses any credential or request outside the public discovery boundary.
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- if(request.Method != HttpMethod.Get || request.RequestUri?.AbsoluteUri != "https://printing.example.invalid/qqq/v1/version"
- || request.Headers.Contains("Cookie") || request.Headers.Contains("Authorization")
+ if(request.Method != HttpMethod.Get
+ || !string.Equals(request.RequestUri?.AbsoluteUri, "https://printing.example.invalid/qqq/v1/version", StringComparison.Ordinal)
+ || request.Headers.Contains("Cookie")
+ || request.Headers.Contains("Authorization")
|| request.Headers.Contains("X-QQQ-Device-Id") || request.Headers.Contains("X-QQQ-Device-Token"))
{
throw new InvalidOperationException("Native discovery must precede authentication and device proof.");
@@ -84,7 +101,7 @@ internal static class NativeAuthDiscoveryChecks
{
apiVersion,
features,
- clientChannels = new[] { "api" },
+ clientChannels = s_channels,
extensions = new[] { new { name = "printing", version = printingVersion, pathPrefix = "/qbit/printing" } }
});
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json) });
diff --git a/dev/native-auth/client/Program.cs b/dev/native-auth/client/Program.cs
index a61fe33..112958d 100644
--- a/dev/native-auth/client/Program.cs
+++ b/dev/native-auth/client/Program.cs
@@ -1,26 +1,55 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-
using System.Net;
using System.Security.Authentication;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Qqq.PrintAgent.Core.Authentication;
-
namespace Printing.NativeAuth;
+
+
+/// Qualify real provider sessions only in the selected experimental environment.
internal static class Program
{
private const string CoreSource = "ecf0f550a0357cec6387c2b54efca128a405a629";
- private const string ClientSource = "346fc285198908c47a2ee9db7b9adb53d5e88c3c";
- private const string InputPath = "/work/input.json";
- private const string ResultPath = "/work/result.json";
- private const string TlsMarker = "/tmp/printing-native-untrusted-tls-passed";
- private static readonly List Checks = [];
- private static string _stage = "input";
- private static bool? _authorizationRequestMatches;
+
+
+ private const string ClientSource = "346fc285198908c47a2ee9db7b9adb53d5e88c3c";
+
+
+
+ private const string InputPath = "/work/input.json";
+
+
+
+ private const string ResultPath = "/work/result.json";
+
+
+
+ private const string TlsMarker = "/tmp/printing-native-untrusted-tls-passed";
+
+
+
+ private static readonly JsonSerializerOptions s_reportOptions = new() { WriteIndented = true };
+
+
+
+ private static readonly List s_checks = [];
+
+
+
+ private static string s_stage = "input";
+
+
+
+ private static bool? s_authorizationRequestMatches;
+
+
+
+ /// Validate synthetic input and record bounded evidence without credential output.
public static async Task Main(string[] arguments)
{
if(arguments is ["--check-discovery"])
@@ -32,21 +61,28 @@ internal static class Program
try
{
if(arguments.Length != 1 || arguments[0] is not ("assert-untrusted" or "test"))
- { throw new InvalidOperationException("An explicit fixture action is required."); }
+ {
+ throw new InvalidOperationException("An explicit fixture action is required.");
+ }
if(new FileInfo(InputPath).Length is <= 0 or > 65536)
- { throw new InvalidOperationException("Fixture input size is invalid."); }
+ {
+ throw new InvalidOperationException("Fixture input size is invalid.");
+ }
input = JsonNode.Parse(await File.ReadAllTextAsync(InputPath))!.AsObject();
ValidateInput(input);
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(5));
- if(arguments[0] == "assert-untrusted")
+ if(string.Equals(arguments[0], "assert-untrusted", StringComparison.Ordinal))
{
- _stage = "untrusted-tls";
+ s_stage = "untrusted-tls";
await AssertUntrustedAsync(new Uri(ProviderSetup.Text(input, "providerOrigin")), deadline.Token);
await File.WriteAllTextAsync(TlsMarker, "passed", deadline.Token);
return 0;
}
- if(!File.Exists(TlsMarker)) { throw new InvalidOperationException("The ordinary TLS negative control did not run."); }
- Checks.Add("untrusted-tls-refused");
+ if(!File.Exists(TlsMarker))
+ {
+ throw new InvalidOperationException("The ordinary TLS negative control did not run.");
+ }
+ s_checks.Add("untrusted-tls-refused");
await QualifyAsync(input, deadline.Token);
await WriteResultAsync(input, "PASS", null, null);
Console.WriteLine("Experimental native Authentik qualification passed; development-only unpublished core.");
@@ -57,158 +93,221 @@ internal static class Program
// Exception messages, response bodies, callback URLs and credentials are deliberately withheld.
await WriteResultAsync(input, "FAILED", exception.GetType().Name,
exception is QqqSessionException sessionException ? sessionException.Failure.ToString() : null);
- Console.Error.WriteLine("Experimental native Authentik qualification failed at " + _stage + " (" + exception.GetType().Name + ").");
+ Console.Error.WriteLine("Experimental native Authentik qualification failed at " + s_stage + " (" + exception.GetType().Name + ").");
return 1;
}
}
+
+
+ /// Refuse fixture identities, origins, or source tuples outside the pinned contract.
private static void ValidateInput(JsonObject input)
{
- if(!Regex.IsMatch(ProviderSetup.Text(input, "project"), "^qbit-printing-dev-[a-f0-9]{32}$", RegexOptions.CultureInvariant)
- || ProviderSetup.Text(input, "coreSource") != CoreSource || ProviderSetup.Text(input, "clientSource") != ClientSource
- || ProviderSetup.Text(input, "providerOrigin") != "https://auth.printing.internal"
- || ProviderSetup.Text(input, "qqqOrigin") != "https://qqq.printing.internal"
- || ProviderSetup.Text(input, "callback") != "https://callback.printing.internal/oauth/callback"
- || ProviderSetup.Text(input, "issuer") != "https://auth.printing.internal/application/o/" + ProviderSetup.Text(input, "project") + "/"
- || ProviderSetup.Text(input, "clientId") != ProviderSetup.Text(input, "project"))
- { throw new InvalidOperationException("Fixture identity or destination differs from the pinned local contract."); }
+ if(!Regex.IsMatch(ProviderSetup.Text(input, "project"), "^qbit-printing-dev-[a-f0-9]{32}$", RegexOptions.CultureInvariant | RegexOptions.NonBacktracking)
+ || !string.Equals(ProviderSetup.Text(input, "coreSource"), CoreSource, StringComparison.Ordinal)
+ || !string.Equals(ProviderSetup.Text(input, "clientSource"), ClientSource, StringComparison.Ordinal)
+ || !string.Equals(ProviderSetup.Text(input, "providerOrigin"), "https://auth.printing.internal", StringComparison.Ordinal)
+ || !string.Equals(ProviderSetup.Text(input, "qqqOrigin"), "https://qqq.printing.internal", StringComparison.Ordinal)
+ || !string.Equals(ProviderSetup.Text(input, "callback"), "https://callback.printing.internal/oauth/callback", StringComparison.Ordinal)
+ || !string.Equals(ProviderSetup.Text(input, "issuer"), "https://auth.printing.internal/application/o/" + ProviderSetup.Text(input, "project") + "/", StringComparison.Ordinal)
+ || !string.Equals(ProviderSetup.Text(input, "clientId"), ProviderSetup.Text(input, "project"), StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("Fixture identity or destination differs from the pinned local contract.");
+ }
}
+
+
+ /// Create the production session client with redirects, cookies, and proxies controlled.
private static QqqSessionClient Session(Uri origin) => new(origin,
new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false, UseProxy = false });
+
+
+ /// Require TLS to reject the private CA before scoped trust is installed.
private static async Task AssertUntrustedAsync(Uri origin, CancellationToken token)
{
using var transport = new FixtureHttp(origin);
- try { await transport.SendAsync("GET", "/-/health/live/", null, token); }
+ try
+ {
+ await transport.SendAsync("GET", "/-/health/live/", null, token);
+ }
catch(HttpRequestException exception)
{
for(Exception? cause = exception; cause is not null; cause = cause.InnerException)
{
- if(cause is AuthenticationException) { return; }
+ if(cause is AuthenticationException)
+ {
+ return;
+ }
}
- throw new InvalidOperationException("The negative TLS control failed for a reason other than certificate authentication.");
+ throw new InvalidOperationException("The negative TLS control failed for a reason other than certificate authentication.", exception);
}
throw new InvalidOperationException("The test unexpectedly trusted its private CA before scoped installation.");
}
+
+
+ /// Exercise real discovery, authorization, origin enforcement, logout, renewal, and revocation.
private static async Task QualifyAsync(JsonObject input, CancellationToken token)
{
var origin = new Uri(ProviderSetup.Text(input, "qqqOrigin"));
var providerOrigin = new Uri(ProviderSetup.Text(input, "providerOrigin"));
var callback = new Uri(ProviderSetup.Text(input, "callback"));
- _stage = "readiness";
+ s_stage = "readiness";
await WaitForQqqAsync(origin, token);
using var provider = new ProviderSetup(input);
- _stage = "provider-readiness";
+ s_stage = "provider-readiness";
await provider.WaitReadyAsync(token);
- _stage = "provider-configuration";
+ s_stage = "provider-configuration";
await provider.ConfigureAsync(token);
using var receiver = new CallbackReceiver(callback);
- using var first = Session(origin);
- _stage = "typed-native-discovery";
+ using QqqSessionClient first = Session(origin);
+ s_stage = "typed-native-discovery";
await NativeAuthDiscovery.DiscoverAsync(first, token);
QqqAuthenticationMetadata metadata = await first.GetAuthenticationMetadataAsync(token);
- if(metadata.Values?.ClientId != ProviderSetup.Text(input, "clientId"))
- { throw new InvalidOperationException("Native metadata did not retain the configured client identifier."); }
- Checks.Add("typed-native-grant");
-
- _stage = "real-provider-login";
+ if(!string.Equals(metadata.Values?.ClientId, ProviderSetup.Text(input, "clientId"), StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("Native metadata did not retain the configured client identifier.");
+ }
+ s_checks.Add("typed-native-grant");
+ s_stage = "real-provider-login";
await LoginAsync(first, receiver, input, providerOrigin, callback, authorized: true, token);
- _stage = "authorized-native-identity";
+ s_stage = "authorized-native-identity";
QqqSessionBinding identity = await first.GetSessionIdentityAsync(token);
- if(string.IsNullOrWhiteSpace(identity.PrincipalId) || identity.TenantId != "printing-native-dev" || identity.DeviceId is not null)
- { throw new InvalidOperationException("The native subject did not receive the intended server-derived identity."); }
- Checks.Add("real-provider-login");
- _stage = "wrong-origin-cookie";
+ if(string.IsNullOrWhiteSpace(identity.PrincipalId) || !string.Equals(identity.TenantId, "printing-native-dev", StringComparison.Ordinal) || identity.DeviceId is not null)
+ {
+ throw new InvalidOperationException("The native subject did not receive the intended server-derived identity.");
+ }
+ s_checks.Add("real-provider-login");
+ s_stage = "wrong-origin-cookie";
await AssertWrongOriginAsync(origin, first.ExportCredential(), token);
await first.GetSessionIdentityAsync(token);
- Checks.Add("wrong-origin-cookie-denied");
-
- _stage = "permission-denied-user";
- using(var denied = Session(origin))
+ s_checks.Add("wrong-origin-cookie-denied");
+ s_stage = "permission-denied-user";
+ using(QqqSessionClient denied = Session(origin))
{
await LoginAsync(denied, receiver, input, providerOrigin, callback, authorized: false, token);
await ExpectFailureAsync(() => denied.GetSessionIdentityAsync(token), QqqSessionException.FailureKind.PermissionDenied);
await denied.SignOutAsync(token);
}
- Checks.Add("permission-denied-user");
+ s_checks.Add("permission-denied-user");
+ await AssertLogoutReplayAsync(first, origin, token);
+ await QualifyRenewalAsync(input, provider, receiver, origin, providerOrigin, callback, token);
+ }
- _stage = "logout-replay";
+
+
+ /// Verify that signing out invalidates the exact previously exported native cookie.
+ private static async Task AssertLogoutReplayAsync(QqqSessionClient first, Uri origin, CancellationToken token)
+ {
+ s_stage = "logout-replay";
QqqSessionCredential retired = first.ExportCredential();
await first.SignOutAsync(token);
- using(var replay = Session(origin))
- { await ExpectFailureAsync(() => replay.RestoreSessionAsync(retired, token), QqqSessionException.FailureKind.AuthenticationRequired); }
- Checks.Add("logout-replay-denied");
+ using(QqqSessionClient replay = Session(origin))
+ {
+ await ExpectFailureAsync(() => replay.RestoreSessionAsync(retired, token), QqqSessionException.FailureKind.AuthenticationRequired);
+ }
+ s_checks.Add("logout-replay-denied");
+ }
- _stage = "fresh-login-for-renewal";
- using var renewable = Session(origin);
+
+
+ /// Cross real provider expiry and revocation while preserving the authenticated subject.
+ private static async Task QualifyRenewalAsync(JsonObject input, ProviderSetup provider, CallbackReceiver receiver,
+ Uri origin, Uri providerOrigin, Uri callback, CancellationToken token)
+ {
+ s_stage = "fresh-login-for-renewal";
+ using QqqSessionClient renewable = Session(origin);
await LoginAsync(renewable, receiver, input, providerOrigin, callback, authorized: true, token);
QqqSessionBinding before = await renewable.GetSessionIdentityAsync(token);
HashSet previousGrants = await provider.AccessGrantIdsAsync(token);
- _stage = "server-refresh-after-expiry";
+ s_stage = "server-refresh-after-expiry";
await Task.Delay(TimeSpan.FromSeconds(22), token);
await renewable.ResumeSessionAsync(token);
QqqSessionBinding after = await renewable.GetSessionIdentityAsync(token);
HashSet refreshedGrants = await provider.AccessGrantIdsAsync(token);
if(before != after || !refreshedGrants.Except(previousGrants, StringComparer.Ordinal).Any())
- { throw new InvalidOperationException("Renewal did not preserve the identity and obtain a new actual provider grant."); }
- Checks.Add("server-refresh-after-expiry");
-
- _stage = "provider-revocation-after-expiry";
+ {
+ throw new InvalidOperationException("Renewal did not preserve the identity and obtain a new actual provider grant.");
+ }
+ s_checks.Add("server-refresh-after-expiry");
+ s_stage = "provider-revocation-after-expiry";
await provider.RevokeRefreshGrantsAsync(token);
await Task.Delay(TimeSpan.FromSeconds(22), token);
await ExpectFailureAsync(() => renewable.ResumeSessionAsync(token), QqqSessionException.FailureKind.AuthenticationRequired);
- Checks.Add("provider-revocation-denied");
- _stage = "complete";
+ s_checks.Add("provider-revocation-denied");
+ s_stage = "complete";
}
+
+
+ /// Use the real PKCE attempt and provider callback without synthesizing identity.
private static async Task LoginAsync(QqqSessionClient client, CallbackReceiver receiver, JsonObject input,
Uri providerOrigin, Uri callback, bool authorized, CancellationToken token)
{
string role = authorized ? "authorized" : "denied";
- _stage = role + "-begin-pkce";
+ s_stage = role + "-begin-pkce";
QqqBrowserLogin attempt = await client.BeginBrowserLoginAsync(callback, token);
- _stage = role + "-generated-authorization-request";
- Dictionary parameters = attempt.AuthorizationUri.Query.TrimStart('?').Split('&')
+ s_stage = role + "-generated-authorization-request";
+ var parameters = attempt.AuthorizationUri.Query.TrimStart('?').Split('&')
.Select(entry => entry.Split('=', 2)).ToDictionary(pair => Uri.UnescapeDataString(pair[0]), pair => Uri.UnescapeDataString(pair[1]), StringComparer.Ordinal);
- _authorizationRequestMatches = attempt.AuthorizationUri.AbsolutePath == "/application/o/authorize/"
- && parameters.GetValueOrDefault("client_id") == ProviderSetup.Text(input, "clientId")
- && parameters.GetValueOrDefault("redirect_uri") == callback.AbsoluteUri
- && parameters.GetValueOrDefault("response_type") == "code"
- && parameters.GetValueOrDefault("code_challenge_method") == "S256"
+ s_authorizationRequestMatches = string.Equals(attempt.AuthorizationUri.AbsolutePath, "/application/o/authorize/", StringComparison.Ordinal)
+ && string.Equals(parameters.GetValueOrDefault("client_id"), ProviderSetup.Text(input, "clientId"), StringComparison.Ordinal)
+ && string.Equals(parameters.GetValueOrDefault("redirect_uri"), callback.AbsoluteUri, StringComparison.Ordinal)
+ && string.Equals(parameters.GetValueOrDefault("response_type"), "code", StringComparison.Ordinal)
+ && string.Equals(parameters.GetValueOrDefault("code_challenge_method"), "S256", StringComparison.Ordinal)
&& parameters.GetValueOrDefault("state")?.Length == 43;
- if(_authorizationRequestMatches != true)
- { throw new InvalidOperationException("The generated authorization request differs from the exact provider registration."); }
- _stage = role + "-provider-flow";
+ if(s_authorizationRequestMatches != true)
+ {
+ throw new InvalidOperationException("The generated authorization request differs from the exact provider registration.");
+ }
+ s_stage = role + "-provider-flow";
await ProviderFlow.AuthenticateAsync(attempt.AuthorizationUri, providerOrigin, callback,
ProviderSetup.Text(input, role + "Username"), ProviderSetup.Text(input, role + "Password"), token);
- _stage = role + "-callback";
+ s_stage = role + "-callback";
Uri actualCallback = await receiver.ReadAsync(token);
- _stage = role + "-exchange";
+ s_stage = role + "-exchange";
await client.CompleteBrowserLoginAsync(attempt, actualCallback, token);
}
+
+
+ /// Require the precise native authentication or permission failure.
private static async Task ExpectFailureAsync(Func operation, QqqSessionException.FailureKind expected)
{
- try { await operation(); }
+ try
+ {
+ await operation();
+ }
catch(QqqSessionException exception) when(exception.Failure == expected) { return; }
throw new InvalidOperationException("The expected native authentication or permission denial did not occur.");
}
+
+
+ /// Verify the authenticated native boundary rejects a foreign Origin.
private static async Task AssertWrongOriginAsync(Uri origin, QqqSessionCredential credential, CancellationToken token)
{
using var http = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false, UseProxy = false })
- { Timeout = TimeSpan.FromSeconds(15), MaxResponseContentBufferSize = 65536 };
+ {
+ Timeout = TimeSpan.FromSeconds(15),
+ MaxResponseContentBufferSize = 65536
+ };
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(origin, "/qqq/v1/qbit/printing/session"));
request.Headers.Add("X-QQQ-Client", QqqSessionClient.ClientChannel);
request.Headers.Add("Cookie", "sessionUUID=" + credential.SessionToken);
request.Headers.Add("Origin", "https://wrong-origin.invalid");
using HttpResponseMessage response = await http.SendAsync(request, token);
if(response.StatusCode != HttpStatusCode.Forbidden)
- { throw new InvalidOperationException("The fixture did not reject a wrong-Origin cookie request."); }
+ {
+ throw new InvalidOperationException("The fixture did not reject a wrong-Origin cookie request.");
+ }
}
+
+
+ /// Bound startup readiness reads to the owned QQQ host.
private static async Task WaitForQqqAsync(Uri origin, CancellationToken token)
{
using var http = new FixtureHttp(origin);
@@ -217,25 +316,45 @@ internal static class Program
try
{
FixtureHttp.Reply response = await http.SendAsync("GET", "/qqq/v1/version", null, token);
- if(response.Status == HttpStatusCode.OK && response.Json is not null) { return; }
+ if(response.Status == HttpStatusCode.OK && response.Json is not null)
+ {
+ return;
+ }
+ }
+ catch(HttpRequestException)
+ {
+ // The owned host is still starting; the bounded attempt loop retries and then fails explicitly.
}
- catch(HttpRequestException) { }
await Task.Delay(TimeSpan.FromSeconds(2), token);
}
throw new InvalidOperationException("The owned native host did not become ready.");
}
+
+
+ /// Write only allowlisted classifications and results to the private report.
private static Task WriteResultAsync(JsonObject? input, string status, string? failureType, string? sessionFailure)
=> File.WriteAllTextAsync(ResultPath, JsonSerializer.Serialize(new
{
- status, scope = "DEVELOPMENT_ONLY_UNRELEASED", coreSource = CoreSource, clientSource = ClientSource,
- project = input?["project"]?.ToString(), observedUtc = DateTimeOffset.UtcNow, stage = _stage, failureType,
- sessionFailure, lastRequestKind = FixtureHttp.LastRequestKind, lastHttpStatus = FixtureHttp.LastStatus,
+ status,
+ scope = "DEVELOPMENT_ONLY_UNRELEASED",
+ coreSource = CoreSource,
+ clientSource = ClientSource,
+ project = input?["project"]?.ToString(),
+ observedUtc = DateTimeOffset.UtcNow,
+ stage = s_stage,
+ failureType,
+ sessionFailure,
+ lastRequestKind = FixtureHttp.LastRequestKind,
+ lastHttpStatus = FixtureHttp.LastStatus,
lastProviderComponent = ProviderFlow.LastComponent,
- authorizationRequestMatches = _authorizationRequestMatches,
+ authorizationRequestMatches = s_authorizationRequestMatches,
requestClassifications = FixtureHttp.Trace,
- providerLookupKind = ProviderSetup.LastLookupKind, providerLookupCount = ProviderSetup.LastLookupCount,
+ providerLookupKind = ProviderSetup.LastLookupKind,
+ providerLookupCount = ProviderSetup.LastLookupCount,
providerReadinessCounts = ProviderSetup.ReadinessCounts,
- checks = Checks, installedUiAcceptance = false, providerRefreshTokensInClient = false,
- }, new JsonSerializerOptions { WriteIndented = true }));
+ checks = s_checks,
+ installedUiAcceptance = false,
+ providerRefreshTokensInClient = false,
+ }, s_reportOptions));
}
diff --git a/dev/native-auth/client/ProviderFlow.cs b/dev/native-auth/client/ProviderFlow.cs
index 0ac26d5..81222c1 100644
--- a/dev/native-auth/client/ProviderFlow.cs
+++ b/dev/native-auth/client/ProviderFlow.cs
@@ -1,15 +1,26 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-
using System.Net;
using System.Text.Json.Nodes;
-
namespace Printing.NativeAuth;
+
+
/// Uses Authentik's real synthetic-user login stages; never fabricates a session or code.
internal static class ProviderFlow
{
- internal static string? LastComponent { get; private set; }
+ internal static string? LastComponent
+ {
+ get;
+
+
+
+ private set;
+ }
+
+
+
+ /// Follow bounded provider navigation while confining redirects to fixture origins.
internal static async Task AuthenticateAsync(Uri authorization, Uri origin, Uri callback, string username, string password, CancellationToken token)
{
using var browser = new FixtureHttp(origin, cookies: true);
@@ -17,15 +28,23 @@ internal static class ProviderFlow
Uri next = authorization;
for(int hop = 0; hop < 24; hop++)
{
- if(next.GetLeftPart(UriPartial.Authority) == callback.GetLeftPart(UriPartial.Authority))
+ if(string.Equals(next.GetLeftPart(UriPartial.Authority), callback.GetLeftPart(UriPartial.Authority), StringComparison.Ordinal))
{
- if(next.AbsolutePath != callback.AbsolutePath) { throw new InvalidOperationException("Unexpected callback path."); }
+ if(!string.Equals(next.AbsolutePath, callback.AbsolutePath, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("Unexpected callback path.");
+ }
FixtureHttp.Reply result = await callbackHttp.SendAsync("GET", next, null, token);
- if(result.Status != HttpStatusCode.OK) { throw new InvalidOperationException("The HTTPS callback receiver refused the provider redirect."); }
+ if(result.Status != HttpStatusCode.OK)
+ {
+ throw new InvalidOperationException("The HTTPS callback receiver refused the provider redirect.");
+ }
return;
}
- if(next.GetLeftPart(UriPartial.Authority) != origin.GetLeftPart(UriPartial.Authority))
- { throw new InvalidOperationException("A provider redirect left the fixture."); }
+ if(!string.Equals(next.GetLeftPart(UriPartial.Authority), origin.GetLeftPart(UriPartial.Authority), StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("A provider redirect left the fixture.");
+ }
FixtureHttp.Reply page = await browser.SendAsync("GET", next, null, token);
if(page.Status is HttpStatusCode.Redirect or HttpStatusCode.SeeOther or HttpStatusCode.TemporaryRedirect)
{
@@ -33,61 +52,82 @@ internal static class ProviderFlow
continue;
}
if(page.Status != HttpStatusCode.OK || !next.AbsolutePath.StartsWith("/if/flow/", StringComparison.Ordinal))
- { throw new InvalidOperationException("Unexpected provider login navigation."); }
+ {
+ throw new InvalidOperationException("Unexpected provider login navigation.");
+ }
string slug = next.AbsolutePath["/if/flow/".Length..].Trim('/');
if(slug.Length == 0 || slug.Any(character => !char.IsAsciiLetterOrDigit(character) && character != '-'))
- { throw new InvalidOperationException("Unexpected provider flow identifier."); }
- var executor = new Uri(origin, "/api/v3/flows/executor/" + slug + "/?query=" + Uri.EscapeDataString(next.Query.TrimStart('?')));
- JsonObject challenge = await ChallengeAsync(browser, executor, await browser.SendAsync("GET", executor, null, token), token);
- bool redirected = false;
- for(int stage = 0; stage < 10; stage++)
{
- string component = ProviderSetup.Text(challenge, "component");
- LastComponent = component switch
- {
- "ak-stage-identification" => "identification",
- "ak-stage-password" => "password",
- "ak-stage-consent" => "consent",
- "xak-flow-redirect" => "redirect",
- "ak-stage-access-denied" => "access-denied",
- "ak-stage-flow-error" => "flow-error",
- _ => "unsupported",
- };
- if(component == "xak-flow-redirect")
- {
- next = new Uri(origin, ProviderSetup.Text(challenge, "to"));
- redirected = true;
- break;
- }
- JsonObject response = component switch
- {
- "ak-stage-identification" => new JsonObject { ["component"] = component, ["uid_field"] = username },
- "ak-stage-password" => new JsonObject { ["component"] = component, ["password"] = password },
- "ak-stage-consent" => new JsonObject { ["component"] = component, ["consent"] = true },
- _ => throw new InvalidOperationException("The provider requested an unsupported test login stage."),
- };
- challenge = await ChallengeAsync(browser, executor, await browser.SendAsync("POST", executor, response, token), token);
- if(challenge["response_errors"] is JsonObject errors && errors.Count != 0)
- { throw new InvalidOperationException("The real provider rejected the synthetic login response."); }
+ throw new InvalidOperationException("Unexpected provider flow identifier.");
}
- if(!redirected) { throw new InvalidOperationException("Provider stage bound exceeded."); }
+ var executor = new Uri(origin, "/api/v3/flows/executor/" + slug + "/?query=" + Uri.EscapeDataString(next.Query.TrimStart('?')));
+ next = await CompleteStagesAsync(browser, executor, origin, username, password, token);
}
throw new InvalidOperationException("Provider redirect bound exceeded.");
}
+
+
+ /// Answer at most ten real provider challenges and return its declared redirect.
+ private static async Task CompleteStagesAsync(FixtureHttp browser, Uri executor, Uri origin, string username, string password, CancellationToken token)
+ {
+ JsonObject challenge = await ChallengeAsync(browser, executor, await browser.SendAsync("GET", executor, null, token), token);
+ for(int stage = 0; stage < 10; stage++)
+ {
+ string component = ProviderSetup.Text(challenge, "component");
+ LastComponent = component switch
+ {
+ "ak-stage-identification" => "identification",
+ "ak-stage-password" => "password",
+ "ak-stage-consent" => "consent",
+ "xak-flow-redirect" => "redirect",
+ "ak-stage-access-denied" => "access-denied",
+ "ak-stage-flow-error" => "flow-error",
+ _ => "unsupported",
+ };
+ if(string.Equals(component, "xak-flow-redirect", StringComparison.Ordinal))
+ {
+ return new Uri(origin, ProviderSetup.Text(challenge, "to"));
+ }
+ JsonObject response = component switch
+ {
+ "ak-stage-identification" => new JsonObject { ["component"] = component, ["uid_field"] = username },
+ "ak-stage-password" => new JsonObject { ["component"] = component, ["password"] = password },
+ "ak-stage-consent" => new JsonObject { ["component"] = component, ["consent"] = true },
+ _ => throw new InvalidOperationException("The provider requested an unsupported test login stage."),
+ };
+ challenge = await ChallengeAsync(browser, executor, await browser.SendAsync("POST", executor, response, token), token);
+ if(challenge["response_errors"] is JsonObject errors && errors.Count != 0)
+ {
+ throw new InvalidOperationException("The real provider rejected the synthetic login response.");
+ }
+ }
+ throw new InvalidOperationException("Provider stage bound exceeded.");
+ }
+
+
+
+ /// Follow only same-executor POST/redirect/GET responses and require a JSON challenge.
private static async Task ChallengeAsync(FixtureHttp browser, Uri executor, FixtureHttp.Reply response, CancellationToken token)
{
// Authentik uses POST/redirect/GET when a real stage accepts its answer.
// Follow only that same HTTPS executor; other navigation uses typed flow redirects.
for(int redirect = 0; redirect < 8; redirect++)
{
- if(response.Status == HttpStatusCode.OK) { return FixtureHttp.RequireJson(response, HttpStatusCode.OK); }
+ if(response.Status == HttpStatusCode.OK)
+ {
+ return FixtureHttp.RequireJson(response, HttpStatusCode.OK);
+ }
if(response.Status is not (HttpStatusCode.Redirect or HttpStatusCode.SeeOther) || response.Location is null)
- { throw new InvalidOperationException("Unexpected provider stage response."); }
+ {
+ throw new InvalidOperationException("Unexpected provider stage response.");
+ }
var location = new Uri(executor, response.Location);
- if(location.GetLeftPart(UriPartial.Authority) != executor.GetLeftPart(UriPartial.Authority)
- || location.AbsolutePath != executor.AbsolutePath)
- { throw new InvalidOperationException("A stage redirect left its exact HTTPS executor."); }
+ if(!string.Equals(location.GetLeftPart(UriPartial.Authority), executor.GetLeftPart(UriPartial.Authority), StringComparison.Ordinal)
+ || !string.Equals(location.AbsolutePath, executor.AbsolutePath, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("A stage redirect left its exact HTTPS executor.");
+ }
response = await browser.SendAsync("GET", location, null, token);
}
throw new InvalidOperationException("Provider stage redirect bound exceeded.");
diff --git a/dev/native-auth/client/ProviderSetup.cs b/dev/native-auth/client/ProviderSetup.cs
index 226f617..9288b21 100644
--- a/dev/native-auth/client/ProviderSetup.cs
+++ b/dev/native-auth/client/ProviderSetup.cs
@@ -1,43 +1,93 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
-
using System.Net;
using System.Text.Json.Nodes;
-
namespace Printing.NativeAuth;
+
+
/// Configures only the fresh project's synthetic provider and subjects.
internal sealed class ProviderSetup : IDisposable
{
- internal static string? LastLookupKind { get; private set; }
- internal static int? LastLookupCount { get; private set; }
+ internal static string? LastLookupKind
+ {
+ get;
+
+
+
+ private set;
+ }
+
+
+
+ internal static int? LastLookupCount
+ {
+ get;
+
+
+
+ private set;
+ }
+
+
+
internal static Dictionary ReadinessCounts { get; } = new(StringComparer.Ordinal);
+
+
+
private readonly JsonObject _input;
+
+
+
private readonly FixtureHttp _admin;
+
+
+
private string _provider = "";
+
+
+
private string _authorizedUser = "";
+
+
+ /// Use the private bootstrap token only with the configured provider origin.
internal ProviderSetup(JsonObject input)
{
_input = input;
_admin = new FixtureHttp(new Uri(Text(input, "providerOrigin")), adminToken: Text(input, "adminToken"));
}
+
+
+ /// Read required synthetic input without disclosing its value in failures.
internal static string Text(JsonObject document, string key) => document[key]?.ToString() ?? throw new InvalidOperationException("Required fixture field is absent.");
+
+
+ /// Require one provider metadata object and retain only its lookup classification.
private async Task ExactAsync(string path, CancellationToken token)
{
JsonObject page = FixtureHttp.RequireJson(await _admin.SendAsync("GET", path, null, token), HttpStatusCode.OK);
JsonArray records = page["results"]!.AsArray();
LastLookupKind = LookupKind(path);
LastLookupCount = records.Count;
- if(records.Count != 1) { throw new InvalidOperationException("Required provider object is absent or ambiguous."); }
+ if(records.Count != 1)
+ {
+ throw new InvalidOperationException("Required provider object is absent or ambiguous.");
+ }
return records[0]!.AsObject();
}
+
+
+ /// Create one synthetic object and require its successful JSON response.
private async Task CreateAsync(string path, JsonObject body, CancellationToken token)
=> FixtureHttp.RequireJson(await _admin.SendAsync("POST", path, body, token), HttpStatusCode.Created);
+
+
+ /// Wait for complete default flows and scopes before configuring the provider.
internal async Task WaitReadyAsync(CancellationToken token)
{
string[] paths = ["/api/v3/flows/instances/?slug=default-authentication-flow",
@@ -58,20 +108,34 @@ internal sealed class ProviderSetup : IDisposable
JsonArray? records = reply.Status == HttpStatusCode.OK ? reply.Json?["results"] as JsonArray : null;
string kind = LookupKind(path);
ReadinessCounts[kind] = records?.Count ?? -1;
- if(records?.Count > 1) { throw new InvalidOperationException("An owned provider bootstrap dependency is ambiguous."); }
+ if(records?.Count > 1)
+ {
+ throw new InvalidOperationException("An owned provider bootstrap dependency is ambiguous.");
+ }
bool present = records is { Count: 1 };
- if(kind == "authentication-flow")
- { present &= records is { Count: 1 } && records[0]?["stages"] is JsonArray { Count: >= 4 }; }
+ if(string.Equals(kind, "authentication-flow", StringComparison.Ordinal))
+ {
+ present &= records is { Count: 1 } && records[0]?["stages"] is JsonArray { Count: >= 4 };
+ }
ready &= present;
}
- if(ready) { return; }
+ if(ready)
+ {
+ return;
+ }
+ }
+ catch(HttpRequestException)
+ {
+ // The owned provider is still starting; the bounded attempt loop retries and then fails explicitly.
}
- catch(HttpRequestException) { }
await Task.Delay(TimeSpan.FromSeconds(2), token);
}
throw new InvalidOperationException("The owned provider bootstrap did not become ready.");
}
+
+
+ /// Classify prerequisite lookups using fixed diagnostic names.
private static string LookupKind(string path) => path switch
{
"/api/v3/flows/instances/?slug=default-authentication-flow" => "authentication-flow",
@@ -84,6 +148,9 @@ internal sealed class ProviderSetup : IDisposable
_ => "other",
};
+
+
+ /// Register the synthetic OAuth provider and authorized and denied users.
internal async Task ConfigureAsync(CancellationToken token)
{
JsonObject authentication = await ExactAsync("/api/v3/flows/instances/?slug=default-authentication-flow", token);
@@ -125,60 +192,97 @@ internal sealed class ProviderSetup : IDisposable
["include_claims_in_id_token"] = true,
}, token);
_provider = Text(provider, "pk");
- if(Text(provider, "client_id") != Text(_input, "clientId")
- || Text(provider, "authentication_flow") != Text(authentication, "pk"))
- { throw new InvalidOperationException("The real provider did not retain the configured client identifier."); }
+ if(!string.Equals(Text(provider, "client_id"), Text(_input, "clientId"), StringComparison.Ordinal) || !string.Equals(Text(provider, "authentication_flow"), Text(authentication, "pk"), StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("The real provider did not retain the configured client identifier.");
+ }
JsonObject application = await CreateAsync("/api/v3/core/applications/", new JsonObject
{
- ["name"] = Text(_input, "project"), ["slug"] = Text(_input, "project"),
- ["provider"] = provider["pk"]!.DeepClone(), ["policy_engine_mode"] = "all",
+ ["name"] = Text(_input, "project"),
+ ["slug"] = Text(_input, "project"),
+ ["provider"] = provider["pk"]!.DeepClone(),
+ ["policy_engine_mode"] = "all",
}, token);
- if(Text(application, "provider") != _provider)
- { throw new InvalidOperationException("The real provider application binding differs from the configured provider."); }
+ if(!string.Equals(Text(application, "provider"), _provider, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("The real provider application binding differs from the configured provider.");
+ }
}
+
+
+ /// Create a synthetic user with configured groups and its private password.
private async Task CreateUserAsync(string usernameKey, string passwordKey, JsonArray groups, CancellationToken token)
{
JsonObject user = await CreateAsync("/api/v3/core/users/", new JsonObject
{
- ["username"] = Text(_input, usernameKey), ["name"] = "Synthetic native qualification subject",
- ["email"] = Text(_input, usernameKey) + "@example.invalid", ["is_active"] = true,
- ["type"] = "internal", ["groups"] = groups,
+ ["username"] = Text(_input, usernameKey),
+ ["name"] = "Synthetic native qualification subject",
+ ["email"] = Text(_input, usernameKey) + "@example.invalid",
+ ["is_active"] = true,
+ ["type"] = "internal",
+ ["groups"] = groups,
}, token);
FixtureHttp.Reply response = await _admin.SendAsync("POST", "/api/v3/core/users/" + Text(user, "pk") + "/set_password/",
new JsonObject { ["password"] = Text(_input, passwordKey) }, token);
- if(response.Status != HttpStatusCode.NoContent) { throw new InvalidOperationException("Synthetic account password configuration failed."); }
+ if(response.Status != HttpStatusCode.NoContent)
+ {
+ throw new InvalidOperationException("Synthetic account password configuration failed.");
+ }
return user;
}
+
+
+ /// Read bounded provider token metadata scoped to this synthetic application.
private async Task GrantsAsync(string kind, CancellationToken token)
{
JsonObject page = FixtureHttp.RequireJson(await _admin.SendAsync("GET", "/api/v3/oauth2/" + kind + "/?provider=" + _provider + "&user=" + _authorizedUser, null, token), HttpStatusCode.OK);
JsonArray records = page["results"]!.AsArray();
- if(records.Count is 0 or > 20) { throw new InvalidOperationException("The synthetic provider grant set is missing or unexpectedly large."); }
+ if(records.Count is 0 or > 20)
+ {
+ throw new InvalidOperationException("The synthetic provider grant set is missing or unexpectedly large.");
+ }
foreach(JsonNode? node in records)
{
JsonObject record = node!.AsObject();
string provider = record["provider"] is JsonObject nested ? Text(nested, "pk") : Text(record, "provider");
- if(provider != _provider || Text(record["user"]!.AsObject(), "pk") != _authorizedUser)
- { throw new InvalidOperationException("The provider grant escaped the synthetic subject scope."); }
+ if(!string.Equals(provider, _provider, StringComparison.Ordinal) || !string.Equals(Text(record["user"]!.AsObject(), "pk"), _authorizedUser, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException("The provider grant escaped the synthetic subject scope.");
+ }
}
return records;
}
+
+
+ /// Snapshot actual access-grant identities for the renewal assertion.
internal async Task> AccessGrantIdsAsync(CancellationToken token)
=> (await GrantsAsync("access_tokens", token)).Select(node => Text(node!.AsObject(), "pk")).ToHashSet(StringComparer.Ordinal);
+
+
+ /// Revoke only refresh grants belonging to the owned synthetic provider.
internal async Task RevokeRefreshGrantsAsync(CancellationToken token)
{
foreach(JsonNode? node in await GrantsAsync("refresh_tokens", token))
{
string id = Text(node!.AsObject(), "pk");
- if(!Guid.TryParse(id, out _) && !long.TryParse(id, out _)) { throw new InvalidOperationException("Unexpected provider grant identifier."); }
+ if(!Guid.TryParse(id, out _) && !long.TryParse(id, System.Globalization.CultureInfo.InvariantCulture, out _))
+ {
+ throw new InvalidOperationException("Unexpected provider grant identifier.");
+ }
FixtureHttp.Reply response = await _admin.SendAsync("DELETE", "/api/v3/oauth2/refresh_tokens/" + id + "/", null, token);
- if(response.Status != HttpStatusCode.NoContent) { throw new InvalidOperationException("Synthetic provider revocation failed."); }
+ if(response.Status != HttpStatusCode.NoContent)
+ {
+ throw new InvalidOperationException("Synthetic provider revocation failed.");
+ }
}
}
+
+
+ /// Dispose the private provider administration transport.
public void Dispose() => _admin.Dispose();
}
diff --git a/dev/native-auth/client/packages.lock.json b/dev/native-auth/client/packages.lock.json
new file mode 100644
index 0000000..d635df9
--- /dev/null
+++ b/dev/native-auth/client/packages.lock.json
@@ -0,0 +1,13 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Meziantou.Analyzer": {
+ "type": "Direct",
+ "requested": "[3.0.296, )",
+ "resolved": "3.0.296",
+ "contentHash": "1Yp/6OK/Ev66hE2YLe9UvIgtJrKOX/xvoGDe0UbdtuG2ZxUoDoyONdEn+HWCDuy02Mu8SmglHEd/9SGPrM1qYg=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dev/native-auth/java/NativeAuthHost.java b/dev/native-auth/java/NativeAuthHost.java
index 9b4f8ff..2e60566 100644
--- a/dev/native-auth/java/NativeAuthHost.java
+++ b/dev/native-auth/java/NativeAuthHost.java
@@ -19,8 +19,10 @@
*/
// SPDX-License-Identifier: Apache-2.0
+
package com.kingsrook.qbits.printing;
+
import java.net.URI;
import java.sql.Connection;
import java.util.List;
@@ -54,22 +56,28 @@ import io.javalin.http.ForbiddenResponse;
import org.json.JSONArray;
import org.json.JSONObject;
-/**
- * Opt-in container fixture for the pinned upstream native-session candidate.
- * Standard OAuth2 code exchange, session storage and encrypted renewal are used
- * unchanged. No installed pairing, password authentication double or printer.
- */
+
+/***************************************************************************
+ ** Opt-in container fixture for the pinned upstream native-session candidate.
+ ** Standard OAuth2 code exchange, session storage and encrypted renewal are used
+ ** unchanged. No installed pairing, password authentication double or printer.
+ ***************************************************************************/
public final class NativeAuthHost extends AbstractQQQApplication
{
- private static final String TENANT = "printing-native-dev";
- private final String backendName = "printing-native-auth-" + UUID.randomUUID();
- private final String jdbcUrl = "jdbc:h2:file:/data/printing-native-auth";
- private final String databasePassword = required("NATIVE_QQQ_DATABASE_PASSWORD");
- private final URI origin = httpsOrigin(required("NATIVE_QQQ_ORIGIN"));
- private final int port = Integer.parseInt(required("NATIVE_QQQ_PORT"));
- private final DriverDataSource source = new DriverDataSource(jdbcUrl, "sa", databasePassword);
- private final PrintingService service = new PrintingService(source, TENANT);
+ private static final String TENANT = "printing-native-dev";
+ private final String backendName = "printing-native-auth-" + UUID.randomUUID();
+ private final String jdbcUrl = "jdbc:h2:file:/data/printing-native-auth";
+ private final String databasePassword = required("NATIVE_QQQ_DATABASE_PASSWORD");
+ private final URI origin = httpsOrigin(required("NATIVE_QQQ_ORIGIN"));
+ private final int port = Integer.parseInt(required("NATIVE_QQQ_PORT"));
+ private final DriverDataSource source = new DriverDataSource(jdbcUrl, "sa", databasePassword);
+ private final PrintingService service = new PrintingService(source, TENANT);
+
+
+ /***************************************************************************
+ ** Initialize only the private fixture database and native session tables.
+ ***************************************************************************/
private NativeAuthHost() throws Exception
{
if(port < 1024 || port > 65535)
@@ -85,6 +93,11 @@ public final class NativeAuthHost extends AbstractQQQApplication
}
}
+
+
+ /***************************************************************************
+ ** Launch the experimental provider host inside its owned container network.
+ ***************************************************************************/
public static void main(String[] args) throws Exception
{
NativeAuthHost host = new NativeAuthHost();
@@ -92,6 +105,11 @@ public final class NativeAuthHost extends AbstractQQQApplication
.withServerCustomizer(host::configure));
}
+
+
+ /***************************************************************************
+ ** Define the private fixture backend, native OAuth2 provider and printing QBit.
+ ***************************************************************************/
@Override
public QInstance defineQInstance() throws QException
{
@@ -105,8 +123,8 @@ public final class NativeAuthHost extends AbstractQQQApplication
instance.addTable(new RedirectStateMetaDataProducer(backendName).produce(instance));
instance.addTable(new UserSessionRefreshTokenMetaDataProducer(backendName).produce(instance));
- String issuer = required("NATIVE_AUTH_ISSUER");
- URI issuerUri = URI.create(issuer);
+ String issuer = required("NATIVE_AUTH_ISSUER");
+ URI issuerUri = URI.create(issuer);
if(!"https".equals(issuerUri.getScheme()) || issuerUri.getHost() == null
|| issuerUri.getRawUserInfo() != null || issuerUri.getRawQuery() != null || issuerUri.getRawFragment() != null || !issuer.endsWith("/"))
{
@@ -142,6 +160,11 @@ public final class NativeAuthHost extends AbstractQQQApplication
return instance;
}
+
+
+ /***************************************************************************
+ ** Register printing routes behind the fixture TLS proxy.
+ ***************************************************************************/
private void configure(QApplicationJavalinServer server)
{
// Only the owned TLS proxy is published; this HTTP listener remains inside
@@ -156,6 +179,11 @@ public final class NativeAuthHost extends AbstractQQQApplication
});
}
+
+
+ /***************************************************************************
+ ** Require the configured HTTPS proxy and same-origin cookie mutations.
+ ***************************************************************************/
private void guardOrigin(Context context)
{
if(!origin.getRawAuthority().equalsIgnoreCase(context.header("Host"))
@@ -181,9 +209,16 @@ public final class NativeAuthHost extends AbstractQQQApplication
}
}
- /** Only maps claims after the unchanged standard OAuth2 module authenticates. */
+
+
+ /***************************************************************************
+ ** Only maps claims after the unchanged standard OAuth2 module authenticates.
+ ***************************************************************************/
public static final class PrintingPermissions implements QAuthenticationModuleCustomizerInterface
{
+ /***************************************************************************
+ ** Grant the printing admin permission only for the fixture group claim.
+ ***************************************************************************/
@Override
public void customizeSession(QInstance instance, QSession session, Map context)
{
@@ -201,6 +236,11 @@ public final class NativeAuthHost extends AbstractQQQApplication
}
}
+
+
+ /***************************************************************************
+ ** Validate the exact root HTTPS origin used by the private TLS fixture.
+ ***************************************************************************/
private static URI httpsOrigin(String value)
{
URI parsed = URI.create(value);
@@ -212,6 +252,11 @@ public final class NativeAuthHost extends AbstractQQQApplication
return parsed;
}
+
+
+ /***************************************************************************
+ ** Read a required synthetic fixture setting without logging its value.
+ ***************************************************************************/
private static String required(String name)
{
String value = System.getenv(name);
diff --git a/docs/JAVA-COMPILER-SAFETY.md b/docs/JAVA-COMPILER-SAFETY.md
new file mode 100644
index 0000000..9d28532
--- /dev/null
+++ b/docs/JAVA-COMPILER-SAFETY.md
@@ -0,0 +1,56 @@
+# Java compiler safety
+
+The repository compiler runs Error Prone 2.51.0 and NullAway 0.14.3 for
+`com.kingsrook.qbits.printing`, including test sources. NullAway findings are
+errors; compiler warnings fail through `-Werror` and Maven `failOnWarning`.
+This implements K22-CODE-JV-06 and the compiler portion of K22-CODE-02. It is
+not a claim that every unrelated coding-standard requirement is complete.
+
+The configuration retains Java 21, compiler plugin 3.14.1, `proc:none`, the
+existing parent and the frozen QQQ dependency tuple. The explicit compiler
+plugin path loads the analyzers even with annotation processing disabled.
+JSpecify 1.0.0 is declared directly for nullable contracts; it was already in
+the runtime closure. NullAway's contract annotations are provided-only.
+Neither analyzer implementation is an application runtime dependency.
+
+Run the usual quality gate:
+
+```sh
+./mvnw -B -ntp -Pui-demo verify
+```
+
+CI also runs the bounded compiler-configuration guard:
+
+```sh
+python3 .github/scripts/check-java-compiler.py
+```
+
+The guard copies the actual POM compiler block into fresh, isolated modules
+under `target/compiler-guard`. Both main and test sources have a positive
+probe and negative probes for NullAway, Error Prone's `DeadException`, and a
+deprecated call rejected by warnings-as-errors. A random Maven failure does
+not satisfy a negative probe: the expected diagnostic must be present.
+Logs and a result manifest are retained in the normal CI test evidence.
+No artifact is installed or published. Optional `--maven`, `--settings`,
+`--local-repository` and `--offline` arguments support an isolated local cache.
+
+Null annotations describe existing optional inputs and transitions; they do
+not convert absent data into required configuration. Existing lifecycle
+checks provide checked initializer contracts. HELLO-dependent work receives
+the validated payload explicitly. Scheduled retention captures its service
+under the existing missing-configuration error contract. A missing realtime
+QQQ instance now fails descriptively at revalidation, without moving host
+registration timing or caching user authority.
+
+Narrow member suppressions retain required framework override return types,
+exact socket identity fencing and the framework's ordered presentation enum.
+Two private accessors document the storage/page lifecycle invariants that
+cannot be inferred from separate nullable fields. They preserve the existing
+use-after-finish or misconfigured-storage failure rather than inventing valid
+null operations. Suppressions are not package-wide, and tests remain analyzed.
+
+The migration separates redundant return-expression parentheses from null
+contracts and compiler configuration for review. Public JVM descriptors and
+the resolved runtime JAR set are verified against the unchanged baseline.
+The ordinary test suite intentionally skips the separately opt-in 10,000-job
+qualification unless `printing.qualification.load=true` is supplied.
diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md
index 7925632..9b7cc6d 100644
--- a/docs/SESSION-STATE.md
+++ b/docs/SESSION-STATE.md
@@ -1,5 +1,28 @@
# Session state — 2026-10-31
+## Standards and small-label release cohort — 2026-11-01
+
+The strict standards tree (commit `43253f6`) combines the 2.3.0 operator/media/retained source with the
+Error Prone 2.51.0 / NullAway / `-Werror` compiler gate, null contracts, strict headers, the development
+fixture style gates and the official-npm Pyright script gates. Observed locally on that tree: main and test
+compile clean and the full `-Pui-demo verify` passed (189 reported, 0 failures, 0 errors, one intentional
+opt-in 10,000-job skip, 0 Checkstyle violations). This qualifies that integrated source only.
+
+The 4 x 1 and 4 x 2 inch label work (compact layouts, explicit PDF job paper in millimetres that never mixes
+with a media profile, and the 4 x 6 path unchanged) is being merged onto it in the release-integration tree.
+Its four conflicts (`PrintingProcesses`, `PrintingService`, `PrintingTestDocuments`, `TestPrintProfile`) were
+resolved by keeping all label behavior and tests and layering the strict contracts: `jobOptions` is
+`@Nullable` (null means the queue default applies), `validPaperMillimetres` accepts a missing value, redundant
+return wrappers are removed, and no suppression or rule change was added. Submission, authority, retained
+binding, replay and queue code paths were not edited.
+
+**Local gate passed (observed):** on the merged cohort, main and test compile clean under the strict compiler and
+the full `-Pui-demo verify` passed with 193 tests, 0 failures, 0 errors, one intentional opt-in 10,000-job skip and
+0 Checkstyle violations. CI and the README pin the final protocol commit
+`42c0d249f81395f00408ec0f92638dcbbf5deb03` (merged Gitea protocol PR 2); earlier pins elsewhere in this file are
+historical. **Still pending, not observed:** hosted Gitea CI, publication, installation and any physical print. The
+installed printer default is untouched; no printing was performed.
+
## Echo operator/media and retained-source candidate — 2026-11-01
The isolated Echo candidate adds native printing extension **2.3.0** (after the protected-main 2.2.0 rendering extension) on the
diff --git a/docs/TODO.md b/docs/TODO.md
index 9ff71b8..9d675de 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -1,10 +1,5 @@
# Follow-up
-## Script quality gates
-
-- [x] Pin Ruff and strict Pyright with a locked restore; reject Python outside `.github/scripts/` and zero-file Pyright runs; negative tool probes pass locally.
-- [ ] Observe the new `verify` job steps pass on hosted CI before relying on them.
-
## Echo integration candidate
- [x] Implement authorized operator catalog, native submission/process permission,
@@ -24,6 +19,32 @@
- [ ] Publish the reviewed additive contract and coordinate client/runtime adoption
before owner-operated end-to-end physical acceptance.
+## Script quality gates
+
+- [x] Pin Ruff and strict Pyright (official npm package) with locked restores; reject Python outside `.github/scripts/` and zero-file Pyright runs; negative tool probes pass locally.
+- [ ] Observe the new `verify` job steps pass on hosted CI before relying on them.
+
+## Integrated compiler, standards and fixture gates
+
+- [x] Layer the reviewed Error Prone/NullAway compiler gate, null contracts and
+ style corrections over the merged 2.3.0 source without relaxing any security check.
+- [x] Integrated source compiles clean under Error Prone 2.51.0/NullAway/-Werror for
+ main and test sources (R27), and the full local verify passed: 189 reported, 0
+ failures, 0 errors, one intentional 10k-workload opt-in skip, zero Checkstyle
+ violations (R28). This qualifies the integrated source only; the earlier
+ 133-test/204-jar results describe the old baseline.
+- [ ] Observe the `development-clients` and `development-servers` jobs, the compiler
+ guard and the script-quality gates pass on hosted CI.
+
+## Small-label release cohort
+
+- [x] Merge the 4 x 1 / 4 x 2 label profiles, compact PDF/ZPL layouts, explicit PDF job paper and their tests
+ onto the strict standards tree, keeping the strict headers, null contracts and Error Prone fixes.
+- [x] Cohort gate passed locally on the merged tree: strict main/test compile and full `-Pui-demo verify`, 193 tests,
+ 0 failures, 0 errors, one intentional opt-in skip, 0 Checkstyle violations.
+- [x] CI and the README pin the final protocol commit `42c0d249f81395f00408ec0f92638dcbbf5deb03` (merged Gitea PR 2).
+- [ ] Observe hosted Gitea CI, then publish and install; physical confirmation of the short labels stays an owner step (4 x 6 stock is loaded).
+
## Current local development qualification
- [x] Reuse production discovery and the shared major-2 gate in the optional
diff --git a/pom.xml b/pom.xml
index 22b7b4d..fc40daf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,6 +25,9 @@
org.junitjunit-bom6.0.1pomimport
+ com.uber.nullawaynullaway-annotations0.14.3provided
+
+ org.jspecifyjspecify1.0.0
com.kingsrook.qqqqqq-backend-core${qqq.version}
com.kingsrook.qqqqqq-backend-module-rdbms${qqq.version}
com.kingsrook.qqqqqq-backend-module-filesystem${qqq.version}
@@ -49,10 +52,57 @@
org.apache.maven.pluginsmaven-checkstyle-plugin3.3.1
com.puppycrawl.toolscheckstyle10.12.6
- checkstyle/config.xmltruetruewarningtrue
+
+ checkstyle/config.xml
+
+
+ ${project.build.sourceDirectory}
+ ${project.basedir}/dev/java
+ ${project.basedir}/dev/native-auth/java
+
+ true
+ true
+ warning
+ true
+
validatecheck
- org.apache.maven.pluginsmaven-compiler-plugin3.14.1
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.1
+
+ 21
+ none
+ true
+ ${java.home}/bin/javac
+ true
+ true
+
+ -Werror
+ -Xlint:deprecation
+ -Xlint:unchecked
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+ -XDaddTypeAnnotationsToSymbol=true
+ -Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages=com.kingsrook.qbits.printing
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+ -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
+ -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+ -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
+
+
+ com.google.errorproneerror_prone_core2.51.0
+ com.uber.nullawaynullaway0.14.3
+
+
+
org.apache.maven.pluginsmaven-surefire-plugin3.5.4
org.apache.maven.pluginsmaven-jar-plugin3.4.2${project.version}
org.codehaus.mojoexec-maven-plugin3.6.3com.kingsrook.qbits.printing.PrintingDemo
diff --git a/src/main/java/com/kingsrook/qbits/printing/DriverDataSource.java b/src/main/java/com/kingsrook/qbits/printing/DriverDataSource.java
index 481482e..8336cb8 100644
--- a/src/main/java/com/kingsrook/qbits/printing/DriverDataSource.java
+++ b/src/main/java/com/kingsrook/qbits/printing/DriverDataSource.java
@@ -27,6 +27,7 @@ import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.logging.Logger;
+import org.jspecify.annotations.Nullable;
/*******************************************************************************
@@ -37,7 +38,7 @@ public record DriverDataSource(String url, String user, String password) impleme
@Override
public Connection getConnection() throws SQLException
{
- return (DriverManager.getConnection(url, user, password));
+ return DriverManager.getConnection(url, user, password);
}
@@ -45,15 +46,15 @@ public record DriverDataSource(String url, String user, String password) impleme
@Override
public Connection getConnection(String username, String secret) throws SQLException
{
- return (DriverManager.getConnection(url, username, secret));
+ return DriverManager.getConnection(url, username, secret);
}
@Override
- public PrintWriter getLogWriter()
+ public @Nullable PrintWriter getLogWriter()
{
- return (null);
+ return null;
}
@@ -77,7 +78,7 @@ public record DriverDataSource(String url, String user, String password) impleme
@Override
public int getLoginTimeout()
{
- return (DriverManager.getLoginTimeout());
+ return DriverManager.getLoginTimeout();
}
@@ -85,7 +86,7 @@ public record DriverDataSource(String url, String user, String password) impleme
@Override
public Logger getParentLogger()
{
- return (Logger.getLogger("com.kingsrook.qbits.printing"));
+ return Logger.getLogger("com.kingsrook.qbits.printing");
}
@@ -95,7 +96,7 @@ public record DriverDataSource(String url, String user, String password) impleme
{
if(type.isInstance(this))
{
- return (type.cast(this));
+ return type.cast(this);
}
throw new SQLException("Not a wrapper");
}
@@ -105,7 +106,7 @@ public record DriverDataSource(String url, String user, String password) impleme
@Override
public boolean isWrapperFor(Class> type)
{
- return (type.isInstance(this));
+ return type.isInstance(this);
}
@@ -113,7 +114,7 @@ public record DriverDataSource(String url, String user, String password) impleme
@Override
public String toString()
{
- return ("DriverDataSource[credentials redacted]");
+ return "DriverDataSource[credentials redacted]";
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintEnqueueEffectTarget.java b/src/main/java/com/kingsrook/qbits/printing/PrintEnqueueEffectTarget.java
index 3ec8cf4..87baeb8 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintEnqueueEffectTarget.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintEnqueueEffectTarget.java
@@ -48,11 +48,11 @@ public class PrintEnqueueEffectTarget implements EffectTarget
try
{
JSONObject job = serviceForEffect().submit(payload);
- return (EffectResult.sent(job.getString("jobId")));
+ return EffectResult.sent(job.getString("jobId"));
}
catch(PrintingException exception)
{
- return (EffectResult.failed(exception.getMessage()));
+ return EffectResult.failed(exception.getMessage());
}
}
@@ -65,7 +65,7 @@ public class PrintEnqueueEffectTarget implements EffectTarget
public ReconcileResult reconcile(EffectRequest request) throws Exception
{
JSONObject job = serviceForEffect().findByIdempotencyKey(request.idempotencyKey());
- return (job == null ? ReconcileResult.notFound() : ReconcileResult.found(job.getString("jobId")));
+ return job == null ? ReconcileResult.notFound() : ReconcileResult.found(job.getString("jobId"));
}
@@ -79,13 +79,13 @@ public class PrintEnqueueEffectTarget implements EffectTarget
{
if(!(QContext.getQSession() instanceof QSystemUserSession))
{
- return (PrintingProcesses.currentService());
+ return PrintingProcesses.currentService();
}
for(QBitMetaData metadata : QContext.getQInstance().getQBits().values())
{
if(metadata.getConfig() instanceof PrintingQBitConfig config && config.getService() != null)
{
- return (config.getService());
+ return config.getService();
}
}
throw new QException("Printing QBit is not configured for this effect dispatcher");
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingAdminPermission.java b/src/main/java/com/kingsrook/qbits/printing/PrintingAdminPermission.java
index 2900748..46ba400 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingAdminPermission.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingAdminPermission.java
@@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
+import org.jspecify.annotations.Nullable;
/*******************************************************************************
@@ -47,7 +48,7 @@ public class PrintingAdminPermission extends UseOtherPermissionNameCustomPermiss
** Enforce authorization even for direct backend-step invocation.
***************************************************************************/
@Override
- public void checkPermissionsThrowing(AbstractActionInput input, MetaDataWithPermissionRules metadata) throws QPermissionDeniedException
+ public void checkPermissionsThrowing(AbstractActionInput input, @Nullable MetaDataWithPermissionRules metadata) throws QPermissionDeniedException
{
if(QContext.getQSession() == null || !QContext.getQSession().hasPermission("printing_admin"))
{
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingAdmission.java b/src/main/java/com/kingsrook/qbits/printing/PrintingAdmission.java
index b1bb55d..7b291b6 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingAdmission.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingAdmission.java
@@ -25,6 +25,7 @@ import java.sql.Connection;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Set;
+import org.jspecify.annotations.Nullable;
/*******************************************************************************
@@ -174,7 +175,7 @@ final class PrintingAdmission
/***************************************************************************
** SQL aggregates stay bounded by active capacity and the rolling hour index.
***************************************************************************/
- private static void checkScope(Connection connection, String tenant, String device, int bytes, long jobLimit, long activeByteLimit, long retainedLimit, long hourlyLimit, String cutoff) throws SQLException
+ private static void checkScope(Connection connection, String tenant, @Nullable String device, int bytes, long jobLimit, long activeByteLimit, long retainedLimit, long hourlyLimit, String cutoff) throws SQLException
{
String predicate = "tenant_id=?" + (device == null ? "" : " AND device_id=?");
String outstanding = "SELECT content_length FROM printing_job WHERE " + predicate + " AND status IN " + ACTIVE
@@ -205,11 +206,11 @@ final class PrintingAdmission
/***************************************************************************
** Failed upload reservations stay charged until exact confirmed removal.
***************************************************************************/
- private static long orphanBytes(Connection connection, String tenant, String device) throws SQLException
+ private static long orphanBytes(Connection connection, String tenant, @Nullable String device) throws SQLException
{
String sql = "SELECT COALESCE(SUM(COALESCE(content_length,20971520)),0) FROM printing_document_orphan o WHERE tenant_id=?"
+ (device == null ? "" : " AND device_id=?") + " AND NOT EXISTS(SELECT 1 FROM printing_job j WHERE j.id=o.id)";
- return (device == null ? number(connection, sql, tenant) : number(connection, sql, tenant, device));
+ return device == null ? number(connection, sql, tenant) : number(connection, sql, tenant, device);
}
@@ -217,7 +218,7 @@ final class PrintingAdmission
/***************************************************************************
** Release only after deletion; an inconsistent counter fails closed.
***************************************************************************/
- static void release(Connection connection, String tenant, String device, long bytes) throws SQLException
+ static void release(Connection connection, String tenant, @Nullable String device, long bytes) throws SQLException
{
int expected = device == null ? 1 : 2;
String predicate = device == null ? "device_id=''" : "device_id IN ('',?)";
@@ -255,7 +256,7 @@ final class PrintingAdmission
/***************************************************************************
** Read one scalar; never materialize payloads or an unbounded row list.
***************************************************************************/
- private static long number(Connection connection, String sql, Object... values) throws SQLException
+ private static long number(Connection connection, String sql, @Nullable Object... values) throws SQLException
{
try(var statement = connection.prepareStatement(sql))
{
@@ -269,7 +270,7 @@ final class PrintingAdmission
{
throw new SQLException("Missing printing resource accounting row");
}
- return (rows.getLong(1));
+ return rows.getLong(1);
}
}
}
@@ -279,7 +280,7 @@ final class PrintingAdmission
/***************************************************************************
** All identifiers are fixed SQL; only values come from callers.
***************************************************************************/
- private static int execute(Connection connection, String sql, Object... values) throws SQLException
+ private static int execute(Connection connection, String sql, @Nullable Object... values) throws SQLException
{
try(var statement = connection.prepareStatement(sql))
{
@@ -287,7 +288,7 @@ final class PrintingAdmission
{
statement.setObject(index + 1, values[index]);
}
- return (statement.executeUpdate());
+ return statement.executeUpdate();
}
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingAdmissionLimits.java b/src/main/java/com/kingsrook/qbits/printing/PrintingAdmissionLimits.java
index 2ed6395..76c95f1 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingAdmissionLimits.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingAdmissionLimits.java
@@ -48,7 +48,7 @@ public record PrintingAdmissionLimits(int deviceOutstandingJobs, int tenantOutst
public static PrintingAdmissionLimits defaults()
{
long gibibyte = 1024L * 1024 * 1024;
- return (new PrintingAdmissionLimits(10_000, 100_000, 10 * gibibyte, 100 * gibibyte,
- 2048 * gibibyte, 20_480 * gibibyte, 2_000, 20_000, 1_000, 20_000));
+ return new PrintingAdmissionLimits(10_000, 100_000, 10 * gibibyte, 100 * gibibyte,
+ 2048 * gibibyte, 20_480 * gibibyte, 2_000, 20_000, 1_000, 20_000);
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingAdmissionSettings.java b/src/main/java/com/kingsrook/qbits/printing/PrintingAdmissionSettings.java
index 254ce07..9891259 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingAdmissionSettings.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingAdmissionSettings.java
@@ -63,7 +63,7 @@ public class PrintingAdmissionSettings
{
try(Connection connection = source.getConnection())
{
- return (json(effective(connection)));
+ return json(effective(connection));
}
}
@@ -85,7 +85,7 @@ public class PrintingAdmissionSettings
throw new SQLException("The tenant admission settings row is unavailable");
}
}
- return (value);
+ return value;
}
@@ -102,10 +102,10 @@ public class PrintingAdmissionSettings
{
if(!rows.next())
{
- return (defaults);
+ return defaults;
}
String stored = rows.getString(1);
- return (stored == null ? defaults : parse(new JSONObject(stored)));
+ return stored == null ? defaults : parse(new JSONObject(stored));
}
}
}
@@ -123,9 +123,9 @@ public class PrintingAdmissionSettings
}
try
{
- return (new PrintingAdmissionLimits(integer(value, "deviceOutstandingJobs"), integer(value, "tenantOutstandingJobs"),
+ return new PrintingAdmissionLimits(integer(value, "deviceOutstandingJobs"), integer(value, "tenantOutstandingJobs"),
number(value, "deviceOutstandingBytes"), number(value, "tenantOutstandingBytes"), number(value, "deviceRetainedBytes"), number(value, "tenantRetainedBytes"),
- integer(value, "deviceJobsPerHour"), integer(value, "tenantJobsPerHour"), integer(value, "devicePrinters"), integer(value, "tenantPrinters")));
+ integer(value, "deviceJobsPerHour"), integer(value, "tenantJobsPerHour"), integer(value, "devicePrinters"), integer(value, "tenantPrinters"));
}
catch(ArithmeticException exception)
{
@@ -140,7 +140,7 @@ public class PrintingAdmissionSettings
***************************************************************************/
private static int integer(JSONObject value, String field)
{
- return (Math.toIntExact(number(value, field)));
+ return Math.toIntExact(number(value, field));
}
@@ -159,7 +159,7 @@ public class PrintingAdmissionSettings
{
throw new IllegalArgumentException(field + " is outside the supported positive range");
}
- return (parsed);
+ return parsed;
}
@@ -169,10 +169,10 @@ public class PrintingAdmissionSettings
***************************************************************************/
private static JSONObject json(PrintingAdmissionLimits value)
{
- return (new JSONObject().put("deviceOutstandingJobs", value.deviceOutstandingJobs()).put("tenantOutstandingJobs", value.tenantOutstandingJobs())
+ return new JSONObject().put("deviceOutstandingJobs", value.deviceOutstandingJobs()).put("tenantOutstandingJobs", value.tenantOutstandingJobs())
.put("deviceOutstandingBytes", value.deviceOutstandingBytes()).put("tenantOutstandingBytes", value.tenantOutstandingBytes())
.put("deviceRetainedBytes", value.deviceRetainedBytes()).put("tenantRetainedBytes", value.tenantRetainedBytes())
.put("deviceJobsPerHour", value.deviceJobsPerHour()).put("tenantJobsPerHour", value.tenantJobsPerHour())
- .put("devicePrinters", value.devicePrinters()).put("tenantPrinters", value.tenantPrinters()));
+ .put("devicePrinters", value.devicePrinters()).put("tenantPrinters", value.tenantPrinters());
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingAgentAuthorization.java b/src/main/java/com/kingsrook/qbits/printing/PrintingAgentAuthorization.java
index 93005e4..edab0d9 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingAgentAuthorization.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingAgentAuthorization.java
@@ -55,7 +55,7 @@ public final class PrintingAgentAuthorization
String principal = PrintingControlAuthorization.principal();
PrintingControlAuthorization.record(PrintingTables.DEVICE, id);
PrintingProcesses.currentService().authorizeAgent(id, token, principal, bind);
- return (id);
+ return id;
}
@@ -69,7 +69,7 @@ public final class PrintingAgentAuthorization
{
throw new QPermissionDeniedException("Printer is not accessible");
}
- return (PrintingControlAuthorization.record(PrintingTables.PRINTER, rows.getFirst().getValueString("id")));
+ return PrintingControlAuthorization.record(PrintingTables.PRINTER, rows.getFirst().getValueString("id"));
}
@@ -96,7 +96,7 @@ public final class PrintingAgentAuthorization
// Even an empty inventory must have native table READ access.
PrintingControlAuthorization.query(new QueryInput().withTableName(PrintingTables.PRINTER)
.withFilter(new QQueryFilter(new QFilterCriteria("device_id", QCriteriaOperator.EQUALS, device)).withLimit(1)));
- return (authorized);
+ return authorized;
}
@@ -124,9 +124,9 @@ public final class PrintingAgentAuthorization
JSONObject result = PrintingProcesses.currentService().claimForJob(device, id);
if(!result.isNull("job"))
{
- return (result);
+ return result;
}
}
- return (new JSONObject().put("job", JSONObject.NULL));
+ return new JSONObject().put("job", JSONObject.NULL);
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingApiSchemas.java b/src/main/java/com/kingsrook/qbits/printing/PrintingApiSchemas.java
index ebf594c..e9523d2 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingApiSchemas.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingApiSchemas.java
@@ -76,28 +76,28 @@ final class PrintingApiSchemas
add(schemas, "ActionRequest", fields("requestId", id(), "issuedAt", time(), "action", actions, "jobId", id()), "requestId", "issuedAt", "action");
add(schemas, "Action", fields("commandId", id(), "requestId", id(), "issuedAt", time(), "deviceId", id(), "action", actions, "status", enumeration("PENDING", "SUCCEEDED", "FAILED", "EXPIRED", "OUTCOME_UNKNOWN"), "createdAt", time(), "expiresAt", time(), "jobId", id(), "resultCode", text(80), "cancelledJobCount", number(), "receipt", ref("ReceiptRequest")), "commandId", "requestId", "issuedAt", "deviceId", "action", "status", "createdAt", "expiresAt");
add(schemas, "Actions", fields("commands", array(ref("Action")), "nextCursor", text(128).withNullable(true)), "commands", "nextCursor");
- return (schemas);
+ return schemas;
}
/** Every schema has the extension prefix required by the native registry. */
static String name(String shortName)
{
- return ("Printing" + shortName + "V1");
+ return "Printing" + shortName + "V1";
}
/** Refer to one typed reusable component. */
static Schema ref(String name)
{
- return (new Schema().withRefToSchema(name(name)));
+ return new Schema().withRefToSchema(name(name));
}
/** Bounded Unicode text. */
static Schema text(int limit)
{
- return (new Schema().withType(Type.STRING).withMaxLength(limit));
+ return new Schema().withType(Type.STRING).withMaxLength(limit);
}
@@ -107,7 +107,7 @@ final class PrintingApiSchemas
@Override
public Schema toSchema()
{
- return (new RequiredSchema(required).withType(Type.OBJECT).withProperties(properties));
+ return new RequiredSchema(required).withType(Type.OBJECT).withProperties(properties);
}
}
@@ -126,7 +126,7 @@ final class PrintingApiSchemas
/** Published by the core Jackson schema serializer. */
public List getRequired()
{
- return (required);
+ return required;
}
}
@@ -139,7 +139,7 @@ final class PrintingApiSchemas
{
fields.put((String) pairs[index], (Schema) pairs[index + 1]);
}
- return (fields);
+ return fields;
}
@@ -153,41 +153,41 @@ final class PrintingApiSchemas
/** Protocol identifier. */
private static Schema id()
{
- return (text(128));
+ return text(128);
}
/** UTC wire timestamp. */
private static Schema time()
{
- return (text(40).withFormat("date-time").withDescription("RFC3339 UTC ending in uppercase Z."));
+ return text(40).withFormat("date-time").withDescription("RFC3339 UTC ending in uppercase Z.");
}
/** Exact JSON integer, never a stringified numeric value. */
private static Schema number()
{
- return (new Schema().withType(Type.INTEGER));
+ return new Schema().withType(Type.INTEGER);
}
/** Required booleans must never be inferred from missing fields. */
private static Schema bool()
{
- return (new Schema().withType(Type.BOOLEAN));
+ return new Schema().withType(Type.BOOLEAN);
}
/** Closed action or status vocabulary. */
private static Schema enumeration(String... values)
{
- return (new Schema().withType(Type.STRING).withEnumValues(List.of(values)));
+ return new Schema().withType(Type.STRING).withEnumValues(List.of(values));
}
/** Homogeneous typed arrays. */
private static Schema array(Schema items)
{
- return (new Schema().withType(Type.ARRAY).withItems(items));
+ return new Schema().withType(Type.ARRAY).withItems(items);
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingChrome.java b/src/main/java/com/kingsrook/qbits/printing/PrintingChrome.java
index dde6b33..b2a4f34 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingChrome.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingChrome.java
@@ -25,6 +25,7 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import org.jspecify.annotations.Nullable;
/*******************************************************************************
@@ -47,16 +48,16 @@ public record PrintingChrome(String app, String title, String server, String use
/***************************************************************************
** The fields from process values, with server-side defaults.
***************************************************************************/
- public static PrintingChrome from(RunBackendStepInput input, String fallbackTitle)
+ public static PrintingChrome from(RunBackendStepInput input, @Nullable String fallbackTitle)
{
- return (new PrintingChrome(
+ return new PrintingChrome(
value(input, "headerApp", defaultApp()),
value(input, "headerTitle", fallbackTitle),
value(input, "headerServer", ""),
value(input, "headerUser", defaultUser()),
value(input, "headerPrintedAt", Instant.now().truncatedTo(ChronoUnit.SECONDS).toString()),
value(input, "footerLeft", "QQQ printing"),
- value(input, "footerNote", "")));
+ value(input, "footerNote", ""));
}
@@ -66,7 +67,7 @@ public record PrintingChrome(String app, String title, String server, String use
***************************************************************************/
public static PrintingChrome defaults(String title)
{
- return (new PrintingChrome(defaultApp(), title == null ? "" : title, "", defaultUser(), Instant.now().truncatedTo(ChronoUnit.SECONDS).toString(), "QQQ printing", ""));
+ return new PrintingChrome(defaultApp(), title == null ? "" : title, "", defaultUser(), Instant.now().truncatedTo(ChronoUnit.SECONDS).toString(), "QQQ printing", "");
}
@@ -74,9 +75,9 @@ public record PrintingChrome(String app, String title, String server, String use
/***************************************************************************
** The same chrome with a footer note (a grid's filter and sort).
***************************************************************************/
- public PrintingChrome withFooterNote(String note)
+ public PrintingChrome withFooterNote(@Nullable String note)
{
- return (new PrintingChrome(app, title, server, user, printedAt, footerLeft, footerNote == null || footerNote.isBlank() ? (note == null ? "" : note) : footerNote));
+ return new PrintingChrome(app, title, server, user, printedAt, footerLeft, footerNote == null || footerNote.isBlank() ? (note == null ? "" : note) : footerNote);
}
@@ -104,7 +105,7 @@ public record PrintingChrome(String app, String title, String server, String use
out.append(out.isEmpty() ? "" : " · ").append(part);
}
}
- return (out.toString());
+ return out.toString();
}
@@ -112,7 +113,7 @@ public record PrintingChrome(String app, String title, String server, String use
/***************************************************************************
** A string value, or the default when absent or blank (bounded length).
***************************************************************************/
- private static String value(RunBackendStepInput input, String name, String fallback)
+ private static String value(RunBackendStepInput input, String name, @Nullable String fallback)
{
String value = input == null ? null : input.getValueString(name);
String chosen = value == null || value.isBlank() ? fallback : value;
@@ -133,7 +134,7 @@ public record PrintingChrome(String app, String title, String server, String use
}
catch(RuntimeException exception)
{
- return ("QQQ");
+ return "QQQ";
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingCommandStatus.java b/src/main/java/com/kingsrook/qbits/printing/PrintingCommandStatus.java
index b853cc7..09baecf 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingCommandStatus.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingCommandStatus.java
@@ -59,7 +59,7 @@ public class PrintingCommandStatus implements TableCustomizerInterface
Set selected = input instanceof QueryInput query ? query.getFieldNamesToInclude() : null;
if(selected != null && !selected.contains("command_status"))
{
- return (records);
+ return records;
}
if(selected != null && !selected.contains("id"))
{
@@ -72,7 +72,7 @@ public class PrintingCommandStatus implements TableCustomizerInterface
QRecord values = dependencies.getOrDefault(record.getValueString("id"), record);
record.setValue("command_status", effective(values, now));
}
- return (records);
+ return records;
}
@@ -103,7 +103,7 @@ public class PrintingCommandStatus implements TableCustomizerInterface
result.put(row.getValueString("id"), row);
}
}
- return (result);
+ return result;
}
@@ -116,24 +116,24 @@ public class PrintingCommandStatus implements TableCustomizerInterface
String status = record.getValueString("status");
if(status != null && TERMINAL.contains(status))
{
- return (status);
+ return status;
}
if(!"PENDING".equals(status))
{
- return ("UNKNOWN");
+ return "UNKNOWN";
}
String expiresAt = record.getValueString("expires_at");
if(expiresAt == null || expiresAt.isBlank())
{
- return ("UNKNOWN");
+ return "UNKNOWN";
}
try
{
- return (Instant.parse(expiresAt).isBefore(now) ? "EXPIRED" : "PENDING");
+ return Instant.parse(expiresAt).isBefore(now) ? "EXPIRED" : "PENDING";
}
catch(DateTimeParseException exception)
{
- return ("UNKNOWN");
+ return "UNKNOWN";
}
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingControlAuthorization.java b/src/main/java/com/kingsrook/qbits/printing/PrintingControlAuthorization.java
index 2398a5e..59ff954 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingControlAuthorization.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingControlAuthorization.java
@@ -58,7 +58,7 @@ public final class PrintingControlAuthorization
}
new PrintingAdminPermission().checkPermissionsThrowing(new RunBackendStepInput(), null);
PrintingProcesses.currentService();
- return (QContext.getQSession().getUser().getIdReference());
+ return QContext.getQSession().getUser().getIdReference();
}
@@ -79,7 +79,7 @@ public final class PrintingControlAuthorization
{
throw new QPermissionDeniedException("Record is not accessible");
}
- return (row);
+ return row;
}
@@ -95,7 +95,7 @@ public final class PrintingControlAuthorization
static QueryOutput query(QueryInput input) throws QException
{
input.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
- return (new QueryAction().execute(input));
+ return new QueryAction().execute(input);
}
@@ -113,7 +113,7 @@ public final class PrintingControlAuthorization
{
throw new QPermissionDeniedException("Printer is not assigned to this device");
}
- return (row);
+ return row;
}
@@ -152,7 +152,7 @@ public final class PrintingControlAuthorization
{
job(deviceId, receipt.getValueString("job_id"));
}
- return (service.deviceAction(deviceId, request, actor));
+ return service.deviceAction(deviceId, request, actor);
}
Set queue = null;
if("CLEAR_QUEUE".equals(selected.wire))
@@ -181,6 +181,6 @@ public final class PrintingControlAuthorization
{
job(deviceId, request.optString("jobId"));
}
- return (service.deviceAction(deviceId, request, actor, queue));
+ return service.deviceAction(deviceId, request, actor, queue);
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceControls.java b/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceControls.java
index de8ae26..c238f84 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceControls.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceControls.java
@@ -30,6 +30,7 @@ import java.util.Set;
import java.util.UUID;
import org.json.JSONArray;
import org.json.JSONObject;
+import org.jspecify.annotations.Nullable;
import static com.kingsrook.qbits.printing.PrintingService.query;
import static com.kingsrook.qbits.printing.PrintingService.update;
@@ -88,19 +89,19 @@ final class PrintingDeviceControls
{
result.put("lastPollAt", row.getString("control_last_poll_at"));
}
- return (result);
+ return result;
}
/** Strict action input and original receipt enforce replay before any side effect. */
JSONObject action(String deviceId, JSONObject request, String actor) throws SQLException
{
- return (action(deviceId, request, actor, null));
+ return action(deviceId, request, actor, null);
}
/** A native authorized queue snapshot must still match after taking the device lock. */
- JSONObject action(String deviceId, JSONObject request, String actor, Set authorizedQueue) throws SQLException
+ JSONObject action(String deviceId, JSONObject request, String actor, @Nullable Set authorizedQueue) throws SQLException
{
fields(request, Set.of("requestId", "issuedAt", "action", "jobId"));
String requestId = text(request, "requestId", 128);
@@ -132,7 +133,7 @@ final class PrintingDeviceControls
throw new PrintingException(409, "Request identity was already used for a different action");
}
connection.commit();
- return (record(old.getFirst()));
+ return record(old.getFirst());
}
Instant issued;
try
@@ -245,7 +246,7 @@ final class PrintingDeviceControls
id, tenant, deviceId, requestId, action, jobId, actor, request.toString(), REMOTE.contains(action) ? "PENDING" : "SUCCEEDED", now, expires, result, cancelled);
JSONObject response = record(command(connection, deviceId, id));
connection.commit();
- return (response);
+ return response;
}
catch(Exception exception)
{
@@ -301,8 +302,8 @@ final class PrintingDeviceControls
commands.put(delivery);
}
connection.commit();
- return (new JSONObject().put("controlVersion", 1).put("serverTime", now).put("pollIntervalSeconds", 5)
- .put("desiredState", state(device).getJSONObject("desiredState")).put("commands", commands));
+ return new JSONObject().put("controlVersion", 1).put("serverTime", now).put("pollIntervalSeconds", 5)
+ .put("desiredState", state(device).getJSONObject("desiredState")).put("commands", commands);
}
catch(Exception exception)
{
@@ -375,7 +376,7 @@ final class PrintingDeviceControls
update(connection, "UPDATE printing_device_command SET status=?,receipt_json=? WHERE id=? AND tenant_id=? AND device_id=?", status, receipt.toString(), commandId, tenant, deviceId);
}
connection.commit();
- return (new JSONObject().put("accepted", true));
+ return new JSONObject().put("accepted", true);
}
catch(Exception exception)
{
@@ -387,7 +388,7 @@ final class PrintingDeviceControls
/** Management history is bounded and remains readable after revocation. */
- JSONObject page(String deviceId, int limit, String before) throws SQLException
+ JSONObject page(String deviceId, int limit, @Nullable String before) throws SQLException
{
if(limit < 1 || limit > 200)
{
@@ -414,7 +415,7 @@ final class PrintingDeviceControls
{
commands.put(record(row));
}
- return (new JSONObject().put("commands", commands).put("nextCursor", rows.size() > limit ? rows.get(limit - 1).getString("id") : JSONObject.NULL));
+ return new JSONObject().put("commands", commands).put("nextCursor", rows.size() > limit ? rows.get(limit - 1).getString("id") : JSONObject.NULL);
}
}
@@ -424,7 +425,7 @@ final class PrintingDeviceControls
{
try(Connection connection = source.getConnection())
{
- return (record(command(connection, deviceId, commandId)));
+ return record(command(connection, deviceId, commandId));
}
}
@@ -437,7 +438,7 @@ final class PrintingDeviceControls
{
throw new PrintingException(404, "Device not found");
}
- return (rows.getFirst());
+ return rows.getFirst();
}
@@ -456,7 +457,7 @@ final class PrintingDeviceControls
{
throw new PrintingException(404, "Command not found");
}
- return (rows.getFirst());
+ return rows.getFirst();
}
@@ -486,14 +487,14 @@ final class PrintingDeviceControls
{
value.put("receipt", new JSONObject(row.getString("receipt_json")));
}
- return (value);
+ return value;
}
/** Numeric aggregates do not load a receipt history into application memory. */
private static long count(Connection connection, String sql, Object... values) throws SQLException
{
- return (query(connection, sql, values).getFirst().getLong("n"));
+ return query(connection, sql, values).getFirst().getLong("n");
}
@@ -504,7 +505,7 @@ final class PrintingDeviceControls
{
throw new PrintingException(400, "Invalid " + field);
}
- return (value);
+ return value;
}
@@ -516,7 +517,7 @@ final class PrintingDeviceControls
{
throw new PrintingException(400, "UTC timestamp required");
}
- return (value);
+ return value;
}
/** Require an exact nonnegative integral JSON value without coercion. */
@@ -527,7 +528,7 @@ final class PrintingDeviceControls
{
throw new PrintingException(400, "Invalid " + field);
}
- return (((Number) value).longValue());
+ return ((Number) value).longValue();
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceProcesses.java b/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceProcesses.java
index 89a75dd..a1444d8 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceProcesses.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceProcesses.java
@@ -162,7 +162,7 @@ public final class PrintingDeviceProcesses
{
if(action.process.equals(input.getProcessName()))
{
- return (action);
+ return action;
}
}
throw new QException("Unknown printing control action");
@@ -182,7 +182,7 @@ public final class PrintingDeviceProcesses
{
throw new QException("Sign in with an authorized QQQ user before controlling a print device");
}
- return (QContext.getQSession().getUser().getIdReference());
+ return QContext.getQSession().getUser().getIdReference();
}
@@ -206,7 +206,7 @@ public final class PrintingDeviceProcesses
{
throw new QException("Use Cancel queued jobs for work that has not been dispatched to Windows");
}
- return (record);
+ return record;
}
@@ -268,10 +268,12 @@ public final class PrintingDeviceProcesses
/************************************************************************
** Standard summary links preserve the native record/table workflow.
************************************************************************/
+ // QQQ ProcessSummaryProvider requires this concrete return type; retain binary/API compatibility.
+ @SuppressWarnings("NonApiType")
@Override
public ArrayList getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen)
{
- return (new ArrayList<>(List.of(ready)));
+ return new ArrayList<>(List.of(ready));
}
}
@@ -342,10 +344,12 @@ public final class PrintingDeviceProcesses
/************************************************************************
** Pending requests are never presented as completed Windows operations.
************************************************************************/
+ // QQQ ProcessSummaryProvider requires this concrete return type; retain binary/API compatibility.
+ @SuppressWarnings("NonApiType")
@Override
public ArrayList getProcessSummary(RunBackendStepOutput output, boolean isForResultScreen)
{
- return (new ArrayList<>(List.of(applied, pending, unresolved)));
+ return new ArrayList<>(List.of(applied, pending, unresolved));
}
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceStatus.java b/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceStatus.java
index f732e03..7236f01 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceStatus.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingDeviceStatus.java
@@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperat
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import org.jspecify.annotations.Nullable;
/*******************************************************************************
@@ -56,7 +57,7 @@ public class PrintingDeviceStatus implements TableCustomizerInterface
Set selected = input instanceof QueryInput query ? query.getFieldNamesToInclude() : null;
if(selected != null && !selected.contains("device_status") && !selected.contains("control_status"))
{
- return (records);
+ return records;
}
if(selected != null && !selected.contains("id"))
{
@@ -76,7 +77,7 @@ public class PrintingDeviceStatus implements TableCustomizerInterface
record.setValue("control_status", application(values));
}
}
- return (records);
+ return records;
}
@@ -108,7 +109,7 @@ public class PrintingDeviceStatus implements TableCustomizerInterface
result.put(row.getValueString("id"), row);
}
}
- return (result);
+ return result;
}
@@ -120,24 +121,24 @@ public class PrintingDeviceStatus implements TableCustomizerInterface
{
if(record.getValueInteger("enabled") != null && record.getValueInteger("enabled") == 0)
{
- return ("REVOKED");
+ return "REVOKED";
}
if("DISCONNECTED".equals(record.getValueString("connection_state")))
{
- return ("DISCONNECTED");
+ return "DISCONNECTED";
}
Instant heartbeat = timestamp(record.getValueString("last_seen_at"));
Instant control = timestamp(record.getValueString("control_last_poll_at"));
Instant seen = heartbeat == null || (control != null && control.isAfter(heartbeat)) ? control : heartbeat;
if(seen == null || seen.isAfter(now.plusSeconds(30)))
{
- return ("UNKNOWN");
+ return "UNKNOWN";
}
if(seen.isBefore(now.minusSeconds(90)))
{
- return ("OFFLINE");
+ return "OFFLINE";
}
- return (Integer.valueOf(1).equals(record.getValueInteger("dispatch_paused")) ? "PAUSED" : "ONLINE");
+ return Integer.valueOf(1).equals(record.getValueInteger("dispatch_paused")) ? "PAUSED" : "ONLINE";
}
@@ -149,11 +150,11 @@ public class PrintingDeviceStatus implements TableCustomizerInterface
{
if(!Integer.valueOf(1).equals(record.getValueInteger("control_version")))
{
- return ("UNKNOWN");
+ return "UNKNOWN";
}
Long desired = record.getValueLong("control_revision");
Long observed = record.getValueLong("control_applied_revision");
- return (desired != null && observed != null && desired.equals(observed) ? "APPLIED" : "PENDING");
+ return desired != null && observed != null && desired.equals(observed) ? "APPLIED" : "PENDING";
}
@@ -161,19 +162,19 @@ public class PrintingDeviceStatus implements TableCustomizerInterface
/***************************************************************************
** Bad observations must remain unknown rather than imply a healthy device.
***************************************************************************/
- private static Instant timestamp(String value)
+ private static @Nullable Instant timestamp(@Nullable String value)
{
if(value == null || value.isBlank())
{
- return (null);
+ return null;
}
try
{
- return (Instant.parse(value));
+ return Instant.parse(value);
}
catch(DateTimeParseException exception)
{
- return (null);
+ return null;
}
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingDocumentStorage.java b/src/main/java/com/kingsrook/qbits/printing/PrintingDocumentStorage.java
index 7445564..b37c85b 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingDocumentStorage.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingDocumentStorage.java
@@ -95,14 +95,14 @@ public class PrintingDocumentStorage
***************************************************************************/
public byte[] get(String table, String reference) throws SQLException
{
- return (get(table, reference, Math.toIntExact(PrintingRealtimePolicy.defaults().get("maxPdfBytes"))));
+ return get(table, reference, Math.toIntExact(PrintingRealtimePolicy.defaults().get("maxPdfBytes")));
}
/** Internal retry callers supply the exact persisted length, independent of the current format policy. */
public byte[] get(String table, String reference, int maximum) throws SQLException
{
- return (execute(() ->
+ return execute(() ->
{
try(InputStream stream = new StorageAction().getInputStream(input(table, reference)))
{
@@ -113,14 +113,14 @@ public class PrintingDocumentStorage
}
return bytes;
}
- }));
+ });
}
/** Return the native backend stream; the request owns closing it and enforcing its declared length. */
public InputStream open(String table, String reference) throws SQLException
{
- return (execute(() -> new StorageAction().getInputStream(input(table, reference))));
+ return execute(() -> new StorageAction().getInputStream(input(table, reference)));
}
@@ -148,7 +148,7 @@ public class PrintingDocumentStorage
{
throw new IllegalArgumentException("Configured printing storage table does not exist");
}
- return (instance.getTable(table).getBackendName());
+ return instance.getTable(table).getBackendName();
}
@@ -166,7 +166,7 @@ public class PrintingDocumentStorage
var backend = instance.getBackend(backendName(table));
if(allowMemoryTest && "memory".equals(backend.getBackendType()))
{
- return (digest(table + ":" + backend.getName()));
+ return digest(table + ":" + backend.getName());
}
if(!table.startsWith("printing_") || !(backend instanceof S3BackendMetaData s3) || !(metadata.getBackendDetails() instanceof S3TableBackendDetails details)
|| s3.getBucketName() == null || s3.getBucketName().isBlank())
@@ -182,9 +182,9 @@ public class PrintingDocumentStorage
throw new IllegalArgumentException("Printing bucket is shared with a non-printing table; configure a dedicated bucket");
}
}
- return (digest(new JSONArray().put(identity).put(java.util.Objects.toString(s3.getRegion(), "")).put(java.util.Objects.toString(s3.getPathStyleAccess(), "false"))
+ return digest(new JSONArray().put(identity).put(java.util.Objects.toString(s3.getRegion(), "")).put(java.util.Objects.toString(s3.getPathStyleAccess(), "false"))
.put(java.util.Objects.toString(s3.getBasePath(), "")).put(java.util.Objects.toString(details.getBasePath(), ""))
- .put(S3_KEY_LAYOUT).put((int) java.io.File.separatorChar).toString()));
+ .put(S3_KEY_LAYOUT).put((int) java.io.File.separatorChar).toString());
}
@@ -195,7 +195,7 @@ public class PrintingDocumentStorage
public String bucketName(String table)
{
var backend = instance.getBackend(backendName(table));
- return (backend instanceof S3BackendMetaData s3 ? s3.getBucketName() : "In-memory test storage");
+ return backend instanceof S3BackendMetaData s3 ? s3.getBucketName() : "In-memory test storage";
}
@@ -205,7 +205,7 @@ public class PrintingDocumentStorage
***************************************************************************/
public String prefix()
{
- return (prefix);
+ return prefix;
}
@@ -241,7 +241,7 @@ public class PrintingDocumentStorage
endpoint = uri.getScheme().toLowerCase(java.util.Locale.ROOT) + "://" + uri.getHost().toLowerCase(java.util.Locale.ROOT)
+ (uri.getPort() < 0 || ("https".equalsIgnoreCase(uri.getScheme()) && uri.getPort() == 443) || ("http".equalsIgnoreCase(uri.getScheme()) && uri.getPort() == 80) ? "" : ":" + uri.getPort()) + java.util.Objects.toString(uri.getPath(), "").replaceAll("/+$", "");
}
- return (new JSONArray().put(endpoint).put(java.util.Objects.toString(backend.getBucketName())).toString());
+ return new JSONArray().put(endpoint).put(java.util.Objects.toString(backend.getBucketName())).toString();
}
@@ -253,7 +253,7 @@ public class PrintingDocumentStorage
{
try
{
- return (HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))));
+ return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)));
}
catch(java.security.NoSuchAlgorithmException exception)
{
@@ -273,7 +273,7 @@ public class PrintingDocumentStorage
{
throw new IllegalArgumentException("Invalid owned printing storage reference");
}
- return (new StorageInput(table).withReference(reference));
+ return new StorageInput(table).withReference(reference);
}
@@ -288,7 +288,7 @@ public class PrintingDocumentStorage
try
{
QContext.init(instance, new QSystemUserSession(instance));
- return (operation.call());
+ return operation.call();
}
catch(PrintingException exception)
{
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingDocumentStream.java b/src/main/java/com/kingsrook/qbits/printing/PrintingDocumentStream.java
index 91e6d06..8345e9c 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingDocumentStream.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingDocumentStream.java
@@ -60,7 +60,7 @@ final class PrintingDocumentStream extends FilterInputStream
public int read() throws IOException
{
byte[] one = new byte[1];
- return (read(one, 0, 1) == -1 ? -1 : Byte.toUnsignedInt(one[0]));
+ return read(one, 0, 1) == -1 ? -1 : Byte.toUnsignedInt(one[0]);
}
@@ -85,6 +85,6 @@ final class PrintingDocumentStream extends FilterInputStream
throw new IOException("Stored print document integrity check failed");
}
}
- return (read);
+ return read;
}
}
\ No newline at end of file
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingException.java b/src/main/java/com/kingsrook/qbits/printing/PrintingException.java
index cb087ea..2d3a151 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingException.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingException.java
@@ -46,7 +46,7 @@ public class PrintingException extends RuntimeException
***************************************************************************/
public int getStatus()
{
- return (status);
+ return status;
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingExtract.java b/src/main/java/com/kingsrook/qbits/printing/PrintingExtract.java
index 59e792b..0ca008a 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingExtract.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingExtract.java
@@ -51,6 +51,6 @@ public class PrintingExtract extends ExtractViaQueryStep
count.setTableName(input.getValueString(FIELD_SOURCE_TABLE));
count.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
PermissionsHelper.checkTablePermissionThrowing(count, TablePermissionSubType.READ);
- return (super.doCount(input));
+ return super.doCount(input);
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingFacilities.java b/src/main/java/com/kingsrook/qbits/printing/PrintingFacilities.java
index 59f64ec..85fe070 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingFacilities.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingFacilities.java
@@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
+import org.jspecify.annotations.Nullable;
/*******************************************************************************
@@ -63,13 +64,13 @@ final class PrintingFacilities
{
instance.addPossibleValueSource(new QPossibleValueSource().withName(DEFAULT_SOURCE).withType(QPossibleValueSourceType.ENUM)
.withEnumValues(List.of(new QPossibleValue<>("default", "Default facility"))));
- return (DEFAULT_SOURCE);
+ return DEFAULT_SOURCE;
}
if(instance.getPossibleValueSource(source) == null)
{
throw new QException("The configured printing facility possible-value source does not exist");
}
- return (source);
+ return source;
}
@@ -80,8 +81,8 @@ final class PrintingFacilities
static QFieldMetaData field(QInstance instance)
{
String source = instance.getTable(PrintingTables.DEVICE).getField("facility_id").getPossibleValueSourceName();
- return (new QFieldMetaData("facilityId", QFieldType.STRING).withLabel("Facility").withIsRequired(true).withMaxLength(200)
- .withPossibleValueSourceName(source).withDefaultValue(DEFAULT_SOURCE.equals(source) ? "default" : null));
+ return new QFieldMetaData("facilityId", QFieldType.STRING).withLabel("Facility").withIsRequired(true).withMaxLength(200)
+ .withPossibleValueSourceName(source).withDefaultValue(DEFAULT_SOURCE.equals(source) ? "default" : null);
}
@@ -89,7 +90,7 @@ final class PrintingFacilities
/***************************************************************************
** Re-query the selected ID under the caller's session, never a system session.
***************************************************************************/
- static String validate(String selected) throws QException
+ static String validate(@Nullable String selected) throws QException
{
if(selected == null || selected.isBlank() || selected.length() > 200)
{
@@ -104,6 +105,6 @@ final class PrintingFacilities
{
throw new QException("The selected facility is unavailable or is not accessible to this user");
}
- return (selected);
+ return selected;
}
}
diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingHome.java b/src/main/java/com/kingsrook/qbits/printing/PrintingHome.java
index d3f8a1c..06fb618 100644
--- a/src/main/java/com/kingsrook/qbits/printing/PrintingHome.java
+++ b/src/main/java/com/kingsrook/qbits/printing/PrintingHome.java
@@ -139,7 +139,7 @@ public class PrintingHome extends AbstractWidgetRenderer
default -> throw new QException("Unknown printing home widget");
};
data.setSublabel("Checked " + DISPLAY_TIME.format(now) + " · current tenant · refresh every 60 seconds");
- return (new RenderWidgetOutput(data));
+ return new RenderWidgetOutput(data);
}
@@ -161,7 +161,7 @@ public class PrintingHome extends AbstractWidgetRenderer
{
data.withBlock(text("No print devices are connected. Open Print devices and use Enroll a print device; then connect the Windows agent and expose its printers.").withLink(new BlockLink(tableUrl(DEVICE, new QQueryFilter()))));
}
- return (data);
+ return data;
}
@@ -198,10 +198,10 @@ public class PrintingHome extends AbstractWidgetRenderer
{
rows.add(issue(PRINTER, printer, printer.getValueString("name"), printer.getValueString("status"), "Check paper, paused/error state and whether this queue is still exposed by the agent.", "last_seen_at"));
}
- return (table(rows, "No issues in this scope. If no devices are enrolled, start from Print devices.",
+ return table(rows, "No issues in this scope. If no devices are enrolled, start from Print devices.",
new TableData.Column("composite", "Issue / record", "issue", null, "left"),
new TableData.Column("default", "Next step", "action", null, "left"),
- new TableData.Column("default", "Observed at", "observed", null, "left")));
+ new TableData.Column("default", "Observed at", "observed", null, "left"));
}
@@ -219,12 +219,12 @@ public class PrintingHome extends AbstractWidgetRenderer
rows.add(Map.of("job", column().withBlock(text(job.getValueString("title")).withLink(new BlockLink(recordUrl(JOB, job)))),
"status", job.getValueString("status"), "format", job.getValueString("format"), "created", displayTime(job.getValueString("created_at"))));
}
- return (table(rows, "No pending or recent print work. Open Printers to enable an exposed queue and run its test-print action.",
+ return table(rows, "No pending or recent print work. Open Printers to enable an exposed queue and run its test-print action.",
new TableData.Column("composite", "Print job", "job", null, "left"),
new TableData.Column("default", "Status", "status", null, "left"),
new TableData.Column("default", "Format", "format", null, "left"),
new TableData.Column("default", "Created at", "created", null, "left"))
- .withLinkText("Open all pending and recent jobs").withLinkURL(tableUrl(JOB, filter.withLimit(null))));
+ .withLinkText("Open all pending and recent jobs").withLinkURL(tableUrl(JOB, filter.withLimit(null)));
}
@@ -234,7 +234,7 @@ public class PrintingHome extends AbstractWidgetRenderer
***************************************************************************/
private static StatisticsData statistic(String table, QQueryFilter filter, String context) throws QException
{
- return (new StatisticsData().withCount(count(table, filter)).withCountContext(context).withCountURL(tableUrl(table, filter)));
+ return new StatisticsData().withCount(count(table, filter)).withCountContext(context).withCountURL(tableUrl(table, filter));
}
@@ -246,7 +246,7 @@ public class PrintingHome extends AbstractWidgetRenderer
{
QueryInput input = new QueryInput().withTableName(table).withFilter(filter);
input.setPermissionEnforcement(com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement.ENFORCE);
- return (new QueryAction().execute(input).getRecords());
+ return new QueryAction().execute(input).getRecords();
}
@@ -258,7 +258,7 @@ public class PrintingHome extends AbstractWidgetRenderer
{
CountInput input = new CountInput(table).withFilter(filter);
input.setPermissionEnforcement(com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement.ENFORCE);
- return (new CountAction().execute(input).getCount());
+ return new CountAction().execute(input).getCount();
}
@@ -270,7 +270,7 @@ public class PrintingHome extends AbstractWidgetRenderer
{
try
{
- return (Math.max(90, PrintingProcesses.currentService().realtimePolicy().get("heartbeatTimeoutSeconds")));
+ return Math.max(90, PrintingProcesses.currentService().realtimePolicy().get("heartbeatTimeoutSeconds"));
}
catch(java.sql.SQLException exception)
{
@@ -286,7 +286,7 @@ public class PrintingHome extends AbstractWidgetRenderer
private static StatisticsData offlineStatistic(Instant now) throws QException
{
long seconds = freshnessSeconds();
- return (statistic(DEVICE, offline(now, seconds), "Enabled devices · now · disconnected by an administrator or unseen for " + seconds + " seconds"));
+ return statistic(DEVICE, offline(now, seconds), "Enabled devices · now · disconnected by an administrator or unseen for " + seconds + " seconds");
}
@@ -299,7 +299,7 @@ public class PrintingHome extends AbstractWidgetRenderer
QQueryFilter unseen = new QQueryFilter().withSubFilter(stale("last_seen_at", now, freshnessSeconds)).withSubFilter(stale("control_last_poll_at", now, freshnessSeconds));
QQueryFilter disconnected = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
.withCriteria("connection_state", QCriteriaOperator.EQUALS, "DISCONNECTED").withSubFilter(unseen);
- return (new QQueryFilter().withCriteria("enabled", QCriteriaOperator.EQUALS, 1).withSubFilter(disconnected));
+ return new QQueryFilter().withCriteria("enabled", QCriteriaOperator.EQUALS, 1).withSubFilter(disconnected);
}
@@ -309,8 +309,8 @@ public class PrintingHome extends AbstractWidgetRenderer
***************************************************************************/
private static QQueryFilter stale(String field, Instant now, long freshnessSeconds)
{
- return (new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
- .withCriteria(field, QCriteriaOperator.IS_BLANK).withCriteria(field, QCriteriaOperator.LESS_THAN, now.minusSeconds(freshnessSeconds).toString()));
+ return new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
+ .withCriteria(field, QCriteriaOperator.IS_BLANK).withCriteria(field, QCriteriaOperator.LESS_THAN, now.minusSeconds(freshnessSeconds).toString());
}
@@ -320,8 +320,8 @@ public class PrintingHome extends AbstractWidgetRenderer
***************************************************************************/
private static QQueryFilter printerIssues()
{
- return (new QQueryFilter().withCriteria("enabled", QCriteriaOperator.EQUALS, 1).withSubFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
- .withCriteria("status", QCriteriaOperator.NOT_EQUALS, "READY").withCriteria("available", QCriteriaOperator.NOT_EQUALS, 1)));
+ return new QQueryFilter().withCriteria("enabled", QCriteriaOperator.EQUALS, 1).withSubFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
+ .withCriteria("status", QCriteriaOperator.NOT_EQUALS, "READY").withCriteria("available", QCriteriaOperator.NOT_EQUALS, 1));
}
@@ -331,7 +331,7 @@ public class PrintingHome extends AbstractWidgetRenderer
***************************************************************************/
private static QQueryFilter statuses(List states)
{
- return (new QQueryFilter().withCriteria("status", QCriteriaOperator.IN, states));
+ return new QQueryFilter().withCriteria("status", QCriteriaOperator.IN, states);
}
@@ -341,7 +341,7 @@ public class PrintingHome extends AbstractWidgetRenderer
***************************************************************************/
private static QQueryFilter failures(Instant now)
{
- return (statuses(FAILURES).withCriteria("created_at", QCriteriaOperator.GREATER_THAN_OR_EQUALS, now.minusSeconds(86400).toString()));
+ return statuses(FAILURES).withCriteria("created_at", QCriteriaOperator.GREATER_THAN_OR_EQUALS, now.minusSeconds(86400).toString());
}
@@ -351,8 +351,8 @@ public class PrintingHome extends AbstractWidgetRenderer
***************************************************************************/
private static Map issue(String table, QRecord record, String label, String status, String action, String timestamp) throws QException
{
- return (Map.of("issue", column().withBlock(text(label + " — " + status).withLink(new BlockLink(recordUrl(table, record)))),
- "action", action, "observed", displayTime(record.getValueString(timestamp))));
+ return Map.of("issue", column().withBlock(text(label + " — " + status).withLink(new BlockLink(recordUrl(table, record)))),
+ "action", action, "observed", displayTime(record.getValueString(timestamp)));
}
@@ -364,15 +364,15 @@ public class PrintingHome extends AbstractWidgetRenderer
{
if(value == null)
{
- return ("Unknown time");
+ return "Unknown time";
}
try
{
- return (DISPLAY_TIME.format(Instant.parse(value)));
+ return DISPLAY_TIME.format(Instant.parse(value));
}
catch(DateTimeParseException exception)
{
- return ("Unknown time");
+ return "Unknown time";
}
}
@@ -383,7 +383,7 @@ public class PrintingHome extends AbstractWidgetRenderer
***************************************************************************/
private static TableData table(List