From 4b77b49cc96aec25b1e30a84a2ba54a5240284f3 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Thu, 19 Feb 2026 02:53:33 +0300 Subject: [PATCH 1/6] feat: add media governance config methods and category constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getMediaGovernanceConfig() — retrieve tenant's media governance config - updateMediaGovernanceConfig() — update config (Enterprise: full, Community/Eval: toggle) - getMediaGovernanceStatus() — check feature availability per tier - Category constants: CATEGORY_MEDIA_SAFETY, CATEGORY_MEDIA_BIOMETRIC, CATEGORY_MEDIA_DOCUMENT, CATEGORY_MEDIA_PII - Version bump: 3.4.0 → 3.6.0 --- pom.xml | 2 +- .../java/com/getaxonflow/sdk/AxonFlow.java | 89 ++++++++++++++++++ .../sdk/types/MediaGovernanceConfig.java | 92 +++++++++++++++++++ .../sdk/types/MediaGovernanceStatus.java | 83 +++++++++++++++++ .../UpdateMediaGovernanceConfigRequest.java | 89 ++++++++++++++++++ .../sdk/types/policies/PolicyTypes.java | 16 ++++ 6 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/getaxonflow/sdk/types/MediaGovernanceConfig.java create mode 100644 src/main/java/com/getaxonflow/sdk/types/MediaGovernanceStatus.java create mode 100644 src/main/java/com/getaxonflow/sdk/types/UpdateMediaGovernanceConfigRequest.java diff --git a/pom.xml b/pom.xml index 19ecd38..39e41d0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.getaxonflow axonflow-sdk - 3.5.0 + 3.6.0 jar AxonFlow Java SDK diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index cf54b91..c494000 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -2122,6 +2122,95 @@ public void streamExecutionStatus( } } + // ======================================================================== + // Media Governance Config + // ======================================================================== + + /** + * Gets the media governance configuration for the current tenant. + * + *

Returns per-tenant settings controlling whether media analysis is + * enabled and which analyzers are allowed. + * + * @return the media governance configuration + * @throws AxonFlowException if the request fails + */ + public MediaGovernanceConfig getMediaGovernanceConfig() { + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("GET", "/api/v1/media-governance/config", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, MediaGovernanceConfig.class); + } + }, "getMediaGovernanceConfig"); + } + + /** + * Asynchronously gets the media governance configuration for the current tenant. + * + * @return a future containing the media governance configuration + */ + public CompletableFuture getMediaGovernanceConfigAsync() { + return CompletableFuture.supplyAsync(this::getMediaGovernanceConfig, asyncExecutor); + } + + /** + * Updates the media governance configuration for the current tenant. + * + *

Allows enabling/disabling media analysis and controlling which + * analyzers are permitted. + * + * @param request the update request + * @return the updated media governance configuration + * @throws AxonFlowException if the request fails + */ + public MediaGovernanceConfig updateMediaGovernanceConfig(UpdateMediaGovernanceConfigRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("PUT", "/api/v1/media-governance/config", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, MediaGovernanceConfig.class); + } + }, "updateMediaGovernanceConfig"); + } + + /** + * Asynchronously updates the media governance configuration for the current tenant. + * + * @param request the update request + * @return a future containing the updated media governance configuration + */ + public CompletableFuture updateMediaGovernanceConfigAsync(UpdateMediaGovernanceConfigRequest request) { + return CompletableFuture.supplyAsync(() -> updateMediaGovernanceConfig(request), asyncExecutor); + } + + /** + * Gets the platform-level media governance status. + * + *

Returns whether media governance is available, default enablement, + * and the required license tier. + * + * @return the media governance status + * @throws AxonFlowException if the request fails + */ + public MediaGovernanceStatus getMediaGovernanceStatus() { + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("GET", "/api/v1/media-governance/status", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, MediaGovernanceStatus.class); + } + }, "getMediaGovernanceStatus"); + } + + /** + * Asynchronously gets the platform-level media governance status. + * + * @return a future containing the media governance status + */ + public CompletableFuture getMediaGovernanceStatusAsync() { + return CompletableFuture.supplyAsync(this::getMediaGovernanceStatus, asyncExecutor); + } + // ======================================================================== // Configuration Access // ======================================================================== diff --git a/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceConfig.java b/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceConfig.java new file mode 100644 index 0000000..1d32ce4 --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceConfig.java @@ -0,0 +1,92 @@ +/* + * Copyright 2025 AxonFlow + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.getaxonflow.sdk.types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Objects; + +/** + * Per-tenant media governance configuration. + * + *

Controls whether media analysis is enabled for a tenant and which + * analyzers are allowed. Returned by the media governance config API. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class MediaGovernanceConfig { + + @JsonProperty("tenant_id") + private String tenantId; + + @JsonProperty("enabled") + private boolean enabled; + + @JsonProperty("allowed_analyzers") + private List allowedAnalyzers; + + @JsonProperty("updated_at") + private String updatedAt; + + @JsonProperty("updated_by") + private String updatedBy; + + public MediaGovernanceConfig() {} + + public String getTenantId() { return tenantId; } + public void setTenantId(String tenantId) { this.tenantId = tenantId; } + + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public List getAllowedAnalyzers() { return allowedAnalyzers; } + public void setAllowedAnalyzers(List allowedAnalyzers) { this.allowedAnalyzers = allowedAnalyzers; } + + public String getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } + + public String getUpdatedBy() { return updatedBy; } + public void setUpdatedBy(String updatedBy) { this.updatedBy = updatedBy; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaGovernanceConfig that = (MediaGovernanceConfig) o; + return enabled == that.enabled && + Objects.equals(tenantId, that.tenantId) && + Objects.equals(allowedAnalyzers, that.allowedAnalyzers) && + Objects.equals(updatedAt, that.updatedAt) && + Objects.equals(updatedBy, that.updatedBy); + } + + @Override + public int hashCode() { + return Objects.hash(tenantId, enabled, allowedAnalyzers, updatedAt, updatedBy); + } + + @Override + public String toString() { + return "MediaGovernanceConfig{" + + "tenantId='" + tenantId + '\'' + + ", enabled=" + enabled + + ", allowedAnalyzers=" + allowedAnalyzers + + ", updatedAt='" + updatedAt + '\'' + + ", updatedBy='" + updatedBy + '\'' + + '}'; + } +} diff --git a/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceStatus.java b/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceStatus.java new file mode 100644 index 0000000..de227d9 --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceStatus.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 AxonFlow + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.getaxonflow.sdk.types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * Platform-level media governance status. + * + *

