Skip to content

Commit b6240bd

Browse files
fix: align conflict DTOs with API shape, allow null policyId
P1: PolicyConflict now has policy_a, policy_b, conflict_type, description, severity, overlapping_field matching the API response. PolicyConflictRef uses id, name, type fields. P2: detectPolicyConflicts() now accepts null to scan all policies. Added no-arg overload detectPolicyConflicts().
1 parent 13f4bb4 commit b6240bd

4 files changed

Lines changed: 125 additions & 48 deletions

File tree

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -927,33 +927,58 @@ public CompletableFuture<ImpactReportResponse> getPolicyImpactReportAsync(Impact
927927
}
928928

929929
/**
930-
* Detects conflicts between a specific policy and other active policies.
930+
* Scans all active policies for conflicts.
931+
*
932+
* <p>Example usage:
933+
* <pre>{@code
934+
* PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts();
935+
* System.out.println("Conflicts found: " + conflicts.getConflictCount());
936+
* for (PolicyConflict conflict : conflicts.getConflicts()) {
937+
* System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
938+
* }
939+
* }</pre>
940+
*
941+
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
942+
*
943+
* @return the conflict detection result
944+
* @throws AxonFlowException if the request fails
945+
*/
946+
public PolicyConflictResponse detectPolicyConflicts() {
947+
return detectPolicyConflicts(null);
948+
}
949+
950+
/**
951+
* Detects conflicts between a specific policy and other active policies,
952+
* or scans all policies if policyId is null.
931953
*
932954
* <p>Example usage:
933955
* <pre>{@code
934956
* PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts("policy_block_pii");
935957
* System.out.println("Conflicts found: " + conflicts.getConflictCount());
936958
* for (PolicyConflict conflict : conflicts.getConflicts()) {
937-
* System.out.println(conflict.getType() + ": " + conflict.getDescription());
959+
* System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
938960
* }
939961
* }</pre>
940962
*
941963
* <p><b>Evaluation+ Feature:</b> Requires AxonFlow Evaluation tier or higher.
942964
*
943-
* @param policyId the policy ID to check for conflicts
965+
* @param policyId the policy ID to check for conflicts, or null to scan all policies
944966
* @return the conflict detection result
945-
* @throws NullPointerException if policyId is null
946-
* @throws IllegalArgumentException if policyId is empty
967+
* @throws IllegalArgumentException if policyId is non-null and empty
947968
* @throws AxonFlowException if the request fails
948969
*/
949970
public PolicyConflictResponse detectPolicyConflicts(String policyId) {
950-
Objects.requireNonNull(policyId, "policyId cannot be null");
951-
if (policyId.isEmpty()) {
971+
if (policyId != null && policyId.isEmpty()) {
952972
throw new IllegalArgumentException("policyId cannot be empty");
953973
}
954974

955975
return retryExecutor.execute(() -> {
956-
java.util.Map<String, String> body = java.util.Map.of("policy_id", policyId);
976+
Object body;
977+
if (policyId != null) {
978+
body = java.util.Map.of("policy_id", policyId);
979+
} else {
980+
body = java.util.Map.of();
981+
}
957982
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/conflicts", body);
958983
try (Response response = httpClient.newCall(httpRequest).execute()) {
959984
JsonNode node = parseResponseNode(response);
@@ -965,10 +990,19 @@ public PolicyConflictResponse detectPolicyConflicts(String policyId) {
965990
}, "detectPolicyConflicts");
966991
}
967992

993+
/**
994+
* Asynchronously scans all active policies for conflicts.
995+
*
996+
* @return a future containing the conflict detection result
997+
*/
998+
public CompletableFuture<PolicyConflictResponse> detectPolicyConflictsAsync() {
999+
return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(), asyncExecutor);
1000+
}
1001+
9681002
/**
9691003
* Asynchronously detects conflicts between a specific policy and other active policies.
9701004
*
971-
* @param policyId the policy ID to check for conflicts
1005+
* @param policyId the policy ID to check for conflicts, or null to scan all policies
9721006
* @return a future containing the conflict detection result
9731007
*/
9741008
public CompletableFuture<PolicyConflictResponse> detectPolicyConflictsAsync(String policyId) {

src/main/java/com/getaxonflow/sdk/simulation/PolicyConflict.java

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,39 +18,49 @@
1818
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
1919
import com.fasterxml.jackson.annotation.JsonProperty;
2020

21-
import java.util.List;
22-
2321
/**
2422
* A detected conflict between policies.
2523
*/
2624
@JsonIgnoreProperties(ignoreUnknown = true)
2725
public final class PolicyConflict {
2826

29-
@JsonProperty("type")
30-
private final String type;
27+
@JsonProperty("policy_a")
28+
private final PolicyConflictRef policyA;
3129

32-
@JsonProperty("severity")
33-
private final String severity;
30+
@JsonProperty("policy_b")
31+
private final PolicyConflictRef policyB;
32+
33+
@JsonProperty("conflict_type")
34+
private final String conflictType;
3435

3536
@JsonProperty("description")
3637
private final String description;
3738

38-
@JsonProperty("policies")
39-
private final List<PolicyConflictRef> policies;
39+
@JsonProperty("severity")
40+
private final String severity;
41+
42+
@JsonProperty("overlapping_field")
43+
private final String overlappingField;
4044

4145
public PolicyConflict(
42-
@JsonProperty("type") String type,
43-
@JsonProperty("severity") String severity,
46+
@JsonProperty("policy_a") PolicyConflictRef policyA,
47+
@JsonProperty("policy_b") PolicyConflictRef policyB,
48+
@JsonProperty("conflict_type") String conflictType,
4449
@JsonProperty("description") String description,
45-
@JsonProperty("policies") List<PolicyConflictRef> policies) {
46-
this.type = type;
47-
this.severity = severity;
50+
@JsonProperty("severity") String severity,
51+
@JsonProperty("overlapping_field") String overlappingField) {
52+
this.policyA = policyA;
53+
this.policyB = policyB;
54+
this.conflictType = conflictType;
4855
this.description = description;
49-
this.policies = policies != null ? List.copyOf(policies) : List.of();
56+
this.severity = severity;
57+
this.overlappingField = overlappingField;
5058
}
5159

52-
public String getType() { return type; }
53-
public String getSeverity() { return severity; }
60+
public PolicyConflictRef getPolicyA() { return policyA; }
61+
public PolicyConflictRef getPolicyB() { return policyB; }
62+
public String getConflictType() { return conflictType; }
5463
public String getDescription() { return description; }
55-
public List<PolicyConflictRef> getPolicies() { return policies; }
64+
public String getSeverity() { return severity; }
65+
public String getOverlappingField() { return overlappingField; }
5666
}

src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictRef.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,25 @@
2424
@JsonIgnoreProperties(ignoreUnknown = true)
2525
public final class PolicyConflictRef {
2626

27-
@JsonProperty("policy_id")
28-
private final String policyId;
27+
@JsonProperty("id")
28+
private final String id;
2929

3030
@JsonProperty("name")
3131
private final String name;
3232

33-
@JsonProperty("action")
34-
private final String action;
33+
@JsonProperty("type")
34+
private final String type;
3535

3636
public PolicyConflictRef(
37-
@JsonProperty("policy_id") String policyId,
37+
@JsonProperty("id") String id,
3838
@JsonProperty("name") String name,
39-
@JsonProperty("action") String action) {
40-
this.policyId = policyId;
39+
@JsonProperty("type") String type) {
40+
this.id = id;
4141
this.name = name;
42-
this.action = action;
42+
this.type = type;
4343
}
4444

45-
public String getPolicyId() { return policyId; }
45+
public String getId() { return id; }
4646
public String getName() { return name; }
47-
public String getAction() { return action; }
47+
public String getType() { return type; }
4848
}

src/test/java/com/getaxonflow/sdk/PolicySimulationTest.java

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ void shouldDetectPolicyConflicts() {
364364
.willReturn(aResponse()
365365
.withStatus(200)
366366
.withHeader("Content-Type", "application/json")
367-
.withBody("{\"success\":true,\"data\":{\"conflicts\":[{\"type\":\"action_conflict\",\"severity\":\"high\",\"description\":\"Policy 'block-pii' blocks requests that 'allow-internal' would allow\",\"policies\":[{\"policy_id\":\"policy_block_pii\",\"name\":\"block-pii\",\"action\":\"block\"},{\"policy_id\":\"policy_allow_internal\",\"name\":\"allow-internal\",\"action\":\"allow\"}]}],\"total_policies\":8,\"conflict_count\":1,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}")));
367+
.withBody("{\"success\":true,\"data\":{\"conflicts\":[{\"policy_a\":{\"id\":\"policy_block_pii\",\"name\":\"block-pii\",\"type\":\"deny\"},\"policy_b\":{\"id\":\"policy_allow_internal\",\"name\":\"allow-internal\",\"type\":\"allow\"},\"conflict_type\":\"action_conflict\",\"description\":\"Policy 'block-pii' blocks requests that 'allow-internal' would allow\",\"severity\":\"high\",\"overlapping_field\":\"input.content\"}],\"total_policies\":8,\"conflict_count\":1,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}")));
368368

369369
PolicyConflictResponse result = axonflow.detectPolicyConflicts("policy_block_pii");
370370

@@ -376,14 +376,18 @@ void shouldDetectPolicyConflicts() {
376376
assertThat(result.getConflicts()).hasSize(1);
377377

378378
PolicyConflict conflict = result.getConflicts().get(0);
379-
assertThat(conflict.getType()).isEqualTo("action_conflict");
379+
assertThat(conflict.getConflictType()).isEqualTo("action_conflict");
380380
assertThat(conflict.getSeverity()).isEqualTo("high");
381381
assertThat(conflict.getDescription()).contains("block-pii");
382-
assertThat(conflict.getPolicies()).hasSize(2);
383-
assertThat(conflict.getPolicies().get(0).getPolicyId()).isEqualTo("policy_block_pii");
384-
assertThat(conflict.getPolicies().get(0).getName()).isEqualTo("block-pii");
385-
assertThat(conflict.getPolicies().get(0).getAction()).isEqualTo("block");
386-
assertThat(conflict.getPolicies().get(1).getPolicyId()).isEqualTo("policy_allow_internal");
382+
assertThat(conflict.getOverlappingField()).isEqualTo("input.content");
383+
assertThat(conflict.getPolicyA()).isNotNull();
384+
assertThat(conflict.getPolicyA().getId()).isEqualTo("policy_block_pii");
385+
assertThat(conflict.getPolicyA().getName()).isEqualTo("block-pii");
386+
assertThat(conflict.getPolicyA().getType()).isEqualTo("deny");
387+
assertThat(conflict.getPolicyB()).isNotNull();
388+
assertThat(conflict.getPolicyB().getId()).isEqualTo("policy_allow_internal");
389+
assertThat(conflict.getPolicyB().getName()).isEqualTo("allow-internal");
390+
assertThat(conflict.getPolicyB().getType()).isEqualTo("allow");
387391

388392
verify(postRequestedFor(urlEqualTo("/api/v1/policies/conflicts"))
389393
.withRequestBody(containing("\"policy_id\":\"policy_block_pii\"")));
@@ -408,11 +412,40 @@ void shouldDetectNoConflicts() {
408412
}
409413

410414
@Test
411-
@DisplayName("should reject null policyId for detectPolicyConflicts")
412-
void shouldRejectNullPolicyIdForConflicts() {
413-
assertThatThrownBy(() -> axonflow.detectPolicyConflicts(null))
414-
.isInstanceOf(NullPointerException.class)
415-
.hasMessageContaining("policyId cannot be null");
415+
@DisplayName("should scan all policies when policyId is null")
416+
void shouldScanAllPoliciesWhenNull() {
417+
stubFor(post(urlEqualTo("/api/v1/policies/conflicts"))
418+
.willReturn(aResponse()
419+
.withStatus(200)
420+
.withHeader("Content-Type", "application/json")
421+
.withBody("{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":5,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}")));
422+
423+
PolicyConflictResponse result = axonflow.detectPolicyConflicts(null);
424+
425+
assertThat(result).isNotNull();
426+
assertThat(result.getConflictCount()).isEqualTo(0);
427+
assertThat(result.getConflicts()).isEmpty();
428+
429+
verify(postRequestedFor(urlEqualTo("/api/v1/policies/conflicts"))
430+
.withRequestBody(equalToJson("{}")));
431+
}
432+
433+
@Test
434+
@DisplayName("should scan all policies with no-arg overload")
435+
void shouldScanAllPoliciesNoArg() {
436+
stubFor(post(urlEqualTo("/api/v1/policies/conflicts"))
437+
.willReturn(aResponse()
438+
.withStatus(200)
439+
.withHeader("Content-Type", "application/json")
440+
.withBody("{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":3,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}")));
441+
442+
PolicyConflictResponse result = axonflow.detectPolicyConflicts();
443+
444+
assertThat(result).isNotNull();
445+
assertThat(result.getConflictCount()).isEqualTo(0);
446+
447+
verify(postRequestedFor(urlEqualTo("/api/v1/policies/conflicts"))
448+
.withRequestBody(equalToJson("{}")));
416449
}
417450

418451
@Test

0 commit comments

Comments
 (0)