Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07d72ca4ea
|
||
|
|
7739a7da58
|
||
|
|
51eb6f0df2
|
||
|
|
d88dd1745b
|
||
|
|
0c26f0b2a6
|
||
|
|
9614d176a8
|
No files matched your search
@@ -0,0 +1,22 @@
|
||||
# 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,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/develop/scanner-host).
|
||||
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).
|
||||
|
||||
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,6 +24,12 @@ 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,6 +37,7 @@ 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;
|
||||
|
||||
|
||||
@@ -49,6 +50,29 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -81,7 +105,7 @@ public class MobileScannersDemo extends AbstractQQQApplication
|
||||
instance.addTable(table);
|
||||
}
|
||||
instance.getTable("demoUser").getField("passwordHash").setIsHidden(true);
|
||||
new MobileScannersQBitProducer().withConfig(new MobileScannersQBitConfig().withBackendName("mobileDb")).produce(instance, "mobile");
|
||||
new MobileScannersQBitProducer().withConfig(mobileConfig).produce(instance, "mobile");
|
||||
for(String name : List.of(MobileScannersQBitProducer.DEVICE, MobileScannersQBitProducer.CREDENTIAL, MobileScannersQBitProducer.SCAN))
|
||||
{
|
||||
QTableMetaData table = instance.getTable(name);
|
||||
@@ -184,6 +208,7 @@ 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);
|
||||
}
|
||||
|
||||
+9
-1
@@ -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);
|
||||
MobileScannersDemo application = new MobileScannersDemo(database).withBranding("Warehouse Mobile", "#007a55", "https://example.com/logo.png");
|
||||
application.initializeDatabase(scannerPassword, adminPassword);
|
||||
Integer port;
|
||||
try(ServerSocket socket = new ServerSocket(0))
|
||||
@@ -61,6 +61,14 @@ 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);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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
@@ -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`, `release/*` and release tags. Require pull requests, code-owner review, resolved conversations and successful applicable checks; block force pushes/deletion.
|
||||
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.
|
||||
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.
|
||||
|
||||
+3
-1
@@ -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/main/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/feature/mobile-scanner-host/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,3 +36,5 @@ 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.
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
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`.
|
||||
|
||||
+5
-1
@@ -9,7 +9,11 @@
|
||||
- [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.
|
||||
- [ ] Connect hosted CI and confirm actual required status checks and repository protections.
|
||||
- [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.
|
||||
- [ ] TC22 physical scanner acceptance.
|
||||
- [x] TC22 install and DataWedge acceptance guide written; physical execution remains pending.
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
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;
|
||||
|
||||
@@ -17,6 +19,86 @@ 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,6 +6,7 @@
|
||||
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;
|
||||
@@ -57,11 +58,31 @@ 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.
|
||||
***************************************************************************/
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user