credentials.totpSecret as array and update credentials scenario test (#186)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/186 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
		| @@ -9,7 +9,7 @@ classDiagram | |||||||
|  |  | ||||||
|     class Credentials{ |     class Credentials{ | ||||||
|         +totpSecret: text |         +totpSecret: text | ||||||
|         +telephonePassword: text |         +phonePassword: text | ||||||
|         +emailAdress: text |         +emailAdress: text | ||||||
|         +smsNumber: text |         +smsNumber: text | ||||||
|         -active: bool [r/w] |         -active: bool [r/w] | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import java.util.function.BiConsumer; | |||||||
|  |  | ||||||
| import io.micrometer.core.annotation.Timed; | import io.micrometer.core.annotation.Timed; | ||||||
| import io.swagger.v3.oas.annotations.security.SecurityRequirement; | import io.swagger.v3.oas.annotations.security.SecurityRequirement; | ||||||
|  | import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource; | ||||||
| import net.hostsharing.hsadminng.config.MessageTranslator; | import net.hostsharing.hsadminng.config.MessageTranslator; | ||||||
| import net.hostsharing.hsadminng.context.Context; | import net.hostsharing.hsadminng.context.Context; | ||||||
| import net.hostsharing.hsadminng.accounts.generated.api.v1.api.CredentialsApi; | import net.hostsharing.hsadminng.accounts.generated.api.v1.api.CredentialsApi; | ||||||
| @@ -67,12 +68,12 @@ public class HsCredentialsController implements CredentialsApi { | |||||||
|             final UUID credentialsUuid) { |             final UUID credentialsUuid) { | ||||||
|         context.assumeRoles(assumedRoles); |         context.assumeRoles(assumedRoles); | ||||||
|  |  | ||||||
|         final var credentials = credentialsRepo.findByUuid(credentialsUuid); |         final var credentialsEntity = credentialsRepo.findByUuid(credentialsUuid); | ||||||
|         if (credentials.isEmpty()) { |         if (credentialsEntity.isEmpty()) { | ||||||
|             return ResponseEntity.notFound().build(); |             return ResponseEntity.notFound().build(); | ||||||
|         } |         } | ||||||
|         final var result = mapper.map( |         final var result = mapper.map( | ||||||
|                 credentials.get(), CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); |                 credentialsEntity.get(), CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); | ||||||
|         return ResponseEntity.ok(result); |         return ResponseEntity.ok(result); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -192,6 +193,7 @@ public class HsCredentialsController implements CredentialsApi { | |||||||
|                         mapper.map(person, HsOfficePersonResource.class) |                         mapper.map(person, HsOfficePersonResource.class) | ||||||
|                 ) |                 ) | ||||||
|         ); |         ); | ||||||
|  |         resource.setContexts(mapper.mapList(entity.getLoginContexts().stream().toList(), ContextResource.class)); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { |     final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.repr.Stringifyable; | |||||||
|  |  | ||||||
| import java.time.LocalDateTime; | import java.time.LocalDateTime; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
|  | import java.util.List; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  |  | ||||||
| @@ -30,7 +31,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str | |||||||
|     protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "credentials") |     protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "credentials") | ||||||
|             .withProp(HsCredentialsEntity::isActive) |             .withProp(HsCredentialsEntity::isActive) | ||||||
|             .withProp(HsCredentialsEntity::getEmailAddress) |             .withProp(HsCredentialsEntity::getEmailAddress) | ||||||
|             .withProp(HsCredentialsEntity::getTotpSecret) |             .withProp(HsCredentialsEntity::getTotpSecrets) | ||||||
|             .withProp(HsCredentialsEntity::getPhonePassword) |             .withProp(HsCredentialsEntity::getPhonePassword) | ||||||
|             .withProp(HsCredentialsEntity::getSmsNumber) |             .withProp(HsCredentialsEntity::getSmsNumber) | ||||||
|             .quotedValues(false); |             .quotedValues(false); | ||||||
| @@ -66,7 +67,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str | |||||||
|     private String onboardingToken; |     private String onboardingToken; | ||||||
|  |  | ||||||
|     @Column |     @Column | ||||||
|     private String totpSecret; |     private List<String> totpSecrets; | ||||||
|  |  | ||||||
|     @Column |     @Column | ||||||
|     private String phonePassword; |     private String phonePassword; | ||||||
| @@ -106,4 +107,5 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str | |||||||
|     public String toString() { |     public String toString() { | ||||||
|         return stringify.apply(this); |         return stringify.apply(this); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatc | |||||||
| import net.hostsharing.hsadminng.mapper.EntityPatcher; | import net.hostsharing.hsadminng.mapper.EntityPatcher; | ||||||
| import net.hostsharing.hsadminng.mapper.OptionalFromJson; | import net.hostsharing.hsadminng.mapper.OptionalFromJson; | ||||||
|  |  | ||||||
|  | import java.util.Optional; | ||||||
|  |  | ||||||
| public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> { | public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> { | ||||||
|  |  | ||||||
| @@ -22,8 +23,8 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatc | |||||||
|         } |         } | ||||||
|         OptionalFromJson.of(resource.getEmailAddress()) |         OptionalFromJson.of(resource.getEmailAddress()) | ||||||
|                 .ifPresent(entity::setEmailAddress); |                 .ifPresent(entity::setEmailAddress); | ||||||
|         OptionalFromJson.of(resource.getTotpSecret()) |         Optional.ofNullable(resource.getTotpSecrets()) | ||||||
|                 .ifPresent(entity::setTotpSecret); |                 .ifPresent(entity::setTotpSecrets); | ||||||
|         OptionalFromJson.of(resource.getSmsNumber()) |         OptionalFromJson.of(resource.getSmsNumber()) | ||||||
|                 .ifPresent(entity::setSmsNumber); |                 .ifPresent(entity::setSmsNumber); | ||||||
|         OptionalFromJson.of(resource.getPhonePassword()) |         OptionalFromJson.of(resource.getPhonePassword()) | ||||||
|   | |||||||
| @@ -14,9 +14,11 @@ components: | |||||||
|                 nickname: |                 nickname: | ||||||
|                     type: string |                     type: string | ||||||
|                     pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname |                     pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname | ||||||
|                 totpSecret: |                 totpSecrets: | ||||||
|  |                     type: array | ||||||
|  |                     items: | ||||||
|                         type: string |                         type: string | ||||||
|                 telephonePassword: |                 phonePassword: | ||||||
|                     type: string |                     type: string | ||||||
|                 emailAddress: |                 emailAddress: | ||||||
|                     type: string |                     type: string | ||||||
| @@ -46,9 +48,10 @@ components: | |||||||
|         CredentialsPatch: |         CredentialsPatch: | ||||||
|             type: object |             type: object | ||||||
|             properties: |             properties: | ||||||
|                 totpSecret: |                 totpSecrets: | ||||||
|  |                     type: array | ||||||
|  |                     items: | ||||||
|                         type: string |                         type: string | ||||||
|                     nullable: true |  | ||||||
|                 phonePassword: |                 phonePassword: | ||||||
|                     type: string |                     type: string | ||||||
|                     nullable: true |                     nullable: true | ||||||
| @@ -75,9 +78,11 @@ components: | |||||||
|                 nickname: |                 nickname: | ||||||
|                     type: string |                     type: string | ||||||
|                     pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname |                     pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname | ||||||
|                 totpSecret: |                 totpSecrets: | ||||||
|  |                     type: array | ||||||
|  |                     items: | ||||||
|                         type: string |                         type: string | ||||||
|                 telephonePassword: |                 phonePassword: | ||||||
|                     type: string |                     type: string | ||||||
|                 emailAddress: |                 emailAddress: | ||||||
|                     type: string |                     type: string | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ create table hs_accounts.credentials | |||||||
|     global_gid       int unique,     -- w/o |     global_gid       int unique,     -- w/o | ||||||
|     onboarding_token text,           -- w/o, but can be set to null to invalidate |     onboarding_token text,           -- w/o, but can be set to null to invalidate | ||||||
|  |  | ||||||
|     totp_secret      text, |     totp_secrets     text[], | ||||||
|     phone_password   text, |     phone_password   text, | ||||||
|     email_address    text, |     email_address    text, | ||||||
|     sms_number       text |     sms_number       text | ||||||
|   | |||||||
| @@ -51,9 +51,9 @@ begin | |||||||
| --     call rbac.grantRoleToRole(hs_accounts.context_REFERRER(context_MATRIX_internal), rbac.global_ADMIN()); | --     call rbac.grantRoleToRole(hs_accounts.context_REFERRER(context_MATRIX_internal), rbac.global_ADMIN()); | ||||||
|  |  | ||||||
|     -- Add test credentials (linking to assumed rbac.subject UUIDs) |     -- Add test credentials (linking to assumed rbac.subject UUIDs) | ||||||
|     INSERT INTO hs_accounts.credentials (uuid, version, person_uuid, active, global_uid, global_gid, onboarding_token, totp_secret, phone_password, email_address, sms_number) VALUES |     INSERT INTO hs_accounts.credentials (uuid, version, person_uuid, active, global_uid, global_gid, onboarding_token, totp_secrets, phone_password, email_address, sms_number) VALUES | ||||||
|         ( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', 'otp-secret-1', 'phone-pw-1', 'alex@example.com', '111-222-3333'), |         ( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'), | ||||||
|         ( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', 'otp-secret-2', 'phone-pw-2', 'fran@example.com', '444-555-6666'); |         ( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', ARRAY['otp-secret-2'], 'phone-pw-2', 'fran@example.com', '444-555-6666'); | ||||||
|  |  | ||||||
|     -- Map credentials to contexts |     -- Map credentials to contexts | ||||||
|     INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES |     INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES | ||||||
|   | |||||||
| @@ -33,13 +33,13 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | |||||||
|  |  | ||||||
|     private static final Boolean INITIAL_ACTIVE = true; |     private static final Boolean INITIAL_ACTIVE = true; | ||||||
|     private static final String INITIAL_EMAIL_ADDRESS = "initial@example.com"; |     private static final String INITIAL_EMAIL_ADDRESS = "initial@example.com"; | ||||||
|     private static final String INITIAL_TOTP_SECRET = "initial_2fa"; |     private static final List<String> INITIAL_TOTP_SECRETS = List.of("initial_2fa"); | ||||||
|     private static final String INITIAL_SMS_NUMBER = "initial_sms"; |     private static final String INITIAL_SMS_NUMBER = "initial_sms"; | ||||||
|     private static final String INITIAL_PHONE_PASSWORD = "initial_phone_pw"; |     private static final String INITIAL_PHONE_PASSWORD = "initial_phone_pw"; | ||||||
|  |  | ||||||
|     private static final Boolean PATCHED_ACTIVE = false; |     private static final Boolean PATCHED_ACTIVE = false; | ||||||
|     private static final String PATCHED_EMAIL_ADDRESS = "patched@example.com"; |     private static final String PATCHED_EMAIL_ADDRESS = "patched@example.com"; | ||||||
|     private static final String PATCHED_TOTP_SECRET = "patched_2fa"; |     private static final List<String> PATCHED_TOTP_SECRETS = List.of("patched_2fa"); | ||||||
|     private static final String PATCHED_SMS_NUMBER = "patched_sms"; |     private static final String PATCHED_SMS_NUMBER = "patched_sms"; | ||||||
|     private static final String PATCHED_PHONE_PASSWORD = "patched_phone_pw"; |     private static final String PATCHED_PHONE_PASSWORD = "patched_phone_pw"; | ||||||
|  |  | ||||||
| @@ -102,7 +102,7 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | |||||||
|         entity.setUuid(INITIAL_CREDENTIALS_UUID); |         entity.setUuid(INITIAL_CREDENTIALS_UUID); | ||||||
|         entity.setActive(INITIAL_ACTIVE); |         entity.setActive(INITIAL_ACTIVE); | ||||||
|         entity.setEmailAddress(INITIAL_EMAIL_ADDRESS); |         entity.setEmailAddress(INITIAL_EMAIL_ADDRESS); | ||||||
|         entity.setTotpSecret(INITIAL_TOTP_SECRET); |         entity.setTotpSecrets(INITIAL_TOTP_SECRETS); | ||||||
|         entity.setSmsNumber(INITIAL_SMS_NUMBER); |         entity.setSmsNumber(INITIAL_SMS_NUMBER); | ||||||
|         entity.setPhonePassword(INITIAL_PHONE_PASSWORD); |         entity.setPhonePassword(INITIAL_PHONE_PASSWORD); | ||||||
|         // Ensure loginContexts is a mutable set for the patcher |         // Ensure loginContexts is a mutable set for the patcher | ||||||
| @@ -137,12 +137,13 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | |||||||
|                         PATCHED_EMAIL_ADDRESS, |                         PATCHED_EMAIL_ADDRESS, | ||||||
|                         HsCredentialsEntity::setEmailAddress, |                         HsCredentialsEntity::setEmailAddress, | ||||||
|                         PATCHED_EMAIL_ADDRESS), |                         PATCHED_EMAIL_ADDRESS), | ||||||
|                 new JsonNullableProperty<>( |                 new SimpleProperty<>( | ||||||
|                         "totpSecret", |                         "totpSecret", | ||||||
|                         CredentialsPatchResource::setTotpSecret, |                         CredentialsPatchResource::setTotpSecrets, | ||||||
|                         PATCHED_TOTP_SECRET, |                         PATCHED_TOTP_SECRETS, | ||||||
|                         HsCredentialsEntity::setTotpSecret, |                         HsCredentialsEntity::setTotpSecrets, | ||||||
|                         PATCHED_TOTP_SECRET), |                         PATCHED_TOTP_SECRETS) | ||||||
|  |                         .notNullable(), | ||||||
|                 new JsonNullableProperty<>( |                 new JsonNullableProperty<>( | ||||||
|                         "smsNumber", |                         "smsNumber", | ||||||
|                         CredentialsPatchResource::setSmsNumber, |                         CredentialsPatchResource::setSmsNumber, | ||||||
|   | |||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | package net.hostsharing.hsadminng.hs.accounts.scenarios; | ||||||
|  |  | ||||||
|  | import lombok.SneakyThrows; | ||||||
|  | import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource; | ||||||
|  | import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; | ||||||
|  | import net.hostsharing.hsadminng.hs.scenarios.UseCase; | ||||||
|  | import org.apache.commons.lang3.tuple.Pair; | ||||||
|  |  | ||||||
|  | import java.util.Arrays; | ||||||
|  |  | ||||||
|  | import static io.restassured.http.ContentType.JSON; | ||||||
|  | import static org.springframework.http.HttpStatus.OK; | ||||||
|  |  | ||||||
|  | public abstract class BaseCredentialsUseCase<T extends UseCase<?>> extends UseCase<T> { | ||||||
|  |  | ||||||
|  |     public BaseCredentialsUseCase(final ScenarioTest testSuite) { | ||||||
|  |         super(testSuite); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SneakyThrows | ||||||
|  |     protected ContextResource[] fetchContextResourcesByDescriptorPairs(final String descriptPairsVarName) { | ||||||
|  |         final var requestedContexts = ScenarioTest.getTypedVariable("contexts", Pair[].class); | ||||||
|  |         final var existingContextsJson = withTitle("Fetch Available Account Contexts", () -> | ||||||
|  |                 httpGet("/api/hs/accounts/contexts").expecting(OK).expecting(JSON) | ||||||
|  |         ).getResponse().body(); | ||||||
|  |         final var existingContexts = objectMapper.readValue(existingContextsJson, ContextResource[].class); | ||||||
|  |         return Arrays.stream(requestedContexts) | ||||||
|  |                 .map(pair -> Arrays.stream(existingContexts) | ||||||
|  |                         .filter(context -> context.getType().equals(pair.getLeft()) | ||||||
|  |                                 && context.getQualifier().equals(pair.getRight())) | ||||||
|  |                         .findFirst() | ||||||
|  |                         .orElseThrow(() -> new IllegalStateException( | ||||||
|  |                                 "No matching context found for type=" + pair.getLeft() | ||||||
|  |                                         + " and qualifier=" + pair.getRight())) | ||||||
|  |                 ) | ||||||
|  |                 .toArray(ContextResource[]::new); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -8,7 +8,7 @@ import org.springframework.http.HttpStatus; | |||||||
| import static io.restassured.http.ContentType.JSON; | import static io.restassured.http.ContentType.JSON; | ||||||
| import static org.springframework.http.HttpStatus.OK; | import static org.springframework.http.HttpStatus.OK; | ||||||
|  |  | ||||||
| public class CreateCredentials extends UseCase<CreateCredentials> { | public class CreateCredentials extends BaseCredentialsUseCase<CreateCredentials> { | ||||||
|  |  | ||||||
|     public CreateCredentials(final ScenarioTest testSuite) { |     public CreateCredentials(final ScenarioTest testSuite) { | ||||||
|         super(testSuite); |         super(testSuite); | ||||||
| @@ -26,9 +26,8 @@ public class CreateCredentials extends UseCase<CreateCredentials> { | |||||||
|                 "In real situations we have more precise measures to find the related person." |                 "In real situations we have more precise measures to find the related person." | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |         given("resolvedContexts", | ||||||
|         obtain("CredentialsContexts", () -> |             fetchContextResourcesByDescriptorPairs("contexts") | ||||||
|                 httpGet("/api/hs/accounts/contexts").expecting(OK).expecting(JSON) |  | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return obtain("newCredentials", () -> |         return obtain("newCredentials", () -> | ||||||
| @@ -37,12 +36,14 @@ public class CreateCredentials extends UseCase<CreateCredentials> { | |||||||
|                      "person.uuid": ${Person: %{personGivenName} %{personFamilyName}}, |                      "person.uuid": ${Person: %{personGivenName} %{personFamilyName}}, | ||||||
|                      "nickname": ${nickname}, |                      "nickname": ${nickname}, | ||||||
|                      "active": %{active}, |                      "active": %{active}, | ||||||
|  |                      "totpSecrets": @{totpSecrets}, | ||||||
|                      "emailAddress": ${emailAddress}, |                      "emailAddress": ${emailAddress}, | ||||||
|                      "telephonePassword": ${telephonePassword}, |                      "phonePassword": ${phonePassword}, | ||||||
|                      "smsNumber": ${smsNumber}, |                      "smsNumber": ${smsNumber}, | ||||||
|  |                      "onboardingToken": ${onboardingToken}, | ||||||
|                      "globalUid": %{globalUid}, |                      "globalUid": %{globalUid}, | ||||||
|                      "globalGid": %{globalGid}, |                      "globalGid": %{globalGid}, | ||||||
|                      "contexts": @{contexts} |                      "contexts": @{resolvedContexts} | ||||||
|                 } |                 } | ||||||
|                 """)) |                 """)) | ||||||
|                 .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) |                 .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) | ||||||
| @@ -57,7 +58,9 @@ public class CreateCredentials extends UseCase<CreateCredentials> { | |||||||
|                         .expecting(OK).expecting(JSON), |                         .expecting(OK).expecting(JSON), | ||||||
|                 path("uuid").contains("%{newCredentials}"), |                 path("uuid").contains("%{newCredentials}"), | ||||||
|                 path("nickname").contains("%{nickname}"), |                 path("nickname").contains("%{nickname}"), | ||||||
|                 path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}") |                 path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}"), | ||||||
|  |                 path("totpSecrets").contains("@{totpSecrets}"), | ||||||
|  |                 path("onboardingToken").contains("%{onboardingToken}") | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,10 +4,12 @@ import lombok.SneakyThrows; | |||||||
| import net.hostsharing.hsadminng.HsadminNgApplication; | import net.hostsharing.hsadminng.HsadminNgApplication; | ||||||
| import net.hostsharing.hsadminng.config.DisableSecurityConfig; | import net.hostsharing.hsadminng.config.DisableSecurityConfig; | ||||||
| import net.hostsharing.hsadminng.hs.scenarios.Produces; | import net.hostsharing.hsadminng.hs.scenarios.Produces; | ||||||
|  | import net.hostsharing.hsadminng.hs.scenarios.Requires; | ||||||
| import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; | import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; | ||||||
| import net.hostsharing.hsadminng.mapper.Array; | import net.hostsharing.hsadminng.mapper.Array; | ||||||
| import net.hostsharing.hsadminng.rbac.test.JpaAttempt; | import net.hostsharing.hsadminng.rbac.test.JpaAttempt; | ||||||
| import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension; | import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension; | ||||||
|  | import org.apache.commons.lang3.tuple.Pair; | ||||||
| import org.junit.jupiter.api.BeforeEach; | import org.junit.jupiter.api.BeforeEach; | ||||||
| import org.junit.jupiter.api.ClassOrderer; | import org.junit.jupiter.api.ClassOrderer; | ||||||
| import org.junit.jupiter.api.MethodOrderer; | import org.junit.jupiter.api.MethodOrderer; | ||||||
| @@ -22,8 +24,6 @@ import org.junit.jupiter.api.extension.ExtendWith; | |||||||
| import org.springframework.boot.test.context.SpringBootTest; | import org.springframework.boot.test.context.SpringBootTest; | ||||||
| import org.springframework.test.context.ActiveProfiles; | import org.springframework.test.context.ActiveProfiles; | ||||||
|  |  | ||||||
| import java.util.Map; |  | ||||||
|  |  | ||||||
| @Tag("scenarioTest") | @Tag("scenarioTest") | ||||||
| @SpringBootTest( | @SpringBootTest( | ||||||
|         webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, |         webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, | ||||||
| @@ -45,6 +45,7 @@ class CredentialsScenarioTests extends ScenarioTest { | |||||||
|     protected void beforeScenario(final TestInfo testInfo) { |     protected void beforeScenario(final TestInfo testInfo) { | ||||||
|         super.beforeScenario(testInfo); |         super.beforeScenario(testInfo); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Nested |     @Nested | ||||||
|     @Order(10) |     @Order(10) | ||||||
|     @TestMethodOrder(MethodOrderer.OrderAnnotation.class) |     @TestMethodOrder(MethodOrderer.OrderAnnotation.class) | ||||||
| @@ -62,22 +63,38 @@ class CredentialsScenarioTests extends ScenarioTest { | |||||||
|                     .given("nickname", "firby-susan") |                     .given("nickname", "firby-susan") | ||||||
|                     // initial credentials |                     // initial credentials | ||||||
|                     .given("active", true) |                     .given("active", true) | ||||||
|  |                     .given("totpSecrets", Array.of("initialSecret")) | ||||||
|                     .given("emailAddress", "susan.firby@example.com") |                     .given("emailAddress", "susan.firby@example.com") | ||||||
|                     .given("telephonePassword", "securePass123") |                     .given("phonePassword", "securePass123") | ||||||
|                     .given("smsNumber", "+49123456789") |                     .given("smsNumber", "+49123456789") | ||||||
|                     .given("globalUid", 21011) |                     .given("globalUid", 21011) | ||||||
|                     .given("globalGid", 21011) |                     .given("globalGid", 21011) | ||||||
|                     .given("contexts", Array.of( |                     .given("contexts", Array.of( | ||||||
|                             Map.ofEntries( |                             Pair.of("HSADMIN", "prod") | ||||||
|                                     // a hardcoded context from test-data |  | ||||||
|                                     // TODO.impl: the uuid should be determined within CreateCredentials just by (HSDAMIN,prod) |  | ||||||
|                                     Map.entry("uuid", "11111111-1111-1111-1111-111111111111"), |  | ||||||
|                                     Map.entry("type", "HSADMIN"), |  | ||||||
|                                     Map.entry("qualifier", "prod") |  | ||||||
|                             ) |  | ||||||
|                     )) |                     )) | ||||||
|  |                     .given("onboardingToken", "fake-unboarding-token") | ||||||
|                     .doRun() |                     .doRun() | ||||||
|                     .keep(); |                     .keep(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         @Test | ||||||
|  |         @Order(1020) | ||||||
|  |         @Requires("Credentials@hsadmin: firby-susan") | ||||||
|  |         void shouldUpdateCredentials() { | ||||||
|  |             new UpdateCredentials(scenarioTest) | ||||||
|  |                     // the credentials to update | ||||||
|  |                     .given("credentialsUuid", "%{Credentials@hsadmin: firby-susan}") | ||||||
|  |                     // updated credentials | ||||||
|  |                     .given("active", false) | ||||||
|  |                     .given("totpSecrets", Array.of("initialSecret", "additionalSecret")) | ||||||
|  |                     .given("emailAddress", "susan.firby@example.org") | ||||||
|  |                     .given("phonePassword", "securePass987") | ||||||
|  |                     .given("smsNumber", "+49987654321") | ||||||
|  |                     .given("contexts", Array.of( | ||||||
|  |                             Pair.of("HSADMIN", "prod"), | ||||||
|  |                             Pair.of("SSH", "internal") | ||||||
|  |                     )) | ||||||
|  |                     .doRun(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,56 @@ | |||||||
|  | package net.hostsharing.hsadminng.hs.accounts.scenarios; | ||||||
|  |  | ||||||
|  | import io.restassured.http.ContentType; | ||||||
|  | import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; | ||||||
|  | import net.hostsharing.hsadminng.hs.scenarios.UseCase; | ||||||
|  | import org.springframework.http.HttpStatus; | ||||||
|  |  | ||||||
|  | import static io.restassured.http.ContentType.JSON; | ||||||
|  | import static org.springframework.http.HttpStatus.OK; | ||||||
|  |  | ||||||
|  | public class UpdateCredentials extends BaseCredentialsUseCase<UpdateCredentials> { | ||||||
|  |  | ||||||
|  |     public UpdateCredentials(final ScenarioTest testSuite) { | ||||||
|  |         super(testSuite); | ||||||
|  |  | ||||||
|  |         introduction("A set of credentials contains the login data for an RBAC subject."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected HttpResponse run() { | ||||||
|  |  | ||||||
|  |         given("resolvedContexts", | ||||||
|  |                 fetchContextResourcesByDescriptorPairs("contexts") | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         withTitle("Patch the Changes to the existing Credentials", () -> | ||||||
|  |             httpPatch("/api/hs/accounts/credentials/%{credentialsUuid}", usingJsonBody(""" | ||||||
|  |                 { | ||||||
|  |                      "active": %{active}, | ||||||
|  |                      "totpSecrets": @{totpSecrets}, | ||||||
|  |                      "emailAddress": ${emailAddress}, | ||||||
|  |                      "phonePassword": ${phonePassword}, | ||||||
|  |                      "smsNumber": ${smsNumber}, | ||||||
|  |                      "contexts": @{resolvedContexts} | ||||||
|  |                 } | ||||||
|  |                 """)) | ||||||
|  |                 .reportWithResponse().expecting(HttpStatus.OK).expecting(ContentType.JSON) | ||||||
|  |                 .extractValue("nickname", "nickname") | ||||||
|  |                 .extractValue("totpSecrets", "totpSecrets") | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void verify(final UseCase<UpdateCredentials>.HttpResponse response) { | ||||||
|  |         verify( | ||||||
|  |                 "Verify the Patched Credentials", | ||||||
|  |                 () -> httpGet("/api/hs/accounts/credentials/%{credentialsUuid}") | ||||||
|  |                         .expecting(OK).expecting(JSON), | ||||||
|  |                 path("uuid").contains("%{newCredentials}"), | ||||||
|  |                 path("nickname").contains("%{nickname}"), | ||||||
|  |                 path("totpSecrets").contains("%{totpSecrets}") | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -18,12 +18,13 @@ public class PathAssertion { | |||||||
|  |  | ||||||
|     @SuppressWarnings({ "unchecked", "rawtypes" }) |     @SuppressWarnings({ "unchecked", "rawtypes" }) | ||||||
|     public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) { |     public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) { | ||||||
|  |         final var resolvedValue = ScenarioTest.resolve(resolvableValue, DROP_COMMENTS); | ||||||
|         return response -> { |         return response -> { | ||||||
|             try { |             try { | ||||||
|                 response.path(path).isEqualTo(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)); |                 response.path(path).isEqualTo(resolvedValue); | ||||||
|             } catch (final AssertionError e) { |             } catch (final AssertionError e) { | ||||||
|                 // without this, the error message is often lacking important context |                 // without this, the error message is often lacking important context | ||||||
|                 fail(e.getMessage() + " in `path(\"" + path +  "\").contains(\"" + resolvableValue + "\")`" ); |                 fail(e.getMessage() + " in `path(\"" + path +  "\").contains(\"" + resolvedValue + "\")`" ); | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -66,13 +66,13 @@ public abstract class ScenarioTest extends ContextBasedTest { | |||||||
|                 keepProducesAlias(currentTestMethod); |                 keepProducesAlias(currentTestMethod); | ||||||
|             }); |             }); | ||||||
|             testReport.createTestLogMarkdownFile(testInfo); |             testReport.createTestLogMarkdownFile(testInfo); | ||||||
|         } catch (Exception exc) { |         } catch (final Exception exc) { | ||||||
|             throw exc; |             throw exc; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @AfterEach |     @AfterEach | ||||||
|     void afterScenario(final TestInfo testInfo) { // final TestInfo testInfo |     void afterScenario(final TestInfo testInfo) { | ||||||
|         verifyProduceDeclaration(testInfo); |         verifyProduceDeclaration(testInfo); | ||||||
|  |  | ||||||
|         properties.clear(); |         properties.clear(); | ||||||
| @@ -191,7 +191,7 @@ public abstract class ScenarioTest extends ContextBasedTest { | |||||||
|         properties.remove(propName); |         properties.remove(propName); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     static Map<String, Object> knowVariables() { |     public static Map<String, Object> knowVariables() { | ||||||
|         final var map = new LinkedHashMap<String, Object>(); |         final var map = new LinkedHashMap<String, Object>(); | ||||||
|         map.putAll(ScenarioTest.aliases); |         map.putAll(ScenarioTest.aliases); | ||||||
|         map.putAll(ScenarioTest.properties); |         map.putAll(ScenarioTest.properties); | ||||||
| @@ -223,4 +223,13 @@ public abstract class ScenarioTest extends ContextBasedTest { | |||||||
|         return (T) resolvedValue; |         return (T) resolvedValue; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public static <T> T getTypedVariable(final String varName, final Class<T> expectedValueClass) { | ||||||
|  |         final var value = knowVariables().get(varName); | ||||||
|  |         if (value != null && !expectedValueClass.isAssignableFrom(value.getClass())) { | ||||||
|  |             throw new IllegalArgumentException("variable '" + varName + "'" + | ||||||
|  |                     " expected to be of type " + expectedValueClass + " " + | ||||||
|  |                     " but got " + value.getClass()); | ||||||
|  |         } | ||||||
|  |         return (T) value; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| package net.hostsharing.hsadminng.hs.scenarios; | package net.hostsharing.hsadminng.hs.scenarios; | ||||||
|  |  | ||||||
|  | import com.fasterxml.jackson.databind.ObjectMapper; | ||||||
|  | import lombok.SneakyThrows; | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
|  |  | ||||||
| import java.net.URLEncoder; | import java.net.URLEncoder; | ||||||
| @@ -17,6 +19,7 @@ import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.D | |||||||
| public class TemplateResolver { | public class TemplateResolver { | ||||||
|  |  | ||||||
|     public static final String JSON_NULL_VALUE_TO_KEEP = "NULL"; |     public static final String JSON_NULL_VALUE_TO_KEEP = "NULL"; | ||||||
|  |     public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||||||
|  |  | ||||||
|     public enum Resolver { |     public enum Resolver { | ||||||
|         DROP_COMMENTS,  // deletes comments ('#{whatever}' -> '') |         DROP_COMMENTS,  // deletes comments ('#{whatever}' -> '') | ||||||
| @@ -230,6 +233,7 @@ public class TemplateResolver { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @SneakyThrows | ||||||
|     private static String jsonObject(final Object value) { |     private static String jsonObject(final Object value) { | ||||||
|         return switch (value) { |         return switch (value) { | ||||||
|             case null -> "null"; |             case null -> "null"; | ||||||
| @@ -237,7 +241,7 @@ public class TemplateResolver { | |||||||
|                     .map(entry -> "\"" + entry.getKey() + "\": " + jsonQuoted(entry.getValue())) |                     .map(entry -> "\"" + entry.getKey() + "\": " + jsonQuoted(entry.getValue())) | ||||||
|                     .collect(Collectors.joining(", ")) + "}"; |                     .collect(Collectors.joining(", ")) + "}"; | ||||||
|             case String string -> "{" + string.replace("\n", " ") + "}"; |             case String string -> "{" + string.replace("\n", " ") + "}"; | ||||||
|             default -> throw new IllegalArgumentException("can not format " + value.getClass() + " (" + value + ") as JSON object"); |             default -> OBJECT_MAPPER.writeValueAsString(value); | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ public abstract class UseCase<T extends UseCase<?>> { | |||||||
|  |  | ||||||
|     private static final HttpClient client = HttpClient.newHttpClient(); |     private static final HttpClient client = HttpClient.newHttpClient(); | ||||||
|     private static final int HTTP_TIMEOUT_SECONDS = 20; // FIXME: configurable in environment |     private static final int HTTP_TIMEOUT_SECONDS = 20; // FIXME: configurable in environment | ||||||
|     private final ObjectMapper objectMapper = new ObjectMapper(); |     protected final ObjectMapper objectMapper = new ObjectMapper(); | ||||||
|  |  | ||||||
|     protected final ScenarioTest testSuite; |     protected final ScenarioTest testSuite; | ||||||
|     private final TestReport testReport; |     private final TestReport testReport; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user