Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public interface PolicyManagementService {
Optional<TrafficPolicy> findPolicy(String resource);
List<PolicyRegistration> findAllPolicies();
TrafficPolicy updatePolicy(String resource, TrafficPolicy policy);
TrafficPolicy activatePolicy(String resource);
TrafficPolicy deactivatePolicy(String resource);
void removePolicy(String resource);
void setDefaultPolicy(TrafficPolicy policy);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -49,6 +50,42 @@ public ResponseEntity<Void> removePolicy(@RequestParam String resource) {
return ResponseEntity.noContent().build();
}


@PostMapping("/activate")
public ResponseEntity<PolicyResponse> 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<PolicyResponse> 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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import com.yashdotdev.distributed_traffic_control.policy.DefaultPolicyManagementService;
import java.time.Duration;

import static org.junit.jupiter.api.Assertions.*;

class TrafficDecisionEngineTest {
Expand All @@ -38,6 +41,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<QuotaLease> 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() {

Expand Down
Loading