Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bug calling onKeyUpdate when should not, and better error messages #780

Merged
merged 8 commits into from
Jan 24, 2019
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ String getType() {
protected static <TEphemeralKey extends AbstractEphemeralKey> TEphemeralKey
fromString(@Nullable String rawJson, Class ephemeralKeyClass) throws JSONException {
if (rawJson == null) {
return null;
throw new IllegalArgumentException("Attempted to instantiate " +
ephemeralKeyClass.getSimpleName() + " with null raw key");
}
JSONObject object = new JSONObject(rawJson);
return fromJson(object, ephemeralKeyClass);
Expand All @@ -226,21 +227,32 @@ String getType() {
protected static <TEphemeralKey extends AbstractEphemeralKey> TEphemeralKey
fromJson(@Nullable JSONObject jsonObject, Class ephemeralKeyClass) {
if (jsonObject == null) {
return null;
throw new IllegalArgumentException("Exception instantiating " +
ephemeralKeyClass.getSimpleName() +
" null JSON");
}

try {
return (TEphemeralKey)
ephemeralKeyClass.getConstructor(JSONObject.class).newInstance(jsonObject);
} catch (InstantiationException e) {
throw new IllegalArgumentException("Exception instantiating " + ephemeralKeyClass, e);
throw new IllegalArgumentException("Exception instantiating " +
ephemeralKeyClass.getSimpleName(), e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Exception instantiating " + ephemeralKeyClass, e);
throw new IllegalArgumentException("Exception instantiating " +
ephemeralKeyClass.getSimpleName(), e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Exception instantiating " + ephemeralKeyClass, e);
if (e.getTargetException() != null) {
throw new IllegalArgumentException("Improperly formatted JSON for ephemeral key " +
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can remove the e.getTargetException().getMessage() part from the message because we're passing e.getTargetException() as an argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hm, in an ideal world yes, but the interface of onKeyError only accepts a String, and that's what we're passing to the dev.

So without explicitely dropping the message in the String, we risk losing it as it is surfaced, if that makes sense

ephemeralKeyClass.getSimpleName() +
" - " + e.getTargetException().getMessage(),
e.getTargetException());
}
throw new IllegalArgumentException("Improperly formatted JSON for ephemeral key " +
ephemeralKeyClass.getSimpleName(), e);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Class " +
ephemeralKeyClass +
ephemeralKeyClass.getSimpleName() +
" does not have an accessible (JSONObject) constructor", e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,35 @@ TEphemeralKey getEphemeralKey() {
return mEphemeralKey;
}

@SuppressWarnings("checkstyle:IllegalCatch")
private void updateKey(
@NonNull String key,
@Nullable String actionString,
@Nullable Map<String, Object> arguments) {
// Key is coming from the user, so even if it's @NonNull annotated we
// want to double check it
if (key == null) {
mListener.onKeyError(HttpURLConnection.HTTP_INTERNAL_ERROR,
"EphemeralKeyUpdateListener.onKeyUpdate was called " +
"with a null value");
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we return; after this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah yes, good catch

return;
}
try {
mEphemeralKey = AbstractEphemeralKey.fromString(key, mEphemeralKeyClass);
mListener.onKeyUpdate(mEphemeralKey, actionString, arguments);
} catch (JSONException e) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we still need to explicitly catch JSONException if we have the catch all below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it helps the message be a tad more explicit

mListener.onKeyError(HttpURLConnection.HTTP_INTERNAL_ERROR,
"The JSON from the key could not be parsed: "
+ e.getLocalizedMessage());
"EphemeralKeyUpdateListener.onKeyUpdate was passed " +
"a value that could not be JSON parsed: ["
+ e.getLocalizedMessage() + "]. The raw body from Stripe's response" +
" should be passed");
} catch (Exception e) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

did you add this for safety?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, if any other exception happens, the error will just be silenced, which was the case for a wrongly formatted JSON (the JSON is parsed, but then the CustomerEphemeralKey cannot be instanciated)

mListener.onKeyError(HttpURLConnection.HTTP_INTERNAL_ERROR,
"EphemeralKeyUpdateListener.onKeyUpdate was passed " +
"a JSON String that was invalid: ["
+ e.getLocalizedMessage() + "]. The raw body from Stripe's response" +
" should be passed");
}
mListener.onKeyUpdate(mEphemeralKey, actionString, arguments);
}

private void updateKeyError(int errorCode, @Nullable String errorMessage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ public interface EphemeralKeyUpdateListener {
/**
* Called when a key update request from your server comes back successfully.
*
* @param rawKey the raw String returned from Stripe's servers
* @param stripeResponseJson the raw JSON String returned from Stripe's servers
*/
void onKeyUpdate(@NonNull String rawKey);
void onKeyUpdate(@NonNull String stripeResponseJson);

/**
* Called when a key update request from your server comes back with an error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;

import java.net.HttpURLConnection;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -25,6 +26,7 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -250,4 +252,67 @@ public void updateKeyIfNecessary_whenReturnsError_setsExistingKeyToNull() {
verifyNoMoreInteractions(mKeyManagerListener);
assertNull(keyManager.getEphemeralKey());
}

@Test
public void triggerCorrectErrorOnInvalidRawKey() {

mTestEphemeralKeyProvider.setNextRawEphemeralKey("Not_a_JSON");
EphemeralKeyManager<CustomerEphemeralKey> keyManager = new EphemeralKeyManager<>(
mTestEphemeralKeyProvider,
mKeyManagerListener,
TEST_SECONDS_BUFFER,
null,
CustomerEphemeralKey.class);

verify(mKeyManagerListener, never()).onKeyUpdate(
(CustomerEphemeralKey) isNull(), (String) isNull(), (Map<String, Object>) isNull());
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is better:

any(CustomerEphemeralKey.class), anyString(), ArgumentMatchers.<String, Object>anyMap()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I initially tried that, but it fails to error when onKeyUpdate is called with nulls

Copy link
Collaborator

Choose a reason for hiding this comment

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

don't we expect it not to be called though? so I would assume "never with any" makes more logical sense than "never with nulls"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, but if I change to the any and re-introduce the bug where onKeyUpdate is called, the test still passes

verify(mKeyManagerListener, times(1)).onKeyError(
HttpURLConnection.HTTP_INTERNAL_ERROR,
"EphemeralKeyUpdateListener.onKeyUpdate was passed a value that " +
"could not be JSON parsed: [Value Not_a_JSON of type java.lang.String " +
"cannot be converted to JSONObject]. The raw body from Stripe's " +
"response should be passed");
assertNull(keyManager.getEphemeralKey());
}

@Test
public void triggerCorrectErrorOnInvalidJsonKey() {

mTestEphemeralKeyProvider.setNextRawEphemeralKey("{}");
EphemeralKeyManager<CustomerEphemeralKey> keyManager = new EphemeralKeyManager<>(
mTestEphemeralKeyProvider,
mKeyManagerListener,
TEST_SECONDS_BUFFER,
null,
CustomerEphemeralKey.class);

verify(mKeyManagerListener, never()).onKeyUpdate(
(CustomerEphemeralKey) isNull(), (String) isNull(), (Map<String, Object>) isNull());
verify(mKeyManagerListener, times(1)).onKeyError(
HttpURLConnection.HTTP_INTERNAL_ERROR,
"EphemeralKeyUpdateListener.onKeyUpdate was passed a JSON String " +
"that was invalid: [Improperly formatted JSON for ephemeral " +
"key CustomerEphemeralKey - No value for created]. The raw body " +
"from Stripe's response should be passed");
assertNull(keyManager.getEphemeralKey());
}

@Test
public void triggerCorrectErrorOnNullKey() {

mTestEphemeralKeyProvider.setNextRawEphemeralKey(null);
EphemeralKeyManager<CustomerEphemeralKey> keyManager = new EphemeralKeyManager<>(
mTestEphemeralKeyProvider,
mKeyManagerListener,
TEST_SECONDS_BUFFER,
null,
CustomerEphemeralKey.class);

verify(mKeyManagerListener, never()).onKeyUpdate(
(CustomerEphemeralKey) isNull(), (String) isNull(), (Map<String, Object>) isNull());
verify(mKeyManagerListener, times(1)).onKeyError(
HttpURLConnection.HTTP_INTERNAL_ERROR,
"EphemeralKeyUpdateListener.onKeyUpdate was called with a null value");
assertNull(keyManager.getEphemeralKey());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public void createEphemeralKey(
keyUpdateListener.onKeyUpdate(mRawEphemeralKey);
} else if (mErrorCode != INVALID_ERROR_CODE) {
keyUpdateListener.onKeyUpdateFailure(mErrorCode, mErrorMessage);
} else {
// Useful to test edge cases
keyUpdateListener.onKeyUpdate(null);
}
}

Expand Down