diff --git a/receipt/build.gradle b/receipt/build.gradle index dac1fd822..6aa8d9d42 100644 --- a/receipt/build.gradle +++ b/receipt/build.gradle @@ -11,7 +11,13 @@ dependencies { implementation "androidx.paging:paging-runtime:$pagingRuntimeVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" + androidTestImplementation "androidx.test:rules:$testRulesVersion" + androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" + androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" + androidTestImplementation "com.squareup.okhttp3:mockwebserver:$mockServerVersion" + androidTestImplementation "com.squareup.leakcanary:leakcanary-android-instrumentation:$leakcanaryVersion" + androidTestImplementation "com.squareup.leakcanary:leakcanary-support-fragment:$leakcanaryVersion" } def aarFile = file("$buildDir/outputs/aar/receipt-$version" + ".aar") diff --git a/receipt/src/androidTest/AndroidManifest.xml b/receipt/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..8495adb45 --- /dev/null +++ b/receipt/src/androidTest/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/HyperwalletInstrumentedTestApplication.java b/receipt/src/androidTest/java/com/hyperwallet/android/HyperwalletInstrumentedTestApplication.java new file mode 100644 index 000000000..dfc559d2e --- /dev/null +++ b/receipt/src/androidTest/java/com/hyperwallet/android/HyperwalletInstrumentedTestApplication.java @@ -0,0 +1,31 @@ +package com.hyperwallet.android; + + +import android.app.Application; + +import com.squareup.leakcanary.InstrumentationLeakDetector; +import com.squareup.leakcanary.LeakCanary; + +public class HyperwalletInstrumentedTestApplication extends Application { + + @Override + public void onCreate() { + + super.onCreate(); + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + installLeakCanary(); + } + + + protected void installLeakCanary() { + + InstrumentationLeakDetector.instrumentationRefWatcher(this) + .buildAndInstall(); + + } + +} \ No newline at end of file diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/receipt/ListReceiptsTest.java b/receipt/src/androidTest/java/com/hyperwallet/android/receipt/ListReceiptsTest.java new file mode 100644 index 000000000..5eb430354 --- /dev/null +++ b/receipt/src/androidTest/java/com/hyperwallet/android/receipt/ListReceiptsTest.java @@ -0,0 +1,265 @@ +package com.hyperwallet.android.receipt; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withParent; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.instanceOf; + +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_OK; + +import static com.hyperwallet.android.util.EspressoUtils.atPosition; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.widget.TextView; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ActivityTestRule; + +import com.hyperwallet.android.Hyperwallet; +import com.hyperwallet.android.receipt.repository.ReceiptRepositoryFactory; +import com.hyperwallet.android.receipt.view.ListReceiptActivity; +import com.hyperwallet.android.rule.HyperwalletExternalResourceManager; +import com.hyperwallet.android.rule.HyperwalletMockWebServer; +import com.hyperwallet.android.util.RecyclerViewCountAssertion; +import com.hyperwallet.android.util.TestAuthenticationProvider; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@RunWith(AndroidJUnit4.class) +public class ListReceiptsTest { + + @ClassRule + public static HyperwalletExternalResourceManager sResourceManager = new HyperwalletExternalResourceManager(); + @Rule + public HyperwalletMockWebServer mMockWebServer = new HyperwalletMockWebServer(8080); + @Rule + public ActivityTestRule mActivityTestRule = + new ActivityTestRule<>(ListReceiptActivity.class, true, false); + + @Before + public void setup() { + Hyperwallet.getInstance(new TestAuthenticationProvider()); + + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager + .getResourceContent("authentication_token_response.json")).mock(); + + setLocale(Locale.US); + } + + @After + public void cleanup() { + ReceiptRepositoryFactory.clearInstance(); + } + + @Test + public void testListReceipts_userHasMultipleTransactions() { + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager + .getResourceContent("receipt_list_response.json")).mock(); + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock(); + + // run test + mActivityTestRule.launchActivity(null); + + // assert + onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar)))) + .check(matches(withText(R.string.title_activity_receipt_list))); + onView(withId(R.id.list_receipts)).check(matches(isDisplayed())); + + onView(withId(R.id.list_receipts)) + .check(matches(atPosition(0, hasDescendant(withText("June 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, + hasDescendant(withText(com.hyperwallet.android.receipt.R.string.credit))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("Payment"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("+ 20.00"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("June 07, 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("USD"))))); + + onView(withId(R.id.list_receipts)).check(matches(atPosition(1, + hasDescendant(withText(com.hyperwallet.android.receipt.R.string.credit))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(1, hasDescendant(withText("Payment"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(1, hasDescendant(withText("+ 25.00"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(1, hasDescendant(withText("June 02, 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(1, hasDescendant(withText("CAD"))))); + + onView(withId(R.id.list_receipts)).check(matches(atPosition(2, + hasDescendant(withText(com.hyperwallet.android.receipt.R.string.debit))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(2, hasDescendant(withText("Card Activation Fee"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(2, hasDescendant(withText("- 1.95"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(2, hasDescendant(withText("June 01, 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(2, hasDescendant(withText("USD"))))); + + onView(withId(R.id.list_receipts)) + .check(matches(atPosition(3, hasDescendant(withText("December 2018"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(3, + hasDescendant(withText(com.hyperwallet.android.receipt.R.string.debit))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(3, hasDescendant(withText("Card Load"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(3, hasDescendant(withText("- 18.05"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(3, hasDescendant(withText("December 01, 2018"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(3, hasDescendant(withText("USD"))))); + + onView(withId(R.id.list_receipts)).check(new RecyclerViewCountAssertion(4)); + } + + @Test + public void testListReceipts_displayCreditTransaction() { + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager + .getResourceContent("receipt_credit_response.json")).mock(); + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock(); + + // run test + mActivityTestRule.launchActivity(null); + + // assert + onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar)))) + .check(matches(withText(R.string.title_activity_receipt_list))); + onView(withId(R.id.list_receipts)).check(matches(isDisplayed())); + + onView(withId(R.id.list_receipts)) + .check(matches(atPosition(0, hasDescendant(withText("June 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, + hasDescendant(withText(com.hyperwallet.android.receipt.R.string.credit))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText(R.string.payment))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("+ 25.00"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("June 02, 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("CAD"))))); + + onView(withId(R.id.list_receipts)).check(new RecyclerViewCountAssertion(1)); + } + + @Test + public void testListReceipts_displayDebitTransaction() { + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager + .getResourceContent("receipt_debit_response.json")).mock(); + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock(); + + // run test + mActivityTestRule.launchActivity(null); + + // assert + onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar)))) + .check(matches(withText(R.string.title_activity_receipt_list))); + onView(withId(R.id.list_receipts)).check(matches(isDisplayed())); + + onView(withId(R.id.list_receipts)) + .check(matches(atPosition(0, hasDescendant(withText("May 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, + hasDescendant(withText(com.hyperwallet.android.receipt.R.string.debit))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText(R.string.transfer_to_prepaid_card))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("- 18.05"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("May 02, 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("USD"))))); + + onView(withId(R.id.list_receipts)).check(new RecyclerViewCountAssertion(1)); + } + + @Test + public void testListReceipts_displayUnknownTransactionType() { + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager + .getResourceContent("receipt_unknown_type_response.json")).mock(); + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock(); + + // run test + mActivityTestRule.launchActivity(null); + + // assert + onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar)))) + .check(matches(withText(R.string.title_activity_receipt_list))); + onView(withId(R.id.list_receipts)).check(matches(isDisplayed())); + + onView(withId(R.id.list_receipts)) + .check(matches(atPosition(0, hasDescendant(withText("June 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, + hasDescendant(withText(com.hyperwallet.android.receipt.R.string.credit))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText(R.string.unknown_type))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("+ 25.00"))))); + onView(withId(R.id.list_receipts)).check( + matches(atPosition(0, hasDescendant(withText("June 02, 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("CAD"))))); + + onView(withId(R.id.list_receipts)).check(new RecyclerViewCountAssertion(1)); + } + + @Test + public void testListReceipt_userHasNoTransactions() { + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock(); + + // run test + mActivityTestRule.launchActivity(null); + + onView(withId(R.id.toolbar)).check(matches(isDisplayed())); + onView(allOf(instanceOf(TextView.class), withParent(withId(R.id.toolbar)))) + .check(matches(withText(R.string.title_activity_receipt_list))); + //todo: check empty view when it will be ready + } + + @Test + public void testListReceipts_checkDateTextOnLocaleChange() { + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_OK).withBody(sResourceManager + .getResourceContent("receipt_debit_response.json")).mock(); + mMockWebServer.mockResponse().withHttpResponseCode(HTTP_NO_CONTENT).withBody("").mock(); + + setLocale(Locale.ITALY); + // run test + mActivityTestRule.launchActivity(null); + // assert + onView(withId(R.id.list_receipts)) + .check(matches(atPosition(0, hasDescendant(withText("maggio 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("maggio 02, 2019"))))); + mActivityTestRule.finishActivity(); + setLocale(Locale.US); + mActivityTestRule.launchActivity(null); + onView(withId(R.id.list_receipts)) + .check(matches(atPosition(0, hasDescendant(withText("May 2019"))))); + onView(withId(R.id.list_receipts)).check(matches(atPosition(0, hasDescendant(withText("May 02, 2019"))))); + } + + private void setLocale(Locale locale) { + Context context = ApplicationProvider.getApplicationContext(); + Locale.setDefault(locale); + Resources resources = context.getResources(); + Configuration configuration = resources.getConfiguration(); + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.JELLY_BEAN) { + configuration.setLocale(locale); + } else { + configuration.locale = locale; + } + resources.updateConfiguration(configuration, resources.getDisplayMetrics()); + } +} diff --git a/receipt/src/test/java/com/hyperwallet/android/receipt/rule/HyperwalletExternalResourceManager.java b/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java similarity index 98% rename from receipt/src/test/java/com/hyperwallet/android/receipt/rule/HyperwalletExternalResourceManager.java rename to receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java index 0c5f6a743..e7e546063 100644 --- a/receipt/src/test/java/com/hyperwallet/android/receipt/rule/HyperwalletExternalResourceManager.java +++ b/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java @@ -1,4 +1,4 @@ -package com.hyperwallet.android.receipt.rule; +package com.hyperwallet.android.rule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; @@ -35,6 +35,7 @@ public String getResourceContent(final String resourceName) { } private String getContent(final String resourceName) { + URL resource = classLoader.getResource(resourceName); InputStream inputStream = null; Writer writer = new StringWriter(); diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletMockWebServer.java b/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletMockWebServer.java new file mode 100644 index 000000000..9f42f6128 --- /dev/null +++ b/receipt/src/androidTest/java/com/hyperwallet/android/rule/HyperwalletMockWebServer.java @@ -0,0 +1,115 @@ +package com.hyperwallet.android.rule; + +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; + +public final class HyperwalletMockWebServer extends TestWatcher { + + private MockWebServer mServer; + private int port; + + public HyperwalletMockWebServer(int port) { + this.port = port; + } + + @Override + protected void starting(Description description) { + super.starting(description); + mServer = new MockWebServer(); + try { + mServer.start(port); + } catch (IOException e) { + throw new IllegalStateException("Unable to start mock server", e); + } + } + + @Override + protected void finished(Description description) { + super.finished(description); + try { + mServer.shutdown(); + mServer.close(); + } catch (IOException e) { + throw new IllegalStateException("Un error occurred when shutting down mock server", e); + } + } + + public HyperwalletMockResponse mockResponse() { + return new Builder(mServer).build(); + } + + public MockWebServer getServer() { + return mServer; + } + + public static class HyperwalletMockResponse { + + private String path; + private String body; + private int httpResponseCode; + private Builder builder; + + HyperwalletMockResponse(Builder builder) { + this.path = builder.path; + this.httpResponseCode = builder.responseCode; + this.body = builder.body; + this.builder = builder; + } + + public HyperwalletMockResponse withHttpResponseCode(final int code) { + builder.responseCode(code); + return builder.build(); + } + + public HyperwalletMockResponse withBody(final String body) { + builder.body(body); + return builder.build(); + } + + public void mock() { + mockRequest(); + } + + private String mockRequest() { + builder.server.enqueue(new MockResponse().setResponseCode(httpResponseCode).setBody(body)); + return builder.server.url(path).toString(); + } + + } + + private static class Builder { + + private String path; + private String body; + private int responseCode; + private MockWebServer server; + + + Builder(final MockWebServer server) { + this.path = ""; + this.responseCode = HttpURLConnection.HTTP_OK; + this.body = ""; + this.server = server; + } + + Builder responseCode(final int code) { + this.responseCode = code; + return this; + } + + Builder body(final String body) { + this.body = body; + return this; + } + + HyperwalletMockResponse build() { + return new HyperwalletMockResponse(this); + } + } +} diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/util/EspressoUtils.java b/receipt/src/androidTest/java/com/hyperwallet/android/util/EspressoUtils.java new file mode 100644 index 000000000..92a711f96 --- /dev/null +++ b/receipt/src/androidTest/java/com/hyperwallet/android/util/EspressoUtils.java @@ -0,0 +1,187 @@ +package com.hyperwallet.android.util; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.test.espresso.ViewAction; +import androidx.test.espresso.action.ViewActions; +import androidx.test.espresso.matcher.BoundedMatcher; + +import com.google.android.material.textfield.TextInputLayout; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import java.util.Objects; + +public class EspressoUtils { + + public static Matcher withHint(final String expectedHint) { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + if (!(view instanceof TextInputLayout)) { + return false; + } + + String hint = Objects.toString(((TextInputLayout) view).getHint()); + return expectedHint.equals(hint); + } + + @Override + public void describeTo(Description description) { + description.appendText(expectedHint); + } + }; + } + + public static Matcher hasErrorText(final String expectedErrorMessage) { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + if (!(view instanceof TextInputLayout)) { + return false; + } + + String errorMessage = Objects.toString(((TextInputLayout) view).getError()); + return expectedErrorMessage.equals(errorMessage); + } + + @Override + public void describeTo(Description description) { + description.appendText(expectedErrorMessage); + } + }; + } + + public static Matcher hasErrorText(final int resourceId) { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + if (!(view instanceof TextInputLayout)) { + return false; + } + String expectedErrorMessage = view.getResources().getString(resourceId); + String errorMessage = Objects.toString(((TextInputLayout) view).getError()); + + return expectedErrorMessage.equals(errorMessage); + } + + @Override + public void describeTo(Description description) { + } + }; + } + + public static Matcher withDrawable(final int resourceId) { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + if (!(view instanceof ImageView)) { + return false; + } + + Drawable drawable = ((ImageView) view).getDrawable(); + if (drawable == null) { + return false; + } + Drawable expectedDrawable = view.getContext().getResources().getDrawable(resourceId); + + Bitmap bitmap = getBitmap(drawable); + Bitmap expectedBitmap = getBitmap(expectedDrawable); + + return bitmap.sameAs(expectedBitmap); + } + + @Override + public void describeTo(Description description) { + } + }; + } + + public static Matcher atPosition(final int position, @NonNull final Matcher matcher) { + return new BoundedMatcher(RecyclerView.class) { + + @Override + protected boolean matchesSafely(final RecyclerView view) { + RecyclerView.ViewHolder viewHolder = view.findViewHolderForAdapterPosition(position); + + if (viewHolder == null) { + return false; + } + + return matcher.matches(viewHolder.itemView); + } + + @Override + public void describeTo(Description description) { + description.appendText("has item at position " + position + ": "); + matcher.describeTo(description); + } + }; + } + + public static ViewAction nestedScrollTo() { + return ViewActions.actionWithAssertions(new NestedScrollToAction()); + } + + private static Bitmap getBitmap(Drawable drawable) { + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } + + public static Matcher hasNoErrorText() { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + if (!(view instanceof TextInputLayout)) { + return false; + } + return ((TextInputLayout) view).getError() == null; + } + + @Override + public void describeTo(Description description) { + description.appendText("has no error text: "); + } + }; + } + + public static Matcher hasEmptyText() { + return new TypeSafeMatcher() { + + @Override + public boolean matchesSafely(View view) { + if (!(view instanceof EditText)) { + return false; + } + String text = ((EditText) view).getText().toString(); + + return text.isEmpty(); + } + + @Override + public void describeTo(Description description) { + } + }; + } +} + diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/util/NestedScrollToAction.java b/receipt/src/androidTest/java/com/hyperwallet/android/util/NestedScrollToAction.java new file mode 100644 index 000000000..392867387 --- /dev/null +++ b/receipt/src/androidTest/java/com/hyperwallet/android/util/NestedScrollToAction.java @@ -0,0 +1,41 @@ +package com.hyperwallet.android.util; + +import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom; +import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA; +import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anyOf; + +import android.view.View; + +import androidx.core.widget.NestedScrollView; +import androidx.test.espresso.UiController; +import androidx.test.espresso.ViewAction; +import androidx.test.espresso.action.ScrollToAction; +import androidx.test.espresso.matcher.ViewMatchers; + +import org.hamcrest.Matcher; + +public class NestedScrollToAction implements ViewAction { + private static final String TAG = ScrollToAction.class.getSimpleName(); + + @SuppressWarnings("unchecked") + @Override + public Matcher getConstraints() { + return allOf( + withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), + isDescendantOfA( + anyOf(isAssignableFrom(NestedScrollView.class)))); + } + + @Override + public void perform(UiController uiController, View view) { + new ScrollToAction().perform(uiController, view); + } + + @Override + public String getDescription() { + return "scroll to"; + } +} diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/util/RecyclerViewCountAssertion.java b/receipt/src/androidTest/java/com/hyperwallet/android/util/RecyclerViewCountAssertion.java new file mode 100644 index 000000000..bb3aecaff --- /dev/null +++ b/receipt/src/androidTest/java/com/hyperwallet/android/util/RecyclerViewCountAssertion.java @@ -0,0 +1,30 @@ +package com.hyperwallet.android.util; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.ViewAssertion; + +public class RecyclerViewCountAssertion implements ViewAssertion { + private final int mCount; + + public RecyclerViewCountAssertion(int count) { + this.mCount = count; + } + + @Override + public void check(View view, NoMatchingViewException noViewFoundException) { + if (noViewFoundException != null) { + throw noViewFoundException; + } + + RecyclerView recyclerView = (RecyclerView) view; + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + + assertThat(adapter.getItemCount(), is(mCount)); + } +} diff --git a/receipt/src/androidTest/java/com/hyperwallet/android/util/TestAuthenticationProvider.java b/receipt/src/androidTest/java/com/hyperwallet/android/util/TestAuthenticationProvider.java new file mode 100644 index 000000000..686ccbf30 --- /dev/null +++ b/receipt/src/androidTest/java/com/hyperwallet/android/util/TestAuthenticationProvider.java @@ -0,0 +1,51 @@ +package com.hyperwallet.android.util; + +import com.hyperwallet.android.HyperwalletAuthenticationTokenListener; +import com.hyperwallet.android.HyperwalletAuthenticationTokenProvider; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.UUID; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class TestAuthenticationProvider implements HyperwalletAuthenticationTokenProvider { + + public static final MediaType JSON + = MediaType.get("application/json; charset=utf-8"); + private static final String mBaseUrl = "http://localhost:8080/rest/v3/users/{0}/authentication-token"; + private static final String mUserToken = "user_token"; + + @Override + public void retrieveAuthenticationToken(final HyperwalletAuthenticationTokenListener authenticationTokenListener) { + + OkHttpClient client = new OkHttpClient(); + + String payload = "{}"; + String baseUrl = MessageFormat.format(mBaseUrl, mUserToken); + + RequestBody body = RequestBody.create(JSON, payload); + Request request = new Request.Builder() + .url(baseUrl) + .post(body) + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + authenticationTokenListener.onFailure(UUID.randomUUID(), e.getMessage()); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + authenticationTokenListener.onSuccess(response.body().string()); + } + }); + } +} diff --git a/receipt/src/main/AndroidManifest.xml b/receipt/src/main/AndroidManifest.xml index 251bb4f88..3039f6d18 100644 --- a/receipt/src/main/AndroidManifest.xml +++ b/receipt/src/main/AndroidManifest.xml @@ -2,7 +2,9 @@ - + + + diff --git a/receipt/src/main/res/xml/network_security_config.xml b/receipt/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..5e4ba9c97 --- /dev/null +++ b/receipt/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + localhost + + \ No newline at end of file diff --git a/receipt/src/test/java/com/hyperwallet/android/receipt/repository/ReceiptDataSourceTest.java b/receipt/src/test/java/com/hyperwallet/android/receipt/repository/ReceiptDataSourceTest.java index efe3c4627..d09854710 100644 --- a/receipt/src/test/java/com/hyperwallet/android/receipt/repository/ReceiptDataSourceTest.java +++ b/receipt/src/test/java/com/hyperwallet/android/receipt/repository/ReceiptDataSourceTest.java @@ -27,7 +27,7 @@ import com.hyperwallet.android.model.paging.HyperwalletPageList; import com.hyperwallet.android.model.receipt.Receipt; import com.hyperwallet.android.model.receipt.ReceiptQueryParam; -import com.hyperwallet.android.receipt.rule.HyperwalletExternalResourceManager; +import com.hyperwallet.android.rule.HyperwalletExternalResourceManager; import org.hamcrest.Matchers; import org.json.JSONObject; diff --git a/receipt/src/test/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java b/receipt/src/test/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java new file mode 100644 index 000000000..9061af9b8 --- /dev/null +++ b/receipt/src/test/java/com/hyperwallet/android/rule/HyperwalletExternalResourceManager.java @@ -0,0 +1,72 @@ +package com.hyperwallet.android.rule; + +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URL; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HyperwalletExternalResourceManager extends TestWatcher { + + private static final String EMPTY = ""; + private ClassLoader classLoader; + private Logger logger; + + @Override + protected void starting(Description description) { + super.starting(description); + classLoader = description.getTestClass().getClassLoader(); + logger = Logger.getLogger(description.getTestClass().getName()); + } + + public String getResourceContent(final String resourceName) { + if (resourceName == null) { + throw new IllegalArgumentException("Parameter resourceName cannot be null"); + } + + return getContent(resourceName); + } + + private String getContent(final String resourceName) { + URL resource = classLoader.getResource(resourceName); + InputStream inputStream = null; + Writer writer = new StringWriter(); + String resourceContent = EMPTY; + if (resource != null) { + try { + inputStream = resource.openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); + String line = reader.readLine(); + while (line != null) { + writer.write(line); + line = reader.readLine(); + } + resourceContent = writer.toString(); + + } catch (Exception e) { + logger.log(Level.WARNING, "There was an error loading an external resource", e); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "There was an error closing input stream", e); + } + try { + writer.close(); + } catch (IOException e) { + logger.log(Level.SEVERE, "There was an error closing writer", e); + } + } + } + return resourceContent; + } +} diff --git a/receipt/src/test/resources/authentication_token_response.json b/receipt/src/test/resources/authentication_token_response.json new file mode 100644 index 000000000..e8e41bf37 --- /dev/null +++ b/receipt/src/test/resources/authentication_token_response.json @@ -0,0 +1,3 @@ +{ + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c3ItZjZlNmZjY2EtNTBmNy00ZWY1LWExYzUtNWZmMDJlMDU2MzgzIiwiaWF0IjoxNTQ5NTgwMzk0LCJleHAiOjI1NDk1ODA5OTQsImF1ZCI6InBndS03YTEyMzJlOC0xNDc5LTQzNzAtOWY1NC03ODc1ZjdiMTg2NmMiLCJpc3MiOiJwcmctY2NhODAyNWUtODVhMy0xMWU2LTg2MGEtNThhZDVlY2NlNjFkIiwicmVzdC11cmkiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVzdC92My8iLCJncmFwaHFsLXVyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9ncmFwaHFsIn0.kILSynYHbepbl4sVqENnNog09iGByfTrckHhSCjVgnuRnuspI72cx3rt0SB2V_neHwzYkD_VfhNKk9gJDOwXeQ" +} \ No newline at end of file diff --git a/receipt/src/test/resources/receipt_credit_response.json b/receipt/src/test/resources/receipt_credit_response.json new file mode 100644 index 000000000..4351d17a2 --- /dev/null +++ b/receipt/src/test/resources/receipt_credit_response.json @@ -0,0 +1,30 @@ +{ + "count": 1, + "offset": 0, + "limit": 10, + "data": [ + { + "journalId": "3051581", + "type": "PAYMENT", + "createdOn": "2019-06-02T17:09:07", + "entry": "CREDIT", + "sourceToken": "act-12345", + "destinationToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d", + "amount": "25.00", + "fee": "0.00", + "currency": "CAD", + "details": { + "clientPaymentId": "ABC1234", + "payeeName": "A Person" + } + } + ], + "links": [ + { + "params": { + "rel": "self" + }, + "href": "https://api.sandbox.hyperwallet.com/rest/v3/users/usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d/receipts?offset=0&limit=10" + } + ] +} \ No newline at end of file diff --git a/receipt/src/test/resources/receipt_debit_response.json b/receipt/src/test/resources/receipt_debit_response.json new file mode 100644 index 000000000..8ed756455 --- /dev/null +++ b/receipt/src/test/resources/receipt_debit_response.json @@ -0,0 +1,25 @@ +{ + "count": 1, + "offset": 0, + "limit": 10, + "data": [ + { + "journalId": "3051590", + "type": "TRANSFER_TO_PREPAID_CARD", + "createdOn": "2019-05-02T17:12:18", + "entry": "DEBIT", + "sourceToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d", + "destinationToken": "trm-12345", + "amount": "18.05", + "currency": "USD" + } + ], + "links": [ + { + "params": { + "rel": "self" + }, + "href": "https://api.sandbox.hyperwallet.com/rest/v3/users/usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d/receipts?offset=0&limit=10" + } + ] +} \ No newline at end of file diff --git a/receipt/src/test/resources/receipt_list_response.json b/receipt/src/test/resources/receipt_list_response.json new file mode 100644 index 000000000..7e7cc58af --- /dev/null +++ b/receipt/src/test/resources/receipt_list_response.json @@ -0,0 +1,64 @@ +{ + "count": 4, + "offset": 0, + "limit": 10, + "data": [ + { + "journalId": "3051579", + "type": "PAYMENT", + "createdOn": "2019-06-07T17:08:58", + "entry": "CREDIT", + "sourceToken": "act-12345", + "destinationToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d", + "amount": "20.00", + "fee": "0.00", + "currency": "USD", + "details": { + "clientPaymentId": "8OxXefx5", + "payeeName": "A Person" + } + }, + { + "journalId": "3051581", + "type": "PAYMENT", + "createdOn": "2019-06-02T16:09:07", + "entry": "CREDIT", + "sourceToken": "act-12345", + "destinationToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d", + "amount": "25.00", + "fee": "0.00", + "currency": "CAD", + "details": { + "clientPaymentId": "Q3SVvpv0", + "payeeName": "A Person" + } + }, + { + "journalId": "3051582", + "type": "CARD_ACTIVATION_FEE", + "createdOn": "2019-06-01T11:09:16", + "entry": "DEBIT", + "sourceToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d", + "amount": "1.95", + "currency": "USD" + }, + { + "journalId": "3051590", + "type": "TRANSFER_TO_PREPAID_CARD", + "createdOn": "2018-12-01T17:12:18", + "entry": "DEBIT", + "sourceToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d", + "destinationToken": "trm-12345", + "amount": "18.05", + "currency": "USD" + } + ], + "links": [ + { + "params": { + "rel": "self" + }, + "href": "https://api.sandbox.hyperwallet.com/rest/v3/users/usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d/receipts?offset=0&limit=10" + } + ] +} \ No newline at end of file diff --git a/receipt/src/test/resources/receipt_unknown_type_response.json b/receipt/src/test/resources/receipt_unknown_type_response.json new file mode 100644 index 000000000..bf856ed63 --- /dev/null +++ b/receipt/src/test/resources/receipt_unknown_type_response.json @@ -0,0 +1,30 @@ +{ + "count": 1, + "offset": 0, + "limit": 10, + "data": [ + { + "journalId": "3051581", + "type": "ICK", + "createdOn": "2019-06-02T17:09:07", + "entry": "CREDIT", + "sourceToken": "act-12345", + "destinationToken": "usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d", + "amount": "25.00", + "fee": "0.00", + "currency": "CAD", + "details": { + "clientPaymentId": "ABC1234", + "payeeName": "A Person" + } + } + ], + "links": [ + { + "params": { + "rel": "self" + }, + "href": "https://api.sandbox.hyperwallet.com/rest/v3/users/usr-fa76a738-f43d-48b9-9a7a-7048d44a5d2d/receipts?offset=0&limit=10" + } + ] +} \ No newline at end of file diff --git a/ui/src/androidTest/AndroidManifest.xml b/ui/src/androidTest/AndroidManifest.xml index 9b2c52c35..0cd133def 100644 --- a/ui/src/androidTest/AndroidManifest.xml +++ b/ui/src/androidTest/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="com.hyperwallet.android.hyperwallet_ui.test">