1
0

align React-GUI and Java API -backend (#188)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/188
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2025-08-12 12:50:48 +02:00
parent 75f8a6a7db
commit 60028697d6
17 changed files with 212 additions and 91 deletions
@@ -19,6 +19,7 @@ import net.hostsharing.hsadminng.repr.Stringifyable;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.repr.Symbol.symbol;
@Getter @Getter
@Setter @Setter
@@ -31,6 +32,10 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
private static Stringify<HsCredentialsContext> stringify = stringify(HsCredentialsContext.class, "loginContext") private static Stringify<HsCredentialsContext> stringify = stringify(HsCredentialsContext.class, "loginContext")
.withProp(HsCredentialsContext::getType) .withProp(HsCredentialsContext::getType)
.withProp(HsCredentialsContext::getQualifier) .withProp(HsCredentialsContext::getQualifier)
.withProp(HsCredentialsContext::isOnlyForNaturalPersons,
value -> value ? symbol("NP-ONLY") : null)
.withProp(HsCredentialsContext::isPublicAccess,
value -> value ? symbol("PUBLIC") : symbol("INTERNAL"))
.quotedValues(false) .quotedValues(false)
.withSeparator(":"); .withSeparator(":");
@@ -53,6 +58,9 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
@Column(name = "only_for_natural_persons") @Column(name = "only_for_natural_persons")
private boolean onlyForNaturalPersons; private boolean onlyForNaturalPersons;
@Column(name = "public_access")
private boolean publicAccess;
@Override @Override
public String toShortString() { public String toShortString() {
return toString(); return toString();
@@ -81,19 +81,15 @@ public class HsCredentialsController implements CredentialsApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
@Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid") @Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid")
public ResponseEntity<List<CredentialsResource>> getListOfCredentialsByPersonUuid( public ResponseEntity<List<CredentialsResource>> getListOfCredentials(
final String assumedRoles, final String assumedRoles,
final UUID personUuid final UUID personUuid
) { ) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow( final var credentials = personUuid == null
() -> new EntityNotFoundException( ? credentialsRepo.findByCurrentSubject()
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid) : findByPersonUuid(personUuid);
)
);
final var credentials = credentialsRepo.findByPerson(person);
final var result = mapper.mapList( final var result = mapper.mapList(
credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(result); return ResponseEntity.ok(result);
@@ -183,6 +179,16 @@ public class HsCredentialsController implements CredentialsApi {
return subjectRepo.findByUuid(newRbacSubject.getUuid()); // attached to EM return subjectRepo.findByUuid(newRbacSubject.getUuid()); // attached to EM
} }
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow(
() -> new EntityNotFoundException(
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
)
);
return credentialsRepo.findByPerson(person);
}
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
ofNullable(entity.getLastUsed()).ifPresent( ofNullable(entity.getLastUsed()).ifPresent(
dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC))); dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC)));
@@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed; import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson; import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import java.util.List; import java.util.List;
@@ -14,7 +15,36 @@ public interface HsCredentialsRepository extends Repository<HsCredentialsEntity,
Optional<HsCredentialsEntity> findByUuid(final UUID uuid); Optional<HsCredentialsEntity> findByUuid(final UUID uuid);
@Timed("app.login.credentials.repo.findByPerson") @Timed("app.login.credentials.repo.findByPerson")
List<HsCredentialsEntity> findByPerson(final HsOfficePerson personUuid); List<HsCredentialsEntity> findByPerson(final HsOfficePerson<?> personUuid);
@Timed("app.login.credentials.repo.findByCurrentSubject")
@Query(nativeQuery = true, value = """
WITH RECURSIVE owned_persons AS (
-- Start with the person linked to current subject's credentials
SELECT p.uuid AS person_uuid
FROM hs_accounts.credentials c
JOIN hs_office.person p ON p.uuid = c.person_uuid
WHERE c.uuid = rbac.currentSubjectUuid()
UNION
-- Add persons where the current person has OWNER role
SELECT p.uuid AS person_uuid
FROM owned_persons op
CROSS JOIN hs_office.person p
WHERE rbac.isGranted(
rbac.currentSubjectUuid(),
rbac.findRoleId(
rbac.roleDescriptorOf('hs_office.person', p.uuid, 'OWNER'::rbac.RoleType, false)
)
)
)
SELECT DISTINCT c.*
FROM hs_accounts.credentials c
WHERE c.uuid = rbac.currentSubjectUuid() -- Include current subject's own credentials
OR c.person_uuid IN (SELECT person_uuid FROM owned_persons) -- Include credentials of owned persons
""")
List<HsCredentialsEntity> findByCurrentSubject();
@Timed("app.login.credentials.repo.save") @Timed("app.login.credentials.repo.save")
HsCredentialsEntity save(final HsCredentialsEntity entity); HsCredentialsEntity save(final HsCredentialsEntity entity);
@@ -1,5 +1,8 @@
package net.hostsharing.hsadminng.repr; package net.hostsharing.hsadminng.repr;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Value;
import net.hostsharing.hsadminng.errors.DisplayAs; import net.hostsharing.hsadminng.errors.DisplayAs;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@@ -11,14 +14,13 @@ import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.lang.Boolean.TRUE;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
public final class Stringify<B> { public final class Stringify<B> {
private final String name; private final String name;
private Function<? extends B, ?> idProp; private Function<? extends B, ?> idProp;
private final List<Property<B>> props = new ArrayList<>(); private final List<Property<B, ?>> props = new ArrayList<>();
private String separator = ", "; private String separator = ", ";
private Boolean quotedValues = null; private Boolean quotedValues = null;
@@ -30,18 +32,6 @@ public final class Stringify<B> {
return new Stringify<>(clazz, null); return new Stringify<>(clazz, null);
} }
public <T extends B> Stringify<T> using(final Class<T> subClass) {
//noinspection unchecked
final var stringify = new Stringify<T>(subClass, null)
.withIdProp(cast(idProp))
.withProps(cast(props))
.withSeparator(separator);
if (quotedValues != null) {
stringify.quotedValues(quotedValues);
}
return stringify;
}
private Stringify(final Class<B> clazz, final String name) { private Stringify(final Class<B> clazz, final String name) {
if (name != null) { if (name != null) {
this.name = name; this.name = name;
@@ -55,31 +45,30 @@ public final class Stringify<B> {
} }
} }
public Stringify<B> withIdProp(final Function<? extends B, ?> getter) { public <V> Stringify<B> withIdProp(final Function<? extends B, V> getter) {
idProp = getter; idProp = getter;
return this; return this;
} }
public Stringify<B> withProp(final String propName, final Function<B, ?> getter) { public <V> Stringify<B> withProp(final String propName, final Function<B, V> getter) {
props.add(new Property<>(propName, getter)); props.add(new Property<>(propName, getter));
return this; return this;
} }
public Stringify<B> withProp(final Function<B, ?> getter) { public <V> Stringify<B> withProp(final Function<B, V> getter) {
props.add(new Property<>(null, getter)); props.add(new Property<>(null, getter));
return this; return this;
} }
private Stringify<B> withProps(final List<Property<B>> props) { public <V> Stringify<B> withProp(final Function<B, V> getter, final Function<V, ?> mapper) {
this.props.addAll(props); props.add(new Property<>(null, getter, mapper));
return this; return this;
} }
public String apply(@NotNull B object) { public String apply(@NotNull B object) {
final var propValues = props.stream() final var propValues = props.stream()
.map(prop -> PropertyValue.of(prop, prop.getter.apply(object))) .map(prop -> new PropertyValue<>(object, prop))
.filter(Objects::nonNull) .filter(PropertyValue::notNullAndNotEmpty)
.filter(PropertyValue::nonEmpty)
.map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal)) .map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
.collect(Collectors.joining(separator)); .collect(Collectors.joining(separator));
return idProp != null return idProp != null
@@ -92,24 +81,36 @@ public final class Stringify<B> {
return this; return this;
} }
private String propName(final PropertyValue<B> propVal, final String delimiter) { private <V> String propName(final PropertyValue<B, V> propVal, final String delimiter) {
return ofNullable(propVal.prop.name).map(v -> v + delimiter).orElse(""); return ofNullable(propVal.prop.name).map(v -> v + delimiter).orElse("");
} }
private String optionallyQuoted(final PropertyValue<B> propVal) { private <B, V> String optionallyQuoted(final PropertyValue<B, V> propVal) {
if (quotedValues == null) if (quotedValues == null)
return quotedQuotedValueType(propVal) return quotableValueType(propVal.getValue())
? ("'" + propVal.value + "'") ? ("'" + propVal.stringValue + "'")
: propVal.value; : propVal.stringValue;
return TRUE == quotedValues return quotedValues
? ("'" + propVal.value + "'") ? ("'" + propVal.stringValue + "'")
: propVal.value; : propVal.stringValue;
} }
private static <B> boolean quotedQuotedValueType(final PropertyValue<B> propVal) { private <V> boolean quotableValueType(final V rawValue) {
return !(propVal.rawValue instanceof Number || propVal.rawValue instanceof Boolean); return !(rawValue instanceof Enum) &&
!(rawValue instanceof Symbol) &&
!(rawValue instanceof Number) &&
!(rawValue instanceof Boolean);
} }
/**
* Specifies whether the values should be quoted (true) or not (false).
*
* If not specified, Enum, Symbol, Number and Boolean values are not quoted;
* other value types are quoted.
*
* @param quotedValues
* @return
*/
public Stringify<B> quotedValues(final boolean quotedValues) { public Stringify<B> quotedValues(final boolean quotedValues) {
this.quotedValues = quotedValues; this.quotedValues = quotedValues;
return this; return this;
@@ -120,23 +121,48 @@ public final class Stringify<B> {
return (T)object; return (T)object;
} }
private record Property<B>(String name, Function<B, ?> getter) {} @Value
@AllArgsConstructor
private class Property<B, V> {
String name;
Function<B, V> getter;
Function<V, ?> mapper; // FIXME: better generics?
private record PropertyValue<B>(Property<B> prop, Object rawValue, String value) { Property(String name, Function<B, V> getter) {
this(name, getter, v -> v);
static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) {
return rawValue != null ? new PropertyValue<>(prop, rawValue, toStringOrShortString(rawValue)) : null;
} }
private static String toStringOrShortString(final Object rawValue) { Object getValue(final B object) {
return rawValue instanceof Stringifyable stringifyable ? stringifyable.toShortString() : rawValue.toString(); return ofNullable(getter.apply(object))
.map(mapper)
.orElse(null);
}
} }
boolean nonEmpty() { @Getter
return rawValue != null && private class PropertyValue<B, V> {
(!(rawValue instanceof Collection<?> c) || !c.isEmpty()) && private Property<B, V> prop;
(!(rawValue instanceof Map<?,?> m) || !m.isEmpty()) && private V value;
(!(rawValue instanceof String s) || !s.isEmpty()); private String stringValue;
@SuppressWarnings("unchecked")
PropertyValue(final B object, final Property<B, ?> prop) {
// FIXME: simplify
final var typedProp = (Property<B, V>) prop;
final var value = typedProp.getValue(object);
final var stringifiedValue = value instanceof Stringifyable stringifyable
? stringifyable.toShortString()
: Objects.toString(value);
this.prop = typedProp;
this.value = (V) value;
this.stringValue = stringifiedValue;
}
boolean notNullAndNotEmpty() {
return value != null &&
(!(value instanceof Collection<?> c) || !c.isEmpty()) &&
(!(value instanceof Map<?,?> m) || !m.isEmpty()) &&
(!(value instanceof String s) || !s.isEmpty());
} }
} }
} }
@@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.repr;
import jakarta.validation.constraints.NotNull;
/**
* A String value which is used as a symbol and thus does not get quoted
* and is, by definition, different from any other symbol with the same name.
*/
public class Symbol {
private final String value;
public static Symbol symbol(@NotNull final String value) {
return new Symbol(value);
}
private Symbol(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}
@@ -17,7 +17,11 @@ components:
maxLength: 80 maxLength: 80
onlyForNaturalPersons: onlyForNaturalPersons:
type: boolean type: boolean
publicAccess:
type: boolean
required: required:
- uuid - uuid
- type - type
- qualifier - qualifier
- onlyForNaturalPersons
- publicAccess
@@ -3,16 +3,16 @@ get:
description: Returns the list of all credentials which are visible to the current subject or any of it's assumed roles. description: Returns the list of all credentials which are visible to the current subject or any of it's assumed roles.
tags: tags:
- credentials - credentials
operationId: getListOfCredentialsByPersonUuid operationId: getListOfCredentials
parameters: parameters:
- $ref: 'auth.yaml#/components/parameters/assumedRoles' - $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: personUuid - name: personUuid
in: query in: query
required: true required: false
schema: schema:
type: string type: string
format: uuid format: uuid
description: The UUID of the person, whose credentials are to be fetched. description: The UUID of the person, whose credentials are to be fetched. Or null, if all credentials of the login-use should be fetched.
responses: responses:
"200": "200":
description: OK description: OK
@@ -40,6 +40,8 @@ create table hs_accounts.context
only_for_natural_persons boolean default false, only_for_natural_persons boolean default false,
public_access boolean default false,
unique (type, qualifier) unique (type, qualifier)
); );
--// --//
@@ -15,7 +15,9 @@ declare
context_HSADMIN_prod hs_accounts.context; context_HSADMIN_prod hs_accounts.context;
context_SSH_internal hs_accounts.context; context_SSH_internal hs_accounts.context;
context_SSH_external hs_accounts.context;
context_MATRIX_internal hs_accounts.context; context_MATRIX_internal hs_accounts.context;
context_MATRIX_external hs_accounts.context;
begin begin
call base.defineContext('creating booking-project test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); call base.defineContext('creating booking-project test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN');
@@ -26,17 +28,25 @@ begin
personFranUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Fran'); personFranUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Fran');
-- Add test contexts -- Add test contexts
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true) ('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true, true)
RETURNING * INTO context_HSADMIN_prod; RETURNING * INTO context_HSADMIN_prod;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true) ('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true, false)
RETURNING * INTO context_SSH_internal; RETURNING * INTO context_SSH_internal;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('33333333-3333-3333-3333-333333333333', 'MATRIX', 'internal', true) ('33333333-3333-3333-3333-333333333333', 'SSH', 'external', false, true)
RETURNING * INTO context_SSH_external;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('44444444-4444-4444-4444-444444444444', 'MATRIX', 'internal', true, false)
RETURNING * INTO context_MATRIX_internal; RETURNING * INTO context_MATRIX_internal;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('44444444-4444-4444-4444-444444444444', 'MASTODON', 'external', false); ('55555555-5555-5555-5555-555555555555', 'MATRIX', 'external', true, true)
RETURNING * INTO context_MATRIX_external;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('66666666-6666-6666-6666-666666666666', 'MASTODON', 'external', false, true);
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('77777777-7777-7777-7777-777777777777', 'BBB', 'external', false, true);
-- grant general access to public credential contexts -- grant general access to public credential contexts
-- TODO_impl: RBAC rules for _rv do not yet work properly -- TODO_impl: RBAC rules for _rv do not yet work properly
@@ -59,12 +69,12 @@ begin
-- 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
(superuserAlexSubjectUuid, '11111111-1111-1111-1111-111111111111'), -- HSADMIN context (superuserAlexSubjectUuid, context_HSADMIN_prod.uuid),
(superuserFranSubjectUuid, '11111111-1111-1111-1111-111111111111'), -- HSADMIN context (superuserFranSubjectUuid, context_HSADMIN_prod.uuid),
(superuserAlexSubjectUuid, '22222222-2222-2222-2222-222222222222'), -- SSH context (superuserAlexSubjectUuid, context_SSH_internal.uuid),
(superuserFranSubjectUuid, '22222222-2222-2222-2222-222222222222'), -- SSH context (superuserFranSubjectUuid, context_SSH_internal.uuid),
(superuserAlexSubjectUuid, '33333333-3333-3333-3333-333333333333'), -- MATRIX context (superuserAlexSubjectUuid, context_MATRIX_internal.uuid),
(superuserFranSubjectUuid, '33333333-3333-3333-3333-333333333333'); -- MATRIX context (superuserFranSubjectUuid, context_MATRIX_internal.uuid);
end; $$; end; $$;
--// --//
@@ -14,7 +14,8 @@ class HsCredentialsContextRbacEntityUnitTest {
.uuid(UUID.randomUUID()) .uuid(UUID.randomUUID())
.type("SSH") .type("SSH")
.qualifier("prod") .qualifier("prod")
.publicAccess(true)
.build(); .build();
assertEquals("loginContext(SSH:prod)", entity.toShortString()); assertEquals("loginContext(SSH:prod:PUBLIC)", entity.toShortString());
} }
} }
@@ -77,7 +77,7 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
// then // then
assertThat(foundEntityOptional).isPresent(); assertThat(foundEntityOptional).isPresent();
assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(HSADMIN:prod)"); assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)");
} }
@Test @Test
@@ -89,7 +89,7 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
// then // then
assertThat(foundEntityOptional).isPresent(); assertThat(foundEntityOptional).isPresent();
assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(SSH:internal)"); assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(SSH:internal:NP-ONLY:INTERNAL)");
} }
@Test @Test
@@ -14,7 +14,8 @@ class HsCredentialsContextRealEntityUnitTest {
.uuid(UUID.randomUUID()) .uuid(UUID.randomUUID())
.type("testType") .type("testType")
.qualifier("testQualifier") .qualifier("testQualifier")
.onlyForNaturalPersons(true)
.build(); .build();
assertEquals("loginContext(testType:testQualifier)", entity.toShortString()); assertEquals("loginContext(testType:testQualifier:NP-ONLY:INTERNAL)", entity.toShortString());
} }
} }
@@ -52,7 +52,9 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
final var rowsBefore = query.getResultList(); final var rowsBefore = query.getResultList();
// then // then
assertThat(rowsBefore).as("hs_accounts.context_hv only contain no rows for a timestamp before test data creation").hasSize(0); assertThat(rowsBefore)
.as("hs_accounts.context_hv only contain no rows for a timestamp before test data creation")
.hasSize(0);
// and when // and when
historicalContext(Timestamp.from(ZonedDateTime.now().toInstant())); historicalContext(Timestamp.from(ZonedDateTime.now().toInstant()));
@@ -60,7 +62,9 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
final var rowsAfter = query.getResultList(); final var rowsAfter = query.getResultList();
// then // then
assertThat(rowsAfter).as("hs_accounts.context_hv should now contain the test-data rows for the current timestamp").hasSize(4); assertThat(rowsAfter)
.as("hs_accounts.context_hv should now contain the test-data rows for the current timestamp")
.hasSize(7);
} }
@Test @Test
@@ -87,7 +91,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
// then // then
assertThat(foundEntityOptional).isPresent(); assertThat(foundEntityOptional).isPresent();
assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(HSADMIN:prod)"); assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)");
} }
@Test @Test
@@ -99,7 +103,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
// then // then
assertThat(foundEntityOptional).isPresent(); assertThat(foundEntityOptional).isPresent();
assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(SSH:internal)"); assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(SSH:internal:NP-ONLY:INTERNAL)");
} }
@Test @Test
@@ -125,7 +125,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
assertThat(foundEntity.getGlobalUid()).isEqualTo(2001); assertThat(foundEntity.getGlobalUid()).isEqualTo(2001);
assertThat(foundEntity.getLoginContexts()).hasSize(1) assertThat(foundEntity.getLoginContexts()).hasSize(1)
.map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod)"); .map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)");
} }
@Test @Test
@@ -117,7 +117,7 @@ class HsOfficePersonEntityUnitTest {
final var actualDisplay = givenPersonEntity.toString(); final var actualDisplay = givenPersonEntity.toString();
assertThat(actualDisplay).isEqualTo("person(personType='NP', tradeName='some trade name', title='Dr.', familyName='some family name', givenName='some given name')"); assertThat(actualDisplay).isEqualTo("person(personType=NP, tradeName='some trade name', title='Dr.', familyName='some family name', givenName='some given name')");
} }
@Test @Test
@@ -28,7 +28,7 @@ class HsOfficeRelationUnitTest {
.holder(holder) .holder(holder)
.build(); .build();
assertThat(given.toString()).isEqualTo("rel(anchor='LP some trade name', type='SUBSCRIBER', mark='members-announce', holder='NP Meier, Mellie')"); assertThat(given.toString()).isEqualTo("rel(anchor='LP some trade name', type=SUBSCRIBER, mark='members-announce', holder='NP Meier, Mellie')");
} }
@Test @Test
@@ -39,7 +39,7 @@ class HsOfficeRelationUnitTest {
.holder(holder) .holder(holder)
.build(); .build();
assertThat(given.toShortString()).isEqualTo("rel(anchor='LP some trade name', type='REPRESENTATIVE', holder='NP Meier, Mellie')"); assertThat(given.toShortString()).isEqualTo("rel(anchor='LP some trade name', type=REPRESENTATIVE, holder='NP Meier, Mellie')");
} }
@Test @Test
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.hsadminng.repr.Symbol.symbol;
import static net.hostsharing.hsadminng.repr.Stringify.stringify; import static net.hostsharing.hsadminng.repr.Stringify.stringify;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -58,11 +59,12 @@ class StringifyUnitTest {
private static Stringify<SubBeanWithUnquotedValues> toString = stringify(SubBeanWithUnquotedValues.class) private static Stringify<SubBeanWithUnquotedValues> toString = stringify(SubBeanWithUnquotedValues.class)
.withProp(SubBeanWithUnquotedValues::getKey) .withProp(SubBeanWithUnquotedValues::getKey)
.withProp(SubBeanWithUnquotedValues::getValue) .withProp(SubBeanWithUnquotedValues::getValue)
.withSeparator(": ") .withProp(SubBeanWithUnquotedValues::isActive, v -> v ? symbol("active") : symbol("inactive"))
.quotedValues(false); .quotedValues(false);
private String key; private String key;
private String value; private String value;
private boolean active;
@Override @Override
public String toString() { public String toString() {
@@ -84,11 +86,13 @@ class StringifyUnitTest {
private static Stringify<SubBeanWithQuotedValues> toString = stringify(SubBeanWithQuotedValues.class) private static Stringify<SubBeanWithQuotedValues> toString = stringify(SubBeanWithQuotedValues.class)
.withProp(SubBeanWithQuotedValues::getKey) .withProp(SubBeanWithQuotedValues::getKey)
.withProp(SubBeanWithQuotedValues::getValue) .withProp(SubBeanWithQuotedValues::getValue)
.withProp(SubBeanWithQuotedValues::isActive, v -> v ? "active" : "inactive")
.withSeparator(": ") .withSeparator(": ")
.quotedValues(true); .quotedValues(true);
private String key; private String key;
private Integer value; private Integer value;
private boolean active;
@Override @Override
public String toString() { public String toString() {
@@ -104,8 +108,8 @@ class StringifyUnitTest {
@Test @Test
void stringifyWhenAllPropsHaveValues() { void stringifyWhenAllPropsHaveValues() {
final var given = new TestBean(UUID.randomUUID(), "some caption", final var given = new TestBean(UUID.randomUUID(), "some caption",
new SubBeanWithUnquotedValues("some key", "some value"), new SubBeanWithUnquotedValues("some key", "some value", true),
new SubBeanWithQuotedValues("some key", 1234), new SubBeanWithQuotedValues("some key", 1234, false),
42, 42,
false); false);
final var result = given.toString(); final var result = given.toString();
@@ -122,15 +126,15 @@ class StringifyUnitTest {
@Test @Test
void stringifyWithoutExplicitNameUsesSimpleClassName() { void stringifyWithoutExplicitNameUsesSimpleClassName() {
final var given = new SubBeanWithUnquotedValues("some key", "some value"); final var given = new SubBeanWithUnquotedValues("some key", "some value", false);
final var result = given.toString(); final var result = given.toString();
assertThat(result).isEqualTo("SubBeanWithUnquotedValues(some key: some value)"); assertThat(result).isEqualTo("SubBeanWithUnquotedValues(some key, some value, inactive)");
} }
@Test @Test
void stringifyWithQuotedValueTrueQuotesEvenIntegers() { void stringifyWithQuotedValueTrueQuotesEvenIntegers() {
final var given = new SubBeanWithQuotedValues("some key", 1234); final var given = new SubBeanWithQuotedValues("some key", 1234, true);
final var result = given.toString(); final var result = given.toString();
assertThat(result).isEqualTo("SubBeanWithQuotedValues('some key': '1234')"); assertThat(result).isEqualTo("SubBeanWithQuotedValues('some key': '1234': 'active')");
} }
} }