This is an automated email from the ASF dual-hosted git repository.
maciej pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 29a27cc00 feat(java): add missing methods to async TCP UsersClient
(#2837)
29a27cc00 is described below
commit 29a27cc0081b9b1310b3dda157df7513e2909e30
Author: Jonathon Henderson <[email protected]>
AuthorDate: Sun Mar 1 11:23:21 2026 +0000
feat(java): add missing methods to async TCP UsersClient (#2837)
Closes #2826
---
.../org/apache/iggy/client/async/UsersClient.java | 115 ++++++++++
.../iggy/client/async/tcp/AsyncTcpConnection.java | 56 +++++
.../iggy/client/async/tcp/UsersTcpClient.java | 95 +++++++-
.../iggy/client/async/tcp/UsersTcpClientTest.java | 239 +++++++++++++++++++++
4 files changed, 502 insertions(+), 3 deletions(-)
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/UsersClient.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/UsersClient.java
index 796e21ed6..4ccbce11e 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/UsersClient.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/UsersClient.java
@@ -19,8 +19,15 @@
package org.apache.iggy.client.async;
+import org.apache.iggy.identifier.UserId;
import org.apache.iggy.user.IdentityInfo;
+import org.apache.iggy.user.Permissions;
+import org.apache.iggy.user.UserInfo;
+import org.apache.iggy.user.UserInfoDetails;
+import org.apache.iggy.user.UserStatus;
+import java.util.List;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
@@ -53,6 +60,114 @@ import java.util.concurrent.CompletableFuture;
*/
public interface UsersClient {
+ /**
+ * Get the details of a user by the ID provided.
+ *
+ * @see #getUser(UserId)
+ */
+ default CompletableFuture<Optional<UserInfoDetails>> getUser(Long userId) {
+ return getUser(UserId.of(userId));
+ }
+
+ /**
+ * Get the details of a user by the ID provided.
+ *
+ * @param userId the ID of the user to retrieve information about.
+ * @return a {@link CompletableFuture} that completes with the user's
{@link UserInfoDetails} on success
+ */
+ CompletableFuture<Optional<UserInfoDetails>> getUser(UserId userId);
+
+ /**
+ * Get a list of the users currently registered.
+ *
+ * @return A {@link CompletableFuture} that completes with a list of
{@link UserInfo} about each of the registered users.
+ */
+ CompletableFuture<List<UserInfo>> getUsers();
+
+ /**
+ * Create a user with the details provided.
+ *
+ * @param username The username of the new user.
+ * @param password The password of the new user.
+ * @param status The status of the new user.
+ * @param permissions An optional set of permissions for the new user.
+ * @return A {@link CompletableFuture} that completes with a {@link
UserInfoDetails} that gives information about the newly created user.
+ */
+ CompletableFuture<UserInfoDetails> createUser(
+ String username, String password, UserStatus status,
Optional<Permissions> permissions);
+
+ /**
+ * Delete the user with the given ID.
+ *
+ * @see #deleteUser(UserId)
+ */
+ default CompletableFuture<Void> deleteUser(Long userId) {
+ return deleteUser(UserId.of(userId));
+ }
+
+ /**
+ * Delete the user with the given ID.
+ *
+ * @param userId The ID of the user to delete.
+ * @return A {@link CompletableFuture} that completes but yields no value.
+ */
+ CompletableFuture<Void> deleteUser(UserId userId);
+
+ /**
+ * Update the user identified by the given userId, setting (if provided)
their username and or status.
+ *
+ * @see #updateUser(UserId, Optional, Optional)
+ */
+ default CompletableFuture<Void> updateUser(Long userId, Optional<String>
username, Optional<UserStatus> status) {
+ return updateUser(UserId.of(userId), username, status);
+ }
+
+ /**
+ * Update the user identified by the given userId, setting (if provided)
their username and or status.
+ * @param userId The ID of the user to update.
+ * @param username The new username of the user, or an empty optional if
no update is required.
+ * @param status The new status of the user, or an empty optional if no
update is required.
+ * @return A {@link CompletableFuture} that completes but yields no value.
+ */
+ CompletableFuture<Void> updateUser(UserId userId, Optional<String>
username, Optional<UserStatus> status);
+
+ /**
+ * Update the permissions of the user identified by the provided userId.
+ *
+ * @see #updatePermissions(UserId, Optional)
+ */
+ default CompletableFuture<Void> updatePermissions(Long userId,
Optional<Permissions> permissions) {
+ return updatePermissions(UserId.of(userId), permissions);
+ }
+
+ /**
+ * Update the permissions of the user identified by the provided userId.
+ *
+ * @param userId The ID of the user of which to update permissions
+ * @param permissions The new permissions of the user
+ * @return A {@link CompletableFuture} that completes but yields no value.
+ */
+ CompletableFuture<Void> updatePermissions(UserId userId,
Optional<Permissions> permissions);
+
+ /**
+ * Change the password of the user identifier by the given userId.
+ *
+ * @see #changePassword(UserId, String, String)
+ */
+ default CompletableFuture<Void> changePassword(Long userId, String
currentPassword, String newPassword) {
+ return changePassword(UserId.of(userId), currentPassword, newPassword);
+ }
+
+ /**
+ * Change the password of the user identifier by the given userId.
+ *
+ * @param userId The ID of the user whose password should be changed.
+ * @param currentPassword The current password of the user
+ * @param newPassword The new password of the user
+ * @return A {@link CompletableFuture} that completes but yields no value.
+ */
+ CompletableFuture<Void> changePassword(UserId userId, String
currentPassword, String newPassword);
+
/**
* Logs in to the Iggy server with the specified credentials.
*
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/AsyncTcpConnection.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/AsyncTcpConnection.java
index d316fcf1c..a49ebc7f8 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/AsyncTcpConnection.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/AsyncTcpConnection.java
@@ -34,18 +34,23 @@ import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
+import org.apache.iggy.exception.IggyEmptyResponseException;
import org.apache.iggy.exception.IggyNotConnectedException;
import org.apache.iggy.exception.IggyServerException;
import org.apache.iggy.exception.IggyTlsException;
+import org.apache.iggy.serde.CommandCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLException;
import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
/**
* Async TCP connection using Netty for non-blocking I/O.
@@ -136,6 +141,57 @@ public class AsyncTcpConnection {
return future;
}
+ public <T> CompletableFuture<T> exchangeForEntity(
+ CommandCode commandCode, ByteBuf payload, Function<ByteBuf, T>
func) {
+ return send(commandCode, payload).thenApply(response -> {
+ try {
+ if (!response.isReadable()) {
+ throw new
IggyEmptyResponseException(commandCode.toString());
+ }
+ return func.apply(response);
+ } finally {
+ response.release();
+ }
+ });
+ }
+
+ public <T> CompletableFuture<List<T>> exchangeForList(
+ CommandCode commandCode, ByteBuf payload, Function<ByteBuf, T>
func) {
+ return send(commandCode, payload).thenApply(response -> {
+ try {
+ var result = new ArrayList<T>();
+ while (response.isReadable()) {
+ result.add(func.apply(response));
+ }
+ return result;
+ } finally {
+ response.release();
+ }
+ });
+ }
+
+ public <T> CompletableFuture<Optional<T>> exchangeForOptional(
+ CommandCode commandCode, ByteBuf payload, Function<ByteBuf, T>
func) {
+ return send(commandCode, payload).thenApply(response -> {
+ try {
+ if (response.isReadable()) {
+ return Optional.of(func.apply(response));
+ }
+ return Optional.empty();
+ } finally {
+ response.release();
+ }
+ });
+ }
+
+ public CompletableFuture<Void> sendAndRelease(CommandCode commandCode,
ByteBuf payload) {
+ return send(commandCode, payload).thenAccept(response ->
response.release());
+ }
+
+ public CompletableFuture<ByteBuf> send(CommandCode commandCode, ByteBuf
payload) {
+ return send(commandCode.getValue(), payload);
+ }
+
/**
* Sends a command asynchronously and returns the response.
* Uses Netty's EventLoop to ensure thread-safe sequential request
processing with FIFO response matching.
diff --git
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/UsersTcpClient.java
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/UsersTcpClient.java
index 7960302ec..632f45d6d 100644
---
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/UsersTcpClient.java
+++
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/UsersTcpClient.java
@@ -22,15 +22,23 @@ package org.apache.iggy.client.async.tcp;
import io.netty.buffer.Unpooled;
import org.apache.iggy.IggyVersion;
import org.apache.iggy.client.async.UsersClient;
-import org.apache.iggy.serde.BytesSerializer;
+import org.apache.iggy.identifier.UserId;
+import org.apache.iggy.serde.BytesDeserializer;
import org.apache.iggy.serde.CommandCode;
import org.apache.iggy.user.IdentityInfo;
+import org.apache.iggy.user.Permissions;
+import org.apache.iggy.user.UserInfo;
+import org.apache.iggy.user.UserInfoDetails;
+import org.apache.iggy.user.UserStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
+import static org.apache.iggy.serde.BytesSerializer.toBytes;
+
/**
* Async TCP implementation of users client.
*/
@@ -43,14 +51,95 @@ public class UsersTcpClient implements UsersClient {
this.connection = connection;
}
+ @Override
+ public CompletableFuture<Optional<UserInfoDetails>> getUser(UserId userId)
{
+ var payload = toBytes(userId);
+ return connection.exchangeForOptional(CommandCode.User.GET, payload,
BytesDeserializer::readUserInfoDetails);
+ }
+
+ @Override
+ public CompletableFuture<List<UserInfo>> getUsers() {
+ var payload = Unpooled.EMPTY_BUFFER;
+ return connection.exchangeForList(CommandCode.User.GET_ALL, payload,
BytesDeserializer::readUserInfo);
+ }
+
+ @Override
+ public CompletableFuture<UserInfoDetails> createUser(
+ String username, String password, UserStatus status,
Optional<Permissions> permissions) {
+ var payload = Unpooled.buffer();
+ payload.writeBytes(toBytes(username));
+ payload.writeBytes(toBytes(password));
+ payload.writeByte(status.asCode());
+ permissions.ifPresentOrElse(
+ perms -> {
+ payload.writeByte(1);
+ var permissionBytes = toBytes(perms);
+ payload.writeIntLE(permissionBytes.readableBytes());
+ payload.writeBytes(permissionBytes);
+ },
+ () -> payload.writeByte(0));
+
+ return connection.exchangeForEntity(CommandCode.User.CREATE, payload,
BytesDeserializer::readUserInfoDetails);
+ }
+
+ @Override
+ public CompletableFuture<Void> deleteUser(UserId userId) {
+ var payload = toBytes(userId);
+ return connection.sendAndRelease(CommandCode.User.DELETE, payload);
+ }
+
+ @Override
+ public CompletableFuture<Void> updateUser(UserId userId, Optional<String>
username, Optional<UserStatus> status) {
+ var payload = toBytes(userId);
+ username.ifPresentOrElse(
+ un -> {
+ payload.writeByte(1);
+ payload.writeBytes(toBytes(un));
+ },
+ () -> payload.writeByte(0));
+ status.ifPresentOrElse(
+ s -> {
+ payload.writeByte(1);
+ payload.writeByte(s.asCode());
+ },
+ () -> payload.writeByte(0));
+
+ return connection.sendAndRelease(CommandCode.User.UPDATE, payload);
+ }
+
+ @Override
+ public CompletableFuture<Void> updatePermissions(UserId userId,
Optional<Permissions> permissions) {
+ var payload = toBytes(userId);
+
+ permissions.ifPresentOrElse(
+ perms -> {
+ payload.writeByte(1);
+ var permissionBytes = toBytes(perms);
+ payload.writeIntLE(permissionBytes.readableBytes());
+ payload.writeBytes(permissionBytes);
+ },
+ () -> payload.writeByte(0));
+
+ return connection.sendAndRelease(CommandCode.User.UPDATE_PERMISSIONS,
payload);
+ }
+
+ @Override
+ public CompletableFuture<Void> changePassword(UserId userId, String
currentPassword, String newPassword) {
+ var payload = toBytes(userId);
+ payload.writeBytes(toBytes(currentPassword));
+ payload.writeBytes(toBytes(newPassword));
+
+ return connection.sendAndRelease(CommandCode.User.CHANGE_PASSWORD,
payload);
+ }
+
@Override
public CompletableFuture<IdentityInfo> login(String username, String
password) {
String version = IggyVersion.getInstance().getUserAgent();
String context = IggyVersion.getInstance().toString();
var payload = Unpooled.buffer();
- var usernameBytes = BytesSerializer.toBytes(username);
- var passwordBytes = BytesSerializer.toBytes(password);
+ var usernameBytes = toBytes(username);
+ var passwordBytes = toBytes(password);
payload.writeBytes(usernameBytes);
payload.writeBytes(passwordBytes);
diff --git
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/tcp/UsersTcpClientTest.java
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/tcp/UsersTcpClientTest.java
new file mode 100644
index 000000000..19194b9cb
--- /dev/null
+++
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/tcp/UsersTcpClientTest.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.iggy.client.async.tcp;
+
+import org.apache.iggy.client.BaseIntegrationTest;
+import org.apache.iggy.identifier.UserId;
+import org.apache.iggy.user.GlobalPermissions;
+import org.apache.iggy.user.IdentityInfo;
+import org.apache.iggy.user.Permissions;
+import org.apache.iggy.user.UserInfo;
+import org.apache.iggy.user.UserStatus;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UsersTcpClientTest extends BaseIntegrationTest {
+
+ private static final Logger log =
LoggerFactory.getLogger(UsersTcpClientTest.class);
+ private static final String USERNAME = "iggy";
+ private static final String PASSWORD = "iggy";
+
+ private static AsyncIggyTcpClient client;
+ private static IdentityInfo loggedInUser;
+
+ @BeforeAll
+ public static void setup() throws Exception {
+ log.info("Setting up async client for integration tests");
+ client = new AsyncIggyTcpClient(serverHost(), serverTcpPort());
+
+ // Connect and login
+ loggedInUser = client.connect()
+ .thenCompose(v -> {
+ log.info("Connected to Iggy server");
+ return client.users().login(USERNAME, PASSWORD);
+ })
+ .get(5, TimeUnit.SECONDS);
+
+ log.info("Successfully logged in as: {}", USERNAME);
+ }
+
+ @Test
+ void shouldLogIn() {
+ assertThat(loggedInUser).isNotNull();
+ assertThat(loggedInUser.userId()).isEqualTo(0L);
+ }
+
+ @Test
+ void shouldGetUserWhenUserExists() throws Exception {
+ var userDetails = client.users().getUser(0L).get(5, TimeUnit.SECONDS);
+
+ assertThat(userDetails).isPresent();
+ assertThat(userDetails.get().id()).isEqualTo(0L);
+ assertThat(userDetails.get().username()).isEqualTo(USERNAME);
+ }
+
+ @Test
+ void shouldGetEmptyOptionalWhenUserDoesNotExist() throws Exception {
+ var userDetails = client.users().getUser(123456L).get(5,
TimeUnit.SECONDS);
+
+ assertThat(userDetails).isNotPresent();
+ }
+
+ @Test
+ void shouldGetUsers() throws Exception {
+ var users = client.users().getUsers().get(5, TimeUnit.SECONDS);
+
+ assertThat(users).isNotEmpty();
+ assertThat(users).hasSize(1);
+ assertThat(users.get(0).username()).isEqualTo(USERNAME);
+ }
+
+ @Test
+ void shouldCreateAndDeleteUsers() throws Exception {
+ var users = client.users().getUsers().get(5, TimeUnit.SECONDS);
+ assertThat(users).hasSize(1);
+
+ var globalPermissions =
+ new GlobalPermissions(true, false, false, false, false, false,
false, false, false, false);
+ var permissions = Optional.of(new Permissions(globalPermissions,
Map.of()));
+
+ var foo = client.users()
+ .createUser("foo", "foo", UserStatus.Active, permissions)
+ .get(5, TimeUnit.SECONDS);
+ assertThat(foo).isNotNull();
+ assertThat(foo.permissions()).isPresent();
+
assertThat(foo.permissions().get().global()).isEqualTo(globalPermissions);
+
+ var bar = client.users()
+ .createUser("bar", "bar", UserStatus.Active, permissions)
+ .get(5, TimeUnit.SECONDS);
+ assertThat(bar).isNotNull();
+ assertThat(bar.permissions()).isPresent();
+
assertThat(bar.permissions().get().global()).isEqualTo(globalPermissions);
+
+ users = client.users().getUsers().get(5, TimeUnit.SECONDS);
+
+ assertThat(users).hasSize(3);
+
assertThat(users).map(UserInfo::username).containsExactlyInAnyOrder(USERNAME,
"foo", "bar");
+
+ client.users().deleteUser(foo.id()).get(5, TimeUnit.SECONDS);
+ users = client.users().getUsers().get(5, TimeUnit.SECONDS);
+ assertThat(users).hasSize(2);
+
+ client.users().deleteUser(UserId.of(bar.id())).get(5,
TimeUnit.SECONDS);
+ users = client.users().getUsers().get(5, TimeUnit.SECONDS);
+ assertThat(users).hasSize(1);
+ }
+
+ @Test
+ void shouldUpdateUser() throws Exception {
+ var created = client.users()
+ .createUser("test", "test", UserStatus.Active,
Optional.empty())
+ .get(5, TimeUnit.SECONDS);
+
+ client.users()
+ .updateUser(created.id(), Optional.of("foo"),
Optional.of(UserStatus.Inactive))
+ .get(5, TimeUnit.SECONDS);
+
+ var user = client.users().getUser(created.id()).get(5,
TimeUnit.SECONDS);
+ assertThat(user).isPresent();
+ assertThat(user.get().username()).isEqualTo("foo");
+ assertThat(user.get().status()).isEqualTo(UserStatus.Inactive);
+
+ client.users()
+ .updateUser(created.id(), Optional.empty(),
Optional.of(UserStatus.Active))
+ .get(5, TimeUnit.SECONDS);
+
+ user = client.users().getUser(created.id()).get(5, TimeUnit.SECONDS);
+ assertThat(user).isPresent();
+ assertThat(user.get().username()).isEqualTo("foo");
+ assertThat(user.get().status()).isEqualTo(UserStatus.Active);
+
+ client.users()
+ .updateUser(UserId.of(created.id()), Optional.of("test"),
Optional.empty())
+ .get(5, TimeUnit.SECONDS);
+
+ user = client.users().getUser(created.id()).get(5, TimeUnit.SECONDS);
+ assertThat(user).isPresent();
+ assertThat(user.get().username()).isEqualTo("test");
+ assertThat(user.get().status()).isEqualTo(UserStatus.Active);
+
+ client.users().deleteUser(created.id()).get(5, TimeUnit.SECONDS);
+
+ var users = client.users().getUsers().get(5, TimeUnit.SECONDS);
+ assertThat(users).hasSize(1);
+ }
+
+ @Test
+ void shouldUpdatePermissions() throws Exception {
+ var created = client.users()
+ .createUser("test", "test", UserStatus.Active,
Optional.empty())
+ .get(5, TimeUnit.SECONDS);
+
+ var allPermissions = new Permissions(
+ new GlobalPermissions(true, true, true, true, true, true,
true, true, true, true), Map.of());
+ var noPermissions = new Permissions(
+ new GlobalPermissions(false, false, false, false, false,
false, false, false, false, false), Map.of());
+
+ client.users()
+ .updatePermissions(created.id(), Optional.of(allPermissions))
+ .get(5, TimeUnit.SECONDS);
+
+ var user = client.users().getUser(created.id()).get(5,
TimeUnit.SECONDS);
+ assertThat(user).isPresent();
+ assertThat(user.get().permissions()).isPresent();
+ assertThat(user.get().permissions().get()).isEqualTo(allPermissions);
+
+ client.users()
+ .updatePermissions(UserId.of(created.id()),
Optional.of(noPermissions))
+ .get(5, TimeUnit.SECONDS);
+
+ user = client.users().getUser(created.id()).get(5, TimeUnit.SECONDS);
+ assertThat(user).isPresent();
+ assertThat(user.get().permissions()).isPresent();
+ assertThat(user.get().permissions().get()).isEqualTo(noPermissions);
+
+ client.users().deleteUser(created.id()).get(5, TimeUnit.SECONDS);
+
+ var users = client.users().getUsers().get(5, TimeUnit.SECONDS);
+ assertThat(users).hasSize(1);
+ }
+
+ @Test
+ void shouldChangePassword() throws Exception {
+ var newUser = client.users()
+ .createUser("test", "test", UserStatus.Active,
Optional.empty())
+ .get(5, TimeUnit.SECONDS);
+ client.users().logout().get(5, TimeUnit.SECONDS);
+
+ var identity = client.users().login("test", "test").get(5,
TimeUnit.SECONDS);
+ assertThat(identity).isNotNull();
+ assertThat(identity.userId()).isEqualTo(newUser.id());
+
+ client.users().changePassword(identity.userId(), "test",
"foobar").get(5, TimeUnit.SECONDS);
+ client.users().logout().get(5, TimeUnit.SECONDS);
+ identity = client.users().login("test", "foobar").get(5,
TimeUnit.SECONDS);
+ assertThat(identity).isNotNull();
+ assertThat(identity.userId()).isEqualTo(newUser.id());
+
+ client.users()
+ .changePassword(UserId.of(identity.userId()), "foobar",
"barfoo")
+ .get(5, TimeUnit.SECONDS);
+ client.users().logout().get(5, TimeUnit.SECONDS);
+ identity = client.users().login("test", "barfoo").get(5,
TimeUnit.SECONDS);
+ assertThat(identity).isNotNull();
+ assertThat(identity.userId()).isEqualTo(newUser.id());
+
+ client.users().logout().get(5, TimeUnit.SECONDS);
+ client.users().login(USERNAME, PASSWORD).get(5, TimeUnit.SECONDS);
+ client.users().deleteUser(newUser.id()).get(5, TimeUnit.SECONDS);
+
+ var users = client.users().getUsers().get(5, TimeUnit.SECONDS);
+ assertThat(users).hasSize(1);
+ }
+}