feat: add splash screen and login auth flow

This commit is contained in:
2026-03-15 19:58:33 -04:00
parent 75e9ad1da2
commit 6ca5c1abaa
24 changed files with 889 additions and 34 deletions

View File

@@ -1,16 +0,0 @@
package space.hackenslacker.kanbn4droid.app
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("space.hackenslacker.kanbn4droid.app", appContext.packageName)
}
}

View File

@@ -0,0 +1,137 @@
package space.hackenslacker.kanbn4droid.app
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.AuthResult
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
@RunWith(AndroidJUnit4::class)
class LoginFlowTest {
@Before
fun setUp() {
MainActivity.dependencies.clear()
Intents.init()
}
@After
fun tearDown() {
Intents.release()
MainActivity.dependencies.clear()
}
@Test
fun showsLoginWithDefaultUrlWhenNoStoredSession() {
MainActivity.dependencies.sessionStoreFactory = { InMemorySessionStore() }
MainActivity.dependencies.apiKeyStoreFactory = { InMemoryApiKeyStore(null) }
MainActivity.dependencies.apiClientFactory = { FakeApiClient(AuthResult.Success) }
ActivityScenario.launch(MainActivity::class.java)
onView(withId(R.id.baseUrlInput)).check(matches(withText("https://kan.bn/")))
onView(withId(R.id.apiKeyInput)).check(matches(isDisplayed()))
onView(withId(R.id.signInButton)).check(matches(isDisplayed()))
}
@Test
fun autoLoginSuccessSkipsLoginAndNavigatesToBoards() {
val sessionStore = InMemorySessionStore("https://kan.bn/")
MainActivity.dependencies.sessionStoreFactory = { sessionStore }
MainActivity.dependencies.apiKeyStoreFactory = { InMemoryApiKeyStore("kan_key") }
MainActivity.dependencies.apiClientFactory = { FakeApiClient(AuthResult.Success) }
ActivityScenario.launch(MainActivity::class.java)
Intents.intended(hasComponent(BoardsPlaceholderActivity::class.java.name))
}
@Test
fun invalidStoredSessionFallsBackToLoginAndKeepsUrl() {
val sessionStore = InMemorySessionStore("https://kan.bn/")
MainActivity.dependencies.sessionStoreFactory = { sessionStore }
MainActivity.dependencies.apiKeyStoreFactory = { InMemoryApiKeyStore("bad-key") }
MainActivity.dependencies.apiClientFactory = {
FakeApiClient(AuthResult.Failure("Authentication failed. Check your API key."))
}
ActivityScenario.launch(MainActivity::class.java)
onView(withId(R.id.baseUrlInput)).check(matches(withText("https://kan.bn/")))
onView(withId(R.id.signInButton)).check(matches(isDisplayed()))
}
@Test
fun manualSignInSuccessNavigatesAndStoresSession() {
val sessionStore = InMemorySessionStore()
val keyStore = InMemoryApiKeyStore(null)
MainActivity.dependencies.sessionStoreFactory = { sessionStore }
MainActivity.dependencies.apiKeyStoreFactory = { keyStore }
MainActivity.dependencies.apiClientFactory = { FakeApiClient(AuthResult.Success) }
ActivityScenario.launch(MainActivity::class.java)
onView(withId(R.id.baseUrlInput)).perform(replaceText("https://kan.bn"), closeSoftKeyboard())
onView(withId(R.id.apiKeyInput)).perform(replaceText("kan_new"), closeSoftKeyboard())
onView(withId(R.id.signInButton)).perform(click())
Intents.intended(hasComponent(BoardsPlaceholderActivity::class.java.name))
assertEquals("https://kan.bn/", sessionStore.getBaseUrl())
assertEquals("kan_new", keyStore.savedKey)
}
private class InMemorySessionStore(
private var baseUrl: String? = null,
) : SessionStore {
override fun getBaseUrl(): String? = baseUrl
override fun saveBaseUrl(url: String) {
baseUrl = url
}
override fun clearBaseUrl() {
baseUrl = null
}
}
private class InMemoryApiKeyStore(
private var key: String?,
) : ApiKeyStore {
var savedKey: String? = null
override suspend fun saveApiKey(baseUrl: String, apiKey: String): Result<Unit> {
key = apiKey
savedKey = apiKey
return Result.success(Unit)
}
override suspend fun getApiKey(baseUrl: String): Result<String?> = Result.success(key)
override suspend fun invalidateApiKey(baseUrl: String): Result<Unit> {
key = null
return Result.success(Unit)
}
}
private class FakeApiClient(
private val result: AuthResult,
) : KanbnApiClient {
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = result
}
}