This is an automated email from the ASF dual-hosted git repository.

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 4845993e7bccc382be9e69e1eaa70a48e7b7cd22
Author: Benoit TELLIER <[email protected]>
AuthorDate: Tue Mar 10 09:47:23 2026 +0100

    JAMES-4189 Ability to list users by domain
---
 .../modules/servers/partials/operate/webadmin.adoc |  22 ++++
 .../store/quota/DefaultQuotaChangeNotifier.java    |   8 +-
 .../mailbox/UsersRepositoryUsernameSupplier.java   |   6 ++
 .../org/apache/james/user/api/UsersRepository.java |   7 ++
 .../james/user/ldap/ReadOnlyLDAPUsersDAO.java      |  46 +++++++++
 .../user/ldap/ReadOnlyUsersLDAPRepositoryTest.java |  20 ++++
 .../java/org/apache/james/user/lib/UsersDAO.java   |   8 ++
 .../apache/james/user/lib/UsersRepositoryImpl.java |   9 ++
 .../james/user/lib/UsersRepositoryContract.java    |  31 ++++++
 .../james/user/postgres/PostgresUsersDAO.java      |  11 ++
 .../james/webadmin/routes/DomainsRoutes.java       |  29 ++++++
 .../james/webadmin/routes/DomainsRoutesTest.java   | 113 +++++++++++++++++++++
 12 files changed, 307 insertions(+), 3 deletions(-)

diff --git a/docs/modules/servers/partials/operate/webadmin.adoc 
b/docs/modules/servers/partials/operate/webadmin.adoc
index 3efd9a83e4..11b1f955fa 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -460,6 +460,28 @@ Response codes:
 
 * 200: The domain list was successfully retrieved
 
+=== Get the list of users for a domain
+
+....
+curl -XGET http://ip:port/domains/domain.tld/users
+....
+
+Will return the list of users for this domain:
+
+....
+[
+  "[email protected]",
+  ...
+]
+....
+
+* 200: The domain aliases was successfully retrieved
+* 400: domain.tld has an invalid syntax
+* 404: domain.tld is not part of handled domains and does
+not have local domains as aliases.
+
+Only supported when virtualHosting is enabled.
+
 === Get the list of aliases for a domain
 
 ....
diff --git 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/DefaultQuotaChangeNotifier.java
 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/DefaultQuotaChangeNotifier.java
