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 static net.hostsharing.hsadminng.repr.Stringify.stringify;
import static net.hostsharing.hsadminng.repr.Symbol.symbol;
@Getter
@Setter
@@ -31,6 +32,10 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
private static Stringify<HsCredentialsContext> stringify = stringify(HsCredentialsContext.class, "loginContext")
.withProp(HsCredentialsContext::getType)
.withProp(HsCredentialsContext::getQualifier)
.withProp(HsCredentialsContext::isOnlyForNaturalPersons,
value -> value ? symbol("NP-ONLY") : null)
.withProp(HsCredentialsContext::isPublicAccess,
value -> value ? symbol("PUBLIC") : symbol("INTERNAL"))
.quotedValues(false)
.withSeparator(":");
@@ -53,6 +58,9 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
@Column(name = "only_for_natural_persons")
private boolean onlyForNaturalPersons;
@Column(name = "public_access")
private boolean publicAccess;
@Override
public String toShortString() {
return toString();
@@ -81,19 +81,15 @@ public class HsCredentialsController implements CredentialsApi {
@Override
@Transactional(readOnly = true)
@Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid")
public ResponseEntity<List<CredentialsResource>> getListOfCredentialsByPersonUuid(
public ResponseEntity<List<CredentialsResource>> getListOfCredentials(
final String assumedRoles,
final UUID personUuid
) {
context.assumeRoles(assumedRoles);
final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow(
() -> new EntityNotFoundException(
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
)
);
final var credentials = credentialsRepo.findByPerson(person);
final var credentials = personUuid == null
? credentialsRepo.findByCurrentSubject()
: findByPersonUuid(personUuid);
final var result = mapper.mapList(
credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(result);
@@ -183,6 +179,16 @@ public class HsCredentialsController implements CredentialsApi {
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) -> {
ofNullable(entity.getLastUsed()).ifPresent(
dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC)));
@@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.accounts;
import io.micrometer.core.annotation.Timed;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
@@ -14,7 +15,36 @@ public interface HsCredentialsRepository extends Repository<HsCredentialsEntity,
Optional<HsCredentialsEntity> findByUuid(final UUID uuid);
@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")
HsCredentialsEntity save(final HsCredentialsEntity entity);
@@ -1,5 +1,8 @@
package net.hostsharing.hsadminng.repr;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Value;
import net.hostsharing.hsadminng.errors.DisplayAs;
import jakarta.validation.constraints.NotNull;
@@ -11,14 +14,13 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.lang.Boolean.TRUE;
import static java.util.Optional.ofNullable;
public final class Stringify<B> {
private final String name;
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 Boolean quotedValues = null;
@@ -30,18 +32,6 @@ public final class Stringify<B> {
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) {
if (name != null) {
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;
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));
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));
return this;
}
private Stringify<B> withProps(final List<Property<B>> props) {
this.props.addAll(props);
public <V> Stringify<B> withProp(final Function<B, V> getter, final Function<V, ?> mapper) {
props.add(new Property<>(null, getter, mapper));
return this;
}
public String apply(@NotNull B object) {
final var propValues = props.stream()
.map(prop -> PropertyValue.of(prop, prop.getter.apply(object)))
.filter(Objects::nonNull)
.filter(PropertyValue::nonEmpty)
.map(prop -> new PropertyValue<>(object, prop))
.filter(PropertyValue::notNullAndNotEmpty)
.map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
.collect(Collectors.joining(separator));
return idProp != null
@@ -92,24 +81,36 @@ public final class Stringify<B> {
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("");
}
private String optionallyQuoted(final PropertyValue<B> propVal) {
private <B, V> String optionallyQuoted(final PropertyValue<B, V> propVal) {
if (quotedValues == null)
return quotedQuotedValueType(propVal)
? ("'" + propVal.value + "'")
: propVal.value;
return TRUE == quotedValues
? ("'" + propVal.value + "'")
: propVal.value;
return quotableValueType(propVal.getValue())
? ("'" + propVal.stringValue + "'")
: propVal.stringValue;
return quotedValues
? ("'" + propVal.stringValue + "'")
: propVal.stringValue;
}
private static <B> boolean quotedQuotedValueType(final PropertyValue<B> propVal) {
return !(propVal.rawValue instanceof Number || propVal.rawValue instanceof Boolean);
private <V> boolean quotableValueType(final V rawValue) {
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) {
this.quotedValues = quotedValues;
return this;
@@ -120,23 +121,48 @@ public final class Stringify<B> {
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) {
static <B> PropertyValue<B> of(Property<B> prop, Object rawValue) {
return rawValue != null ? new PropertyValue<>(prop, rawValue, toStringOrShortString(rawValue)) : null;
Property(String name, Function<B, V> getter) {
this(name, getter, v -> v);
}
private static String toStringOrShortString(final Object rawValue) {
return rawValue instanceof Stringifyable stringifyable ? stringifyable.toShortString() : rawValue.toString();
Object getValue(final B object) {
return ofNullable(getter.apply(object))
.map(mapper)
.orElse(null);
}
}
boolean nonEmpty() {
return rawValue != null &&
(!(rawValue instanceof Collection<?> c) || !c.isEmpty()) &&
(!(rawValue instanceof Map<?,?> m) || !m.isEmpty()) &&
(!(rawValue instanceof String s) || !s.isEmpty());
@Getter
private class PropertyValue<B, V> {
private Property<B, V> prop;
private V value;
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
onlyForNaturalPersons:
type: boolean
publicAccess:
type: boolean
required:
- uuid
- type
- 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.
tags:
- credentials
operationId: getListOfCredentialsByPersonUuid
operationId: getListOfCredentials
parameters:
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: personUuid
in: query
required: true
required: false
schema:
type: string
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:
"200":
description: OK
@@ -40,6 +40,8 @@ create table hs_accounts.context
only_for_natural_persons boolean default false,
public_access boolean default false,
unique (type, qualifier)
);
--//
@@ -15,7 +15,9 @@ declare
context_HSADMIN_prod hs_accounts.context;
context_SSH_internal hs_accounts.context;
context_SSH_external hs_accounts.context;
context_MATRIX_internal hs_accounts.context;
context_MATRIX_external hs_accounts.context;
begin
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');
-- Add test contexts
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES
('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true)
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true, true)
RETURNING * INTO context_HSADMIN_prod;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES
('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true)
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true, false)
RETURNING * INTO context_SSH_internal;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES
('33333333-3333-3333-3333-333333333333', 'MATRIX', 'internal', true)
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('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;
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES
('44444444-4444-4444-4444-444444444444', 'MASTODON', 'external', false);
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
('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
-- TODO_impl: RBAC rules for _rv do not yet work properly
@@ -59,12 +69,12 @@ begin
-- Map credentials to contexts
INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES
(superuserAlexSubjectUuid, '11111111-1111-1111-1111-111111111111'), -- HSADMIN context
(superuserFranSubjectUuid, '11111111-1111-1111-1111-111111111111'), -- HSADMIN context
(superuserAlexSubjectUuid, '22222222-2222-2222-2222-222222222222'), -- SSH context
(superuserFranSubjectUuid, '22222222-2222-2222-2222-222222222222'), -- SSH context
(superuserAlexSubjectUuid, '33333333-3333-3333-3333-333333333333'), -- MATRIX context
(superuserFranSubjectUuid, '33333333-3333-3333-3333-333333333333'); -- MATRIX context
(superuserAlexSubjectUuid, context_HSADMIN_prod.uuid),
(superuserFranSubjectUuid, context_HSADMIN_prod.uuid),
(superuserAlexSubjectUuid, context_SSH_internal.uuid),
(superuserFranSubjectUuid, context_SSH_internal.uuid),
(superuserAlexSubjectUuid, context_MATRIX_internal.uuid),
(superuserFranSubjectUuid, context_MATRIX_internal.uuid);
end; $$;
--//
@@ -14,7 +14,8 @@ class HsCredentialsContextRbacEntityUnitTest {
.uuid(UUID.randomUUID())
.type("SSH")
.qualifier("prod")
.publicAccess(true)
.build();
assertEquals("loginContext(SSH:prod)", entity.toShortString());
assertEquals("loginContext(SSH:prod:PUBLIC)", entity.toShortString());
}
}
@@ -77,7 +77,7 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
// then
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
@@ -89,7 +89,7 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
// then
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
@@ -14,7 +14,8 @@ class HsCredentialsContextRealEntityUnitTest {
.uuid(UUID.randomUUID())
.type("testType")
.qualifier("testQualifier")
.onlyForNaturalPersons(true)
.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();
// 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
historicalContext(Timestamp.from(ZonedDateTime.now().toInstant()));
@@ -60,7 +62,9 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
final var rowsAfter = query.getResultList();
// 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
@@ -87,7 +91,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
// then
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
@@ -99,7 +103,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
// then
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
@@ -125,7 +125,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
assertThat(foundEntity.getGlobalUid()).isEqualTo(2001);
assertThat(foundEntity.getLoginContexts()).hasSize(1)
.map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod)");
.map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)");
}
@Test
@@ -117,7 +117,7 @@ class HsOfficePersonEntityUnitTest {
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
@@ -28,7 +28,7 @@ class HsOfficeRelationUnitTest {
.holder(holder)
.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
@@ -39,7 +39,7 @@ class HsOfficeRelationUnitTest {
.holder(holder)
.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
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
import java.util.UUID;
import static net.hostsharing.hsadminng.repr.Symbol.symbol;
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
import static org.assertj.core.api.Assertions.assertThat;
@@ -58,11 +59,12 @@ class StringifyUnitTest {
private static Stringify<SubBeanWithUnquotedValues> toString = stringify(SubBeanWithUnquotedValues.class)
.withProp(SubBeanWithUnquotedValues::getKey)
.withProp(SubBeanWithUnquotedValues::getValue)
.withSeparator(": ")
.withProp(SubBeanWithUnquotedValues::isActive, v -> v ? symbol("active") : symbol("inactive"))
.quotedValues(false);
private String key;
private String value;
private boolean active;
@Override
public String toString() {
@@ -84,11 +86,13 @@ class StringifyUnitTest {
private static Stringify<SubBeanWithQuotedValues> toString = stringify(SubBeanWithQuotedValues.class)
.withProp(SubBeanWithQuotedValues::getKey)
.withProp(SubBeanWithQuotedValues::getValue)
.withProp(SubBeanWithQuotedValues::isActive, v -> v ? "active" : "inactive")
.withSeparator(": ")
.quotedValues(true);
private String key;
private Integer value;
private boolean active;
@Override
public String toString() {
@@ -104,8 +108,8 @@ class StringifyUnitTest {
@Test
void stringifyWhenAllPropsHaveValues() {
final var given = new TestBean(UUID.randomUUID(), "some caption",
new SubBeanWithUnquotedValues("some key", "some value"),
new SubBeanWithQuotedValues("some key", 1234),
new SubBeanWithUnquotedValues("some key", "some value", true),
new SubBeanWithQuotedValues("some key", 1234, false),
42,
false);
final var result = given.toString();
@@ -122,15 +126,15 @@ class StringifyUnitTest {
@Test
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();
assertThat(result).isEqualTo("SubBeanWithUnquotedValues(some key: some value)");
assertThat(result).isEqualTo("SubBeanWithUnquotedValues(some key, some value, inactive)");
}
@Test
void stringifyWithQuotedValueTrueQuotesEvenIntegers() {
final var given = new SubBeanWithQuotedValues("some key", 1234);
final var given = new SubBeanWithQuotedValues("some key", 1234, true);
final var result = given.toString();
assertThat(result).isEqualTo("SubBeanWithQuotedValues('some key': '1234')");
assertThat(result).isEqualTo("SubBeanWithQuotedValues('some key': '1234': 'active')");
}
}