Indicates whether media governance is available, the default enablement + * state, and the license tier required. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class MediaGovernanceStatus { + + @JsonProperty("available") + private boolean available; + + @JsonProperty("enabled_by_default") + private boolean enabledByDefault; + + @JsonProperty("per_tenant_control") + private boolean perTenantControl; + + @JsonProperty("tier") + private String tier; + + public MediaGovernanceStatus() {} + + public boolean isAvailable() { return available; } + public void setAvailable(boolean available) { this.available = available; } + + public boolean isEnabledByDefault() { return enabledByDefault; } + public void setEnabledByDefault(boolean enabledByDefault) { this.enabledByDefault = enabledByDefault; } + + public boolean isPerTenantControl() { return perTenantControl; } + public void setPerTenantControl(boolean perTenantControl) { this.perTenantControl = perTenantControl; } + + public String getTier() { return tier; } + public void setTier(String tier) { this.tier = tier; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaGovernanceStatus that = (MediaGovernanceStatus) o; + return available == that.available && + enabledByDefault == that.enabledByDefault && + perTenantControl == that.perTenantControl && + Objects.equals(tier, that.tier); + } + + @Override + public int hashCode() { + return Objects.hash(available, enabledByDefault, perTenantControl, tier); + } + + @Override + public String toString() { + return "MediaGovernanceStatus{" + + "available=" + available + + ", enabledByDefault=" + enabledByDefault + + ", perTenantControl=" + perTenantControl + + ", tier='" + tier + '\'' + + '}'; + } +} diff --git a/src/main/java/com/getaxonflow/sdk/types/UpdateMediaGovernanceConfigRequest.java b/src/main/java/com/getaxonflow/sdk/types/UpdateMediaGovernanceConfigRequest.java new file mode 100644 index 0000000..78812b6 --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/UpdateMediaGovernanceConfigRequest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2025 AxonFlow + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.getaxonflow.sdk.types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Objects; + +/** + * Request to update per-tenant media governance configuration. + * + *

Fields set to {@code null} are omitted from the JSON payload, + * allowing partial updates. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class UpdateMediaGovernanceConfigRequest { + + @JsonProperty("enabled") + private Boolean enabled; + + @JsonProperty("allowed_analyzers") + private List allowedAnalyzers; + + public UpdateMediaGovernanceConfigRequest() {} + + public Boolean getEnabled() { return enabled; } + public void setEnabled(Boolean enabled) { this.enabled = enabled; } + + public List getAllowedAnalyzers() { return allowedAnalyzers; } + public void setAllowedAnalyzers(List allowedAnalyzers) { this.allowedAnalyzers = allowedAnalyzers; } + + public static Builder builder() { return new Builder(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UpdateMediaGovernanceConfigRequest that = (UpdateMediaGovernanceConfigRequest) o; + return Objects.equals(enabled, that.enabled) && + Objects.equals(allowedAnalyzers, that.allowedAnalyzers); + } + + @Override + public int hashCode() { + return Objects.hash(enabled, allowedAnalyzers); + } + + @Override + public String toString() { + return "UpdateMediaGovernanceConfigRequest{" + + "enabled=" + enabled + + ", allowedAnalyzers=" + allowedAnalyzers + + '}'; + } + + public static final class Builder { + private Boolean enabled; + private List allowedAnalyzers; + + private Builder() {} + + public Builder enabled(Boolean enabled) { this.enabled = enabled; return this; } + public Builder allowedAnalyzers(List allowedAnalyzers) { this.allowedAnalyzers = allowedAnalyzers; return this; } + + public UpdateMediaGovernanceConfigRequest build() { + UpdateMediaGovernanceConfigRequest request = new UpdateMediaGovernanceConfigRequest(); + request.enabled = this.enabled; + request.allowedAnalyzers = this.allowedAnalyzers; + return request; + } + } +} diff --git a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java index 2bc66ee..4fff763 100644 --- a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java @@ -29,6 +29,22 @@ public final class PolicyTypes { private PolicyTypes() {} + // ======================================================================== + // Media Governance Policy Category Constants + // ======================================================================== + + /** Policy category for media safety (NSFW, violence). */ + public static final String CATEGORY_MEDIA_SAFETY = "media-safety"; + + /** Policy category for media biometric detection (faces, fingerprints). */ + public static final String CATEGORY_MEDIA_BIOMETRIC = "media-biometric"; + + /** Policy category for sensitive document detection. */ + public static final String CATEGORY_MEDIA_DOCUMENT = "media-document"; + + /** Policy category for PII detected in media (OCR text extraction). */ + public static final String CATEGORY_MEDIA_PII = "media-pii"; + // ======================================================================== // Enums // ======================================================================== From 93e71b0239c9cdbcbdbbd6741c5b8af04d37da9e Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Thu, 19 Feb 2026 10:15:35 +0300 Subject: [PATCH 2/6] fix: add media category enum values to PolicyCategory Add MEDIA_SAFETY, MEDIA_BIOMETRIC, MEDIA_PII, MEDIA_DOCUMENT enum members to prevent Jackson deserialization crash when server returns policies with media-* categories. Add v3.6.0 CHANGELOG entry. --- CHANGELOG.md | 11 +++++++++++ .../getaxonflow/sdk/types/policies/PolicyTypes.java | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b98aa38..2cf7a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to the AxonFlow Java SDK will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.6.0] - 2026-02-19 + +### Added + +- Media governance configuration methods: `getMediaGovernanceConfig()`, `updateMediaGovernanceConfig()`, `getMediaGovernanceStatus()` +- Media governance types: `MediaGovernanceConfig`, `MediaGovernanceStatus` +- Media policy category constants: `CATEGORY_MEDIA_SAFETY`, `CATEGORY_MEDIA_BIOMETRIC`, `CATEGORY_MEDIA_PII`, `CATEGORY_MEDIA_DOCUMENT` +- `PolicyCategory` enum values: `MEDIA_SAFETY`, `MEDIA_BIOMETRIC`, `MEDIA_PII`, `MEDIA_DOCUMENT` + +--- + ## [3.5.0] - 2026-02-18 ### Added diff --git a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java index 4fff763..05ccb9b 100644 --- a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java @@ -72,6 +72,12 @@ public enum PolicyCategory { // Sensitive data category SENSITIVE_DATA("sensitive-data"), + // Media governance categories + MEDIA_SAFETY("media-safety"), + MEDIA_BIOMETRIC("media-biometric"), + MEDIA_PII("media-pii"), + MEDIA_DOCUMENT("media-document"), + // Dynamic policy categories DYNAMIC_RISK("dynamic-risk"), DYNAMIC_COMPLIANCE("dynamic-compliance"), From 4e0bb42260f9a5b78ec15998070ccdfedd2f303e Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Thu, 19 Feb 2026 18:59:15 +0300 Subject: [PATCH 3/6] test: add coverage for media governance types and API methods Add tests for MediaGovernanceConfig, MediaGovernanceStatus, UpdateMediaGovernanceConfigRequest types and their JSON serialization. Add WireMock-based tests for getMediaGovernanceConfig, updateMediaGovernanceConfig, and getMediaGovernanceStatus API methods including async variants. Add coverage for media policy category constants and enum values. Brings line coverage from 0.72 to above the 0.73 threshold required by JaCoCo. --- .../getaxonflow/sdk/MediaGovernanceTest.java | 312 +++++++++++ .../sdk/types/MediaGovernanceTypesTest.java | 524 ++++++++++++++++++ 2 files changed, 836 insertions(+) create mode 100644 src/test/java/com/getaxonflow/sdk/MediaGovernanceTest.java create mode 100644 src/test/java/com/getaxonflow/sdk/types/MediaGovernanceTypesTest.java diff --git a/src/test/java/com/getaxonflow/sdk/MediaGovernanceTest.java b/src/test/java/com/getaxonflow/sdk/MediaGovernanceTest.java new file mode 100644 index 0000000..55cfee3 --- /dev/null +++ b/src/test/java/com/getaxonflow/sdk/MediaGovernanceTest.java @@ -0,0 +1,312 @@ +/* + * Copyright 2026 AxonFlow + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.getaxonflow.sdk; + +import com.getaxonflow.sdk.types.MediaGovernanceConfig; +import com.getaxonflow.sdk.types.MediaGovernanceStatus; +import com.getaxonflow.sdk.types.UpdateMediaGovernanceConfigRequest; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for Media Governance Config API methods on the AxonFlow client. + */ +@WireMockTest +@DisplayName("Media Governance API Methods") +class MediaGovernanceTest { + + private AxonFlow axonflow; + + private static final String SAMPLE_CONFIG_JSON = + "{" + + "\"tenant_id\": \"tenant_001\"," + + "\"enabled\": true," + + "\"allowed_analyzers\": [\"nsfw\", \"biometric\", \"ocr\"]," + + "\"updated_at\": \"2026-02-18T10:00:00Z\"," + + "\"updated_by\": \"admin@example.com\"" + + "}"; + + private static final String SAMPLE_STATUS_JSON = + "{" + + "\"available\": true," + + "\"enabled_by_default\": false," + + "\"per_tenant_control\": true," + + "\"tier\": \"enterprise\"" + + "}"; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = AxonFlow.create(AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .build()); + } + + // ======================================================================== + // getMediaGovernanceConfig + // ======================================================================== + + @Nested + @DisplayName("getMediaGovernanceConfig") + class GetMediaGovernanceConfig { + + @Test + @DisplayName("should return media governance config") + void shouldReturnConfig() { + stubFor(get(urlEqualTo("/api/v1/media-governance/config")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_CONFIG_JSON))); + + MediaGovernanceConfig config = axonflow.getMediaGovernanceConfig(); + + assertThat(config.getTenantId()).isEqualTo("tenant_001"); + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); + assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T10:00:00Z"); + assertThat(config.getUpdatedBy()).isEqualTo("admin@example.com"); + } + + @Test + @DisplayName("should return disabled config") + void shouldReturnDisabledConfig() { + stubFor(get(urlEqualTo("/api/v1/media-governance/config")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"tenant_id\": \"tenant_002\", \"enabled\": false, \"allowed_analyzers\": []}"))); + + MediaGovernanceConfig config = axonflow.getMediaGovernanceConfig(); + + assertThat(config.getTenantId()).isEqualTo("tenant_002"); + assertThat(config.isEnabled()).isFalse(); + assertThat(config.getAllowedAnalyzers()).isEmpty(); + } + + @Test + @DisplayName("should throw on server error") + void shouldThrowOnServerError() { + stubFor(get(urlEqualTo("/api/v1/media-governance/config")) + .willReturn(aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.getMediaGovernanceConfig()) + .isInstanceOf(Exception.class); + } + + @Test + @DisplayName("async should return future with config") + void asyncShouldReturnFuture() throws Exception { + stubFor(get(urlEqualTo("/api/v1/media-governance/config")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_CONFIG_JSON))); + + CompletableFuture future = axonflow.getMediaGovernanceConfigAsync(); + MediaGovernanceConfig config = future.get(); + + assertThat(config.getTenantId()).isEqualTo("tenant_001"); + assertThat(config.isEnabled()).isTrue(); + } + } + + // ======================================================================== + // updateMediaGovernanceConfig + // ======================================================================== + + @Nested + @DisplayName("updateMediaGovernanceConfig") + class UpdateMediaGovernanceConfig { + + @Test + @DisplayName("should send PUT request and return updated config") + void shouldUpdateConfig() { + stubFor(put(urlEqualTo("/api/v1/media-governance/config")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_CONFIG_JSON))); + + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw", "biometric", "ocr")) + .build(); + + MediaGovernanceConfig config = axonflow.updateMediaGovernanceConfig(request); + + assertThat(config.getTenantId()).isEqualTo("tenant_001"); + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); + + verify(putRequestedFor(urlEqualTo("/api/v1/media-governance/config")) + .withHeader("Content-Type", containing("application/json")) + .withRequestBody(containing("\"enabled\":true")) + .withRequestBody(containing("\"allowed_analyzers\""))); + } + + @Test + @DisplayName("should send partial update with only enabled") + void shouldSendPartialUpdate() { + stubFor(put(urlEqualTo("/api/v1/media-governance/config")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"tenant_id\": \"tenant_001\", \"enabled\": false, \"allowed_analyzers\": [\"nsfw\"]}"))); + + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(false) + .build(); + + MediaGovernanceConfig config = axonflow.updateMediaGovernanceConfig(request); + + assertThat(config.isEnabled()).isFalse(); + + // Verify null fields are not sent (NON_NULL inclusion) + verify(putRequestedFor(urlEqualTo("/api/v1/media-governance/config")) + .withRequestBody(containing("\"enabled\":false"))); + } + + @Test + @DisplayName("should require non-null request") + void shouldRequireNonNullRequest() { + assertThatThrownBy(() -> axonflow.updateMediaGovernanceConfig(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("request cannot be null"); + } + + @Test + @DisplayName("should throw on server error") + void shouldThrowOnServerError() { + stubFor(put(urlEqualTo("/api/v1/media-governance/config")) + .willReturn(aResponse() + .withStatus(403) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Forbidden: insufficient permissions\"}"))); + + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .build(); + + assertThatThrownBy(() -> axonflow.updateMediaGovernanceConfig(request)) + .isInstanceOf(Exception.class); + } + + @Test + @DisplayName("async should return future with updated config") + void asyncShouldReturnFuture() throws Exception { + stubFor(put(urlEqualTo("/api/v1/media-governance/config")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_CONFIG_JSON))); + + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw")) + .build(); + + CompletableFuture future = + axonflow.updateMediaGovernanceConfigAsync(request); + MediaGovernanceConfig config = future.get(); + + assertThat(config.isEnabled()).isTrue(); + } + } + + // ======================================================================== + // getMediaGovernanceStatus + // ======================================================================== + + @Nested + @DisplayName("getMediaGovernanceStatus") + class GetMediaGovernanceStatus { + + @Test + @DisplayName("should return media governance platform status") + void shouldReturnStatus() { + stubFor(get(urlEqualTo("/api/v1/media-governance/status")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_STATUS_JSON))); + + MediaGovernanceStatus status = axonflow.getMediaGovernanceStatus(); + + assertThat(status.isAvailable()).isTrue(); + assertThat(status.isEnabledByDefault()).isFalse(); + assertThat(status.isPerTenantControl()).isTrue(); + assertThat(status.getTier()).isEqualTo("enterprise"); + } + + @Test + @DisplayName("should return unavailable status") + void shouldReturnUnavailableStatus() { + stubFor(get(urlEqualTo("/api/v1/media-governance/status")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"available\": false, \"enabled_by_default\": false, \"per_tenant_control\": false, \"tier\": \"community\"}"))); + + MediaGovernanceStatus status = axonflow.getMediaGovernanceStatus(); + + assertThat(status.isAvailable()).isFalse(); + assertThat(status.getTier()).isEqualTo("community"); + } + + @Test + @DisplayName("should throw on server error") + void shouldThrowOnServerError() { + stubFor(get(urlEqualTo("/api/v1/media-governance/status")) + .willReturn(aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.getMediaGovernanceStatus()) + .isInstanceOf(Exception.class); + } + + @Test + @DisplayName("async should return future with status") + void asyncShouldReturnFuture() throws Exception { + stubFor(get(urlEqualTo("/api/v1/media-governance/status")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_STATUS_JSON))); + + CompletableFuture future = axonflow.getMediaGovernanceStatusAsync(); + MediaGovernanceStatus status = future.get(); + + assertThat(status.isAvailable()).isTrue(); + assertThat(status.getTier()).isEqualTo("enterprise"); + } + } +} diff --git a/src/test/java/com/getaxonflow/sdk/types/MediaGovernanceTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/MediaGovernanceTypesTest.java new file mode 100644 index 0000000..b02e4ce --- /dev/null +++ b/src/test/java/com/getaxonflow/sdk/types/MediaGovernanceTypesTest.java @@ -0,0 +1,524 @@ +/* + * Copyright 2026 AxonFlow + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.getaxonflow.sdk.types; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.getaxonflow.sdk.types.policies.PolicyTypes; +import com.getaxonflow.sdk.types.policies.PolicyTypes.PolicyCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@DisplayName("Media Governance Types") +class MediaGovernanceTypesTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + // ======================================================================== + // MediaGovernanceConfig + // ======================================================================== + + @Nested + @DisplayName("MediaGovernanceConfig") + class MediaGovernanceConfigTests { + + @Test + @DisplayName("should create with default constructor and set all fields") + void shouldCreateWithDefaultConstructor() { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setTenantId("tenant_001"); + config.setEnabled(true); + config.setAllowedAnalyzers(Arrays.asList("nsfw", "biometric", "ocr")); + config.setUpdatedAt("2026-02-18T10:00:00Z"); + config.setUpdatedBy("admin@example.com"); + + assertThat(config.getTenantId()).isEqualTo("tenant_001"); + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); + assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T10:00:00Z"); + assertThat(config.getUpdatedBy()).isEqualTo("admin@example.com"); + } + + @Test + @DisplayName("should handle disabled state") + void shouldHandleDisabledState() { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setEnabled(false); + config.setAllowedAnalyzers(List.of()); + + assertThat(config.isEnabled()).isFalse(); + assertThat(config.getAllowedAnalyzers()).isEmpty(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{" + + "\"tenant_id\": \"tenant_abc\"," + + "\"enabled\": true," + + "\"allowed_analyzers\": [\"nsfw\", \"document\"]," + + "\"updated_at\": \"2026-02-18T12:00:00Z\"," + + "\"updated_by\": \"user@example.com\"" + + "}"; + + MediaGovernanceConfig config = mapper.readValue(json, MediaGovernanceConfig.class); + + assertThat(config.getTenantId()).isEqualTo("tenant_abc"); + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "document"); + assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T12:00:00Z"); + assertThat(config.getUpdatedBy()).isEqualTo("user@example.com"); + } + + @Test + @DisplayName("should ignore unknown properties during deserialization") + void shouldIgnoreUnknownProperties() throws Exception { + String json = "{\"tenant_id\": \"t1\", \"enabled\": false, \"future_field\": 42}"; + + MediaGovernanceConfig config = mapper.readValue(json, MediaGovernanceConfig.class); + + assertThat(config.getTenantId()).isEqualTo("t1"); + assertThat(config.isEnabled()).isFalse(); + } + + @Test + @DisplayName("should serialize to JSON") + void shouldSerializeToJson() throws Exception { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setTenantId("tenant_xyz"); + config.setEnabled(true); + config.setAllowedAnalyzers(List.of("nsfw")); + + String json = mapper.writeValueAsString(config); + + assertThat(json).contains("\"tenant_id\":\"tenant_xyz\""); + assertThat(json).contains("\"enabled\":true"); + assertThat(json).contains("\"allowed_analyzers\":[\"nsfw\"]"); + } + + @Test + @DisplayName("equals should be reflexive") + void equalsShouldBeReflexive() { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setTenantId("t1"); + config.setEnabled(true); + + assertThat(config).isEqualTo(config); + } + + @Test + @DisplayName("equals should compare all fields") + void equalsShouldCompareAllFields() { + MediaGovernanceConfig config1 = new MediaGovernanceConfig(); + config1.setTenantId("t1"); + config1.setEnabled(true); + config1.setAllowedAnalyzers(List.of("nsfw")); + config1.setUpdatedAt("2026-02-18T10:00:00Z"); + config1.setUpdatedBy("admin"); + + MediaGovernanceConfig config2 = new MediaGovernanceConfig(); + config2.setTenantId("t1"); + config2.setEnabled(true); + config2.setAllowedAnalyzers(List.of("nsfw")); + config2.setUpdatedAt("2026-02-18T10:00:00Z"); + config2.setUpdatedBy("admin"); + + assertThat(config1).isEqualTo(config2); + assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); + } + + @Test + @DisplayName("equals should detect differences") + void equalsShouldDetectDifferences() { + MediaGovernanceConfig config1 = new MediaGovernanceConfig(); + config1.setTenantId("t1"); + config1.setEnabled(true); + + MediaGovernanceConfig config2 = new MediaGovernanceConfig(); + config2.setTenantId("t2"); + config2.setEnabled(true); + + MediaGovernanceConfig config3 = new MediaGovernanceConfig(); + config3.setTenantId("t1"); + config3.setEnabled(false); + + assertThat(config1).isNotEqualTo(config2); + assertThat(config1).isNotEqualTo(config3); + assertThat(config1).isNotEqualTo(null); + assertThat(config1).isNotEqualTo("string"); + } + + @Test + @DisplayName("toString should include all fields") + void toStringShouldIncludeAllFields() { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setTenantId("t1"); + config.setEnabled(true); + config.setAllowedAnalyzers(List.of("nsfw", "biometric")); + config.setUpdatedAt("2026-02-18T10:00:00Z"); + config.setUpdatedBy("admin"); + + String str = config.toString(); + + assertThat(str).contains("t1"); + assertThat(str).contains("true"); + assertThat(str).contains("nsfw"); + assertThat(str).contains("biometric"); + assertThat(str).contains("2026-02-18T10:00:00Z"); + assertThat(str).contains("admin"); + } + } + + // ======================================================================== + // MediaGovernanceStatus + // ======================================================================== + + @Nested + @DisplayName("MediaGovernanceStatus") + class MediaGovernanceStatusTests { + + @Test + @DisplayName("should create with default constructor and set all fields") + void shouldCreateWithDefaultConstructor() { + MediaGovernanceStatus status = new MediaGovernanceStatus(); + status.setAvailable(true); + status.setEnabledByDefault(false); + status.setPerTenantControl(true); + status.setTier("enterprise"); + + assertThat(status.isAvailable()).isTrue(); + assertThat(status.isEnabledByDefault()).isFalse(); + assertThat(status.isPerTenantControl()).isTrue(); + assertThat(status.getTier()).isEqualTo("enterprise"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{" + + "\"available\": true," + + "\"enabled_by_default\": true," + + "\"per_tenant_control\": false," + + "\"tier\": \"professional\"" + + "}"; + + MediaGovernanceStatus status = mapper.readValue(json, MediaGovernanceStatus.class); + + assertThat(status.isAvailable()).isTrue(); + assertThat(status.isEnabledByDefault()).isTrue(); + assertThat(status.isPerTenantControl()).isFalse(); + assertThat(status.getTier()).isEqualTo("professional"); + } + + @Test + @DisplayName("should ignore unknown properties during deserialization") + void shouldIgnoreUnknownProperties() throws Exception { + String json = "{\"available\": false, \"tier\": \"free\", \"unknown_field\": true}"; + + MediaGovernanceStatus status = mapper.readValue(json, MediaGovernanceStatus.class); + + assertThat(status.isAvailable()).isFalse(); + assertThat(status.getTier()).isEqualTo("free"); + } + + @Test + @DisplayName("should serialize to JSON") + void shouldSerializeToJson() throws Exception { + MediaGovernanceStatus status = new MediaGovernanceStatus(); + status.setAvailable(true); + status.setEnabledByDefault(true); + status.setPerTenantControl(true); + status.setTier("enterprise"); + + String json = mapper.writeValueAsString(status); + + assertThat(json).contains("\"available\":true"); + assertThat(json).contains("\"enabled_by_default\":true"); + assertThat(json).contains("\"per_tenant_control\":true"); + assertThat(json).contains("\"tier\":\"enterprise\""); + } + + @Test + @DisplayName("equals should be reflexive") + void equalsShouldBeReflexive() { + MediaGovernanceStatus status = new MediaGovernanceStatus(); + status.setAvailable(true); + + assertThat(status).isEqualTo(status); + } + + @Test + @DisplayName("equals should compare all fields") + void equalsShouldCompareAllFields() { + MediaGovernanceStatus status1 = new MediaGovernanceStatus(); + status1.setAvailable(true); + status1.setEnabledByDefault(false); + status1.setPerTenantControl(true); + status1.setTier("enterprise"); + + MediaGovernanceStatus status2 = new MediaGovernanceStatus(); + status2.setAvailable(true); + status2.setEnabledByDefault(false); + status2.setPerTenantControl(true); + status2.setTier("enterprise"); + + assertThat(status1).isEqualTo(status2); + assertThat(status1.hashCode()).isEqualTo(status2.hashCode()); + } + + @Test + @DisplayName("equals should detect differences") + void equalsShouldDetectDifferences() { + MediaGovernanceStatus status1 = new MediaGovernanceStatus(); + status1.setAvailable(true); + status1.setTier("enterprise"); + + MediaGovernanceStatus status2 = new MediaGovernanceStatus(); + status2.setAvailable(false); + status2.setTier("enterprise"); + + MediaGovernanceStatus status3 = new MediaGovernanceStatus(); + status3.setAvailable(true); + status3.setTier("free"); + + assertThat(status1).isNotEqualTo(status2); + assertThat(status1).isNotEqualTo(status3); + assertThat(status1).isNotEqualTo(null); + assertThat(status1).isNotEqualTo("string"); + } + + @Test + @DisplayName("toString should include all fields") + void toStringShouldIncludeAllFields() { + MediaGovernanceStatus status = new MediaGovernanceStatus(); + status.setAvailable(true); + status.setEnabledByDefault(false); + status.setPerTenantControl(true); + status.setTier("enterprise"); + + String str = status.toString(); + + assertThat(str).contains("true"); + assertThat(str).contains("enterprise"); + } + } + + // ======================================================================== + // UpdateMediaGovernanceConfigRequest + // ======================================================================== + + @Nested + @DisplayName("UpdateMediaGovernanceConfigRequest") + class UpdateMediaGovernanceConfigRequestTests { + + @Test + @DisplayName("should create with default constructor and set fields") + void shouldCreateWithDefaultConstructor() { + UpdateMediaGovernanceConfigRequest request = new UpdateMediaGovernanceConfigRequest(); + request.setEnabled(true); + request.setAllowedAnalyzers(List.of("nsfw", "ocr")); + + assertThat(request.getEnabled()).isTrue(); + assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "ocr"); + } + + @Test + @DisplayName("should build with builder pattern") + void shouldBuildWithBuilder() { + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw", "biometric")) + .build(); + + assertThat(request.getEnabled()).isTrue(); + assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "biometric"); + } + + @Test + @DisplayName("builder should handle null enabled for partial update") + void builderShouldHandleNullEnabled() { + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .allowedAnalyzers(List.of("nsfw")) + .build(); + + assertThat(request.getEnabled()).isNull(); + assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw"); + } + + @Test + @DisplayName("builder should handle null allowedAnalyzers for partial update") + void builderShouldHandleNullAnalyzers() { + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(false) + .build(); + + assertThat(request.getEnabled()).isFalse(); + assertThat(request.getAllowedAnalyzers()).isNull(); + } + + @Test + @DisplayName("should serialize omitting null fields") + void shouldSerializeOmittingNulls() throws Exception { + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .build(); + + String json = mapper.writeValueAsString(request); + + assertThat(json).contains("\"enabled\":true"); + assertThat(json).doesNotContain("allowed_analyzers"); + } + + @Test + @DisplayName("should serialize with all fields") + void shouldSerializeAllFields() throws Exception { + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(false) + .allowedAnalyzers(List.of("ocr")) + .build(); + + String json = mapper.writeValueAsString(request); + + assertThat(json).contains("\"enabled\":false"); + assertThat(json).contains("\"allowed_analyzers\":[\"ocr\"]"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{\"enabled\": true, \"allowed_analyzers\": [\"nsfw\", \"biometric\"]}"; + + UpdateMediaGovernanceConfigRequest request = + mapper.readValue(json, UpdateMediaGovernanceConfigRequest.class); + + assertThat(request.getEnabled()).isTrue(); + assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "biometric"); + } + + @Test + @DisplayName("equals should be reflexive") + void equalsShouldBeReflexive() { + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .build(); + + assertThat(request).isEqualTo(request); + } + + @Test + @DisplayName("equals should compare all fields") + void equalsShouldCompareAllFields() { + UpdateMediaGovernanceConfigRequest r1 = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw")) + .build(); + + UpdateMediaGovernanceConfigRequest r2 = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw")) + .build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + } + + @Test + @DisplayName("equals should detect differences") + void equalsShouldDetectDifferences() { + UpdateMediaGovernanceConfigRequest r1 = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .build(); + + UpdateMediaGovernanceConfigRequest r2 = UpdateMediaGovernanceConfigRequest.builder() + .enabled(false) + .build(); + + assertThat(r1).isNotEqualTo(r2); + assertThat(r1).isNotEqualTo(null); + assertThat(r1).isNotEqualTo("string"); + } + + @Test + @DisplayName("toString should include fields") + void toStringShouldIncludeFields() { + UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw", "biometric")) + .build(); + + String str = request.toString(); + + assertThat(str).contains("true"); + assertThat(str).contains("nsfw"); + assertThat(str).contains("biometric"); + } + } + + // ======================================================================== + // Media Policy Category Constants & Enum Values + // ======================================================================== + + @Nested + @DisplayName("Media Policy Categories") + class MediaPolicyCategoryTests { + + @Test + @DisplayName("CATEGORY_MEDIA_SAFETY constant should match enum value") + void mediaSafetyConstantShouldMatchEnum() { + assertThat(PolicyTypes.CATEGORY_MEDIA_SAFETY).isEqualTo("media-safety"); + assertThat(PolicyCategory.MEDIA_SAFETY.getValue()).isEqualTo("media-safety"); + assertThat(PolicyTypes.CATEGORY_MEDIA_SAFETY).isEqualTo(PolicyCategory.MEDIA_SAFETY.getValue()); + } + + @Test + @DisplayName("CATEGORY_MEDIA_BIOMETRIC constant should match enum value") + void mediaBiometricConstantShouldMatchEnum() { + assertThat(PolicyTypes.CATEGORY_MEDIA_BIOMETRIC).isEqualTo("media-biometric"); + assertThat(PolicyCategory.MEDIA_BIOMETRIC.getValue()).isEqualTo("media-biometric"); + assertThat(PolicyTypes.CATEGORY_MEDIA_BIOMETRIC).isEqualTo(PolicyCategory.MEDIA_BIOMETRIC.getValue()); + } + + @Test + @DisplayName("CATEGORY_MEDIA_DOCUMENT constant should match enum value") + void mediaDocumentConstantShouldMatchEnum() { + assertThat(PolicyTypes.CATEGORY_MEDIA_DOCUMENT).isEqualTo("media-document"); + assertThat(PolicyCategory.MEDIA_DOCUMENT.getValue()).isEqualTo("media-document"); + assertThat(PolicyTypes.CATEGORY_MEDIA_DOCUMENT).isEqualTo(PolicyCategory.MEDIA_DOCUMENT.getValue()); + } + + @Test + @DisplayName("CATEGORY_MEDIA_PII constant should match enum value") + void mediaPiiConstantShouldMatchEnum() { + assertThat(PolicyTypes.CATEGORY_MEDIA_PII).isEqualTo("media-pii"); + assertThat(PolicyCategory.MEDIA_PII.getValue()).isEqualTo("media-pii"); + assertThat(PolicyTypes.CATEGORY_MEDIA_PII).isEqualTo(PolicyCategory.MEDIA_PII.getValue()); + } + + @Test + @DisplayName("all media categories should exist in PolicyCategory enum") + void allMediaCategoriesShouldExist() { + assertThat(PolicyCategory.valueOf("MEDIA_SAFETY")).isNotNull(); + assertThat(PolicyCategory.valueOf("MEDIA_BIOMETRIC")).isNotNull(); + assertThat(PolicyCategory.valueOf("MEDIA_DOCUMENT")).isNotNull(); + assertThat(PolicyCategory.valueOf("MEDIA_PII")).isNotNull(); + } + } +} From 89238867964e3a858e4a7c70e53775b96df492f9 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sun, 22 Feb 2026 01:28:30 +0300 Subject: [PATCH 4/6] docs: update v3.6.0 release date to 2026-02-22 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf7a05..cbaa13a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to the AxonFlow Java SDK will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [3.6.0] - 2026-02-19 +## [3.6.0] - 2026-02-22 ### Added From 790d28e6d54607d3d4dde4f1a88d43b0f248b973 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sun, 22 Feb 2026 11:55:56 +0300 Subject: [PATCH 5/6] feat: add post-execution metrics to MarkStepCompletedRequest tokens_in, tokens_out, cost_usd fields with builder support on MarkStepCompletedRequest allow reporting actual LLM usage at step completion time. --- CHANGELOG.md | 1 + .../sdk/types/workflow/WorkflowTypes.java | 67 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaa13a..b8d5a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Media governance types: `MediaGovernanceConfig`, `MediaGovernanceStatus` - Media policy category constants: `CATEGORY_MEDIA_SAFETY`, `CATEGORY_MEDIA_BIOMETRIC`, `CATEGORY_MEDIA_PII`, `CATEGORY_MEDIA_DOCUMENT` - `PolicyCategory` enum values: `MEDIA_SAFETY`, `MEDIA_BIOMETRIC`, `MEDIA_PII`, `MEDIA_DOCUMENT` +- StepComplete now accepts post-execution metrics (`tokens_in`, `tokens_out`, `cost_usd`) --- diff --git a/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java b/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java index ca3c76c..6f18e64 100644 --- a/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java @@ -853,12 +853,27 @@ public static final class MarkStepCompletedRequest { @JsonProperty("metadata") private final Map metadata; + @JsonProperty("tokens_in") + private final Integer tokensIn; + + @JsonProperty("tokens_out") + private final Integer tokensOut; + + @JsonProperty("cost_usd") + private final Double costUsd; + @JsonCreator public MarkStepCompletedRequest( @JsonProperty("output") Map output, - @JsonProperty("metadata") Map metadata) { + @JsonProperty("metadata") Map metadata, + @JsonProperty("tokens_in") Integer tokensIn, + @JsonProperty("tokens_out") Integer tokensOut, + @JsonProperty("cost_usd") Double costUsd) { this.output = output != null ? Collections.unmodifiableMap(output) : Collections.emptyMap(); this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); + this.tokensIn = tokensIn; + this.tokensOut = tokensOut; + this.costUsd = costUsd; } public Map getOutput() { @@ -869,6 +884,36 @@ public Map getMetadata() { return metadata; } + /** + * Returns the number of input tokens consumed by the step. + * + * @return input token count, or null if not provided + * @since 3.6.0 + */ + public Integer getTokensIn() { + return tokensIn; + } + + /** + * Returns the number of output tokens produced by the step. + * + * @return output token count, or null if not provided + * @since 3.6.0 + */ + public Integer getTokensOut() { + return tokensOut; + } + + /** + * Returns the cost in USD incurred by the step. + * + * @return cost in USD, or null if not provided + * @since 3.6.0 + */ + public Double getCostUsd() { + return costUsd; + } + public static Builder builder() { return new Builder(); } @@ -876,6 +921,9 @@ public static Builder builder() { public static final class Builder { private Map output; private Map metadata; + private Integer tokensIn; + private Integer tokensOut; + private Double costUsd; public Builder output(Map output) { this.output = output; @@ -887,8 +935,23 @@ public Builder metadata(Map metadata) { return this; } + public Builder tokensIn(Integer tokensIn) { + this.tokensIn = tokensIn; + return this; + } + + public Builder tokensOut(Integer tokensOut) { + this.tokensOut = tokensOut; + return this; + } + + public Builder costUsd(Double costUsd) { + this.costUsd = costUsd; + return this; + } + public MarkStepCompletedRequest build() { - return new MarkStepCompletedRequest(output, metadata); + return new MarkStepCompletedRequest(output, metadata, tokensIn, tokensOut, costUsd); } } } From 7d76fa322a07d9d3ee6ea195ab4a9b2049e60970 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sun, 22 Feb 2026 12:17:10 +0300 Subject: [PATCH 6/6] fix: add @JsonInclude(NON_NULL) to MarkStepCompletedRequest Prevents null metrics fields from serializing as explicit "null" values in JSON. Matches the pattern used by all other request types in the SDK. --- CHANGELOG.md | 2 +- .../java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d5a69..2e337c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Media governance types: `MediaGovernanceConfig`, `MediaGovernanceStatus` - Media policy category constants: `CATEGORY_MEDIA_SAFETY`, `CATEGORY_MEDIA_BIOMETRIC`, `CATEGORY_MEDIA_PII`, `CATEGORY_MEDIA_DOCUMENT` - `PolicyCategory` enum values: `MEDIA_SAFETY`, `MEDIA_BIOMETRIC`, `MEDIA_PII`, `MEDIA_DOCUMENT` -- StepComplete now accepts post-execution metrics (`tokens_in`, `tokens_out`, `cost_usd`) +- `MarkStepCompletedRequest` now accepts post-execution metrics (`tokens_in`, `tokens_out`, `cost_usd`) --- diff --git a/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java b/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java index 6f18e64..a68ba75 100644 --- a/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; @@ -845,6 +846,7 @@ public int getTotal() { * Request to mark a step as completed. */ @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) public static final class MarkStepCompletedRequest { @JsonProperty("output")