prevent mixup of account timelines (#4599)

This does 2 things:

- Removes `AccountSwitchInterceptor`, the main culprit for the bug. APIs
can no longer change their base url after they have been created. As a
result they are not Singletons anymore.
- Additionally, I refactored how MainActivity handles Intents to make it
less likely to have multiple instances of it active.

Here is how I could reliably reproduce the bug:

- Be logged in with account A and B
- Write a post with account A, cancel it before it sends (go into flight
mode for that)
- Switch to account B
- Open the "this post failed to send" notification from account A,
drafts will open
- Go back. You are in the MainActivity of account A, everything seems
fine.
- Go back again. You are in the old, now broken MainActivity of account
B. It uses the database of account B but the network of account A.
Refreshing will show posts from A.

closes #4567 
closes #4554
closes #4402 
closes #4148
closes #2663
and possibly #4588
This commit is contained in:
Konrad Pozniak 2024-08-14 18:58:12 +02:00 committed by GitHub
commit c7387c7b52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 306 additions and 261 deletions

View file

@ -1,142 +0,0 @@
package com.keylesspalace.tusky.network
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.entity.AccountEntity
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
class InstanceSwitchAuthInterceptorTest {
private val mockWebServer = MockWebServer()
@Before
fun setup() {
mockWebServer.start()
}
@After
fun teardown() {
mockWebServer.shutdown()
}
@Test
fun `should make regular request when requested`() {
mockWebServer.enqueue(MockResponse())
val accountManager: AccountManager = mock {
on { activeAccount } doAnswer { null }
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
.build()
val request = Request.Builder()
.get()
.url(mockWebServer.url("/test"))
.build()
val response = okHttpClient.newCall(request).execute()
assertEquals(200, response.code)
}
@Test
fun `should make request to instance requested in special header`() {
mockWebServer.enqueue(MockResponse())
val accountManager: AccountManager = mock {
on { activeAccount } doAnswer {
AccountEntity(
id = 1,
domain = "test.domain",
accessToken = "fakeToken",
clientId = "fakeId",
clientSecret = "fakeSecret",
isActive = true
)
}
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
.build()
val request = Request.Builder()
.get()
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test")
.header(MastodonApi.DOMAIN_HEADER, mockWebServer.hostName)
.build()
val response = okHttpClient.newCall(request).execute()
assertEquals(200, response.code)
assertNull(mockWebServer.takeRequest().getHeader("Authorization"))
}
@Test
fun `should make request to current instance when requested and user is logged in`() {
mockWebServer.enqueue(MockResponse())
val accountManager: AccountManager = mock {
on { activeAccount } doAnswer {
AccountEntity(
id = 1,
domain = mockWebServer.hostName,
accessToken = "fakeToken",
clientId = "fakeId",
clientSecret = "fakeSecret",
isActive = true
)
}
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
.build()
val request = Request.Builder()
.get()
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test")
.build()
val response = okHttpClient.newCall(request).execute()
assertEquals(200, response.code)
assertEquals("Bearer fakeToken", mockWebServer.takeRequest().getHeader("Authorization"))
}
@Test
fun `should fail to make request when request to current instance is requested but no user is logged in`() {
mockWebServer.enqueue(MockResponse())
val accountManager: AccountManager = mock {
on { activeAccount } doAnswer { null }
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
.build()
val request = Request.Builder()
.get()
.url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + "/test")
.build()
val response = okHttpClient.newCall(request).execute()
assertEquals(400, response.code)
assertEquals(0, mockWebServer.requestCount)
}
}

View file

@ -0,0 +1,126 @@
package com.keylesspalace.tusky.network
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.entity.Instance
import com.squareup.moshi.Moshi
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
class RetrofitApiTest {
private val mockWebServer = MockWebServer()
private val okHttpClient = OkHttpClient.Builder().build()
private val moshi = Moshi.Builder().build()
@Before
fun setup() {
mockWebServer.start()
}
@After
fun teardown() {
mockWebServer.shutdown()
}
private fun retrofit() = Retrofit.Builder()
.baseUrl("http://${MastodonApi.PLACEHOLDER_DOMAIN}:${mockWebServer.port}")
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
.build()
@Test
fun `should make request to the active account's instance`() = runTest {
mockInstanceResponse()
val account = AccountEntity(
id = 1,
domain = mockWebServer.hostName,
accessToken = "fakeToken",
clientId = "fakeId",
clientSecret = "fakeSecret",
isActive = true
)
val retrofit = retrofit()
val api: MastodonApi = apiForAccount(account, okHttpClient, retrofit, "http://", mockWebServer.port)
val instanceResponse = api.getInstance()
assertTrue(instanceResponse.isSuccess)
assertEquals("Bearer fakeToken", mockWebServer.takeRequest().getHeader("Authorization"))
}
@Test
fun `should make request to instance requested in special header when account active`() = runTest {
mockInstanceResponse()
val account = AccountEntity(
id = 1,
domain = "test.domain",
accessToken = "fakeToken",
clientId = "fakeId",
clientSecret = "fakeSecret",
isActive = true
)
val retrofit = retrofit()
val api: MastodonApi = apiForAccount(account, okHttpClient, retrofit, "http://", mockWebServer.port)
val instanceResponse = api.getInstance(domain = mockWebServer.hostName)
assertTrue(instanceResponse.isSuccess)
assertNull(mockWebServer.takeRequest().getHeader("Authorization"))
}
@Test
fun `should make request to instance requested in special header when no account active`() = runTest {
mockInstanceResponse()
val retrofit = retrofit()
val api: MastodonApi = apiForAccount(null, okHttpClient, retrofit, "http://", mockWebServer.port)
val instanceResponse = api.getInstance(domain = mockWebServer.hostName)
assertTrue(instanceResponse.isSuccess)
assertNull(mockWebServer.takeRequest().getHeader("Authorization"))
}
@Test
fun `should fail when current instance is requested but no user is logged in`() = runTest {
mockInstanceResponse()
val retrofit = retrofit()
val api: MastodonApi = apiForAccount(null, okHttpClient, retrofit, "http://", mockWebServer.port)
val instanceResponse = api.getInstance()
assertTrue(instanceResponse.isFailure)
assertEquals(0, mockWebServer.requestCount)
}
private fun mockInstanceResponse() {
mockWebServer.enqueue(
MockResponse()
.setBody(
moshi.adapter(Instance::class.java).toJson(
Instance(
domain = "example.org",
version = "1.0.0"
)
)
)
)
}
}