This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph-toolchain.git
The following commit(s) were added to refs/heads/master by this push:
new eac4707dd feat(hubble): use bearer token for authenticated clients
(#753)
eac4707dd is described below
commit eac4707dd8d252aa0500808893c3226a80dc3797
Author: imbajin <[email protected]>
AuthorDate: Fri Aug 7 23:10:39 2026 +0800
feat(hubble): use bearer token for authenticated clients (#753)
- send Hubble JWT credentials with the standard Bearer scheme
- preserve Bearer authentication when reusing request-scoped clients
- verify the real HugeClient request header sent to the Gremlin endpoint
---
hugegraph-hubble/Dockerfile | 13 ++
.../hugegraph/controller/BaseController.java | 7 +-
.../controller/ingest/IngestController.java | 45 +++++-
.../controller/saas/SaaSMetricsController.java | 4 +-
.../hugegraph/service/graphs/GraphsService.java | 13 +-
.../hugegraph/service/load/LoadTaskService.java | 1 +
.../hugegraph/service/space/GraphSpaceService.java | 27 +++-
.../org/apache/hugegraph/util/HugeClientUtil.java | 38 ++++-
.../controller/ingest/IngestControllerTest.java | 173 +++++++++++++++++++++
.../controller/saas/SaaSMetricsControllerTest.java | 74 +++++++++
.../service/space/GraphSpaceServiceTest.java | 41 +++++
.../unit/BaseControllerGremlinClientTest.java | 42 +++++
.../hugegraph/unit/GraphsServiceDefaultTest.java | 30 ++++
.../apache/hugegraph/unit/LoadTaskServiceTest.java | 34 +++-
.../apache/hugegraph/util/HugeClientUtilTest.java | 153 ++++++++++++++++++
.../src/test/python/check_server_status.feature | 2 +-
.../src/test/python/steps/check_server_status.py | 12 +-
.../assembly/static/bin/common_functions | 29 +++-
.../assembly/static/bin/start-hubble.sh | 6 +-
.../static/conf/hugegraph-hubble.properties | 4 +-
.../assembly/travis/run_live_hubble_smoke.py | 22 +--
.../assembly/travis/run_ui_browser_smoke.js | 126 +++++++++++----
.../assembly/travis/run_ui_browser_smoke.test.js | 143 +++++++++++++++++
.../hubble-dist/assembly/travis/start-hubble.sh | 6 +-
.../assembly/travis/test_run_live_hubble_smoke.py | 36 ++++-
.../src/i18n/empty-result-copy-context.test.js | 66 ++++++++
.../src/i18n/resources/en-US/modules/analysis.json | 3 +-
.../src/i18n/resources/zh-CN/modules/analysis.json | 3 +-
.../modules/algorithm/GraphResult/Home/index.js | 2 +-
.../algorithm/GraphResult/RankApiView/index.js | 2 +-
.../hugegraph/loader/executor/LoadOptions.java | 7 +-
.../loader/reader/file/FileLineFetcher.java | 1 +
.../hugegraph/loader/source/file/FileSource.java | 36 ++++-
.../loader/test/unit/FileLineFetcherTest.java | 110 +++++++++++++
.../loader/test/unit/LoadOptionsTest.java | 27 ++++
35 files changed, 1236 insertions(+), 102 deletions(-)
diff --git a/hugegraph-hubble/Dockerfile b/hugegraph-hubble/Dockerfile
index 97f72ed57..b1d5b3c19 100644
--- a/hugegraph-hubble/Dockerfile
+++ b/hugegraph-hubble/Dockerfile
@@ -38,7 +38,20 @@ RUN set -x \
FROM eclipse-temurin:11-jre-jammy
+RUN apt-get -q update \
+ && apt-get install --no-install-recommends curl -yq \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
COPY --from=build /pkg/hugegraph-hubble/apache-hugegraph-hubble-*/ /hubble
+RUN sed -i \
+ -e 's/^server\.host=.*/server.host=0.0.0.0/' \
+ -e 's/^dashboard\.address=.*/dashboard.address=/' \
+ /hubble/conf/hugegraph-hubble.properties \
+ && grep -Fqx 'server.host=0.0.0.0' \
+ /hubble/conf/hugegraph-hubble.properties \
+ && grep -Fqx 'dashboard.address=' \
+ /hubble/conf/hugegraph-hubble.properties
WORKDIR /hubble/
# SECURITY: This is a plain HTTP port. Do not publish it to an untrusted
network;
diff --git
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
index 1d2fc210b..807da39fc 100644
---
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
+++
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
@@ -42,6 +42,7 @@ import org.apache.hugegraph.common.Identifiable;
import org.apache.hugegraph.common.Mergeable;
import org.apache.hugegraph.util.EntityUtil;
import org.apache.hugegraph.util.Ex;
+import org.apache.hugegraph.util.HugeClientUtil;
@Component
public abstract class BaseController {
@@ -196,7 +197,11 @@ public abstract class BaseController {
HttpServletRequest request = getRequest();
if (request.getAttribute("hugeClient") != null) {
HugeClient client = (HugeClient)
request.getAttribute("hugeClient");
- client.setAuthContext("Basic " + this.getToken());
+ String token = this.getToken();
+ if (org.apache.commons.lang3.StringUtils.isNotBlank(token)) {
+ client.setAuthContext(
+ HugeClientUtil.bearerAuthContext(token));
+ }
return client;
}
HugeClient client =
this.hugeClientPoolService.createTempTokenClient(this.getToken());
diff --git
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
index d0d6c201c..b464063c4 100644
---
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
+++
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
@@ -19,6 +19,9 @@ package org.apache.hugegraph.controller.ingest;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
@@ -45,6 +48,7 @@ import org.apache.hugegraph.entity.load.NullValues;
import org.apache.hugegraph.entity.load.ValueMappingItem;
import org.apache.hugegraph.entity.load.VertexMapping;
import org.apache.hugegraph.exception.InternalException;
+import org.apache.hugegraph.loader.source.file.FileFormat;
import org.apache.hugegraph.options.HubbleOptions;
import org.apache.hugegraph.service.load.DatasourceService;
import org.apache.hugegraph.service.load.FileMappingService;
@@ -76,6 +80,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -88,6 +93,10 @@ import java.util.stream.Collectors;
@RequestMapping(Constant.API_VERSION + "ingest")
public class IngestController extends BaseController {
+ private static final ObjectMapper JSON_MAPPER =
+ new ObjectMapper().disable(
+ JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION);
+
@Autowired
private JobManagerService jobManagerService;
@Autowired
@@ -193,10 +202,11 @@ public class IngestController extends BaseController {
.data(header).build();
}
+ boolean hasPhysicalHeader = this.hasPhysicalHeader(config);
FileSetting setting = this.buildFileSetting(config,
Collections.emptyList(),
- false);
+ hasPhysicalHeader);
ColumnInfo columns = this.readColumns(this.requireUploadFile(config),
- setting, false);
+ setting, hasPhysicalHeader);
return Response.builder().status(Constant.STATUS_OK)
.data(columns.names).build();
}
@@ -234,8 +244,7 @@ public class IngestController extends BaseController {
File sourceFile = this.requireUploadFile(input);
long totalSize = sourceFile.length();
List<String> header = this.stringList(input.get("header"));
- boolean hasPhysicalHeader = !this.stringList(dsConfig.get("header"))
- .isEmpty();
+ boolean hasPhysicalHeader = this.hasPhysicalHeader(dsConfig);
FileSetting setting = this.buildFileSetting(input, header,
hasPhysicalHeader);
if (setting.getColumnNames() == null ||
@@ -548,6 +557,13 @@ public class IngestController extends BaseController {
return setting;
}
+ private boolean hasPhysicalHeader(Map<String, Object> config) {
+ FileFormat format = FileFormat.valueOf(
+ this.stringOrDefault(config.get("format"), "CSV"));
+ return format.needHeader() &&
+ this.stringList(config.get("header")).isEmpty();
+ }
+
private ColumnInfo readColumns(File file, FileSetting setting,
boolean hasPhysicalHeader) {
try (BufferedReader reader = Files.newBufferedReader(file.toPath())) {
@@ -559,6 +575,10 @@ public class IngestController extends BaseController {
}
Ex.check(line != null, "The file has no data line can treat as
header");
+ if (FileFormat.JSON.name().equals(setting.getFormat())) {
+ return this.readJsonColumns(line, file);
+ }
+
List<String> firstLine = this.splitLine(line,
setting.getDelimiter());
if (hasPhysicalHeader) {
String sample = reader.readLine();
@@ -579,6 +599,23 @@ public class IngestController extends BaseController {
}
}
+ private ColumnInfo readJsonColumns(String line, File file) {
+ try {
+ Map<String, Object> fields = JSON_MAPPER.readValue(
+ line, new TypeReference<LinkedHashMap<String, Object>>() {
+ });
+ List<String> names = new ArrayList<>(fields.keySet());
+ List<String> values = names.stream()
+ .map(fields::get)
+ .map(String::valueOf)
+ .collect(Collectors.toList());
+ return new ColumnInfo(names, values);
+ } catch (IOException ignored) {
+ throw new InternalException(
+ "Failed to read JSON fields from file %s", file);
+ }
+ }
+
private List<String> splitLine(String line, String delimiter) {
if (line == null) {
return Collections.emptyList();
diff --git
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java
index cd23903a4..31f8a3fa7 100644
---
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java
+++
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaaSMetricsController.java
@@ -285,13 +285,13 @@ public class SaaSMetricsController extends BaseController
{
private long vertexLabelCount;
@JsonProperty("vertex-count")
- private long vertexCount;
+ private Long vertexCount;
@JsonProperty("edge-label-count")
private long edgeLabelCount;
@JsonProperty("edge-count")
- private long edgeCount;
+ private Long edgeCount;
@JsonProperty("graph-space-count")
private long graphSpaceCount;
diff --git
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java
index 63d280868..6361aed2c 100644
---
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java
+++
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/graphs/GraphsService.java
@@ -42,6 +42,7 @@ import org.apache.hugegraph.entity.query.GremlinQuery;
import org.apache.hugegraph.entity.space.BuiltInEntity;
import org.apache.hugegraph.exception.ServerException;
import org.apache.hugegraph.loader.util.JsonUtil;
+import org.apache.hugegraph.options.HubbleOptions;
import org.apache.hugegraph.service.algorithm.AsyncTaskService;
import org.apache.hugegraph.service.auth.UserService;
import org.apache.hugegraph.service.load.LoadTaskService;
@@ -784,11 +785,15 @@ public class GraphsService {
Long vertexCount = null;
String statisticDate = HubbleUtil.dateFormatDay(HubbleUtil.nowDate());
client.assignGraph(graphSpace, graph);
- GraphMetricsAPI.ElementCount statistic =
- client.graph().getEVCount(statisticDate);
- if (statistic == null) {
- statisticDate = HubbleUtil.dateFormatLastDay();
+ GraphMetricsAPI.ElementCount statistic = null;
+ boolean pdEnabled = this.config != null &&
+ this.config.get(HubbleOptions.PD_ENABLED);
+ if (!pdEnabled) {
statistic = client.graph().getEVCount(statisticDate);
+ if (statistic == null) {
+ statisticDate = HubbleUtil.dateFormatLastDay();
+ statistic = client.graph().getEVCount(statisticDate);
+ }
}
if (statistic != null) {
diff --git
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java
index 12678e378..a07a09bd2 100644
---
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java
+++
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/LoadTaskService.java
@@ -557,6 +557,7 @@ public class LoadTaskService {
Ex.check(setting.getColumnNames() != null,
"Must do file setting firstly");
source.header(setting.getColumnNames().toArray(new String[]{}));
+ source.hasHeader(setting.isHasHeader());
// NOTE: format and delimiter must be CSV and "," temporarily
source.format(FileFormat.valueOf(setting.getFormat()));
source.delimiter(setting.getDelimiter());
diff --git
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
index 3b3813fb8..b5b85dca5 100644
---
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
+++
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
@@ -72,8 +72,8 @@ public class GraphSpaceService {
public Map<String, Long> metrics(HugeClient client) {
long gsCount = 0L;
long gCount = 0L;
- long vCount = 0L;
- long eCount = 0L;
+ Long vCount = 0L;
+ Long eCount = 0L;
long vlCount = 0L;
long elCount = 0L;
long preDayTaskCount = 0L;
@@ -86,8 +86,10 @@ public class GraphSpaceService {
Map<String, Object> elVl = elAndVlCount(client, gs);
Map<String, Object> task = preDayTaskCount(client, gs);
- vCount += ((Number) ev.get("vertex")).longValue();
- eCount += ((Number) ev.get("edge")).longValue();
+ vCount = addAvailableCount(vCount,
+ (Number) ev.get("vertex"));
+ eCount = addAvailableCount(eCount,
+ (Number) ev.get("edge"));
vlCount += ((Number) elVl.get("vertexlabel")).longValue();
elCount += ((Number) elVl.get("edgelabel")).longValue();
preDayTaskCount += ((Number) task.get("task")).longValue();
@@ -199,8 +201,8 @@ public class GraphSpaceService {
* @return
*/
Map<String, Object> evCount(HugeClient client, String graphSpace) {
- long vertexTotal = 0L;
- long edgeTotal = 0L;
+ Long vertexTotal = 0L;
+ Long edgeTotal = 0L;
Map<String, Object> statisticTotal = new HashMap<>();
client.assignGraph(graphSpace, "");
Set<String> graphs = graphsService.listGraphNames(client, graphSpace,
"");
@@ -219,8 +221,10 @@ public class GraphSpaceService {
statisticDate = null;
}
- vertexTotal += ((Number) graphEvCount.get("vertex")).longValue();
- edgeTotal += ((Number) graphEvCount.get("edge")).longValue();
+ Number vertexCount = (Number) graphEvCount.get("vertex");
+ Number edgeCount = (Number) graphEvCount.get("edge");
+ vertexTotal = addAvailableCount(vertexTotal, vertexCount);
+ edgeTotal = addAvailableCount(edgeTotal, edgeCount);
}
if (graphs.isEmpty()) {
statisticDate = HubbleUtil.dateFormatDay(HubbleUtil.nowDate());
@@ -232,6 +236,13 @@ public class GraphSpaceService {
return statisticTotal;
}
+ private static Long addAvailableCount(Long total, Number count) {
+ if (total == null || count == null) {
+ return null;
+ }
+ return Math.addExact(total, count.longValue());
+ }
+
/**
* 统计指定图空间下的edgeLabel总数和vertexLabel边总数
* @param client
diff --git
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java
index f3e6b7f34..e7258ab26 100644
---
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java
+++
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/util/HugeClientUtil.java
@@ -34,6 +34,7 @@ import com.google.common.collect.ImmutableSet;
public final class HugeClientUtil {
private static final String DEFAULT_PROTOCOL = "http";
+ private static final String BEARER_SCHEME = "Bearer";
private static final Set<String> ACCEPTABLE_EXCEPTIONS = ImmutableSet.of(
"Permission denied: execute Resource"
@@ -44,7 +45,12 @@ public final class HugeClientUtil {
String graph = connection.getGraph();
String host = connection.getHost();
Integer port = connection.getPort();
- String token = normalizeToken(connection.getToken());
+ String tokenInput = connection.getToken();
+ String token = normalizeTokenPayload(tokenInput);
+ String authContext = null;
+ if (StringUtils.isNotBlank(tokenInput)) {
+ authContext = bearerAuthContext(tokenInput);
+ }
String username = connection.getUsername();
String password = connection.getPassword();
int timeout = connection.getTimeout();
@@ -112,14 +118,36 @@ public final class HugeClientUtil {
throw e;
}
+ if (authContext != null) {
+ client.setAuthContext(authContext);
+ }
+
return client;
}
- private static String normalizeToken(String token) {
- if (StringUtils.isBlank(token) || token.startsWith(" ")) {
- return token;
+ public static String bearerAuthContext(String token) {
+ String normalized = normalizeTokenPayload(token);
+ if (StringUtils.isBlank(normalized)) {
+ throw new IllegalArgumentException(
+ "Bearer token must contain a non-blank payload");
+ }
+ return BEARER_SCHEME + " " + normalized;
+ }
+
+ private static String normalizeTokenPayload(String token) {
+ String normalized = StringUtils.stripToNull(token);
+ if (normalized == null) {
+ return null;
+ }
+ int schemeLength = BEARER_SCHEME.length();
+ if (normalized.length() >= schemeLength &&
+ normalized.regionMatches(true, 0, BEARER_SCHEME, 0, schemeLength)
&&
+ (normalized.length() == schemeLength ||
+ Character.isWhitespace(normalized.charAt(schemeLength)))) {
+ normalized = StringUtils.stripToNull(
+ normalized.substring(schemeLength));
}
- return " " + token;
+ return normalized;
}
private static boolean isAcceptable(String message) {
diff --git
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
index 79a03d224..639bc341b 100644
---
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
+++
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
@@ -45,6 +45,7 @@ import org.apache.hugegraph.entity.enums.JobStatus;
import org.apache.hugegraph.entity.enums.LoadStatus;
import org.apache.hugegraph.entity.GraphConnection;
import org.apache.hugegraph.entity.load.Datasource;
+import org.apache.hugegraph.entity.load.FileSetting;
import org.apache.hugegraph.entity.load.FileMapping;
import org.apache.hugegraph.entity.load.JobManager;
import org.apache.hugegraph.entity.load.LoadTask;
@@ -123,6 +124,7 @@ public class IngestControllerTest {
mapping.getFileStatus());
Assert.assertEquals(Collections.singletonList("name"),
mapping.getFileSetting().getColumnNames());
+ Assert.assertFalse(mapping.getFileSetting().isHasHeader());
Assert.assertEquals(Collections.singletonList("name"),
mapping.getVertexMappings().iterator().next()
.getIdFields());
@@ -130,6 +132,130 @@ public class IngestControllerTest {
.getJobStatus());
}
+ @Test
+ public void testDatasourceSchemaUsesPhysicalHeaderWhenNamesOmitted()
+ throws Exception {
+ Path uploadRoot = Files.createTempDirectory("hubble-ingest-schema");
+ Path dataFile = uploadRoot.resolve("data.csv");
+ Files.write(dataFile, List.of("name", "Carol"));
+
+ IngestController controller = new IngestController();
+ DatasourceService datasourceService =
+ Mockito.mock(DatasourceService.class);
+ FileMappingService fileMappingService =
+ Mockito.mock(FileMappingService.class);
+ this.setField(controller, "datasourceService", datasourceService);
+ this.setField(controller, "fileMappingService", fileMappingService);
+
+ Datasource datasource = new Datasource();
+ datasource.setId(1);
+ Map<String, Object> datasourceConfig = new HashMap<>();
+ datasourceConfig.put("type", "FILE");
+ datasourceConfig.put("path", dataFile.toString());
+ datasourceConfig.put("format", "CSV");
+ datasource.setDatasourceConfig(datasourceConfig);
+ Mockito.when(datasourceService.get(1)).thenReturn(datasource);
+ Mockito.when(fileMappingService.requirePathUnderUploadRoot(
+ dataFile.toString())).thenReturn(dataFile.toFile());
+
+ Response response = controller.datasourceSchema(1);
+
+ Assert.assertEquals(Constant.STATUS_OK, response.getStatus());
+ Assert.assertEquals(Collections.singletonList("name"),
+ response.getData());
+ }
+
+ @Test
+ public void testDatasourceSchemaUsesJsonObjectKeys()
+ throws Exception {
+ Path uploadRoot =
Files.createTempDirectory("hubble-ingest-json-schema");
+ Path dataFile = uploadRoot.resolve("data.json");
+ Files.write(dataFile,
+
Collections.singletonList("{\"name\":\"Carol\",\"age\":1}"));
+
+ IngestController controller = new IngestController();
+ DatasourceService datasourceService =
+ Mockito.mock(DatasourceService.class);
+ FileMappingService fileMappingService =
+ Mockito.mock(FileMappingService.class);
+ this.setField(controller, "datasourceService", datasourceService);
+ this.setField(controller, "fileMappingService", fileMappingService);
+
+ Datasource datasource = new Datasource();
+ datasource.setId(1);
+ Map<String, Object> datasourceConfig = new HashMap<>();
+ datasourceConfig.put("type", "FILE");
+ datasourceConfig.put("path", dataFile.toString());
+ datasourceConfig.put("format", "JSON");
+ datasource.setDatasourceConfig(datasourceConfig);
+ Mockito.when(datasourceService.get(1)).thenReturn(datasource);
+ Mockito.when(fileMappingService.requirePathUnderUploadRoot(
+ dataFile.toString())).thenReturn(dataFile.toFile());
+
+ Response response = controller.datasourceSchema(1);
+
+ Assert.assertEquals(Constant.STATUS_OK, response.getStatus());
+ Assert.assertEquals(List.of("name", "age"), response.getData());
+ }
+
+ @Test
+ public void testDatasourceSchemaDoesNotEchoMalformedJson()
+ throws Exception {
+ Path uploadRoot = Files.createTempDirectory("hubble-ingest-bad-json");
+ Path dataFile = uploadRoot.resolve("data.json");
+ String canary = "private-json-canary";
+ Files.write(dataFile, Collections.singletonList(
+ "{\"password\":\"" + canary + "\""));
+
+ IngestController controller = new IngestController();
+ DatasourceService datasourceService =
+ Mockito.mock(DatasourceService.class);
+ FileMappingService fileMappingService =
+ Mockito.mock(FileMappingService.class);
+ this.setField(controller, "datasourceService", datasourceService);
+ this.setField(controller, "fileMappingService", fileMappingService);
+
+ Datasource datasource = new Datasource();
+ datasource.setId(1);
+ Map<String, Object> datasourceConfig = new HashMap<>();
+ datasourceConfig.put("type", "FILE");
+ datasourceConfig.put("path", dataFile.toString());
+ datasourceConfig.put("format", "JSON");
+ datasource.setDatasourceConfig(datasourceConfig);
+ Mockito.when(datasourceService.get(1)).thenReturn(datasource);
+ Mockito.when(fileMappingService.requirePathUnderUploadRoot(
+ dataFile.toString())).thenReturn(dataFile.toFile());
+
+ try {
+ controller.datasourceSchema(1);
+ Assert.fail("Expected malformed JSON to be rejected");
+ } catch (Exception e) {
+ Assert.assertFalse(e.toString().contains(canary));
+ }
+ }
+
+ @Test
+ public void testCreateFileTaskMarksDetectedHeaderAsPhysical()
+ throws Exception {
+ FileSetting setting = this.captureFileSetting(
+ List.of("name", "Carol"), "CSV");
+
+ Assert.assertTrue(setting.isHasHeader());
+ Assert.assertEquals(Collections.singletonList("name"),
+ setting.getColumnNames());
+ }
+
+ @Test
+ public void testCreateJsonFileTaskDoesNotMarkPhysicalHeader()
+ throws Exception {
+ FileSetting setting = this.captureFileSetting(
+ Collections.singletonList("{\"name\":\"Carol\"}"), "JSON");
+
+ Assert.assertFalse(setting.isHasHeader());
+ Assert.assertEquals(Collections.singletonList("name"),
+ setting.getColumnNames());
+ }
+
@Test
public void testCreateFileTaskRejectsEmptyMappingBeforePersistence()
throws Exception {
@@ -262,6 +388,53 @@ public class IngestControllerTest {
return request;
}
+ private FileSetting captureFileSetting(List<String> lines, String format)
+ throws Exception {
+ Path uploadRoot = Files.createTempDirectory("hubble-ingest-capture");
+ Path dataFile = uploadRoot.resolve("data." + format.toLowerCase());
+ Files.write(dataFile, lines);
+
+ TestIngestController controller = new TestIngestController();
+ DatasourceService datasourceService =
+ Mockito.mock(DatasourceService.class);
+ JobManagerService jobService = Mockito.mock(JobManagerService.class);
+ FileMappingService fileMappingService =
+ Mockito.mock(FileMappingService.class);
+ this.setField(controller, "config", this.mockConfig(uploadRoot));
+ this.setField(controller, "datasourceService", datasourceService);
+ this.setField(controller, "jobManagerService", jobService);
+ this.setField(controller, "fileMappingService", fileMappingService);
+
+ Datasource datasource = new Datasource();
+ datasource.setId(1);
+ Map<String, Object> datasourceConfig = new HashMap<>();
+ datasourceConfig.put("type", "FILE");
+ datasourceConfig.put("path", dataFile.toString());
+ datasourceConfig.put("format", format);
+ datasource.setDatasourceConfig(datasourceConfig);
+ Mockito.when(datasourceService.get(1)).thenReturn(datasource);
+ Mockito.when(fileMappingService.requirePathUnderUploadRoot(
+ dataFile.toString())).thenReturn(dataFile.toFile());
+ Mockito.when(jobService.createIngestTask(
+ Mockito.any(JobManager.class), Mockito.any(FileMapping.class),
+ Mockito.any(GraphConnection.class),
Mockito.any(HugeClient.class)))
+ .thenAnswer(invocation -> {
+ JobManager job = invocation.getArgument(0);
+ job.setId(7);
+ return LoadTask.builder().id(9).build();
+ });
+ this.bindRequestSession("alice");
+
+ controller.createTask(this.request(dataFile));
+
+ ArgumentCaptor<FileMapping> mappingCaptor =
+ ArgumentCaptor.forClass(FileMapping.class);
+ Mockito.verify(jobService).createIngestTask(
+ Mockito.any(JobManager.class), mappingCaptor.capture(),
+ Mockito.any(GraphConnection.class),
Mockito.any(HugeClient.class));
+ return mappingCaptor.getValue().getFileSetting();
+ }
+
private HugeConfig mockConfig(Path uploadRoot) {
HugeConfig config = Mockito.mock(HugeConfig.class);
Mockito.when(config.get(HubbleOptions.UPLOAD_FILE_LOCATION))
diff --git
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/saas/SaaSMetricsControllerTest.java
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/saas/SaaSMetricsControllerTest.java
new file mode 100644
index 000000000..4fef871ad
--- /dev/null
+++
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/saas/SaaSMetricsControllerTest.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.controller.saas;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.hugegraph.driver.HugeClient;
+import org.apache.hugegraph.service.saas.PrometheusService;
+import org.apache.hugegraph.service.space.GraphSpaceService;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class SaaSMetricsControllerTest {
+
+ @Test
+ public void testMetricsSerializesUnavailableElementCounts() {
+ HugeClient client = Mockito.mock(HugeClient.class);
+ GraphSpaceService graphSpaceService =
+ Mockito.mock(GraphSpaceService.class);
+ PrometheusService prometheusService =
+ Mockito.mock(PrometheusService.class);
+ SaaSMetricsController controller = new SaaSMetricsController() {
+ @Override
+ protected HugeClient authClient(String graphSpace, String graph) {
+ return client;
+ }
+ };
+ ReflectionTestUtils.setField(controller, "graphSpaceService",
+ graphSpaceService);
+ ReflectionTestUtils.setField(controller, "prometheusService",
+ prometheusService);
+
+ Map<String, Long> metrics = new HashMap<>();
+ metrics.put("gsCount", 1L);
+ metrics.put("gCount", 2L);
+ metrics.put("vCount", null);
+ metrics.put("eCount", null);
+ metrics.put("vlCount", 3L);
+ metrics.put("elCount", 4L);
+ metrics.put("preDayTaskCount", 5L);
+ Mockito.when(graphSpaceService.metrics(client)).thenReturn(metrics);
+ Mockito.when(prometheusService.queryCountOffSet1Day(
+ Mockito.anyString())).thenReturn(6L);
+
+ Object result = controller.metrics();
+ JsonNode json = new ObjectMapper().valueToTree(result);
+
+ Assert.assertTrue(json.get("vertex-count").isNull());
+ Assert.assertTrue(json.get("edge-count").isNull());
+ Assert.assertEquals(1L, json.get("graph-space-count").longValue());
+ Assert.assertEquals(2L, json.get("graph-count").longValue());
+ }
+}
diff --git
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
index 72df7ebae..2f5958124 100644
---
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
+++
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
@@ -137,6 +137,26 @@ public class GraphSpaceServiceTest {
Assert.assertEquals(Long.valueOf(3L), result.get("eCount"));
}
+ @Test
+ public void testMetricsPreservesUnavailableElementCounts() {
+ GraphSpaceService spy = Mockito.spy(this.service);
+ Mockito.doReturn(java.util.Arrays.asList("available", "unavailable"))
+ .when(spy).listAll(this.client);
+ Mockito.doReturn(statistic("20260712", 2L, 3L))
+ .when(spy).evCount(this.client, "available");
+ Mockito.doReturn(statistic(null, null, null))
+ .when(spy).evCount(this.client, "unavailable");
+ Mockito.when(this.graphsService.listGraphNames(
+ Mockito.eq(this.client), Mockito.anyString(),
+ Mockito.eq("")))
+ .thenReturn(java.util.Collections.emptySet());
+
+ Map<String, Long> result = spy.metrics(this.client);
+
+ Assert.assertNull(result.get("vCount"));
+ Assert.assertNull(result.get("eCount"));
+ }
+
@Test
public void testStatisticDoesNotClaimMixedDates() {
LinkedHashSet<String> graphs = new LinkedHashSet<>();
@@ -203,6 +223,27 @@ public class GraphSpaceServiceTest {
Assert.assertEquals(10L, result.get("edge"));
}
+ @Test
+ public void testStatisticKeepsUnavailableCountsForGraphSpace() {
+ LinkedHashSet<String> graphs = new LinkedHashSet<>();
+ graphs.add("available");
+ graphs.add("unavailable");
+ Mockito.when(this.graphsService.listGraphNames(this.client, "space",
""))
+ .thenReturn(graphs);
+ Mockito.when(this.graphsService.evCount(this.client, "space",
+ "available"))
+ .thenReturn(statistic("20260712", 2L, 3L));
+ Mockito.when(this.graphsService.evCount(this.client, "space",
+ "unavailable"))
+ .thenReturn(statistic(null, null, null));
+
+ Map<String, Object> result = this.service.evCount(this.client,
"space");
+
+ Assert.assertNull(result.get("date"));
+ Assert.assertNull(result.get("vertex"));
+ Assert.assertNull(result.get("edge"));
+ }
+
@Test
public void testStatisticFormatsCurrentDateForEmptyGraphSpace() {
Mockito.when(this.graphsService.listGraphNames(this.client, "space",
""))
diff --git
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java
index 0fcff79d0..1e5a7c6a3 100644
---
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java
+++
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java
@@ -81,6 +81,44 @@ public class BaseControllerGremlinClientTest {
Assert.assertEquals("hugegraph", controller.graph);
}
+ @Test
+ public void testReusedTemporaryClientUsesBearerToken() {
+ HugeClient tokenClient = Mockito.mock(HugeClient.class);
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.getSession().setAttribute(Constant.USERNAME_KEY, "admin");
+ request.getSession().setAttribute(Constant.TOKEN_KEY, "jwt-token");
+ request.setAttribute("hugeClient", tokenClient);
+ RequestContextHolder.setRequestAttributes(
+ new ServletRequestAttributes(request));
+
+ TestController controller = new TestController();
+
+ Assert.assertSame(tokenClient, controller.temporaryTokenClient());
+ Mockito.verify(tokenClient).setAuthContext("Bearer jwt-token");
+ }
+
+ @Test
+ public void testInvalidTokenIsRejectedForReusedTemporaryClient() {
+ HugeClient tokenClient = Mockito.mock(HugeClient.class);
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.getSession().setAttribute(Constant.USERNAME_KEY, "admin");
+ request.getSession().setAttribute(Constant.TOKEN_KEY, "Bearer ");
+ request.setAttribute("hugeClient", tokenClient);
+ RequestContextHolder.setRequestAttributes(
+ new ServletRequestAttributes(request));
+
+ TestController controller = new TestController();
+
+ try {
+ controller.temporaryTokenClient();
+ Assert.fail("Expected a scheme-only Bearer token to be rejected");
+ } catch (IllegalArgumentException ignored) {
+ // Expected
+ }
+ Mockito.verify(tokenClient, Mockito.never())
+ .setAuthContext(Mockito.anyString());
+ }
+
private MockHttpServletRequest requestWithAuth() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.getSession().setAttribute(Constant.USERNAME_KEY, "admin");
@@ -102,6 +140,10 @@ public class BaseControllerGremlinClientTest {
return this.authGremlinClient(graphSpace, graph);
}
+ HugeClient temporaryTokenClient() {
+ return this.tempTokenClient();
+ }
+
@Override
protected HugeClient authClient(String graphSpace, String graph) {
this.authClientCreated = true;
diff --git
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java
index b381e293c..7cd14011a 100644
---
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java
+++
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsServiceDefaultTest.java
@@ -319,6 +319,36 @@ public class GraphsServiceDefaultTest {
Mockito.verifyZeroInteractions(query);
}
+ @Test
+ public void testElementCountSkipsUnavailableSnapshotsInPdMode() {
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+ ReflectionTestUtils.setField(this.service, "config", config);
+
+ QueryService query = Mockito.mock(QueryService.class);
+ ReflectionTestUtils.setField(this.service, "queryService", query);
+
+ GraphManager graph = Mockito.mock(GraphManager.class);
+ Mockito.when(this.client.graph()).thenReturn(graph);
+ Vertex vertex = Mockito.mock(Vertex.class);
+ Edge edge = Mockito.mock(Edge.class);
+ Mockito.when(vertex.label()).thenReturn("person");
+ Mockito.when(edge.label()).thenReturn("knows");
+ Mockito.when(graph.iterateVertices(1000))
+ .thenReturn(Collections.nCopies(2, vertex).iterator());
+ Mockito.when(graph.iterateEdges(1000))
+ .thenReturn(Collections.singletonList(edge).iterator());
+
+ Map<String, Object> result =
+ this.service.evCount(this.client, "DEFAULT", "demo");
+
+ Assert.assertEquals(2L, result.get("vertex"));
+ Assert.assertEquals(1L, result.get("edge"));
+ Assert.assertNotNull(result.get("date"));
+ Mockito.verify(graph, Mockito.never()).getEVCount(Mockito.anyString());
+ Mockito.verifyZeroInteractions(query);
+ }
+
@Test
public void testElementCountReturnsUnavailableWhenLiveFallbackFails() {
QueryService query = Mockito.mock(QueryService.class);
diff --git
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java
index 287e31429..68c0ab58e 100644
---
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java
+++
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoadTaskServiceTest.java
@@ -18,8 +18,8 @@
package org.apache.hugegraph.unit;
-import java.lang.reflect.Method;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
@@ -28,10 +28,13 @@ import org.junit.Test;
import org.apache.hugegraph.entity.GraphConnection;
import org.apache.hugegraph.entity.enums.LoadStatus;
import org.apache.hugegraph.entity.load.FileMapping;
+import org.apache.hugegraph.entity.load.FileSetting;
+import org.apache.hugegraph.entity.load.ListFormat;
import org.apache.hugegraph.entity.load.LoadParameter;
import org.apache.hugegraph.entity.load.LoadTask;
import org.apache.hugegraph.handler.LoadTaskExecutor;
import org.apache.hugegraph.loader.executor.LoadOptions;
+import org.apache.hugegraph.loader.source.file.FileSource;
import org.apache.hugegraph.service.load.LoadTaskService;
import org.apache.hugegraph.mapper.load.LoadTaskMapper;
import org.apache.hugegraph.testutil.Assert;
@@ -207,6 +210,35 @@ public class LoadTaskServiceTest {
Assert.assertEquals(8080, options.port);
}
+ @Test
+ public void testBuildFileSourcePreservesPhysicalHeaderFlag()
+ throws Exception {
+ LoadTaskService service = new LoadTaskService();
+ Method method = LoadTaskService.class.getDeclaredMethod(
+ "buildFileSource", FileMapping.class);
+ method.setAccessible(true);
+ FileMapping mapping = this.fileMapping();
+ FileSetting setting = FileSetting.builder()
+ .columnNames(Arrays.asList("name"))
+ .hasHeader(false)
+ .format("CSV")
+ .delimiter(",")
+ .charset("UTF-8")
+ .dateFormat("yyyy-MM-dd HH:mm:ss")
+ .timeZone("GMT+8")
+ .skippedLine("(^#|^//).*")
+ .listFormat(new ListFormat())
+ .build();
+ mapping.setFileSetting(setting);
+
+ FileSource source = (FileSource) method.invoke(service, mapping);
+
+ Assert.assertFalse(source.hasHeader());
+ setting.setHasHeader(true);
+ source = (FileSource) method.invoke(service, mapping);
+ Assert.assertTrue(source.hasHeader());
+ }
+
@Test
public void testLoadOptionsPreferDirectServerOverPdPeers()
throws Exception {
diff --git
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/util/HugeClientUtilTest.java
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/util/HugeClientUtilTest.java
new file mode 100644
index 000000000..eeac2973d
--- /dev/null
+++
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/util/HugeClientUtilTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hugegraph.api.gremlin.GremlinRequest;
+import org.apache.hugegraph.driver.HugeClient;
+import org.apache.hugegraph.entity.GraphConnection;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+
+public class HugeClientUtilTest {
+
+ @Test
+ public void testTokenClientSendsBearerToGremlin() throws Exception {
+ this.assertAuthorizationHeader("jwt-value", "Bearer jwt-value");
+ }
+
+ @Test
+ public void testTokenClientDoesNotDuplicateBearerScheme() throws Exception
{
+ this.assertAuthorizationHeader(" bearer jwt-value ",
+ "Bearer jwt-value");
+ }
+
+ @Test
+ public void testUnauthenticatedClientDoesNotSendAuthorization()
+ throws Exception {
+ this.assertAuthorizationHeader(null, null);
+ }
+
+ @Test
+ public void testBearerAuthContextRejectsMissingPayload() {
+ this.assertInvalidBearerToken(null);
+ this.assertInvalidBearerToken("");
+ this.assertInvalidBearerToken(" ");
+ this.assertInvalidBearerToken("Bearer");
+ this.assertInvalidBearerToken(" bearer ");
+ this.assertInvalidBearerToken("Bearer\u2003");
+ }
+
+ @Test
+ public void testSchemeOnlyTokenIsRejectedBeforeVersionRequest()
+ throws Exception {
+ AtomicInteger requests = new AtomicInteger();
+ HttpServer server = HttpServer.create(new
InetSocketAddress("127.0.0.1",
+ 0), 0);
+ server.createContext("/versions", exchange -> {
+ requests.incrementAndGet();
+ respond(exchange, "{\"versions\":{\"core\":\"1.8.0\"," +
+ "\"gremlin\":\"3.7.3\",\"api\":\"0.71\"}}");
+ });
+ server.start();
+
+ GraphConnection connection = this.connection(server, "Bearer\u2003");
+ try {
+ HugeClientUtil.tryConnect(connection);
+ Assert.fail("Expected a scheme-only Bearer token to be rejected");
+ } catch (IllegalArgumentException ignored) {
+ Assert.assertEquals(0, requests.get());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ private void assertAuthorizationHeader(String token, String expected)
+ throws Exception {
+ AtomicReference<String> versionAuthorization = new AtomicReference<>();
+ AtomicReference<String> gremlinAuthorization = new AtomicReference<>();
+ HttpServer server = HttpServer.create(new
InetSocketAddress("127.0.0.1",
+ 0), 0);
+ server.createContext("/versions", exchange -> {
+ versionAuthorization.set(exchange.getRequestHeaders()
+ .getFirst("Authorization"));
+ respond(exchange, "{\"versions\":{\"core\":\"1.8.0\"," +
+ "\"gremlin\":\"3.7.3\",\"api\":\"0.71\"}}");
+ });
+ server.createContext("/gremlin", exchange -> {
+ gremlinAuthorization.set(exchange.getRequestHeaders()
+ .getFirst("Authorization"));
+ respond(exchange, "{\"requestId\":\"1\"," +
+ "\"status\":{\"message\":\"\",\"code\":200," +
+ "\"attributes\":{}}," +
+ "\"result\":{\"data\":[1],\"meta\":{}}}");
+ });
+ server.start();
+
+ GraphConnection connection = this.connection(server, token);
+ try (HugeClient client = HugeClientUtil.tryConnect(connection)) {
+ Assert.assertEquals(expected, versionAuthorization.get());
+ client.gremlin().execute(new GremlinRequest("g.V().count()"));
+ Assert.assertEquals(expected, gremlinAuthorization.get());
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ private GraphConnection connection(HttpServer server, String token) {
+ return GraphConnection.builder()
+ .graphSpace("DEFAULT")
+ .graph("hugegraph")
+ .host("127.0.0.1")
+ .port(server.getAddress().getPort())
+ .timeout(5)
+ .token(token)
+ .build();
+ }
+
+ private void assertInvalidBearerToken(String token) {
+ try {
+ HugeClientUtil.bearerAuthContext(token);
+ Assert.fail("Expected an invalid Bearer token to be rejected");
+ } catch (IllegalArgumentException ignored) {
+ // Expected
+ }
+ }
+
+ private static void respond(HttpExchange exchange, String response)
+ throws IOException {
+ byte[] body = response.getBytes(StandardCharsets.UTF_8);
+ try (InputStream input = exchange.getRequestBody()) {
+ input.transferTo(OutputStream.nullOutputStream());
+ }
+ exchange.sendResponseHeaders(200, body.length);
+ try (OutputStream output = exchange.getResponseBody()) {
+ output.write(body);
+ }
+ }
+}
diff --git
a/hugegraph-hubble/hubble-be/src/test/python/check_server_status.feature
b/hugegraph-hubble/hubble-be/src/test/python/check_server_status.feature
index 68333599b..550c866be 100644
--- a/hugegraph-hubble/hubble-be/src/test/python/check_server_status.feature
+++ b/hugegraph-hubble/hubble-be/src/test/python/check_server_status.feature
@@ -6,4 +6,4 @@ Feature: check hugegraph-hubble server health status
Then code:<code> response:<response>
Examples:
| scene | url | code | response |
- | check server health status | localhost:8088 | 200 | {"status":"UP"} |
+ | check server health status | localhost:8088 | 200 |
{"status":200,"name":"hugegraph-hubble"} |
diff --git
a/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py
b/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py
index c84385383..c5a80334c 100644
--- a/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py
+++ b/hugegraph-hubble/hubble-be/src/test/python/steps/check_server_status.py
@@ -29,8 +29,8 @@ use_step_matcher("re")
@when("scene:(?P<scene>.+) url:(?P<url>.+)")
def step_impl(context, scene, url):
- http_url = "http://" + url + "/actuator/health"
- context.response = requests.get(http_url)
+ http_url = "http://" + url + "/about"
+ context.response = requests.get(http_url, timeout=5)
context.code = context.response.status_code
context.json = context.response.json()
@@ -38,7 +38,10 @@ def step_impl(context, scene, url):
@then("code:(?P<expect_code>.+) response:(?P<expect_json>.+)")
def step_impl(context, expect_code, expect_json):
actual_code = context.code
- actual_json = context.json
+ actual_json = {
+ "status": context.json["status"],
+ "name": context.json["data"]["name"]
+ }
expect_code = int(expect_code)
expect_json = json.loads(expect_json)
@@ -47,4 +50,5 @@ def step_impl(context, expect_code, expect_json):
.is_equal_to(expect_code)
assert_that(actual_json).described_as(context.response) \
.is_equal_to(expect_json)
-
+ assert_that(context.json["data"]["version"]) \
+ .described_as(context.response).is_not_empty()
diff --git a/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions
b/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions
index eb160dc80..b071dca5c 100644
--- a/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions
+++ b/hugegraph-hubble/hubble-dist/assembly/static/bin/common_functions
@@ -40,19 +40,36 @@ function wait_for_startup() {
local timeout_s=$2
local now_s=$(date '+%s')
local stop_s=$(( now_s + timeout_s ))
- local status=0
+ local body=""
+ local request_timeout_s=0
+ local sleep_s=0
- while [[ ${now_s} -le ${stop_s} ]]; do
+ while [[ ${now_s} -lt ${stop_s} ]]; do
echo -n .
- status=$(curl -o /dev/null -s -w %{http_code} "${server_url}")
- if [[ ${status} -eq 200 ]]; then
+ request_timeout_s=$(( stop_s - now_s ))
+ if [[ ${request_timeout_s} -gt 5 ]]; then
+ request_timeout_s=5
+ fi
+ body=$(curl -fsS --connect-timeout 1 \
+ --max-time "${request_timeout_s}" \
+ "${server_url}" 2>/dev/null || true)
+ if [[ "${body}" == *'"status":200'* &&
+ "${body}" == *'"name":"hugegraph-hubble"'* ]]; then
echo "OK"
return 0
fi
- sleep 2
+ now_s=$(date '+%s')
+ if [[ ${now_s} -ge ${stop_s} ]]; then
+ break
+ fi
+ sleep_s=$(( stop_s - now_s ))
+ if [[ ${sleep_s} -gt 2 ]]; then
+ sleep_s=2
+ fi
+ sleep "${sleep_s}"
now_s=$(date '+%s')
done
- echo "timed out with http status $status" >&2
+ echo "timed out waiting for HugeGraph Hubble readiness response" >&2
return 1
}
diff --git a/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh
b/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh
index 8699689f1..6c71becb7 100644
--- a/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh
+++ b/hugegraph-hubble/hubble-dist/assembly/static/bin/start-hubble.sh
@@ -103,9 +103,9 @@ echo ${PID} > "${PID_FILE}"
# wait hubble start
TIMEOUT_S=30
-SERVER_HOST=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties
hubble.host)
-SERVER_PORT=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties
hubble.port)
-SERVER_URL="http://${SERVER_HOST}:${SERVER_PORT}/actuator/health"
+SERVER_HOST=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties
server.host)
+SERVER_PORT=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties
server.port)
+SERVER_URL="http://${SERVER_HOST}:${SERVER_PORT}/about"
wait_for_startup "${SERVER_URL}" ${TIMEOUT_S} || {
cat "${LOG}"
diff --git
a/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties
b/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties
index ffe6f7f72..8dfce484c 100644
---
a/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties
+++
b/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties
@@ -15,8 +15,8 @@
# under the License.
#
-hubble.host=0.0.0.0
-hubble.port=8088
+server.host=localhost
+server.port=8088
gremlin.suffix_limit=250
gremlin.vertex_degree_limit=100
diff --git
a/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py
b/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py
index 0fbf6518e..0f6031a96 100755
--- a/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_live_hubble_smoke.py
@@ -84,7 +84,7 @@ def is_hubble_readiness_response(response):
return False
data = response.get("data")
return (isinstance(data, dict) and
- isinstance(data.get("name"), str) and bool(data["name"]) and
+ data.get("name") == "hugegraph-hubble" and
isinstance(data.get("version"), str) and bool(data["version"]))
@@ -144,23 +144,15 @@ def configure_hubble_endpoint(hubble_home, hubble_url,
bind_host, server_url):
lines = []
replaced_host = False
replaced_port = False
- replaced_server_address = False
- replaced_server_port = False
replaced_pd_enabled = False
replaced_direct_url = False
for line in text.splitlines():
- if host and line.startswith("hubble.host="):
- lines.append(f"hubble.host={host}")
+ if host and line.startswith("server.host="):
+ lines.append(f"server.host={host}")
replaced_host = True
- elif port and line.startswith("hubble.port="):
- lines.append(f"hubble.port={port}")
- replaced_port = True
- elif host and line.startswith("server.address="):
- lines.append(f"server.address={host}")
- replaced_server_address = True
elif port and line.startswith("server.port="):
lines.append(f"server.port={port}")
- replaced_server_port = True
+ replaced_port = True
elif line.startswith("pd.enabled="):
lines.append("pd.enabled=false")
replaced_pd_enabled = True
@@ -170,12 +162,8 @@ def configure_hubble_endpoint(hubble_home, hubble_url,
bind_host, server_url):
else:
lines.append(line)
if host and not replaced_host:
- lines.append(f"hubble.host={host}")
- if host and not replaced_server_address:
- lines.append(f"server.address={host}")
+ lines.append(f"server.host={host}")
if port and not replaced_port:
- lines.append(f"hubble.port={port}")
- if port and not replaced_server_port:
lines.append(f"server.port={port}")
if not replaced_pd_enabled:
lines.append("pd.enabled=false")
diff --git
a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
index 7ca54f803..1b46bd1cf 100755
--- a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.js
@@ -81,6 +81,59 @@ async function loadPlaywright() {
}
}
+function createApiEntry(response) {
+ const request = response.request();
+ return {
+ method: request.method(),
+ url: response.url(),
+ httpStatus: response.status(),
+ ok: response.ok(),
+ businessStatus: null
+ };
+}
+
+async function readBusinessStatus(response, entry) {
+ try {
+ const body = await response.json();
+ entry.businessStatus = body && body.status !== undefined ? body.status :
null;
+ } catch (_) {
+ // Non-JSON API responses are still represented by HTTP status.
+ }
+ return entry;
+}
+
+function matchesRequiredApi(response, requiredRequest) {
+ try {
+ const url = new URL(response.url());
+ return response.request().method() === 'GET' &&
+ url.pathname === requiredRequest.path &&
+ Object.entries(requiredRequest.query || {}).every(([key, value]) =>
(
+ url.searchParams.get(key) === value
+ ));
+ } catch (_) {
+ return false;
+ }
+}
+
+async function waitForRequiredResponse(page, requiredRequest) {
+ try {
+ const response = await page.waitForResponse(
+ item => matchesRequiredApi(item, requiredRequest),
+ {timeout: 30000}
+ );
+ return {
+ entry: await readBusinessStatus(response, createApiEntry(response)),
+ error: null
+ };
+ } catch (error) {
+ return {entry: null, error: error.message};
+ }
+}
+
+function apiEntryPassed(entry) {
+ return Boolean(entry && entry.ok && entry.businessStatus === 200);
+}
+
async function main() {
const hubbleUrl = (argValue('--hubble-url', process.env.HUBBLE_URL) ||
'http://127.0.0.1:8088').replace(/\/$/, '');
@@ -106,23 +159,11 @@ async function main() {
const auth = await authenticateUi(context, page, hubbleUrl, username,
password);
page.on('response', async (response) => {
- const request = response.request();
const url = response.url();
if (url.includes('/api/v1.3/')) {
- let businessStatus = null;
- try {
- const body = await response.json();
- businessStatus = body && body.status !== undefined ? body.status :
null;
- } catch (_) {
- // Non-JSON API responses are still represented by HTTP status.
- }
- network.push({
- method: request.method(),
- url,
- httpStatus: response.status(),
- ok: response.ok(),
- businessStatus
- });
+ const entry = createApiEntry(response);
+ network.push(entry);
+ await readBusinessStatus(response, entry);
}
});
page.on('console', (message) => {
@@ -133,21 +174,27 @@ async function main() {
const graphspaceRoute = auth.pdEnabled
? { name: 'graphspace', path: '/graphspace',
- requiredApis: ['/api/v1.3/graphspaces'],
+ requiredRequests: [{
+ path: '/api/v1.3/graphspaces',
+ query: {page_no: '1', page_size: '11'}
+ }],
readySelector: '[data-testid="graphspace-page-title"]' }
- : { name: 'graphspace', path: '/graphspace',
- requiredApis: ['/api/v1.3/graphspaces/DEFAULT/graphs'],
+ : { name: 'graphspace', path: '/graphspace/DEFAULT',
+ requiredRequests: [{
+ path: '/api/v1.3/graphspaces/DEFAULT/graphs',
+ query: {page_no: '1', page_size: '11'}
+ }],
textPattern: /图管理|Graph Management/ };
const routes = [
graphspaceRoute,
{ name: 'gremlin', path: '/gremlin',
- requiredApis: ['/api/v1.3/graphspaces/list'],
+ requiredRequests: [{path: '/api/v1.3/graphspaces/list'}],
textPattern: /Gremlin|图查询|查询/ },
{ name: 'algorithms', path: '/algorithms',
- requiredApis: ['/api/v1.3/graphspaces/list'],
+ requiredRequests: [{path: '/api/v1.3/graphspaces/list'}],
textPattern: /算法|Algorithm|OLTP|OLAP/ },
{ name: 'asyncTasks', path: '/asyncTasks',
- requiredApis: ['/api/v1.3/graphspaces/list'],
+ requiredRequests: [{path: '/api/v1.3/graphspaces/list'}],
textPattern: /异步|Async|Task|任务/ }
];
@@ -155,10 +202,14 @@ async function main() {
try {
for (const route of routes) {
network.length = 0;
+ const requiredResponsePromises =
route.requiredRequests.map(requiredRequest => (
+ waitForRequiredResponse(page, requiredRequest)
+ ));
await page.goto(hubbleUrl + route.path, {
- waitUntil: 'networkidle',
+ waitUntil: 'domcontentloaded',
timeout: 30000
});
+ const requiredResponses = await Promise.all(requiredResponsePromises);
await page.waitForTimeout(500);
const screenshot = path.join(outputDir, `${route.name}.png`);
await page.screenshot({ path: screenshot, fullPage: true });
@@ -167,14 +218,15 @@ async function main() {
'\\b(addition|analysis|async-tasks|common|home|manage|navigation|' +
'server-data-import|Topbar)\\.[A-Za-z0-9_.-]+'
);
- const matchedApis = route.requiredApis.map((requiredApi) => {
- const entries = network.filter((entry) =>
entry.url.includes(requiredApi));
+ const matchedApis = route.requiredRequests.map((requiredRequest, index)
=> {
+ const requiredResponse = requiredResponses[index];
+ const entries = requiredResponse.entry ? [requiredResponse.entry] : [];
return {
- requiredApi,
+ requiredApi: requiredRequest.path,
+ requiredQuery: requiredRequest.query || {},
entries,
- passed: entries.some((entry) => entry.ok &&
- (entry.businessStatus === null ||
- entry.businessStatus === 200))
+ waitError: requiredResponse.error,
+ passed: entries.some(apiEntryPassed)
};
});
const routeTextMatched = route.readySelector
@@ -217,7 +269,17 @@ async function main() {
}
}
-main().catch((error) => {
- console.error(error.message);
- process.exit(1);
-});
+if (require.main === module) {
+ main().catch((error) => {
+ console.error(error.message);
+ process.exit(1);
+ });
+}
+
+module.exports = {
+ apiEntryPassed,
+ createApiEntry,
+ matchesRequiredApi,
+ readBusinessStatus,
+ waitForRequiredResponse
+};
diff --git
a/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.test.js
b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.test.js
new file mode 100644
index 000000000..cc76af269
--- /dev/null
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/run_ui_browser_smoke.test.js
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const assert = require('node:assert/strict');
+const test = require('node:test');
+
+const {
+ apiEntryPassed,
+ matchesRequiredApi,
+ waitForRequiredResponse
+} = require('./run_ui_browser_smoke');
+
+function response(url, options = {}) {
+ const method = options.method || 'GET';
+ const httpStatus = options.httpStatus || 200;
+ const body = options.body === undefined ? {status: 200} : options.body;
+ return {
+ request: () => ({method: () => method}),
+ url: () => url,
+ status: () => httpStatus,
+ ok: () => httpStatus >= 200 && httpStatus < 300,
+ json: async () => {
+ if (options.jsonError) {
+ throw new Error(options.jsonError);
+ }
+ return body;
+ }
+ };
+}
+
+test('matches the exact page-level required GET request', () => {
+ const requiredRequest = {
+ path: '/api/v1.3/graphspaces/DEFAULT/graphs',
+ query: {page_no: '1', page_size: '11'}
+ };
+ assert.equal(matchesRequiredApi(
+ response(`http://hubble${requiredRequest.path}?page_no=1&page_size=11`),
+ requiredRequest
+ ), true);
+ assert.equal(matchesRequiredApi(
+ response(`http://hubble${requiredRequest.path}?page_no=1&page_size=-1`),
+ requiredRequest
+ ), false);
+ assert.equal(matchesRequiredApi(
+ response(
+ `http://hubble${requiredRequest.path}?page_no=1&page_size=11`,
+ {method: 'POST'}
+ ), requiredRequest
+ ), false);
+});
+
+test('waits for and validates the required API response body', async () => {
+ const requiredRequest = {
+ path: '/api/v1.3/graphspaces/DEFAULT/graphs',
+ query: {page_no: '1', page_size: '11'}
+ };
+ const apiResponse = response(
+ `http://hubble${requiredRequest.path}?page_no=1&page_size=11`,
+ {
+ body: {status: 200, data: {records: []}}
+ }
+ );
+ const page = {
+ waitForResponse: async (predicate, options) => {
+ assert.deepEqual(options, {timeout: 30000});
+ assert.equal(predicate(apiResponse), true);
+ return apiResponse;
+ }
+ };
+
+ const result = await waitForRequiredResponse(page, requiredRequest);
+ assert.equal(result.error, null);
+ assert.deepEqual(result.entry, {
+ method: 'GET',
+ url: `http://hubble${requiredRequest.path}?page_no=1&page_size=11`,
+ httpStatus: 200,
+ ok: true,
+ businessStatus: 200
+ });
+ assert.equal(apiEntryPassed(result.entry), true);
+});
+
+test('keeps a missing required API fail-closed', async () => {
+ const page = {
+ waitForResponse: async () => {
+ throw new Error('required API timeout');
+ }
+ };
+
+ const result = await waitForRequiredResponse(
+ page, {path: '/api/v1.3/graphspaces/DEFAULT/graphs'}
+ );
+ assert.equal(result.entry, null);
+ assert.equal(result.error, 'required API timeout');
+ assert.equal(apiEntryPassed(result.entry), false);
+});
+
+test('rejects unsuccessful or malformed required API responses', async () => {
+ const failures = [
+ response('http://hubble/api/v1.3/graphspaces', {
+ httpStatus: 500,
+ body: {status: 500}
+ }),
+ response('http://hubble/api/v1.3/graphspaces', {
+ body: {status: 500}
+ }),
+ response('http://hubble/api/v1.3/graphspaces', {
+ body: {data: {records: []}}
+ }),
+ response('http://hubble/api/v1.3/graphspaces', {
+ jsonError: 'invalid JSON'
+ })
+ ];
+
+ for (const apiResponse of failures) {
+ const page = {
+ waitForResponse: async predicate => {
+ assert.equal(predicate(apiResponse), true);
+ return apiResponse;
+ }
+ };
+ const result = await waitForRequiredResponse(
+ page, {path: '/api/v1.3/graphspaces'}
+ );
+ assert.equal(apiEntryPassed(result.entry), false);
+ }
+});
diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh
b/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh
index d47268d7c..f164cf46f 100644
--- a/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/start-hubble.sh
@@ -94,9 +94,9 @@ pid=$!
echo ${pid} > "${PID_FILE}"
timeout_s=30
-server_host=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties
hubble.host)
-server_port=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties
hubble.port)
-server_url="http://${server_host}:${server_port}/actuator/health"
+server_host=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties
server.host)
+server_port=$(read_property "${CONF_PATH}"/hugegraph-hubble.properties
server.port)
+server_url="http://${server_host}:${server_port}/about"
wait_for_startup "${server_url}" ${timeout_s} || {
cat "${log}"
diff --git
a/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py
b/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py
index 945cc9539..78eb8b957 100644
--- a/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py
+++ b/hugegraph-hubble/hubble-dist/assembly/travis/test_run_live_hubble_smoke.py
@@ -17,6 +17,7 @@
#
import importlib.util
+import tempfile
import unittest
from pathlib import Path
from unittest import mock
@@ -30,17 +31,48 @@ SPEC.loader.exec_module(SMOKE)
class HubbleReadinessTest(unittest.TestCase):
+ def test_configure_endpoint_uses_registered_server_options(self):
+ with tempfile.TemporaryDirectory() as directory:
+ home = Path(directory)
+ conf_dir = home / "conf"
+ conf_dir.mkdir()
+ conf = conf_dir / "hugegraph-hubble.properties"
+ conf.write_text(
+ "server.host=localhost\n"
+ "server.port=8088\n"
+ "pd.enabled=true\n",
+ encoding="utf-8"
+ )
+
+ SMOKE.configure_hubble_endpoint(
+ home,
+ "http://127.0.0.1:19088",
+ "0.0.0.0",
+ "http://127.0.0.1:18080"
+ )
+
+ configured = conf.read_text(encoding="utf-8")
+ self.assertIn("server.host=0.0.0.0", configured)
+ self.assertIn("server.port=19088", configured)
+ self.assertNotIn("hubble.host=", configured)
+ self.assertNotIn("hubble.port=", configured)
+ self.assertNotIn("server.address=", configured)
+
def test_readiness_rejects_spa_html(self):
self.assertFalse(SMOKE.is_hubble_readiness_response(
'<div id="root"></div>'))
def test_readiness_requires_about_response_shape(self):
self.assertFalse(SMOKE.is_hubble_readiness_response(
- {"status": 200, "data": {"name": "HugeGraph-Hubble"}}))
- self.assertTrue(SMOKE.is_hubble_readiness_response({
+ {"status": 200, "data": {"name": "hugegraph-hubble"}}))
+ self.assertFalse(SMOKE.is_hubble_readiness_response({
"status": 200,
"data": {"name": "HugeGraph-Hubble", "version": "3.0.0"}
}))
+ self.assertTrue(SMOKE.is_hubble_readiness_response({
+ "status": 200,
+ "data": {"name": "hugegraph-hubble", "version": "3.0.0"}
+ }))
@mock.patch.object(SMOKE, "request")
def test_preflight_uses_strict_about_api(self, request):
diff --git
a/hugegraph-hubble/hubble-fe/src/i18n/empty-result-copy-context.test.js
b/hugegraph-hubble/hubble-fe/src/i18n/empty-result-copy-context.test.js
new file mode 100644
index 000000000..931c78b42
--- /dev/null
+++ b/hugegraph-hubble/hubble-fe/src/i18n/empty-result-copy-context.test.js
@@ -0,0 +1,66 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import fs from 'fs';
+import path from 'path';
+
+import enAnalysis from './resources/en-US/modules/analysis.json';
+import zhAnalysis from './resources/zh-CN/modules/analysis.json';
+
+const readSource = relativePath => fs.readFileSync(
+ path.resolve(__dirname, '..', relativePath),
+ 'utf8'
+);
+
+describe('empty graph result copy stays scoped to its product flow', () => {
+ it.each([
+ ['English', enAnalysis.analysis],
+ ['Chinese', zhAnalysis.analysis],
+ ])('keeps real %s query and algorithm copy distinct', (name, analysis) => {
+ expect(analysis.query_result.no_graph_result).toMatch(/Table|表格/);
+ expect(analysis.query_result.no_graph_result).toMatch(/JSON/);
+
expect(analysis.algorithm.result.no_graph_result).not.toMatch(/Table|表格|JSON/);
+ });
+
+ it('uses the algorithm-only key in both algorithm empty-result views', ()
=> {
+ const graphResult =
readSource('modules/algorithm/GraphResult/Home/index.js');
+ const rankResult = readSource(
+ 'modules/algorithm/GraphResult/RankApiView/index.js'
+ );
+
+ expect(graphResult).toContain(
+ "t('analysis.algorithm.result.no_graph_result')"
+ );
+ expect(rankResult).toContain(
+ "t('analysis.algorithm.result.no_graph_result')"
+ );
+ });
+
+ it('keeps the query key in the Gremlin empty-graph view', () => {
+ const queryResult = readSource(
+ 'modules/analysis/QueryResult/GraphResult/Home/index.js'
+ );
+
+ expect(queryResult).toContain(
+ "t('analysis.query_result.no_graph_result')"
+ );
+ expect(queryResult).not.toContain(
+ "t('analysis.algorithm.result.no_graph_result')"
+ );
+ });
+});
diff --git
a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json
b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json
index cec574354..cea1beb88 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json
@@ -545,7 +545,8 @@
"similarity_value": "Similarity Value",
"rank_score": "Rank Score:",
"category": "Category {{index}}",
- "no_neighbor_at_degree": "No {{index}}-hop neighbors in this scenario"
+ "no_neighbor_at_degree": "No {{index}}-hop neighbors in this scenario",
+ "no_graph_result": "This algorithm returned no graph result to display"
},
"mode": {
"OLTP": "Interactive exploration",
diff --git
a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json
b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json
index 3e018c3ef..f2117218a 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json
@@ -545,7 +545,8 @@
"similarity_value": "相似度的值",
"rank_score": "排名得分:",
"category": "分类{{index}}",
- "no_neighbor_at_degree": "本场景下没有第{{index}}度邻居"
+ "no_neighbor_at_degree": "本场景下没有第{{index}}度邻居",
+ "no_graph_result": "当前算法没有可显示的图结果"
},
"mode": {
"OLTP": "交互式探索",
diff --git
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js
index 94eb6043d..76f0326b0 100644
--- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js
+++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/Home/index.js
@@ -536,7 +536,7 @@ const GraphResult = props => {
return (
<GraphStatusView
status={SUCCESS}
- message={t('analysis.query_result.no_graph_result')}
+
message={t('analysis.algorithm.result.no_graph_result')}
/>
);
}
diff --git
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js
index 20da0bb46..40fa906f8 100644
---
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js
+++
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.js
@@ -35,7 +35,7 @@ const RankApiView = props => {
return (
<GraphStatusView
status={GRAPH_STATUS.SUCCESS}
- message={t('analysis.query_result.no_graph_result')}
+ message={t('analysis.algorithm.result.no_graph_result')}
/>
);
}
diff --git
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadOptions.java
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadOptions.java
index b80ca0985..0c685bed2 100644
---
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadOptions.java
+++
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/executor/LoadOptions.java
@@ -49,6 +49,8 @@ public final class LoadOptions implements Cloneable {
private static final int DEFAULT_MAX_CONNECTIONS = CPUS * 4;
private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = CPUS * 2;
private static final int MINIMUM_REQUIRED_ARGS = 3;
+ private static final Set<String> SENSITIVE_PARAMETER_FIELDS =
+ ImmutableSet.of("password", "trustStoreToken", "token", "pdToken");
@Parameter(names = {"-f", "--file"}, required = true, arity = 1,
validateWith = {FileValidator.class},
@@ -359,7 +361,10 @@ public final class LoadOptions implements Cloneable {
for (Field field : fields) {
if (field.isAnnotationPresent(Parameter.class)) {
try {
- LOG.info(" {}={}", field.getName(), field.get(this));
+ Object value = SENSITIVE_PARAMETER_FIELDS.contains(
+ field.getName()) ? "[REDACTED]" :
+ field.get(this);
+ LOG.info(" {}={}", field.getName(), value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
diff --git
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/reader/file/FileLineFetcher.java
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/reader/file/FileLineFetcher.java
index d2e05ab7b..a2f1a0607 100644
---
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/reader/file/FileLineFetcher.java
+++
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/reader/file/FileLineFetcher.java
@@ -226,6 +226,7 @@ public class FileLineFetcher extends LineFetcher {
*/
private boolean checkMatchHeader(String line) {
if (!this.source().format().needHeader() ||
+ Boolean.FALSE.equals(this.source().hasHeader()) ||
this.offset() != FIRST_LINE_OFFSET) {
return false;
}
diff --git
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/source/file/FileSource.java
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/source/file/FileSource.java
index 680fe069a..954e5f77d 100644
---
a/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/source/file/FileSource.java
+++
b/hugegraph-loader/src/main/java/org/apache/hugegraph/loader/source/file/FileSource.java
@@ -26,6 +26,7 @@ import org.apache.hugegraph.loader.source.AbstractSource;
import org.apache.hugegraph.loader.source.SourceType;
import org.apache.hugegraph.loader.util.DateUtil;
import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.google.common.collect.ImmutableList;
@@ -58,6 +59,9 @@ public class FileSource extends AbstractSource {
// Only works for single files
@JsonProperty("split_count")
private int splitCount;
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ @JsonProperty("has_header")
+ private Boolean hasHeader;
// Whether header needs to be case-sensitive
private final boolean headerCaseSensitive;
@@ -66,10 +70,9 @@ public class FileSource extends AbstractSource {
this(null, new DirFilter(), new FileFilter(), FileFormat.CSV,
Constants.COMMA_STR, Constants.DATE_FORMAT,
ImmutableList.of(), Constants.TIME_ZONE,
- new SkippedLine(), Compression.NONE, 500);
+ new SkippedLine(), Compression.NONE, 500, null);
}
- @JsonCreator
public FileSource(@JsonProperty("path") String path,
@JsonProperty("dir_filter") DirFilter dirFilter,
@JsonProperty("filter") FileFilter filter,
@@ -82,6 +85,25 @@ public class FileSource extends AbstractSource {
@JsonProperty("skipped_line") SkippedLine skippedLine,
@JsonProperty("compression") Compression compression,
@JsonProperty("batch_size") Integer batchSize) {
+ this(path, dirFilter, filter, format, delimiter, dateFormat,
+ extraDateFormats, timeZone, skippedLine, compression, batchSize,
+ null);
+ }
+
+ @JsonCreator
+ public FileSource(@JsonProperty("path") String path,
+ @JsonProperty("dir_filter") DirFilter dirFilter,
+ @JsonProperty("filter") FileFilter filter,
+ @JsonProperty("format") FileFormat format,
+ @JsonProperty("delimiter") String delimiter,
+ @JsonProperty("date_format") String dateFormat,
+ @JsonProperty("extra_date_formats")
+ List<String> extraDateFormats,
+ @JsonProperty("time_zone") String timeZone,
+ @JsonProperty("skipped_line") SkippedLine skippedLine,
+ @JsonProperty("compression") Compression compression,
+ @JsonProperty("batch_size") Integer batchSize,
+ @JsonProperty("has_header") Boolean hasHeader) {
this.path = path;
this.dirFilter = dirFilter != null ? dirFilter : new DirFilter();
this.filter = filter != null ? filter : new FileFilter();
@@ -97,6 +119,7 @@ public class FileSource extends AbstractSource {
this.skippedLine = skippedLine != null ? skippedLine : new
SkippedLine();
this.compression = compression != null ? compression :
Compression.NONE;
this.batchSize = batchSize != null ? batchSize : 500;
+ this.hasHeader = hasHeader;
// When input is orc/parquet, header is case-insensitive
if (Compression.ORC.equals(this.compression()) ||
@@ -225,6 +248,14 @@ public class FileSource extends AbstractSource {
return this.splitCount;
}
+ public Boolean hasHeader() {
+ return this.hasHeader;
+ }
+
+ public void hasHeader(boolean hasHeader) {
+ this.hasHeader = hasHeader;
+ }
+
@Override
public FileSource asFileSource() {
FileSource source = new FileSource();
@@ -240,6 +271,7 @@ public class FileSource extends AbstractSource {
source.extraDateFormats = this.extraDateFormats;
source.skippedLine = this.skippedLine;
source.compression = this.compression;
+ source.hasHeader = this.hasHeader;
return source;
}
diff --git
a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/FileLineFetcherTest.java
b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/FileLineFetcherTest.java
new file mode 100644
index 000000000..d73ac9e65
--- /dev/null
+++
b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/FileLineFetcherTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hugegraph.loader.test.unit;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.hugegraph.loader.progress.InputItemProgress;
+import org.apache.hugegraph.loader.reader.Readable;
+import org.apache.hugegraph.loader.reader.file.FileLineFetcher;
+import org.apache.hugegraph.loader.reader.line.Line;
+import org.apache.hugegraph.loader.source.file.FileSource;
+import org.apache.hugegraph.testutil.Assert;
+import org.junit.Test;
+
+public class FileLineFetcherTest {
+
+ @Test
+ public void testHeaderlessFileKeepsFirstRowMatchingColumnName()
+ throws Exception {
+ Path file = Files.createTempFile("headerless-", ".csv");
+ Files.write(file, List.of("name", "Carol"));
+ FileLineFetcher fetcher = this.fetcher(file, false);
+ try {
+ Line first = fetcher.fetch();
+ Line second = fetcher.fetch();
+
+ Assert.assertEquals("name", first.rawLine());
+ Assert.assertEquals("Carol", second.rawLine());
+ Assert.assertNull(fetcher.fetch());
+ } finally {
+ fetcher.closeReader();
+ Files.deleteIfExists(file);
+ }
+ }
+
+ @Test
+ public void testPhysicalHeaderSkipsMatchingFirstRow()
+ throws Exception {
+ Path file = Files.createTempFile("physical-header-", ".csv");
+ Files.write(file, List.of("name", "Carol"));
+ FileLineFetcher fetcher = this.fetcher(file, true);
+ try {
+ Line first = fetcher.fetch();
+
+ Assert.assertEquals("Carol", first.rawLine());
+ Assert.assertNull(fetcher.fetch());
+ } finally {
+ fetcher.closeReader();
+ Files.deleteIfExists(file);
+ }
+ }
+
+ private FileLineFetcher fetcher(Path path, boolean hasHeader) {
+ FileSource source = new FileSource();
+ source.header(new String[]{"name"});
+ source.hasHeader(hasHeader);
+ FileLineFetcher fetcher = new FileLineFetcher(source);
+ fetcher.openReader(new TestReadable(path));
+ return fetcher;
+ }
+
+ private static final class TestReadable implements Readable {
+
+ private final Path path;
+
+ private TestReadable(Path path) {
+ this.path = path;
+ }
+
+ @Override
+ public String name() {
+ return this.path.getFileName().toString();
+ }
+
+ @Override
+ public org.apache.hadoop.fs.Path path() {
+ return new org.apache.hadoop.fs.Path(this.path.toString());
+ }
+
+ @Override
+ public InputStream open() throws IOException {
+ return new FileInputStream(this.path.toFile());
+ }
+
+ @Override
+ public InputItemProgress inputItemProgress() {
+ return null;
+ }
+ }
+}
diff --git
a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadOptionsTest.java
b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadOptionsTest.java
index b327f59ef..90f466cab 100644
---
a/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadOptionsTest.java
+++
b/hugegraph-loader/src/test/java/org/apache/hugegraph/loader/test/unit/LoadOptionsTest.java
@@ -133,6 +133,33 @@ public class LoadOptionsTest {
}
}
+ @Test
+ public void testDumpParamsRedactsSensitiveValues() {
+ LoadOptions options = new LoadOptions();
+ options.password = "graph-password";
+ options.trustStoreToken = "trust-store-password";
+ options.token = "graph-jwt";
+ options.pdToken = "pd-token";
+
+ CapturingAppender appender = attachAppender();
+ try {
+ options.dumpParams();
+ } finally {
+ detachAppender(appender);
+ }
+
+ Assert.assertTrue(appender.contains("password=[REDACTED]"));
+ Assert.assertTrue(appender.contains("trustStoreToken=[REDACTED]"));
+ Assert.assertTrue(appender.contains("token=[REDACTED]"));
+ Assert.assertTrue(appender.contains("pdToken=[REDACTED]"));
+ Assert.assertTrue(appender.contains("graph=hugegraph"));
+ Assert.assertTrue(appender.contains("host=localhost"));
+ Assert.assertFalse(appender.contains("graph-password"));
+ Assert.assertFalse(appender.contains("trust-store-password"));
+ Assert.assertFalse(appender.contains("graph-jwt"));
+ Assert.assertFalse(appender.contains("pd-token"));
+ }
+
private static int readStaticInt(Class<?> type, String name)
throws Exception {
Field field = type.getDeclaredField(name);