Compare commits

..
Author SHA1 Message Date
dependabot[bot] dbb0712ea9 build(deps): bump the github-actions group with 4 updates
Bumps the github-actions group with 4 updates: [QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml](https://github.com/qrun-io/qqq-orb), [actions/checkout](https://github.com/actions/checkout), [actions/setup-java](https://github.com/actions/setup-java) and [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml` from c288a8864fe9054f68410e1d78f5863aea4c4d22 to 1c389210e6dfda1630d053c07ae4dae523dd5576
- [Release notes](https://github.com/qrun-io/qqq-orb/releases)
- [Changelog](https://github.com/QRun-IO/qqq-orb/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/qrun-io/qqq-orb/compare/c288a8864fe9054f68410e1d78f5863aea4c4d22...1c389210e6dfda1630d053c07ae4dae523dd5576)

Updates `actions/checkout` from 4.4.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/11d5960a326750d5838078e36cf38b85af677262...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `actions/setup-java` from 4.9.1 to 6.1.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/cf277c60eb25467037889841efdb72551f06f6c3...b843ebf9b913146b38da2eaa7aeddf76074ae1ad)

Updates `actions/upload-artifact` from 4.6.2 to 7.0.2
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

---
updated-dependencies:
- dependency-name: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml
  dependency-version: 1c389210e6dfda1630d053c07ae4dae523dd5576
  dependency-type: direct:production
  dependency-group: github-actions
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-java
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-29 19:17:06 +00:00
14 changed files with 11 additions and 330 deletions

No files matched your search

-22
View File
@@ -1,22 +0,0 @@
# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository.
name: qrun-release
on:
push: {}
workflow_dispatch: {}
permissions:
contents: read
actions: read
jobs:
release:
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-maven.yaml@qrun-release-v1
with:
runner: linux-small
postpublish:
if: ${{ needs.release.outputs.capture == 'true' }}
needs: release
permissions:
contents: write
actions: write
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1
with:
runner: linux-small
+1 -1
View File
@@ -16,4 +16,4 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@1c389210e6dfda1630d053c07ae4dae523dd5576
+4 -4
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: windows-2025
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Analyze scripts
@@ -28,10 +28,10 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
- uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: '21'
@@ -41,7 +41,7 @@ jobs:
run: ./mvnw --batch-mode --no-transfer-progress -f demo/pom.xml verify
- name: Retain test reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: server-test-reports
path: |
+1 -7
View File
@@ -1,6 +1,6 @@
# QQQ Mobile Scanners
Server support for approved Android handhelds running native QQQ apps. The first app, **Scan Capture**, stores barcode events in a QQQ table. The companion Android application is [QQQ Android's scanner host](https://github.com/QRun-IO/qqq-android/tree/feature/mobile-scanner-host/scanner-host), currently in [pull request #8](https://github.com/QRun-IO/qqq-android/pull/8).
Server support for approved Android handhelds running native QQQ apps. The first app, **Scan Capture**, stores barcode events in a QQQ table. The companion Android application is [QQQ Android's scanner host](https://github.com/QRun-IO/qqq-android/tree/develop/scanner-host).
Version one is online only. Devices require an authenticated user and administrator approval. Android renders existing QQQ app, process, frontend-step and widget metadata; this qbit adds the scanner widget and enrollment lifecycle.
@@ -24,12 +24,6 @@ The wrapper pins Maven 3.9.9. On Unix, use `./mvnw` in place of `mvnw.cmd`. This
For the Windows demo, follow [local startup and credentials](docs/LOCAL-DEMO.md). For the scanner, follow [TC22 installation and acceptance](docs/TC22-ACCEPTANCE.md).
## Customer branding and connection diagnostics
Configure the customer name, primary color and optional HTTPS logo on the server; the same Android APK applies them before login and throughout the mobile screens. See [branding configuration](docs/BRANDING.md). The shared launcher remains QQQ Mobile.
The app footer checks server reachability while foregrounded. Tap it to inspect and copy recent safe diagnostic events. The public status endpoint reports reachability independently of user authentication and device approval.
## Contributing and CI
Development pull requests target `develop`. CircleCI uses the shared QRun-IO QBit pipeline; GitHub Actions runs the demo HTTP contract and CodeQL. All Maven publication switches start disabled. See [contributing](CONTRIBUTING.md), [CI and release operation](docs/CI-CD.md), [security reporting](SECURITY.md) and the [code of conduct](CODE_OF_CONDUCT.md). This project is licensed under [Apache 2.0](LICENSE).
@@ -37,7 +37,6 @@ import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
public class MobileScannersDemo extends AbstractQQQApplication
{
private final String jdbcUrl;
private MobileScannersQBitConfig mobileConfig = new MobileScannersQBitConfig().withBackendName("mobileDb");
private QInstance instance;
@@ -50,29 +49,6 @@ public class MobileScannersDemo extends AbstractQQQApplication
this.jdbcUrl = jdbcUrl;
}
/*******************************************************************************
** Configure the same public appearance for every client of this demo server.
*******************************************************************************/
public MobileScannersDemo withBranding(String displayName, String primaryColor, String logoUrl)
{
if(instance != null)
{
throw new IllegalStateException("Configure branding before defining the demo instance.");
}
MobileScannersQBitConfig configured = new MobileScannersQBitConfig().withBackendName("mobileDb");
if(displayName != null)
{
configured.withDisplayName(displayName);
}
if(primaryColor != null)
{
configured.withPrimaryColor(primaryColor);
}
configured.withLogoUrl(logoUrl);
mobileConfig = configured;
return this;
}
/*******************************************************************************
@@ -105,7 +81,7 @@ public class MobileScannersDemo extends AbstractQQQApplication
instance.addTable(table);
}
instance.getTable("demoUser").getField("passwordHash").setIsHidden(true);
new MobileScannersQBitProducer().withConfig(mobileConfig).produce(instance, "mobile");
new MobileScannersQBitProducer().withConfig(new MobileScannersQBitConfig().withBackendName("mobileDb")).produce(instance, "mobile");
for(String name : List.of(MobileScannersQBitProducer.DEVICE, MobileScannersQBitProducer.CREDENTIAL, MobileScannersQBitProducer.SCAN))
{
QTableMetaData table = instance.getTable(name);
@@ -208,7 +184,6 @@ public class MobileScannersDemo extends AbstractQQQApplication
Path database = Path.of("data", "mobile").toAbsolutePath().normalize();
Files.createDirectories(database.getParent());
MobileScannersDemo application = new MobileScannersDemo("jdbc:h2:file:" + database.toString().replace('\\', '/') + ";DATABASE_TO_UPPER=FALSE");
application.withBranding(System.getenv("MOBILE_BRAND_DISPLAY_NAME"), System.getenv("MOBILE_BRAND_PRIMARY_COLOR"), System.getenv("MOBILE_BRAND_LOGO_URL"));
application.initializeDatabase(System.getenv("MOBILE_DEMO_SCANNER_PASSWORD"), System.getenv("MOBILE_DEMO_ADMIN_PASSWORD"));
application.start(port);
}
@@ -50,7 +50,7 @@ class MobileHttpAcceptanceTest
String database = "jdbc:h2:mem:mobileHttp" + UUID.randomUUID().toString().replace("-", "") + ";DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE";
String scannerPassword = UUID.randomUUID().toString();
String adminPassword = UUID.randomUUID().toString();
MobileScannersDemo application = new MobileScannersDemo(database).withBranding("Warehouse Mobile", "#007a55", "https://example.com/logo.png");
MobileScannersDemo application = new MobileScannersDemo(database);
application.initializeDatabase(scannerPassword, adminPassword);
Integer port;
try(ServerSocket socket = new ServerSocket(0))
@@ -61,14 +61,6 @@ class MobileHttpAcceptanceTest
QApplicationJavalinServer server = application.start(port);
try
{
var branding = request("GET", "/mobile/v1/branding", null, null, false);
assertEquals(200, branding.statusCode());
assertEquals("no-store", branding.headers().firstValue("Cache-Control").orElseThrow());
JSONObject brand = new JSONObject(branding.body());
assertEquals(3, brand.length());
assertEquals("Warehouse Mobile", brand.getString("displayName"));
assertEquals("#007A55", brand.getString("primaryColor"));
assertEquals("https://example.com/logo.png", brand.getString("logoUrl"));
assertEquals(401, request("POST", "/mobile/v1/devices/enroll", null, "{}", false).statusCode());
String scanner = login("scanner", scannerPassword);
String admin = login("admin", adminPassword);
-43
View File
@@ -1,43 +0,0 @@
# Customer branding and connection status
The shared Android APK loads appearance from the selected QQQ server before login. A customer can configure the in-app display name, primary color and optional logo without compiling another APK. The installed application's Android launcher name, icon and package identity remain properties of the APK.
## Host configuration
Configure the existing qbit before producing its metadata:
```java
new MobileScannersQBitConfig()
.withBackendName("mobileDb")
.withDisplayName("Warehouse Mobile")
.withPrimaryColor("#007A55")
.withLogoUrl("https://assets.example.com/mobile-logo.png");
```
`displayName` must be 1–80 trimmed plain-text characters with no control characters or angle brackets. `primaryColor` must be `#RRGGBB`; the server normalizes hex letters to uppercase. `logoUrl` is optional (`null` clears it), at most 2048 characters, and must be an absolute HTTPS URL without user credentials or a fragment. Invalid settings fail configuration. Use a public static PNG or JPEG image; the server does not download or proxy it. The client must load it without forwarding QQQ session cookies, device credentials or authorization headers, and use its default visual treatment if loading fails. Branding is presentation, not verified customer identity or an authorization policy.
For the local demo, set any of these before launching `scripts/Start-Demo.ps1`:
```powershell
$env:MOBILE_BRAND_DISPLAY_NAME = 'Warehouse Mobile'
$env:MOBILE_BRAND_PRIMARY_COLOR = '#007A55'
$env:MOBILE_BRAND_LOGO_URL = 'https://assets.example.com/mobile-logo.png'
```
Unset variables use defaults: `QQQ Mobile Scanners`, `#1565C0`, and no logo. Restart the server after configuration changes, then reconnect the client. The existing Windows launcher passes these environment variables through to Java. Use only public logo assets. Branding is public; keep private credentials and sensitive tenant details out.
## Public client contract
`GET /mobile/v1/branding` returns HTTP 200 with `Cache-Control: no-store` and JSON:
```json
{
"displayName": "Warehouse Mobile",
"primaryColor": "#007A55",
"logoUrl": "https://assets.example.com/mobile-logo.png"
}
```
The `logoUrl` property is omitted when unset. The endpoint requires no session or approved device and ignores stale device headers. It exposes only those allowlisted appearance fields, never qbit configuration, backend names, secrets or enrollment records. A client connecting to an older server without this endpoint should retain its default theme.
`GET /mobile/v1/status` likewise returns HTTP 200, `Cache-Control: no-store`, and `{"status":"ok"}` without authentication. This means the HTTP service is reachable; it does not test database readiness, authenticate a user, or approve a device. Clients must continue using authenticated device status and normal process responses for those decisions. No diagnostics, database details or credentials are returned.
+1 -1
View File
@@ -28,7 +28,7 @@ To enable publishing later, submit a reviewed change to the manifest after TC22
These are external settings; files alone cannot enable them:
1. Set the default branch to `develop`; protect `develop`, `main` and `release/*`. Require pull requests, signed commits, resolved conversations and successful applicable checks; block force pushes/deletion. Bootstrap allows a pull request with zero mandatory human approvals, matching a maintainer-operated project. CODEOWNERS identifies the responsible maintainer; add mandatory reviewers when the organization assigns a review team. Protect release tags when releases are enabled.
1. Set the default branch to `develop`; protect `develop`, `main`, `release/*` and release tags. Require pull requests, code-owner review, resolved conversations and successful applicable checks; block force pushes/deletion.
2. Connect the repository to the QRun-IO CircleCI organization and enable dynamic configuration. The shared QBit job expects its established `qqq-maven-registry-credentials` context even for test-only verification; grant only the intended trusted repositories and actors access. Do not put its values in this repository.
3. Require CircleCI `setup` and `verify` plus demo acceptance and CodeQL using the actual check names/apps emitted by the first successful hosted run. Do not guess status contexts and leave every pull request permanently blocked.
4. Enable Actions with read-only default workflow permissions, the dependency graph, Dependabot security updates, secret scanning/push protection and private vulnerability reporting when supported.
+1 -3
View File
@@ -20,7 +20,7 @@ Sign in with username `admin`, paste the password, then clear the clipboard with
## Emulator walkthrough
Follow the Android repository's [emulator guide](https://github.com/QRun-IO/qqq-android/blob/feature/mobile-scanner-host/docs/EMULATOR.md) to start a virtual device and install the debug APK. Forward its port to the host:
Follow the Android repository's [emulator guide](https://github.com/QRun-IO/qqq-android/blob/main/docs/EMULATOR.md) to start a virtual device and install the debug APK. Forward its port to the host:
```powershell
adb reverse tcp:8080 tcp:8080
@@ -36,5 +36,3 @@ If the host server uses `-Port 18080`, use `adb reverse tcp:8080 tcp:18080` inst
6. Revoke the device and confirm subsequent capture is refused.
The emulator exercises native screens, login, approval, manual capture and persistence. Verify the physical trigger, DataWedge delivery and barcode decoding on the Zebra TC22 using [the hardware checklist](TC22-ACCEPTANCE.md).
See [branding configuration](BRANDING.md) to test customer names, colors and logos with the same APK. Tap the Android connection footer to view or copy diagnostic events.
-9
View File
@@ -2,15 +2,6 @@
Updated: 2026-10-29
## Current repository and demo state
- GitHub repository created at https://github.com/QRun-IO/qbit-mobile-scanners with `develop` as default. Initial signed commit `abbcc39` is published. Companion Android implementation is in https://github.com/QRun-IO/qqq-android/pull/8.
- Hosted initial-commit checks passed: CircleCI `setup`/`verify`, CodeQL `analyze / Analyze branch source`, demo HTTP acceptance and Windows helper analysis. CircleCI dynamic configuration is enabled. Maven artifact publication remains disabled.
- Runtime branding is configurable on the server before login; the shared Android APK uses the customer name, primary color and optional HTTPS logo. Public branding/status endpoints carry no credentials. Root tests now pass 13 cases; demo HTTP acceptance also passes.
- Windows demo helpers store generated demo passwords using per-user Windows DPAPI outside the repository. An Android 15/API35 emulator is running with WHPX; six native UI instrumentation tests passed. Physical TC22 acceptance is still pending.
- The current workstation demo uses loopback port 57832 because other services occupy the conventional ports. `adb reverse tcp:8080 tcp:57832` preserves the Android server URL `http://127.0.0.1:8080/`. Start it again with `pwsh -File scripts/Start-Demo.ps1 -SkipBuild -Port 57832` after stopping the existing instance. See `LOCAL-DEMO.md` for login helpers.
- The earlier verification/handoff notes below describe the initial implementation before repository publication and emulator setup.
## Confirmed scope
- Native Android host for Zebra TC22 and other handheld scanners, extending `qqq-android`.
+1 -5
View File
@@ -9,11 +9,7 @@
- [x] Android build and meaningful unit checks.
- [x] Standard repository files, current shared CircleCI profile, demo/Windows CI, CodeQL and Dependabot.
- [x] Validate CircleCI, workflow syntax and verification-only publication policy locally.
- [x] Connect hosted CI and confirm actual successful status checks.
- [ ] Finish repository protections after publishing bootstrap additions.
- [x] Server-configured customer branding for one shared APK.
- [x] Public status endpoint for the Android connection footer and diagnostics.
- [x] Android emulator installed; native UI tests passed.
- [ ] Connect hosted CI and confirm actual required status checks and repository protections.
- [ ] TC22 physical scanner acceptance.
- [x] TC22 install and DataWedge acceptance guide written; physical execution remains pending.
@@ -6,9 +6,7 @@
package com.kingsrook.qbits.mobilescanners;
import java.net.URI;
import java.util.List;
import java.util.Locale;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
@@ -19,86 +17,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
public class MobileScannersQBitConfig implements QBitConfig
{
private String backendName;
private String displayName = "QQQ Mobile Scanners";
private String primaryColor = "#1565C0";
private String logoUrl;
/***************************************************************************
** Public customer name shown by the shared mobile client.
***************************************************************************/
public String getDisplayName()
{
return displayName;
}
/***************************************************************************
** Plain display text only, never markup or control characters.
***************************************************************************/
public MobileScannersQBitConfig withDisplayName(String displayName)
{
if(displayName == null || displayName.isBlank() || displayName.length() > 80 || !displayName.equals(displayName.strip())
|| displayName.chars().anyMatch(value -> Character.isISOControl(value) || value == '<' || value == '>'))
{
throw new IllegalArgumentException("Mobile displayName must be 1-80 trimmed plain-text characters.");
}
this.displayName = displayName;
return this;
}
/***************************************************************************
** Opaque RGB theme color; clients choose readable foreground colors.
***************************************************************************/
public String getPrimaryColor()
{
return primaryColor;
}
/***************************************************************************
** Restrict branding to a six-digit hex color, not arbitrary CSS.
***************************************************************************/
public MobileScannersQBitConfig withPrimaryColor(String primaryColor)
{
if(primaryColor == null || !primaryColor.matches("#[0-9a-fA-F]{6}"))
{
throw new IllegalArgumentException("Mobile primaryColor must use #RRGGBB format.");
}
this.primaryColor = primaryColor.toUpperCase(Locale.ROOT);
return this;
}
/***************************************************************************
** Optional public logo; no authenticated assets or embedded data.
***************************************************************************/
public String getLogoUrl()
{
return logoUrl;
}
/***************************************************************************
** The client loads an HTTPS image without forwarding QQQ credentials.
***************************************************************************/
public MobileScannersQBitConfig withLogoUrl(String logoUrl)
{
if(logoUrl != null)
{
URI uri;
try
{
uri = URI.create(logoUrl);
}
catch(IllegalArgumentException e)
{
throw new IllegalArgumentException("Mobile logoUrl must be a public HTTPS URL.");
}
if(logoUrl.length() > 2048 || !"https".equalsIgnoreCase(uri.getScheme()) || uri.getHost() == null
|| uri.getUserInfo() != null || uri.getFragment() != null || uri.getPort() > 65535)
{
throw new IllegalArgumentException("Mobile logoUrl must be a public HTTPS URL without credentials or a fragment.");
}
}
this.logoUrl = logoUrl;
return this;
}
/***************************************************************************
** Read the configured backend.
@@ -6,7 +6,6 @@
package com.kingsrook.qbits.mobilescanners;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
@@ -58,31 +57,11 @@ public class MobileScannersRouteProvider implements QJavalinRouteProviderInterfa
{
ApiBuilder.before("/qqq/*", this::bindDevice);
ApiBuilder.after("/qqq/*", context -> MobilePermissions.REQUEST_DEVICE.remove());
ApiBuilder.get("/mobile/v1/branding", this::branding);
ApiBuilder.get("/mobile/v1/status", context -> context.header("Cache-Control", "no-store").json(Map.of("status", "ok")));
ApiBuilder.post("/mobile/v1/devices/enroll", context -> handle(context, true));
ApiBuilder.get("/mobile/v1/devices/current", context -> handle(context, false));
};
}
/***************************************************************************
** Public, explicitly allowlisted appearance data, available before login.
***************************************************************************/
private void branding(Context context)
{
MobileScannersQBitConfig config = instance.getQBits().values().stream().map(bit -> bit.getConfig())
.filter(MobileScannersQBitConfig.class::isInstance).map(MobileScannersQBitConfig.class::cast)
.findFirst().orElseGet(MobileScannersQBitConfig::new);
Map<String, String> response = new LinkedHashMap<>();
response.put("displayName", config.getDisplayName());
response.put("primaryColor", config.getPrimaryColor());
if(config.getLogoUrl() != null)
{
response.put("logoUrl", config.getLogoUrl());
}
context.header("Cache-Control", "no-store").json(response);
}
/***************************************************************************
** Verify credentials before native handlers; authentication remains QQQ's.
***************************************************************************/
@@ -1,97 +0,0 @@
/*
* Copyright (c) 2026 Kingsrook, LLC
* SPDX-License-Identifier: Apache-2.0
*/
package com.kingsrook.qbits.mobilescanners;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
import io.javalin.Javalin;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*******************************************************************************
** Branding is public appearance data, never an authentication requirement.
*******************************************************************************/
class MobileBrandingTest
{
/***************************************************************************
** Reject markup, controls, CSS and credential-bearing or non-HTTPS logos.
***************************************************************************/
@Test
void rejectsInvalidConfiguration()
{
MobileScannersQBitConfig config = new MobileScannersQBitConfig();
for(String name : List.of("", " ", " name", "name ", "x\ny", "<b>name</b>", "x".repeat(81)))
{
assertThrows(IllegalArgumentException.class, () -> config.withDisplayName(name));
}
for(String color : List.of("red", "#FFF", "#11223344", "#gg2233", " #112233"))
{
assertThrows(IllegalArgumentException.class, () -> config.withPrimaryColor(color));
}
for(String url : List.of("", "/logo.png", "http://example.com/logo.png", "data:image/png;base64,x", "file:///logo.png",
"https://user:password@example.com/logo.png", "https://example.com/logo.png#fragment", "https://example.com:99999/logo.png",
"https://example.com/" + "a".repeat(2048)))
{
assertThrows(IllegalArgumentException.class, () -> config.withLogoUrl(url));
}
assertThrows(IllegalArgumentException.class, () -> config.withDisplayName(null));
assertThrows(IllegalArgumentException.class, () -> config.withPrimaryColor(null));
assertEquals("#AABBCC", config.withPrimaryColor("#aabbcc").getPrimaryColor());
}
/***************************************************************************
** Defaults remain available with no session and stale device credentials.
***************************************************************************/
@Test
void publicDefaultsIgnoreStaleDeviceCredentials() throws Exception
{
QInstance instance = new QInstance();
instance.addQBit(new QBitMetaData().withGroupId("com.kingsrook.qbits").withArtifactId("qbit-mobile-scanners")
.withVersion("0.1.0-SNAPSHOT").withNamespace("mobile").withConfig(new MobileScannersQBitConfig().withBackendName("privateBackend")));
MobileScannersRouteProvider provider = new MobileScannersRouteProvider(instance);
Javalin server = Javalin.create(config ->
{
config.jetty.host = "127.0.0.1";
config.routes.apiBuilder(provider.getJavalinEndpointGroup());
}).start(0);
try
{
HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + server.port() + "/mobile/v1/branding"))
.timeout(Duration.ofSeconds(5)).header(MobileScannersRouteProvider.DEVICE_HEADER, "stale")
.header(MobileScannersRouteProvider.TOKEN_HEADER, "invalid").GET().build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertEquals("no-store", response.headers().firstValue("Cache-Control").orElseThrow());
JSONObject branding = new JSONObject(response.body());
assertEquals(2, branding.length());
assertEquals("QQQ Mobile Scanners", branding.getString("displayName"));
assertEquals("#1565C0", branding.getString("primaryColor"));
HttpRequest heartbeat = HttpRequest.newBuilder(URI.create("http://127.0.0.1:" + server.port() + "/mobile/v1/status"))
.timeout(Duration.ofSeconds(5)).header(MobileScannersRouteProvider.DEVICE_HEADER, "stale")
.header(MobileScannersRouteProvider.TOKEN_HEADER, "invalid").GET().build();
var status = HttpClient.newHttpClient().send(heartbeat, HttpResponse.BodyHandlers.ofString());
assertEquals(200, status.statusCode());
assertEquals("no-store", status.headers().firstValue("Cache-Control").orElseThrow());
JSONObject health = new JSONObject(status.body());
assertEquals(1, health.length());
assertEquals("ok", health.getString("status"));
}
finally
{
server.stop();
}
}
}