package org.edx.mobile.event; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.gson.internal.bind.util.ISO8601Utils; import org.edx.mobile.BuildConfig; import org.edx.mobile.http.NewVersionBroadcastInterceptor; import org.edx.mobile.test.BaseTestCase; import org.edx.mobile.util.Version; import org.junit.AssumptionViolatedException; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.mockito.InOrder; import org.robolectric.RuntimeEnvironment; import java.io.IOException; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; import java.util.List; import de.greenrobot.event.EventBus; import okhttp3.Interceptor; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import static org.assertj.core.api.Java6Assertions.assertThat; import static org.edx.mobile.http.HttpStatus.ACCEPTED; import static org.edx.mobile.http.HttpStatus.UPGRADE_REQUIRED; import static org.edx.mobile.http.NewVersionBroadcastInterceptor.HEADER_APP_LATEST_VERSION; import static org.edx.mobile.http.NewVersionBroadcastInterceptor.HEADER_APP_VERSION_LAST_SUPPORTED_DATE; import static org.edx.mobile.test.util.TimeUtilsForTests.DEFAULT_TIME; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; /** * Tests for verifying implementation correctness of {@link NewVersionAvailableEvent}, and the * modules that post it (to ensure that they're respecting it's priority ordering while posting it), * such as {@link NewVersionBroadcastInterceptor}. */ /* TODO: Once a custom Snackbar shadow is implemented, UI tests should be written to test the event * bus registration for this event, and the subsequent display of the appropriate Snackbar * notification. */ public class NewVersionAvailableEventTest extends BaseTestCase { /** * Verify that constructing a new instance of {@link NewVersionAvailableEvent} with empty * parameters (@{code null} or {@code false}) throws an {@link IllegalArgumentException}. */ @Test(expected = IllegalArgumentException.class) public void testNewInstance_withNoInput_throwsException() { new NewVersionAvailableEvent(null, null, false); } /** * Verify that constructing a new instance of {@link NewVersionAvailableEvent}, with the current * version number provided as the new version, and empty values (@{code null} or {@code false}) * provided for all the other parameters, throws an {@link IllegalArgumentException}. * * @throws ParseException If the current version doesn't correspond to the schema. */ @Test(expected = IllegalArgumentException.class) public void testNewInstance_withOnlyCurrentVersionInput_throwsException() throws ParseException { new NewVersionAvailableEvent(getVersionOffset(0), null, false); } /** * Verify that constructing a new instance of {@link NewVersionAvailableEvent}, with an older * version number provided as the new version, and empty values (@{code null} or {@code false}) * provided for all the other parameters, throws an {@link IllegalArgumentException}. * * @throws ParseException If the current version doesn't correspond to the schema. */ @Test(expected = IllegalArgumentException.class) public void testNewInstance_withOnlyOldVersionInput_throwsException() throws ParseException { new NewVersionAvailableEvent(getVersionOffset(-1), null, false); } /** * Verify that attempting to post a {@link NewVersionAvailableEvent} instance through the * {@link NewVersionAvailableEvent#post(Version, Date, boolean)} method with empty parameters * (@{code null} or {@code false}) doesn't do anything. */ @Test public void testPost_withNoInput_doesNothing() { assertNull(postAndRemoveEvent(null, null, false)); } /** * Verify that attempting to post a {@link NewVersionAvailableEvent} instance through the * {@link NewVersionAvailableEvent#post(Version, Date, boolean)} method, with the current * version number provided as the new version, and empty values (@{code null} or {@code false}) * provided for all the other parameters, doesn't do anything. * * @throws ParseException If the current version doesn't correspond to the schema. */ @Test public void testPost_withOnlyCurrentVersionInput_doesNothing() throws ParseException { assertNull(postAndRemoveEvent(getVersionOffset(0), null, false)); } /** * Verify that attempting to post a {@link NewVersionAvailableEvent} instance through the * {@link NewVersionAvailableEvent#post(Version, Date, boolean)} method, with an older * version number provided as the new version, and empty values (@{code null} or {@code false}) * provided for all the other parameters, doesn't do anything. * * @throws ParseException If the current version doesn't correspond to the schema. */ @Test public void testPost_withOnlyOldVersionInput__doesNothing() throws ParseException { assertNull(postAndRemoveEvent(getVersionOffset(-1), null, false)); } /** * Verify that attempting to post a {@link NewVersionAvailableEvent} instance by passing the * appropriate headers and status code in a request chain through an * {@link NewVersionBroadcastInterceptor} with empty parameters (@{code null} or {@code false}) * doesn't do anything. */ @Test public void testPostFromInterceptor_withNoInput_doesNothing() throws IOException { assertNull(postAndRemoveEventFromInterceptor(null, null, false)); } /** * Verify that attempting to post a {@link NewVersionAvailableEvent} instance by passing the * appropriate headers and status code in a request chain through an * {@link NewVersionBroadcastInterceptor}, with the current version number provided as the new * version, and empty values (@{code null} or {@code false}) provided for all the other * parameters, doesn't do anything. */ @Test public void testPostFromInterceptor_withOnlyCurrentVersionInput_doesNothing() throws IOException, ParseException { assertNull(postAndRemoveEventFromInterceptor(getVersionOffset(0), null, false)); } /** * Verify that attempting to post a {@link NewVersionAvailableEvent} instance by passing the * appropriate headers and status code in a request chain through an * {@link NewVersionBroadcastInterceptor}, with an older version number provided as the new * version, and empty values (@{code null} or {@code false}) provided for all the other * parameters, doesn't do anything. */ @Test public void testPostFromInterceptor_withOnlyOldVersionInput__doesNothing() throws IOException, ParseException { assertNull(postAndRemoveEventFromInterceptor(getVersionOffset(-1), null, false)); } /** * Tests for verifying implementation correctness on all valid construction parameter * permutations of {@link NewVersionAvailableEvent}, and on the modules that post it, such as * {@link NewVersionBroadcastInterceptor}. */ @RunWith(Parameterized.class) public static class PermutationsValidityTest { /** * @return A list of all the parameter permutations possible for constructing a valid * {@link NewVersionAvailableEvent}, on which the tests will be iterated. * * @throws ParseException If the current version doesn't correspond to the schema. */ @Parameters(name = "{index}: new version = {0}, last supported date = {1}, " + "unsupported = {2}") @NonNull public static Iterable<Object[]> getParameterPermutations() throws ParseException { final List<Object[]> data = new ArrayList<>(); final Version currentVersion = new Version(BuildConfig.VERSION_NAME); for (final Version newVersion : new Version[] { getVersionOffset(1), currentVersion, getVersionOffset(-1), null }) { for (final Date lastSupportedDate : new Date[] { new Date(DEFAULT_TIME), null }) { for (final boolean isUnsupported : new boolean[] { true, false }) { if (isUnsupported || lastSupportedDate != null || (newVersion != null && newVersion.compareTo(currentVersion) > 0)) { data.add(new Object[] { newVersion, lastSupportedDate, isUnsupported }); } } } } return data; } /** * The new version; */ @Nullable private final Version newVersion; /** * The last supported date. */ @Nullable private final Date lastSupportedDate; /** * Whether the current version is unsupported. */ private final boolean isUnsupported; /** * Construct a new iteration of the test suite with a specific permutation of the parameters * for constructing a {@link NewVersionAvailableEvent} instance. */ public PermutationsValidityTest(@Nullable final Version newVersion, @Nullable final Date lastSupportedDate, final boolean isUnsupported) { this.newVersion = newVersion; // Don't reuse a single Date instance across tests, as it's not immutable. this.lastSupportedDate = lastSupportedDate == null ? null : (Date) lastSupportedDate.clone(); this.isUnsupported = isUnsupported; } /** * Verify that all the getters of {@link NewVersionAvailableEvent} correctly return the data * that was provided to it on instantiation. */ @Test public void testGetters_returnCorrectValues() { final NewVersionAvailableEvent event = new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported); assertEquals(newVersion, event.getNewVersion()); assertEquals(lastSupportedDate, event.getLastSupportedDate()); assertEquals(isUnsupported, event.isUnsupported()); } /** * Verify that the {@link NewVersionAvailableEvent#equals(Object)} method returns * {@code true} if passed another instance created with an identical (but different) set of * parameters. * * @throws ParseException If the current version doesn't correspond to the schema. */ @Test public void testEquals_withIdenticalInstance_isTrue() throws ParseException { final NewVersionAvailableEvent event = new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported); final NewVersionAvailableEvent clone = new NewVersionAvailableEvent( newVersion == null ? null : new Version(newVersion.toString()), lastSupportedDate == null ? null : (Date) lastSupportedDate.clone(), isUnsupported); assertEquals(event, clone); } /** * Verify that the {@link NewVersionAvailableEvent#compareTo(NewVersionAvailableEvent)} * method returns zero if passed another instance created with an identical (but different) * set of parameters. */ @Test public void testCompare_withIdenticalInstance_isEqual() { final NewVersionAvailableEvent event = new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported); assertThat(event).isEqualByComparingTo(new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported)); } /** * Verify that the {@link NewVersionAvailableEvent#isConsumed} method returns {@code false} * immediately after initialization. */ @Test public void testIsConsumed_atInitialization_isFalse() { final NewVersionAvailableEvent event = new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported); assertFalse(event.isConsumed()); } /** * Verify that the {@link NewVersionAvailableEvent#isConsumed} method returns {@code true} * after being marked as consumed via the {@link NewVersionAvailableEvent#markAsConsumed()} * method. */ @Test public void testIsConsumed_afterMarkingAsConsumed_isTrue() { final NewVersionAvailableEvent event = new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported); event.markAsConsumed(); assertTrue(event.isConsumed()); } /** * Verify that the {@link NewVersionAvailableEvent#getNotificationString(Context)} returns a * non-null value. */ // TODO: Include this once we have a Robolectric parameterized test runner. @Ignore @Test public void testGetNotificationString_returnsNonNull() { final NewVersionAvailableEvent event = new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported); assertNotNull(event.getNotificationString(RuntimeEnvironment.application)); } /** * Verify that the {@link NewVersionAvailableEvent#post(Version, Date, boolean)} method * posts an instance of {@link NewVersionAvailableEvent} on the event bus as a sticky event, * and with the same data that was passed to it. */ @Test public void testPost_postsEventOnBus() { final NewVersionAvailableEvent event = postAndRemoveEvent( newVersion, lastSupportedDate, isUnsupported); assertEquals(event, new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported)); } /** * Verify that the {@link NewVersionBroadcastInterceptor} posts an instance of * {@link NewVersionAvailableEvent} on the event bus as a sticky event, and with the same * data that was provided in the headers and the status code in it's request chain. */ @Test public void testPostFromInterceptor_postsEventOnBus() throws IOException { final NewVersionAvailableEvent event = postAndRemoveEventFromInterceptor( newVersion, lastSupportedDate, isUnsupported); assertEquals(event, new NewVersionAvailableEvent( newVersion, lastSupportedDate, isUnsupported)); } } /** * Tests for verifying implementation correctness of the comparison logic using all valid non- * identical permutations of the combination of two sets of parameters for * {@link NewVersionAvailableEvent}, on the event class itself, and also the modules that post * it, such as {@link NewVersionBroadcastInterceptor}. */ @RunWith(Parameterized.class) public static class ComparisonTests { /** * @return A list of all valid permutations containing two instances of * {@link NewVersionAvailableEvent} that were created with a different set of parameters, on * which the comparison tests will be iterated. The first item will always be of a higher * priority (and comparison order) than the second one. * * @throws ParseException If the current version doesn't correspond to the schema. */ @Parameters(name = "{index}: higher priority event = {0}, lower priority event = {1}") @NonNull public static Iterable<Object[]> getDifferentParametersPermutations() throws ParseException { final List<Object[]> data = new ArrayList<>(); final Version[] newVersionsSet = new Version[] { getVersionOffset(2), getVersionOffset(1), null }; final Date[] lastSupportedDatesSet = new Date[] { new Date(DEFAULT_TIME), new Date(DEFAULT_TIME + 1), null }; final boolean[] isUnsupportedValuesSet = new boolean[] { true, false }; /* Add all the valid permutations where the event has higher priority based on the new * version parameter. Since this is the parameter that is compared last, all the other * parameters must be the same on both event sets here. */ for (final Date lastSupportedDate : lastSupportedDatesSet) { for (final boolean isUnsupported : isUnsupportedValuesSet) { for (int newVersionIndex = 0; newVersionIndex < newVersionsSet.length - 1; newVersionIndex++) { if (isUnsupported || lastSupportedDate != null || (newVersionsSet[newVersionIndex] != null && newVersionsSet[newVersionIndex + 1] != null)) { data.add(new Object[] { new NewVersionAvailableEvent( newVersionsSet[newVersionIndex], // Don't reuse a single Date // instance, as it's not immutable. lastSupportedDate == null ? null : (Date) lastSupportedDate.clone(), isUnsupported), new NewVersionAvailableEvent( newVersionsSet[newVersionIndex + 1], // Don't reuse a single Date // instance, as it's not immutable. lastSupportedDate == null ? null : (Date) lastSupportedDate.clone(), isUnsupported) }); } } } } /* Add all the valid permutations where the event has higher priority based on the last * supported date parameter. The new version parameter is compared after this one, so * it's permutations are not restricted here. The unsupported flag parameter is * compared at the start, so it must be the same on both event sets here. */ for (final boolean isUnsupported : isUnsupportedValuesSet) { for (int lastSupportedDateIndex = 0; lastSupportedDateIndex < lastSupportedDatesSet.length - 1; lastSupportedDateIndex++) { for (int newVersionIndex = 0; newVersionIndex < newVersionsSet.length - 1; newVersionIndex++) { if (isUnsupported || (lastSupportedDatesSet[lastSupportedDateIndex] != null && lastSupportedDatesSet[lastSupportedDateIndex + 1] != null) || (newVersionsSet[newVersionIndex] != null && newVersionsSet[newVersionIndex + 1] != null)) { for (final boolean reverseNewVersionIndexComparison : new boolean[] { false, true }) { data.add(new Object[] { new NewVersionAvailableEvent( newVersionsSet[reverseNewVersionIndexComparison ? newVersionIndex + 1 : newVersionIndex], // Don't reuse a single Date // instance, as it's not immutable. lastSupportedDatesSet[lastSupportedDateIndex] == null ? null : (Date) lastSupportedDatesSet[ lastSupportedDateIndex].clone(), isUnsupported), new NewVersionAvailableEvent( newVersionsSet[reverseNewVersionIndexComparison ? newVersionIndex : newVersionIndex + 1], // Don't reuse a single Date // instance, as it's not immutable. lastSupportedDatesSet[ lastSupportedDateIndex + 1] == null ? null : (Date) lastSupportedDatesSet[ lastSupportedDateIndex + 1].clone(), isUnsupported) }); } } } } } /* Add all the valid permutations where the event has higher priority based on the * unsupported flag parameter. This is the first parameter that is compared, so all the * other parameter permutations are completely unrestricted here. */ for (int isUnsupportedIndex = 0; isUnsupportedIndex < isUnsupportedValuesSet.length - 1; isUnsupportedIndex++) { for (int lastSupportedDateIndex = 0; lastSupportedDateIndex < lastSupportedDatesSet.length - 1; lastSupportedDateIndex++) { for (int newVersionIndex = 0; newVersionIndex < newVersionsSet.length - 1; newVersionIndex++) { if ((isUnsupportedValuesSet[isUnsupportedIndex] && isUnsupportedValuesSet[isUnsupportedIndex + 1]) || (lastSupportedDatesSet[lastSupportedDateIndex] != null && lastSupportedDatesSet[lastSupportedDateIndex + 1] != null) || (newVersionsSet[newVersionIndex] != null && newVersionsSet[newVersionIndex + 1] != null)) { for (final boolean reverseLastSupportedDateComparison : new boolean[] { false, true }) { for (final boolean reverseNewVersionComparison : new boolean[] { false, true }) { data.add(new Object[] { new NewVersionAvailableEvent(newVersionsSet[ reverseNewVersionComparison ? newVersionIndex + 1 : newVersionIndex], // Don't reuse a single Date // instance, as it's not immutable. lastSupportedDatesSet[ reverseLastSupportedDateComparison ? lastSupportedDateIndex + 1 : lastSupportedDateIndex] == null ? null : (Date) lastSupportedDatesSet[ reverseLastSupportedDateComparison ? lastSupportedDateIndex + 1 : lastSupportedDateIndex].clone(), isUnsupportedValuesSet[isUnsupportedIndex]), new NewVersionAvailableEvent(newVersionsSet[ reverseNewVersionComparison ? newVersionIndex : newVersionIndex + 1], // Don't reuse a single Date // instance, as it's not immutable. lastSupportedDatesSet[ reverseLastSupportedDateComparison ? lastSupportedDateIndex : lastSupportedDateIndex + 1] == null ? null : (Date) lastSupportedDatesSet[ reverseLastSupportedDateComparison ? lastSupportedDateIndex : lastSupportedDateIndex + 1] .clone(), isUnsupportedValuesSet[isUnsupportedIndex + 1]), }); } } } } } } return data; } /** * The higher priority event instance. */ @NonNull private final NewVersionAvailableEvent higherPriorityEvent; /** * The lower priority event instance. */ @NonNull private final NewVersionAvailableEvent lowerPriorityEvent; /** * Construct a new iteration of the test suite with a specific permutation of the set of two * {@link NewVersionAvailableEvent} instances with variable parameters. * * @param higherPriorityEvent The higher priority instance. * @param lowerPriorityEvent The lower priority instance. */ public ComparisonTests(@NonNull NewVersionAvailableEvent higherPriorityEvent, @NonNull NewVersionAvailableEvent lowerPriorityEvent) { this.higherPriorityEvent = higherPriorityEvent; this.lowerPriorityEvent = lowerPriorityEvent; } /** * Verify that the {@link NewVersionAvailableEvent#compareTo(NewVersionAvailableEvent)} * method returns a positive integer if passed another instance that is of lower priority. */ @Test public void testCompare_withLowerPriorityInstance_isGreaterThan() { assertThat(higherPriorityEvent).isGreaterThan(lowerPriorityEvent); } /** * Verify that the {@link NewVersionAvailableEvent#compareTo(NewVersionAvailableEvent)} * method returns a negative integer if passed another instance that is of higher priority. */ @Test public void testCompare_withHigherPriorityInstance_isLessThan() { assertThat(lowerPriorityEvent).isLessThan(higherPriorityEvent); } /** * Verify that the {@link NewVersionAvailableEvent#post(Version, Date, boolean)} method * doesn't do anything when provided parameters that resolve into a lower priority event * than one that has already been posted. */ @Test public void testPost_lowerPriorityEvent_doesNothing() { assertNotEquals(higherPriorityEvent, lowerPriorityEvent); // This will throw an AssumptionViolatedException if the Android runtime // isn't loaded (i.e. through using the Robolectric test runner). final EventBus eventBus = getEventBus(); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); assertEquals(higherPriorityEvent, postEvent(higherPriorityEvent)); assertEquals(higherPriorityEvent, postEvent(lowerPriorityEvent)); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); } /** * Verify that the {@link NewVersionAvailableEvent#post(Version, Date, boolean)} method * posts an instance of {@link NewVersionAvailableEvent} on the event bus as a sticky event * correctly, when provided parameters that resolve into a higher priority event than one * that has already been posted. */ @Test public void testPost_higherPriorityEvent_postsEventOnBus() { assertNotEquals(higherPriorityEvent, lowerPriorityEvent); // This will throw an AssumptionViolatedException if the Android runtime // isn't loaded (i.e. through using the Robolectric test runner). final EventBus eventBus = getEventBus(); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); assertEquals(lowerPriorityEvent, postEvent(lowerPriorityEvent)); assertEquals(higherPriorityEvent, postEvent(higherPriorityEvent)); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); } /** * Verify that the {@link NewVersionBroadcastInterceptor} doesn't do anything when provided * data (headers and status code) in it's request chain that resolve into a lower priority * event than one that has already been posted. */ @Test public void testPostFromInterceptor_lowerPriorityEvent_doesNothing() throws IOException { assertNotEquals(higherPriorityEvent, lowerPriorityEvent); // This will throw an AssumptionViolatedException if the Android runtime // isn't loaded (i.e. through using the Robolectric test runner). final EventBus eventBus = getEventBus(); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); final NewVersionBroadcastInterceptor interceptor = new NewVersionBroadcastInterceptor(); assertEquals(higherPriorityEvent, postEventFromInterceptor( interceptor, higherPriorityEvent)); assertEquals(higherPriorityEvent, postEventFromInterceptor( interceptor, lowerPriorityEvent)); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); } /** * Verify that the {@link NewVersionBroadcastInterceptor} posts an instance of * {@link NewVersionAvailableEvent} on the event bus as a sticky event correctly, when * provided data (headers and status code) in it's request chain that resolve into a higher * priority event than one that has already been posted. */ @Test public void testPostFromInterceptor_higherPriorityEvent_postsEventOnBus() throws IOException { assertNotEquals(higherPriorityEvent, lowerPriorityEvent); // This will throw an AssumptionViolatedException if the Android runtime // isn't loaded (i.e. through using the Robolectric test runner). final EventBus eventBus = getEventBus(); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); final NewVersionBroadcastInterceptor interceptor = new NewVersionBroadcastInterceptor(); assertEquals(lowerPriorityEvent, postEventFromInterceptor( interceptor, lowerPriorityEvent)); assertEquals(higherPriorityEvent, postEventFromInterceptor( interceptor, higherPriorityEvent)); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); } /** * Post the provided event's data via the * {@link NewVersionAvailableEvent#post(Version, Date, boolean)} method. * * @param event The event whose data is to be posted. * @return The event instance that was actually posted. */ @NonNull private static NewVersionAvailableEvent postEvent( @NonNull final NewVersionAvailableEvent event) { // Since we already have the event generated, we can be sure that // the data is valid, and should in fact be posted. //noinspection ConstantConditions return NewVersionAvailableEventTest.postEvent(event.getNewVersion(), event.getLastSupportedDate(), event.isUnsupported()); } /** * Post the provided event's data by passing the appropriate headers and status code in a * request chain through an {@link NewVersionBroadcastInterceptor}. * * @param event The event whose data is to be posted. * @return The event instance that was actually posted. */ @NonNull private static NewVersionAvailableEvent postEventFromInterceptor( @NonNull final NewVersionBroadcastInterceptor interceptor, @NonNull final NewVersionAvailableEvent event) throws IOException { // Since we already have the event generated, we can be sure that // the data is valid, and should in fact be posted. //noinspection ConstantConditions return NewVersionAvailableEventTest.postEventFromInterceptor(interceptor, event.getNewVersion(), event.getLastSupportedDate(), event.isUnsupported()); } } /** * Post the provided data via the {@link NewVersionAvailableEvent#post(Version, Date, boolean)} * method. * * @param newVersion The new version. * @param lastSupportedDate The last supported date. * @param isUnsupported Whether the current version is unsupported. * @return The posted event. This can be null if the data does not constitute a valid event. */ @Nullable private static NewVersionAvailableEvent postEvent(@Nullable final Version newVersion, @Nullable final Date lastSupportedDate, final boolean isUnsupported) { // This will throw an AssumptionViolatedException if the Android runtime // isn't loaded (i.e. through using the Robolectric test runner). final EventBus eventBus = getEventBus(); NewVersionAvailableEvent.post(newVersion, lastSupportedDate, isUnsupported); return eventBus.getStickyEvent(NewVersionAvailableEvent.class); } /** * Post the provided data via the {@link NewVersionAvailableEvent#post(Version, Date, boolean)} * method, then remove the sticky event from the event bus. * * @param newVersion The new version. * @param lastSupportedDate The last supported date. * @param isUnsupported Whether the current version is unsupported. * @return The event that was posted. This can be null if the data does not constitute a valid * event. */ @Nullable private static NewVersionAvailableEvent postAndRemoveEvent( @Nullable final Version newVersion, @Nullable final Date lastSupportedDate, final boolean isUnsupported) { // This will throw an AssumptionViolatedException if the Android runtime // isn't loaded (i.e. through using the Robolectric test runner). final EventBus eventBus = getEventBus(); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); final NewVersionAvailableEvent event = postEvent(newVersion, lastSupportedDate, isUnsupported); if (event != null) { assertTrue(eventBus.removeStickyEvent(event)); } return event; } /** * Post the provided data by passing the appropriate headers and status code in a request chain * through an {@link NewVersionBroadcastInterceptor}. * * @param interceptor The interceptor through which the data is to be posted. * @param newVersion The new version. * @param lastSupportedDate The last supported date. * @param isUnsupported Whether the current version is unsupported. * @return The posted event. This can be null if the data does not constitute a valid event. */ @Nullable private static NewVersionAvailableEvent postEventFromInterceptor( @NonNull final NewVersionBroadcastInterceptor interceptor, @Nullable final Version newVersion, @Nullable final Date lastSupportedDate, final boolean isUnsupported) throws IOException { // This will throw an AssumptionViolatedException if the Android runtime // isn't loaded (i.e. through using the Robolectric test runner). final EventBus eventBus = getEventBus(); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); final Interceptor.Chain chain = mock(Interceptor.Chain.class); final Request request = new Request.Builder() .url("https://localhost:1/") .build(); final Response response; { final Response.Builder responseBuilder = new Response.Builder(); responseBuilder.request(request); responseBuilder.protocol(Protocol.HTTP_1_1); responseBuilder.code(isUnsupported ? UPGRADE_REQUIRED : ACCEPTED); if (newVersion != null) { responseBuilder.header(HEADER_APP_LATEST_VERSION, newVersion.toString()); } if (lastSupportedDate != null) { responseBuilder.header(HEADER_APP_VERSION_LAST_SUPPORTED_DATE, ISO8601Utils.format(lastSupportedDate, true)); } response = responseBuilder.build(); } when(chain.request()).thenReturn(request); when(chain.proceed(request)).thenReturn(response); interceptor.intercept(chain); final InOrder inOrder = inOrder(chain); inOrder.verify(chain).request(); inOrder.verify(chain).proceed(request); verifyNoMoreInteractions(chain); return eventBus.getStickyEvent(NewVersionAvailableEvent.class); } /** * Post the provided data by passing the appropriate headers and status code in a request chain * through an {@link NewVersionBroadcastInterceptor}, then remove the sticky event from the * event bus. * * @param newVersion The new version. * @param lastSupportedDate The last supported date. * @param isUnsupported Whether the current version is unsupported. * @return The event that was posted. This can be null if the data does not constitute a valid * event. */ @Nullable private static NewVersionAvailableEvent postAndRemoveEventFromInterceptor( @Nullable final Version newVersion, @Nullable final Date lastSupportedDate, final boolean isUnsupported) throws IOException { // This will throw an AssumptionViolatedException if the Android runtime // isn't loaded (i.e. through using the Robolectric test runner). final EventBus eventBus = getEventBus(); eventBus.removeStickyEvent(NewVersionAvailableEvent.class); final NewVersionAvailableEvent event = postEventFromInterceptor( new NewVersionBroadcastInterceptor(), newVersion, lastSupportedDate, isUnsupported); if (event != null) { assertTrue(eventBus.removeStickyEvent(event)); } return event; } /** * Get the event bus that's being used in the app. * * @return The event bus. * @throws AssumptionViolatedException If the default event bus can't be constructed because of * the Android framework not being loaded. This will stop the calling tests from being reported * as failures. */ @NonNull private static EventBus getEventBus() { try { return EventBus.getDefault(); } catch (RuntimeException e) { /* The event bus uses the Looper from the Android framework, so * initializing it would throw a runtime exception if the * framework is not loaded. Nor can RoboGuice be used to inject * a mocked instance to get around this issue, since customizing * RoboGuice injections requires a Context. * * Robolectric can't be used to solve this issue, because it * doesn't support parameterized testing. The only solution that * could work at the moment would be to make this an * instrumented test suite. * * TODO: Mock the event bus when RoboGuice is replaced with * another dependency injection framework, or when there is a * Robolectric test runner available that support parameterized * tests. */ throw new AssumptionViolatedException( "Event bus requires Android framework", e, nullValue()); } } /** * Construct and return a {@link Version} instance at the specified offset from the current * build's version number. * * @param offset The offset from the current build's version. * @return The version at the specified offset. * @throws ParseException If the current version doesn't correspond to the schema. */ @NonNull private static Version getVersionOffset(final int offset) throws ParseException { final Version currentVersion = new Version(BuildConfig.VERSION_NAME); if (offset == 0) { return currentVersion; } int majorVersion = currentVersion.getMajorVersion(); int minorVersion = currentVersion.getMinorVersion(); int patchVersion = currentVersion.getPatchVersion(); if (offset > 0) { patchVersion += offset; } else { if (patchVersion > 0) patchVersion += offset; else if (minorVersion > 0) minorVersion += offset; else if (majorVersion > 0) majorVersion += offset; else throw new IllegalStateException("Version must be a positive number: " + currentVersion); } return new Version(majorVersion + "." + minorVersion + "." + patchVersion); } }