From 65819696fdf0e02d0324dd56e87b33dc6a7e1fd9 Mon Sep 17 00:00:00 2001 From: Jimmie Date: Sun, 8 Feb 2026 07:21:24 +0800 Subject: [PATCH 1/2] feat: add simplified Space API and unit test mocks - Add SpaceResource for simplified space registration API - Add SpaceRegistryInfo and SpaceRegistryResult DTOs - Add mock implementations for Redis-dependent tests: - MockGTRClient: in-memory replacement for Redis GT routes - MockNSRClient: in-memory replacement for Redis network routes - MockDistributedLockFactory: in-memory lock for tests - Add unit tests: BasicResourceTest, NetworkResourceTest, SpaceResourceTest - Update README with contributing guidelines Co-Authored-By: Claude Opus 4.5 --- README.md | 88 ++++++++ README_cn.md | 88 ++++++++ .../dto/registry/SpaceRegistryInfo.java | 49 +++++ .../dto/registry/SpaceRegistryResult.java | 52 +++++ .../services/registry/rest/SpaceResource.java | 92 ++++++++ .../registry/service/RegistryService.java | 80 +++++++ .../basic/rest/BasicResourceTest.java | 94 ++++++++ .../services/cache/MockGTRClient.java | 152 +++++++++++++ .../services/cache/MockNSRClient.java | 68 ++++++ .../lock/MockDistributedLockFactory.java | 79 +++++++ .../network/rest/NetworkResourceTest.java | 167 ++++++++++++++ .../registry/rest/SpaceResourceTest.java | 206 ++++++++++++++++++ 12 files changed, 1215 insertions(+) create mode 100644 eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/dto/registry/SpaceRegistryInfo.java create mode 100644 eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/dto/registry/SpaceRegistryResult.java create mode 100644 eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/rest/SpaceResource.java create mode 100644 eulixplatform-registry/src/test/java/xyz/eulix/platform/services/basic/rest/BasicResourceTest.java create mode 100644 eulixplatform-registry/src/test/java/xyz/eulix/platform/services/cache/MockGTRClient.java create mode 100644 eulixplatform-registry/src/test/java/xyz/eulix/platform/services/cache/MockNSRClient.java create mode 100644 eulixplatform-registry/src/test/java/xyz/eulix/platform/services/lock/MockDistributedLockFactory.java create mode 100644 eulixplatform-registry/src/test/java/xyz/eulix/platform/services/network/rest/NetworkResourceTest.java create mode 100644 eulixplatform-registry/src/test/java/xyz/eulix/platform/services/registry/rest/SpaceResourceTest.java diff --git a/README.md b/README.md index d32a6c2a..d74afe4c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ English | [简体中文](./README_cn.md) - [Base Service](#base-service) - [Environment Variables](#environment-variables) - [Build and Run Application](#build-and-run-application) +- [Testing](#testing) +- [Simplified Deployment](#simplified-deployment) - [Using OpenAPI and Swagger UI](#using-openapi-and-swagger-ui) - [Evolution Plan](#evolution-plan) - [Contribution Guidelines](#contribution-guidelines) @@ -128,6 +130,92 @@ The application is now runnable using the following command: java -jar target/quarkus-app/quarkus-run.jar ``` +## Testing + +### Running Unit Tests + +```shell script +./mvnw test +``` + +### Running Tests with Coverage Report + +```shell script +./mvnw clean verify jacoco:report +``` + +The coverage report will be generated at `target/site/jacoco/index.html`. + +### Test Structure + +The project includes the following test categories: + +| Test File | Coverage | +|-----------|----------| +| `RegistryResourceTest` | Box/User/Client registration API | +| `SpaceResourceTest` | Simplified space registration API | +| `NetworkResourceTest` | Network authentication and server APIs | +| `BasicResourceTest` | Platform status and ability APIs | +| `TokenResourceTest` | Token management APIs | +| `AuthServiceTest` | Authentication service logic | +| `CommonUtilsTest` | Utility functions | +| `OperationUtilsTest` | Crypto operations | + +### API Smoke Test + +After deployment, you can run the smoke test: + +```bash +# Set the platform base URL +export PLATFORM_BASE=http://localhost:8080 + +# Run smoke test +../../scripts/platform-smoke.sh + +# Run full API test +../../scripts/platform-api-test.sh +``` + +## Simplified Deployment + +For single-machine personal deployment, we provide a simplified setup that reduces complexity: + +- **Containers**: 6 instead of 7 (merged mysql-update) +- **DNS Records**: 2 instead of 7 (just `@` and `*`) +- **Registration API**: 1 step instead of 4 (new `/v2/platform/spaces` endpoint) + +### Quick Start (Simplified) + +```bash +cd deploy/platform +cp .env.simple.example .env +# Edit .env with your domain and passwords + +mkdir -p data/ssl +# Place your SSL certificate (tls.crt, tls.key) in data/ssl/ + +docker compose -f docker-compose.simple.yml up -d +./scripts/init-network.sh +``` + +### Simplified API + +The new `/v2/platform/spaces` endpoint combines box, user, and client registration: + +```bash +curl -X POST https://platform.example.com/v2/platform/spaces \ + -H "Content-Type: application/json" \ + -H "Request-Id: $(uuidgen)" \ + -d '{ + "boxUUID": "your-box-uuid", + "userId": "admin", + "clientUUID": "your-client-uuid", + "subdomain": "myspace" + }' +``` + +For detailed API changes, see [Platform API Changes](../../docs/en/platform-api-changes.md). + ## Using OpenAPI and Swagger UI OpenAPI descriptor and Swagger UI frontend to test your REST endpoints: `http://localhost:8080/platform/q/swagger-ui/` diff --git a/README_cn.md b/README_cn.md index 60d4c6b0..ed7207be 100644 --- a/README_cn.md +++ b/README_cn.md @@ -8,6 +8,8 @@ - [Base Service](#base-service) - [环境变量](#环境变量) - [构建和运行应用程序](#构建和运行应用程序) +- [测试](#测试) +- [精简部署](#精简部署) - [使用 OpenAPI 和 Swagger UI](#使用-openapi-和-swagger-ui) - [演进计划](#演进计划) - [贡献指南](#贡献指南) @@ -125,6 +127,92 @@ app: java -jar target/quarkus-app/quarkus-run.jar ``` +## 测试 + +### 运行单元测试 + +```shell脚本 +./mvnw test +``` + +### 运行测试并生成覆盖率报告 + +```shell脚本 +./mvnw clean verify jacoco:report +``` + +覆盖率报告将生成在 `target/site/jacoco/index.html`。 + +### 测试结构 + +项目包含以下测试类别: + +| 测试文件 | 覆盖范围 | +|---------|---------| +| `RegistryResourceTest` | 盒子/用户/客户端注册 API | +| `SpaceResourceTest` | 简化的空间注册 API | +| `NetworkResourceTest` | 网络认证和服务器 API | +| `BasicResourceTest` | 平台状态和能力 API | +| `TokenResourceTest` | Token 管理 API | +| `AuthServiceTest` | 认证服务逻辑 | +| `CommonUtilsTest` | 工具函数 | +| `OperationUtilsTest` | 加密操作 | + +### API 烟雾测试 + +部署后,可以运行烟雾测试: + +```bash +# 设置平台基础 URL +export PLATFORM_BASE=http://localhost:8080 + +# 运行烟雾测试 +../../scripts/platform-smoke.sh + +# 运行完整 API 测试 +../../scripts/platform-api-test.sh +``` + +## 精简部署 + +对于单机个人部署,我们提供了简化的配置: + +- **容器数量**: 6 个(合并了 mysql-update) +- **DNS 记录**: 2 条(只需 `@` 和 `*`) +- **注册 API**: 1 步完成(新的 `/v2/platform/spaces` 端点) + +### 快速开始(精简版) + +```bash +cd deploy/platform +cp .env.simple.example .env +# 编辑 .env 设置域名和密码 + +mkdir -p data/ssl +# 将 SSL 证书 (tls.crt, tls.key) 放入 data/ssl/ + +docker compose -f docker-compose.simple.yml up -d +./scripts/init-network.sh +``` + +### 简化 API + +新的 `/v2/platform/spaces` 端点合并了盒子、用户、客户端注册: + +```bash +curl -X POST https://platform.example.com/v2/platform/spaces \ + -H "Content-Type: application/json" \ + -H "Request-Id: $(uuidgen)" \ + -d '{ + "boxUUID": "your-box-uuid", + "userId": "admin", + "clientUUID": "your-client-uuid", + "subdomain": "myspace" + }' +``` + +详细的 API 变化请参阅 [平台 API 变更说明](../../docs/cn/platform-api-changes.md)。 + ## 使用 OpenAPI 和 Swagger UI OpenAPI 描述符和 Swagger UI 前端来测试 REST 端点,访问地址:`http://localhost:8080/platform/q/swagger-ui/` diff --git a/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/dto/registry/SpaceRegistryInfo.java b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/dto/registry/SpaceRegistryInfo.java new file mode 100644 index 00000000..b5eb80d8 --- /dev/null +++ b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/dto/registry/SpaceRegistryInfo.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.registry.dto.registry; + +import lombok.Data; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * Simplified space registration request DTO. + * Combines box, user, and client registration into one step. + */ +@Data +public class SpaceRegistryInfo { + @NotBlank + @Schema(description = "Box UUID") + private String boxUUID; + + @NotBlank + @Schema(description = "User ID") + private String userId; + + @NotBlank + @Schema(description = "Client UUID") + private String clientUUID; + + @Schema(description = "Preferred subdomain (optional, auto-generated if not provided)") + @Pattern(regexp = "^[a-z][a-z0-9]{5,19}$", message = "Subdomain must start with a letter, contain only lowercase letters and numbers, and be 6-20 characters long") + private String subdomain; + + @Schema(description = "User type: user_admin or user_member, defaults to user_admin") + private String userType; +} diff --git a/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/dto/registry/SpaceRegistryResult.java b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/dto/registry/SpaceRegistryResult.java new file mode 100644 index 00000000..47680c7f --- /dev/null +++ b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/dto/registry/SpaceRegistryResult.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.registry.dto.registry; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Simplified space registration result DTO. + * Returns all necessary information for the client to connect. + */ +@Data +@AllArgsConstructor(staticName = "of") +@NoArgsConstructor +public class SpaceRegistryResult { + @Schema(description = "Box UUID") + private String boxUUID; + + @Schema(description = "User ID") + private String userId; + + @Schema(description = "Client UUID") + private String clientUUID; + + @Schema(description = "Assigned subdomain") + private String subdomain; + + @Schema(description = "Full user domain for accessing the space") + private String userDomain; + + @Schema(description = "User type: user_admin or user_member") + private String userType; + + @Schema(description = "Network client credentials for NAT traversal") + private NetworkClient networkClient; +} diff --git a/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/rest/SpaceResource.java b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/rest/SpaceResource.java new file mode 100644 index 00000000..424fa77f --- /dev/null +++ b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/rest/SpaceResource.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.registry.rest; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; +import xyz.eulix.platform.common.support.log.Logged; +import xyz.eulix.platform.services.registry.dto.registry.SpaceRegistryInfo; +import xyz.eulix.platform.services.registry.dto.registry.SpaceRegistryResult; +import xyz.eulix.platform.services.registry.service.RegistryService; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Simplified Space Registration API. + * Provides a one-step registration process for personal deployment scenarios. + */ +@RequestScoped +@Path("/v2/platform") +@Tag(name = "Platform Space Service", description = "Simplified space registration API for personal deployment") +public class SpaceResource { + private static final Logger LOG = Logger.getLogger("app.log"); + + @Inject + RegistryService registryService; + + @Logged + @POST + @Path("/spaces") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(description = "One-step space registration. Registers box, user, and client in a single call. " + + "Returns network credentials and user domain for immediate use.") + public SpaceRegistryResult registerSpace( + @Valid SpaceRegistryInfo spaceInfo, + @HeaderParam("Request-Id") @NotBlank String reqId) { + LOG.infov("Space registration request: boxUUID={0}, userId={1}, clientUUID={2}, subdomain={3}", + spaceInfo.getBoxUUID(), spaceInfo.getUserId(), spaceInfo.getClientUUID(), spaceInfo.getSubdomain()); + return registryService.registerSpace(spaceInfo); + } + + @Logged + @DELETE + @Path("/spaces/{box_uuid}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Delete space registration. Removes box, all users, clients, and subdomains.") + public Response deleteSpace( + @PathParam("box_uuid") @NotBlank String boxUUID, + @HeaderParam("Request-Id") @NotBlank String reqId) { + LOG.infov("Space deletion request: boxUUID={0}", boxUUID); + boolean exists = registryService.hasBoxRegistered(boxUUID); + if (!exists) { + LOG.warnv("Box not registered: boxUUID={0}", boxUUID); + throw new WebApplicationException("Box not registered", Response.Status.NOT_FOUND); + } + registryService.resetBox(boxUUID); + return Response.noContent().build(); + } + + @Logged + @GET + @Path("/spaces/{box_uuid}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(description = "Get space registration details including users, clients, and subdomains.") + public Response getSpace( + @PathParam("box_uuid") @NotBlank String boxUUID, + @HeaderParam("Request-Id") @NotBlank String reqId) { + LOG.infov("Space query request: boxUUID={0}", boxUUID); + return Response.ok(registryService.boxRegistryBindUserAndClientInfo(boxUUID)).build(); + } +} diff --git a/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/service/RegistryService.java b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/service/RegistryService.java index 09e48cc5..8648e8e1 100644 --- a/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/service/RegistryService.java +++ b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/service/RegistryService.java @@ -142,6 +142,86 @@ public BoxRegistryResult registryBox (BoxTokenEntity boxToken, String networkCli return BoxRegistryResult.of(boxEntity.getBoxUUID(), NetworkClient.of(boxEntity.getNetworkClientId(), networkClientSecret)); } + /** + * Simplified one-step space registration. + * Registers box, user, and client in a single transaction. + * + * @param spaceInfo Registration information + * @return Complete registration result with network credentials + */ + @Transactional + public SpaceRegistryResult registerSpace(SpaceRegistryInfo spaceInfo) { + String boxUUID = spaceInfo.getBoxUUID(); + String userId = spaceInfo.getUserId(); + String clientUUID = spaceInfo.getClientUUID(); + String userType = CommonUtils.isNullOrEmpty(spaceInfo.getUserType()) ? + RegistryTypeEnum.USER_ADMIN.getName() : spaceInfo.getUserType(); + + // Step 1: Register or get existing box + RegistryBoxEntity boxEntity; + var existingBox = boxEntityRepository.findByBoxUUID(boxUUID); + if (existingBox.isPresent()) { + boxEntity = existingBox.get(); + LOG.infov("Box already registered, reusing: boxUUID={0}", boxUUID); + } else { + boxEntity = registryBox(boxUUID, "space_reg_" + CommonUtils.createUnifiedRandomCharacters(6), CommonUtils.getUUID()); + networkService.calculateNetworkRoute(boxEntity.getNetworkClientId()); + LOG.infov("New box registered: boxUUID={0}, networkClientId={1}", boxUUID, boxEntity.getNetworkClientId()); + } + + // Step 2: Check and reset existing user if necessary + Optional existingUser = userEntityRepository.findUserByBoxUUIDAndUserId(boxUUID, userId); + if (existingUser.isPresent()) { + resetUserInner(boxUUID, userId); + LOG.infov("Existing user reset: boxUUID={0}, userId={1}", boxUUID, userId); + } + + // Step 3: Generate or validate subdomain + SubdomainEntity subdomainEntity; + if (CommonUtils.isNullOrEmpty(spaceInfo.getSubdomain())) { + subdomainEntity = subdomainGen(boxUUID); + } else { + // Try to use the requested subdomain + Optional existingSubdomain = subdomainEntityRepository.findBySubdomain(spaceInfo.getSubdomain()); + if (existingSubdomain.isPresent()) { + // Check if it belongs to this box/user + SubdomainEntity existing = existingSubdomain.get(); + if (!existing.getBoxUUID().equals(boxUUID)) { + throw new ServiceOperationException(ServiceError.SUBDOMAIN_ALREADY_USED); + } + subdomainEntity = existing; + } else { + // Create new subdomain with requested name + subdomainEntity = subdomainSave(boxUUID, spaceInfo.getSubdomain(), null); + } + } + + // Step 4: Register user + RegistryUserEntity userEntity = registryUser(boxUUID, userId, RegistryTypeEnum.fromValue(userType)); + + // Step 5: Update subdomain state to USED + subdomainEntityRepository.updateBySubdomain(userId, SubdomainStateEnum.USED.getState(), subdomainEntity.getSubdomain()); + + // Step 6: Cache GT route + networkService.cacheGTRouteBasic(userId, subdomainEntity.getUserDomain(), boxUUID); + + // Step 7: Register client + RegistryClientEntity clientEntity = registryClient(boxUUID, userId, clientUUID, RegistryTypeEnum.CLIENT_BIND); + + LOG.infov("Space registration complete: boxUUID={0}, userId={1}, subdomain={2}, userDomain={3}", + boxUUID, userId, subdomainEntity.getSubdomain(), subdomainEntity.getUserDomain()); + + return SpaceRegistryResult.of( + boxUUID, + userId, + clientUUID, + subdomainEntity.getSubdomain(), + subdomainEntity.getUserDomain(), + userEntity.getRegistryType(), + NetworkClient.of(boxEntity.getNetworkClientId(), boxEntity.getNetworkSecretKey()) + ); + } + @Transactional public UserRegistryResult registryUser (UserRegistryInfo userRegistryInfo, String boxUUID) { diff --git a/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/basic/rest/BasicResourceTest.java b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/basic/rest/BasicResourceTest.java new file mode 100644 index 00000000..c50256f2 --- /dev/null +++ b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/basic/rest/BasicResourceTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.basic.rest; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +class BasicResourceTest { + + @Test + void status_returnsOkAndVersion() { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .get("/v2/platform/status") + .then() + .statusCode(200) + .body("status", equalTo("ok")) + .body("version", notNullValue()); + } + + @Test + void status_missingRequestId_fails() { + given() + .when() + .get("/v2/platform/status") + .then() + .statusCode(400); + } + + @Test + void ability_returnsPlatformApis() { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .get("/v2/platform/ability") + .then() + .statusCode(200) + .body("platformApis", notNullValue()); + } + + @Test + void ability_containsExpectedApis() { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .get("/v2/platform/ability") + .then() + .statusCode(200) + .body("platformApis", not(empty())); + } + + @Test + void ability_missingRequestId_fails() { + given() + .when() + .get("/v2/platform/ability") + .then() + .statusCode(400); + } + + @Test + void status_multipleRequests_allSucceed() { + for (int i = 0; i < 5; i++) { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .get("/v2/platform/status") + .then() + .statusCode(200) + .body("status", equalTo("ok")); + } + } +} diff --git a/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/cache/MockGTRClient.java b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/cache/MockGTRClient.java new file mode 100644 index 00000000..3f322cc3 --- /dev/null +++ b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/cache/MockGTRClient.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.cache; + +import io.quarkus.test.Mock; +import org.jboss.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Mock GTRClient for tests - uses in-memory storage instead of Redis. + */ +@Mock +@ApplicationScoped +public class MockGTRClient extends GTRClient { + private static final Logger LOG = Logger.getLogger("app.log"); + + private final Map gtRouteBasicCache = new HashMap<>(); + private final Map> gtRouteClientsCache = new HashMap<>(); + private final Map> gtRouteAppTokensCache = new HashMap<>(); + + @Override + public GTRouteBasic getGTRouteBasic(String subdomain) { + NetworkBasic networkBasic = gtRouteBasicCache.get(GTR_PREV + subdomain); + LOG.debugv("[Mock] get GTRouteBasic, subdomain:{0}, found:{1}", subdomain, networkBasic != null); + return new GTRouteBasic(subdomain, networkBasic); + } + + @Override + public void setGTRouteBasic(GTRouteBasic gtRouteBasic) { + String key = GTR_PREV + gtRouteBasic.getSubdomain(); + gtRouteBasicCache.put(key, gtRouteBasic.getNetworkBasic()); + LOG.debugv("[Mock] set GTRouteBasic, key:{0}", key); + } + + @Override + public void setGTRouteClients(GTRouteClients gtRouteClients) { + String key = GTR_PREV_CLIENTS + gtRouteClients.getBoxUUID() + SEPARATOR + gtRouteClients.getUserId(); + if (gtRouteClients.getClientUUIDs() == null || gtRouteClients.getClientUUIDs().isEmpty()) { + return; + } + gtRouteClientsCache.computeIfAbsent(key, k -> new HashSet<>()) + .addAll(gtRouteClients.getClientUUIDs()); + LOG.debugv("[Mock] set GTRouteClients, key:{0}", key); + } + + @Override + public void setRedirect(String subdomain, String serverAddr, String clientId, String newUserDomain, + NSRRedirectStateEnum redirectState, String boxUUID, String userId) { + NetworkBasic networkBasic = new NetworkBasic(serverAddr, clientId, newUserDomain, redirectState.getState(), boxUUID, userId); + GTRouteBasic gtRouteBasic = new GTRouteBasic(subdomain, networkBasic); + setGTRouteBasic(gtRouteBasic); + } + + @Override + public void addClientUUID(String boxUUID, String userId, String clientUUID) { + String key = GTR_PREV_CLIENTS + boxUUID + SEPARATOR + userId; + gtRouteClientsCache.computeIfAbsent(key, k -> new HashSet<>()).add(clientUUID); + LOG.debugv("[Mock] add GTRouteClient, key:{0}, value:{1}", key, clientUUID); + } + + @Override + public void removeClientUUID(String boxUUID, String userId, String clientUUID) { + String key = GTR_PREV_CLIENTS + boxUUID + SEPARATOR + userId; + Set clients = gtRouteClientsCache.get(key); + if (clients != null) { + clients.remove(clientUUID); + } + LOG.debugv("[Mock] remove GTRouteClient, key:{0}, value:{1}", key, clientUUID); + } + + @Override + public void addAppToken(String boxUUID, String appToken) { + String key = GTR_PREV_APP_TOKENS + boxUUID; + gtRouteAppTokensCache.computeIfAbsent(key, k -> new HashSet<>()).add(appToken); + LOG.debugv("[Mock] add GTRouteAppToken, key:{0}, value:{1}", key, appToken); + } + + @Override + public void removeAppToken(String boxUUID, String appToken) { + String key = GTR_PREV_APP_TOKENS + boxUUID; + Set tokens = gtRouteAppTokensCache.get(key); + if (tokens != null) { + tokens.remove(appToken); + } + LOG.debugv("[Mock] remove GTRouteAppToken, key:{0}, value:{1}", key, appToken); + } + + @Override + public void expireGTRouteBasic(String subdomain, String expireSeconds) { + LOG.debugv("[Mock] expire GTRouteBasic (no-op), subdomain:{0}", subdomain); + } + + @Override + public void expireGTRouteClients(String boxUUID, String userId, String expireSeconds) { + LOG.debugv("[Mock] expire GTRouteClients (no-op), boxUUID:{0}, userId:{1}", boxUUID, userId); + } + + @Override + public void expireGTRouteAppTokens(String boxUUID, String expireSeconds) { + LOG.debugv("[Mock] expire GTRouteAppTokens (no-op), boxUUID:{0}", boxUUID); + } + + @Override + public void clearGTRouteBasic(List subdomains) { + for (String subdomain : subdomains) { + gtRouteBasicCache.remove(GTR_PREV + subdomain); + } + LOG.debugv("[Mock] clear GTRouteBasic, subdomains:{0}", subdomains); + } + + @Override + public void clearGTRouteClients(String boxUUID, String userId) { + String key = GTR_PREV_CLIENTS + boxUUID + SEPARATOR + userId; + gtRouteClientsCache.remove(key); + LOG.debugv("[Mock] clear GTRouteClients, key:{0}", key); + } + + @Override + public void clearNSRouteAppTokens(List boxUUIDs) { + for (String boxUUID : boxUUIDs) { + gtRouteAppTokensCache.remove(GTR_PREV_APP_TOKENS + boxUUID); + } + LOG.debugv("[Mock] clear NSRouteAppTokens, boxUUIDs:{0}", boxUUIDs); + } + + // Utility method to clear all caches (useful for test cleanup) + public void clearAll() { + gtRouteBasicCache.clear(); + gtRouteClientsCache.clear(); + gtRouteAppTokensCache.clear(); + } +} diff --git a/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/cache/MockNSRClient.java b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/cache/MockNSRClient.java new file mode 100644 index 00000000..8193878a --- /dev/null +++ b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/cache/MockNSRClient.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.cache; + +import io.quarkus.test.Mock; +import org.jboss.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import java.util.HashMap; +import java.util.Map; + +/** + * Mock NSRClient for tests - uses in-memory storage instead of Redis. + */ +@Mock +@ApplicationScoped +public class MockNSRClient extends NSRClient { + private static final Logger LOG = Logger.getLogger("app.log"); + + private final Map nsRouteCache = new HashMap<>(); + + @Override + public NSRoute getNSRoute(String userDomain) { + String networkInfo = nsRouteCache.get(NSR_PREV + userDomain); + LOG.debugv("[Mock] get NSRoute, userDomain:{0}, found:{1}", userDomain, networkInfo != null); + return new NSRoute(NSR_PREV + userDomain, networkInfo); + } + + @Override + public void setNSRoute(NSRoute nsRoute) { + nsRouteCache.put(nsRoute.getUserDomain(), nsRoute.getNetworkInfo()); + LOG.debugv("[Mock] set NSRoute, key:{0}, value:{1}", nsRoute.getUserDomain(), nsRoute.getNetworkInfo()); + } + + @Override + public void setNSRoute(String userDomain, String serverAddr, String clientId) { + setNSRoute(new NSRoute(NSR_PREV + userDomain, serverAddr + "," + clientId)); + } + + @Override + public void setRedirect(String userDomain, String serverAddr, String clientId, String newUserDomain, NSRRedirectStateEnum redirectState) { + setNSRoute(new NSRoute(NSR_PREV + userDomain, serverAddr + "," + clientId + "," + newUserDomain + "," + redirectState.getState())); + } + + @Override + public void expireNSRoute(String userDomain, String expireSeconds) { + LOG.debugv("[Mock] expire NSRoute (no-op), userDomain:{0}", userDomain); + } + + // Utility method to clear all caches (useful for test cleanup) + public void clearAll() { + nsRouteCache.clear(); + } +} diff --git a/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/lock/MockDistributedLockFactory.java b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/lock/MockDistributedLockFactory.java new file mode 100644 index 00000000..563be5dc --- /dev/null +++ b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/lock/MockDistributedLockFactory.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.lock; + +import io.quarkus.test.Mock; +import org.jboss.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Mock DistributedLockFactory for tests - uses in-memory locks instead of Redis. + */ +@Mock +@ApplicationScoped +public class MockDistributedLockFactory extends DistributedLockFactory { + private static final Logger LOG = Logger.getLogger("app.log"); + + private static final ConcurrentHashMap locks = new ConcurrentHashMap<>(); + + @Override + public DistributedLock newLock(String keyName) { + return new MockDistributedLock(keyName); + } + + private static class MockDistributedLock implements DistributedLock { + private static final Logger LOG = Logger.getLogger("app.log"); + private final String keyName; + private final ReentrantLock lock; + + public MockDistributedLock(String keyName) { + this.keyName = keyName; + this.lock = locks.computeIfAbsent(keyName, k -> new ReentrantLock()); + } + + @Override + public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException { + boolean acquired = lock.tryLock(waitTime, unit); + LOG.debugv("[Mock] tryLock with timeout, key:{0}, acquired:{1}", keyName, acquired); + return acquired; + } + + @Override + public boolean tryLock() { + boolean acquired = lock.tryLock(); + LOG.debugv("[Mock] tryLock, key:{0}, acquired:{1}", keyName, acquired); + return acquired; + } + + @Override + public void unlock() { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + LOG.debugv("[Mock] unlock, key:{0}", keyName); + } + } + } + + // Utility method to clear all locks (useful for test cleanup) + public static void clearAll() { + locks.clear(); + } +} diff --git a/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/network/rest/NetworkResourceTest.java b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/network/rest/NetworkResourceTest.java new file mode 100644 index 00000000..837c0607 --- /dev/null +++ b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/network/rest/NetworkResourceTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.network.rest; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import xyz.eulix.platform.services.network.dto.NetworkAuthReq; +import xyz.eulix.platform.services.registry.dto.registry.SpaceRegistryInfo; + +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +class NetworkResourceTest { + + private String boxUUID; + private String networkClientId; + private String networkSecretKey; + private String subdomain; + + @BeforeEach + void setup() { + // Register a space first to get valid network credentials + boxUUID = UUID.randomUUID().toString(); + String userId = "test-user-" + System.currentTimeMillis(); + String clientUUID = UUID.randomUUID().toString(); + + SpaceRegistryInfo info = new SpaceRegistryInfo(); + info.setBoxUUID(boxUUID); + info.setUserId(userId); + info.setClientUUID(clientUUID); + + var response = given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(info) + .when() + .post("/v2/platform/spaces") + .then() + .statusCode(200) + .extract(); + + networkClientId = response.path("networkClient.clientId"); + networkSecretKey = response.path("networkClient.secretKey"); + subdomain = response.path("subdomain"); + } + + @Test + void networkClientAuth_validCredentials_returnsTrue() { + NetworkAuthReq req = NetworkAuthReq.of(networkClientId, networkSecretKey); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(req) + .when() + .post("/v2/platform/clients/network/auth") + .then() + .statusCode(200) + .body("result", equalTo(true)); + } + + @Test + void networkClientAuth_invalidCredentials_returnsFalse() { + NetworkAuthReq req = NetworkAuthReq.of("invalid-client-id", "invalid-secret"); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(req) + .when() + .post("/v2/platform/clients/network/auth") + .then() + .statusCode(200) + .body("result", equalTo(false)); + } + + @Test + void networkClientAuth_wrongSecretKey_returnsFalse() { + NetworkAuthReq req = NetworkAuthReq.of(networkClientId, "wrong-secret"); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(req) + .when() + .post("/v2/platform/clients/network/auth") + .then() + .statusCode(200) + .body("result", equalTo(false)); + } + + @Test + void networkClientAuth_missingRequestId_fails() { + NetworkAuthReq req = NetworkAuthReq.of(networkClientId, networkSecretKey); + + given() + .contentType(ContentType.JSON) + .body(req) + .when() + .post("/v2/platform/clients/network/auth") + .then() + .statusCode(400); + } + + @Test + void networkServerDetail_validClientId_returnsServerInfo() { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .queryParam("network_client_id", networkClientId) + .when() + .get("/v2/platform/servers/network/detail") + .then() + .statusCode(200); + // Note: Server info may be empty if no network server is configured + } + + @Test + void networkServerDetail_missingClientId_fails() { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .get("/v2/platform/servers/network/detail") + .then() + .statusCode(400); + } + + @Test + void stunServerDetail_validSubdomain_returnsStunInfo() { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .queryParam("subdomain", subdomain) + .when() + .get("/v2/platform/servers/stun/detail") + .then() + .statusCode(200); + // Note: STUN info may be empty if no STUN server is configured + } + + @Test + void stunServerDetail_missingSubdomain_fails() { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .get("/v2/platform/servers/stun/detail") + .then() + .statusCode(400); + } +} diff --git a/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/registry/rest/SpaceResourceTest.java b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/registry/rest/SpaceResourceTest.java new file mode 100644 index 00000000..72c4837d --- /dev/null +++ b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/registry/rest/SpaceResourceTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2022 Institute of Software Chinese Academy of Sciences (ISCAS) + * + * Licensed 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 xyz.eulix.platform.services.registry.rest; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import xyz.eulix.platform.services.registry.dto.registry.SpaceRegistryInfo; + +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +class SpaceResourceTest { + + @Test + void registerSpace_success() { + String boxUUID = UUID.randomUUID().toString(); + String userId = "test-user-" + System.currentTimeMillis(); + String clientUUID = UUID.randomUUID().toString(); + + SpaceRegistryInfo info = new SpaceRegistryInfo(); + info.setBoxUUID(boxUUID); + info.setUserId(userId); + info.setClientUUID(clientUUID); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(info) + .when() + .post("/v2/platform/spaces") + .then() + .statusCode(200) + .body("boxUUID", equalTo(boxUUID)) + .body("userId", equalTo(userId)) + .body("clientUUID", equalTo(clientUUID)) + .body("subdomain", notNullValue()) + .body("userDomain", notNullValue()) + .body("userType", equalTo("user_admin")) + .body("networkClient.clientId", notNullValue()) + .body("networkClient.secretKey", notNullValue()); + } + + @Test + void registerSpace_withSubdomain() { + String boxUUID = UUID.randomUUID().toString(); + String userId = "test-user-" + System.currentTimeMillis(); + String clientUUID = UUID.randomUUID().toString(); + String subdomain = "test" + System.currentTimeMillis() % 1000000; + + SpaceRegistryInfo info = new SpaceRegistryInfo(); + info.setBoxUUID(boxUUID); + info.setUserId(userId); + info.setClientUUID(clientUUID); + info.setSubdomain(subdomain); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(info) + .when() + .post("/v2/platform/spaces") + .then() + .statusCode(200) + .body("subdomain", equalTo(subdomain)); + } + + @Test + void registerSpace_duplicateBox_reuses() { + String boxUUID = UUID.randomUUID().toString(); + String userId1 = "user1-" + System.currentTimeMillis(); + String userId2 = "user2-" + System.currentTimeMillis(); + String clientUUID1 = UUID.randomUUID().toString(); + String clientUUID2 = UUID.randomUUID().toString(); + + // First registration + SpaceRegistryInfo info1 = new SpaceRegistryInfo(); + info1.setBoxUUID(boxUUID); + info1.setUserId(userId1); + info1.setClientUUID(clientUUID1); + + String networkClientId = given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(info1) + .when() + .post("/v2/platform/spaces") + .then() + .statusCode(200) + .extract().path("networkClient.clientId"); + + // Second registration with same box + SpaceRegistryInfo info2 = new SpaceRegistryInfo(); + info2.setBoxUUID(boxUUID); + info2.setUserId(userId2); + info2.setClientUUID(clientUUID2); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(info2) + .when() + .post("/v2/platform/spaces") + .then() + .statusCode(200) + .body("networkClient.clientId", equalTo(networkClientId)); // Same network client + } + + @Test + void registerSpace_missingBoxUUID_fails() { + SpaceRegistryInfo info = new SpaceRegistryInfo(); + info.setUserId("user"); + info.setClientUUID(UUID.randomUUID().toString()); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(info) + .when() + .post("/v2/platform/spaces") + .then() + .statusCode(400); + } + + @Test + void deleteSpace_success() { + // First register + String boxUUID = UUID.randomUUID().toString(); + SpaceRegistryInfo info = new SpaceRegistryInfo(); + info.setBoxUUID(boxUUID); + info.setUserId("user"); + info.setClientUUID(UUID.randomUUID().toString()); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(info) + .when() + .post("/v2/platform/spaces") + .then() + .statusCode(200); + + // Then delete + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .delete("/v2/platform/spaces/" + boxUUID) + .then() + .statusCode(204); + } + + @Test + void deleteSpace_notFound() { + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .delete("/v2/platform/spaces/" + UUID.randomUUID()) + .then() + .statusCode(404); + } + + @Test + void getSpace_success() { + // First register + String boxUUID = UUID.randomUUID().toString(); + SpaceRegistryInfo info = new SpaceRegistryInfo(); + info.setBoxUUID(boxUUID); + info.setUserId("user"); + info.setClientUUID(UUID.randomUUID().toString()); + + given() + .contentType(ContentType.JSON) + .header("Request-Id", UUID.randomUUID().toString()) + .body(info) + .when() + .post("/v2/platform/spaces") + .then() + .statusCode(200); + + // Then get + given() + .header("Request-Id", UUID.randomUUID().toString()) + .when() + .get("/v2/platform/spaces/" + boxUUID) + .then() + .statusCode(200) + .body("networkClientId", notNullValue()); + } +} From c0857b72050f0fb58804d76ee788e3e2a57a6800 Mon Sep 17 00:00:00 2001 From: Jimmie Date: Sun, 8 Feb 2026 07:42:47 +0800 Subject: [PATCH 2/2] fix: resolve rebase compatibility issues - Fix registryBox call to include 4th parameter (networkSecretKey) - Fix registerSpace to return plaintext network secret instead of hash - Update MockDistributedLockFactory to use real MySQL locks for MySQL type Co-Authored-By: Claude Opus 4.5 --- .../registry/service/RegistryService.java | 8 +++++-- .../lock/MockDistributedLockFactory.java | 24 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/service/RegistryService.java b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/service/RegistryService.java index 8648e8e1..90c9d8d9 100644 --- a/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/service/RegistryService.java +++ b/eulixplatform-registry/src/main/java/xyz/eulix/platform/services/registry/service/RegistryService.java @@ -159,12 +159,16 @@ public SpaceRegistryResult registerSpace(SpaceRegistryInfo spaceInfo) { // Step 1: Register or get existing box RegistryBoxEntity boxEntity; + String networkSecretKeyPlaintext = null; // Keep plaintext for response var existingBox = boxEntityRepository.findByBoxUUID(boxUUID); if (existingBox.isPresent()) { boxEntity = existingBox.get(); LOG.infov("Box already registered, reusing: boxUUID={0}", boxUUID); + // Note: For existing boxes, we cannot retrieve the plaintext secret + // The client should have saved it from initial registration } else { - boxEntity = registryBox(boxUUID, "space_reg_" + CommonUtils.createUnifiedRandomCharacters(6), CommonUtils.getUUID()); + networkSecretKeyPlaintext = CommonUtils.getUUID(); + boxEntity = registryBox(boxUUID, "space_reg_" + CommonUtils.createUnifiedRandomCharacters(6), CommonUtils.getUUID(), networkSecretKeyPlaintext); networkService.calculateNetworkRoute(boxEntity.getNetworkClientId()); LOG.infov("New box registered: boxUUID={0}, networkClientId={1}", boxUUID, boxEntity.getNetworkClientId()); } @@ -218,7 +222,7 @@ public SpaceRegistryResult registerSpace(SpaceRegistryInfo spaceInfo) { subdomainEntity.getSubdomain(), subdomainEntity.getUserDomain(), userEntity.getRegistryType(), - NetworkClient.of(boxEntity.getNetworkClientId(), boxEntity.getNetworkSecretKey()) + NetworkClient.of(boxEntity.getNetworkClientId(), networkSecretKeyPlaintext) ); } diff --git a/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/lock/MockDistributedLockFactory.java b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/lock/MockDistributedLockFactory.java index 563be5dc..209c3560 100644 --- a/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/lock/MockDistributedLockFactory.java +++ b/eulixplatform-registry/src/test/java/xyz/eulix/platform/services/lock/MockDistributedLockFactory.java @@ -18,14 +18,18 @@ import io.quarkus.test.Mock; import org.jboss.logging.Logger; +import xyz.eulix.platform.services.config.ApplicationProperties; +import xyz.eulix.platform.services.lock.service.ReentrantLockService; import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** - * Mock DistributedLockFactory for tests - uses in-memory locks instead of Redis. + * Mock DistributedLockFactory for tests - uses in-memory locks for Redis, real MySQL locks. */ @Mock @ApplicationScoped @@ -34,8 +38,24 @@ public class MockDistributedLockFactory extends DistributedLockFactory { private static final ConcurrentHashMap locks = new ConcurrentHashMap<>(); + @Inject + ReentrantLockService mysqlLockService; + + @Inject + ApplicationProperties applicationProperties; + @Override - public DistributedLock newLock(String keyName) { + public DistributedLock newLock(String keyName, LockType lockType) { + LOG.debugv("[Mock] Creating lock, key:{0}, type:{1}", keyName, lockType); + + // Use real MySQL implementation for MySQL locks + if (lockType.equals(LockType.MySQLReentrantLock)) { + String lockValue = UUID.randomUUID().toString(); + Integer timeout = applicationProperties.getLockExpireTime(); + return new MySQLReentrantLock(mysqlLockService, keyName, lockValue, timeout); + } + + // Use in-memory mock for Redis locks return new MockDistributedLock(keyName); }