index ec1904511d..2d4eb5b6de 100644
--- 
a/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/DefaultQuotaChangeNotifier.java
+++ 
b/mailbox/store/src/main/java/org/apache/james/mailbox/store/quota/DefaultQuotaChangeNotifier.java
@@ -43,7 +43,10 @@ import reactor.core.publisher.Mono;
 public class DefaultQuotaChangeNotifier implements QuotaChangeNotifier {
     @FunctionalInterface
     public interface UsernameSupplier extends Supplier<Flux<Username>> {
-
+        default Flux<Username> getUsersForDomain(Domain domain) {
+            return get()
+                .filter(username -> 
username.getDomainPart().map(domain::equals).orElse(false));
+        }
     }
 
     private static final ImmutableSet<RegistrationKey> NO_REGISTRATION_KEYS = 
ImmutableSet.of();
@@ -80,9 +83,8 @@ public class DefaultQuotaChangeNotifier implements 
QuotaChangeNotifier {
 
     @Override
     public Publisher<Void> notifyUpdate(Domain domain) {
-        return usernameSupplier.get()
+        return usernameSupplier.getUsersForDomain(domain)
             .map(quotaRootResolver::forUser)
-            .filter(user -> user.getDomain().map(domain::equals).orElse(false))
             .concatMap(this::notifyUpdate)
             .then();
     }
diff --git 
a/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/UsersRepositoryUsernameSupplier.java
 
b/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/UsersRepositoryUsernameSupplier.java
index 08b60ba398..fcc886efbd 100644
--- 
a/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/UsersRepositoryUsernameSupplier.java
+++ 
b/server/container/mailbox-adapter/src/main/java/org/apache/james/adapter/mailbox/UsersRepositoryUsernameSupplier.java
@@ -21,6 +21,7 @@ package org.apache.james.adapter.mailbox;
 
 import jakarta.inject.Inject;
 
+import org.apache.james.core.Domain;
 import org.apache.james.core.Username;
 import org.apache.james.mailbox.store.quota.DefaultQuotaChangeNotifier;
 import org.apache.james.user.api.UsersRepository;
@@ -39,4 +40,9 @@ public class UsersRepositoryUsernameSupplier implements 
DefaultQuotaChangeNotifi
     public Flux<Username> get() {
         return Flux.from(usersRepository.listReactive());
     }
+
+    @Override
+    public Flux<Username> getUsersForDomain(Domain domain) {
+        return Flux.from(usersRepository.listUsersOfADomainReactive(domain));
+    }
 }
diff --git 
a/server/data/data-api/src/main/java/org/apache/james/user/api/UsersRepository.java
 
b/server/data/data-api/src/main/java/org/apache/james/user/api/UsersRepository.java
index 3edf2c303a..16fabdab51 100644
--- 
a/server/data/data-api/src/main/java/org/apache/james/user/api/UsersRepository.java
+++ 
b/server/data/data-api/src/main/java/org/apache/james/user/api/UsersRepository.java
@@ -188,6 +188,13 @@ public interface UsersRepository {
     }
 
     default Publisher<Username> listUsersOfADomainReactive(Domain domain) {
+        try {
+            if (!supportVirtualHosting()) {
+                return Flux.error(new 
IllegalStateException("listUsersOfADomainReactive is not supported when virtual 
hosting is disabled"));
+            }
+        } catch (UsersRepositoryException e) {
+            return Flux.error(e);
+        }
         return Flux.from(listReactive())
             .filter(username -> username.getDomainPart()
                 .map(domain::equals)
diff --git 
a/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/ReadOnlyLDAPUsersDAO.java
 
b/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/ReadOnlyLDAPUsersDAO.java
index 6b749be5b1..70bb67f675 100644
--- 
a/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/ReadOnlyLDAPUsersDAO.java
+++ 
b/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/ReadOnlyLDAPUsersDAO.java
@@ -60,6 +60,9 @@ import com.unboundid.ldap.sdk.SearchResult;
 import com.unboundid.ldap.sdk.SearchResultEntry;
 import com.unboundid.ldap.sdk.SearchScope;
 
+import reactor.core.publisher.Flux;
+import reactor.core.scheduler.Schedulers;
+
 public class ReadOnlyLDAPUsersDAO implements UsersDAO, Configurable {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ReadOnlyLDAPUsersDAO.class);
 
@@ -338,6 +341,49 @@ public class ReadOnlyLDAPUsersDAO implements UsersDAO, 
Configurable {
     }
 
 
+    @Override
+    public Flux<Username> listUsersOfADomainReactive(Domain domain) {
+        return Flux.fromStream(() -> {
+                try {
+                    return getUsernamesForDomain(domain);
+                } catch (LDAPException e) {
+                    throw new RuntimeException(e);
+                }
+            })
+            .subscribeOn(Schedulers.boundedElastic());
+    }
+
+    private Stream<Username> getUsernamesForDomain(Domain domain) throws 
LDAPException {
+        if (!ldapConfiguration.getRestriction().isActivated()) {
+            return getUnrestrictedUsernamesForDomain(domain);
+        }
+        return buildUserCollection(getValidUserDNs()).stream()
+            .map(ReadOnlyLDAPUser::getUserName)
+            .filter(username -> 
username.getDomainPart().map(domain::equals).orElse(false))
+            .distinct();
+    }
+
+    private Stream<Username> getUnrestrictedUsernamesForDomain(Domain domain) 
throws LDAPException {
+        String usernameAttribute = 
ldapConfiguration.getUsernameAttribute().orElse(ldapConfiguration.getUserIdAttribute());
+        Filter domainFilter = Filter.createSubstringFilter(usernameAttribute, 
null, null, "@" + domain.asString());
+        SearchRequest searchRequest = new SearchRequest(userBase(domain), 
SearchScope.SUB,
+            Filter.createANDFilter(listingFilter, domainFilter), 
usernameAttribute);
+        return ldapConnectionPool.search(searchRequest)
+            .getSearchEntries().stream()
+            .flatMap(entry -> 
Optional.ofNullable(entry.getAttribute(usernameAttribute)).stream())
+            .map(Attribute::getValue)
+            .flatMap(name -> {
+                try {
+                    return Stream.of(Username.of(name));
+                } catch (Exception e) {
+                    LOGGER.warn("Invalid username in the LDAP: {}", name, e);
+                    return Stream.empty();
+                }
+            })
+            .filter(username -> 
username.getDomainPart().map(domain::equals).orElse(false))
+            .distinct();
+    }
+
     private Collection<DN> getValidUserDNs() throws LDAPException {
         Set<DN> userDNs = getAllUsersDNFromLDAP();
         Collection<DN> validUserDNs;
diff --git 
a/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
 
b/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
index 6673bc1361..440f2256e0 100644
--- 
a/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
+++ 
b/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
@@ -35,6 +35,7 @@ import 
org.apache.commons.configuration2.HierarchicalConfiguration;
 import org.apache.commons.configuration2.ex.ConversionException;
 import org.apache.commons.configuration2.plist.PropertyListConfiguration;
 import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.core.Domain;
 import org.apache.james.core.Username;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.mock.SimpleDomainList;
@@ -56,6 +57,8 @@ import org.slf4j.LoggerFactory;
 import com.google.common.collect.ImmutableList;
 import com.unboundid.ldap.sdk.LDAPException;
 
+import reactor.core.publisher.Flux;
+
 class ReadOnlyUsersLDAPRepositoryTest {
 
     static final Logger LOGGER = 
LoggerFactory.getLogger(ReadOnlyUsersLDAPRepositoryTest.class);
@@ -347,6 +350,22 @@ class ReadOnlyUsersLDAPRepositoryTest {
             
assertThat(usersRepository.contains(usersRepository.getUsername(JAMES_USER_MAIL.asMailAddress()))).isTrue();
         }
 
+        @Test
+        void listUsersOfADomainReactiveShouldReturnUsersOfDomain() throws 
Exception {
+            
assertThat(Flux.from(usersRepository.listUsersOfADomainReactive(Domain.of(DOMAIN)))
+                .collectList()
+                .block())
+                .containsOnly(JAMES_USER_MAIL);
+        }
+
+        @Test
+        void listUsersOfADomainReactiveShouldReturnEmptyForUnknownDomain() 
throws Exception {
+            
assertThat(Flux.from(usersRepository.listUsersOfADomainReactive(Domain.of("other.org")))
+                .collectList()
+                .block())
+                .isEmpty();
+        }
+
         @Disabled("JAMES-3088 Users are provisioned by default from 
Dockerfile, cannot setup this test case," +
             "See @link{ReadOnlyUsersLDAPRepositoryEmptyListTest}")
         @Override
@@ -580,6 +599,7 @@ class ReadOnlyUsersLDAPRepositoryTest {
         PropertyListConfiguration configuration = 
baseConfiguration(ldapContainer);
         configuration.addProperty("[@userIdAttribute]", "mail");
         configuration.addProperty("supportsVirtualHosting", true);
+        configuration.addProperty("enableVirtualHosting", true);
         administrator.ifPresent(username -> 
configuration.addProperty("[@administratorId]", username.asString()));
         return configuration;
     }
diff --git 
a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersDAO.java
 
b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersDAO.java
index d345406292..c333f3af76 100644
--- 
a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersDAO.java
+++ 
b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersDAO.java
@@ -22,6 +22,7 @@ package org.apache.james.user.lib;
 import java.util.Iterator;
 import java.util.Optional;
 
+import org.apache.james.core.Domain;
 import org.apache.james.core.Username;
 import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.user.api.model.User;
@@ -64,5 +65,12 @@ public interface UsersDAO {
         }).subscribeOn(Schedulers.boundedElastic());
     }
 
+    default Publisher<Username> listUsersOfADomainReactive(Domain domain) {
+        return Flux.from(listReactive())
+            .filter(username -> username.getDomainPart()
+                .map(domain::equals)
+                .orElse(false));
+    }
+
     void addUser(Username username, String password) throws 
UsersRepositoryException;
 }
diff --git 
a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
 
b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
index b27bc3bc3d..cf0beacad3 100644
--- 
a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
+++ 
b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
@@ -52,6 +52,7 @@ import com.github.fge.lambdas.Throwing;
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableSet;
 
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 public class UsersRepositoryImpl<T extends UsersDAO> implements 
UsersRepository, Configurable {
@@ -262,6 +263,14 @@ public class UsersRepositoryImpl<T extends UsersDAO> 
implements UsersRepository,
         return usersDAO.listReactive();
     }
 
+    @Override
+    public Publisher<Username> listUsersOfADomainReactive(Domain domain) {
+        if (!virtualHosting) {
+            return Flux.error(new 
IllegalStateException("listUsersOfADomainReactive is not supported when virtual 
hosting is disabled"));
+        }
+        return usersDAO.listUsersOfADomainReactive(domain);
+    }
+
     @Override
     public boolean supportVirtualHosting() {
         return virtualHosting;
diff --git 
a/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
 
b/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
index 61f062c453..fbad8b98eb 100644
--- 
a/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
+++ 
b/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
@@ -582,6 +582,28 @@ public interface UsersRepositoryContract {
                 .containsOnly(Username.of("[email protected]"));
         }
 
+        @Test
+        default void 
listUsersOfADomainShouldReturnEmptyWhenNoneInDomain(TestSystem testSystem) 
throws Exception {
+            testSystem.domainList.addDomain(Domain.of("empty.tld"));
+
+            
assertThat(Flux.from(testee().listUsersOfADomainReactive(Domain.of("empty.tld")))
+                .collectList()
+                .block())
+                .isEmpty();
+        }
+
+        @Test
+        default void listUsersOfADomainShouldReturnAllUsersOfDomain(TestSystem 
testSystem) throws Exception {
+            testSystem.domainList.addDomain(Domain.of("domain.tld"));
+            testee().addUser(Username.of("[email protected]"), "password");
+            testee().addUser(Username.of("[email protected]"), "password");
+
+            
assertThat(Flux.from(testee().listUsersOfADomainReactive(Domain.of("domain.tld")))
+                .collectList()
+                .block())
+                .containsExactlyInAnyOrder(Username.of("[email protected]"), 
Username.of("[email protected]"));
+        }
+
         @Test
         default void 
addUserShouldThrowWhenUserDoesNotBelongToDomainList(TestSystem testSystem) {
             assertThatThrownBy(() -> 
testee().addUser(testSystem.userWithUnknownDomain, "password"))
@@ -760,6 +782,15 @@ public interface UsersRepositoryContract {
             assertThatCode(() -> testee().assertValid(withOutDomainPart))
                 .doesNotThrowAnyException();
         }
+
+        @Test
+        default void 
listUsersOfADomainShouldFailWhenVirtualHostingIsDisabled(TestSystem testSystem) 
{
+            assertThatThrownBy(() -> 
Flux.from(testee().listUsersOfADomainReactive(TestSystem.DOMAIN))
+                .collectList()
+                .block())
+                .isInstanceOf(IllegalStateException.class)
+                .hasMessageContaining("listUsersOfADomainReactive is not 
supported when virtual hosting is disabled");
+        }
     }
 
     interface WithVirtualHostingContract extends 
WithVirtualHostingReadOnlyContract, WithVirtualHostingReadWriteContract {
diff --git 
a/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUsersDAO.java
 
b/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUsersDAO.java
index 1340c87703..ba7c49e631 100644
--- 
a/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUsersDAO.java
+++ 
b/server/data/data-postgres/src/main/java/org/apache/james/user/postgres/PostgresUsersDAO.java
@@ -38,6 +38,7 @@ import jakarta.inject.Inject;
 import jakarta.inject.Named;
 
 import org.apache.james.backends.postgres.utils.PostgresExecutor;
+import org.apache.james.core.Domain;
 import org.apache.james.core.Username;
 import org.apache.james.user.api.AlreadyExistInUsersRepositoryException;
 import org.apache.james.user.api.UsersRepositoryException;
@@ -146,6 +147,16 @@ public class PostgresUsersDAO implements UsersDAO {
             .map(record -> Username.of(record.get(USERNAME)));
     }
 
+    @Override
+    public Flux<Username> listUsersOfADomainReactive(Domain domain) {
+        String domainPattern = "%@" + domain.asString();
+        return postgresExecutor.executeRows(dslContext -> Flux.from(
+                dslContext.select(USERNAME)
+                    .from(TABLE_NAME)
+                    .where(USERNAME.like(domainPattern))), EAGER_FETCH)
+            .map(record -> Username.of(record.get(USERNAME)));
+    }
+
     @Override
     public void addUser(Username username, String password) {
         DefaultUser user = new DefaultUser(username, algorithm, algorithm);
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
index b3951145e7..a1108e5926 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
@@ -21,11 +21,13 @@ package org.apache.james.webadmin.routes;
 
 import static org.apache.james.webadmin.Constants.SEPARATOR;
 
+import java.util.List;
 import java.util.stream.Collectors;
 
 import jakarta.inject.Inject;
 
 import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
 import org.apache.james.domainlist.api.AutoDetectedDomainRemovalException;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.DomainListException;
@@ -33,6 +35,7 @@ import 
org.apache.james.rrt.api.RecipientRewriteTableException;
 import org.apache.james.rrt.api.SameSourceAndDestinationException;
 import org.apache.james.task.TaskManager;
 import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.dto.DomainAliasResponse;
 import org.apache.james.webadmin.service.DeleteUserDataService;
@@ -51,6 +54,7 @@ import org.slf4j.LoggerFactory;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
 
+import reactor.core.publisher.Flux;
 import spark.HaltException;
 import spark.Request;
 import spark.Response;
@@ -67,6 +71,8 @@ public class DomainsRoutes implements Routes {
     private static final String SPECIFIC_DOMAIN = DOMAINS + SEPARATOR + 
DOMAIN_NAME;
     private static final String ALIASES = "aliases";
     private static final String DOMAIN_ALIASES = SPECIFIC_DOMAIN + SEPARATOR + 
ALIASES;
+    private static final String USERS = "users";
+    private static final String DOMAIN_USERS = SPECIFIC_DOMAIN + SEPARATOR + 
USERS;
     private static final String DELETE_ALL_USERS_DATA_OF_A_DOMAIN_PATH = 
"/domains/:domainName";
     private static final String SPECIFIC_ALIAS = DOMAINS + SEPARATOR + 
DESTINATION_DOMAIN + SEPARATOR + ALIASES + SEPARATOR + SOURCE_DOMAIN;
     private static final TaskRegistrationKey DELETE_USERS_DATA = 
TaskRegistrationKey.of("deleteData");
@@ -108,6 +114,7 @@ public class DomainsRoutes implements Routes {
         defineListAliases(service);
         defineAddAlias(service);
         defineRemoveAlias(service);
+        defineListUsersOfDomain(service);
 
         // delete data of all users of a domain
         service.post(DELETE_ALL_USERS_DATA_OF_A_DOMAIN_PATH, 
deleteAllUsersData(), jsonTransformer);
@@ -148,6 +155,28 @@ public class DomainsRoutes implements Routes {
         service.get(DOMAIN_ALIASES, this::listDomainAliases, jsonTransformer);
     }
 
+    public void defineListUsersOfDomain(Service service) {
+        service.get(DOMAIN_USERS, this::listUsersOfDomain, jsonTransformer);
+    }
+
+    private List<String> listUsersOfDomain(Request request, Response response) 
throws UsersRepositoryException, DomainListException {
+        if (!usersRepository.supportVirtualHosting()) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.METHOD_NOT_ALLOWED_405)
+                .type(ErrorType.WRONG_STATE)
+                .message("Virtual hosting must be enabled to list users by 
domain")
+                .haltError();
+        }
+        Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
+        if (!domainList.containsDomain(domain)) {
+            throw domainNotFound(domain);
+        }
+        return Flux.from(usersRepository.listUsersOfADomainReactive(domain))
+            .map(Username::asString)
+            .collectList()
+            .block();
+    }
+
     public void defineRemoveAlias(Service service) {
         service.delete(SPECIFIC_ALIAS, this::removeDomainAlias, 
jsonTransformer);
     }
diff --git 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
index 9804973478..f7b583165f 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
@@ -107,6 +107,20 @@ class DomainsRoutesTest {
     private WebAdminServer webAdminServer;
     private MemoryUsersRepository usersRepository;
 
+    private void createServerWithoutVirtualHosting(DomainList domainList) {
+        MemoryTaskManager taskManager = new MemoryTaskManager(new 
Hostname("foo"));
+        DomainAliasService domainAliasService = new DomainAliasService(new 
MemoryRecipientRewriteTable(), domainList);
+        usersRepository = 
MemoryUsersRepository.withoutVirtualHosting(domainList);
+        webAdminServer = WebAdminUtils.createWebAdminServer(new 
DomainsRoutes(domainList, domainAliasService, new JsonTransformer(),
+                    new DeleteUserDataService(Set.of()), usersRepository, 
taskManager))
+            .start();
+
+        RestAssured.requestSpecification = 
WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .setBasePath(DomainsRoutes.DOMAINS)
+            .setUrlEncodingEnabled(false)
+            .build();
+    }
+
     private void createServer(DomainList domainList) {
         MemoryTaskManager taskManager = new MemoryTaskManager(new 
Hostname("foo"));
         DomainAliasService domainAliasService = new DomainAliasService(new 
MemoryRecipientRewriteTable(), domainList);
@@ -746,6 +760,74 @@ class DomainsRoutesTest {
             }
         }
 
+        @Nested
+        class DomainUsers {
+
+            @Test
+            void getUsersOfDomainShouldReturnEmptyWhenNone() {
+                with().put(DOMAIN);
+
+                when()
+                    .get(DOMAIN + "/users")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body(".", hasSize(0));
+            }
+
+            @Test
+            void getUsersOfDomainShouldReturnUsersWhenSomeExist() throws 
Exception {
+                with().put(DOMAIN);
+                usersRepository.addUser(Username.of("user1@" + DOMAIN), 
"password");
+                usersRepository.addUser(Username.of("user2@" + DOMAIN), 
"password");
+
+                when()
+                    .get(DOMAIN + "/users")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body(".", containsInAnyOrder("user1@" + DOMAIN, "user2@" 
+ DOMAIN));
+            }
+
+            @Test
+            void getUsersOfDomainShouldNotReturnUsersOfOtherDomain() throws 
Exception {
+                with().put(DOMAIN);
+                with().put(ALIAS_DOMAIN);
+                usersRepository.addUser(Username.of("user1@" + DOMAIN), 
"password");
+                usersRepository.addUser(Username.of("user2@" + ALIAS_DOMAIN), 
"password");
+
+                when()
+                    .get(DOMAIN + "/users")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.OK_200)
+                    .body(".", containsInAnyOrder("user1@" + DOMAIN));
+            }
+
+            @Test
+            void getUsersOfDomainShouldReturnNotFoundWhenDomainDoesNotExist() {
+                when()
+                    .get(DOMAIN + "/users")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.NOT_FOUND_404)
+                    .body("statusCode", is(HttpStatus.NOT_FOUND_404))
+                    .body("type", is("InvalidArgument"))
+                    .body("message", is("The domain list does not contain: " + 
DOMAIN));
+            }
+
+            @Test
+            void getUsersOfDomainShouldReturnBadRequestWhenDomainIsInvalid() {
+                when()
+                    .get("invalid@domain/users")
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.BAD_REQUEST_400)
+                    .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+                    .body("type", is("InvalidArgument"));
+            }
+        }
+
     }
 
     @Nested
@@ -843,6 +925,37 @@ class DomainsRoutesTest {
 
     }
 
+    @Nested
+    class DomainUsersWithoutVirtualHosting {
+
+        @BeforeEach
+        void setUp() throws Exception {
+            DNSService dnsService = mock(DNSService.class);
+            when(dnsService.getHostName(any())).thenReturn("localhost");
+            
when(dnsService.getLocalHost()).thenReturn(InetAddress.getByName("localhost"));
+
+            MemoryDomainList domainList = new MemoryDomainList(dnsService);
+            domainList.configure(DomainListConfiguration.builder()
+                .autoDetect(false)
+                .autoDetectIp(false)
+                .build());
+            domainList.addDomain(Domain.of(DOMAIN));
+            createServerWithoutVirtualHosting(domainList);
+        }
+
+        @Test
+        void getUsersOfDomainShouldReturn405WhenVirtualHostingDisabled() {
+            when()
+                .get(DOMAIN + "/users")
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.METHOD_NOT_ALLOWED_405)
+                .body("statusCode", is(HttpStatus.METHOD_NOT_ALLOWED_405))
+                .body("type", is("WrongState"))
+                .body("message", is("Virtual hosting must be enabled to list 
users by domain"));
+        }
+    }
+
     @Nested
     class DetectedDomainHandling {
         @BeforeEach


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to