From fdf878e4f29b7240919ad1c50a98b97ee65e001d Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Sun, 24 Mar 2024 21:21:02 +0100 Subject: [PATCH] feat: Add basic tenant details APIs Signed-off-by: Thomas Vitale --- .../multitenancy/tenantdetails/Tenant.java | 81 +++++++++++++++++++ .../tenantdetails/TenantDetails.java | 28 +++++++ .../tenantdetails/TenantDetailsService.java | 17 ++++ .../tenantdetails/package-info.java | 6 ++ .../tenantdetails/TenantTests.java | 31 +++++++ .../MultitenancyCoreAutoConfiguration.java | 14 +++- .../PropertiesTenantDetailsService.java | 39 +++++++++ .../TenantDetailsProperties.java | 50 ++++++++++++ .../core/tenantdetails/package-info.java | 6 ++ .../HttpTenantResolutionConfiguration.java | 5 +- ...itional-spring-configuration-metadata.json | 4 + ...ultitenancyCoreAutoConfigurationTests.java | 15 ++++ .../PropertiesTenantDetailsServiceTests.java | 51 ++++++++++++ .../context/filters/TenantContextFilter.java | 30 +++++-- .../filters/TenantContextFilterTests.java | 64 +++++++++++++-- 15 files changed, 426 insertions(+), 15 deletions(-) create mode 100644 arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/Tenant.java create mode 100644 arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/TenantDetails.java create mode 100644 arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/TenantDetailsService.java create mode 100644 arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/package-info.java create mode 100644 arconia-core/src/test/java/io/arconia/core/multitenancy/tenantdetails/TenantTests.java create mode 100644 arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/PropertiesTenantDetailsService.java create mode 100644 arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/TenantDetailsProperties.java create mode 100644 arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/package-info.java create mode 100644 arconia-spring-boot-autoconfigure/src/test/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/PropertiesTenantDetailsServiceTests.java diff --git a/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/Tenant.java b/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/Tenant.java new file mode 100644 index 0000000..e6051aa --- /dev/null +++ b/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/Tenant.java @@ -0,0 +1,81 @@ +package io.arconia.core.multitenancy.tenantdetails; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Default implementation to hold tenant details. + */ +public class Tenant implements TenantDetails { + + private final String identifier; + + private final boolean enabled; + + private final Map attributes; + + public Tenant(String identifier, boolean enabled, Map attributes) { + Assert.hasText(identifier, "identifier cannot be null or empty"); + Assert.notNull(attributes, "attributes cannot be null"); + + this.identifier = identifier; + this.enabled = enabled; + this.attributes = attributes; + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public Map getAttributes() { + return attributes; + } + + public static Builder create() { + return new Builder(); + } + + public static class Builder { + + private String identifier; + + private boolean enabled; + + private Map attributes = new HashMap<>(); + + public Builder identifier(String identifier) { + this.identifier = identifier; + return this; + } + + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder attributes(Map attributes) { + this.attributes = attributes; + return this; + } + + public Builder addAttribute(String key, Object value) { + attributes.put(key, value); + return this; + } + + public Tenant build() { + return new Tenant(identifier, enabled, attributes); + } + + } + +} diff --git a/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/TenantDetails.java b/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/TenantDetails.java new file mode 100644 index 0000000..b06baf5 --- /dev/null +++ b/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/TenantDetails.java @@ -0,0 +1,28 @@ +package io.arconia.core.multitenancy.tenantdetails; + +import java.io.Serializable; +import java.util.Map; + +/** + * Provides core tenant information. + */ +public interface TenantDetails extends Serializable { + + /** + * Identifier for the tenant. + */ + String getIdentifier(); + + /** + * Whether the tenant is enabled. + */ + boolean isEnabled(); + + /** + * Additional information about the tenant. + */ + default Map getAttributes() { + return Map.of(); + } + +} diff --git a/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/TenantDetailsService.java b/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/TenantDetailsService.java new file mode 100644 index 0000000..e4e58d4 --- /dev/null +++ b/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/TenantDetailsService.java @@ -0,0 +1,17 @@ +package io.arconia.core.multitenancy.tenantdetails; + +import java.util.List; + +import org.springframework.lang.Nullable; + +/** + * Loads tenant-specific data. It is used throughout the framework as a tenant DAO. + */ +public interface TenantDetailsService { + + List loadAllTenants(); + + @Nullable + TenantDetails loadTenantByIdentifier(String identifier); + +} diff --git a/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/package-info.java b/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/package-info.java new file mode 100644 index 0000000..5bdac34 --- /dev/null +++ b/arconia-core/src/main/java/io/arconia/core/multitenancy/tenantdetails/package-info.java @@ -0,0 +1,6 @@ +@NonNullApi +@NonNullFields +package io.arconia.core.multitenancy.tenantdetails; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/arconia-core/src/test/java/io/arconia/core/multitenancy/tenantdetails/TenantTests.java b/arconia-core/src/test/java/io/arconia/core/multitenancy/tenantdetails/TenantTests.java new file mode 100644 index 0000000..0056a04 --- /dev/null +++ b/arconia-core/src/test/java/io/arconia/core/multitenancy/tenantdetails/TenantTests.java @@ -0,0 +1,31 @@ +package io.arconia.core.multitenancy.tenantdetails; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link Tenant}. + */ +class TenantTests { + + @Test + void whenIdentifierIsNullThenThrow() { + assertThatThrownBy(() -> Tenant.create().build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("identifier cannot be null or empty"); + } + + @Test + void whenIdentifierIsEmptyThenThrow() { + assertThatThrownBy(() -> Tenant.create().identifier("").build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("identifier cannot be null or empty"); + } + + @Test + void whenAttributesIsNullThenThrow() { + assertThatThrownBy(() -> Tenant.create().identifier("acme").attributes(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("attributes cannot be null"); + } + +} diff --git a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/MultitenancyCoreAutoConfiguration.java b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/MultitenancyCoreAutoConfiguration.java index a6b582e..6c8d6d6 100644 --- a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/MultitenancyCoreAutoConfiguration.java +++ b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/MultitenancyCoreAutoConfiguration.java @@ -7,6 +7,8 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; +import io.arconia.autoconfigure.multitenancy.core.tenantdetails.PropertiesTenantDetailsService; +import io.arconia.autoconfigure.multitenancy.core.tenantdetails.TenantDetailsProperties; import io.arconia.core.multitenancy.cache.DefaultTenantKeyGenerator; import io.arconia.core.multitenancy.cache.TenantKeyGenerator; import io.arconia.core.multitenancy.context.events.HolderTenantContextEventListener; @@ -15,12 +17,14 @@ import io.arconia.core.multitenancy.context.resolvers.FixedTenantResolver; import io.arconia.core.multitenancy.events.DefaultTenantEventPublisher; import io.arconia.core.multitenancy.events.TenantEventPublisher; +import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService; /** * Auto-configuration for core multitenancy. */ @AutoConfiguration -@EnableConfigurationProperties({ FixedTenantResolutionProperties.class, TenantManagementProperties.class }) +@EnableConfigurationProperties({ FixedTenantResolutionProperties.class, TenantDetailsProperties.class, + TenantManagementProperties.class }) public class MultitenancyCoreAutoConfiguration { @Bean @@ -67,4 +71,12 @@ TenantEventPublisher tenantEventPublisher(ApplicationEventPublisher applicationE return new DefaultTenantEventPublisher(applicationEventPublisher); } + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = TenantDetailsProperties.CONFIG_PREFIX, name = "source", havingValue = "properties", + matchIfMissing = true) + TenantDetailsService tenantDetailsService(TenantDetailsProperties tenantDetailsProperties) { + return new PropertiesTenantDetailsService(tenantDetailsProperties); + } + } diff --git a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/PropertiesTenantDetailsService.java b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/PropertiesTenantDetailsService.java new file mode 100644 index 0000000..1b0eb91 --- /dev/null +++ b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/PropertiesTenantDetailsService.java @@ -0,0 +1,39 @@ +package io.arconia.autoconfigure.multitenancy.core.tenantdetails; + +import java.util.List; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import io.arconia.core.multitenancy.tenantdetails.TenantDetails; +import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService; + +/** + * An implementation of {@link TenantDetailsService} that uses application properties as + * the source for the tenant details. + */ +public class PropertiesTenantDetailsService implements TenantDetailsService { + + private final TenantDetailsProperties tenantDetailsProperties; + + public PropertiesTenantDetailsService(TenantDetailsProperties tenantDetailsProperties) { + this.tenantDetailsProperties = tenantDetailsProperties; + } + + @Override + public List loadAllTenants() { + return tenantDetailsProperties.getTenants(); + } + + @Nullable + @Override + public TenantDetails loadTenantByIdentifier(String identifier) { + Assert.hasText(identifier, "identifier cannot be null or empty"); + return tenantDetailsProperties.getTenants() + .stream() + .filter(tenant -> tenant.getIdentifier().equals(identifier)) + .findFirst() + .orElse(null); + } + +} diff --git a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/TenantDetailsProperties.java b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/TenantDetailsProperties.java new file mode 100644 index 0000000..223654e --- /dev/null +++ b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/TenantDetailsProperties.java @@ -0,0 +1,50 @@ +package io.arconia.autoconfigure.multitenancy.core.tenantdetails; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import io.arconia.core.multitenancy.tenantdetails.Tenant; + +/** + * Configuration properties for tenant details. + */ +@ConfigurationProperties(prefix = TenantDetailsProperties.CONFIG_PREFIX) +public class TenantDetailsProperties { + + public static final String CONFIG_PREFIX = "arconia.multitenancy.details"; + + /** + * The source of tenant details. + */ + private Source source = Source.PROPERTIES; + + /** + * List of tenant details. + */ + private List tenants = new ArrayList<>(); + + public Source getSource() { + return source; + } + + public void setSource(Source source) { + this.source = source; + } + + public List getTenants() { + return tenants; + } + + public void setTenants(List tenants) { + this.tenants = tenants; + } + + public enum Source { + + HTTP, JDBC, PROPERTIES + + } + +} diff --git a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/package-info.java b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/package-info.java new file mode 100644 index 0000000..724226b --- /dev/null +++ b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/package-info.java @@ -0,0 +1,6 @@ +@NonNullApi +@NonNullFields +package io.arconia.autoconfigure.multitenancy.core.tenantdetails; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/HttpTenantResolutionConfiguration.java b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/HttpTenantResolutionConfiguration.java index a524935..b2c2744 100644 --- a/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/HttpTenantResolutionConfiguration.java +++ b/arconia-spring-boot-autoconfigure/src/main/java/io/arconia/autoconfigure/multitenancy/web/HttpTenantResolutionConfiguration.java @@ -10,6 +10,7 @@ import io.arconia.autoconfigure.multitenancy.core.FixedTenantResolutionProperties; import io.arconia.core.multitenancy.context.resolvers.FixedTenantResolver; import io.arconia.core.multitenancy.events.TenantEventPublisher; +import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService; import io.arconia.web.multitenancy.context.filters.TenantContextFilter; import io.arconia.web.multitenancy.context.filters.TenantContextIgnorePathMatcher; import io.arconia.web.multitenancy.context.resolvers.CookieTenantResolver; @@ -58,9 +59,9 @@ static class HttpTenantFilterConfiguration { @ConditionalOnMissingBean TenantContextFilter tenantContextFilter(HttpRequestTenantResolver httpRequestTenantResolver, TenantContextIgnorePathMatcher tenantContextIgnorePathMatcher, - TenantEventPublisher tenantEventPublisher) { + TenantDetailsService tenantDetailsService, TenantEventPublisher tenantEventPublisher) { return new TenantContextFilter(httpRequestTenantResolver, tenantContextIgnorePathMatcher, - tenantEventPublisher); + tenantDetailsService, tenantEventPublisher); } @Bean diff --git a/arconia-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/arconia-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index f1470f7..bd51ac6 100644 --- a/arconia-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/arconia-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,5 +1,9 @@ { "properties": [ + { + "name": "arconia.multitenancy.details.source", + "defaultValue": "properties" + }, { "name": "arconia.multitenancy.management.observations.cardinality", "defaultValue": "high" diff --git a/arconia-spring-boot-autoconfigure/src/test/java/io/arconia/autoconfigure/multitenancy/core/MultitenancyCoreAutoConfigurationTests.java b/arconia-spring-boot-autoconfigure/src/test/java/io/arconia/autoconfigure/multitenancy/core/MultitenancyCoreAutoConfigurationTests.java index 8c78251..ee6caf6 100644 --- a/arconia-spring-boot-autoconfigure/src/test/java/io/arconia/autoconfigure/multitenancy/core/MultitenancyCoreAutoConfigurationTests.java +++ b/arconia-spring-boot-autoconfigure/src/test/java/io/arconia/autoconfigure/multitenancy/core/MultitenancyCoreAutoConfigurationTests.java @@ -10,6 +10,7 @@ import io.arconia.core.multitenancy.context.events.ObservationTenantContextEventListener; import io.arconia.core.multitenancy.context.resolvers.FixedTenantResolver; import io.arconia.core.multitenancy.events.TenantEventPublisher; +import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService; import static org.assertj.core.api.Assertions.assertThat; @@ -84,4 +85,18 @@ void tenantEventPublisher() { }); } + @Test + void tenantDetailsServiceWhenDefault() { + contextRunner.run(context -> { + assertThat(context).hasSingleBean(TenantDetailsService.class); + }); + } + + @Test + void tenantDetailsServiceWhenDisabled() { + contextRunner.withPropertyValues("arconia.multitenancy.details.source=http").run(context -> { + assertThat(context).doesNotHaveBean(TenantDetailsService.class); + }); + } + } diff --git a/arconia-spring-boot-autoconfigure/src/test/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/PropertiesTenantDetailsServiceTests.java b/arconia-spring-boot-autoconfigure/src/test/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/PropertiesTenantDetailsServiceTests.java new file mode 100644 index 0000000..da5c466 --- /dev/null +++ b/arconia-spring-boot-autoconfigure/src/test/java/io/arconia/autoconfigure/multitenancy/core/tenantdetails/PropertiesTenantDetailsServiceTests.java @@ -0,0 +1,51 @@ +package io.arconia.autoconfigure.multitenancy.core.tenantdetails; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import io.arconia.core.multitenancy.tenantdetails.Tenant; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link PropertiesTenantDetailsService}. + */ +class PropertiesTenantDetailsServiceTests { + + @Test + void loadAllTenants() { + var tenantDetailsProperties = new TenantDetailsProperties(); + tenantDetailsProperties.setTenants(List.of(Tenant.create().identifier("acme").enabled(true).build(), + Tenant.create().identifier("sam").enabled(false).build())); + + var tenantDetailsService = new PropertiesTenantDetailsService(tenantDetailsProperties); + var tenants = tenantDetailsService.loadAllTenants(); + + assertThat(tenants).isNotNull(); + assertThat(tenants).hasSize(2); + } + + @Test + void whenTenantEnabledThenReturn() { + var tenantDetailsProperties = new TenantDetailsProperties(); + tenantDetailsProperties.setTenants(List.of(Tenant.create().identifier("acme").enabled(true).build())); + + var tenantDetailsService = new PropertiesTenantDetailsService(tenantDetailsProperties); + var tenant = tenantDetailsService.loadTenantByIdentifier("acme"); + + assertThat(tenant).isNotNull(); + } + + @Test + void whenTenantDisabledThenReturn() { + var tenantDetailsProperties = new TenantDetailsProperties(); + tenantDetailsProperties.setTenants(List.of(Tenant.create().identifier("acme").build())); + + var tenantDetailsService = new PropertiesTenantDetailsService(tenantDetailsProperties); + var tenant = tenantDetailsService.loadTenantByIdentifier("acme"); + + assertThat(tenant).isNotNull(); + } + +} diff --git a/arconia-web/src/main/java/io/arconia/web/multitenancy/context/filters/TenantContextFilter.java b/arconia-web/src/main/java/io/arconia/web/multitenancy/context/filters/TenantContextFilter.java index 0cccd7c..d608b16 100644 --- a/arconia-web/src/main/java/io/arconia/web/multitenancy/context/filters/TenantContextFilter.java +++ b/arconia-web/src/main/java/io/arconia/web/multitenancy/context/filters/TenantContextFilter.java @@ -16,6 +16,7 @@ import io.arconia.core.multitenancy.context.events.TenantContextClosedEvent; import io.arconia.core.multitenancy.events.TenantEventPublisher; import io.arconia.core.multitenancy.exceptions.TenantResolutionException; +import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService; import io.arconia.web.multitenancy.context.resolvers.HttpRequestTenantResolver; /** @@ -27,26 +28,27 @@ public final class TenantContextFilter extends OncePerRequestFilter { private final TenantContextIgnorePathMatcher tenantContextIgnorePathMatcher; + private final TenantDetailsService tenantDetailsService; + private final TenantEventPublisher tenantEventPublisher; public TenantContextFilter(HttpRequestTenantResolver httpRequestTenantResolver, - TenantContextIgnorePathMatcher tenantContextIgnorePathMatcher, TenantEventPublisher tenantEventPublisher) { + TenantContextIgnorePathMatcher tenantContextIgnorePathMatcher, TenantDetailsService tenantDetailsService, + TenantEventPublisher tenantEventPublisher) { Assert.notNull(httpRequestTenantResolver, "httpRequestTenantResolver cannot be null"); Assert.notNull(tenantContextIgnorePathMatcher, "ignorePathMatcher cannot be null"); + Assert.notNull(tenantDetailsService, "tenantDetailsService cannot be null"); Assert.notNull(tenantEventPublisher, "tenantEventPublisher cannot be null"); this.httpRequestTenantResolver = httpRequestTenantResolver; this.tenantContextIgnorePathMatcher = tenantContextIgnorePathMatcher; + this.tenantDetailsService = tenantDetailsService; this.tenantEventPublisher = tenantEventPublisher; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - var tenantIdentifier = httpRequestTenantResolver.resolveTenantIdentifier(request); - if (!StringUtils.hasText(tenantIdentifier)) { - throw new TenantResolutionException( - "A tenant identifier must be specified for HTTP requests to: " + request.getRequestURI()); - } + var tenantIdentifier = resolveAndValidateTenant(request); publishTenantContextAttachedEvent(tenantIdentifier, request); try { @@ -62,6 +64,22 @@ protected boolean shouldNotFilter(HttpServletRequest request) { return tenantContextIgnorePathMatcher.matches(request); } + private String resolveAndValidateTenant(HttpServletRequest request) { + var tenantIdentifier = httpRequestTenantResolver.resolveTenantIdentifier(request); + + if (!StringUtils.hasText(tenantIdentifier)) { + throw new TenantResolutionException( + "A tenant identifier must be specified for HTTP requests to: " + request.getRequestURI()); + } + + var tenant = tenantDetailsService.loadTenantByIdentifier(tenantIdentifier); + if (tenant == null || !tenant.isEnabled()) { + throw new TenantResolutionException("The resolved tenant is invalid or disabled"); + } + + return tenantIdentifier; + } + private void publishTenantContextAttachedEvent(String tenantIdentifier, HttpServletRequest request) { var tenantContextAttachedEvent = new TenantContextAttachedEvent(tenantIdentifier, request); var observationContext = ServerHttpObservationFilter.findObservationContext(request); diff --git a/arconia-web/src/test/java/io/arconia/web/multitenancy/context/filters/TenantContextFilterTests.java b/arconia-web/src/test/java/io/arconia/web/multitenancy/context/filters/TenantContextFilterTests.java index d3aba81..fc386b1 100644 --- a/arconia-web/src/test/java/io/arconia/web/multitenancy/context/filters/TenantContextFilterTests.java +++ b/arconia-web/src/test/java/io/arconia/web/multitenancy/context/filters/TenantContextFilterTests.java @@ -19,11 +19,15 @@ import io.arconia.core.multitenancy.events.TenantEvent; import io.arconia.core.multitenancy.events.TenantEventPublisher; import io.arconia.core.multitenancy.exceptions.TenantResolutionException; +import io.arconia.core.multitenancy.tenantdetails.Tenant; +import io.arconia.core.multitenancy.tenantdetails.TenantDetailsService; import io.arconia.web.multitenancy.context.resolvers.HeaderTenantResolver; import io.arconia.web.multitenancy.context.resolvers.HttpRequestTenantResolver; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; /** * Unit tests for {@link TenantContextFilter}. @@ -34,7 +38,9 @@ class TenantContextFilterTests { void whenNullTenantResolverThenThrow() { var noTenantPathMatcher = Mockito.mock(TenantContextIgnorePathMatcher.class); var tenantEventPublisher = Mockito.mock(TenantEventPublisher.class); - assertThatThrownBy(() -> new TenantContextFilter(null, noTenantPathMatcher, tenantEventPublisher)) + var tenantDetailsService = Mockito.mock(TenantDetailsService.class); + assertThatThrownBy( + () -> new TenantContextFilter(null, noTenantPathMatcher, tenantDetailsService, tenantEventPublisher)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("httpRequestTenantResolver cannot be null"); } @@ -43,16 +49,31 @@ void whenNullTenantResolverThenThrow() { void whenNullPathMatcherThenThrow() { var httpRequestTenantResolver = Mockito.mock(HttpRequestTenantResolver.class); var tenantEventPublisher = Mockito.mock(TenantEventPublisher.class); - assertThatThrownBy(() -> new TenantContextFilter(httpRequestTenantResolver, null, tenantEventPublisher)) + var tenantDetailsService = Mockito.mock(TenantDetailsService.class); + assertThatThrownBy(() -> new TenantContextFilter(httpRequestTenantResolver, null, tenantDetailsService, + tenantEventPublisher)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("ignorePathMatcher cannot be null"); } + @Test + void whenNullTenantDetailsServiceThenThrow() { + var httpRequestTenantResolver = Mockito.mock(HttpRequestTenantResolver.class); + var noTenantPathMatcher = Mockito.mock(TenantContextIgnorePathMatcher.class); + var tenantEventPublisher = Mockito.mock(TenantEventPublisher.class); + assertThatThrownBy(() -> new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, null, + tenantEventPublisher)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("tenantDetailsService cannot be null"); + } + @Test void whenNullEventPublisherThenThrow() { var httpRequestTenantResolver = Mockito.mock(HttpRequestTenantResolver.class); var noTenantPathMatcher = Mockito.mock(TenantContextIgnorePathMatcher.class); - assertThatThrownBy(() -> new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, null)) + var tenantDetailsService = Mockito.mock(TenantDetailsService.class); + assertThatThrownBy(() -> new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, + tenantDetailsService, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("tenantEventPublisher cannot be null"); } @@ -68,8 +89,12 @@ void whenTenantResolvedThenPublishEvent() throws ServletException, IOException { var filterChain = new MockFilterChain(); var httpRequestTenantResolver = new HeaderTenantResolver(); var noTenantPathMatcher = new TenantContextIgnorePathMatcher(List.of()); + var tenantDetailsService = Mockito.mock(TenantDetailsService.class); + when(tenantDetailsService.loadTenantByIdentifier(anyString())) + .thenReturn(Tenant.create().identifier(tenantIdentifier).enabled(true).build()); var tenantEventPublisher = Mockito.mock(TenantEventPublisher.class); - var filter = new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, tenantEventPublisher); + var filter = new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, tenantDetailsService, + tenantEventPublisher); filter.doFilter(request, response, filterChain); @@ -98,7 +123,9 @@ void whenRequiredTenantNotResolvedThenThrow() { var httpRequestTenantResolver = new HeaderTenantResolver(); var noTenantPathMatcher = new TenantContextIgnorePathMatcher(List.of()); var tenantEventPublisher = Mockito.mock(TenantEventPublisher.class); - var filter = new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, tenantEventPublisher); + var tenantDetailsService = Mockito.mock(TenantDetailsService.class); + var filter = new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, tenantDetailsService, + tenantEventPublisher); assertThatThrownBy(() -> filter.doFilter(request, response, filterChain)) .isInstanceOf(TenantResolutionException.class) @@ -107,6 +134,29 @@ void whenRequiredTenantNotResolvedThenThrow() { Mockito.verify(tenantEventPublisher, Mockito.times(0)).publishTenantEvent(Mockito.any(TenantEvent.class)); } + @Test + void whenResolvedTenantIsDisabledThenThrow() { + var tenantIdentifier = "acme"; + var request = new MockHttpServletRequest(); + request.addHeader(HeaderTenantResolver.DEFAULT_HEADER_NAME, tenantIdentifier); + var response = new MockHttpServletResponse(); + var filterChain = new MockFilterChain(); + var httpRequestTenantResolver = new HeaderTenantResolver(); + var noTenantPathMatcher = new TenantContextIgnorePathMatcher(List.of()); + var tenantEventPublisher = Mockito.mock(TenantEventPublisher.class); + var tenantDetailsService = Mockito.mock(TenantDetailsService.class); + when(tenantDetailsService.loadTenantByIdentifier(anyString())) + .thenReturn(Tenant.create().identifier(tenantIdentifier).build()); + var filter = new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, tenantDetailsService, + tenantEventPublisher); + + assertThatThrownBy(() -> filter.doFilter(request, response, filterChain)) + .isInstanceOf(TenantResolutionException.class) + .hasMessageContaining("The resolved tenant is invalid or disabled"); + + Mockito.verify(tenantEventPublisher, Mockito.times(0)).publishTenantEvent(Mockito.any(TenantEvent.class)); + } + @Test void whenIgnorePathThenNoTenantResolvedAndNoEventPublished() throws ServletException, IOException { var path = "/ignore-path"; @@ -117,7 +167,9 @@ void whenIgnorePathThenNoTenantResolvedAndNoEventPublished() throws ServletExcep var httpRequestTenantResolver = new HeaderTenantResolver(); var noTenantPathMatcher = new TenantContextIgnorePathMatcher(List.of(path)); var tenantEventPublisher = Mockito.mock(TenantEventPublisher.class); - var filter = new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, tenantEventPublisher); + var tenantDetailsService = Mockito.mock(TenantDetailsService.class); + var filter = new TenantContextFilter(httpRequestTenantResolver, noTenantPathMatcher, tenantDetailsService, + tenantEventPublisher); filter.doFilter(request, response, filterChain);