diff --git a/build.gradle.kts b/build.gradle.kts index a6bcda6..8c4400a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ import internal.getBooleanProperty plugins { id("internal.errorprone-convention") + id("internal.jacoco-convention") id("internal.java-library-convention") id("internal.mrjar-module-info-convention") id("internal.publishing-convention") diff --git a/buildSrc/src/main/kotlin/internal.jacoco-convention.gradle.kts b/buildSrc/src/main/kotlin/internal.jacoco-convention.gradle.kts new file mode 100644 index 0000000..7366c1d --- /dev/null +++ b/buildSrc/src/main/kotlin/internal.jacoco-convention.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("jacoco") +} + +tasks.named("jacocoTestReport") { + dependsOn(tasks.named("test")) + + reports { + xml.required = true + html.required = true + csv.required = false + } +} + +tasks.named("check") { + finalizedBy(tasks.named("jacocoTestReport")) +} diff --git a/src/test/java/io/github/problem4j/core/ProblemBuilderTest.java b/src/test/java/io/github/problem4j/core/AbstractProblemBuilderTest.java similarity index 72% rename from src/test/java/io/github/problem4j/core/ProblemBuilderTest.java rename to src/test/java/io/github/problem4j/core/AbstractProblemBuilderTest.java index 7124678..7dffeb6 100644 --- a/src/test/java/io/github/problem4j/core/ProblemBuilderTest.java +++ b/src/test/java/io/github/problem4j/core/AbstractProblemBuilderTest.java @@ -21,6 +21,7 @@ package io.github.problem4j.core; +import static io.github.problem4j.core.Problem.extension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -29,20 +30,23 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; /** * Some of the tests in this class may appear trivial or unnecessary. They are intentionally - * included to explore and validate the behavior of various code coverage analysis tools. These - * tests help ensure that the coverage reports correctly reflect different execution paths, edge - * cases, and instrumentation scenarios. + * included to explore and validate the behavior of various code coverage analysis tools. */ -class ProblemBuilderTest { +class AbstractProblemBuilderTest { + + private AbstractProblemBuilder newInstance() { + return new AbstractProblemBuilder() {}; + } @Test void givenNullURIType_shouldNotSetIt() { - Problem problem = Problem.builder().type((URI) null).build(); + Problem problem = newInstance().type((URI) null).build(); assertThat(problem.getType()).isEqualTo(Problem.BLANK_TYPE); assertThat(problem.isTypeNonBlank()).isFalse(); @@ -50,7 +54,7 @@ void givenNullURIType_shouldNotSetIt() { @Test void givenNullStringType_shouldNotSetIt() { - Problem problem = Problem.builder().type((String) null).build(); + Problem problem = newInstance().type((String) null).build(); assertThat(problem.getType()).isEqualTo(Problem.BLANK_TYPE); assertThat(problem.isTypeNonBlank()).isFalse(); @@ -58,7 +62,7 @@ void givenNullStringType_shouldNotSetIt() { @Test void givenBlankURIType_shouldNotSetIt() { - Problem problem = Problem.builder().type(Problem.BLANK_TYPE).build(); + Problem problem = newInstance().type(Problem.BLANK_TYPE).build(); assertThat(problem.getType()).isEqualTo(Problem.BLANK_TYPE); assertThat(problem.isTypeNonBlank()).isFalse(); @@ -66,7 +70,7 @@ void givenBlankURIType_shouldNotSetIt() { @Test void givenBlankStringType_shouldNotSetIt() { - Problem problem = Problem.builder().type(Problem.BLANK_TYPE.toString()).build(); + Problem problem = newInstance().type(Problem.BLANK_TYPE.toString()).build(); assertThat(problem.getType()).isEqualTo(Problem.BLANK_TYPE); assertThat(problem.isTypeNonBlank()).isFalse(); @@ -74,7 +78,7 @@ void givenBlankStringType_shouldNotSetIt() { @Test void givenNullProblemStatus_shouldNotSetTitleOrStatus() { - Problem problem = Problem.builder().status(null).build(); + Problem problem = newInstance().status(null).build(); assertThat(problem.getStatus()).isZero(); assertThat(problem.getTitle()).isEqualTo(Problem.UNKNOWN_TITLE); @@ -82,7 +86,7 @@ void givenNullProblemStatus_shouldNotSetTitleOrStatus() { @Test void givenProblemStatus_shouldSetNumericStatusAndTitle() { - Problem problem = Problem.builder().status(ProblemStatus.BAD_REQUEST).build(); + Problem problem = newInstance().status(ProblemStatus.BAD_REQUEST).build(); assertThat(problem.getStatus()).isEqualTo(ProblemStatus.BAD_REQUEST.getStatus()); assertThat(problem.getTitle()).isEqualTo(ProblemStatus.BAD_REQUEST.getTitle()); @@ -90,7 +94,7 @@ void givenProblemStatus_shouldSetNumericStatusAndTitle() { @Test void givenProblemStatus_shouldPreferExplicitStatusValueWhenSetEarlier() { - Problem problem = Problem.builder().status(405).status(ProblemStatus.I_AM_A_TEAPOT).build(); + Problem problem = newInstance().status(405).status(ProblemStatus.I_AM_A_TEAPOT).build(); assertThat(problem.getStatus()).isEqualTo(ProblemStatus.I_AM_A_TEAPOT.getStatus()); assertThat(problem.getTitle()).isEqualTo(ProblemStatus.I_AM_A_TEAPOT.getTitle()); @@ -98,8 +102,7 @@ void givenProblemStatus_shouldPreferExplicitStatusValueWhenSetEarlier() { @Test void givenExplicitTitle_thenStatusProblemStatus_shouldNotOverrideTitle() { - Problem problem = - Problem.builder().title("Custom Title").status(ProblemStatus.BAD_REQUEST).build(); + Problem problem = newInstance().title("Custom Title").status(ProblemStatus.BAD_REQUEST).build(); assertThat(problem.getStatus()).isEqualTo(ProblemStatus.BAD_REQUEST.getStatus()); assertThat(problem.getTitle()).isEqualTo("Custom Title"); @@ -107,7 +110,7 @@ void givenExplicitTitle_thenStatusProblemStatus_shouldNotOverrideTitle() { @Test void givenStatusProblemStatus_thenExplicitTitle_shouldOverrideDerivedTitle() { - Problem problem = Problem.builder().status(ProblemStatus.NOT_FOUND).title("My Title").build(); + Problem problem = newInstance().status(ProblemStatus.NOT_FOUND).title("My Title").build(); assertThat(problem.getStatus()).isEqualTo(ProblemStatus.NOT_FOUND.getStatus()); assertThat(problem.getTitle()).isEqualTo("My Title"); @@ -115,40 +118,40 @@ void givenStatusProblemStatus_thenExplicitTitle_shouldOverrideDerivedTitle() { @Test void givenInvalidTypeString_shouldThrowIllegalArgumentException() { - assertThatThrownBy(() -> Problem.builder().type("ht tp://not a uri")) + assertThatThrownBy(() -> newInstance().type("ht tp://not a uri")) .isInstanceOf(IllegalArgumentException.class); } @Test void givenInvalidInstanceString_shouldThrowIllegalArgumentException() { - assertThatThrownBy(() -> Problem.builder().instance("::://invalid")) + assertThatThrownBy(() -> newInstance().instance("::://invalid")) .isInstanceOf(IllegalArgumentException.class); } @Test void givenNullURIInstance_shouldNotSetIt() { - Problem problem = Problem.builder().instance((URI) null).build(); + Problem problem = newInstance().instance((URI) null).build(); assertThat(problem.getInstance()).isNull(); } @Test void givenNullStringInstance_shouldNotSetIt() { - Problem problem = Problem.builder().instance((String) null).build(); + Problem problem = newInstance().instance((String) null).build(); assertThat(problem.getInstance()).isNull(); } @Test void givenNullNameExtension_shouldIgnoreIt() { - Problem problem = Problem.builder().extension(null, "value").build(); + Problem problem = newInstance().extension(null, "value").build(); assertThat(problem.getExtensions()).isEmpty(); } @Test void givenNullValueExtension_shouldNotIncludeIt() { - Problem problem = Problem.builder().extension("key", null).build(); + Problem problem = newInstance().extension("key", null).build(); assertThat(problem.getExtensions()).isEmpty(); assertThat(problem.hasExtension("key")).isFalse(); @@ -159,9 +162,7 @@ void givenNullValueExtension_shouldNotIncludeIt() { @Test void givenNullValueExtensionViaVarargs_shouldNotIncludeIt() { Problem problem = - Problem.builder() - .extensions(Problem.extension("key1", null), Problem.extension("key2", null)) - .build(); + newInstance().extensions(extension("key1", null), extension("key2", null)).build(); assertThat(problem.getExtensions()).isEmpty(); assertThat(problem.getExtensionMembers()).isEqualTo(Map.of()); @@ -176,9 +177,8 @@ void givenNullValueExtensionViaVarargs_shouldNotIncludeIt() { @Test void givenNullValueExtensionViaObject_shouldNotIncludeIt() { Problem problem = - Problem.builder() - .extensions( - Arrays.asList(Problem.extension("key1", null), Problem.extension("key2", null))) + newInstance() + .extensions(Arrays.asList(extension("key1", null), extension("key2", null))) .build(); assertThat(problem.getExtensions()).isEmpty(); @@ -193,7 +193,7 @@ void givenNullValueExtensionViaObject_shouldNotIncludeIt() { @Test void givenNullMapExtension_shouldIgnoreIt() { - Problem problem = Problem.builder().extensions((Map) null).build(); + Problem problem = newInstance().extensions((Map) null).build(); assertThat(problem.getExtensions()).isEmpty(); } @@ -204,7 +204,7 @@ void givenMapExtensionWithNullValue_shouldIgnoreNullValue() { map.put("ignored", null); map.put("a", "b"); - Problem problem = Problem.builder().extensions(map).build(); + Problem problem = newInstance().extensions(map).build(); assertThat(problem.getExtensions()).containsExactly("a"); assertThat(problem.hasExtension("a")).isTrue(); @@ -214,17 +214,14 @@ void givenMapExtensionWithNullValue_shouldIgnoreNullValue() { @Test void givenNullVarargArray_shouldIgnoreIt() { - Problem problem = Problem.builder().extensions((Problem.Extension[]) null).build(); + Problem problem = newInstance().extensions((Problem.Extension[]) null).build(); assertThat(problem.getExtensions()).isEmpty(); } @Test void givenVarargWithNullElement_shouldIgnoreNullElement() { - Problem problem = - Problem.builder() - .extensions(Problem.extension("a", 1), null, Problem.extension("b", 2)) - .build(); + Problem problem = newInstance().extensions(extension("a", 1), null, extension("b", 2)).build(); assertThat(problem.getExtensions()).containsExactlyInAnyOrder("a", "b"); assertThat(problem.hasExtension("a")).isTrue(); @@ -234,7 +231,7 @@ void givenVarargWithNullElement_shouldIgnoreNullElement() { @Test void givenNullCollection_shouldIgnoreIt() { - Problem problem = Problem.builder().extensions((Collection) null).build(); + Problem problem = newInstance().extensions((Collection) null).build(); assertThat(problem.getExtensions()).isEmpty(); } @@ -242,9 +239,8 @@ void givenNullCollection_shouldIgnoreIt() { @Test void givenCollectionWithNullElement_shouldIgnoreNullElement() { Problem problem = - Problem.builder() - .extensions( - Arrays.asList(Problem.extension("x", "1"), null, Problem.extension("y", "2"))) + newInstance() + .extensions(Arrays.asList(extension("x", "1"), null, extension("y", "2"))) .build(); assertThat(problem.getExtensions()).containsExactlyInAnyOrder("x", "y"); @@ -255,7 +251,7 @@ void givenCollectionWithNullElement_shouldIgnoreNullElement() { @Test void givenNumericStatus_shouldDeriveTitleWhenKnown() { - Problem problem = Problem.builder().status(ProblemStatus.MULTI_STATUS.getStatus()).build(); + Problem problem = newInstance().status(ProblemStatus.MULTI_STATUS.getStatus()).build(); assertThat(problem.getStatus()).isEqualTo(ProblemStatus.MULTI_STATUS.getStatus()); assertThat(problem.getTitle()).isEqualTo(ProblemStatus.MULTI_STATUS.getTitle()); @@ -263,7 +259,7 @@ void givenNumericStatus_shouldDeriveTitleWhenKnown() { @Test void givenUnknownNumericStatus_shouldNotDeriveTitle() { - Problem problem = Problem.builder().status(999).build(); + Problem problem = newInstance().status(999).build(); assertThat(problem.getStatus()).isEqualTo(999); assertThat(problem.getTitle()).isEqualTo(Problem.UNKNOWN_TITLE); @@ -274,7 +270,7 @@ void givenTypeAndInstance_stringOverloads_shouldAcceptValidUris() { String t = "http://example.org/type"; String i = "http://example.org/instance"; - Problem problem = Problem.builder().type(t).instance(i).build(); + Problem problem = newInstance().type(t).instance(i).build(); assertThat(problem.getType()).isEqualTo(URI.create(t)); assertThat(problem.getInstance()).isEqualTo(URI.create(i)); @@ -283,7 +279,7 @@ void givenTypeAndInstance_stringOverloads_shouldAcceptValidUris() { @Test void givenEmptyMapExtension_shouldBeIgnored() { Map m = new HashMap<>(); - Problem problem = Problem.builder().extensions(m).build(); + Problem problem = newInstance().extensions(m).build(); assertThat(problem.getExtensions()).isEmpty(); assertThat(problem.getExtensionMembers()).isEqualTo(Collections.emptyMap()); @@ -291,8 +287,7 @@ void givenEmptyMapExtension_shouldBeIgnored() { @Test void givenAssigningTheSameExtensionLater_shouldOverwriteEarlierValues() { - Problem problem = - Problem.builder().extension("k", "v1").extension(Problem.extension("k", "v2")).build(); + Problem problem = newInstance().extension("k", "v1").extension(extension("k", "v2")).build(); assertThat(problem.getExtensionValue("k")).isEqualTo("v2"); assertThat(problem.getExtensions()).containsExactly("k"); @@ -301,7 +296,7 @@ void givenAssigningTheSameExtensionLater_shouldOverwriteEarlierValues() { @Test void givenExtensionSetThenUnsetWithNull_shouldRemoveExtension() { - Problem problem = Problem.builder().extension("name", "Mark").extension("name", null).build(); + Problem problem = newInstance().extension("name", "Mark").extension("name", null).build(); assertThat(problem.getExtensions()).isEmpty(); assertThat(problem.hasExtension("name")).isFalse(); @@ -314,7 +309,7 @@ void givenExtensionSetThenUnsetViaMap_shouldRemoveExtension() { Map removals = new HashMap<>(); removals.put("name", null); - Problem problem = Problem.builder().extension("name", "Mark").extensions(removals).build(); + Problem problem = newInstance().extension("name", "Mark").extensions(removals).build(); assertThat(problem.getExtensions()).isEmpty(); assertThat(problem.hasExtension("name")).isFalse(); @@ -323,10 +318,7 @@ void givenExtensionSetThenUnsetViaMap_shouldRemoveExtension() { @Test void givenExtensionSetThenUnsetViaVarargs_shouldRemoveExtension() { Problem problem = - Problem.builder() - .extension("name", "Mark") - .extensions(Problem.extension("name", null)) - .build(); + newInstance().extension("name", "Mark").extensions(Problem.extension("name", null)).build(); assertThat(problem.getExtensions()).isEmpty(); assertThat(problem.hasExtension("name")).isFalse(); @@ -335,7 +327,7 @@ void givenExtensionSetThenUnsetViaVarargs_shouldRemoveExtension() { @Test void givenExtensionSetThenUnsetViaCollection_shouldRemoveExtension() { Problem problem = - Problem.builder() + newInstance() .extension("name", "Mark") .extensions(Collections.singletonList(Problem.extension("name", null))) .build(); @@ -358,7 +350,7 @@ void givenAllFieldsPopulated_whenToString_thenContainsAllFields() { extensions.put("objectExt", new DummyObject("boo\nfoo")); ProblemBuilder builder = - Problem.builder().type(type).title(title).status(status).detail(detail).instance(instance); + newInstance().type(type).title(title).status(status).detail(detail).instance(instance); extensions.forEach(builder::extension); String result = builder.toString(); @@ -376,16 +368,14 @@ void givenAllFieldsPopulated_whenToString_thenContainsAllFields() { @Test void givenNullExtensionsAndNullableFields_whenToString_thenOmitsNulls() { - ProblemBuilder builder = Problem.builder().status(200); + ProblemBuilder builder = newInstance().status(200); String result = builder.toString(); assertThat(result).isEqualTo("ProblemBuilder{status=200}"); } @Test void givenOnlyNumberExtensions_whenToString_thenContainsNumbers() { - ProblemBuilder builder = Problem.builder(); - builder.extension("ext1", 123); - builder.extension("ext2", 456.78); + ProblemBuilder builder = newInstance().extension("ext1", 123).extension("ext2", 456.78); String result = builder.toString(); assertThat(result).contains("ext1=123"); assertThat(result).contains("ext2=456.78"); @@ -393,9 +383,8 @@ void givenOnlyNumberExtensions_whenToString_thenContainsNumbers() { @Test void givenOnlyBooleanExtensions_whenToString_thenContainsBooleans() { - ProblemBuilder builder = Problem.builder(); - builder.extension("flag1", true); - builder.extension("flag2", false); + ProblemBuilder builder = newInstance().extension("flag1", true).extension("flag2", false); + String result = builder.toString(); assertThat(result).contains("flag1=true"); assertThat(result).contains("flag2=false"); @@ -403,9 +392,44 @@ void givenOnlyBooleanExtensions_whenToString_thenContainsBooleans() { @Test void givenNonPrimitiveExtension_whenToString_thenUsesItsToString() { - ProblemBuilder builder = Problem.builder(); + ProblemBuilder builder = newInstance(); builder.extension("obj", new DummyObject("biz\tbar")); String result = builder.toString(); assertThat(result).contains("obj=DummyObject{value=biz\tbar}"); } + + @SuppressWarnings("deprecation") + @Test + void givenDeprecatedExtensionMap_whenBuilding_thenExtensionsAreSet() { + Map map = Map.of("a", "1", "b", "2"); + + Problem problem = newInstance().extension(map).build(); + + assertThat(problem.getExtensionMembers()).isEqualTo(map); + } + + @SuppressWarnings("deprecation") + @Test + void givenDeprecatedExtensionVarargs_whenBuilding_thenExtensionsAreSet() { + Problem problem = newInstance().extension(extension("a", "1"), extension("b", "2")).build(); + + assertThat(problem.getExtensions()).containsExactlyInAnyOrder("a", "b"); + } + + @SuppressWarnings("deprecation") + @Test + void givenDeprecatedExtensionCollection_whenBuilding_thenExtensionsAreSet() { + List exts = Arrays.asList(extension("x", "1"), extension("y", "2")); + + Problem problem = newInstance().extension(exts).build(); + + assertThat(problem.getExtensions()).containsExactlyInAnyOrder("x", "y"); + } + + @Test + void givenNullExtensionObject_whenBuilding_thenIgnored() { + Problem problem = newInstance().extension((Problem.Extension) null).status(200).build(); + + assertThat(problem.getExtensions()).isEmpty(); + } } diff --git a/src/test/java/io/github/problem4j/core/AbstractProblemTest.java b/src/test/java/io/github/problem4j/core/AbstractProblemTest.java new file mode 100644 index 0000000..e78ba0f --- /dev/null +++ b/src/test/java/io/github/problem4j/core/AbstractProblemTest.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2025-2026 The Problem4J Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.github.problem4j.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Some of the tests in this class may appear trivial or unnecessary. They are intentionally + * included to explore and validate the behavior of various code coverage analysis tools. + */ +class AbstractProblemTest { + + @Test + void givenAllFieldsPopulated_whenToString_thenContainsAllFields() { + URI type = URI.create("https://example.com/problem"); + String title = "Test Problem"; + int status = 400; + String detail = "Something went wrong"; + URI instance = URI.create("https://example.com/instance"); + Map extensions = new HashMap<>(); + extensions.put("stringExt", "value"); + extensions.put("numberExt", 42); + extensions.put("booleanExt", true); + extensions.put("objectExt", new DummyObject("boo\nfoo")); + + Problem problem = new AbstractProblem(type, title, status, detail, instance, extensions) {}; + + String result = problem.toString(); + + assertThat(result).contains("type=" + type); + assertThat(result).contains("title=" + title); + assertThat(result).contains("status=" + status); + assertThat(result).contains("detail=" + detail); + assertThat(result).contains("instance=" + instance); + assertThat(result).contains("stringExt=value"); + assertThat(result).contains("numberExt=42"); + assertThat(result).contains("booleanExt=true"); + assertThat(result).contains("objectExt=DummyObject{value=boo\nfoo}"); + } + + @Test + void givenNullExtensionsAndNullableFields_whenToString_thenOmitsNulls() { + Map extensions = new HashMap<>(); + Problem problem = + new AbstractProblem( + Problem.BLANK_TYPE, ProblemStatus.OK_TITLE, 200, null, null, extensions) {}; + + String result = problem.toString(); + + assertThat(result).isEqualTo("Problem{title=OK, status=200}"); + } + + @Test + void givenStatusOnly_whenCreatingProblem_thenTitleIsDerivedFromStatus() { + Problem problem = new AbstractProblem(404) {}; + + assertThat(problem.getStatus()).isEqualTo(404); + assertThat(problem.getTitle()).isEqualTo("Not Found"); + assertThat(problem.getDetail()).isNull(); + assertThat(problem.getType()).isEqualTo(Problem.BLANK_TYPE); + } + + @Test + void givenStatusAndDetail_whenCreatingProblem_thenTitleDerivedAndDetailSet() { + Problem problem = new AbstractProblem(500, "server exploded") {}; + + assertThat(problem.getStatus()).isEqualTo(500); + assertThat(problem.getTitle()).isEqualTo("Internal Server Error"); + assertThat(problem.getDetail()).isEqualTo("server exploded"); + } + + @Test + void givenTitleAndStatus_whenCreatingProblem_thenFieldsAreSet() { + Problem problem = new AbstractProblem("Custom Title", 400) {}; + + assertThat(problem.getTitle()).isEqualTo("Custom Title"); + assertThat(problem.getStatus()).isEqualTo(400); + assertThat(problem.getDetail()).isNull(); + } + + @Test + void givenTitleStatusAndDetail_whenCreatingProblem_thenFieldsAreSet() { + Problem problem = new AbstractProblem("Custom Title", 422, "invalid input") {}; + + assertThat(problem.getTitle()).isEqualTo("Custom Title"); + assertThat(problem.getStatus()).isEqualTo(422); + assertThat(problem.getDetail()).isEqualTo("invalid input"); + assertThat(problem.getInstance()).isNull(); + } + + @Test + void givenTypeTitleAndStatus_whenCreatingProblem_thenFieldsAreSet() { + URI type = URI.create("urn:test"); + Problem problem = new AbstractProblem(type, "Typed", 409) {}; + + assertThat(problem.getType()).isEqualTo(type); + assertThat(problem.getTitle()).isEqualTo("Typed"); + assertThat(problem.getStatus()).isEqualTo(409); + assertThat(problem.getDetail()).isNull(); + } + + @Test + void givenTypeTitleStatusAndDetail_whenCreatingProblem_thenFieldsAreSet() { + URI type = URI.create("urn:test"); + Problem problem = new AbstractProblem(type, "Typed", 409, "conflict detail") {}; + + assertThat(problem.getType()).isEqualTo(type); + assertThat(problem.getTitle()).isEqualTo("Typed"); + assertThat(problem.getStatus()).isEqualTo(409); + assertThat(problem.getDetail()).isEqualTo("conflict detail"); + } + + @Test + void givenNullExtensionsMap_whenCreatingProblem_thenExtensionsAreEmpty() { + Problem problem = new AbstractProblem(Problem.BLANK_TYPE, "T", 200, null, null, null) {}; + + assertThat(problem.getExtensionMembers()).isEmpty(); + } + + @Test + void givenUnknownStatus_whenFindTitle_thenReturnsUnknownTitle() { + Problem problem = new AbstractProblem(999) {}; + + assertThat(problem.getTitle()).isEqualTo(Problem.UNKNOWN_TITLE); + } + + @Test + void givenExtension_whenSetValue_thenValueIsUpdated() { + Problem.Extension ext = new AbstractProblem.AbstractExtension("key", "original") {}; + + Object result = ext.setValue("updated"); + + assertThat(result).isEqualTo("updated"); + assertThat(ext.getValue()).isEqualTo("updated"); + } + + @Test + void givenExtension_whenToString_thenFormattedCorrectly() { + Problem.Extension ext = new AbstractProblem.AbstractExtension("myKey", 42) {}; + + assertThat(ext.toString()).isEqualTo("Extension{myKey=42}"); + } + + @Test + void givenExtensionWithNullValue_whenToString_thenFormattedCorrectly() { + Problem.Extension ext = new AbstractProblem.AbstractExtension("myKey", null) {}; + + assertThat(ext.toString()).isEqualTo("Extension{myKey=null}"); + } + + @Test + void givenTwoProblemsWithDifferentTypes_whenEquals_thenNotEqual() { + Problem p1 = new AbstractProblem(URI.create("urn:type1"), "T", 400) {}; + Problem p2 = new AbstractProblem(URI.create("urn:type2"), "T", 400) {}; + + assertThat(p1).isNotEqualTo(p2); + } + + @Test + void givenTwoProblemsWithDifferentTitles_whenEquals_thenNotEqual() { + Problem p1 = new AbstractProblem("T1", 400) {}; + Problem p2 = new AbstractProblem("T2", 400) {}; + + assertThat(p1).isNotEqualTo(p2); + } + + @Test + void givenTwoProblemsWithDifferentStatuses_whenEquals_thenNotEqual() { + Problem p1 = new AbstractProblem("T", 400) {}; + Problem p2 = new AbstractProblem("T", 500) {}; + + assertThat(p1).isNotEqualTo(p2); + } + + @Test + void givenTwoProblemsWithDifferentDetails_whenEquals_thenNotEqual() { + Problem p1 = new AbstractProblem("T", 400, "d1") {}; + Problem p2 = new AbstractProblem("T", 400, "d2") {}; + + assertThat(p1).isNotEqualTo(p2); + } + + @Test + void givenTwoProblemsWithDifferentInstances_whenEquals_thenNotEqual() { + Problem p1 = + new AbstractProblem(Problem.BLANK_TYPE, "T", 400, null, URI.create("urn:i1"), null) {}; + Problem p2 = + new AbstractProblem(Problem.BLANK_TYPE, "T", 400, null, URI.create("urn:i2"), null) {}; + + assertThat(p1).isNotEqualTo(p2); + } + + @Test + void givenTwoProblemsWithDifferentExtensions_whenEquals_thenNotEqual() { + Problem p1 = + new AbstractProblem(Problem.BLANK_TYPE, "T", 400, null, null, Map.of("k", "v1")) {}; + Problem p2 = + new AbstractProblem(Problem.BLANK_TYPE, "T", 400, null, null, Map.of("k", "v2")) {}; + + assertThat(p1).isNotEqualTo(p2); + } + + @Test + void givenTwoExtensionsWithSameKeyDifferentValue_whenEquals_thenNotEqual() { + Problem.Extension e1 = new AbstractProblem.AbstractExtension("k", "v1") {}; + Problem.Extension e2 = new AbstractProblem.AbstractExtension("k", "v2") {}; + + assertThat(e1).isNotEqualTo(e2); + } +} diff --git a/src/test/java/io/github/problem4j/core/ProblemContextImplTest.java b/src/test/java/io/github/problem4j/core/ProblemContextImplTest.java index 5142ccb..0e5f41f 100644 --- a/src/test/java/io/github/problem4j/core/ProblemContextImplTest.java +++ b/src/test/java/io/github/problem4j/core/ProblemContextImplTest.java @@ -30,9 +30,7 @@ /** * Some of the tests in this class may appear trivial or unnecessary. They are intentionally - * included to explore and validate the behavior of various code coverage analysis tools. These - * tests help ensure that the coverage reports correctly reflect different execution paths, edge - * cases, and instrumentation scenarios. + * included to explore and validate the behavior of various code coverage analysis tools. */ class ProblemContextImplTest { diff --git a/src/test/java/io/github/problem4j/core/ProblemExceptionTest.java b/src/test/java/io/github/problem4j/core/ProblemExceptionTest.java index 652ef79..28966b3 100644 --- a/src/test/java/io/github/problem4j/core/ProblemExceptionTest.java +++ b/src/test/java/io/github/problem4j/core/ProblemExceptionTest.java @@ -29,9 +29,7 @@ /** * Some of the tests in this class may appear trivial or unnecessary. They are intentionally - * included to explore and validate the behavior of various code coverage analysis tools. These - * tests help ensure that the coverage reports correctly reflect different execution paths, edge - * cases, and instrumentation scenarios. + * included to explore and validate the behavior of various code coverage analysis tools. */ class ProblemExceptionTest { @@ -221,4 +219,78 @@ void givenCtorWithParameters_whenCreatingProblemException_problemIsNotRecreated( assertSame(problem, exception.getProblem()); } + + @Test + void givenCtorWithNullMessage_whenCreatingProblemException_thenMessageIsDerivedFromProblem() { + Problem problem = Problem.builder().title("Bad Request").status(400).build(); + + ProblemException exception = new ProblemException(null, problem); + + assertEquals("Bad Request (code: 400)", exception.getMessage()); + assertSame(problem, exception.getProblem()); + } + + @Test + void givenCtorWithEmptyMessage_whenCreatingProblemException_thenMessageIsDerivedFromProblem() { + Problem problem = Problem.builder().title("Bad Request").status(400).build(); + + ProblemException exception = new ProblemException("", problem); + + assertEquals("Bad Request (code: 400)", exception.getMessage()); + assertSame(problem, exception.getProblem()); + } + + @Test + void + givenCtorWithNullMessageAndCause_whenCreatingProblemException_thenMessageIsDerivedFromProblem() { + Problem problem = Problem.builder().title("Bad Request").status(400).build(); + Throwable cause = new RuntimeException("root"); + + ProblemException exception = new ProblemException(null, problem, cause); + + assertEquals("Bad Request (code: 400)", exception.getMessage()); + assertSame(cause, exception.getCause()); + } + + @Test + void + givenCtorWithEmptyMessageAndCause_whenCreatingProblemException_thenMessageIsDerivedFromProblem() { + Problem problem = Problem.builder().title("Bad Request").status(400).build(); + Throwable cause = new RuntimeException("root"); + + ProblemException exception = new ProblemException("", problem, cause); + + assertEquals("Bad Request (code: 400)", exception.getMessage()); + assertSame(cause, exception.getCause()); + } + + @Test + void givenFullCtorWithNullMessage_whenCreatingProblemException_thenMessageIsDerivedFromProblem() { + Problem problem = Problem.builder().title("Bad Request").status(400).build(); + Throwable cause = new RuntimeException("root"); + + ProblemException exception = new ProblemException(null, problem, cause, true, true); + + assertEquals("Bad Request (code: 400)", exception.getMessage()); + } + + @Test + void + givenFullCtorWithEmptyMessage_whenCreatingProblemException_thenMessageIsDerivedFromProblem() { + Problem problem = Problem.builder().title("Bad Request").status(400).build(); + Throwable cause = new RuntimeException("root"); + + ProblemException exception = new ProblemException("", problem, cause, true, true); + + assertEquals("Bad Request (code: 400)", exception.getMessage()); + } + + @Test + void givenProblemWithEmptyTitleAndStatus_whenCreatingException_thenOnlyStatusInMessage() { + Problem problem = Problem.builder().title("").status(500).build(); + + ProblemException exception = new ProblemException(problem); + + assertEquals("(code: 500)", exception.getMessage()); + } } diff --git a/src/test/java/io/github/problem4j/core/ProblemImplTest.java b/src/test/java/io/github/problem4j/core/ProblemImplTest.java index c04f99d..17e325c 100644 --- a/src/test/java/io/github/problem4j/core/ProblemImplTest.java +++ b/src/test/java/io/github/problem4j/core/ProblemImplTest.java @@ -23,47 +23,16 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.net.URI; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; /** * Some of the tests in this class may appear trivial or unnecessary. They are intentionally - * included to explore and validate the behavior of various code coverage analysis tools. These - * tests help ensure that the coverage reports correctly reflect different execution paths, edge - * cases, and instrumentation scenarios. + * included to explore and validate the behavior of various code coverage analysis tools. */ class ProblemImplTest { - @Test - void givenAllFieldsPopulated_whenToString_thenContainsAllFields() { - URI type = URI.create("https://example.com/problem"); - String title = "Test Problem"; - int status = 400; - String detail = "Something went wrong"; - URI instance = URI.create("https://example.com/instance"); - Map extensions = new HashMap<>(); - extensions.put("stringExt", "value"); - extensions.put("numberExt", 42); - extensions.put("booleanExt", true); - extensions.put("objectExt", new DummyObject("boo\nfoo")); - - Problem problem = new ProblemImpl(type, title, status, detail, instance, extensions); - - String result = problem.toString(); - - assertThat(result).contains("type=" + type); - assertThat(result).contains("title=" + title); - assertThat(result).contains("status=" + status); - assertThat(result).contains("detail=" + detail); - assertThat(result).contains("instance=" + instance); - assertThat(result).contains("stringExt=value"); - assertThat(result).contains("numberExt=42"); - assertThat(result).contains("booleanExt=true"); - assertThat(result).contains("objectExt=DummyObject{value=boo\nfoo}"); - } - @Test void givenNullExtensionsAndNullableFields_whenToString_thenOmitsNulls() { Map extensions = new HashMap<>(); diff --git a/src/test/java/io/github/problem4j/core/ProblemMapperTest.java b/src/test/java/io/github/problem4j/core/ProblemMapperTest.java index 49d9ba3..b790261 100644 --- a/src/test/java/io/github/problem4j/core/ProblemMapperTest.java +++ b/src/test/java/io/github/problem4j/core/ProblemMapperTest.java @@ -316,4 +316,311 @@ class SubException extends BaseException {} void isMappingCandidate_returnsFalse_forNull() { assertThat(mapper.isMappingCandidate(null)).isFalse(); } + + @Test + void givenNullThrowable_whenToProblemBuilder_thenReturnsEmptyBuilder() { + Problem problem = mapper.toProblemBuilder(null).build(); + + assertThat(problem.getStatus()).isZero(); + assertThat(problem.getTitle()).isEqualTo(Problem.UNKNOWN_TITLE); + } + + @Test + void givenUnannotatedThrowable_whenToProblemBuilder_thenReturnsEmptyBuilder() { + Problem problem = mapper.toProblemBuilder(new RuntimeException("plain")).build(); + + assertThat(problem.getStatus()).isZero(); + assertThat(problem.getTitle()).isEqualTo(Problem.UNKNOWN_TITLE); + } + + @Test + void givenNullThrowable_whenToProblemBuilderWithContext_thenReturnsEmptyBuilder() { + Problem problem = mapper.toProblemBuilder(null, ProblemContext.create()).build(); + + assertThat(problem.getStatus()).isZero(); + } + + @Test + void givenInvalidTypeUri_whenMapping_thenTypeIsIgnored() { + + @ProblemMapping(type = "ht tp://invalid uri", title = "Title", status = 400) + class InvalidTypeException extends RuntimeException { + InvalidTypeException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new InvalidTypeException()).build(); + + assertThat(problem.getType()).isEqualTo(Problem.BLANK_TYPE); + assertThat(problem.getTitle()).isEqualTo("Title"); + } + + @Test + void givenInvalidInstanceUri_whenMapping_thenInstanceIsIgnored() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + instance = "ht tp://invalid uri") + class InvalidInstanceException extends RuntimeException { + InvalidInstanceException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new InvalidInstanceException()).build(); + + assertThat(problem.getInstance()).isNull(); + } + + @Test + void givenTypeResolvingToEmpty_whenMapping_thenTypeIsNotSet() { + + @ProblemMapping(type = "{missing}", title = "Title", status = 400) + class EmptyTypeException extends RuntimeException { + EmptyTypeException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new EmptyTypeException()).build(); + + assertThat(problem.getType()).isEqualTo(Problem.BLANK_TYPE); + } + + @Test + void givenTitleResolvingToEmpty_whenMapping_thenTitleFallsBackToDefault() { + + @ProblemMapping(type = "https://example.org/err", title = "{missing}", status = 400) + class EmptyTitleException extends RuntimeException { + EmptyTitleException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new EmptyTitleException()).build(); + + assertThat(problem.getTitle()).isEqualTo(ProblemStatus.BAD_REQUEST.getTitle()); + } + + @Test + void givenDetailResolvingToEmpty_whenMapping_thenDetailIsNotSet() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + detail = "{missing}") + class EmptyDetailException extends RuntimeException { + EmptyDetailException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new EmptyDetailException()).build(); + + assertThat(problem.getDetail()).isNull(); + } + + @Test + void givenInstanceResolvingToEmpty_whenMapping_thenInstanceIsNotSet() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + instance = "{missing}") + class EmptyInstanceException extends RuntimeException { + EmptyInstanceException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new EmptyInstanceException()).build(); + + assertThat(problem.getInstance()).isNull(); + } + + @Test + void givenExtensionWithBlankName_whenMapping_thenBlankNameIsSkipped() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + extensions = {" ", " \t "}) + class BlankExtException extends RuntimeException { + BlankExtException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new BlankExtException()).build(); + + assertThat(problem.getExtensions()).isEmpty(); + } + + @Test + void givenExtensionWithEmptyStringValue_whenMapping_thenExtensionIsOmitted() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + extensions = {"emptyField"}) + class EmptyExtException extends RuntimeException { + + private final String emptyField; + + EmptyExtException() { + super("err"); + this.emptyField = ""; + } + } + + Problem problem = mapper.toProblemBuilder(new EmptyExtException()).build(); + + assertThat(problem.getExtensions()).isEmpty(); + } + + @Test + void givenContextWithMissingKey_whenInterpolating_thenResolvesToEmptyString() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + detail = "trace:{context.missingKey}") + class CtxException extends RuntimeException { + CtxException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new CtxException(), ProblemContext.create()).build(); + + assertThat(problem.getDetail()).isEqualTo("trace:"); + } + + @Test + void givenContextKeyWithNullContext_whenInterpolating_thenResolvesToEmptyString() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + detail = "trace:{context.traceId}") + class CtxException extends RuntimeException { + CtxException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new CtxException()).build(); + + assertThat(problem.getDetail()).isEqualTo("trace:"); + } + + @Test + void givenNullMessage_whenInterpolating_thenMessageResolvesToEmpty() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + detail = "msg:{message}") + class NullMsgException extends RuntimeException { + NullMsgException() { + super((String) null); + } + } + + Problem problem = mapper.toProblemBuilder(new NullMsgException()).build(); + + assertThat(problem.getDetail()).isEqualTo("msg:"); + } + + @Test + void givenZeroStatus_whenMapping_thenStatusIsNotSet() { + + @ProblemMapping(type = "https://example.org/err", title = "Title", status = 0) + class ZeroStatusException extends RuntimeException { + ZeroStatusException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new ZeroStatusException()).build(); + + assertThat(problem.getStatus()).isZero(); + } + + @Test + void givenExtensionFieldInSuperclass_whenMapping_thenFieldIsResolved() { + + class BaseException extends RuntimeException { + + protected final String code; + + BaseException(String code, String message) { + super(message); + this.code = code; + } + } + + @ProblemMapping( + type = "https://example.org/err", + title = "Inherited", + status = 400, + extensions = {"code"}) + class ChildException extends BaseException { + ChildException(String code, String message) { + super(code, message); + } + } + + Problem problem = mapper.toProblemBuilder(new ChildException("ERR-1", "fail")).build(); + + assertThat(problem.getExtensionValue("code")).isEqualTo("ERR-1"); + } + + @Test + void givenExtensionFieldThatDoesNotExist_whenMapping_thenExtensionIsOmitted() { + + @ProblemMapping( + type = "https://example.org/err", + title = "No field", + status = 400, + extensions = {"nonexistent"}) + class NoFieldException extends RuntimeException { + NoFieldException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new NoFieldException()).build(); + + assertThat(problem.getExtensions()).isEmpty(); + } + + @Test + void givenEmptyPlaceholder_whenInterpolating_thenResolvesToEmpty() { + + @ProblemMapping( + type = "https://example.org/err", + title = "Title", + status = 400, + detail = "before{}after") + class EmptyPlaceholderException extends RuntimeException { + EmptyPlaceholderException() { + super("err"); + } + } + + Problem problem = mapper.toProblemBuilder(new EmptyPlaceholderException()).build(); + + assertThat(problem.getDetail()).isEqualTo("before{}after"); + } } diff --git a/src/test/java/io/github/problem4j/core/ProblemMappingExceptionTest.java b/src/test/java/io/github/problem4j/core/ProblemMappingExceptionTest.java new file mode 100644 index 0000000..fa6d968 --- /dev/null +++ b/src/test/java/io/github/problem4j/core/ProblemMappingExceptionTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025-2026 The Problem4J Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.github.problem4j.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Some of the tests in this class may appear trivial or unnecessary. They are intentionally + * included to explore and validate the behavior of various code coverage analysis tools. + */ +class ProblemMappingExceptionTest { + + @Test + void givenNoArgs_whenCreatingException_thenMessageIsNull() { + ProblemMappingException ex = new ProblemMappingException(); + + assertThat(ex.getMessage()).isNull(); + assertThat(ex.getCause()).isNull(); + } + + @Test + void givenMessage_whenCreatingException_thenMessageIsSet() { + ProblemMappingException ex = new ProblemMappingException("mapping failed"); + + assertThat(ex.getMessage()).isEqualTo("mapping failed"); + assertThat(ex.getCause()).isNull(); + } + + @Test + void givenCause_whenCreatingException_thenCauseIsSet() { + RuntimeException cause = new RuntimeException("root"); + ProblemMappingException ex = new ProblemMappingException(cause); + + assertThat(ex.getCause()).isSameAs(cause); + } + + @Test + void givenMessageAndCause_whenCreatingException_thenBothAreSet() { + RuntimeException cause = new RuntimeException("root"); + ProblemMappingException ex = new ProblemMappingException("mapping failed", cause); + + assertThat(ex.getMessage()).isEqualTo("mapping failed"); + assertThat(ex.getCause()).isSameAs(cause); + } + + @Test + void givenFullConstructor_whenCreatingException_thenAllFieldsAreSet() { + RuntimeException cause = new RuntimeException("root"); + ProblemMappingException ex = + new TestProblemMappingException("mapping failed", cause, true, false); + + assertThat(ex.getMessage()).isEqualTo("mapping failed"); + assertThat(ex.getCause()).isSameAs(cause); + } + + private static class TestProblemMappingException extends ProblemMappingException { + TestProblemMappingException( + String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + } +} diff --git a/src/test/java/io/github/problem4j/core/ProblemStatusTest.java b/src/test/java/io/github/problem4j/core/ProblemStatusTest.java index 41bc216..48d5e1c 100644 --- a/src/test/java/io/github/problem4j/core/ProblemStatusTest.java +++ b/src/test/java/io/github/problem4j/core/ProblemStatusTest.java @@ -39,6 +39,16 @@ void givenNullStatusCode_shouldReturnEmptyOptional() { assertThat(optionalStatus).isEmpty(); } + @Test + void givenNonNullIntegerStatusCode_shouldReturnMatchingStatus() { + Integer statusCode = 200; + + Optional optionalStatus = ProblemStatus.findValue(statusCode); + + assertThat(optionalStatus).isPresent(); + assertThat(optionalStatus.get()).isEqualTo(ProblemStatus.OK); + } + @ParameterizedTest @ValueSource(ints = {103, 413, 414, 416, 422}) void givenAmbiguousStatusCode_shouldPrioritizeNonDeprecatedOne(int value) diff --git a/src/test/java/io/github/problem4j/core/ProblemTest.java b/src/test/java/io/github/problem4j/core/ProblemTest.java index 0c69244..397bf75 100644 --- a/src/test/java/io/github/problem4j/core/ProblemTest.java +++ b/src/test/java/io/github/problem4j/core/ProblemTest.java @@ -41,6 +41,22 @@ void givenIntStatus_shouldSetStatus() { assertThat(problem.getStatus()).isEqualTo(ProblemStatus.MULTI_STATUS.getStatus()); } + @Test + void givenStatusAndDetail_shouldSetFields() { + Problem problem = Problem.of(400, "bad input"); + + assertThat(problem.getStatus()).isEqualTo(400); + assertThat(problem.getDetail()).isEqualTo("bad input"); + assertThat(problem.getTitle()).isEqualTo(ProblemStatus.BAD_REQUEST.getTitle()); + } + + @Test + void givenTypeWithEmptyUriString_whenIsTypeNonBlank_thenReturnsFalse() { + Problem problem = Problem.builder().type(URI.create("")).status(200).build(); + + assertThat(problem.isTypeNonBlank()).isFalse(); + } + @Test void givenTitleAndStatus_shouldSetFields() { Problem problem = Problem.of("Test", 400);