sequential transaction-id (#178)
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/178
This commit is contained in:
		| @@ -1,13 +1,13 @@ | |||||||
| classDiagram | classDiagram | ||||||
|     direction LR |     direction LR | ||||||
|  |  | ||||||
|     OfficePerson o.. "*" LoginCredentials |     OfficePerson o.. "*" Credentials | ||||||
|     LoginCredentials "1" o-- "1" RbacSubject |     Credentials "1" o-- "1" RbacSubject | ||||||
|  |  | ||||||
|     LoginContext "1..n" --o "1" LoginContextMapping |     CredentialsContext "1..n" --o "1" CredentialsContextMapping | ||||||
|     LoginCredentials "1..n" --o "1" LoginContextMapping |     Credentials "1..n" --o "1" CredentialsContextMapping | ||||||
|  |  | ||||||
|     class LoginCredentials{ |     class Credentials{ | ||||||
|         +twoFactorAuth: text |         +twoFactorAuth: text | ||||||
|         +telephonePassword: text |         +telephonePassword: text | ||||||
|         +emailAdress: text |         +emailAdress: text | ||||||
| @@ -18,14 +18,14 @@ classDiagram | |||||||
|         -onboardingToken: text [w/o] |         -onboardingToken: text [w/o] | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     class LoginContext{ |     class CredentialsContext{ | ||||||
|         -type: Enum [SSH, Matrix, Mastodon, ...] |         -type: Enum [SSH, Matrix, Mastodon, ...] | ||||||
|         -qualifier: text |         -qualifier: text | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     class LoginContextMapping{ |     class CredentialsContextMapping{ | ||||||
|     } |     } | ||||||
|     note for LoginContextMapping "Assigns LoginCredentials to LoginContexts" |     note for CredentialsContextMapping "Assigns Credentials to CredentialsContexts" | ||||||
|  |  | ||||||
|     class RbacSubject{ |     class RbacSubject{ | ||||||
|         +uuid: uuid |         +uuid: uuid | ||||||
| @@ -41,9 +41,9 @@ classDiagram | |||||||
|         +salutation: text |         +salutation: text | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     style LoginContext fill:#00f,color:#fff |     style CredentialsContext fill:#00f,color:#fff | ||||||
|     style LoginContextMapping fill:#00f,color:#fff |     style CredentialsContextMapping fill:#00f,color:#fff | ||||||
|     style LoginCredentials fill:#00f,color:#fff |     style Credentials fill:#00f,color:#fff | ||||||
|  |  | ||||||
|     style RbacSubject fill:#f96,color:#fff |     style RbacSubject fill:#f96,color:#fff | ||||||
|     style OfficePerson fill:#f66,color:#000 |     style OfficePerson fill:#f66,color:#000 | ||||||
|   | |||||||
| @@ -2,16 +2,19 @@ package net.hostsharing.hsadminng.credentials; | |||||||
|  |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
|  | import io.micrometer.core.annotation.Timed; | ||||||
|  | import net.hostsharing.hsadminng.config.NoSecurityRequirement; | ||||||
| import net.hostsharing.hsadminng.context.Context; | import net.hostsharing.hsadminng.context.Context; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.api.LoginContextsApi; | import net.hostsharing.hsadminng.credentials.generated.api.v1.api.ContextsApi; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextResource; | import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResource; | ||||||
| import net.hostsharing.hsadminng.mapper.StrictMapper; | import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||||
| import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||||
| import org.springframework.http.ResponseEntity; | import org.springframework.http.ResponseEntity; | ||||||
| import org.springframework.web.bind.annotation.RestController; | import org.springframework.web.bind.annotation.RestController; | ||||||
|  |  | ||||||
| @RestController | @RestController | ||||||
| public class HsCredentialsContextsController implements LoginContextsApi { | @NoSecurityRequirement | ||||||
|  | public class HsCredentialsContextsController implements ContextsApi { | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     private Context context; |     private Context context; | ||||||
| @@ -23,11 +26,12 @@ public class HsCredentialsContextsController implements LoginContextsApi { | |||||||
|     private HsCredentialsContextRbacRepository contextRepo; |     private HsCredentialsContextRbacRepository contextRepo; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public ResponseEntity<List<LoginContextResource>> getListOfLoginContexts(final String assumedRoles) { |     @Timed("app.credentials.contexts.getListOfLoginContexts") | ||||||
|  |     public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) { | ||||||
|         context.assumeRoles(assumedRoles); |         context.assumeRoles(assumedRoles); | ||||||
|  |  | ||||||
|         final var loginContexts = contextRepo.findAll(); |         final var loginContexts = contextRepo.findAll(); | ||||||
|         final var result = mapper.mapList(loginContexts, LoginContextResource.class); |         final var result = mapper.mapList(loginContexts, ContextResource.class); | ||||||
|         return ResponseEntity.ok(result); |         return ResponseEntity.ok(result); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,11 +2,15 @@ package net.hostsharing.hsadminng.credentials; | |||||||
|  |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  |  | ||||||
|  | import io.micrometer.core.annotation.Timed; | ||||||
|  | import io.swagger.v3.oas.annotations.security.SecurityRequirement; | ||||||
|  | import net.hostsharing.hsadminng.config.MessageTranslator; | ||||||
| import net.hostsharing.hsadminng.context.Context; | import net.hostsharing.hsadminng.context.Context; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.api.LoginCredentialsApi; | import net.hostsharing.hsadminng.credentials.generated.api.v1.api.CredentialsApi; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsInsertResource; | import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsInsertResource; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsPatchResource; | import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsResource; | import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsResource; | ||||||
| import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository; | import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository; | ||||||
| import net.hostsharing.hsadminng.mapper.StrictMapper; | import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||||
| import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | ||||||
| @@ -14,8 +18,11 @@ import org.springframework.beans.factory.annotation.Autowired; | |||||||
| import org.springframework.http.ResponseEntity; | import org.springframework.http.ResponseEntity; | ||||||
| import org.springframework.web.bind.annotation.RestController; | import org.springframework.web.bind.annotation.RestController; | ||||||
|  |  | ||||||
|  | import jakarta.persistence.EntityNotFoundException; | ||||||
|  |  | ||||||
| @RestController | @RestController | ||||||
| public class HsCredentialsController implements LoginCredentialsApi { | @SecurityRequirement(name = "casTicket") | ||||||
|  | public class HsCredentialsController implements CredentialsApi { | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     private Context context; |     private Context context; | ||||||
| @@ -26,71 +33,84 @@ public class HsCredentialsController implements LoginCredentialsApi { | |||||||
|     @Autowired |     @Autowired | ||||||
|     private StrictMapper mapper; |     private StrictMapper mapper; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     private MessageTranslator messageTranslator; | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     private HsOfficePersonRbacRepository personRepo; |     private HsOfficePersonRbacRepository personRepo; | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     private HsCredentialsRepository loginCredentialsRepo; |     private HsCredentialsRepository credentialsRepo; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public ResponseEntity<LoginCredentialsResource> getSingleLoginCredentialsByUuid( |     @Timed("app.credentials.credentials.getSingleCredentialsByUuid") | ||||||
|  |     public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid( | ||||||
|             final String assumedRoles, |             final String assumedRoles, | ||||||
|             final UUID loginCredentialsUuid) { |             final UUID credentialsUuid) { | ||||||
|         context.assumeRoles(assumedRoles); |         context.assumeRoles(assumedRoles); | ||||||
|  |  | ||||||
|         final var credentials = loginCredentialsRepo.findByUuid(loginCredentialsUuid); |         final var credentials = credentialsRepo.findByUuid(credentialsUuid); | ||||||
|         final var result = mapper.map(credentials, LoginCredentialsResource.class); |         final var result = mapper.map(credentials, CredentialsResource.class); | ||||||
|         return ResponseEntity.ok(result); |         return ResponseEntity.ok(result); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public ResponseEntity<List<LoginCredentialsResource>> getListOfLoginCredentialsByPersonUuid( |     @Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid") | ||||||
|  |     public ResponseEntity<List<CredentialsResource>> getListOfCredentialsByPersonUuid( | ||||||
|             final String assumedRoles, |             final String assumedRoles, | ||||||
|             final UUID personUuid |             final UUID personUuid | ||||||
|     ) { |     ) { | ||||||
|         context.assumeRoles(assumedRoles); |         context.assumeRoles(assumedRoles); | ||||||
|  |  | ||||||
|         final var person = personRepo.findByUuid(personUuid).orElseThrow(); // FIXME: use proper exception |         final var person = personRepo.findByUuid(personUuid).orElseThrow( | ||||||
|         final var credentials = loginCredentialsRepo.findByPerson(person); |                 () -> new EntityNotFoundException( | ||||||
|         final var result = mapper.mapList(credentials, LoginCredentialsResource.class); |                         messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |         ); // FIXME: use proper exception | ||||||
|  |         final var credentials = credentialsRepo.findByPerson(person); | ||||||
|  |         final var result = mapper.mapList(credentials, CredentialsResource.class); | ||||||
|         return ResponseEntity.ok(result); |         return ResponseEntity.ok(result); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public ResponseEntity<LoginCredentialsResource> postNewLoginCredentials( |     @Timed("app.credentials.credentials.postNewCredentials") | ||||||
|  |     public ResponseEntity<CredentialsResource> postNewCredentials( | ||||||
|             final String assumedRoles, |             final String assumedRoles, | ||||||
|             final LoginCredentialsInsertResource body |             final CredentialsInsertResource body | ||||||
|     ) { |     ) { | ||||||
|         context.assumeRoles(assumedRoles); |         context.assumeRoles(assumedRoles); | ||||||
|  |  | ||||||
|         final var newLoginCredentialsEntity = mapper.map(body, HsCredentialsEntity.class); |         final var newCredentialsEntity = mapper.map(body, HsCredentialsEntity.class); | ||||||
|         final var savedLoginCredentialsEntity = loginCredentialsRepo.save(newLoginCredentialsEntity); |         final var savedCredentialsEntity = credentialsRepo.save(newCredentialsEntity); | ||||||
|         final var newLoginCredentialsResource = mapper.map(savedLoginCredentialsEntity, LoginCredentialsResource.class); |         final var newCredentialsResource = mapper.map(savedCredentialsEntity, CredentialsResource.class); | ||||||
|         return ResponseEntity.ok(newLoginCredentialsResource); |         return ResponseEntity.ok(newCredentialsResource); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public ResponseEntity<Void> deleteLoginCredentialsByUuid(final String assumedRoles, final UUID loginCredentialsUuid) { |     @Timed("app.credentials.credentials.deleteCredentialsByUuid") | ||||||
|  |     public ResponseEntity<Void> deleteCredentialsByUuid(final String assumedRoles, final UUID credentialsUuid) { | ||||||
|         context.assumeRoles(assumedRoles); |         context.assumeRoles(assumedRoles); | ||||||
|         final var loginCredentialsEntity = em.getReference(HsCredentialsEntity.class, loginCredentialsUuid); |         final var credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid); | ||||||
|         em.remove(loginCredentialsEntity); |         em.remove(credentialsEntity); | ||||||
|         return ResponseEntity.noContent().build(); |         return ResponseEntity.noContent().build(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public ResponseEntity<LoginCredentialsResource> patchLoginCredentials( |     @Timed("app.credentials.credentials.patchCredentials") | ||||||
|  |     public ResponseEntity<CredentialsResource> patchCredentials( | ||||||
|             final String assumedRoles, |             final String assumedRoles, | ||||||
|             final UUID loginCredentialsUuid, |             final UUID credentialsUuid, | ||||||
|             final LoginCredentialsPatchResource body |             final CredentialsPatchResource body | ||||||
|     ) { |     ) { | ||||||
|         context.assumeRoles(assumedRoles); |         context.assumeRoles(assumedRoles); | ||||||
|  |  | ||||||
|         final var current = loginCredentialsRepo.findByUuid(loginCredentialsUuid).orElseThrow(); |         final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow(); | ||||||
|  |  | ||||||
|         new HsCredentialsEntityPatcher(em, current).apply(body); |         new HsCredentialsEntityPatcher(em, messageTranslator, current).apply(body); | ||||||
|  |  | ||||||
|         final var saved = loginCredentialsRepo.save(current); |         final var saved = credentialsRepo.save(current); | ||||||
|         final var mapped = mapper.map(saved, LoginCredentialsResource.class); |         final var mapped = mapper.map(saved, CredentialsResource.class); | ||||||
|         return ResponseEntity.ok(mapped); |         return ResponseEntity.ok(mapped); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ import static net.hostsharing.hsadminng.repr.Stringify.stringify; | |||||||
| @AllArgsConstructor | @AllArgsConstructor | ||||||
| public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Stringifyable { | public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Stringifyable { | ||||||
|  |  | ||||||
|     protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "loginCredentials") |     protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "credentials") | ||||||
|             .withProp(HsCredentialsEntity::isActive) |             .withProp(HsCredentialsEntity::isActive) | ||||||
|             .withProp(HsCredentialsEntity::getEmailAddress) |             .withProp(HsCredentialsEntity::getEmailAddress) | ||||||
|             .withProp(HsCredentialsEntity::getTwoFactorAuth) |             .withProp(HsCredentialsEntity::getTwoFactorAuth) | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| package net.hostsharing.hsadminng.credentials; | package net.hostsharing.hsadminng.credentials; | ||||||
|  |  | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextResource; | import net.hostsharing.hsadminng.config.MessageTranslator; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsPatchResource; | import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResource; | ||||||
|  | import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource; | ||||||
| import net.hostsharing.hsadminng.mapper.EntityPatcher; | import net.hostsharing.hsadminng.mapper.EntityPatcher; | ||||||
| import net.hostsharing.hsadminng.mapper.OptionalFromJson; | import net.hostsharing.hsadminng.mapper.OptionalFromJson; | ||||||
|  |  | ||||||
| @@ -11,18 +12,20 @@ import java.util.List; | |||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| public class HsCredentialsEntityPatcher implements EntityPatcher<LoginCredentialsPatchResource> { | public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> { | ||||||
|  |  | ||||||
|     private final EntityManager em; |     private final EntityManager em; | ||||||
|  |     private MessageTranslator messageTranslator; | ||||||
|     private final HsCredentialsEntity entity; |     private final HsCredentialsEntity entity; | ||||||
|  |  | ||||||
|     public HsCredentialsEntityPatcher(final EntityManager em, final HsCredentialsEntity entity) { |     public HsCredentialsEntityPatcher(final EntityManager em, MessageTranslator messageTranslator, final HsCredentialsEntity entity) { | ||||||
|         this.em = em; |         this.em = em; | ||||||
|  |         this.messageTranslator = messageTranslator; | ||||||
|         this.entity = entity; |         this.entity = entity; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void apply(final LoginCredentialsPatchResource resource) { |     public void apply(final CredentialsPatchResource resource) { | ||||||
|         if ( resource.getActive() != null ) { |         if ( resource.getActive() != null ) { | ||||||
|                 entity.setActive(resource.getActive()); |                 entity.setActive(resource.getActive()); | ||||||
|         } |         } | ||||||
| @@ -40,11 +43,11 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<LoginCredential | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void syncLoginContextEntities( |     public void syncLoginContextEntities( | ||||||
|             List<LoginContextResource> resources, |             List<ContextResource> resources, | ||||||
|             Set<HsCredentialsContextRealEntity> entities |             Set<HsCredentialsContextRealEntity> entities | ||||||
|     ) { |     ) { | ||||||
|         final var resourceUuids = resources.stream() |         final var resourceUuids = resources.stream() | ||||||
|                 .map(LoginContextResource::getUuid) |                 .map(ContextResource::getUuid) | ||||||
|                 .collect(Collectors.toSet()); |                 .collect(Collectors.toSet()); | ||||||
|  |  | ||||||
|         final var entityUuids = entities.stream() |         final var entityUuids = entities.stream() | ||||||
| @@ -57,14 +60,15 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<LoginCredential | |||||||
|             if (!entityUuids.contains(resource.getUuid())) { |             if (!entityUuids.contains(resource.getUuid())) { | ||||||
|                 final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid()); |                 final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid()); | ||||||
|                 if ( existingContextEntity == null ) { |                 if ( existingContextEntity == null ) { | ||||||
|                     // FIXME: i18n |  | ||||||
|                     throw new EntityNotFoundException( |                     throw new EntityNotFoundException( | ||||||
|                             HsCredentialsContextRealEntity.class.getName() + " with uuid " + resource.getUuid() + " not found."); |                             messageTranslator.translate("{0} \"{1}\" not found or not accessible", | ||||||
|  |                                     "credentials uuid", resource.getUuid())); | ||||||
|                 } |                 } | ||||||
|                 if (!existingContextEntity.getType().equals(resource.getType().name()) && |                 if (!existingContextEntity.getType().equals(resource.getType()) && | ||||||
|                     !existingContextEntity.getQualifier().equals(resource.getQualifier())) { |                     !existingContextEntity.getQualifier().equals(resource.getQualifier())) { | ||||||
|                     // FIXME: i18n |                     throw new EntityNotFoundException( | ||||||
|                     throw new EntityNotFoundException("existing " +  existingContextEntity + " does not match given resource " + resource); |                             messageTranslator.translate("existing {0} does not match given resource {1}", | ||||||
|  |                                     existingContextEntity, resource)); | ||||||
|                 } |                 } | ||||||
|                 entities.add(existingContextEntity); |                 entities.add(existingContextEntity); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ paths: | |||||||
|   # Credentials |   # Credentials | ||||||
|  |  | ||||||
|   /api/hs/credentials/credentials: |   /api/hs/credentials/credentials: | ||||||
|     $ref: "api-paths.yaml" |     $ref: "credentials.yaml" | ||||||
|  |  | ||||||
|   /api/hs/credentials/credentials/{credentialsUuid}: |   /api/hs/credentials/credentials/{credentialsUuid}: | ||||||
|     $ref: "credentials-with-uuid.yaml" |     $ref: "credentials-with-uuid.yaml" | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ components: | |||||||
|                 contexts: |                 contexts: | ||||||
|                     type: array |                     type: array | ||||||
|                     items: |                     items: | ||||||
|                         $ref: '-context-schemas.yaml#/components/schemas/Context' |                         $ref: 'context-schemas.yaml#/components/schemas/Context' | ||||||
|             required: |             required: | ||||||
|                 - uuid |                 - uuid | ||||||
|                 - active |                 - active | ||||||
| @@ -55,7 +55,7 @@ components: | |||||||
|                 contexts: |                 contexts: | ||||||
|                     type: array |                     type: array | ||||||
|                     items: |                     items: | ||||||
|                         $ref: '-context-schemas.yaml#/components/schemas/Context' |                         $ref: 'context-schemas.yaml#/components/schemas/Context' | ||||||
|             additionalProperties: false |             additionalProperties: false | ||||||
|  |  | ||||||
|         CredentialsInsert: |         CredentialsInsert: | ||||||
| @@ -83,7 +83,7 @@ components: | |||||||
|                 contexts: |                 contexts: | ||||||
|                     type: array |                     type: array | ||||||
|                     items: |                     items: | ||||||
|                         $ref: '-context-schemas.yaml#/components/schemas/Context' |                         $ref: 'context-schemas.yaml#/components/schemas/Context' | ||||||
|             required: |             required: | ||||||
|                 - uuid |                 - uuid | ||||||
|                 - active |                 - active | ||||||
|   | |||||||
| @@ -34,6 +34,33 @@ create table base.tx_context | |||||||
| create index on base.tx_context using brin (txTimestamp); | create index on base.tx_context using brin (txTimestamp); | ||||||
| --// | --// | ||||||
|  |  | ||||||
|  |  | ||||||
|  | -- ============================================================================ | ||||||
|  | --changeset michael.hoennig:audit-TX-CONTEXT-TABLE-COLUMN-SEQUENTIAL-TX-ID endDelimiter:--// | ||||||
|  | -- ---------------------------------------------------------------------------- | ||||||
|  | /* | ||||||
|  |     Adds a column to base.tx_context which keeps a strictly sequentially ordered tx-id. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | alter table base.tx_context | ||||||
|  |     add column seqTxId BIGINT; | ||||||
|  |  | ||||||
|  | CREATE OR REPLACE FUNCTION set_next_sequential_txid() | ||||||
|  |     RETURNS TRIGGER AS $$ | ||||||
|  | BEGIN | ||||||
|  |     LOCK TABLE base.tx_context IN EXCLUSIVE MODE; | ||||||
|  |     SELECT COALESCE(MAX(seqTxId)+1, 0) INTO NEW.seqTxId FROM base.tx_context; | ||||||
|  |     RETURN NEW; | ||||||
|  | END; | ||||||
|  | $$ LANGUAGE plpgsql; | ||||||
|  |  | ||||||
|  | CREATE TRIGGER set_commit_order_trigger | ||||||
|  |     BEFORE INSERT ON base.tx_context | ||||||
|  |     FOR EACH ROW | ||||||
|  | EXECUTE FUNCTION set_next_sequential_txid(); | ||||||
|  | --// | ||||||
|  |  | ||||||
|  |  | ||||||
| -- ============================================================================ | -- ============================================================================ | ||||||
| --changeset michael.hoennig:audit-TX-JOURNAL-TABLE endDelimiter:--// | --changeset michael.hoennig:audit-TX-JOURNAL-TABLE endDelimiter:--// | ||||||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||||||
| @@ -53,13 +80,24 @@ create index on base.tx_journal (targetTable, targetUuid); | |||||||
| --// | --// | ||||||
|  |  | ||||||
| -- ============================================================================ | -- ============================================================================ | ||||||
| --changeset michael.hoennig:audit-TX-JOURNAL-VIEW endDelimiter:--// | --changeset michael.hoennig:audit-TX-JOURNAL-VIEW runOnChange:true validCheckSum:ANY endDelimiter:--// | ||||||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||||||
| /* | /* | ||||||
|     A view combining base.tx_journal with base.tx_context. |     A view combining base.tx_journal with base.tx_context. | ||||||
|  */ |  */ | ||||||
|  | drop view if exists base.tx_journal_v; | ||||||
| create view base.tx_journal_v as | create view base.tx_journal_v as | ||||||
| select txc.*, txj.targettable, txj.targetop, txj.targetuuid, txj.targetdelta | select txc.seqTxId, | ||||||
|  |        txc.txId, | ||||||
|  |        txc.txTimeStamp, | ||||||
|  |        txc.currentSubject, | ||||||
|  |        txc.assumedRoles, | ||||||
|  |        txc.currentTask, | ||||||
|  |        txc.currentRequest, | ||||||
|  |        txj.targetTable, | ||||||
|  |        txj.targeTop, | ||||||
|  |        txj.targetUuid, | ||||||
|  |        txj.targetDelta | ||||||
|     from base.tx_journal txj |     from base.tx_journal txj | ||||||
|     left join base.tx_context txc using (txId) |     left join base.tx_context txc using (txId) | ||||||
|     order by txc.txtimestamp; |     order by txc.txtimestamp; | ||||||
|   | |||||||
| @@ -12,6 +12,9 @@ unknown\ authorization\ ticket=unbekanntes Autorisierungs-Ticket | |||||||
| {0}\ "{1}"\ not\ found\ or\ not\ accessible={0} "{1}" nicht gefunden oder nicht zugänglich | {0}\ "{1}"\ not\ found\ or\ not\ accessible={0} "{1}" nicht gefunden oder nicht zugänglich | ||||||
| but\ is=ist aber | but\ is=ist aber | ||||||
|  |  | ||||||
|  | # credentials validations | ||||||
|  | existing\ {0}\ does\ not\ match\ given\ resource\ {1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1} | ||||||
|  |  | ||||||
| # office.coop-shares | # office.coop-shares | ||||||
| for\ transactionType\={0},\ shareCount\ must\ be\ positive\ but\ is\ {1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1} | for\ transactionType\={0},\ shareCount\ must\ be\ positive\ but\ is\ {1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1} | ||||||
| for\ transactionType\={0},\ shareCount\ must\ be\ negative\ but\ is\ {1}=für transactionType={0}, muss shareCount negativ sein, ist aber {1} | for\ transactionType\={0},\ shareCount\ must\ be\ negative\ but\ is\ {1}=für transactionType={0}, muss shareCount negativ sein, ist aber {1} | ||||||
|   | |||||||
| @@ -52,6 +52,7 @@ public class ArchitectureTest { | |||||||
|                     "..credentials", |                     "..credentials", | ||||||
|                     "..hash", |                     "..hash", | ||||||
|                     "..lambda", |                     "..lambda", | ||||||
|  |                     "..journal", | ||||||
|                     "..generated..", |                     "..generated..", | ||||||
|                     "..persistence..", |                     "..persistence..", | ||||||
|                     "..reflection", |                     "..reflection", | ||||||
| @@ -155,14 +156,14 @@ public class ArchitectureTest { | |||||||
|     public static final ArchRule testPackagesRule = classes() |     public static final ArchRule testPackagesRule = classes() | ||||||
|             .that().resideInAPackage("..test.(*)..") |             .that().resideInAPackage("..test.(*)..") | ||||||
|             .should().onlyBeAccessed().byClassesThat() |             .should().onlyBeAccessed().byClassesThat() | ||||||
|             .resideInAnyPackage("..test.(*).."); |             .resideInAnyPackage("..test.(*)..", "..journal.."); | ||||||
|  |  | ||||||
|     @ArchTest |     @ArchTest | ||||||
|     @SuppressWarnings("unused") |     @SuppressWarnings("unused") | ||||||
|     public static final ArchRule testPackagePackageRule = classes() |     public static final ArchRule testPackagePackageRule = classes() | ||||||
|             .that().resideInAPackage("..test.pac..") |             .that().resideInAPackage("..test.pac..") | ||||||
|             .should().onlyBeAccessed().byClassesThat() |             .should().onlyBeAccessed().byClassesThat() | ||||||
|             .resideInAnyPackage("..test.pac.."); |             .resideInAnyPackage("..test.pac..", "..journal.."); | ||||||
|  |  | ||||||
|     @ArchTest |     @ArchTest | ||||||
|     @SuppressWarnings("unused") |     @SuppressWarnings("unused") | ||||||
| @@ -174,6 +175,7 @@ public class ArchitectureTest { | |||||||
|                     "..hs.office.(*)..", |                     "..hs.office.(*)..", | ||||||
|                     "..hs.booking.(*)..", |                     "..hs.booking.(*)..", | ||||||
|                     "..hs.hosting.(*)..", |                     "..hs.hosting.(*)..", | ||||||
|  |                     "..credentials..", | ||||||
|                     "..hs.scenarios", |                     "..hs.scenarios", | ||||||
|                     "..hs.migration", |                     "..hs.migration", | ||||||
|                     "..rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest |                     "..rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest | ||||||
|   | |||||||
| @@ -11,10 +11,10 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; | |||||||
| import org.springframework.context.annotation.Import; | import org.springframework.context.annotation.Import; | ||||||
| import org.springframework.test.context.ActiveProfiles; | import org.springframework.test.context.ActiveProfiles; | ||||||
| import org.springframework.test.context.bean.override.mockito.MockitoBean; | import org.springframework.test.context.bean.override.mockito.MockitoBean; | ||||||
|  | import org.springframework.transaction.annotation.Transactional; | ||||||
|  |  | ||||||
| import jakarta.persistence.PersistenceException; | import jakarta.persistence.PersistenceException; | ||||||
| import jakarta.servlet.http.HttpServletRequest; | import jakarta.servlet.http.HttpServletRequest; | ||||||
| import jakarta.transaction.Transactional; |  | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  |  | ||||||
| import static org.assertj.core.api.Assertions.assertThat; | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|   | |||||||
| @@ -97,7 +97,7 @@ class HsCredentialsContextsControllerRestTest { | |||||||
|  |  | ||||||
|         // when |         // when | ||||||
|         mockMvc.perform(MockMvcRequestBuilders |         mockMvc.perform(MockMvcRequestBuilders | ||||||
|                 .get("/api/login/contexts") |                 .get("/api/hs/credentials/contexts") | ||||||
|                 .header("Authorization", "Bearer superuser-alex@hostsharing.net") |                 .header("Authorization", "Bearer superuser-alex@hostsharing.net") | ||||||
|                 .accept(MediaType.APPLICATION_JSON)) |                 .accept(MediaType.APPLICATION_JSON)) | ||||||
|                 .andDo(print()) |                 .andDo(print()) | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| package net.hostsharing.hsadminng.credentials; | package net.hostsharing.hsadminng.credentials; | ||||||
|  |  | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextResource; | import net.hostsharing.hsadminng.config.MessageTranslator; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginContextTypeResource; | import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResource; | ||||||
| import net.hostsharing.hsadminng.credentials.generated.api.v1.model.LoginCredentialsPatchResource; | import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource; | ||||||
| import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; | import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; | ||||||
| import org.junit.jupiter.api.BeforeEach; | import org.junit.jupiter.api.BeforeEach; | ||||||
| import org.junit.jupiter.api.TestInstance; | import org.junit.jupiter.api.TestInstance; | ||||||
| @@ -20,11 +20,12 @@ import java.util.stream.Stream; | |||||||
| import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; | import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; | ||||||
| import static org.mockito.ArgumentMatchers.eq; | import static org.mockito.ArgumentMatchers.eq; | ||||||
| import static org.mockito.Mockito.lenient; | import static org.mockito.Mockito.lenient; | ||||||
|  | import static org.mockito.Mockito.mock; | ||||||
|  |  | ||||||
| @TestInstance(PER_CLASS) | @TestInstance(PER_CLASS) | ||||||
| @ExtendWith(MockitoExtension.class) | @ExtendWith(MockitoExtension.class) | ||||||
| class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | ||||||
|         LoginCredentialsPatchResource, |         CredentialsPatchResource, | ||||||
|         HsCredentialsEntity |         HsCredentialsEntity | ||||||
|         > { |         > { | ||||||
|  |  | ||||||
| @@ -58,8 +59,8 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | |||||||
|             .qualifier("dev") |             .qualifier("dev") | ||||||
|             .build(); |             .build(); | ||||||
|  |  | ||||||
|     private LoginContextResource patchContextResource2; |     private ContextResource patchContextResource2; | ||||||
|     private LoginContextResource patchContextResource3; |     private ContextResource patchContextResource3; | ||||||
|  |  | ||||||
|     // This is what em.find should return for CONTEXT_UUID_3 |     // This is what em.find should return for CONTEXT_UUID_3 | ||||||
|     private final HsCredentialsContextRealEntity newContextEntity3 = HsCredentialsContextRealEntity.builder() |     private final HsCredentialsContextRealEntity newContextEntity3 = HsCredentialsContextRealEntity.builder() | ||||||
| @@ -69,7 +70,7 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | |||||||
|             .build(); |             .build(); | ||||||
|  |  | ||||||
|     private final Set<HsCredentialsContextRealEntity> initialContextEntities = Set.of(initialContextEntity1, initialContextEntity2); |     private final Set<HsCredentialsContextRealEntity> initialContextEntities = Set.of(initialContextEntity1, initialContextEntity2); | ||||||
|     private List<LoginContextResource> patchedContextResources; |     private List<ContextResource> patchedContextResources; | ||||||
|     private final Set<HsCredentialsContextRealEntity> expectedPatchedContextEntities = Set.of(initialContextEntity2, newContextEntity3); |     private final Set<HsCredentialsContextRealEntity> expectedPatchedContextEntities = Set.of(initialContextEntity2, newContextEntity3); | ||||||
|  |  | ||||||
|     @Mock |     @Mock | ||||||
| @@ -82,14 +83,14 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | |||||||
|         lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_2))).thenReturn(initialContextEntity2); |         lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_2))).thenReturn(initialContextEntity2); | ||||||
|         lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_3))).thenReturn(newContextEntity3); |         lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_3))).thenReturn(newContextEntity3); | ||||||
|  |  | ||||||
|         patchContextResource2 = new LoginContextResource(); |         patchContextResource2 = new ContextResource(); | ||||||
|         patchContextResource2.setUuid(CONTEXT_UUID_2); |         patchContextResource2.setUuid(CONTEXT_UUID_2); | ||||||
|         patchContextResource2.setType(LoginContextTypeResource.SSH); |         patchContextResource2.setType("SSH"); | ||||||
|         patchContextResource2.setQualifier("dev"); |         patchContextResource2.setQualifier("dev"); | ||||||
|  |  | ||||||
|         patchContextResource3 = new LoginContextResource(); |         patchContextResource3 = new ContextResource(); | ||||||
|         patchContextResource3.setUuid(CONTEXT_UUID_3); |         patchContextResource3.setUuid(CONTEXT_UUID_3); | ||||||
|         patchContextResource3.setType(LoginContextTypeResource.HSADMIN); |         patchContextResource3.setType("HSADMIN"); | ||||||
|         patchContextResource3.setQualifier("test"); |         patchContextResource3.setQualifier("test"); | ||||||
|  |  | ||||||
|         patchedContextResources = List.of(patchContextResource2, patchContextResource3); |         patchedContextResources = List.of(patchContextResource2, patchContextResource3); | ||||||
| @@ -110,13 +111,13 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected LoginCredentialsPatchResource newPatchResource() { |     protected CredentialsPatchResource newPatchResource() { | ||||||
|         return new LoginCredentialsPatchResource(); |         return new CredentialsPatchResource(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     protected HsCredentialsEntityPatcher createPatcher(final HsCredentialsEntity entity) { |     protected HsCredentialsEntityPatcher createPatcher(final HsCredentialsEntity entity) { | ||||||
|         return new HsCredentialsEntityPatcher(em, entity); |         return new HsCredentialsEntityPatcher(em, mock(MessageTranslator.class), entity); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -124,38 +125,38 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | |||||||
|         return Stream.of( |         return Stream.of( | ||||||
|                 new SimpleProperty<>( |                 new SimpleProperty<>( | ||||||
|                         "active", |                         "active", | ||||||
|                         LoginCredentialsPatchResource::setActive, |                         CredentialsPatchResource::setActive, | ||||||
|                         PATCHED_ACTIVE, |                         PATCHED_ACTIVE, | ||||||
|                         HsCredentialsEntity::setActive, |                         HsCredentialsEntity::setActive, | ||||||
|                         PATCHED_ACTIVE) |                         PATCHED_ACTIVE) | ||||||
|                     .notNullable(), |                     .notNullable(), | ||||||
|                 new JsonNullableProperty<>( |                 new JsonNullableProperty<>( | ||||||
|                         "emailAddress", |                         "emailAddress", | ||||||
|                         LoginCredentialsPatchResource::setEmailAddress, |                         CredentialsPatchResource::setEmailAddress, | ||||||
|                         PATCHED_EMAIL_ADDRESS, |                         PATCHED_EMAIL_ADDRESS, | ||||||
|                         HsCredentialsEntity::setEmailAddress, |                         HsCredentialsEntity::setEmailAddress, | ||||||
|                         PATCHED_EMAIL_ADDRESS), |                         PATCHED_EMAIL_ADDRESS), | ||||||
|                 new JsonNullableProperty<>( |                 new JsonNullableProperty<>( | ||||||
|                         "twoFactorAuth", |                         "twoFactorAuth", | ||||||
|                         LoginCredentialsPatchResource::setTwoFactorAuth, |                         CredentialsPatchResource::setTwoFactorAuth, | ||||||
|                         PATCHED_TWO_FACTOR_AUTH, |                         PATCHED_TWO_FACTOR_AUTH, | ||||||
|                         HsCredentialsEntity::setTwoFactorAuth, |                         HsCredentialsEntity::setTwoFactorAuth, | ||||||
|                         PATCHED_TWO_FACTOR_AUTH), |                         PATCHED_TWO_FACTOR_AUTH), | ||||||
|                 new JsonNullableProperty<>( |                 new JsonNullableProperty<>( | ||||||
|                         "smsNumber", |                         "smsNumber", | ||||||
|                         LoginCredentialsPatchResource::setSmsNumber, |                         CredentialsPatchResource::setSmsNumber, | ||||||
|                         PATCHED_SMS_NUMBER, |                         PATCHED_SMS_NUMBER, | ||||||
|                         HsCredentialsEntity::setSmsNumber, |                         HsCredentialsEntity::setSmsNumber, | ||||||
|                         PATCHED_SMS_NUMBER), |                         PATCHED_SMS_NUMBER), | ||||||
|                 new JsonNullableProperty<>( |                 new JsonNullableProperty<>( | ||||||
|                         "phonePassword", |                         "phonePassword", | ||||||
|                         LoginCredentialsPatchResource::setPhonePassword, |                         CredentialsPatchResource::setPhonePassword, | ||||||
|                         PATCHED_PHONE_PASSWORD, |                         PATCHED_PHONE_PASSWORD, | ||||||
|                         HsCredentialsEntity::setPhonePassword, |                         HsCredentialsEntity::setPhonePassword, | ||||||
|                         PATCHED_PHONE_PASSWORD), |                         PATCHED_PHONE_PASSWORD), | ||||||
|                 new SimpleProperty<>( |                 new SimpleProperty<>( | ||||||
|                         "contexts", |                         "contexts", | ||||||
|                         LoginCredentialsPatchResource::setContexts, |                         CredentialsPatchResource::setContexts, | ||||||
|                         patchedContextResources, |                         patchedContextResources, | ||||||
|                         HsCredentialsEntity::setLoginContexts, |                         HsCredentialsEntity::setLoginContexts, | ||||||
|                         expectedPatchedContextEntities) |                         expectedPatchedContextEntities) | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|     HttpServletRequest request; |     HttpServletRequest request; | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     private HsCredentialsRepository loginCredentialsRepository; |     private HsCredentialsRepository credentialsRepository; | ||||||
|  |  | ||||||
|     @Autowired |     @Autowired | ||||||
|     private HsCredentialsContextRealRepository loginContextRealRepo; |     private HsCredentialsContextRealRepository loginContextRealRepo; | ||||||
| @@ -88,7 +88,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|     @Test |     @Test | ||||||
|     void shouldFindByUuidUsingTestData() { |     void shouldFindByUuidUsingTestData() { | ||||||
|         // when |         // when | ||||||
|         final var foundEntityOptional = loginCredentialsRepository.findByUuid(alexSubject.getUuid()); |         final var foundEntityOptional = credentialsRepository.findByUuid(alexSubject.getUuid()); | ||||||
|  |  | ||||||
|         // then |         // then | ||||||
|         assertThat(foundEntityOptional).isPresent() |         assertThat(foundEntityOptional).isPresent() | ||||||
| @@ -96,7 +96,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     void shouldSaveLoginCredentialsWithExistingContext() { |     void shouldSaveCredentialsWithExistingContext() { | ||||||
|         // given |         // given | ||||||
|         final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod") |         final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod") | ||||||
|                 .orElseThrow(); |                 .orElseThrow(); | ||||||
| @@ -111,12 +111,12 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|                 .build(); |                 .build(); | ||||||
|  |  | ||||||
|         // when |         // when | ||||||
|         loginCredentialsRepository.save(newCredentials); |         credentialsRepository.save(newCredentials); | ||||||
|         em.flush(); |         em.flush(); | ||||||
|         em.clear(); |         em.clear(); | ||||||
|  |  | ||||||
|         // then |         // then | ||||||
|         final var foundEntityOptional = loginCredentialsRepository.findByUuid(drewSubject.getUuid()); |         final var foundEntityOptional = credentialsRepository.findByUuid(drewSubject.getUuid()); | ||||||
|         assertThat(foundEntityOptional).isPresent(); |         assertThat(foundEntityOptional).isPresent(); | ||||||
|         final var foundEntity = foundEntityOptional.get(); |         final var foundEntity = foundEntityOptional.get(); | ||||||
|         assertThat(foundEntity.getEmailAddress()).isEqualTo("drew.new@example.com"); |         assertThat(foundEntity.getEmailAddress()).isEqualTo("drew.new@example.com"); | ||||||
| @@ -129,7 +129,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     void shouldNotSaveLoginCredentialsWithNewContext() { |     void shouldNotSaveCredentialsWithNewContext() { | ||||||
|         // given |         // given | ||||||
|         final var newContext = HsCredentialsContextRealEntity.builder() |         final var newContext = HsCredentialsContextRealEntity.builder() | ||||||
|                 .type("MATRIX") |                 .type("MATRIX") | ||||||
| @@ -146,7 +146,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|  |  | ||||||
|         // when |         // when | ||||||
|         final var exception = catchThrowable(() -> { |         final var exception = catchThrowable(() -> { | ||||||
|             loginCredentialsRepository.save(newCredentials); |             credentialsRepository.save(newCredentials); | ||||||
|             em.flush(); |             em.flush(); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
| @@ -155,7 +155,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     void shouldSaveNewLoginCredentialsWithoutContext() { |     void shouldSaveNewCredentialsWithoutContext() { | ||||||
|         // given |         // given | ||||||
|         final var newCredentials = HsCredentialsEntity.builder() |         final var newCredentials = HsCredentialsEntity.builder() | ||||||
|                 .subject(testUserSubject) |                 .subject(testUserSubject) | ||||||
| @@ -167,12 +167,12 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|                 .build(); |                 .build(); | ||||||
|  |  | ||||||
|         // when |         // when | ||||||
|         loginCredentialsRepository.save(newCredentials); |         credentialsRepository.save(newCredentials); | ||||||
|         em.flush(); |         em.flush(); | ||||||
|         em.clear(); |         em.clear(); | ||||||
|  |  | ||||||
|         // then |         // then | ||||||
|         final var foundEntityOptional = loginCredentialsRepository.findByUuid(testUserSubject.getUuid()); |         final var foundEntityOptional = credentialsRepository.findByUuid(testUserSubject.getUuid()); | ||||||
|         assertThat(foundEntityOptional).isPresent(); |         assertThat(foundEntityOptional).isPresent(); | ||||||
|         final var foundEntity = foundEntityOptional.get(); |         final var foundEntity = foundEntityOptional.get(); | ||||||
|         assertThat(foundEntity.getEmailAddress()).isEqualTo("test.user.new@example.com"); |         assertThat(foundEntity.getEmailAddress()).isEqualTo("test.user.new@example.com"); | ||||||
| @@ -183,21 +183,21 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     void shouldUpdateExistingLoginCredentials() { |     void shouldUpdateExistingCredentials() { | ||||||
|         // given |         // given | ||||||
|         final var entityToUpdate = loginCredentialsRepository.findByUuid(alexSubject.getUuid()).orElseThrow(); |         final var entityToUpdate = credentialsRepository.findByUuid(alexSubject.getUuid()).orElseThrow(); | ||||||
|         final var initialVersion = entityToUpdate.getVersion(); |         final var initialVersion = entityToUpdate.getVersion(); | ||||||
|  |  | ||||||
|         // when |         // when | ||||||
|         entityToUpdate.setActive(false); |         entityToUpdate.setActive(false); | ||||||
|         entityToUpdate.setEmailAddress("updated.user1@example.com"); |         entityToUpdate.setEmailAddress("updated.user1@example.com"); | ||||||
|         final var savedEntity = loginCredentialsRepository.save(entityToUpdate); |         final var savedEntity = credentialsRepository.save(entityToUpdate); | ||||||
|         em.flush(); |         em.flush(); | ||||||
|         em.clear(); |         em.clear(); | ||||||
|  |  | ||||||
|         // then |         // then | ||||||
|         assertThat(savedEntity.getVersion()).isGreaterThan(initialVersion); |         assertThat(savedEntity.getVersion()).isGreaterThan(initialVersion); | ||||||
|         final var updatedEntityOptional = loginCredentialsRepository.findByUuid(alexSubject.getUuid()); |         final var updatedEntityOptional = credentialsRepository.findByUuid(alexSubject.getUuid()); | ||||||
|         assertThat(updatedEntityOptional).isPresent(); |         assertThat(updatedEntityOptional).isPresent(); | ||||||
|         final var updatedEntity = updatedEntityOptional.get(); |         final var updatedEntity = updatedEntityOptional.get(); | ||||||
|         assertThat(updatedEntity.isActive()).isFalse(); |         assertThat(updatedEntity.isActive()).isFalse(); | ||||||
|   | |||||||
| @@ -0,0 +1,122 @@ | |||||||
|  | package net.hostsharing.hsadminng.journal; | ||||||
|  |  | ||||||
|  | import lombok.SneakyThrows; | ||||||
|  | import net.hostsharing.hsadminng.context.Context; | ||||||
|  | import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; | ||||||
|  | import net.hostsharing.hsadminng.rbac.test.JpaAttempt; | ||||||
|  | import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerEntity; | ||||||
|  | import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerRepository; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  | import org.junit.jupiter.api.Tag; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; | ||||||
|  | import org.springframework.context.annotation.Import; | ||||||
|  | import org.springframework.test.context.bean.override.mockito.MockitoBean; | ||||||
|  | import org.springframework.transaction.PlatformTransactionManager; | ||||||
|  | import org.springframework.transaction.annotation.Transactional; | ||||||
|  |  | ||||||
|  | import jakarta.servlet.http.HttpServletRequest; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import static org.assertj.core.api.Assertions.assertThat; | ||||||
|  | import static org.springframework.transaction.annotation.Propagation.NEVER; | ||||||
|  |  | ||||||
|  | @DataJpaTest | ||||||
|  | @Import({ Context.class, JpaAttempt.class }) | ||||||
|  | @Tag("generalIntegrationTest") | ||||||
|  | class TransactionContextIntegrationTest extends ContextBasedTestWithCleanup { | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     private PlatformTransactionManager transactionManager; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     JpaAttempt jpaAttempt; | ||||||
|  |  | ||||||
|  |     @MockitoBean | ||||||
|  |     HttpServletRequest request; | ||||||
|  |  | ||||||
|  |     @Autowired | ||||||
|  |     private TestCustomerRepository repository; | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     @Transactional(propagation = NEVER) | ||||||
|  |     void testConcurrentCommitOrder() { | ||||||
|  |  | ||||||
|  |         // determine initial row count | ||||||
|  |         final var rowCount = jpaAttempt.transacted(() -> { | ||||||
|  |             context("superuser-alex@hostsharing.net"); | ||||||
|  |             return em.createQuery("SELECT e FROM TestCustomerEntity e", TestCustomerEntity.class).getResultList(); | ||||||
|  |         }).assertSuccessful().returnedValue().size(); | ||||||
|  |  | ||||||
|  |         // when 3 transactions with different runtime run concurrently | ||||||
|  |         runThreads( | ||||||
|  |                 // starts first, ends last (because it's slow) | ||||||
|  |                 createTransactionThread("t01", 91001, 500), | ||||||
|  |  | ||||||
|  |                 // starts second, ends first (because it's faster than the one that got started first) | ||||||
|  |                 createTransactionThread("t02", 91002, 0), | ||||||
|  |  | ||||||
|  |                 // starts third, ends second | ||||||
|  |                 createTransactionThread("t03", 91003, 100) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         // then all 3 threads did insert one row each | ||||||
|  |         jpaAttempt.transacted(() -> { | ||||||
|  |             context("superuser-alex@hostsharing.net"); | ||||||
|  |             var all = em.createQuery("SELECT e FROM TestCustomerEntity e", TestCustomerEntity.class).getResultList(); | ||||||
|  |             assertThat(all).hasSize(rowCount + 3); | ||||||
|  |         }).assertSuccessful(); | ||||||
|  |  | ||||||
|  |         // and seqTxId order is in correct order | ||||||
|  |         final var txContextsX = em.createNativeQuery( | ||||||
|  |                 "select concat(c.txId, ':', c.currentTask) from base.tx_context c order by c.seqTxId" | ||||||
|  |             ).getResultList(); | ||||||
|  |         final var txContextTasks = last(3, txContextsX).stream().map(Object::toString).toList(); | ||||||
|  |         assertThat(txContextTasks.get(0)).endsWith( | ||||||
|  |                 ":TestCustomerEntity(uuid=null, version=0, prefix=t02, reference=91002, adminUserName=null)"); | ||||||
|  |         assertThat(txContextTasks.get(1)).endsWith( | ||||||
|  |                 "TestCustomerEntity(uuid=null, version=0, prefix=t03, reference=91003, adminUserName=null)"); | ||||||
|  |         assertThat(txContextTasks.get(2)).endsWith( | ||||||
|  |                 "TestCustomerEntity(uuid=null, version=0, prefix=t01, reference=91001, adminUserName=null)"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private @NotNull Thread createTransactionThread(final String t01, final int reference, final int millis) { | ||||||
|  |         return new Thread(() -> { | ||||||
|  |             jpaAttempt.transacted(() -> { | ||||||
|  |                 final var entity1 = new TestCustomerEntity(); | ||||||
|  |                 entity1.setPrefix(t01); | ||||||
|  |                 entity1.setReference(reference); | ||||||
|  |  | ||||||
|  |                 context.define(entity1.toString(), null, "superuser-alex@hostsharing.net", null); | ||||||
|  |                 entity1.setReference(80000 + toInt(em.createNativeQuery("SELECT txid_current()").getSingleResult())); | ||||||
|  |                 repository.save(entity1); | ||||||
|  |                 sleep(millis); // simulate a delay | ||||||
|  |             }).assertSuccessful(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private int toInt(final Object singleResult) { | ||||||
|  |         return ((Long)singleResult).intValue(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SneakyThrows | ||||||
|  |     private void sleep(final int millis) { | ||||||
|  |         Thread.sleep(millis); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SneakyThrows | ||||||
|  |     private void runThreads(final Thread... threads) { | ||||||
|  |         for (final Thread thread : threads) { | ||||||
|  |             thread.start(); | ||||||
|  |             sleep(100); | ||||||
|  |         } | ||||||
|  |         for (final Thread thread : threads) { | ||||||
|  |             thread.join(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |     private List<?> last(final int n, final List<?> list) { | ||||||
|  |         return list.subList(Math.max(list.size() - n, 0), list.size()); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user