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 <noreply@anthropic.com>
This commit is contained in:
+23
@@ -73,6 +73,7 @@ public class SandboxSteps {
|
|||||||
.statusCode(201);
|
.statusCode(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Given("a sandbox service with endpoint {string} capability {string} and extension {string} is registered in {string}")
|
@Given("a sandbox service with endpoint {string} capability {string} and extension {string} is registered in {string}")
|
||||||
public void aSandboxServiceWithExtensionIsRegistered(String endpoint, String capability,
|
public void aSandboxServiceWithExtensionIsRegistered(String endpoint, String capability,
|
||||||
String extension, String sandboxName) {
|
String extension, String sandboxName) {
|
||||||
@@ -254,6 +255,28 @@ public class SandboxSteps {
|
|||||||
.andReturn();
|
.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<String, Object> 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}")
|
@When("GET \\/services is called with capability {string} and the sandbox key for {string}")
|
||||||
public void getServicesWithCapabilityAndSandboxKey(String capability, String sandboxName) {
|
public void getServicesWithCapabilityAndSandboxKey(String capability, String sandboxName) {
|
||||||
String key = resolveKey(sandboxName);
|
String key = resolveKey(sandboxName);
|
||||||
|
|||||||
@@ -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
|
||||||
+8
-2
@@ -200,7 +200,13 @@ public class SandboxService {
|
|||||||
service.livenessStatus = LivenessStatus.PENDING;
|
service.livenessStatus = LivenessStatus.PENDING;
|
||||||
service.registeredAt = now;
|
service.registeredAt = now;
|
||||||
service.registrantOrgType = payload.registrantOrgType() != null ? payload.registrantOrgType() : OrgType.INDIVIDUAL;
|
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.registryStatus = RegistryStatus.ACTIVE;
|
||||||
service.version = 1;
|
service.version = 1;
|
||||||
|
|
||||||
@@ -238,7 +244,7 @@ public class SandboxService {
|
|||||||
List<String> properties) {
|
List<String> properties) {
|
||||||
ServiceStage targetStage = stage != null
|
ServiceStage targetStage = stage != null
|
||||||
? ServiceStage.valueOf(stage.toUpperCase())
|
? ServiceStage.valueOf(stage.toUpperCase())
|
||||||
: ServiceStage.PRODUCTION;
|
: ServiceStage.DEVELOPMENT;
|
||||||
|
|
||||||
List<String[]> props = RegistryService.parsePropertyFilters(properties);
|
List<String[]> props = RegistryService.parsePropertyFilters(properties);
|
||||||
StringBuilder sql = new StringBuilder(
|
StringBuilder sql = new StringBuilder(
|
||||||
|
|||||||
Reference in New Issue
Block a user