Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238package social.grain
import android.net.Uriimport androidx.test.ext.junit.runners.AndroidJUnit4import kotlinx.coroutines.flow.firstimport kotlinx.coroutines.runBlockingimport kotlinx.coroutines.withTimeoutOrNullimport org.junit.Afterimport org.junit.Assert.assertEqualsimport org.junit.Assert.assertFalseimport org.junit.Assert.assertNullimport org.junit.Assert.assertTrueimport org.junit.Beforeimport org.junit.Ruleimport org.junit.Testimport org.junit.runner.RunWithimport social.grain.data.auth.SecureStoreimport social.grain.data.local.Appearanceimport social.grain.data.local.AppearanceStoreimport social.grain.data.push.PushManagerimport social.grain.data.upload.GalleryDraftStoreimport social.grain.data.upload.GalleryUploadCenterimport social.grain.feature.notifications.UnseenNotificationCountimport social.grain.navigation.DeepLinkimport social.grain.support.Fixturesimport social.grain.support.Harnessimport social.grain.support.MainDispatcherRuleimport social.grain.support.appContextimport social.grain.support.awaitCallimport social.grain.support.awaitValueimport social.grain.support.jsonimport social.grain.support.settleimport social.grain.support.testJson
/** * The pieces that outlive any one screen. * * The OAuth callback in particular can't live on the login screen — the * redirect is delivered to the Activity, by which time that screen's view model * may be gone — and a deep link tapped before sign-in has to survive it rather * than being dropped. */@RunWith(AndroidJUnit4::class)class MainViewModelTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
private lateinit var harness: Harness private lateinit var appearance: AppearanceStore
@Before fun setUp() { harness = Harness() appearance = AppearanceStore(appContext) appearance.set(Appearance.AUTOMATIC) harness.server .on( "social.grain.unspecced.getActorProfile", json( Fixtures.profileDetailed( did = harness.did, handle = "renamed.grain.social", ), ), ) .on("social.grain.unspecced.getNotifications", """{"notifications":[],"unseenCount":3}""") }
@After fun tearDown() { appearance.set(Appearance.AUTOMATIC) harness.tearDown() }
private fun viewModel(): MainViewModel { val uploads = GalleryUploadCenter(appContext, GalleryDraftStore(appContext, testJson)) return MainViewModel( auth = harness.auth, client = harness.client, uploadCenter = uploads, feedCache = harness.feedCache, unseenNotifications = UnseenNotificationCount( appContext, harness.client, harness.auth, ), recentSearches = harness.recentSearches, push = PushManager( appContext, harness.client, harness.auth, SecureStore(appContext), harness.scope, ), storyStatusCache = harness.storyStatus, viewedStories = harness.viewedStories, appearanceStore = appearance, ) }
@Test fun signingInPullsTheOwnProfileSoSelfAvatarsAreNotPlaceholders() { val model = viewModel()
harness.server.awaitCall("social.grain.unspecced.getActorProfile") model.authState.awaitValue("the resolved handle") { it.handle == "renamed.grain.social" } assertEquals( "https://cdn.grain.test/avatar/renamed.grain.social.jpg", harness.auth.state.value.avatar, ) }
@Test fun theProfileIsPulledOnceRatherThanOnEveryStateChange() { viewModel() harness.server.awaitCall("social.grain.unspecced.getActorProfile") settle() Thread.sleep(400)
assertEquals( "Storing the avatar re-emits the auth state; that must not loop", 1, harness.server.countOf("social.grain.unspecced.getActorProfile"), ) }
@Test fun theBadgeIsFilledInFromTheCheapPollOnLaunch() { val model = viewModel() model.unseenCount.awaitValue("the badge") { it == 3 } }
@Test fun theAppearanceChoiceIsPublishedForTheWholeApp() { val model = viewModel() assertEquals(Appearance.AUTOMATIC, model.appearance.value)
appearance.set(Appearance.DARK)
assertEquals(Appearance.DARK, model.appearance.value) }
@Test fun storyRingsAreAssembledFromWhoHasOneAndWhatHasBeenWatched() { val model = viewModel() harness.storyStatus.update( listOf(Fixtures.storyAuthor(profile = Fixtures.profile(did = "did:plc:alice"))), )
val presence = runBlocking { withTimeoutOrNull(5_000) { model.storyPresence.first { it.hasStory("did:plc:alice") } } } assertTrue("Alice's ring lights up", presence!!.hasStory("did:plc:alice")) assertFalse("Nothing watched yet, so it isn't grey", presence.isCaughtUp("did:plc:alice")) }
@Test fun aLinkIsHeldUntilSomethingNavigatesToIt() { val model = viewModel() val link = DeepLink.Profile("alice.grain.social")
model.onDeepLink(link) assertEquals(link, model.pendingDeepLink.value)
model.consumeDeepLink() assertNull("A consumed link must not replay", model.pendingDeepLink.value) }
@Test fun aDeniedSignInIsACancellationRatherThanAnErrorToShow() { harness.server.on("par", """{"request_uri":"urn:abc"}""") val model = viewModel() runBlocking { harness.auth.beginLogin() }
model.handleOAuthCallback(Uri.parse("grain://oauth/callback?error=access_denied")) settle() Thread.sleep(300)
assertNull(model.signInError.value) }
@Test fun anyOtherCallbackFailureIsSurfacedAndCanBeDismissed() { harness.server.on("par", """{"request_uri":"urn:abc"}""") val model = viewModel() runBlocking { harness.auth.beginLogin() }
model.handleOAuthCallback( Uri.parse("grain://oauth/callback?error=server_error&error_description=Boom"), )
model.signInError.awaitValue("the error") { it != null } assertEquals("Boom", model.signInError.value)
model.dismissSignInError() assertNull(model.signInError.value) }
@Test fun comingBackToTheAppReReadsTheBadge() { val model = viewModel() model.unseenCount.awaitValue("the badge") { it == 3 } val before = harness.server.countOf("social.grain.unspecced.getNotifications")
harness.server.on( "social.grain.unspecced.getNotifications", """{"notifications":[],"unseenCount":9}""", ) model.onForeground()
model.unseenCount.awaitValue("the refreshed badge") { it == 9 } assertTrue(harness.server.countOf("social.grain.unspecced.getNotifications") > before) }
@Test fun signingOutFromAnywhereClearsTheSession() { val model = viewModel() model.authState.awaitValue("the session") { it.isAuthenticated }
model.signOut()
model.authState.awaitValue("the sign-out") { !it.isAuthenticated } }
@Test fun theUploadStageIsPublishedForTheResumeBanner() { assertEquals( social.grain.data.upload.UploadStage.Idle, viewModel().uploadStage.value, ) }}