From 2f4fe361dae204680e562ae2bfb209d61bdab528 Mon Sep 17 00:00:00 2001 From: jvmdev4 Date: Mon, 24 Aug 2026 15:00:12 +0400 Subject: [PATCH 1/4] Bump Spring Boot 4 --- .github/workflows/build.yml | 2 +- .github/workflows/deploy.yml | 2 +- pom.xml | 68 ++++++++---------- .../orgmanager/OrgManagerApplication.java | 2 +- .../vality/orgmanager/config/AppConfig.java | 15 ++-- .../config/ConfigResolverConfig.java | 6 +- .../vality/orgmanager/config/OtelConfig.java | 70 ------------------- .../config/TomcatWebServerCustomizer.java | 4 +- .../config/properties/OtelProperties.java | 17 ----- .../converter/InvitationConverter.java | 8 +-- .../converter/OrganizationConverter.java | 8 +-- .../pagination/OffsetBasedPageRequest.java | 2 + .../repository/MemberRepository.java | 6 +- .../repository/OrganizationRepository.java | 14 ++-- .../orgmanager/service/KeycloakService.java | 6 +- .../vality/orgmanager/util/JsonMapper.java | 27 ------- src/main/resources/application.yml | 5 -- .../controller/AbstractControllerTest.java | 11 +-- .../converter/InvitationConverterTest.java | 18 +++-- .../converter/OrganizationConverterTest.java | 7 +- .../repository/AbstractRepositoryTest.java | 4 +- .../service/BouncerServiceImplTest.java | 18 +++-- .../ResourceAccessServiceImplTest.java | 8 +-- src/test/resources/wiremock.properties | 1 - 24 files changed, 106 insertions(+), 223 deletions(-) delete mode 100644 src/main/java/dev/vality/orgmanager/config/OtelConfig.java delete mode 100644 src/main/java/dev/vality/orgmanager/config/properties/OtelProperties.java delete mode 100644 src/main/java/dev/vality/orgmanager/util/JsonMapper.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dbb1a95a..8ffed7c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,4 +7,4 @@ on: jobs: build: - uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v3 + uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9f366b27..93aebc69 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,7 +7,7 @@ on: jobs: build-and-deploy: - uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v3 + uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v4 secrets: github-token: ${{ secrets.GITHUB_TOKEN }} mm-webhook-url: ${{ secrets.MATTERMOST_WEBHOOK_URL }} diff --git a/pom.xml b/pom.xml index e8ed8868..cf8165f1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.vality service-parent-pom - 3.1.8 + 4.0.0 org-manager @@ -16,8 +16,7 @@ org-manager - 21 - 21 + 25 UTF-8 UTF-8 8022 @@ -27,9 +26,9 @@ 17.0.0 4.34.0 1.38-8c9b968 - 2.32.0 1.685-5c25c2e - 3.0.1 + 5.0.0 + 3.0.0 @@ -37,11 +36,16 @@ dev.vality.woody woody-thrift + ${woody.version} + + + dev.vality.woody + woody-api + ${woody.version} dev.vality shared-resources - ${shared-resources.version} dev.vality @@ -82,7 +86,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc org.springframework.boot @@ -98,7 +102,7 @@ org.springframework.boot - spring-boot-starter-oauth2-resource-server + spring-boot-starter-security-oauth2-resource-server net.javacrumbs.shedlock @@ -118,8 +122,8 @@ org.bouncycastle - bcprov-jdk15on - 1.70 + bcprov-jdk18on + 1.84 @@ -140,14 +144,15 @@ org.projectlombok lombok + provided org.postgresql postgresql - org.flywaydb - flyway-core + org.springframework.boot + spring-boot-starter-flyway org.flywaydb @@ -161,18 +166,11 @@ org.openapitools jackson-databind-nullable - 0.2.2 + 0.2.10 org.yaml snakeyaml - 2.0 - - - - io.opentelemetry - opentelemetry-semconv - 1.29.0-alpha @@ -180,23 +178,16 @@ org.springframework.boot spring-boot-starter-test test - - - org.junit.vintage - junit-vintage-engine - - - org.springframework.cloud - spring-cloud-contract-wiremock - 4.1.5 + org.springframework.boot + spring-boot-starter-webmvc-test test - com.github.tomakehurst - wiremock-jre8-standalone - ${wiremock-jre8-standalone.version} + org.wiremock.integrations + wiremock-spring-boot + 4.0.9 test @@ -207,8 +198,7 @@ org.testcontainers - postgresql - 1.19.1 + testcontainers-postgresql test @@ -219,8 +209,7 @@ org.testcontainers - junit-jupiter - 1.19.1 + testcontainers-junit-jupiter test @@ -240,6 +229,7 @@ true Dockerfile + opentelemetry-javaagent.jar @@ -281,15 +271,13 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.15.0 - 21 - 21 org.projectlombok lombok - 1.18.36 + ${lombok.version} diff --git a/src/main/java/dev/vality/orgmanager/OrgManagerApplication.java b/src/main/java/dev/vality/orgmanager/OrgManagerApplication.java index 1338b8b7..e74f6f6c 100644 --- a/src/main/java/dev/vality/orgmanager/OrgManagerApplication.java +++ b/src/main/java/dev/vality/orgmanager/OrgManagerApplication.java @@ -3,7 +3,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -import org.springframework.boot.web.servlet.ServletComponentScan; +import org.springframework.boot.web.server.servlet.context.ServletComponentScan; @ServletComponentScan @ConfigurationPropertiesScan diff --git a/src/main/java/dev/vality/orgmanager/config/AppConfig.java b/src/main/java/dev/vality/orgmanager/config/AppConfig.java index 59e468b3..5a5f0c39 100644 --- a/src/main/java/dev/vality/orgmanager/config/AppConfig.java +++ b/src/main/java/dev/vality/orgmanager/config/AppConfig.java @@ -1,20 +1,23 @@ package dev.vality.orgmanager.config; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.vality.orgmanager.controller.converter.InvitationStatusConverter; -import org.openapitools.jackson.nullable.JsonNullableModule; +import org.openapitools.jackson.nullable.JsonNullableJackson3Module; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.format.support.FormattingConversionService; @Configuration public class AppConfig { - @Autowired - public void objectMapper(ObjectMapper mapper) { - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - mapper.registerModule(new JsonNullableModule()); + @Bean + public JsonMapperBuilderCustomizer jsonMapperCustomizer() { + return builder -> builder + .changeDefaultPropertyInclusion(inclusion -> + inclusion.withValueInclusion(JsonInclude.Include.NON_NULL)) + .addModule(new JsonNullableJackson3Module()); } @Autowired diff --git a/src/main/java/dev/vality/orgmanager/config/ConfigResolverConfig.java b/src/main/java/dev/vality/orgmanager/config/ConfigResolverConfig.java index 7152cfd0..230e17a9 100644 --- a/src/main/java/dev/vality/orgmanager/config/ConfigResolverConfig.java +++ b/src/main/java/dev/vality/orgmanager/config/ConfigResolverConfig.java @@ -13,13 +13,13 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.Base64; import java.util.List; import java.util.stream.Collectors; @@ -65,7 +65,7 @@ private RSAPublicKey readPublicKey(String publicKey) { private String readKeyFromFile(String filePath) { try { - List strings = Files.readAllLines(Paths.get(filePath)); + List strings = Files.readAllLines(Path.of(filePath)); strings.remove(strings.size() - 1); strings.remove(0); diff --git a/src/main/java/dev/vality/orgmanager/config/OtelConfig.java b/src/main/java/dev/vality/orgmanager/config/OtelConfig.java deleted file mode 100644 index 630c3aa8..00000000 --- a/src/main/java/dev/vality/orgmanager/config/OtelConfig.java +++ /dev/null @@ -1,70 +0,0 @@ -package dev.vality.orgmanager.config; - -import dev.vality.orgmanager.config.properties.OtelProperties; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; -import io.opentelemetry.sdk.trace.samplers.Sampler; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.time.Duration; - -@Slf4j -@Configuration -@ConditionalOnProperty(value = "otel.enabled", havingValue = "true", matchIfMissing = true) -@RequiredArgsConstructor -public class OtelConfig { - - private final OtelProperties otelProperties; - - @Value("${spring.application.name}") - private String applicationName; - - @Bean - public OpenTelemetry openTelemetryConfig() { - var resource = Resource.getDefault() - .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))); - var sdkTracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(BatchSpanProcessor.builder(OtlpHttpSpanExporter.builder() - .setEndpoint(otelProperties.getResource()) - .setTimeout(Duration.ofMillis(otelProperties.getTimeout())) - .build()) - .build()) - .setSampler(Sampler.alwaysOn()) - .setResource(resource) - .build(); - var openTelemetrySdk = OpenTelemetrySdk.builder() - .setTracerProvider(sdkTracerProvider) - .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) - .build(); - registerGlobalOpenTelemetry(openTelemetrySdk); - return openTelemetrySdk; - } - - private static void registerGlobalOpenTelemetry(OpenTelemetry openTelemetry) { - try { - GlobalOpenTelemetry.set(openTelemetry); - } catch (Throwable ex) { - log.warn("Please initialize the ObservabilitySdk before starting the application", ex); - GlobalOpenTelemetry.resetForTest(); - try { - GlobalOpenTelemetry.set(openTelemetry); - } catch (Throwable ex1) { - log.warn("Unable to set GlobalOpenTelemetry", ex1); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/dev/vality/orgmanager/config/TomcatWebServerCustomizer.java b/src/main/java/dev/vality/orgmanager/config/TomcatWebServerCustomizer.java index 6ecb1fa2..ee654ec9 100644 --- a/src/main/java/dev/vality/orgmanager/config/TomcatWebServerCustomizer.java +++ b/src/main/java/dev/vality/orgmanager/config/TomcatWebServerCustomizer.java @@ -2,7 +2,7 @@ import org.apache.catalina.connector.Connector; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -19,6 +19,6 @@ public void customize(TomcatServletWebServerFactory factory) { Connector connector = new Connector(); connector.setPort(restPort); - factory.addAdditionalTomcatConnectors(connector); + factory.addAdditionalConnectors(connector); } } diff --git a/src/main/java/dev/vality/orgmanager/config/properties/OtelProperties.java b/src/main/java/dev/vality/orgmanager/config/properties/OtelProperties.java deleted file mode 100644 index ff18b2d3..00000000 --- a/src/main/java/dev/vality/orgmanager/config/properties/OtelProperties.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.vality.orgmanager.config.properties; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Getter -@Setter -@Component -@ConfigurationProperties(prefix = "otel") -public class OtelProperties { - - private String resource; - private Long timeout; - -} diff --git a/src/main/java/dev/vality/orgmanager/converter/InvitationConverter.java b/src/main/java/dev/vality/orgmanager/converter/InvitationConverter.java index b9242262..d91232e0 100644 --- a/src/main/java/dev/vality/orgmanager/converter/InvitationConverter.java +++ b/src/main/java/dev/vality/orgmanager/converter/InvitationConverter.java @@ -2,7 +2,7 @@ import dev.vality.orgmanager.config.properties.InviteTokenProperties; import dev.vality.orgmanager.entity.InvitationEntity; -import dev.vality.orgmanager.util.JsonMapper; +import dev.vality.orgmanager.util.JsonCodec; import dev.vality.swag.organizations.model.*; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -21,7 +21,7 @@ @RequiredArgsConstructor public class InvitationConverter { - private final JsonMapper jsonMapper; + private final JsonCodec jsonCodec; private final MemberRoleConverter memberRoleConverter; private final InviteTokenProperties inviteTokenProperties; @@ -48,7 +48,7 @@ public InvitationEntity toEntity(InvitationRequest invitation, String orgId) { .stream() .map(role -> memberRoleConverter.toEntity(role, orgId)) .collect(toSet())) - .metadata(jsonMapper.toJson(invitation.getMetadata())) + .metadata(jsonCodec.toJson(invitation.getMetadata())) .status(InvitationStatusName.PENDING.getValue()) .acceptToken(UUID.randomUUID().toString()) // TODO [a.romanov]: token .build(); @@ -81,7 +81,7 @@ public Invitation toDomain(InvitationEntity entity) { invitation.id(entity.getId()) .createdAt(OffsetDateTime.of(entity.getCreatedAt(), ZoneOffset.UTC)) .expiresAt(OffsetDateTime.of(entity.getExpiresAt(), ZoneOffset.UTC)) - .metadata(entity.getMetadata() != null ? jsonMapper.toMap(entity.getMetadata()) : null) + .metadata(entity.getMetadata() != null ? jsonCodec.toMap(entity.getMetadata()) : null) .invitee(new Invitee() .contact(new InviteeContact() .type(InviteeContact.TypeEnum.fromValue(entity.getInviteeContactType())) diff --git a/src/main/java/dev/vality/orgmanager/converter/OrganizationConverter.java b/src/main/java/dev/vality/orgmanager/converter/OrganizationConverter.java index 80951cb0..ad504520 100644 --- a/src/main/java/dev/vality/orgmanager/converter/OrganizationConverter.java +++ b/src/main/java/dev/vality/orgmanager/converter/OrganizationConverter.java @@ -1,7 +1,7 @@ package dev.vality.orgmanager.converter; import dev.vality.orgmanager.entity.OrganizationEntity; -import dev.vality.orgmanager.util.JsonMapper; +import dev.vality.orgmanager.util.JsonCodec; import dev.vality.swag.organizations.model.Organization; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,7 +15,7 @@ @RequiredArgsConstructor public class OrganizationConverter { - private final JsonMapper jsonMapper; + private final JsonCodec jsonCodec; public OrganizationEntity toEntity(Organization organization, String ownerId) { String orgId = UUID.randomUUID().toString(); @@ -26,7 +26,7 @@ public OrganizationEntity toEntity(Organization organization, String ownerId) { .name(organization.getName()) .owner(ownerId) .party(partyId) - .metadata(jsonMapper.toJson(organization.getMetadata())) + .metadata(jsonCodec.toJson(organization.getMetadata())) .build(); } @@ -37,7 +37,7 @@ public Organization toDomain(OrganizationEntity entity) { .name(entity.getName()) .owner(entity.getOwner()) .party(entity.getParty()) - .metadata(entity.getMetadata() != null ? jsonMapper.toMap(entity.getMetadata()) : null); + .metadata(entity.getMetadata() != null ? jsonCodec.toMap(entity.getMetadata()) : null); } } diff --git a/src/main/java/dev/vality/orgmanager/pagination/OffsetBasedPageRequest.java b/src/main/java/dev/vality/orgmanager/pagination/OffsetBasedPageRequest.java index 44d8ca62..7482b04c 100644 --- a/src/main/java/dev/vality/orgmanager/pagination/OffsetBasedPageRequest.java +++ b/src/main/java/dev/vality/orgmanager/pagination/OffsetBasedPageRequest.java @@ -4,11 +4,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import java.io.Serial; import java.io.Serializable; @EqualsAndHashCode public class OffsetBasedPageRequest implements Pageable, Serializable { + @Serial private static final long serialVersionUID = -35826477329623545L; private int limit; diff --git a/src/main/java/dev/vality/orgmanager/repository/MemberRepository.java b/src/main/java/dev/vality/orgmanager/repository/MemberRepository.java index a064e034..7435e685 100644 --- a/src/main/java/dev/vality/orgmanager/repository/MemberRepository.java +++ b/src/main/java/dev/vality/orgmanager/repository/MemberRepository.java @@ -3,7 +3,7 @@ import dev.vality.orgmanager.entity.MemberEntity; import dev.vality.orgmanager.service.dto.MemberWithRoleDto; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.stereotype.Repository; import java.util.List; @@ -12,7 +12,7 @@ public interface MemberRepository extends JpaRepository { - @Query(value = "SELECT m.id, " + + @NativeQuery("SELECT m.id, " + " m.email, " + " mr.id as memberRoleId, " + " mr.organization_id as organizationId, " + @@ -29,7 +29,7 @@ public interface MemberRepository extends JpaRepository { " AND mr.active = 'true' " + " AND mr.id = mtmr.member_role_id " + " AND mr.organization_id = mto.organization_id " + - " AND m.id = mtmr.member_id ", nativeQuery = true) + " AND m.id = mtmr.member_id ") List getOrgMemberList(String orgId); boolean existsById(String id); diff --git a/src/main/java/dev/vality/orgmanager/repository/OrganizationRepository.java b/src/main/java/dev/vality/orgmanager/repository/OrganizationRepository.java index bf18329d..a5581c2c 100644 --- a/src/main/java/dev/vality/orgmanager/repository/OrganizationRepository.java +++ b/src/main/java/dev/vality/orgmanager/repository/OrganizationRepository.java @@ -2,7 +2,7 @@ import dev.vality.orgmanager.entity.OrganizationEntity; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.stereotype.Repository; import java.util.List; @@ -11,8 +11,7 @@ @Repository public interface OrganizationRepository extends JpaRepository { - @Query(value = - " SELECT * FROM org_manager.organization AS o " + + @NativeQuery(" SELECT * FROM org_manager.organization AS o " + " WHERE o.id IN " + " ( " + " SELECT mo.organization_id FROM org_manager.member_to_organization AS mo " + @@ -20,12 +19,10 @@ public interface OrganizationRepository extends JpaRepository findAllByMember(String userId); - @Query(value = - " SELECT * FROM org_manager.organization AS o " + + @NativeQuery(" SELECT * FROM org_manager.organization AS o " + " WHERE o.id IN " + " ( " + " SELECT mo.organization_id FROM org_manager.member_to_organization AS mo " + @@ -34,8 +31,7 @@ public interface OrganizationRepository extends JpaRepository findAllByMember(String userId, String continuationId); Set findAllByOwner(String owner); diff --git a/src/main/java/dev/vality/orgmanager/service/KeycloakService.java b/src/main/java/dev/vality/orgmanager/service/KeycloakService.java index 23fdc5b3..218a93da 100644 --- a/src/main/java/dev/vality/orgmanager/service/KeycloakService.java +++ b/src/main/java/dev/vality/orgmanager/service/KeycloakService.java @@ -1,12 +1,12 @@ package dev.vality.orgmanager.service; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.keycloak.representations.AccessToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Service; +import tools.jackson.databind.json.JsonMapper; import java.time.Instant; import java.util.HashMap; @@ -16,14 +16,14 @@ @RequiredArgsConstructor public class KeycloakService { - private final ObjectMapper objectMapper; + private final JsonMapper jsonMapper; public AccessToken getAccessToken() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) { throw new IllegalStateException("Jwt principal is required"); } - return objectMapper.convertValue(normalizedClaims(jwt.getClaims()), AccessToken.class); + return jsonMapper.convertValue(normalizedClaims(jwt.getClaims()), AccessToken.class); } private Map normalizedClaims(Map claims) { diff --git a/src/main/java/dev/vality/orgmanager/util/JsonMapper.java b/src/main/java/dev/vality/orgmanager/util/JsonMapper.java deleted file mode 100644 index 27a3ec67..00000000 --- a/src/main/java/dev/vality/orgmanager/util/JsonMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.vality.orgmanager.util; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.springframework.stereotype.Service; - -import java.util.Map; - -@Service -@RequiredArgsConstructor -public class JsonMapper { - - private final ObjectMapper objectMapper; - - @SneakyThrows(JsonProcessingException.class) - public String toJson(Object data) { - return objectMapper.writeValueAsString(data); - } - - @SuppressWarnings("rawtypes") - @SneakyThrows(JsonProcessingException.class) - public Map toMap(String json) { - return objectMapper.readValue(json, Map.class); - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a318fdad..c7941c52 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -92,8 +92,3 @@ bouncer: access-check: enabled: false - -otel: - resource: http://localhost:4318/v1/traces - timeout: 60000 - enabled: false diff --git a/src/test/java/dev/vality/orgmanager/controller/AbstractControllerTest.java b/src/test/java/dev/vality/orgmanager/controller/AbstractControllerTest.java index 8a96c7d2..f39fc712 100644 --- a/src/test/java/dev/vality/orgmanager/controller/AbstractControllerTest.java +++ b/src/test/java/dev/vality/orgmanager/controller/AbstractControllerTest.java @@ -1,20 +1,21 @@ package dev.vality.orgmanager.controller; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.vality.orgmanager.repository.AbstractRepositoryTest; import dev.vality.orgmanager.service.ResourceAccessService; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.context.annotation.Import; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.web.servlet.MockMvc; +import org.wiremock.spring.ConfigureWireMock; +import org.wiremock.spring.EnableWireMock; +import tools.jackson.databind.json.JsonMapper; @Import(KeycloakTestConfig.class) @AutoConfigureMockMvc -@AutoConfigureWireMock(port = 0) +@EnableWireMock(@ConfigureWireMock(port = 0, baseUrlProperties = "wiremock.server.baseUrl")) @TestPropertySource( locations = "classpath:wiremock.properties", properties = "auth.enabled=true" @@ -37,7 +38,7 @@ public void setUp() throws Exception { protected MockMvc mockMvc; @Autowired - protected ObjectMapper objectMapper; + protected JsonMapper objectMapper; protected String generateJwt(long iat, long exp, String... roles) { return keycloakOpenIdStub.generateJwt(iat, exp, roles); diff --git a/src/test/java/dev/vality/orgmanager/converter/InvitationConverterTest.java b/src/test/java/dev/vality/orgmanager/converter/InvitationConverterTest.java index 6f45c357..df6e3ffa 100644 --- a/src/test/java/dev/vality/orgmanager/converter/InvitationConverterTest.java +++ b/src/test/java/dev/vality/orgmanager/converter/InvitationConverterTest.java @@ -1,13 +1,19 @@ package dev.vality.orgmanager.converter; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.vality.orgmanager.config.properties.InviteTokenProperties; import dev.vality.orgmanager.entity.InvitationEntity; import dev.vality.orgmanager.entity.MemberRoleEntity; -import dev.vality.orgmanager.util.JsonMapper; -import dev.vality.swag.organizations.model.*; +import dev.vality.orgmanager.util.JsonCodec; +import dev.vality.swag.organizations.model.Invitation; +import dev.vality.swag.organizations.model.InvitationPending; +import dev.vality.swag.organizations.model.InvitationRequest; +import dev.vality.swag.organizations.model.InvitationStatusName; +import dev.vality.swag.organizations.model.Invitee; +import dev.vality.swag.organizations.model.InviteeContact; +import dev.vality.swag.organizations.model.MemberRole; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -36,7 +42,7 @@ public void setUp() { when(inviteTokenProperties.getLifeTimeInDays()).thenReturn(30L); converter = new InvitationConverter( - new JsonMapper(new ObjectMapper()), + new JsonCodec(new JsonMapper()), memberRoleConverter, inviteTokenProperties ); @@ -63,7 +69,9 @@ void shouldConvertToEntity() throws Exception { assertThat(entity.getInviteeContactType()).isEqualTo(invitation.getInvitee().getContact().getType().getValue()); assertThat(entity.getOrganizationId()).isEqualTo(orgId); assertThat(entity.getStatus()).isEqualTo(InvitationStatusName.PENDING.getValue()); - assertThat(entity.getMetadata()).isEqualTo(new ObjectMapper().writeValueAsString(invitation.getMetadata())); + String expectedMetadata = new JsonMapper() + .writeValueAsString(invitation.getMetadata()); + assertThat(entity.getMetadata()).isEqualTo(expectedMetadata); } @Test diff --git a/src/test/java/dev/vality/orgmanager/converter/OrganizationConverterTest.java b/src/test/java/dev/vality/orgmanager/converter/OrganizationConverterTest.java index 4b13be14..b693a468 100644 --- a/src/test/java/dev/vality/orgmanager/converter/OrganizationConverterTest.java +++ b/src/test/java/dev/vality/orgmanager/converter/OrganizationConverterTest.java @@ -1,12 +1,12 @@ package dev.vality.orgmanager.converter; -import com.fasterxml.jackson.databind.ObjectMapper; import dev.vality.orgmanager.TestObjectFactory; import dev.vality.orgmanager.entity.OrganizationEntity; -import dev.vality.orgmanager.util.JsonMapper; +import dev.vality.orgmanager.util.JsonCodec; import dev.vality.swag.organizations.model.Organization; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; import java.util.Map; @@ -20,8 +20,7 @@ class OrganizationConverterTest { @BeforeEach public void setUp() { converter = new OrganizationConverter( - new JsonMapper( - new ObjectMapper())); + new JsonCodec(new JsonMapper())); } @Test diff --git a/src/test/java/dev/vality/orgmanager/repository/AbstractRepositoryTest.java b/src/test/java/dev/vality/orgmanager/repository/AbstractRepositoryTest.java index 8e8ea355..c5a3e1c3 100644 --- a/src/test/java/dev/vality/orgmanager/repository/AbstractRepositoryTest.java +++ b/src/test/java/dev/vality/orgmanager/repository/AbstractRepositoryTest.java @@ -11,7 +11,7 @@ import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.annotation.Transactional; -import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.postgresql.PostgreSQLContainer; import org.testcontainers.utility.DockerImageName; @ActiveProfiles("test") @@ -69,7 +69,7 @@ static void beforeAll() { } @SuppressWarnings("rawtypes") - static PostgreSQLContainer postgres = new PostgreSQLContainer<>(DockerImageName + static PostgreSQLContainer postgres = new PostgreSQLContainer(DockerImageName .parse(POSTGRESQL_IMAGE_NAME) .withTag(POSTGRESQL_VERSION)); diff --git a/src/test/java/dev/vality/orgmanager/service/BouncerServiceImplTest.java b/src/test/java/dev/vality/orgmanager/service/BouncerServiceImplTest.java index 4a89c489..3ca63a8a 100644 --- a/src/test/java/dev/vality/orgmanager/service/BouncerServiceImplTest.java +++ b/src/test/java/dev/vality/orgmanager/service/BouncerServiceImplTest.java @@ -1,6 +1,12 @@ package dev.vality.orgmanager.service; -import dev.vality.bouncer.decisions.*; +import dev.vality.bouncer.decisions.ArbiterSrv; +import dev.vality.bouncer.decisions.Context; +import dev.vality.bouncer.decisions.Judgement; +import dev.vality.bouncer.decisions.Resolution; +import dev.vality.bouncer.decisions.ResolutionAllowed; +import dev.vality.bouncer.decisions.ResolutionRestricted; +import dev.vality.bouncer.decisions.RulesetNotFound; import dev.vality.orgmanagement.UserNotFound; import dev.vality.orgmanager.TestObjectFactory; import dev.vality.orgmanager.config.properties.BouncerProperties; @@ -11,16 +17,18 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.mockito.junit.jupiter.MockitoExtension; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; -@ExtendWith(SpringExtension.class) +@ExtendWith(MockitoExtension.class) class BouncerServiceImplTest { @Mock @@ -89,4 +97,4 @@ void havePrivilegesWithAllowedResolution() throws TException { assertTrue(result); } -} \ No newline at end of file +} diff --git a/src/test/java/dev/vality/orgmanager/service/ResourceAccessServiceImplTest.java b/src/test/java/dev/vality/orgmanager/service/ResourceAccessServiceImplTest.java index 28a09f66..3f5418ba 100644 --- a/src/test/java/dev/vality/orgmanager/service/ResourceAccessServiceImplTest.java +++ b/src/test/java/dev/vality/orgmanager/service/ResourceAccessServiceImplTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.mockito.junit.jupiter.MockitoExtension; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -21,7 +21,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -@ExtendWith(SpringExtension.class) +@ExtendWith(MockitoExtension.class) class ResourceAccessServiceImplTest { private AccessProperties accessProperties; @@ -75,7 +75,6 @@ void checkJoinOrgRightsWithNotExistOrg() { ResourceDto resource = ResourceDto.builder() .invitationToken(TestObjectFactory.randomString()) .build(); - when(bouncerService.havePrivileges(any(BouncerContextDto.class))).thenReturn(true); when(organizationService.getOrgIdByInvitationToken(resource.getInvitationToken())) .thenThrow(new ResourceNotFoundException()); @@ -120,7 +119,6 @@ void checkMemberRoleRightsWithNotExistRole() { ResourceDto resource = ResourceDto.builder() .memberRoleId(TestObjectFactory.randomString()) .build(); - when(bouncerService.havePrivileges(any(BouncerContextDto.class))).thenReturn(true); when(memberRoleService.findById(resource.getMemberRoleId())) .thenThrow(new ResourceNotFoundException()); @@ -154,4 +152,4 @@ void checkInvitationRights() { assertDoesNotThrow(() -> resourceAccessService.checkRights(resource)); } -} \ No newline at end of file +} diff --git a/src/test/resources/wiremock.properties b/src/test/resources/wiremock.properties index d56ce677..83d00cf4 100644 --- a/src/test/resources/wiremock.properties +++ b/src/test/resources/wiremock.properties @@ -1,2 +1 @@ -wiremock.server.baseUrl=http://localhost:${wiremock.server.port} keycloak.auth-server-url=${wiremock.server.baseUrl}/auth From 753e7c37bf3fb207b043dd1deb49dd9b87f99071 Mon Sep 17 00:00:00 2001 From: jvmdev4 Date: Mon, 24 Aug 2026 15:09:05 +0400 Subject: [PATCH 2/4] fix --- .../dev/vality/orgmanager/util/JsonCodec.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/dev/vality/orgmanager/util/JsonCodec.java diff --git a/src/main/java/dev/vality/orgmanager/util/JsonCodec.java b/src/main/java/dev/vality/orgmanager/util/JsonCodec.java new file mode 100644 index 00000000..b2a6a270 --- /dev/null +++ b/src/main/java/dev/vality/orgmanager/util/JsonCodec.java @@ -0,0 +1,27 @@ +package dev.vality.orgmanager.util; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.json.JsonMapper; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class JsonCodec { + + private final JsonMapper jsonMapper; + + @SneakyThrows(JacksonException.class) + public String toJson(Object data) { + return jsonMapper.writeValueAsString(data); + } + + @SuppressWarnings("rawtypes") + @SneakyThrows(JacksonException.class) + public Map toMap(String json) { + return jsonMapper.readValue(json, Map.class); + } +} From 819c9ff1376f6083c653e588686954e1ab738a9e Mon Sep 17 00:00:00 2001 From: jvmdev4 Date: Tue, 25 Aug 2026 12:52:33 +0400 Subject: [PATCH 3/4] impl --- pom.xml | 2 +- .../converter/AdminManagementConverter.java | 148 +++++++ .../orgmanager/entity/OrganizationEntity.java | 2 + .../repository/OrganizationRepository.java | 14 + .../service/AdminManagementService.java | 393 ++++++++++++++++++ .../orgmanager/service/UserServiceImpl.java | 6 + .../servlet/AdminManagementServlet.java | 34 ++ .../V10__add_organization_status.sql | 2 + .../service/AdminManagementServiceTest.java | 231 ++++++++++ .../service/UserServiceImplTest.java | 15 + 10 files changed, 846 insertions(+), 1 deletion(-) create mode 100644 src/main/java/dev/vality/orgmanager/converter/AdminManagementConverter.java create mode 100644 src/main/java/dev/vality/orgmanager/service/AdminManagementService.java create mode 100644 src/main/java/dev/vality/orgmanager/servlet/AdminManagementServlet.java create mode 100644 src/main/resources/db/migration/V10__add_organization_status.sql create mode 100644 src/test/java/dev/vality/orgmanager/service/AdminManagementServiceTest.java diff --git a/pom.xml b/pom.xml index cf8165f1..e12c770b 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ dev.vality org-management-proto - 1.18-6b1eb95 + 1.19-e5d3e5f dev.vality diff --git a/src/main/java/dev/vality/orgmanager/converter/AdminManagementConverter.java b/src/main/java/dev/vality/orgmanager/converter/AdminManagementConverter.java new file mode 100644 index 00000000..476e3989 --- /dev/null +++ b/src/main/java/dev/vality/orgmanager/converter/AdminManagementConverter.java @@ -0,0 +1,148 @@ +package dev.vality.orgmanager.converter; + +import dev.vality.orgmanagement.Invitation; +import dev.vality.orgmanagement.InvitationStatus; +import dev.vality.orgmanagement.Member; +import dev.vality.orgmanagement.MemberRole; +import dev.vality.orgmanagement.Organization; +import dev.vality.orgmanagement.OrganizationRole; +import dev.vality.orgmanagement.OrganizationStatus; +import dev.vality.orgmanagement.RoleAssignment; +import dev.vality.orgmanagement.RoleScope; +import dev.vality.orgmanager.entity.InvitationEntity; +import dev.vality.orgmanager.entity.MemberEntity; +import dev.vality.orgmanager.entity.MemberRoleEntity; +import dev.vality.orgmanager.entity.OrganizationEntity; +import dev.vality.orgmanager.entity.OrganizationRoleEntity; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +@Component +public class AdminManagementConverter { + + public Organization toOrganization(OrganizationEntity entity) { + Organization organization = new Organization( + entity.getId(), + entity.getParty(), + entity.getOwner(), + entity.getName(), + formatDateTime(entity.getCreatedAt()), + toOrganizationStatus(entity.getStatus())); + if (entity.getMetadata() != null) { + organization.setMetadata(entity.getMetadata()); + } + return organization; + } + + public Member toMember(MemberEntity entity, String organizationId) { + List roles = collectionOrEmpty(entity.getRoles()).stream() + .filter(MemberRoleEntity::isActive) + .filter(role -> organizationId.equals(role.getOrganizationId())) + .sorted(Comparator.comparing(MemberRoleEntity::getId)) + .map(this::toMemberRole) + .toList(); + Member member = new Member(entity.getId(), roles); + if (entity.getEmail() != null) { + member.setEmail(entity.getEmail()); + } + return member; + } + + public MemberRole toMemberRole(MemberRoleEntity entity) { + MemberRole role = new MemberRole(entity.getId(), entity.getRoleId()); + if (entity.getScopeId() != null) { + role.setScope(toRoleScope(entity)); + } + return role; + } + + public RoleAssignment toRoleAssignment(MemberRoleEntity entity) { + RoleAssignment assignment = new RoleAssignment(entity.getRoleId()); + if (entity.getScopeId() != null) { + assignment.setScope(toRoleScope(entity)); + } + return assignment; + } + + public Invitation toInvitation(InvitationEntity entity) { + List roles = collectionOrEmpty(entity.getInviteeRoles()).stream() + .sorted(Comparator.comparing(MemberRoleEntity::getId)) + .map(this::toRoleAssignment) + .toList(); + Invitation invitation = new Invitation( + entity.getId(), + entity.getOrganizationId(), + formatDateTime(entity.getCreatedAt()), + formatDateTime(entity.getExpiresAt()), + entity.getInviteeContactEmail(), + roles, + effectiveInvitationStatus(entity)); + if (entity.getMetadata() != null) { + invitation.setMetadata(entity.getMetadata()); + } + if (entity.getAcceptedAt() != null) { + invitation.setAcceptedAt(formatDateTime(entity.getAcceptedAt())); + } + if (entity.getAcceptedMemberId() != null) { + invitation.setAcceptedMemberId(entity.getAcceptedMemberId()); + } + if (entity.getRevokedAt() != null) { + invitation.setRevokedAt(formatDateTime(entity.getRevokedAt())); + } + if (entity.getRevocationReason() != null) { + invitation.setRevocationReason(entity.getRevocationReason()); + } + return invitation; + } + + public InvitationStatus effectiveInvitationStatus(InvitationEntity entity) { + InvitationStatus storedStatus = toInvitationStatus(entity.getStatus()); + if (storedStatus == InvitationStatus.pending && entity.isExpired()) { + return InvitationStatus.expired; + } + return storedStatus; + } + + public OrganizationRole toOrganizationRole(OrganizationRoleEntity entity) { + List scopeIds = collectionOrEmpty(entity.getPossibleScopes()).stream() + .map(scope -> scope.getId()) + .filter(Objects::nonNull) + .sorted() + .toList(); + return new OrganizationRole(entity.getRoleId(), entity.getName(), scopeIds); + } + + private RoleScope toRoleScope(MemberRoleEntity entity) { + RoleScope scope = new RoleScope(entity.getScopeId()); + if (entity.getResourceId() != null) { + scope.setResourceId(entity.getResourceId()); + } + return scope; + } + + private OrganizationStatus toOrganizationStatus(String status) { + if (status == null) { + return OrganizationStatus.active; + } + return OrganizationStatus.valueOf(status.toLowerCase(Locale.ROOT)); + } + + private InvitationStatus toInvitationStatus(String status) { + return InvitationStatus.valueOf(status.toLowerCase(Locale.ROOT)); + } + + private String formatDateTime(LocalDateTime dateTime) { + return dateTime == null ? null : dateTime.atOffset(ZoneOffset.UTC).toString(); + } + + private Collection collectionOrEmpty(Collection collection) { + return collection == null ? List.of() : collection; + } +} diff --git a/src/main/java/dev/vality/orgmanager/entity/OrganizationEntity.java b/src/main/java/dev/vality/orgmanager/entity/OrganizationEntity.java index c5ccf292..e32f09eb 100644 --- a/src/main/java/dev/vality/orgmanager/entity/OrganizationEntity.java +++ b/src/main/java/dev/vality/orgmanager/entity/OrganizationEntity.java @@ -42,4 +42,6 @@ public class OrganizationEntity implements Serializable { private String owner; private String party; private String metadata; + @Builder.Default + private String status = "active"; } diff --git a/src/main/java/dev/vality/orgmanager/repository/OrganizationRepository.java b/src/main/java/dev/vality/orgmanager/repository/OrganizationRepository.java index a5581c2c..808daa29 100644 --- a/src/main/java/dev/vality/orgmanager/repository/OrganizationRepository.java +++ b/src/main/java/dev/vality/orgmanager/repository/OrganizationRepository.java @@ -3,6 +3,7 @@ import dev.vality.orgmanager.entity.OrganizationEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.NativeQuery; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import java.util.List; @@ -36,4 +37,17 @@ public interface OrganizationRepository extends JpaRepository findAllByOwner(String owner); + boolean existsByParty(String party); + + List findAllByOrderByIdDesc(Pageable pageable); + + List findByIdLessThanOrderByIdDesc(String id, Pageable pageable); + + List findByStatusOrderByIdDesc(String status, Pageable pageable); + + List findByStatusAndIdLessThanOrderByIdDesc( + String status, + String id, + Pageable pageable); + } diff --git a/src/main/java/dev/vality/orgmanager/service/AdminManagementService.java b/src/main/java/dev/vality/orgmanager/service/AdminManagementService.java new file mode 100644 index 00000000..f014a7a7 --- /dev/null +++ b/src/main/java/dev/vality/orgmanager/service/AdminManagementService.java @@ -0,0 +1,393 @@ +package dev.vality.orgmanager.service; + +import dev.vality.orgmanagement.AddMemberRequest; +import dev.vality.orgmanagement.AdminManagementSrv; +import dev.vality.orgmanagement.AssignMemberRoleRequest; +import dev.vality.orgmanagement.CreateInvitationRequest; +import dev.vality.orgmanagement.CreateOrganizationRequest; +import dev.vality.orgmanagement.InvalidOrganizationState; +import dev.vality.orgmanagement.Invitation; +import dev.vality.orgmanagement.InvitationNotFound; +import dev.vality.orgmanagement.InvitationStatus; +import dev.vality.orgmanagement.ListInvitationsRequest; +import dev.vality.orgmanagement.ListOrganizationsRequest; +import dev.vality.orgmanagement.ListOrganizationsResult; +import dev.vality.orgmanagement.Member; +import dev.vality.orgmanagement.MemberNotFound; +import dev.vality.orgmanagement.MemberRole; +import dev.vality.orgmanagement.MemberRoleNotFound; +import dev.vality.orgmanagement.Organization; +import dev.vality.orgmanagement.OrganizationNotFound; +import dev.vality.orgmanagement.OrganizationRole; +import dev.vality.orgmanagement.OrganizationStatus; +import dev.vality.orgmanagement.PartyAlreadyBound; +import dev.vality.orgmanagement.RevokeInvitationRequest; +import dev.vality.orgmanager.config.properties.InviteTokenProperties; +import dev.vality.orgmanager.converter.AdminManagementConverter; +import dev.vality.orgmanager.entity.InvitationEntity; +import dev.vality.orgmanager.entity.MemberEntity; +import dev.vality.orgmanager.entity.MemberRoleEntity; +import dev.vality.orgmanager.entity.OrganizationEntity; +import dev.vality.orgmanager.entity.OrganizationRoleEntity; +import dev.vality.orgmanager.repository.InvitationRepository; +import dev.vality.orgmanager.repository.MemberRepository; +import dev.vality.orgmanager.repository.MemberRoleRepository; +import dev.vality.orgmanager.repository.OrganizationRepository; +import dev.vality.orgmanager.repository.OrganizationRoleRepository; +import dev.vality.swag.organizations.model.InvitationStatusName; +import dev.vality.swag.organizations.model.InviteeContact; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminManagementService implements AdminManagementSrv.Iface { + + static final int DEFAULT_ORGANIZATION_LIMIT = 20; + static final int MAX_ORGANIZATION_LIMIT = 1000; + + private static final String ACTIVE_STATUS = OrganizationStatus.active.name(); + private static final String DEACTIVATED_STATUS = OrganizationStatus.deactivated.name(); + + private final OrganizationRepository organizationRepository; + private final MemberRepository memberRepository; + private final MemberRoleRepository memberRoleRepository; + private final OrganizationRoleRepository organizationRoleRepository; + private final InvitationRepository invitationRepository; + private final AdminManagementConverter converter; + private final InviteTokenProperties inviteTokenProperties; + private final MailMessageSender mailMessageSender; + + @Override + @Transactional + public Organization createOrganization(CreateOrganizationRequest request) throws PartyAlreadyBound { + if (organizationRepository.existsByParty(request.getPartyId())) { + throw new PartyAlreadyBound(); + } + OrganizationEntity entity = OrganizationEntity.builder() + .id(UUID.randomUUID().toString()) + .party(request.getPartyId()) + .owner(request.getOwnerId()) + .name(request.getName()) + .metadata(request.getMetadata()) + .createdAt(LocalDateTime.now()) + .status(ACTIVE_STATUS) + .members(new HashSet<>()) + .roles(new HashSet<>()) + .build(); + try { + return converter.toOrganization(organizationRepository.saveAndFlush(entity)); + } catch (DataIntegrityViolationException exception) { + throw new PartyAlreadyBound(); + } + } + + @Override + public Organization getOrganization(String organizationId) throws OrganizationNotFound { + return converter.toOrganization(findOrganization(organizationId)); + } + + @Override + public ListOrganizationsResult listOrganizations(ListOrganizationsRequest request) { + ListOrganizationsRequest safeRequest = request == null ? new ListOrganizationsRequest() : request; + int limit = organizationLimit(safeRequest); + Pageable pageable = PageRequest.of(0, limit + 1); + List entities = findOrganizations(safeRequest, pageable); + + String continuationToken = null; + if (entities.size() > limit) { + entities = new ArrayList<>(entities.subList(0, limit)); + continuationToken = entities.get(entities.size() - 1).getId(); + } + ListOrganizationsResult result = new ListOrganizationsResult( + entities.stream().map(converter::toOrganization).toList()); + if (continuationToken != null) { + result.setContinuationToken(continuationToken); + } + return result; + } + + @Override + @Transactional + public Organization renameOrganization(String organizationId, String name) throws OrganizationNotFound { + OrganizationEntity organization = findOrganization(organizationId); + organization.setName(name); + return converter.toOrganization(organizationRepository.save(organization)); + } + + @Override + @Transactional + public Organization deactivateOrganization(String organizationId) + throws OrganizationNotFound, InvalidOrganizationState { + return changeOrganizationStatus(organizationId, ACTIVE_STATUS, DEACTIVATED_STATUS); + } + + @Override + @Transactional + public Organization activateOrganization(String organizationId) + throws OrganizationNotFound, InvalidOrganizationState { + return changeOrganizationStatus(organizationId, DEACTIVATED_STATUS, ACTIVE_STATUS); + } + + @Override + public Member getMember(String organizationId, String userId) + throws OrganizationNotFound, MemberNotFound { + OrganizationEntity organization = findOrganization(organizationId); + return converter.toMember(findMember(organization, userId), organizationId); + } + + @Override + public List listMembers(String organizationId) throws OrganizationNotFound { + OrganizationEntity organization = findOrganization(organizationId); + return collectionOrEmpty(organization.getMembers()).stream() + .sorted(Comparator.comparing(MemberEntity::getId)) + .map(member -> converter.toMember(member, organizationId)) + .toList(); + } + + @Override + @Transactional + public Member addMember(String organizationId, AddMemberRequest request) throws OrganizationNotFound { + OrganizationEntity organization = findOrganization(organizationId); + MemberEntity member = memberRepository.findById(request.getUserId()) + .orElseGet(() -> MemberEntity.builder() + .id(request.getUserId()) + .roles(new HashSet<>()) + .organizations(new HashSet<>()) + .build()); + if (request.isSetEmail()) { + member.setEmail(request.getEmail()); + } + member = memberRepository.save(member); + Set members = new HashSet<>(collectionOrEmpty(organization.getMembers())); + members.add(member); + organization.setMembers(members); + organizationRepository.save(organization); + return converter.toMember(member, organizationId); + } + + @Override + @Transactional + public void removeMember(String organizationId, String userId) + throws OrganizationNotFound, MemberNotFound { + OrganizationEntity organization = findOrganization(organizationId); + MemberEntity member = findMember(organization, userId); + + Set retainedRoles = new HashSet<>(); + List removedRoles = new ArrayList<>(); + for (MemberRoleEntity role : collectionOrEmpty(member.getRoles())) { + if (organizationId.equals(role.getOrganizationId()) && role.isActive()) { + role.setActive(false); + removedRoles.add(role); + } else { + retainedRoles.add(role); + } + } + member.setRoles(retainedRoles); + memberRoleRepository.saveAll(removedRoles); + memberRepository.save(member); + + Set members = new HashSet<>(collectionOrEmpty(organization.getMembers())); + members.remove(member); + organization.setMembers(members); + organizationRepository.save(organization); + } + + @Override + @Transactional + public MemberRole assignMemberRole( + String organizationId, + String userId, + AssignMemberRoleRequest request) throws OrganizationNotFound, MemberNotFound { + OrganizationEntity organization = findOrganization(organizationId); + MemberEntity member = findMember(organization, userId); + MemberRoleEntity role = toMemberRoleEntity(organizationId, request.getRoleId(), request.getScope()); + role = memberRoleRepository.save(role); + Set roles = new HashSet<>(collectionOrEmpty(member.getRoles())); + roles.add(role); + member.setRoles(roles); + memberRepository.save(member); + return converter.toMemberRole(role); + } + + @Override + @Transactional + public void removeMemberRole(String organizationId, String userId, String memberRoleId) + throws OrganizationNotFound, MemberNotFound, MemberRoleNotFound { + OrganizationEntity organization = findOrganization(organizationId); + MemberEntity member = findMember(organization, userId); + MemberRoleEntity role = collectionOrEmpty(member.getRoles()).stream() + .filter(candidate -> candidate.getId().equals(memberRoleId)) + .filter(candidate -> organizationId.equals(candidate.getOrganizationId())) + .filter(MemberRoleEntity::isActive) + .findFirst() + .orElseThrow(MemberRoleNotFound::new); + role.setActive(false); + Set roles = new HashSet<>(collectionOrEmpty(member.getRoles())); + roles.remove(role); + member.setRoles(roles); + memberRoleRepository.save(role); + memberRepository.save(member); + } + + @Override + public OrganizationRole getOrganizationRole(String organizationId, String roleId) + throws OrganizationNotFound { + findOrganization(organizationId); + OrganizationRoleEntity role = organizationRoleRepository.findByOrganizationIdAndRoleId(organizationId, roleId) + .orElseThrow(OrganizationNotFound::new); + return converter.toOrganizationRole(role); + } + + @Override + public List listOrganizationRoles(String organizationId) throws OrganizationNotFound { + OrganizationEntity organization = findOrganization(organizationId); + return collectionOrEmpty(organization.getRoles()).stream() + .sorted(Comparator.comparing(OrganizationRoleEntity::getRoleId)) + .map(converter::toOrganizationRole) + .toList(); + } + + @Override + @Transactional + public Invitation createInvitation(String organizationId, CreateInvitationRequest request) + throws OrganizationNotFound { + findOrganization(organizationId); + Set roles = request.getRoles().stream() + .map(role -> toMemberRoleEntity(organizationId, role.getRoleId(), role.getScope())) + .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); + LocalDateTime now = LocalDateTime.now(); + InvitationEntity invitation = InvitationEntity.builder() + .id(UUID.randomUUID().toString()) + .organizationId(organizationId) + .createdAt(now) + .expiresAt(now.plusDays(inviteTokenProperties.getLifeTimeInDays())) + .acceptToken(UUID.randomUUID().toString()) + .inviteeContactType(InviteeContact.TypeEnum.E_MAIL.getValue()) + .inviteeContactEmail(request.getEmail()) + .inviteeRoles(roles) + .metadata(request.getMetadata()) + .status(InvitationStatusName.PENDING.getValue()) + .build(); + invitation = invitationRepository.save(invitation); + mailMessageSender.send(invitation.getAcceptToken(), invitation.getInviteeContactEmail()); + return converter.toInvitation(invitation); + } + + @Override + public Invitation getInvitation(String organizationId, String invitationId) + throws OrganizationNotFound, InvitationNotFound { + findOrganization(organizationId); + InvitationEntity invitation = invitationRepository.findByIdAndOrganizationId(invitationId, organizationId) + .orElseThrow(InvitationNotFound::new); + return converter.toInvitation(invitation); + } + + @Override + public List listInvitations(String organizationId, ListInvitationsRequest request) + throws OrganizationNotFound { + findOrganization(organizationId); + InvitationStatus status = request == null ? null : request.getStatus(); + return invitationRepository.findByOrganizationId(organizationId).stream() + .filter(invitation -> status == null || converter.effectiveInvitationStatus(invitation) == status) + .sorted(Comparator.comparing(InvitationEntity::getCreatedAt).reversed()) + .map(converter::toInvitation) + .toList(); + } + + @Override + @Transactional + public void revokeInvitation(String organizationId, String invitationId, RevokeInvitationRequest request) + throws OrganizationNotFound, InvitationNotFound, InvalidOrganizationState { + findOrganization(organizationId); + InvitationEntity invitation = invitationRepository.findByIdAndOrganizationId(invitationId, organizationId) + .orElseThrow(InvitationNotFound::new); + InvitationStatus status = converter.effectiveInvitationStatus(invitation); + if (status != InvitationStatus.pending) { + throw new InvalidOrganizationState("Only a pending invitation can be revoked"); + } + invitation.setStatus(InvitationStatusName.REVOKED.getValue()); + invitation.setRevocationReason(request.getReason()); + invitation.setRevokedAt(LocalDateTime.now()); + invitationRepository.save(invitation); + } + + private List findOrganizations(ListOrganizationsRequest request, Pageable pageable) { + String token = request.getContinuationToken(); + OrganizationStatus status = request.getStatus(); + if (status != null && token != null) { + return organizationRepository.findByStatusAndIdLessThanOrderByIdDesc(status.name(), token, pageable); + } + if (status != null) { + return organizationRepository.findByStatusOrderByIdDesc(status.name(), pageable); + } + if (token != null) { + return organizationRepository.findByIdLessThanOrderByIdDesc(token, pageable); + } + return organizationRepository.findAllByOrderByIdDesc(pageable); + } + + private int organizationLimit(ListOrganizationsRequest request) { + if (!request.isSetLimit() || request.getLimit() <= 0) { + return DEFAULT_ORGANIZATION_LIMIT; + } + return Math.min(request.getLimit(), MAX_ORGANIZATION_LIMIT); + } + + private Organization changeOrganizationStatus(String organizationId, String expected, String target) + throws OrganizationNotFound, InvalidOrganizationState { + OrganizationEntity organization = findOrganization(organizationId); + String current = organization.getStatus() == null ? ACTIVE_STATUS : organization.getStatus(); + if (!expected.equals(current)) { + throw new InvalidOrganizationState( + "Expected organization status " + expected + ", but was " + current); + } + organization.setStatus(target); + return converter.toOrganization(organizationRepository.save(organization)); + } + + private OrganizationEntity findOrganization(String organizationId) throws OrganizationNotFound { + return organizationRepository.findById(organizationId) + .orElseThrow(OrganizationNotFound::new); + } + + private MemberEntity findMember(OrganizationEntity organization, String userId) throws MemberNotFound { + return collectionOrEmpty(organization.getMembers()).stream() + .filter(member -> member.getId().equals(userId)) + .findFirst() + .orElseThrow(MemberNotFound::new); + } + + private MemberRoleEntity toMemberRoleEntity( + String organizationId, + String roleId, + dev.vality.orgmanagement.RoleScope scope) { + return MemberRoleEntity.builder() + .id(UUID.randomUUID().toString()) + .organizationId(organizationId) + .roleId(roleId) + .scopeId(scope == null ? null : scope.getScopeId()) + .resourceId(scope == null ? null : scope.getResourceId()) + .active(true) + .build(); + } + + private Collection collectionOrEmpty(Collection collection) { + return collection == null ? List.of() : collection; + } +} diff --git a/src/main/java/dev/vality/orgmanager/service/UserServiceImpl.java b/src/main/java/dev/vality/orgmanager/service/UserServiceImpl.java index 8d17c590..bed9546e 100644 --- a/src/main/java/dev/vality/orgmanager/service/UserServiceImpl.java +++ b/src/main/java/dev/vality/orgmanager/service/UserServiceImpl.java @@ -34,7 +34,13 @@ public UserInfo findById(String id) { return new UserInfo( user.orElse(null), Stream.concat(memberOrganizations.stream(), ownedOrganizations.stream()) + .filter(this::isActive) .collect(Collectors.toSet()) ); } + + private boolean isActive(OrganizationEntity organization) { + return organization.getStatus() == null + || !organization.getStatus().equalsIgnoreCase("deactivated"); + } } diff --git a/src/main/java/dev/vality/orgmanager/servlet/AdminManagementServlet.java b/src/main/java/dev/vality/orgmanager/servlet/AdminManagementServlet.java new file mode 100644 index 00000000..3894172d --- /dev/null +++ b/src/main/java/dev/vality/orgmanager/servlet/AdminManagementServlet.java @@ -0,0 +1,34 @@ +package dev.vality.orgmanager.servlet; + +import dev.vality.orgmanagement.AdminManagementSrv; +import dev.vality.woody.thrift.impl.http.THServiceBuilder; +import jakarta.servlet.GenericServlet; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebServlet; +import lombok.RequiredArgsConstructor; + +import java.io.IOException; + +@WebServlet("/admin-management") +@RequiredArgsConstructor +public class AdminManagementServlet extends GenericServlet { + + private final AdminManagementSrv.Iface adminManagement; + private Servlet thriftServlet; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + thriftServlet = new THServiceBuilder() + .build(AdminManagementSrv.Iface.class, adminManagement); + } + + @Override + public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { + thriftServlet.service(request, response); + } +} diff --git a/src/main/resources/db/migration/V10__add_organization_status.sql b/src/main/resources/db/migration/V10__add_organization_status.sql new file mode 100644 index 00000000..795b1800 --- /dev/null +++ b/src/main/resources/db/migration/V10__add_organization_status.sql @@ -0,0 +1,2 @@ +ALTER TABLE org_manager.organization + ADD COLUMN IF NOT EXISTS status CHARACTER VARYING NOT NULL DEFAULT 'active'; diff --git a/src/test/java/dev/vality/orgmanager/service/AdminManagementServiceTest.java b/src/test/java/dev/vality/orgmanager/service/AdminManagementServiceTest.java new file mode 100644 index 00000000..9e45345a --- /dev/null +++ b/src/test/java/dev/vality/orgmanager/service/AdminManagementServiceTest.java @@ -0,0 +1,231 @@ +package dev.vality.orgmanager.service; + +import dev.vality.orgmanagement.AddMemberRequest; +import dev.vality.orgmanagement.AssignMemberRoleRequest; +import dev.vality.orgmanagement.CreateInvitationRequest; +import dev.vality.orgmanagement.CreateOrganizationRequest; +import dev.vality.orgmanagement.InvalidOrganizationState; +import dev.vality.orgmanagement.InvitationStatus; +import dev.vality.orgmanagement.ListInvitationsRequest; +import dev.vality.orgmanagement.ListOrganizationsRequest; +import dev.vality.orgmanagement.MemberRoleNotFound; +import dev.vality.orgmanagement.OrganizationStatus; +import dev.vality.orgmanagement.PartyAlreadyBound; +import dev.vality.orgmanagement.RevokeInvitationRequest; +import dev.vality.orgmanager.config.properties.InviteTokenProperties; +import dev.vality.orgmanager.converter.AdminManagementConverter; +import dev.vality.orgmanager.entity.InvitationEntity; +import dev.vality.orgmanager.entity.MemberEntity; +import dev.vality.orgmanager.entity.MemberRoleEntity; +import dev.vality.orgmanager.entity.OrganizationEntity; +import dev.vality.orgmanager.repository.InvitationRepository; +import dev.vality.orgmanager.repository.MemberRepository; +import dev.vality.orgmanager.repository.MemberRoleRepository; +import dev.vality.orgmanager.repository.OrganizationRepository; +import dev.vality.orgmanager.repository.OrganizationRoleRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AdminManagementServiceTest { + + @Mock + private OrganizationRepository organizationRepository; + @Mock + private MemberRepository memberRepository; + @Mock + private MemberRoleRepository memberRoleRepository; + @Mock + private OrganizationRoleRepository organizationRoleRepository; + @Mock + private InvitationRepository invitationRepository; + @Mock + private InviteTokenProperties inviteTokenProperties; + @Mock + private MailMessageSender mailMessageSender; + + private AdminManagementService service; + + @BeforeEach + void setUp() { + service = new AdminManagementService( + organizationRepository, + memberRepository, + memberRoleRepository, + organizationRoleRepository, + invitationRepository, + new AdminManagementConverter(), + inviteTokenProperties, + mailMessageSender); + } + + @Test + void shouldCreateOrganizationWithRequestedPartyAndOwner() throws Exception { + CreateOrganizationRequest request = new CreateOrganizationRequest("party", "owner", "name") + .setMetadata("{\"source\":\"admin\"}"); + when(organizationRepository.saveAndFlush(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + var result = service.createOrganization(request); + + assertEquals("party", result.getPartyId()); + assertEquals("owner", result.getOwnerId()); + assertEquals(OrganizationStatus.active, result.getStatus()); + assertEquals(request.getMetadata(), result.getMetadata()); + } + + @Test + void shouldRejectPartyAlreadyBoundToOrganization() { + CreateOrganizationRequest request = new CreateOrganizationRequest("party", "owner", "name"); + when(organizationRepository.existsByParty("party")).thenReturn(true); + + assertThrows(PartyAlreadyBound.class, () -> service.createOrganization(request)); + verifyNoInteractions(memberRepository); + } + + @Test + void shouldReturnContinuationTokenWhenMoreOrganizationsExist() { + ListOrganizationsRequest request = new ListOrganizationsRequest().setLimit(2); + List entities = List.of( + organization("c", OrganizationStatus.active), + organization("b", OrganizationStatus.active), + organization("a", OrganizationStatus.active)); + when(organizationRepository.findAllByOrderByIdDesc(any(Pageable.class))).thenReturn(entities); + + var result = service.listOrganizations(request); + + assertEquals(List.of("c", "b"), result.getOrganizations().stream().map(it -> it.getId()).toList()); + assertEquals("b", result.getContinuationToken()); + } + + @Test + void shouldValidateOrganizationStatusTransitions() throws Exception { + OrganizationEntity organization = organization("org", OrganizationStatus.active); + when(organizationRepository.findById("org")).thenReturn(Optional.of(organization)); + when(organizationRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + var deactivated = service.deactivateOrganization("org"); + + assertEquals(OrganizationStatus.deactivated, deactivated.getStatus()); + assertThrows(InvalidOrganizationState.class, () -> service.deactivateOrganization("org")); + assertEquals(OrganizationStatus.active, service.activateOrganization("org").getStatus()); + } + + @Test + void shouldAddMemberAndManageRoleWithoutLastRoleRestriction() throws Exception { + OrganizationEntity organization = organization("org", OrganizationStatus.active); + when(organizationRepository.findById("org")).thenReturn(Optional.of(organization)); + when(organizationRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(memberRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + when(memberRoleRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + var member = service.addMember("org", new AddMemberRequest("user").setEmail("user@example.com")); + var role = service.assignMemberRole("org", "user", new AssignMemberRoleRequest("manager")); + service.removeMemberRole("org", "user", role.getId()); + + assertEquals("user@example.com", member.getEmail()); + assertTrue(service.getMember("org", "user").getRoles().isEmpty()); + } + + @Test + void shouldRejectRemovingRoleFromAnotherOrganization() throws Exception { + MemberRoleEntity role = MemberRoleEntity.builder() + .id("role") + .organizationId("another-org") + .roleId("manager") + .active(true) + .build(); + MemberEntity member = MemberEntity.builder() + .id("user") + .roles(new HashSet<>(Set.of(role))) + .build(); + OrganizationEntity organization = organization("org", OrganizationStatus.active); + organization.setMembers(new HashSet<>(Set.of(member))); + when(organizationRepository.findById("org")).thenReturn(Optional.of(organization)); + + assertThrows(MemberRoleNotFound.class, + () -> service.removeMemberRole("org", "user", "role")); + } + + @Test + void shouldUseEffectiveInvitationStatusForListingAndRevocation() throws Exception { + OrganizationEntity organization = organization("org", OrganizationStatus.active); + InvitationEntity expired = invitation("expired", LocalDateTime.now().minusMinutes(1)); + InvitationEntity pending = invitation("pending", LocalDateTime.now().plusDays(1)); + when(organizationRepository.findById("org")).thenReturn(Optional.of(organization)); + when(invitationRepository.findByOrganizationId("org")).thenReturn(List.of(expired, pending)); + when(invitationRepository.findByIdAndOrganizationId("expired", "org")).thenReturn(Optional.of(expired)); + when(invitationRepository.findByIdAndOrganizationId("pending", "org")).thenReturn(Optional.of(pending)); + + var result = service.listInvitations( + "org", + new ListInvitationsRequest().setStatus(InvitationStatus.expired)); + + assertEquals(List.of("expired"), result.stream().map(it -> it.getId()).toList()); + assertThrows(InvalidOrganizationState.class, + () -> service.revokeInvitation("org", "expired", new RevokeInvitationRequest("obsolete"))); + service.revokeInvitation("org", "pending", new RevokeInvitationRequest("obsolete")); + assertEquals("Revoked", pending.getStatus()); + assertEquals("obsolete", pending.getRevocationReason()); + verify(invitationRepository).save(pending); + } + + @Test + void shouldCreateInvitationAndSendMail() throws Exception { + OrganizationEntity organization = organization("org", OrganizationStatus.active); + when(organizationRepository.findById("org")).thenReturn(Optional.of(organization)); + when(inviteTokenProperties.getLifeTimeInDays()).thenReturn(7L); + when(invitationRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + CreateInvitationRequest request = new CreateInvitationRequest("user@example.com", new ArrayList<>()); + + var result = service.createInvitation("org", request); + + assertEquals(InvitationStatus.pending, result.getStatus()); + assertEquals("user@example.com", result.getEmail()); + verify(mailMessageSender).send(any(String.class), eq("user@example.com")); + } + + private OrganizationEntity organization(String id, OrganizationStatus status) { + return OrganizationEntity.builder() + .id(id) + .party("party-" + id) + .owner("owner-" + id) + .name("name-" + id) + .createdAt(LocalDateTime.now()) + .status(status.name()) + .members(new HashSet<>()) + .roles(new HashSet<>()) + .build(); + } + + private InvitationEntity invitation(String id, LocalDateTime expiresAt) { + return InvitationEntity.builder() + .id(id) + .organizationId("org") + .createdAt(LocalDateTime.now()) + .expiresAt(expiresAt) + .inviteeContactEmail("user@example.com") + .inviteeRoles(new HashSet<>()) + .status("Pending") + .build(); + } +} diff --git a/src/test/java/dev/vality/orgmanager/service/UserServiceImplTest.java b/src/test/java/dev/vality/orgmanager/service/UserServiceImplTest.java index 3fe8f5cb..be52e725 100644 --- a/src/test/java/dev/vality/orgmanager/service/UserServiceImplTest.java +++ b/src/test/java/dev/vality/orgmanager/service/UserServiceImplTest.java @@ -124,4 +124,19 @@ void findMemberUserWithSameMemberAndOwnedOrganizations() { assertEquals(1, userInfo.getOrganizations().size()); assertEquals(organization.getId(), userInfo.getOrganizations().iterator().next().getId()); } + + @Test + void doesNotExposeDeactivatedOrganizationInUserContext() { + String memberId = TestObjectFactory.randomString(); + MemberEntity member = TestObjectFactory.testMemberEntity(memberId); + OrganizationEntity organization = TestObjectFactory.buildOrganization(member); + organization.setStatus("deactivated"); + memberRepository.save(member); + organizationRepository.save(organization); + + UserInfo userInfo = userService.findById(memberId); + + assertEquals(memberId, userInfo.getMember().getId()); + assertTrue(userInfo.getOrganizations().isEmpty()); + } } From 43814591d614d4d4efc03b418f72cd0e7a9fbd7a Mon Sep 17 00:00:00 2001 From: jvmdev4 Date: Thu, 27 Aug 2026 13:34:18 +0400 Subject: [PATCH 4/4] bump --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1aadee48..b1695345 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ dev.vality org-management-proto - 1.18-6b1eb95 + 1.19-e5d3e5f dev.vality