fix(GH-21): follow listener address for the API catalog
This commit is contained in:
6 files changed
+104
-6
No files matched your search
@@ -68,6 +68,10 @@ API key, script editor, or user-supplied code is needed.
|
||||
| `calculateOrderTotal` | Executes fixed JavaScript through `ExecuteCodeAction`: quantity × unit price in cents → `totalCents`. It changes no records. | Admin only. |
|
||||
| `demoNote` | Memory-backed table, seeded with `Transient demo note` on every start and cleared on shutdown. | Admin can edit; Demo/Viewer can read. |
|
||||
|
||||
The internal catalog URL follows `QQQ_ALL_BIND_HOST` and the configured HTTP port.
|
||||
IPv4/IPv6 wildcard listeners use their corresponding loopback address; literal
|
||||
IPv6 hosts are bracketed correctly in the URL.
|
||||
|
||||
The catalog endpoint exposes **only synthetic public demo data**. It is not an
|
||||
outbound proxy and does not forward session credentials. The adapter supports
|
||||
ascending ID order, limit/skip pagination, and one exact ID filter; unsupported
|
||||
|
||||
@@ -45,7 +45,7 @@ final class CoreRuntime implements AutoCloseable
|
||||
Path brokerDirectory = dataDirectory.resolve("artemis").toAbsolutePath();
|
||||
String brokerUrl = "tcp://127.0.0.1:" + brokerPort;
|
||||
DemoOrderWebhookReceiver.clear();
|
||||
DemoQBitData.ensureSchema(dataDirectory, new QqqAllApplication(dataDirectory, brokerUrl, true, port).defineQInstance(), port);
|
||||
DemoQBitData.ensureSchema(dataDirectory, new QqqAllApplication(dataDirectory, brokerUrl, true, port, bindHost).defineQInstance(), port);
|
||||
Configuration configuration = new ConfigurationImpl()
|
||||
.setPersistenceEnabled(false).setSecurityEnabled(false).setJMXManagementEnabled(false)
|
||||
.setBindingsDirectory(brokerDirectory.resolve("bindings").toString())
|
||||
@@ -58,7 +58,7 @@ final class CoreRuntime implements AutoCloseable
|
||||
try
|
||||
{
|
||||
QApplicationLauncher launcher = QApplicationLauncher.run(
|
||||
new QqqAllApplication(dataDirectory, brokerUrl, true, port),
|
||||
new QqqAllApplication(dataDirectory, brokerUrl, true, port, bindHost),
|
||||
new QApplicationLauncherConfig().withRegisterShutdownHook(false)
|
||||
.withServerCustomizer(server -> server.withPort(port)
|
||||
.withJavalinConfigCustomizer(config -> config.jetty.host = bindHost)));
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
*/
|
||||
package com.kingsrook.qqq.all;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
@@ -33,10 +39,10 @@ final class DemoRuntimeFeatures
|
||||
{
|
||||
}
|
||||
|
||||
static void add(QInstance instance, int port)
|
||||
static void add(QInstance instance, int port, String bindHost) throws QException
|
||||
{
|
||||
instance.addBackend(new APIBackendMetaData().withName("demoApi")
|
||||
.withBaseUrl("http://127.0.0.1:" + port + "/demo")
|
||||
.withBaseUrl(catalogBaseUrl(bindHost, port))
|
||||
.withAuthorizationType(AuthorizationType.NONE).withContentType("application/json")
|
||||
.withActionUtil(new QCodeReference(DemoCatalogApi.class)));
|
||||
instance.addTable(new QTableMetaData().withName("apiCatalog").withLabel("API Catalog")
|
||||
@@ -65,4 +71,20 @@ final class DemoRuntimeFeatures
|
||||
new QFieldMetaData("totalCents", QFieldType.INTEGER))))));
|
||||
QJavalinMetaData.ofOrWithNew(instance).withAdditionalRouteProviderReference(new QCodeReference(DemoCatalogRoutes.class));
|
||||
}
|
||||
|
||||
private static String catalogBaseUrl(String bindHost, int port) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
InetAddress address = InetAddress.getByName(bindHost);
|
||||
String host = address.isAnyLocalAddress()
|
||||
? (address instanceof Inet6Address ? "::1" : "127.0.0.1") : bindHost;
|
||||
return new URI("http", null, host, port, "/demo", null, null).toASCIIString();
|
||||
}
|
||||
catch(UnknownHostException | URISyntaxException e)
|
||||
{
|
||||
throw new QException("Could not derive demo catalog URL from HTTP bind host", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -67,7 +67,7 @@ final class FullProfileApplication extends AbstractQQQApplication
|
||||
@Override
|
||||
public QInstance defineQInstance() throws QException
|
||||
{
|
||||
QInstance instance = new QqqAllApplication(dataDirectory, settings.get("ARTEMIS_URL"), false, settings.port("PORT", "8080")).defineQInstance();
|
||||
QInstance instance = new QqqAllApplication(dataDirectory, settings.get("ARTEMIS_URL"), false, settings.port("PORT", "8080"), settings.get("BIND_HOST")).defineQInstance();
|
||||
addBackends(instance);
|
||||
addTables(instance);
|
||||
addAuthentication(instance);
|
||||
|
||||
@@ -59,6 +59,7 @@ public final class QqqAllApplication extends AbstractQQQApplication
|
||||
private final String brokerUrl;
|
||||
private final boolean includeDemoQbits;
|
||||
private final int httpPort;
|
||||
private final String bindHost;
|
||||
|
||||
public QqqAllApplication(Path dataDirectory, String brokerUrl)
|
||||
{
|
||||
@@ -72,6 +73,12 @@ public final class QqqAllApplication extends AbstractQQQApplication
|
||||
|
||||
QqqAllApplication(Path dataDirectory, String brokerUrl, boolean includeDemoQbits, int httpPort)
|
||||
{
|
||||
this(dataDirectory, brokerUrl, includeDemoQbits, httpPort, "127.0.0.1");
|
||||
}
|
||||
|
||||
QqqAllApplication(Path dataDirectory, String brokerUrl, boolean includeDemoQbits, int httpPort, String bindHost)
|
||||
{
|
||||
this.bindHost = bindHost;
|
||||
this.httpPort = httpPort;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.brokerUrl = brokerUrl;
|
||||
@@ -159,7 +166,7 @@ public final class QqqAllApplication extends AbstractQQQApplication
|
||||
QJavalinMetaData.ofOrWithNew(instance)
|
||||
.withAdditionalRouteProviderReference(new QCodeReference(DemoOrderWebhookReceiver.class));
|
||||
}
|
||||
DemoRuntimeFeatures.add(instance, httpPort);
|
||||
DemoRuntimeFeatures.add(instance, httpPort, bindHost);
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -101,6 +103,69 @@ class CoreJarIT
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"::1", "::", "0:0:0:0:0:0:0:0", "0.0.0.0"})
|
||||
void packagedCatalogUsesConfiguredListener(String bindHost) throws Exception
|
||||
{
|
||||
String clientHost = bindHost.contains(":") ? "::1" : "127.0.0.1";
|
||||
int port;
|
||||
try(ServerSocket socket = new ServerSocket(0, 50, java.net.InetAddress.getByName(clientHost)))
|
||||
{
|
||||
port = socket.getLocalPort();
|
||||
}
|
||||
URI base = new URI("http", null, clientHost, port, "/", null, null);
|
||||
Path log = dataDirectory.resolve("bound-app.log");
|
||||
ProcessBuilder builder = new ProcessBuilder(Path.of(System.getProperty("java.home"), "bin", "java").toString(),
|
||||
"-jar", System.getProperty("qqqAllJar"));
|
||||
builder.environment().put("QQQ_ALL_PROFILE", "core");
|
||||
builder.environment().put("QQQ_ALL_BIND_HOST", bindHost);
|
||||
builder.environment().put("QQQ_ALL_PORT", Integer.toString(port));
|
||||
builder.environment().put("QQQ_ALL_DATA_DIR", dataDirectory.resolve("bound-data").toString());
|
||||
builder.redirectErrorStream(true).redirectOutput(log.toFile());
|
||||
Process process = builder.start();
|
||||
try(HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build())
|
||||
{
|
||||
assertEquals(200, awaitHealth(client, base.resolve("health"), process, log).statusCode());
|
||||
var direct = client.send(HttpRequest.newBuilder(base.resolve("demo/catalog"))
|
||||
.timeout(Duration.ofSeconds(10)).build(), HttpResponse.BodyHandlers.ofString());
|
||||
assertEquals(200, direct.statusCode(), "Configured listener must serve its catalog before testing the backend");
|
||||
assertEquals(2, new org.json.JSONArray(direct.body()).length());
|
||||
|
||||
var all = catalogQuery(client, base, "{}");
|
||||
assertEquals(java.util.List.of(101, 102, 103), all.toList().stream()
|
||||
.map(row -> ((java.util.Map<?, ?>) ((java.util.Map<?, ?>) row).get("values")).get("id")).toList());
|
||||
for(int skip = 0; skip < 3; skip++)
|
||||
{
|
||||
var page = catalogQuery(client, base, "{\"filter\":{\"skip\":" + skip + ",\"limit\":1}}");
|
||||
assertEquals(1, page.length());
|
||||
assertEquals(java.util.List.of(101, 102, 103).get(skip), page.getJSONObject(0).getJSONObject("values").getInt("id"));
|
||||
}
|
||||
assertEquals(0, catalogQuery(client, base, "{\"filter\":{\"skip\":3,\"limit\":1}}").length());
|
||||
var get = client.send(HttpRequest.newBuilder(base.resolve("data/apiCatalog/103"))
|
||||
.timeout(Duration.ofSeconds(10)).build(), HttpResponse.BodyHandlers.ofString());
|
||||
assertEquals(200, get.statusCode(), get.body());
|
||||
assertEquals("Shipping Box", new org.json.JSONObject(get.body()).getJSONObject("values").getString("name"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.destroy();
|
||||
if(!process.waitFor(5, TimeUnit.SECONDS))
|
||||
{
|
||||
process.destroyForcibly();
|
||||
assertTrue(process.waitFor(5, TimeUnit.SECONDS), "Owned packaged process must stop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static org.json.JSONArray catalogQuery(HttpClient client, URI base, String body) throws Exception
|
||||
{
|
||||
var response = client.send(HttpRequest.newBuilder(base.resolve("qqq/v1/table/apiCatalog/query"))
|
||||
.timeout(Duration.ofSeconds(10)).header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
|
||||
assertEquals(200, response.statusCode(), response.body());
|
||||
return new org.json.JSONObject(response.body()).getJSONArray("records");
|
||||
}
|
||||
|
||||
private static HttpResponse<String> awaitHealth(HttpClient client, URI uri, Process process, Path log) throws Exception
|
||||
{
|
||||
long deadline = System.nanoTime() + Duration.ofSeconds(30).toNanos();
|
||||
|
||||
Reference in new issue
Block a user