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{ | ||||
|         +totpSecret: text | ||||
|         +telephonePassword: text | ||||
|         +phonePassword: text | ||||
|         +emailAdress: text | ||||
|         +smsNumber: text | ||||
|         -active: bool [r/w] | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import java.util.function.BiConsumer; | ||||
|  | ||||
| import io.micrometer.core.annotation.Timed; | ||||
| 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.context.Context; | ||||
| import net.hostsharing.hsadminng.accounts.generated.api.v1.api.CredentialsApi; | ||||
| @@ -67,12 +68,12 @@ public class HsCredentialsController implements CredentialsApi { | ||||
|             final UUID credentialsUuid) { | ||||
|         context.assumeRoles(assumedRoles); | ||||
|  | ||||
|         final var credentials = credentialsRepo.findByUuid(credentialsUuid); | ||||
|         if (credentials.isEmpty()) { | ||||
|         final var credentialsEntity = credentialsRepo.findByUuid(credentialsUuid); | ||||
|         if (credentialsEntity.isEmpty()) { | ||||
|             return ResponseEntity.notFound().build(); | ||||
|         } | ||||
|         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); | ||||
|     } | ||||
|  | ||||
| @@ -192,6 +193,7 @@ public class HsCredentialsController implements CredentialsApi { | ||||
|                         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) -> { | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.repr.Stringifyable; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| import java.util.UUID; | ||||
|  | ||||
| @@ -30,7 +31,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str | ||||
|     protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "credentials") | ||||
|             .withProp(HsCredentialsEntity::isActive) | ||||
|             .withProp(HsCredentialsEntity::getEmailAddress) | ||||
|             .withProp(HsCredentialsEntity::getTotpSecret) | ||||
|             .withProp(HsCredentialsEntity::getTotpSecrets) | ||||
|             .withProp(HsCredentialsEntity::getPhonePassword) | ||||
|             .withProp(HsCredentialsEntity::getSmsNumber) | ||||
|             .quotedValues(false); | ||||
| @@ -66,7 +67,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str | ||||
|     private String onboardingToken; | ||||
|  | ||||
|     @Column | ||||
|     private String totpSecret; | ||||
|     private List<String> totpSecrets; | ||||
|  | ||||
|     @Column | ||||
|     private String phonePassword; | ||||
| @@ -106,4 +107,5 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str | ||||
|     public String toString() { | ||||
|         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.OptionalFromJson; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> { | ||||
|  | ||||
| @@ -22,8 +23,8 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatc | ||||
|         } | ||||
|         OptionalFromJson.of(resource.getEmailAddress()) | ||||
|                 .ifPresent(entity::setEmailAddress); | ||||
|         OptionalFromJson.of(resource.getTotpSecret()) | ||||
|                 .ifPresent(entity::setTotpSecret); | ||||
|         Optional.ofNullable(resource.getTotpSecrets()) | ||||
|                 .ifPresent(entity::setTotpSecrets); | ||||
|         OptionalFromJson.of(resource.getSmsNumber()) | ||||
|                 .ifPresent(entity::setSmsNumber); | ||||
|         OptionalFromJson.of(resource.getPhonePassword()) | ||||
|   | ||||
| @@ -14,9 +14,11 @@ components: | ||||
|                 nickname: | ||||
|                     type: string | ||||
|                     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 | ||||
|                 telephonePassword: | ||||
|                 phonePassword: | ||||
|                     type: string | ||||
|                 emailAddress: | ||||
|                     type: string | ||||
| @@ -46,9 +48,10 @@ components: | ||||
|         CredentialsPatch: | ||||
|             type: object | ||||
|             properties: | ||||
|                 totpSecret: | ||||
|                 totpSecrets: | ||||
|                     type: array | ||||
|                     items: | ||||
|                         type: string | ||||
|                     nullable: true | ||||
|                 phonePassword: | ||||
|                     type: string | ||||
|                     nullable: true | ||||
| @@ -75,9 +78,11 @@ components: | ||||
|                 nickname: | ||||
|                     type: string | ||||
|                     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 | ||||
|                 telephonePassword: | ||||
|                 phonePassword: | ||||
|                     type: string | ||||
|                 emailAddress: | ||||
|                     type: string | ||||
|   | ||||
| @@ -18,7 +18,7 @@ create table hs_accounts.credentials | ||||
|     global_gid       int unique,     -- w/o | ||||
|     onboarding_token text,           -- w/o, but can be set to null to invalidate | ||||
|  | ||||
|     totp_secret      text, | ||||
|     totp_secrets     text[], | ||||
|     phone_password   text, | ||||
|     email_address    text, | ||||
|     sms_number       text | ||||
|   | ||||
| @@ -51,9 +51,9 @@ begin | ||||
| --     call rbac.grantRoleToRole(hs_accounts.context_REFERRER(context_MATRIX_internal), rbac.global_ADMIN()); | ||||
|  | ||||
|     -- 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 | ||||
|         ( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', 'otp-secret-1', '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'); | ||||
|     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', ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'), | ||||
|         ( 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 | ||||
|     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 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_PHONE_PASSWORD = "initial_phone_pw"; | ||||
|  | ||||
|     private static final Boolean PATCHED_ACTIVE = false; | ||||
|     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_PHONE_PASSWORD = "patched_phone_pw"; | ||||
|  | ||||
| @@ -102,7 +102,7 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | ||||
|         entity.setUuid(INITIAL_CREDENTIALS_UUID); | ||||
|         entity.setActive(INITIAL_ACTIVE); | ||||
|         entity.setEmailAddress(INITIAL_EMAIL_ADDRESS); | ||||
|         entity.setTotpSecret(INITIAL_TOTP_SECRET); | ||||
|         entity.setTotpSecrets(INITIAL_TOTP_SECRETS); | ||||
|         entity.setSmsNumber(INITIAL_SMS_NUMBER); | ||||
|         entity.setPhonePassword(INITIAL_PHONE_PASSWORD); | ||||
|         // Ensure loginContexts is a mutable set for the patcher | ||||
| @@ -137,12 +137,13 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< | ||||
|                         PATCHED_EMAIL_ADDRESS, | ||||
|                         HsCredentialsEntity::setEmailAddress, | ||||
|                         PATCHED_EMAIL_ADDRESS), | ||||
|                 new JsonNullableProperty<>( | ||||
|                 new SimpleProperty<>( | ||||
|                         "totpSecret", | ||||
|                         CredentialsPatchResource::setTotpSecret, | ||||
|                         PATCHED_TOTP_SECRET, | ||||
|                         HsCredentialsEntity::setTotpSecret, | ||||
|                         PATCHED_TOTP_SECRET), | ||||
|                         CredentialsPatchResource::setTotpSecrets, | ||||
|                         PATCHED_TOTP_SECRETS, | ||||
|                         HsCredentialsEntity::setTotpSecrets, | ||||
|                         PATCHED_TOTP_SECRETS) | ||||
|                         .notNullable(), | ||||
|                 new JsonNullableProperty<>( | ||||
|                         "smsNumber", | ||||
|                         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 org.springframework.http.HttpStatus.OK; | ||||
|  | ||||
| public class CreateCredentials extends UseCase<CreateCredentials> { | ||||
| public class CreateCredentials extends BaseCredentialsUseCase<CreateCredentials> { | ||||
|  | ||||
|     public CreateCredentials(final ScenarioTest 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." | ||||
|         ); | ||||
|  | ||||
|  | ||||
|         obtain("CredentialsContexts", () -> | ||||
|                 httpGet("/api/hs/accounts/contexts").expecting(OK).expecting(JSON) | ||||
|         given("resolvedContexts", | ||||
|             fetchContextResourcesByDescriptorPairs("contexts") | ||||
|         ); | ||||
|  | ||||
|         return obtain("newCredentials", () -> | ||||
| @@ -37,12 +36,14 @@ public class CreateCredentials extends UseCase<CreateCredentials> { | ||||
|                      "person.uuid": ${Person: %{personGivenName} %{personFamilyName}}, | ||||
|                      "nickname": ${nickname}, | ||||
|                      "active": %{active}, | ||||
|                      "totpSecrets": @{totpSecrets}, | ||||
|                      "emailAddress": ${emailAddress}, | ||||
|                      "telephonePassword": ${telephonePassword}, | ||||
|                      "phonePassword": ${phonePassword}, | ||||
|                      "smsNumber": ${smsNumber}, | ||||
|                      "onboardingToken": ${onboardingToken}, | ||||
|                      "globalUid": %{globalUid}, | ||||
|                      "globalGid": %{globalGid}, | ||||
|                      "contexts": @{contexts} | ||||
|                      "contexts": @{resolvedContexts} | ||||
|                 } | ||||
|                 """)) | ||||
|                 .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) | ||||
| @@ -57,7 +58,9 @@ public class CreateCredentials extends UseCase<CreateCredentials> { | ||||
|                         .expecting(OK).expecting(JSON), | ||||
|                 path("uuid").contains("%{newCredentials}"), | ||||
|                 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.config.DisableSecurityConfig; | ||||
| 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.mapper.Array; | ||||
| import net.hostsharing.hsadminng.rbac.test.JpaAttempt; | ||||
| import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension; | ||||
| import org.apache.commons.lang3.tuple.Pair; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import org.junit.jupiter.api.ClassOrderer; | ||||
| 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.test.context.ActiveProfiles; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| @Tag("scenarioTest") | ||||
| @SpringBootTest( | ||||
|         webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, | ||||
| @@ -45,6 +45,7 @@ class CredentialsScenarioTests extends ScenarioTest { | ||||
|     protected void beforeScenario(final TestInfo testInfo) { | ||||
|         super.beforeScenario(testInfo); | ||||
|     } | ||||
|  | ||||
|     @Nested | ||||
|     @Order(10) | ||||
|     @TestMethodOrder(MethodOrderer.OrderAnnotation.class) | ||||
| @@ -62,22 +63,38 @@ class CredentialsScenarioTests extends ScenarioTest { | ||||
|                     .given("nickname", "firby-susan") | ||||
|                     // initial credentials | ||||
|                     .given("active", true) | ||||
|                     .given("totpSecrets", Array.of("initialSecret")) | ||||
|                     .given("emailAddress", "susan.firby@example.com") | ||||
|                     .given("telephonePassword", "securePass123") | ||||
|                     .given("phonePassword", "securePass123") | ||||
|                     .given("smsNumber", "+49123456789") | ||||
|                     .given("globalUid", 21011) | ||||
|                     .given("globalGid", 21011) | ||||
|                     .given("contexts", Array.of( | ||||
|                             Map.ofEntries( | ||||
|                                     // 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") | ||||
|                             ) | ||||
|                             Pair.of("HSADMIN", "prod") | ||||
|                     )) | ||||
|                     .given("onboardingToken", "fake-unboarding-token") | ||||
|                     .doRun() | ||||
|                     .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" }) | ||||
|     public Consumer<UseCase.HttpResponse> contains(final String resolvableValue) { | ||||
|         final var resolvedValue = ScenarioTest.resolve(resolvableValue, DROP_COMMENTS); | ||||
|         return response -> { | ||||
|             try { | ||||
|                 response.path(path).isEqualTo(ScenarioTest.resolve(resolvableValue, DROP_COMMENTS)); | ||||
|                 response.path(path).isEqualTo(resolvedValue); | ||||
|             } catch (final AssertionError e) { | ||||
|                 // 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); | ||||
|             }); | ||||
|             testReport.createTestLogMarkdownFile(testInfo); | ||||
|         } catch (Exception exc) { | ||||
|         } catch (final Exception exc) { | ||||
|             throw exc; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @AfterEach | ||||
|     void afterScenario(final TestInfo testInfo) { // final TestInfo testInfo | ||||
|     void afterScenario(final TestInfo testInfo) { | ||||
|         verifyProduceDeclaration(testInfo); | ||||
|  | ||||
|         properties.clear(); | ||||
| @@ -191,7 +191,7 @@ public abstract class ScenarioTest extends ContextBasedTest { | ||||
|         properties.remove(propName); | ||||
|     } | ||||
|  | ||||
|     static Map<String, Object> knowVariables() { | ||||
|     public static Map<String, Object> knowVariables() { | ||||
|         final var map = new LinkedHashMap<String, Object>(); | ||||
|         map.putAll(ScenarioTest.aliases); | ||||
|         map.putAll(ScenarioTest.properties); | ||||
| @@ -223,4 +223,13 @@ public abstract class ScenarioTest extends ContextBasedTest { | ||||
|         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; | ||||
|  | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| import lombok.SneakyThrows; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
|  | ||||
| import java.net.URLEncoder; | ||||
| @@ -17,6 +19,7 @@ import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.D | ||||
| public class TemplateResolver { | ||||
|  | ||||
|     public static final String JSON_NULL_VALUE_TO_KEEP = "NULL"; | ||||
|     public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||||
|  | ||||
|     public enum Resolver { | ||||
|         DROP_COMMENTS,  // deletes comments ('#{whatever}' -> '') | ||||
| @@ -230,6 +233,7 @@ public class TemplateResolver { | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     @SneakyThrows | ||||
|     private static String jsonObject(final Object value) { | ||||
|         return switch (value) { | ||||
|             case null -> "null"; | ||||
| @@ -237,7 +241,7 @@ public class TemplateResolver { | ||||
|                     .map(entry -> "\"" + entry.getKey() + "\": " + jsonQuoted(entry.getValue())) | ||||
|                     .collect(Collectors.joining(", ")) + "}"; | ||||
|             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 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; | ||||
|     private final TestReport testReport; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user