Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-1366.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: improvement
improvement:
description: Add endpoint service exception test util
links:
- https://github.com/palantir/conjure-java-runtime-api/pull/1366
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* (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.testing;

import com.palantir.logsafe.Arg;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Utility to collect safe and unsafe arguments into separate collections and assert unique entries in each.
*/
record AssertableArgs(Map<String, Object> safeArgs, Map<String, Object> unsafeArgs) {

AssertableArgs(List<Arg<?>> args) {
this(new HashMap<>(), new HashMap<>());

args.forEach(arg -> {
if (arg.isSafeForLogging()) {
assertPutSafe(arg);
} else {
assertPutUnsafe(arg);
}
});
}

private void assertPutSafe(Arg<?> arg) {
assertPut(safeArgs, arg.getName(), arg.getValue(), "safe");
}

private void assertPutUnsafe(Arg<?> arg) {
assertPut(unsafeArgs, arg.getName(), arg.getValue(), "unsafe");
}

private static void assertPut(Map<String, Object> map, String key, Object value, String name) {
Object previous = map.put(key, value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutable records is sort of an anti-pattern. I would just make this a normal class.

IMO this class just feels like unnecessary indirection. We have a class that contains two fields, only to ever consume each field independently.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, would revert back to normal class. I just extracted this from the existing class, but happy to tweak things.

@mpritham is going to take this over since they implemented the new error type. It was suggested the two exceptions should probably share an interface or ancestor so we can de-duplicate a lot of this logic.

if (previous != null) {
throw new AssertionError(String.format(
"Duplicate %s arg name '%s', first value: %s, second value: %s", name, key, previous, value));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,6 +41,10 @@ public static QosExceptionAssert assertThat(QosException actual) {
return new QosExceptionAssert(actual);
}

public static EndpointServiceExceptionAssert assertThat(EndpointServiceException actual) {
return new EndpointServiceExceptionAssert(actual);
}

@CanIgnoreReturnValue
public static ServiceExceptionAssert assertThatServiceExceptionThrownBy(ThrowingCallable shouldRaiseThrowable) {
return assertThatThrownBy(shouldRaiseThrowable).asInstanceOf(ServiceExceptionAssert.instanceOfAssertFactory());
Expand All @@ -54,4 +59,11 @@ public static RemoteExceptionAssert assertThatRemoteExceptionThrownBy(ThrowingCa
public static QosExceptionAssert assertThatQosExceptionThrownBy(ThrowingCallable shouldRaiseThrowable) {
return assertThatThrownBy(shouldRaiseThrowable).asInstanceOf(QosExceptionAssert.instanceOfAssertFactory());
}

@CanIgnoreReturnValue
public static EndpointServiceExceptionAssert assertThatEndpointServiceExceptionThrownBy(
ThrowingCallable shouldRaiseThrowable) {
return assertThatThrownBy(shouldRaiseThrowable)
.asInstanceOf(EndpointServiceExceptionAssert.instanceOfAssertFactory());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* (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.testing;

import com.palantir.conjure.java.api.errors.EndpointServiceException;
import com.palantir.conjure.java.api.errors.ErrorType;
import com.palantir.logsafe.Arg;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.assertj.core.api.AbstractThrowableAssert;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.assertj.core.util.Throwables;

public class EndpointServiceExceptionAssert
extends AbstractThrowableAssert<EndpointServiceExceptionAssert, EndpointServiceException> {

private static final InstanceOfAssertFactory<EndpointServiceException, EndpointServiceExceptionAssert>
INSTANCE_OF_ASSERT_FACTORY =
new InstanceOfAssertFactory<>(EndpointServiceException.class, EndpointServiceExceptionAssert::new);

EndpointServiceExceptionAssert(EndpointServiceException actual) {
super(actual, EndpointServiceExceptionAssert.class);
}

public static InstanceOfAssertFactory<EndpointServiceException, EndpointServiceExceptionAssert>
instanceOfAssertFactory() {
return INSTANCE_OF_ASSERT_FACTORY;
}

public final EndpointServiceExceptionAssert hasCode(ErrorType.Code code) {
isNotNull();
failIfNotEqual(
"Expected ErrorType.Code to be %s, but found %s",
code, actual.getErrorType().code());
return this;
}

public final EndpointServiceExceptionAssert hasType(ErrorType type) {
isNotNull();
failIfNotEqual("Expected ErrorType to be %s, but found %s", type, actual.getErrorType());
return this;
}

public final EndpointServiceExceptionAssert hasArgs(Arg<?>... args) {
isNotNull();

AssertableArgs actualArgs = new AssertableArgs(actual.getArgs());
AssertableArgs expectedArgs = new AssertableArgs(Arrays.asList(args));

failIfNotEqual("Expected safe args to be %s, but found %s", expectedArgs.safeArgs(), actualArgs.safeArgs());
failIfNotEqual(
"Expected unsafe args to be %s, but found %s", expectedArgs.unsafeArgs(), actualArgs.unsafeArgs());

return this;
}

public final EndpointServiceExceptionAssert hasNoArgs() {
isNotNull();

AssertableArgs actualArgs = new AssertableArgs(actual.getArgs());
if (!actualArgs.safeArgs().isEmpty() || !actualArgs.unsafeArgs().isEmpty()) {
Map<String, Object> allArgs = new HashMap<>();
allArgs.putAll(actualArgs.safeArgs());
allArgs.putAll(actualArgs.unsafeArgs());
failWithMessage(
"Expected no args, but found %s; service exception: %s", allArgs, Throwables.getStackTrace(actual));
}

return this;
}

private <T> void failIfNotEqual(String message, T expectedValue, T actualValue) {
if (!Objects.equals(expectedValue, actualValue)) {
failWithMessage(
message + "; service exception: ", expectedValue, actualValue, Throwables.getStackTrace(actual));
}
}

public final EndpointServiceExceptionAssert containsArgs(Arg<?>... args) {
isNotNull();

AssertableArgs actualArgs = new AssertableArgs(actual.getArgs());
AssertableArgs expectedArgs = new AssertableArgs(Arrays.asList(args));

failIfDoesNotContain(
"Expected safe args to contain %s, but found %s", expectedArgs.safeArgs(), actualArgs.safeArgs());
failIfDoesNotContain(
"Expected unsafe args to contain %s, but found %s", expectedArgs.unsafeArgs(), actualArgs.unsafeArgs());

return this;
}

private void failIfDoesNotContain(
String message, Map<String, Object> expectedArgs, Map<String, Object> actualArgs) {
if (!actualArgs.entrySet().containsAll(expectedArgs.entrySet())) {
failWithMessage(
message + "; service exception: %s", expectedArgs, actualArgs, Throwables.getStackTrace(actual));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import com.palantir.logsafe.Arg;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.assertj.core.api.AbstractThrowableAssert;
Expand Down Expand Up @@ -61,8 +60,9 @@ public final ServiceExceptionAssert hasArgs(Arg<?>... args) {
AssertableArgs actualArgs = new AssertableArgs(actual.getArgs());
AssertableArgs expectedArgs = new AssertableArgs(Arrays.asList(args));

failIfNotEqual("Expected safe args to be %s, but found %s", expectedArgs.safeArgs, actualArgs.safeArgs);
failIfNotEqual("Expected unsafe args to be %s, but found %s", expectedArgs.unsafeArgs, actualArgs.unsafeArgs);
failIfNotEqual("Expected safe args to be %s, but found %s", expectedArgs.safeArgs(), actualArgs.safeArgs());
failIfNotEqual(
"Expected unsafe args to be %s, but found %s", expectedArgs.unsafeArgs(), actualArgs.unsafeArgs());

return this;
}
Expand All @@ -71,10 +71,10 @@ public final ServiceExceptionAssert hasNoArgs() {
isNotNull();

AssertableArgs actualArgs = new AssertableArgs(actual.getArgs());
if (!actualArgs.safeArgs.isEmpty() || !actualArgs.unsafeArgs.isEmpty()) {
if (!actualArgs.safeArgs().isEmpty() || !actualArgs.unsafeArgs().isEmpty()) {
Map<String, Object> allArgs = new HashMap<>();
allArgs.putAll(actualArgs.safeArgs);
allArgs.putAll(actualArgs.unsafeArgs);
allArgs.putAll(actualArgs.safeArgs());
allArgs.putAll(actualArgs.unsafeArgs());
failWithMessage(
"Expected no args, but found %s; service exception: %s", allArgs, Throwables.getStackTrace(actual));
}
Expand All @@ -96,9 +96,9 @@ public final ServiceExceptionAssert containsArgs(Arg<?>... args) {
AssertableArgs expectedArgs = new AssertableArgs(Arrays.asList(args));

failIfDoesNotContain(
"Expected safe args to contain %s, but found %s", expectedArgs.safeArgs, actualArgs.safeArgs);
"Expected safe args to contain %s, but found %s", expectedArgs.safeArgs(), actualArgs.safeArgs());
failIfDoesNotContain(
"Expected unsafe args to contain %s, but found %s", expectedArgs.unsafeArgs, actualArgs.unsafeArgs);
"Expected unsafe args to contain %s, but found %s", expectedArgs.unsafeArgs(), actualArgs.unsafeArgs());

return this;
}
Expand All @@ -110,35 +110,4 @@ private void failIfDoesNotContain(
message + "; service exception: %s", expectedArgs, actualArgs, Throwables.getStackTrace(actual));
}
}

private static final class AssertableArgs {
private final Map<String, Object> safeArgs = new HashMap<>();
private final Map<String, Object> unsafeArgs = new HashMap<>();

private AssertableArgs(List<Arg<?>> args) {
args.forEach(arg -> {
if (arg.isSafeForLogging()) {
assertPutSafe(arg);
} else {
assertPutUnsafe(arg);
}
});
}

private void assertPutSafe(Arg<?> arg) {
assertPut(safeArgs, arg.getName(), arg.getValue(), "safe");
}

private void assertPutUnsafe(Arg<?> arg) {
assertPut(unsafeArgs, arg.getName(), arg.getValue(), "unsafe");
}

private static void assertPut(Map<String, Object> map, String key, Object value, String name) {
Object previous = map.put(key, value);
if (previous != null) {
throw new AssertionError(String.format(
"Duplicate %s arg name '%s', first value: %s, second value: %s", name, key, previous, value));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@

package com.palantir.conjure.java.api.testing;

import static com.palantir.conjure.java.api.testing.Assertions.assertThatEndpointServiceExceptionThrownBy;
import static com.palantir.conjure.java.api.testing.Assertions.assertThatQosExceptionThrownBy;
import static com.palantir.conjure.java.api.testing.Assertions.assertThatRemoteExceptionThrownBy;
import static com.palantir.conjure.java.api.testing.Assertions.assertThatServiceExceptionThrownBy;
import static com.palantir.conjure.java.api.testing.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.QosException;
import com.palantir.conjure.java.api.errors.QosReason;
Expand Down Expand Up @@ -113,4 +115,31 @@ public void testAssertThatQosExceptionThrownBy_catchesQosException() {
})
.hasReason(QosReason.of("reason"));
}

@Test
public void testAssertThatEndpointServiceExceptionThrownBy_failsIfNothingThrown() {
assertThatThrownBy(() -> assertThatEndpointServiceExceptionThrownBy(() -> {
// Not going to throw anything
}))
.hasMessageContaining("Expecting code to raise a throwable.");
}

@Test
public void testAssertThatEndpointServiceExceptionThrownBy_failsIfWrongExceptionThrown() {
assertThatThrownBy(() -> assertThatEndpointServiceExceptionThrownBy(() -> {
throw new RuntimeException("My message");
}))
.hasMessageContaining(
"com.palantir.conjure.java.api.errors.EndpointServiceException",
"java.lang.RuntimeException",
"My message");
}

@Test
public void testAssertThatEndpointServiceExceptionThrownBy_catchesEndpointServiceException() {
assertThatEndpointServiceExceptionThrownBy(() -> {
throw new EndpointServiceException(ErrorType.INTERNAL) {};
})
.hasType(ErrorType.INTERNAL);
}
}
Loading