diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2d8d5f6..7bcc6037 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*') }} restore-keys: | ${{ runner.os }}-gradle- diff --git a/src/main/java/io/getstream/client/AuditLogsClient.java b/src/main/java/io/getstream/client/AuditLogsClient.java new file mode 100644 index 00000000..18e592a0 --- /dev/null +++ b/src/main/java/io/getstream/client/AuditLogsClient.java @@ -0,0 +1,97 @@ +package io.getstream.client; + +import io.getstream.core.Stream; +import io.getstream.core.http.Token; +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.AuditLog; +import io.getstream.core.options.RequestOption; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.utils.Auth.TokenAction; +import java8.util.concurrent.CompletableFuture; + +import java.util.ArrayList; +import java.util.List; + +import static io.getstream.core.utils.Auth.buildAuditLogsToken; + +/** + * Client for querying Stream audit logs. + * Audit logs record changes to various entities within your Stream app. + */ +public final class AuditLogsClient { + private final String secret; + private final Stream stream; + + public AuditLogsClient(String secret, Stream stream) { + this.secret = secret; + this.stream = stream; + } + + /** + * Query audit logs with the specified filters and default pagination. + * + * @param filters Filters to apply to the query (either entityType+entityID OR userID is required) + * @return CompletableFuture with the query response + * @throws StreamException if the filters are invalid or if there's an API error + */ + public CompletableFuture queryAuditLogs(QueryAuditLogsFilters filters) throws StreamException { + return queryAuditLogs(filters, new QueryAuditLogsPager()); + } + + /** + * Query audit logs with the specified filters and pagination. + * + * @param filters Filters to apply to the query (either entityType+entityID OR userID is required) + * @param pager Pagination settings for the query + * @return CompletableFuture with the query response + * @throws StreamException if the filters are invalid or if there's an API error + */ + public CompletableFuture queryAuditLogs(QueryAuditLogsFilters filters, QueryAuditLogsPager pager) throws StreamException { + // Validate filters before making the API call + if (filters == null) { + throw new StreamException("Filters cannot be null for audit logs queries"); + } + + final Token token = buildAuditLogsToken(secret, TokenAction.READ); + + RequestOption[] options = buildRequestOptions(filters, pager); + return stream.queryAuditLogs(token, options); + } + + /** + * Builds request options from filters and pagination settings. + * + * @param filters Filters to apply to the query + * @param pager Pagination settings + * @return Array of RequestOption for the API call + */ + private RequestOption[] buildRequestOptions(QueryAuditLogsFilters filters, QueryAuditLogsPager pager) { + List options = new ArrayList<>(); + + if (filters.getEntityType() != null && !filters.getEntityType().isEmpty() && + filters.getEntityID() != null && !filters.getEntityID().isEmpty()) { + options.add(new CustomQueryParameter("entity_type", filters.getEntityType())); + options.add(new CustomQueryParameter("entity_id", filters.getEntityID())); + } + + if (filters.getUserID() != null && !filters.getUserID().isEmpty()) { + options.add(new CustomQueryParameter("user_id", filters.getUserID())); + } + + if (pager != null) { + if (pager.getNext() != null && !pager.getNext().isEmpty()) { + options.add(new CustomQueryParameter("next", pager.getNext())); + } + + if (pager.getPrev() != null && !pager.getPrev().isEmpty()) { + options.add(new CustomQueryParameter("prev", pager.getPrev())); + } + + if (pager.getLimit() > 0) { + options.add(new CustomQueryParameter("limit", Integer.toString(pager.getLimit()))); + } + } + + return options.toArray(new RequestOption[0]); + } +} \ No newline at end of file diff --git a/src/main/java/io/getstream/client/Client.java b/src/main/java/io/getstream/client/Client.java index 79301137..49fba80c 100644 --- a/src/main/java/io/getstream/client/Client.java +++ b/src/main/java/io/getstream/client/Client.java @@ -256,6 +256,10 @@ public ModerationClient moderation() { return new ModerationClient(secret, stream.moderation()); } + public AuditLogsClient auditLogs() { + return new AuditLogsClient(secret, stream); + } + public FileStorageClient files() { return new FileStorageClient(secret, stream.files()); } diff --git a/src/main/java/io/getstream/client/QueryAuditLogsFilters.java b/src/main/java/io/getstream/client/QueryAuditLogsFilters.java new file mode 100644 index 00000000..991ba18f --- /dev/null +++ b/src/main/java/io/getstream/client/QueryAuditLogsFilters.java @@ -0,0 +1,202 @@ +package io.getstream.client; + +import io.getstream.core.exceptions.StreamException; + +/** + * Filters for querying audit logs. + * Either entityType+entityID pair OR userID is required by the API. + * + * Common entity types in Stream include: + * - "activity" - for feed activities + * - "reaction" - for activity reactions + * - "user" - for Stream users + * - "feed" - for feed configurations + */ +public class QueryAuditLogsFilters { + private String entityType; + private String entityID; + private String userID; + + /** + * Private constructor, use builder instead. + */ + private QueryAuditLogsFilters() { + } + + /** + * Creates a new builder for QueryAuditLogsFilters. + * + * @return a new QueryAuditLogsFilters.Builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a new filter for user ID queries. + * + * @param userID The ID of the user + * @return a new QueryAuditLogsFilters with the user ID set + */ + public static QueryAuditLogsFilters forUser(String userID) { + return builder().withUserID(userID).build(); + } + + /** + * Creates a new filter for entity type and ID queries. + * + * @param entityType The type of entity (e.g., "activity", "reaction", "user", "feed") + * @param entityID The ID of the entity + * @return a new QueryAuditLogsFilters with the entity type and ID set + */ + public static QueryAuditLogsFilters forEntity(String entityType, String entityID) { + return builder().withEntityType(entityType).withEntityID(entityID).build(); + } + + /** + * Convenience method to create a filter for activity entities. + * + * @param activityID The ID of the activity + * @return a new QueryAuditLogsFilters for the activity + */ + public static QueryAuditLogsFilters forActivity(String activityID) { + return forEntity("activity", activityID); + } + + /** + * Convenience method to create a filter for reaction entities. + * + * @param reactionID The ID of the reaction + * @return a new QueryAuditLogsFilters for the reaction + */ + public static QueryAuditLogsFilters forReaction(String reactionID) { + return forEntity("reaction", reactionID); + } + + public String getEntityType() { + return entityType; + } + + public String getEntityID() { + return entityID; + } + + public String getUserID() { + return userID; + } + + /** + * Set the entity type for existing filter instance. + * + * @param entityType The type of entity (e.g., "activity", "reaction") + * @return this instance for method chaining + */ + public QueryAuditLogsFilters setEntityType(String entityType) { + this.entityType = entityType; + return this; + } + + /** + * Set the entity ID for existing filter instance. + * + * @param entityID The ID of the entity + * @return this instance for method chaining + */ + public QueryAuditLogsFilters setEntityID(String entityID) { + this.entityID = entityID; + return this; + } + + /** + * Set the user ID for existing filter instance. + * + * @param userID The ID of the user + * @return this instance for method chaining + */ + public QueryAuditLogsFilters setUserID(String userID) { + this.userID = userID; + return this; + } + + /** + * Validates that the filters contain the required fields. + * Either (entityType AND entityID) OR userID must be set. + * + * @throws StreamException if the required fields are not set + */ + public void validate() throws StreamException { + boolean hasEntityFields = entityType != null && !entityType.isEmpty() && + entityID != null && !entityID.isEmpty(); + boolean hasUserID = userID != null && !userID.isEmpty(); + + if (!hasEntityFields && !hasUserID) { + throw new StreamException("Either entityType+entityID or userID is required for audit logs queries"); + } + } + + /** + * Checks if the filter is valid according to API requirements. + * + * @return true if either (entityType AND entityID) OR userID is set + */ + public boolean isValid() { + boolean hasEntityFields = entityType != null && !entityType.isEmpty() && + entityID != null && !entityID.isEmpty(); + boolean hasUserID = userID != null && !userID.isEmpty(); + + return hasEntityFields || hasUserID; + } + + /** + * Builder class for QueryAuditLogsFilters. + */ + public static class Builder { + private final QueryAuditLogsFilters filters; + + private Builder() { + filters = new QueryAuditLogsFilters(); + } + + /** + * Set the entity type. + * + * @param entityType The type of entity (e.g., "activity", "reaction") + * @return this builder for method chaining + */ + public Builder withEntityType(String entityType) { + filters.entityType = entityType; + return this; + } + + /** + * Set the entity ID. + * + * @param entityID The ID of the entity + * @return this builder for method chaining + */ + public Builder withEntityID(String entityID) { + filters.entityID = entityID; + return this; + } + + /** + * Set the user ID. + * + * @param userID The ID of the user + * @return this builder for method chaining + */ + public Builder withUserID(String userID) { + filters.userID = userID; + return this; + } + + /** + * Builds the QueryAuditLogsFilters instance. + * + * @return a new QueryAuditLogsFilters instance + */ + public QueryAuditLogsFilters build() { + return filters; + } + } +} \ No newline at end of file diff --git a/src/main/java/io/getstream/client/QueryAuditLogsPager.java b/src/main/java/io/getstream/client/QueryAuditLogsPager.java new file mode 100644 index 00000000..8b78da59 --- /dev/null +++ b/src/main/java/io/getstream/client/QueryAuditLogsPager.java @@ -0,0 +1,44 @@ +package io.getstream.client; + +public class QueryAuditLogsPager { + private String next; + private String prev; + private int limit; + + public QueryAuditLogsPager() { + } + + public QueryAuditLogsPager(int limit) { + this.limit = limit; + } + + public QueryAuditLogsPager(String next, String prev, int limit) { + this.next = next; + this.prev = prev; + this.limit = limit; + } + + public String getNext() { + return next; + } + + public void setNext(String next) { + this.next = next; + } + + public String getPrev() { + return prev; + } + + public void setPrev(String prev) { + this.prev = prev; + } + + public int getLimit() { + return limit; + } + + public void setLimit(int limit) { + this.limit = limit; + } +} \ No newline at end of file diff --git a/src/main/java/io/getstream/client/QueryAuditLogsResponse.java b/src/main/java/io/getstream/client/QueryAuditLogsResponse.java new file mode 100644 index 00000000..1739ba3a --- /dev/null +++ b/src/main/java/io/getstream/client/QueryAuditLogsResponse.java @@ -0,0 +1,86 @@ +package io.getstream.client; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import io.getstream.core.models.AuditLog; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class QueryAuditLogsResponse { + @JsonProperty("audit_logs") + private List auditLogs; + + private String next; + private String prev; + private String duration; + + // Default constructor + public QueryAuditLogsResponse() { + this.auditLogs = new ArrayList<>(); + } + + // Constructor with parameters + @JsonCreator + public QueryAuditLogsResponse( + @JsonProperty("audit_logs") List auditLogs, + @JsonProperty("next") String next, + @JsonProperty("prev") String prev, + @JsonProperty("duration") String duration) { + // Initialize mandatory fields with safe defaults if they're null + this.auditLogs = auditLogs != null ? auditLogs : new ArrayList<>(); + this.next = next; + this.prev = prev; + this.duration = duration != null ? duration : ""; + } + + public List getAuditLogs() { + return auditLogs; + } + + public String getNext() { + return next; + } + + public String getPrev() { + return prev; + } + + public String getDuration() { + return duration; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QueryAuditLogsResponse that = (QueryAuditLogsResponse) o; + return Objects.equals(auditLogs, that.auditLogs) && + Objects.equals(next, that.next) && + Objects.equals(prev, that.prev) && + Objects.equals(duration, that.duration); + } + + @Override + public int hashCode() { + return Objects.hash(auditLogs, next, prev, duration); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("auditLogs", this.auditLogs) + .add("next", this.next) + .add("prev", this.prev) + .add("duration", this.duration) + .toString(); + } +} \ No newline at end of file diff --git a/src/main/java/io/getstream/core/Stream.java b/src/main/java/io/getstream/core/Stream.java index a788533d..897817c9 100644 --- a/src/main/java/io/getstream/core/Stream.java +++ b/src/main/java/io/getstream/core/Stream.java @@ -572,4 +572,22 @@ public CompletableFuture exportUserActivities(Token token, St throw new StreamException(e); } } + + public CompletableFuture queryAuditLogs(Token token, RequestOption... options) throws StreamException { + try { + final URL url = buildAuditLogsURL(baseURL); + return httpClient + .execute(buildGet(url, key, token, options)) + .thenApply( + response -> { + try { + return deserialize(response, io.getstream.client.QueryAuditLogsResponse.class); + } catch (StreamException | IOException e) { + throw new CompletionException(e); + } + }); + } catch (MalformedURLException | URISyntaxException e) { + throw new StreamException(e); + } + } } diff --git a/src/main/java/io/getstream/core/models/AuditLog.java b/src/main/java/io/getstream/core/models/AuditLog.java new file mode 100644 index 00000000..82527981 --- /dev/null +++ b/src/main/java/io/getstream/core/models/AuditLog.java @@ -0,0 +1,47 @@ +package io.getstream.core.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; +import java.util.Map; + +public class AuditLog { + @JsonProperty("entity_type") + private String entityType; + + @JsonProperty("entity_id") + private String entityID; + + private String action; + + @JsonProperty("user_id") + private String userID; + + private Map custom; + + @JsonProperty("created_at") + private Date createdAt; + + public String getEntityType() { + return entityType; + } + + public String getEntityID() { + return entityID; + } + + public String getAction() { + return action; + } + + public String getUserID() { + return userID; + } + + public Map getCustom() { + return custom; + } + + public Date getCreatedAt() { + return createdAt; + } +} \ No newline at end of file diff --git a/src/main/java/io/getstream/core/utils/Auth.java b/src/main/java/io/getstream/core/utils/Auth.java index 0c4ece57..92ab6966 100644 --- a/src/main/java/io/getstream/core/utils/Auth.java +++ b/src/main/java/io/getstream/core/utils/Auth.java @@ -43,7 +43,8 @@ public enum TokenResource { REACTIONS("reactions"), USERS("users"), MODERATION("moderation"), - DATAPRIVACY("data_privacy"); + DATAPRIVACY("data_privacy"), + AUDITLOGS("audit_logs"); private final String resource; @@ -128,6 +129,10 @@ public static Token buildFilesToken(String secret, TokenAction action) { return buildBackendToken(secret, TokenResource.FILES, action, "*"); } + public static Token buildAuditLogsToken(String secret, TokenAction action) { + return buildBackendToken(secret, TokenResource.AUDITLOGS, action, "*"); + } + public static Token buildFrontendToken(String secret, String userID) { return buildFrontendToken(secret, userID, null); } diff --git a/src/main/java/io/getstream/core/utils/Routes.java b/src/main/java/io/getstream/core/utils/Routes.java index dbab60a6..37892db5 100644 --- a/src/main/java/io/getstream/core/utils/Routes.java +++ b/src/main/java/io/getstream/core/utils/Routes.java @@ -47,7 +47,8 @@ public static URL buildEnrichedFeedURL(URL baseURL, FeedID feed, String path) return new URL(baseURL, basePath + enrichedFeedPath(feed) + path); } - public static URL buildToTargetUpdateURL(URL baseURL, FeedID feed) throws MalformedURLException { + public static URL buildToTargetUpdateURL(URL baseURL, FeedID feed) + throws MalformedURLException, URISyntaxException { return new URL(baseURL, basePath + feedTargetsPath(feed) + toTargetUpdatePath); } @@ -143,6 +144,10 @@ public static URL followStatsPath(URL baseURL) throws MalformedURLException { return new URL(baseURL, basePath + followStatsPath); } + public static URL buildAuditLogsURL(URL baseURL) throws MalformedURLException, URISyntaxException { + return new URL(baseURL, basePath + "audit_logs/"); + } + private static URL buildSubdomainPath(URL baseURL, String subdomain, String apiPath, String path) throws MalformedURLException { try { diff --git a/src/test/java/io/getstream/client/AuditLogsClientIntegrationTest.java b/src/test/java/io/getstream/client/AuditLogsClientIntegrationTest.java new file mode 100644 index 00000000..82aaec15 --- /dev/null +++ b/src/test/java/io/getstream/client/AuditLogsClientIntegrationTest.java @@ -0,0 +1,56 @@ +package io.getstream.client; + +import io.getstream.core.exceptions.StreamException; +import io.getstream.core.models.Activity; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Integration test for the AuditLogsClient + * Uses a Stream app with audit logs enabled + */ +public class AuditLogsClientIntegrationTest { + // Credentials for a Stream app with audit logs enabled + private static final String apiKey = + System.getenv("STREAM_KEY") != null + ? System.getenv("STREAM_KEY") + : System.getProperty("STREAM_KEY"); + private static final String secret = + System.getenv("STREAM_SECRET") != null + ? System.getenv("STREAM_SECRET") + : System.getProperty("STREAM_SECRET"); + + private Client client; + + @Before + public void setUp() throws Exception { + client = Client.builder(apiKey, secret).build(); + } + + + @Test + public void testQueryAuditLogs() throws StreamException { + FlatFeed userFeed = client.flatFeed("user", "1"); + Activity a = Activity.builder().actor("userid:1").verb("tweet").object("Tweet:1").build(); + a = userFeed.addActivity(a).join(); + + // Using the convenience method for activity entities + QueryAuditLogsPager pager = new QueryAuditLogsPager(5); // limit to 5 results + QueryAuditLogsFilters filters = QueryAuditLogsFilters.forActivity(a.getID()); + QueryAuditLogsResponse response = client.auditLogs().queryAuditLogs(filters, pager).join(); + + // Verify response structure + assertNotNull("Response should not be null", response); + assertNotNull("Audit logs list should not be null", response.getAuditLogs()); + assertNotEquals(0, response.getAuditLogs().size()); + + filters = QueryAuditLogsFilters.forUser("userid:1"); + response = client.auditLogs().queryAuditLogs(filters, pager).join(); + + // Verify response structure + assertNotNull("Response should not be null", response); + assertNotNull("Audit logs list should not be null", response.getAuditLogs()); + assertNotEquals(0, response.getAuditLogs().size()); + } +} \ No newline at end of file diff --git a/src/test/java/io/getstream/client/AuditLogsClientTest.java b/src/test/java/io/getstream/client/AuditLogsClientTest.java new file mode 100644 index 00000000..6fa69ffa --- /dev/null +++ b/src/test/java/io/getstream/client/AuditLogsClientTest.java @@ -0,0 +1,269 @@ +package io.getstream.client; + +import static org.junit.Assert.*; + +import io.getstream.core.http.HTTPClient; +import io.getstream.core.http.Request; +import io.getstream.core.http.Response; +import io.getstream.core.http.Token; +import io.getstream.core.models.AuditLog; +import io.getstream.core.options.CustomQueryParameter; +import io.getstream.core.options.RequestOption; +import io.getstream.core.utils.Auth; + +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Collections; + +import java8.util.concurrent.CompletableFuture; +import org.junit.Before; +import org.junit.Test; + +public class AuditLogsClientTest { + // Using fixed test credentials for unit tests + private static final String apiKey = "test_key"; + private static final String secret = "test_secret"; + + private MockHTTPClient mockHTTPClient; + private Client client; + + class MockHTTPClient extends HTTPClient { + public Request lastRequest; + private final String responseJson; + + public MockHTTPClient(String responseJson) { + this.responseJson = responseJson; + } + + @Override + public T getImplementation() { + return null; + } + + @Override + public CompletableFuture execute(Request request) { + lastRequest = request; + + Response response = new Response(200, new ByteArrayInputStream(responseJson.getBytes(StandardCharsets.UTF_8))); + CompletableFuture future = new CompletableFuture<>(); + future.complete(response); + return future; + } + } + + @Before + public void setUp() throws Exception { + String mockResponse = "{\n" + + " \"audit_logs\": [\n" + + " {\n" + + " \"entity_type\": \"user\",\n" + + " \"entity_id\": \"user-123\",\n" + + " \"action\": \"update\",\n" + + " \"user_id\": \"admin-user\",\n" + + " \"custom\": {\"changes\": {\"name\": \"New Name\"}},\n" + + " \"created_at\": \"2023-01-01T12:00:00.000Z\"\n" + + " },\n" + + " {\n" + + " \"entity_type\": \"feed\",\n" + + " \"entity_id\": \"feed-456\",\n" + + " \"action\": \"delete\",\n" + + " \"user_id\": \"admin-user\",\n" + + " \"custom\": {},\n" + + " \"created_at\": \"2023-01-02T12:00:00.000Z\"\n" + + " }\n" + + " ],\n" + + " \"next\": \"next-page-token\",\n" + + " \"prev\": \"prev-page-token\",\n" + + " \"duration\": \"42ms\"\n" + + "}"; + + mockHTTPClient = new MockHTTPClient(mockResponse); + client = Client.builder(apiKey, secret) + .httpClient(mockHTTPClient) + .build(); + } + + @Test + public void testQueryAuditLogs() throws Exception { + QueryAuditLogsFilters filters = QueryAuditLogsFilters.forEntity("activity", "activity-123"); + QueryAuditLogsPager pager = new QueryAuditLogsPager(10); + + QueryAuditLogsResponse response = client.auditLogs().queryAuditLogs(filters, pager).join(); + + // Verify the response + assertNotNull(response); + assertNotNull(response.getAuditLogs()); + assertEquals(2, response.getAuditLogs().size()); + assertEquals("next-page-token", response.getNext()); + assertEquals("prev-page-token", response.getPrev()); + + // Verify first audit log + AuditLog firstLog = response.getAuditLogs().get(0); + assertEquals("user", firstLog.getEntityType()); + assertEquals("user-123", firstLog.getEntityID()); + assertEquals("update", firstLog.getAction()); + assertEquals("admin-user", firstLog.getUserID()); + assertNotNull(firstLog.getCustom()); + assertNotNull(firstLog.getCreatedAt()); + + // Verify second audit log + AuditLog secondLog = response.getAuditLogs().get(1); + assertEquals("feed", secondLog.getEntityType()); + assertEquals("feed-456", secondLog.getEntityID()); + assertEquals("delete", secondLog.getAction()); + + // Verify request parameters + Request lastRequest = mockHTTPClient.lastRequest; + assertNotNull(lastRequest); + + // Extract query parameters from URL + String urlQuery = lastRequest.getURL().getQuery(); + Map queryParams = extractQueryParams(urlQuery); + + assertEquals("activity", queryParams.get("entity_type")); + assertEquals("activity-123", queryParams.get("entity_id")); + assertEquals("10", queryParams.get("limit")); + } + + @Test + public void testQueryAuditLogsWithUserFilter() throws Exception { + QueryAuditLogsFilters filters = QueryAuditLogsFilters.forUser("admin-user"); + + client.auditLogs().queryAuditLogs(filters).join(); + + // Verify request parameters + Request lastRequest = mockHTTPClient.lastRequest; + assertNotNull(lastRequest); + + // Extract query parameters from URL + String urlQuery = lastRequest.getURL().getQuery(); + Map queryParams = extractQueryParams(urlQuery); + + assertEquals("admin-user", queryParams.get("user_id")); + } + + @Test + public void testQueryAuditLogsWithPagination() throws Exception { + QueryAuditLogsPager pager = new QueryAuditLogsPager(); + pager.setNext("next-token"); + + QueryAuditLogsFilters filters = QueryAuditLogsFilters.forUser("admin-user"); + client.auditLogs().queryAuditLogs(filters, pager).join(); + + // Verify request parameters + Request lastRequest = mockHTTPClient.lastRequest; + assertNotNull(lastRequest); + + // Extract query parameters from URL + String urlQuery = lastRequest.getURL().getQuery(); + Map queryParams = extractQueryParams(urlQuery); + + assertEquals("next-token", queryParams.get("next")); + assertEquals("admin-user", queryParams.get("user_id")); + } + + @Test + public void testQueryAuditLogsTokenGeneration() throws Exception { + // Create a client with a specific secret to test token generation + String testSecret = "test-secret"; + Client client = Client.builder(apiKey, testSecret) + .httpClient(mockHTTPClient) + .build(); + + QueryAuditLogsFilters filters = QueryAuditLogsFilters.forUser("admin-user"); + client.auditLogs().queryAuditLogs(filters).join(); + + // Verify the token was generated using the correct resource and action + Request lastRequest = mockHTTPClient.lastRequest; + assertNotNull(lastRequest); + + Token token = lastRequest.getToken(); + assertNotNull(token); + // We can't directly test the token's contents, but we can verify it's not null + } + + @Test + public void testBuilderPatternFlexibility() throws Exception { + // Test the full builder pattern flexibility + QueryAuditLogsFilters filters = QueryAuditLogsFilters.builder() + .withEntityType("reaction") + .withEntityID("reaction-123") + .withUserID("admin") + .build(); + + client.auditLogs().queryAuditLogs(filters).join(); + + // Verify request parameters + Request lastRequest = mockHTTPClient.lastRequest; + assertNotNull(lastRequest); + + // Extract query parameters from URL + String urlQuery = lastRequest.getURL().getQuery(); + Map queryParams = extractQueryParams(urlQuery); + + assertEquals("reaction", queryParams.get("entity_type")); + assertEquals("reaction-123", queryParams.get("entity_id")); + assertEquals("admin", queryParams.get("user_id")); + } + + @Test + public void testForActivityConvenienceMethod() throws Exception { + // Test the convenience method for activities + QueryAuditLogsFilters filters = QueryAuditLogsFilters.forActivity("activity-789"); + + client.auditLogs().queryAuditLogs(filters).join(); + + // Verify request parameters + Request lastRequest = mockHTTPClient.lastRequest; + assertNotNull(lastRequest); + + // Extract query parameters from URL + String urlQuery = lastRequest.getURL().getQuery(); + Map queryParams = extractQueryParams(urlQuery); + + assertEquals("activity", queryParams.get("entity_type")); + assertEquals("activity-789", queryParams.get("entity_id")); + } + + @Test + public void testForReactionConvenienceMethod() throws Exception { + // Test the convenience method for reactions + QueryAuditLogsFilters filters = QueryAuditLogsFilters.forReaction("reaction-456"); + + client.auditLogs().queryAuditLogs(filters).join(); + + // Verify request parameters + Request lastRequest = mockHTTPClient.lastRequest; + assertNotNull(lastRequest); + + // Extract query parameters from URL + String urlQuery = lastRequest.getURL().getQuery(); + Map queryParams = extractQueryParams(urlQuery); + + assertEquals("reaction", queryParams.get("entity_type")); + assertEquals("reaction-456", queryParams.get("entity_id")); + } + + private Map extractQueryParams(String query) { + if (query == null || query.isEmpty()) { + return Collections.emptyMap(); + } + + Map params = new HashMap<>(); + String[] pairs = query.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("="); + if (keyValue.length == 2) { + params.put(keyValue[0], keyValue[1]); + } + } + + return params; + } +} \ No newline at end of file diff --git a/src/test/java/io/getstream/client/ModerationClientTest.java b/src/test/java/io/getstream/client/ModerationClientTest.java index 75899094..0a2aeec0 100644 --- a/src/test/java/io/getstream/client/ModerationClientTest.java +++ b/src/test/java/io/getstream/client/ModerationClientTest.java @@ -66,8 +66,8 @@ public void testFlagReaction() throws Exception { Activity activityResponse = client.flatFeed("flat", "1").addActivity(activity).join(); assertNotNull(activityResponse); - Reaction reactionResponse = - client.reactions().add("bad-user", "like", activityResponse.getID()).join(); + Reaction r = Reaction.builder().activityID(activityResponse.getID()).kind("like").userID("bad-user").moderationTemplate("moderation_template_reaction").build(); + Reaction reactionResponse = client.reactions().add("bad-user", r).join(); assertNotNull(reactionResponse); Response flagResponse =