diff --git a/changelog/@unreleased/pr-1383.v2.yml b/changelog/@unreleased/pr-1383.v2.yml new file mode 100644 index 000000000..c589ce06a --- /dev/null +++ b/changelog/@unreleased/pr-1383.v2.yml @@ -0,0 +1,6 @@ +type: improvement +improvement: + description: Add `AbstractServiceException` and extend `ServiceExceptionAssert` + to support `EndpointServiceException` + links: + - https://github.com/palantir/conjure-java-runtime-api/pull/1383 diff --git a/errors/src/main/java/com/palantir/conjure/java/api/errors/AbstractServiceException.java b/errors/src/main/java/com/palantir/conjure/java/api/errors/AbstractServiceException.java new file mode 100644 index 000000000..b92e5311c --- /dev/null +++ b/errors/src/main/java/com/palantir/conjure/java/api/errors/AbstractServiceException.java @@ -0,0 +1,30 @@ +/* + * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.conjure.java.api.errors; + +import com.palantir.logsafe.SafeLoggable; + +/** + * This class should not be used directly. The parent class for ServiceException and EndpointServiceException. + */ +public abstract class AbstractServiceException extends RuntimeException implements SafeLoggable { + public abstract ErrorType getErrorType(); + + protected AbstractServiceException(Throwable cause) { + super(cause); + } +} diff --git a/errors/src/main/java/com/palantir/conjure/java/api/errors/EndpointServiceException.java b/errors/src/main/java/com/palantir/conjure/java/api/errors/EndpointServiceException.java index d8008c160..037952c00 100644 --- a/errors/src/main/java/com/palantir/conjure/java/api/errors/EndpointServiceException.java +++ b/errors/src/main/java/com/palantir/conjure/java/api/errors/EndpointServiceException.java @@ -17,7 +17,6 @@ package com.palantir.conjure.java.api.errors; import com.palantir.logsafe.Arg; -import com.palantir.logsafe.SafeLoggable; import java.util.List; import javax.annotation.Nullable; @@ -25,7 +24,7 @@ * This is identical to ServiceException, but is used in Conjure-generated code to indicate that an exception was thrown * from a service endpoint. */ -public abstract class EndpointServiceException extends RuntimeException implements SafeLoggable { +public abstract class EndpointServiceException extends AbstractServiceException { private static final String EXCEPTION_NAME = "EndpointServiceException"; private final ErrorType errorType; private final List> args; // This is an unmodifiable list. @@ -52,6 +51,7 @@ public EndpointServiceException(ErrorType errorType, @Nullable Throwable cause, } /** The {@link ErrorType} that gave rise to this exception. */ + @Override public final ErrorType getErrorType() { return errorType; } diff --git a/errors/src/main/java/com/palantir/conjure/java/api/errors/ServiceException.java b/errors/src/main/java/com/palantir/conjure/java/api/errors/ServiceException.java index 6b2e985d7..b420ae740 100644 --- a/errors/src/main/java/com/palantir/conjure/java/api/errors/ServiceException.java +++ b/errors/src/main/java/com/palantir/conjure/java/api/errors/ServiceException.java @@ -17,12 +17,11 @@ package com.palantir.conjure.java.api.errors; import com.palantir.logsafe.Arg; -import com.palantir.logsafe.SafeLoggable; import java.util.List; import javax.annotation.Nullable; /** A {@link ServiceException} thrown in server-side code to indicate server-side {@link ErrorType error states}. */ -public final class ServiceException extends RuntimeException implements SafeLoggable { +public final class ServiceException extends AbstractServiceException { private static final String EXCEPTION_NAME = "ServiceException"; private final ErrorType errorType; private final List> args; // unmodifiable @@ -53,6 +52,7 @@ public ServiceException(ErrorType errorType, @Nullable Throwable cause, Arg.. } /** The {@link ErrorType} that gave rise to this exception. */ + @Override public ErrorType getErrorType() { return errorType; } diff --git a/test-utils/src/main/java/com/palantir/conjure/java/api/testing/Assertions.java b/test-utils/src/main/java/com/palantir/conjure/java/api/testing/Assertions.java index 420500425..4d940c919 100644 --- a/test-utils/src/main/java/com/palantir/conjure/java/api/testing/Assertions.java +++ b/test-utils/src/main/java/com/palantir/conjure/java/api/testing/Assertions.java @@ -16,6 +16,7 @@ package com.palantir.conjure.java.api.testing; +import com.palantir.conjure.java.api.errors.EndpointServiceException; import com.palantir.conjure.java.api.errors.QosException; import com.palantir.conjure.java.api.errors.RemoteException; import com.palantir.conjure.java.api.errors.ServiceException; @@ -32,6 +33,10 @@ public static ServiceExceptionAssert assertThat(ServiceException actual) { return new ServiceExceptionAssert(actual); } + public static ServiceExceptionAssert assertThat(EndpointServiceException actual) { + return new ServiceExceptionAssert(actual); + } + public static RemoteExceptionAssert assertThat(RemoteException actual) { return new RemoteExceptionAssert(actual); } diff --git a/test-utils/src/main/java/com/palantir/conjure/java/api/testing/ServiceExceptionAssert.java b/test-utils/src/main/java/com/palantir/conjure/java/api/testing/ServiceExceptionAssert.java index d2366f28d..29a80c8e3 100644 --- a/test-utils/src/main/java/com/palantir/conjure/java/api/testing/ServiceExceptionAssert.java +++ b/test-utils/src/main/java/com/palantir/conjure/java/api/testing/ServiceExceptionAssert.java @@ -16,8 +16,8 @@ package com.palantir.conjure.java.api.testing; +import com.palantir.conjure.java.api.errors.AbstractServiceException; import com.palantir.conjure.java.api.errors.ErrorType; -import com.palantir.conjure.java.api.errors.ServiceException; import com.palantir.logsafe.Arg; import java.util.Arrays; import java.util.HashMap; @@ -28,16 +28,17 @@ import org.assertj.core.api.InstanceOfAssertFactory; import org.assertj.core.util.Throwables; -public class ServiceExceptionAssert extends AbstractThrowableAssert { +public class ServiceExceptionAssert extends AbstractThrowableAssert { - private static final InstanceOfAssertFactory INSTANCE_OF_ASSERT_FACTORY = - new InstanceOfAssertFactory<>(ServiceException.class, ServiceExceptionAssert::new); + private static final InstanceOfAssertFactory + INSTANCE_OF_ASSERT_FACTORY = + new InstanceOfAssertFactory<>(AbstractServiceException.class, ServiceExceptionAssert::new); - ServiceExceptionAssert(ServiceException actual) { + ServiceExceptionAssert(AbstractServiceException actual) { super(actual, ServiceExceptionAssert.class); } - public static InstanceOfAssertFactory instanceOfAssertFactory() { + public static InstanceOfAssertFactory instanceOfAssertFactory() { return INSTANCE_OF_ASSERT_FACTORY; } diff --git a/test-utils/src/test/java/com/palantir/conjure/java/api/testing/AssertionsTest.java b/test-utils/src/test/java/com/palantir/conjure/java/api/testing/AssertionsTest.java index dc6bca0f1..d7c82d178 100644 --- a/test-utils/src/test/java/com/palantir/conjure/java/api/testing/AssertionsTest.java +++ b/test-utils/src/test/java/com/palantir/conjure/java/api/testing/AssertionsTest.java @@ -45,7 +45,7 @@ public void testAssertThatServiceExceptionThrownBy_failsIfWrongExceptionThrown() throw new RuntimeException("My message"); })) .hasMessageContaining( - "com.palantir.conjure.java.api.errors.ServiceException", + "com.palantir.conjure.java.api.errors.AbstractServiceException", "java.lang.RuntimeException", "My message"); } diff --git a/test-utils/src/test/java/com/palantir/conjure/java/api/testing/ServiceExceptionAssertTest.java b/test-utils/src/test/java/com/palantir/conjure/java/api/testing/ServiceExceptionAssertTest.java index 964727deb..6483f5abd 100644 --- a/test-utils/src/test/java/com/palantir/conjure/java/api/testing/ServiceExceptionAssertTest.java +++ b/test-utils/src/test/java/com/palantir/conjure/java/api/testing/ServiceExceptionAssertTest.java @@ -18,14 +18,22 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.palantir.conjure.java.api.errors.EndpointServiceException; import com.palantir.conjure.java.api.errors.ErrorType; import com.palantir.conjure.java.api.errors.ServiceException; +import com.palantir.logsafe.Arg; import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.UnsafeArg; import org.junit.jupiter.api.Test; public class ServiceExceptionAssertTest { + private static class TestEndpointServiceException extends EndpointServiceException { + TestEndpointServiceException(ErrorType errorType, Arg... safeArgs) { + super(errorType, safeArgs); + } + } + @Test public void testSanity() { ErrorType actualType = ErrorType.FAILED_PRECONDITION; @@ -35,47 +43,92 @@ public void testSanity() { .hasType(actualType) .hasArgs(SafeArg.of("a", "b"), UnsafeArg.of("c", "d")); + Assertions.assertThat( + new TestEndpointServiceException(actualType, SafeArg.of("a", "b"), UnsafeArg.of("c", "d"))) + .hasCode(actualType.code()) + .hasType(actualType) + .hasArgs(SafeArg.of("a", "b"), UnsafeArg.of("c", "d")); + Assertions.assertThat(new ServiceException(actualType, SafeArg.of("a", "b"), UnsafeArg.of("c", "d"))) .hasCode(actualType.code()) .hasType(actualType) .hasArgs(UnsafeArg.of("c", "d"), SafeArg.of("a", "b")); // Order doesn't matter + Assertions.assertThat( + new TestEndpointServiceException(actualType, SafeArg.of("a", "b"), UnsafeArg.of("c", "d"))) + .hasCode(actualType.code()) + .hasType(actualType) + .hasArgs(UnsafeArg.of("c", "d"), SafeArg.of("a", "b")); // Order doesn't matter + Assertions.assertThat(new ServiceException(actualType)).hasNoArgs(); + Assertions.assertThat(new TestEndpointServiceException(actualType)).hasNoArgs(); assertThatThrownBy(() -> Assertions.assertThat(new ServiceException(actualType)).hasCode(ErrorType.Code.INTERNAL)) .isInstanceOf(AssertionError.class) .hasMessageContaining( "Expected ErrorType.Code to be %s, but found %s", ErrorType.Code.INTERNAL, actualType.code()); + assertThatThrownBy(() -> Assertions.assertThat(new TestEndpointServiceException(actualType)) + .hasCode(ErrorType.Code.INTERNAL)) + .isInstanceOf(AssertionError.class) + .hasMessageContaining( + "Expected ErrorType.Code to be %s, but found %s", ErrorType.Code.INTERNAL, actualType.code()); assertThatThrownBy(() -> Assertions.assertThat(new ServiceException(actualType)).hasType(ErrorType.INTERNAL)) .isInstanceOf(AssertionError.class) .hasMessageContaining("Expected ErrorType to be %s, but found %s", ErrorType.INTERNAL, actualType); + assertThatThrownBy(() -> Assertions.assertThat(new TestEndpointServiceException(actualType)) + .hasType(ErrorType.INTERNAL)) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected ErrorType to be %s, but found %s", ErrorType.INTERNAL, actualType); assertThatThrownBy(() -> Assertions.assertThat(new ServiceException(actualType, SafeArg.of("a", "b"))) .hasArgs(SafeArg.of("c", "d"))) .isInstanceOf(AssertionError.class) .hasMessageContaining("Expected safe args to be {c=d}, but found {a=b}"); + assertThatThrownBy( + () -> Assertions.assertThat(new TestEndpointServiceException(actualType, SafeArg.of("a", "b"))) + .hasArgs(SafeArg.of("c", "d"))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected safe args to be {c=d}, but found {a=b}"); assertThatThrownBy(() -> Assertions.assertThat(new ServiceException(actualType, UnsafeArg.of("a", "b"))) .hasArgs(UnsafeArg.of("c", "d"))) .isInstanceOf(AssertionError.class) .hasMessageContaining("Expected unsafe args to be {c=d}, but found {a=b}"); + assertThatThrownBy(() -> Assertions.assertThat( + new TestEndpointServiceException(actualType, UnsafeArg.of("a", "b"))) + .hasArgs(UnsafeArg.of("c", "d"))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected unsafe args to be {c=d}, but found {a=b}"); assertThatThrownBy(() -> Assertions.assertThat(new ServiceException(actualType, SafeArg.of("a", "b"))) .hasNoArgs()) .isInstanceOf(AssertionError.class) .hasMessageContaining("Expected no args, but found {a=b}"); + assertThatThrownBy( + () -> Assertions.assertThat(new TestEndpointServiceException(actualType, SafeArg.of("a", "b"))) + .hasNoArgs()) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected no args, but found {a=b}"); assertThatThrownBy(() -> Assertions.assertThat( new ServiceException(actualType, SafeArg.of("a", "b"), UnsafeArg.of("c", "d"))) .hasNoArgs()) .isInstanceOf(AssertionError.class) .hasMessageContaining("Expected no args, but found {a=b, c=d}"); + assertThatThrownBy(() -> Assertions.assertThat(new TestEndpointServiceException( + actualType, SafeArg.of("a", "b"), UnsafeArg.of("c", "d"))) + .hasNoArgs()) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected no args, but found {a=b, c=d}"); Assertions.assertThat(new ServiceException(actualType, UnsafeArg.of("a", "b"), UnsafeArg.of("c", "d"))) .containsArgs(UnsafeArg.of("a", "b")); + Assertions.assertThat( + new TestEndpointServiceException(actualType, UnsafeArg.of("a", "b"), UnsafeArg.of("c", "d"))) + .containsArgs(UnsafeArg.of("a", "b")); // Safety matters assertThatThrownBy(() -> Assertions.assertThat( @@ -83,15 +136,30 @@ public void testSanity() { .containsArgs(UnsafeArg.of("a", "b"))) .isInstanceOf(AssertionError.class) .hasMessageContaining("Expected unsafe args to contain {a=b}, but found {c=d}"); + assertThatThrownBy(() -> Assertions.assertThat(new TestEndpointServiceException( + actualType, SafeArg.of("a", "b"), UnsafeArg.of("c", "d"))) + .containsArgs(UnsafeArg.of("a", "b"))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected unsafe args to contain {a=b}, but found {c=d}"); assertThatThrownBy(() -> Assertions.assertThat(new ServiceException(actualType, SafeArg.of("a", "b"))) .containsArgs(SafeArg.of("c", "d"))) .isInstanceOf(AssertionError.class) .hasMessageContaining("Expected safe args to contain {c=d}, but found {a=b}"); + assertThatThrownBy( + () -> Assertions.assertThat(new TestEndpointServiceException(actualType, SafeArg.of("a", "b"))) + .containsArgs(SafeArg.of("c", "d"))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected safe args to contain {c=d}, but found {a=b}"); assertThatThrownBy(() -> Assertions.assertThat(new ServiceException(actualType, UnsafeArg.of("a", "b"))) .containsArgs(UnsafeArg.of("c", "d"))) .isInstanceOf(AssertionError.class) .hasMessageContaining("Expected unsafe args to contain {c=d}, but found {a=b}"); + assertThatThrownBy(() -> Assertions.assertThat( + new TestEndpointServiceException(actualType, UnsafeArg.of("a", "b"))) + .containsArgs(UnsafeArg.of("c", "d"))) + .isInstanceOf(AssertionError.class) + .hasMessageContaining("Expected unsafe args to contain {c=d}, but found {a=b}"); } }