From f047fa0e2d38f1637bfb834ef5500f5fdc7b992d Mon Sep 17 00:00:00 2001 From: Carsten Rehfeld Date: Mon, 18 May 2026 22:23:41 +0200 Subject: [PATCH] feat(registry): service stage semantics for sandbox registration - Sandbox search without ?stage= now defaults to DEVELOPMENT (was PRODUCTION), so freshly registered services are discoverable by default - DEPRECATED and DECOMMISSIONED stages are rejected at creation with 400; they are lifecycle-only transitions, not valid initial states - PRODUCTION is accepted at creation for established services going live immediately BDD coverage: sandbox-stage.feature (6 scenarios, all green) Fixes 2 pre-existing failures in sandbox-service-isolation.feature. Co-Authored-By: Mira Rehfeld --- .../apix/registry/bdd/SandboxSteps.java | 23 ++++++++++ .../features/sandbox/sandbox-stage.feature | 44 +++++++++++++++++++ .../apix/registry/service/SandboxService.java | 10 ++++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 apix-registry/src/integration-test/resources/features/sandbox/sandbox-stage.feature diff --git a/apix-registry/src/integration-test/java/org/botstandards/apix/registry/bdd/SandboxSteps.java b/apix-registry/src/integration-test/java/org/botstandards/apix/registry/bdd/SandboxSteps.java index 72a77bc..dce39e2 100644 --- a/apix-registry/src/integration-test/java/org/botstandards/apix/registry/bdd/SandboxSteps.java +++ b/apix-registry/src/integration-test/java/org/botstandards/apix/registry/bdd/SandboxSteps.java @@ -73,6 +73,7 @@ public class SandboxSteps { .statusCode(201); } + @Given("a sandbox service with endpoint {string} capability {string} and extension {string} is registered in {string}") public void aSandboxServiceWithExtensionIsRegistered(String endpoint, String capability, String extension, String sandboxName) { @@ -254,6 +255,28 @@ public class SandboxSteps { .andReturn(); } + @When("sandbox {string} services are searched by capability {string} and stage {string}") + public void sandboxServiceSearchCalledWithStage(String sandboxName, String capability, String stage) { + lastResponse = given() + .get("/sandbox/" + resolveUuid(sandboxName) + "/services?capability=" + capability + "&stage=" + stage) + .andReturn(); + } + + @When("a sandbox service with stage {string} and endpoint {string} and capability {string} is registered in {string}") + public void whenASandboxServiceWithStageIsRegistered(String stage, String endpoint, String capability, String sandboxName) { + String key = resolveKey(sandboxName); + String uuid = resolveUuid(sandboxName); + Map payload = buildServicePayload("SandboxService-" + endpoint.hashCode(), endpoint, capability); + payload.put("serviceStage", stage); + lastResponse = given() + .contentType(JSON) + .header(SANDBOX_API_KEY_HEADER, key) + .body(payload) + .when() + .post("/sandbox/" + uuid + "/services") + .andReturn(); + } + @When("GET \\/services is called with capability {string} and the sandbox key for {string}") public void getServicesWithCapabilityAndSandboxKey(String capability, String sandboxName) { String key = resolveKey(sandboxName); diff --git a/apix-registry/src/integration-test/resources/features/sandbox/sandbox-stage.feature b/apix-registry/src/integration-test/resources/features/sandbox/sandbox-stage.feature new file mode 100644 index 0000000..002e7c8 --- /dev/null +++ b/apix-registry/src/integration-test/resources/features/sandbox/sandbox-stage.feature @@ -0,0 +1,44 @@ +Feature: Service stage semantics on creation and sandbox search + + # Creation rules: + # DEVELOPMENT → allowed (default when omitted) + # PRODUCTION → allowed (established services going live immediately) + # DEPRECATED / DECOMMISSIONED → rejected (lifecycle-only stages, not creation stages) + # + # Sandbox search default: + # no ?stage= param → DEVELOPMENT (find services you just registered) + + Background: + Given a sandbox named "stage-test" exists + + Scenario: Service created without stage defaults to DEVELOPMENT + Given a sandbox service with endpoint "https://dev-default.example.com" and capability "stage.test" is registered in "stage-test" + When sandbox "stage-test" services are searched by capability "stage.test" + Then the response code is 200 + And "https://dev-default.example.com" is in the endpoint list + + Scenario: Service created with explicit DEVELOPMENT stage is found by default sandbox search + Given a sandbox service with stage "DEVELOPMENT" and endpoint "https://dev-explicit.example.com" and capability "stage.test" is registered in "stage-test" + When sandbox "stage-test" services are searched by capability "stage.test" + Then the response code is 200 + And "https://dev-explicit.example.com" is in the endpoint list + + Scenario: Service created with PRODUCTION stage is not returned by default sandbox search + Given a sandbox service with stage "PRODUCTION" and endpoint "https://prod-explicit.example.com" and capability "stage.test" is registered in "stage-test" + When sandbox "stage-test" services are searched by capability "stage.test" + Then the response code is 200 + And "https://prod-explicit.example.com" is not in the endpoint list + + Scenario: Service created with PRODUCTION stage is found when searching with stage=PRODUCTION + Given a sandbox service with stage "PRODUCTION" and endpoint "https://prod-search.example.com" and capability "stage.test" is registered in "stage-test" + When sandbox "stage-test" services are searched by capability "stage.test" and stage "PRODUCTION" + Then the response code is 200 + And "https://prod-search.example.com" is in the endpoint list + + Scenario: Creating a service with DEPRECATED stage is rejected + When a sandbox service with stage "DEPRECATED" and endpoint "https://dep.example.com" and capability "stage.test" is registered in "stage-test" + Then the response code is 400 + + Scenario: Creating a service with DECOMMISSIONED stage is rejected + When a sandbox service with stage "DECOMMISSIONED" and endpoint "https://dec.example.com" and capability "stage.test" is registered in "stage-test" + Then the response code is 400 diff --git a/apix-registry/src/main/java/org/botstandards/apix/registry/service/SandboxService.java b/apix-registry/src/main/java/org/botstandards/apix/registry/service/SandboxService.java index 0ddfed8..ee6cf9b 100644 --- a/apix-registry/src/main/java/org/botstandards/apix/registry/service/SandboxService.java +++ b/apix-registry/src/main/java/org/botstandards/apix/registry/service/SandboxService.java @@ -200,7 +200,13 @@ public class SandboxService { service.livenessStatus = LivenessStatus.PENDING; service.registeredAt = now; service.registrantOrgType = payload.registrantOrgType() != null ? payload.registrantOrgType() : OrgType.INDIVIDUAL; - service.serviceStage = payload.serviceStage() != null ? payload.serviceStage() : ServiceStage.DEVELOPMENT; + ServiceStage requestedStage = payload.serviceStage() != null ? payload.serviceStage() : ServiceStage.DEVELOPMENT; + if (requestedStage == ServiceStage.DEPRECATED || requestedStage == ServiceStage.DECOMMISSIONED) { + throw new WebApplicationException(Response.status(400) + .entity(Map.of("message", "Stage " + requestedStage + " is a lifecycle-only stage and cannot be set at creation")) + .build()); + } + service.serviceStage = requestedStage; service.registryStatus = RegistryStatus.ACTIVE; service.version = 1; @@ -238,7 +244,7 @@ public class SandboxService { List properties) { ServiceStage targetStage = stage != null ? ServiceStage.valueOf(stage.toUpperCase()) - : ServiceStage.PRODUCTION; + : ServiceStage.DEVELOPMENT; List props = RegistryService.parsePropertyFilters(properties); StringBuilder sql = new StringBuilder(