Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180package social.grain
import androidx.test.ext.junit.runners.AndroidJUnit4import 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.api.ActorSuggestionimport social.grain.feature.login.LoginViewModelimport social.grain.support.Harnessimport social.grain.support.MainDispatcherRuleimport social.grain.support.applySnapshotimport social.grain.support.awaitCallimport social.grain.support.awaitValueimport social.grain.support.settleimport social.grain.support.type
/** * The sign-in screen: a handle field with a typeahead, and the PAR call behind * the button. * * The subtle one is that choosing a suggestion writes it into the field, which * the collector watching the field sees as a fresh query — without the settled * handle it would reopen the suggestion list under the reader's finger. */@RunWith(AndroidJUnit4::class)class LoginViewModelTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
private lateinit var harness: Harness
private val typeahead = "social.grain.unspecced.searchActorsTypeahead"
@Before fun setUp() { harness = Harness(signedIn = false) harness.server .on( typeahead, """{"actors":[{"did":"did:plc:alice","handle":"alice.grain.social"}]}""", ) .on("par", """{"request_uri":"urn:ietf:params:oauth:request_uri:abc"}""") }
@After fun tearDown() = harness.tearDown()
private fun viewModel() = LoginViewModel(harness.auth, harness.client)
@Test fun typingAtLeastTwoCharactersFetchesSuggestions() { val model = viewModel() model.handleState.type("al")
val state = model.state.awaitValue("the suggestions") { it.suggestions.isNotEmpty() } assertEquals("alice.grain.social", state.suggestions.single().handle) assertEquals("al", harness.server.lastCall(typeahead).params["q"]) }
@Test fun theLeadingAtIsStrippedBeforeAsking() { val model = viewModel() model.handleState.type("@alice")
harness.server.awaitCall(typeahead) assertEquals("alice", harness.server.lastCall(typeahead).params["q"]) }
@Test fun oneCharacterIsNotEnoughToSuggestAnything() { val model = viewModel() model.handleState.type("a") settle() Thread.sleep(500)
assertEquals(0, harness.server.countOf(typeahead)) assertTrue(model.state.value.suggestions.isEmpty()) }
@Test fun choosingASuggestionFillsTheFieldWithoutReopeningTheList() { val model = viewModel() model.handleState.type("al") model.state.awaitValue("the suggestions") { it.suggestions.isNotEmpty() } val before = harness.server.countOf(typeahead)
model.chooseSuggestion(ActorSuggestion(did = "did:plc:alice", handle = "alice.grain.social")) applySnapshot() Thread.sleep(500) settle()
assertEquals("alice.grain.social", model.handleState.text.toString()) assertTrue(model.state.value.suggestions.isEmpty()) assertEquals("The chosen handle isn't looked up again", before, harness.server.countOf(typeahead)) }
@Test fun dismissingTheListKeepsItClosedForWhatIsAlreadyTyped() { val model = viewModel() model.handleState.type("alice.grain.social") model.state.awaitValue("the suggestions") { it.suggestions.isNotEmpty() }
model.clearSuggestions() applySnapshot() Thread.sleep(500)
assertTrue(model.state.value.suggestions.isEmpty()) }
@Test fun signingInRunsParAndHandsBackTheUrlToOpen() { val model = viewModel() model.handleState.type("alice.grain.social")
model.signIn()
val state = model.state.awaitValue("the authorize url") { it.authorizeUrl != null } assertTrue(state.authorizeUrl!!.startsWith("https://grain.social/oauth/authorize")) assertTrue(state.authorizeUrl!!.contains("request_uri=urn")) assertFalse(state.isLoading)
val par = harness.server.lastCall("par") assertTrue(par.body.contains("code_challenge_method=S256")) assertTrue(par.body.contains("login_hint=alice.grain.social")) assertEquals(3, par.dpopProof!!.split('.').size)
model.consumeAuthorizeUrl() assertNull("The url is consumed so a recomposition can't reopen the browser", model.state.value.authorizeUrl) }
@Test fun creatingAnAccountAsksForTheSignUpPageInstead() { val model = viewModel() model.signIn(createAccount = true) model.state.awaitValue("the authorize url") { it.authorizeUrl != null }
val par = harness.server.lastCall("par") assertTrue(par.body.contains("prompt=create")) }
@Test fun aParThatFailsReportsItAndLeavesTheButtonUsable() { harness.server.on("par", """{"error":"no"}""", code = 500) val model = viewModel()
model.signIn()
val state = model.state.awaitValue("the error") { it.errorMessage != null } assertFalse(state.isLoading) assertNull(state.authorizeUrl)
model.dismissError() assertNull(model.state.value.errorMessage) }
@Test fun aSecondTapWhileTheFirstIsInFlightIsIgnored() { harness.server.on("par", """{"request_uri":"urn:a"}""") val model = viewModel()
model.signIn() model.signIn()
model.state.awaitValue("the authorize url") { it.authorizeUrl != null } assertEquals(1, harness.server.countOf("par")) }
@Test fun thereIsNoReauthReasonOnAPlainSignIn() { assertNull(viewModel().reauthReason) }}