diff --git a/CHANGELOG.md b/CHANGELOG.md index b98aa38..cbaa13a 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-22 + +### 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/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..05ccb9b 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 // ======================================================================== @@ -56,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"), 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(); + } + } +}