From a5d4f078e0eaff7064a218ccfd169941cf0fc6c2 Mon Sep 17 00:00:00 2001 From: Aryan_Gupta Date: Mon, 19 Oct 2026 21:54:58 +0530 Subject: [PATCH 1/2] feat: add policy lifecycle operations --- .../DefaultPolicyManagementService.java | 20 +++++ .../policy/PolicyManagementService.java | 2 + .../policy/PolicyNotFoundException.java | 8 ++ .../policy/TrafficPolicy.java | 12 +++ .../policy/api/PolicyController.java | 37 +++++++++ .../DefaultPolicyManagementServiceTest.java | 82 ++++++++++++++++++- .../policy/api/PolicyControllerTest.java | 73 ++++++++++++++++- .../traffic/TrafficDecisionEngineTest.java | 75 +++++++++++++++++ 8 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/yashdotdev/distributed_traffic_control/policy/PolicyNotFoundException.java diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/DefaultPolicyManagementService.java b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/DefaultPolicyManagementService.java index 6819590..5d24b28 100644 --- a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/DefaultPolicyManagementService.java +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/DefaultPolicyManagementService.java @@ -37,6 +37,26 @@ public TrafficPolicy updatePolicy(String resource, TrafficPolicy policy) { return policy; } + @Override + public TrafficPolicy activatePolicy(String resource) { + return changePolicyStatus(resource, PolicyStatus.ACTIVE); + } + + @Override + public TrafficPolicy deactivatePolicy(String resource) { + return changePolicyStatus(resource, PolicyStatus.INACTIVE); + } + + private TrafficPolicy changePolicyStatus(String resource, PolicyStatus status) { + TrafficPolicy policy = policyProvider.findPolicy(resource) + .orElseThrow(() -> new PolicyNotFoundException(resource)); + + TrafficPolicy updatedPolicy = policy.withStatus(status); + policyProvider.registerPolicy(resource, updatedPolicy); + + return updatedPolicy; + } + @Override public void removePolicy(String resource) { policyProvider.removePolicy(resource); diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/PolicyManagementService.java b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/PolicyManagementService.java index 53132ae..2e3f83f 100644 --- a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/PolicyManagementService.java +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/PolicyManagementService.java @@ -9,6 +9,8 @@ public interface PolicyManagementService { Optional findPolicy(String resource); List findAllPolicies(); TrafficPolicy updatePolicy(String resource, TrafficPolicy policy); + TrafficPolicy activatePolicy(String resource); + TrafficPolicy deactivatePolicy(String resource); void removePolicy(String resource); void setDefaultPolicy(TrafficPolicy policy); } \ No newline at end of file diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/PolicyNotFoundException.java b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/PolicyNotFoundException.java new file mode 100644 index 0000000..10b7a72 --- /dev/null +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/PolicyNotFoundException.java @@ -0,0 +1,8 @@ +package com.yashdotdev.distributed_traffic_control.policy; + +public class PolicyNotFoundException extends RuntimeException { + + public PolicyNotFoundException(String resource) { + super("No policy registered for resource: " + resource); + } +} diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/TrafficPolicy.java b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/TrafficPolicy.java index a9d3442..3f7c231 100644 --- a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/TrafficPolicy.java +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/TrafficPolicy.java @@ -48,4 +48,16 @@ public TrafficPolicy(String policyId, String name, TrafficPolicyType type, Polic public boolean isActive() { return status == PolicyStatus.ACTIVE; } + public TrafficPolicy withStatus(PolicyStatus newStatus) { + return new TrafficPolicy( + policyId, + name, + type, + Objects.requireNonNull(newStatus, "status must not be null"), + capacity, + refillRate, + windowDuration, + createdAt + ); + } } \ No newline at end of file diff --git a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/api/PolicyController.java b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/api/PolicyController.java index 98bd976..02b285e 100644 --- a/src/main/java/com/yashdotdev/distributed_traffic_control/policy/api/PolicyController.java +++ b/src/main/java/com/yashdotdev/distributed_traffic_control/policy/api/PolicyController.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import com.yashdotdev.distributed_traffic_control.policy.PolicyNotFoundException; import java.util.List; @@ -49,6 +50,42 @@ public ResponseEntity removePolicy(@RequestParam String resource) { return ResponseEntity.noContent().build(); } + + @PostMapping("/activate") + public ResponseEntity activatePolicy( + @RequestParam String resource) { + + if (resource.isBlank()) { + return ResponseEntity.badRequest().build(); + } + + try { + TrafficPolicy policy = + policyManagementService.activatePolicy(resource); + return ResponseEntity.ok(toResponse(resource, policy)); + } catch (PolicyNotFoundException exception) { + return ResponseEntity.notFound().build(); + } + } + + @PostMapping("/deactivate") + public ResponseEntity deactivatePolicy( + @RequestParam String resource) { + + if (resource.isBlank()) { + return ResponseEntity.badRequest().build(); + } + + try { + TrafficPolicy policy = + policyManagementService.deactivatePolicy(resource); + return ResponseEntity.ok(toResponse(resource, policy)); + } catch (PolicyNotFoundException exception) { + return ResponseEntity.notFound().build(); + } + } + + private TrafficPolicy toPolicy(PolicyCreateRequest request) { return new TrafficPolicy(request.getPolicyId(), request.getName(), request.getType(), request.getStatus(), request.getCapacity(), request.getRefillRate(), request.getWindowDuration(), request.getCreatedAt()); } diff --git a/src/test/java/com/yashdotdev/distributed_traffic_control/policy/DefaultPolicyManagementServiceTest.java b/src/test/java/com/yashdotdev/distributed_traffic_control/policy/DefaultPolicyManagementServiceTest.java index a028ffa..b873c1c 100644 --- a/src/test/java/com/yashdotdev/distributed_traffic_control/policy/DefaultPolicyManagementServiceTest.java +++ b/src/test/java/com/yashdotdev/distributed_traffic_control/policy/DefaultPolicyManagementServiceTest.java @@ -1,13 +1,14 @@ package com.yashdotdev.distributed_traffic_control.policy; import com.yashdotdev.distributed_traffic_control.traffic.TrafficRequest; +import com.yashdotdev.distributed_traffic_control.policy.PolicyNotFoundException; + import org.junit.jupiter.api.Test; import java.time.Duration; import java.time.Instant; import static org.junit.jupiter.api.Assertions.*; - class DefaultPolicyManagementServiceTest { private static final Instant CREATED_AT = Instant.parse("2026-09-03T00:00:00Z"); @@ -54,4 +55,83 @@ private TrafficPolicy createPolicy(String policyId, TrafficPolicyType type) { private TrafficRequest createRequest(String resource) { return new TrafficRequest("request-1", new com.yashdotdev.distributed_traffic_control.traffic.TrafficSubject("user-123", com.yashdotdev.distributed_traffic_control.traffic.TrafficSubjectType.USER), resource, CREATED_AT); } + + + @Test + void shouldActivateInactivePolicyAndPreserveItsFields() { + InMemoryPolicyProvider provider = new InMemoryPolicyProvider(); + DefaultPolicyManagementService service = new DefaultPolicyManagementService(provider); + + TrafficPolicy original = new TrafficPolicy( + "orders-policy", "Orders", TrafficPolicyType.TOKEN_BUCKET, + PolicyStatus.INACTIVE, 10, 2, Duration.ofMinutes(1), CREATED_AT + ); + service.registerPolicy("/api/orders", original); + + TrafficPolicy result = service.activatePolicy("/api/orders"); + + assertEquals(PolicyStatus.ACTIVE, result.getStatus()); + assertEquals(original.getPolicyId(), result.getPolicyId()); + assertEquals(original.getName(), result.getName()); + assertEquals(original.getType(), result.getType()); + assertEquals(original.getCapacity(), result.getCapacity()); + assertEquals(original.getRefillRate(), result.getRefillRate()); + assertEquals(original.getWindowDuration(), result.getWindowDuration()); + assertEquals(original.getCreatedAt(), result.getCreatedAt()); + assertSame(result, provider.findPolicy("/api/orders").orElseThrow()); + } + + @Test + void shouldDeactivateActivePolicy() { + InMemoryPolicyProvider provider = new InMemoryPolicyProvider(); + DefaultPolicyManagementService service = new DefaultPolicyManagementService(provider); + service.registerPolicy("/api/orders", + createPolicy("orders-policy", TrafficPolicyType.TOKEN_BUCKET)); + + TrafficPolicy result = service.deactivatePolicy("/api/orders"); + + assertEquals(PolicyStatus.INACTIVE, result.getStatus()); + assertEquals(PolicyStatus.INACTIVE, + provider.findPolicy("/api/orders").orElseThrow().getStatus()); + } + + @Test + void activatingAnAlreadyActivePolicyShouldBeIdempotent() { + InMemoryPolicyProvider provider = new InMemoryPolicyProvider(); + DefaultPolicyManagementService service = new DefaultPolicyManagementService(provider); + service.registerPolicy("/api/orders", + createPolicy("orders-policy", TrafficPolicyType.TOKEN_BUCKET)); + + TrafficPolicy first = service.activatePolicy("/api/orders"); + TrafficPolicy second = service.activatePolicy("/api/orders"); + + assertEquals(PolicyStatus.ACTIVE, second.getStatus()); + assertEquals(first.getPolicyId(), second.getPolicyId()); + assertEquals(first.getCreatedAt(), second.getCreatedAt()); + } + + @Test + void deactivatingAnAlreadyInactivePolicyShouldBeIdempotent() { + InMemoryPolicyProvider provider = new InMemoryPolicyProvider(); + DefaultPolicyManagementService service = new DefaultPolicyManagementService(provider); + service.registerPolicy("/api/orders", + createPolicy("orders-policy", TrafficPolicyType.TOKEN_BUCKET)); + + service.deactivatePolicy("/api/orders"); + TrafficPolicy second = service.deactivatePolicy("/api/orders"); + + assertEquals(PolicyStatus.INACTIVE, second.getStatus()); + } + + @Test + void changingStatusOfMissingPolicyShouldThrowException() { + InMemoryPolicyProvider provider = new InMemoryPolicyProvider(); + DefaultPolicyManagementService service = new DefaultPolicyManagementService(provider); + + assertThrows(PolicyNotFoundException.class, + () -> service.activatePolicy("/api/missing")); + assertThrows(PolicyNotFoundException.class, + () -> service.deactivatePolicy("/api/missing")); + } + } \ No newline at end of file diff --git a/src/test/java/com/yashdotdev/distributed_traffic_control/policy/api/PolicyControllerTest.java b/src/test/java/com/yashdotdev/distributed_traffic_control/policy/api/PolicyControllerTest.java index 64f0df2..f5d589e 100644 --- a/src/test/java/com/yashdotdev/distributed_traffic_control/policy/api/PolicyControllerTest.java +++ b/src/test/java/com/yashdotdev/distributed_traffic_control/policy/api/PolicyControllerTest.java @@ -3,6 +3,7 @@ import com.yashdotdev.distributed_traffic_control.policy.PolicyManagementService; import com.yashdotdev.distributed_traffic_control.policy.PolicyStatus; import com.yashdotdev.distributed_traffic_control.policy.TrafficPolicy; +import com.yashdotdev.distributed_traffic_control.policy.PolicyNotFoundException; import com.yashdotdev.distributed_traffic_control.policy.TrafficPolicyType; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -21,7 +22,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - @WebMvcTest(PolicyController.class) class PolicyControllerTest { @@ -200,4 +200,75 @@ void shouldReturnBadRequestForInvalidPolicyRequest() ) .andExpect(status().isBadRequest()); } + + + @Test + void shouldActivatePolicy() throws Exception { + TrafficPolicy policy = new TrafficPolicy( + "orders-policy", "Orders Policy", + TrafficPolicyType.TOKEN_BUCKET, PolicyStatus.ACTIVE, + 100, 10, null, + Instant.parse("2026-09-04T00:00:00Z") + ); + + when(policyManagementService.activatePolicy("/api/orders")) + .thenReturn(policy); + + mockMvc.perform(post("/api/v1/policies/activate") + .param("resource", "/api/orders")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.policyId").value("orders-policy")) + .andExpect(jsonPath("$.status").value("ACTIVE")) + .andExpect(jsonPath("$.resource").value("/api/orders")); + + verify(policyManagementService).activatePolicy("/api/orders"); + } + + @Test + void shouldDeactivatePolicy() throws Exception { + TrafficPolicy policy = new TrafficPolicy( + "orders-policy", "Orders Policy", + TrafficPolicyType.TOKEN_BUCKET, PolicyStatus.INACTIVE, + 100, 10, null, + Instant.parse("2026-09-04T00:00:00Z") + ); + + when(policyManagementService.deactivatePolicy("/api/orders")) + .thenReturn(policy); + + mockMvc.perform(post("/api/v1/policies/deactivate") + .param("resource", "/api/orders")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.policyId").value("orders-policy")) + .andExpect(jsonPath("$.status").value("INACTIVE")) + .andExpect(jsonPath("$.resource").value("/api/orders")); + + verify(policyManagementService).deactivatePolicy("/api/orders"); + } + + @Test + void shouldReturnNotFoundWhenActivatingMissingPolicy() throws Exception { + when(policyManagementService.activatePolicy("/api/missing")) + .thenThrow(new PolicyNotFoundException("/api/missing")); + + mockMvc.perform(post("/api/v1/policies/activate") + .param("resource", "/api/missing")) + .andExpect(status().isNotFound()); + } + + @Test + void shouldReturnNotFoundWhenDeactivatingMissingPolicy() throws Exception { + when(policyManagementService.deactivatePolicy("/api/missing")) + .thenThrow(new PolicyNotFoundException("/api/missing")); + + mockMvc.perform(post("/api/v1/policies/deactivate") + .param("resource", "/api/missing")) + .andExpect(status().isNotFound()); + } + @Test + void shouldReturnBadRequestForBlankResourceWhenActivatingPolicy() throws Exception { + mockMvc.perform(post("/api/v1/policies/activate") + .param("resource", " ")) + .andExpect(status().isBadRequest()); + } } \ No newline at end of file diff --git a/src/test/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngineTest.java b/src/test/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngineTest.java index 9dceb97..da5728f 100644 --- a/src/test/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngineTest.java +++ b/src/test/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngineTest.java @@ -22,6 +22,10 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; +import com.yashdotdev.distributed_traffic_control.policy.DefaultPolicyManagementService; +import java.time.Duration; +import com.yashdotdev.distributed_traffic_control.policy.DefaultPolicyManagementService; + import static org.junit.jupiter.api.Assertions.*; class TrafficDecisionEngineTest { @@ -38,6 +42,77 @@ void shouldAllowRequestWhenPolicyIsActiveAndCapacityIsAvailable() { assertTrue(decision.isAllowed()); } + + @Test + void shouldRejectRequestsAfterPolicyIsDeactivated() { + Instant now = Instant.now(); + + TrafficPolicy policy = new TrafficPolicy( + "orders-policy", + "Orders Policy", + TrafficPolicyType.TOKEN_BUCKET, + PolicyStatus.ACTIVE, + 100, + 10, + now + ); + + // Both components share the same provider. + InMemoryPolicyProvider provider = new InMemoryPolicyProvider(); + DefaultPolicyManagementService policyService = + new DefaultPolicyManagementService(provider); + policyService.registerPolicy("/api/orders", policy); + + CapacityAllocator noOpAllocator = new CapacityAllocator() { + @Override + public Optional allocate( + TrafficPolicy trafficPolicy, QuotaKey quotaKey) { + return Optional.empty(); + } + + @Override + public LeaseConsumptionResult tryConsume( + QuotaLease lease, Instant currentTime) { + return new LeaseConsumptionResult( + false, lease.getRemainingCapacity()); + } + }; + + TrafficDecisionEngine engine = new TrafficDecisionEngine( + provider, + new InMemoryQuotaCoordinator(), + noOpAllocator, + null, + null, + new InMemoryDistributedTokenBucket() + ); + + TrafficSubject subject = + new TrafficSubject("user-123", TrafficSubjectType.USER); + + TrafficRequest firstRequest = new TrafficRequest( + "request-1", subject, "/api/orders", now); + + // The active policy allows the request. + assertTrue(engine.evaluate(firstRequest).isAllowed()); + + // Deactivate the policy through the service. + TrafficPolicy deactivated = + policyService.deactivatePolicy("/api/orders"); + + assertEquals(PolicyStatus.INACTIVE, deactivated.getStatus()); + + TrafficRequest secondRequest = new TrafficRequest( + "request-2", subject, "/api/orders", now); + + // The engine must see the updated policy and reject the request. + TrafficDecision decision = engine.evaluate(secondRequest); + + assertFalse(decision.isAllowed()); + assertEquals("Traffic policy is inactive", decision.getReason()); + } + + @Test void shouldRejectRequestWhenQuotaIsExhausted() { From 1eff4ebac6c05b4d32b33b9d937b05dd6792ba68 Mon Sep 17 00:00:00 2001 From: Aryan_Gupta Date: Mon, 19 Oct 2026 22:05:15 +0530 Subject: [PATCH 2/2] chore: remove duplicate test imports --- .../traffic/TrafficDecisionEngineTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngineTest.java b/src/test/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngineTest.java index da5728f..3e9493c 100644 --- a/src/test/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngineTest.java +++ b/src/test/java/com/yashdotdev/distributed_traffic_control/traffic/TrafficDecisionEngineTest.java @@ -24,7 +24,6 @@ import com.yashdotdev.distributed_traffic_control.policy.DefaultPolicyManagementService; import java.time.Duration; -import com.yashdotdev.distributed_traffic_control.policy.DefaultPolicyManagementService; import static org.junit.jupiter.api.Assertions.*;