Browse Source
#7: Add more tests to OAuth component See merge request funkwhale/funkwhale-android!49enhancement/speed-up-pipelines
Ryan Harg
3 years ago
6 changed files with 338 additions and 97 deletions
@ -0,0 +1,270 @@
@@ -0,0 +1,270 @@
|
||||
package audio.funkwhale.ffa.utils |
||||
|
||||
import android.content.Context |
||||
import com.github.kittinunf.fuel.core.Client |
||||
import com.github.kittinunf.fuel.core.FuelManager |
||||
import com.github.kittinunf.fuel.core.Request |
||||
import com.google.gson.Gson |
||||
import com.google.gson.reflect.TypeToken |
||||
import com.preference.PowerPreference |
||||
import com.preference.Preference |
||||
import io.mockk.MockKAnnotations |
||||
import io.mockk.coVerify |
||||
import io.mockk.every |
||||
import io.mockk.impl.annotations.InjectMockKs |
||||
import io.mockk.impl.annotations.MockK |
||||
import io.mockk.mockk |
||||
import io.mockk.mockkStatic |
||||
import io.mockk.slot |
||||
import io.mockk.verify |
||||
import net.openid.appauth.AuthState |
||||
import net.openid.appauth.AuthorizationService |
||||
import net.openid.appauth.AuthorizationServiceConfiguration |
||||
import net.openid.appauth.ClientSecretPost |
||||
import org.junit.Before |
||||
import org.junit.Test |
||||
import strikt.api.expectThat |
||||
import strikt.api.expectThrows |
||||
import strikt.assertions.isEqualTo |
||||
import strikt.assertions.isFalse |
||||
import strikt.assertions.isNotNull |
||||
import strikt.assertions.isNull |
||||
import strikt.assertions.isTrue |
||||
|
||||
|
||||
class DefaultOAuthTest { |
||||
|
||||
@InjectMockKs |
||||
private lateinit var oAuth: DefaultOAuth |
||||
|
||||
@MockK |
||||
private lateinit var authServiceFactory: AuthorizationServiceFactory |
||||
|
||||
@MockK |
||||
private lateinit var authService: AuthorizationService |
||||
|
||||
@MockK |
||||
private lateinit var mockPreference: Preference |
||||
|
||||
@MockK |
||||
private lateinit var context: Context |
||||
|
||||
@Before |
||||
fun setup() { |
||||
MockKAnnotations.init(this, relaxUnitFun = true) |
||||
} |
||||
|
||||
@Test |
||||
fun `tryState() should return null if saved state is missing`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns null |
||||
expectThat(oAuth.tryState()).isNull() |
||||
} |
||||
|
||||
@Test |
||||
fun `tryState() should return null if saved state is empty`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns "" |
||||
expectThat(oAuth.tryState()).isNull() |
||||
} |
||||
|
||||
@Test |
||||
fun `tryState() should return deserialized object if saved state is present`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns "{}" |
||||
expectThat(oAuth.tryState()).isNotNull() |
||||
} |
||||
|
||||
@Test |
||||
fun `state() should return deserialized object if saved state is present`() { |
||||
|
||||
mockkStatic(PowerPreference::class) |
||||
mockkStatic(AuthState::class) |
||||
|
||||
val authState = AuthState() |
||||
every { AuthState.jsonDeserialize(any<String>()) } returns authState |
||||
|
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns "{}" |
||||
|
||||
val result = oAuth.state() |
||||
expectThat(result).isEqualTo(authState) |
||||
} |
||||
|
||||
@Test |
||||
fun `state() should throw error if saved state is missing`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns null |
||||
|
||||
expectThrows<IllegalStateException> { oAuth.state() } |
||||
} |
||||
|
||||
@Test |
||||
fun `isAuthorized() should return false if no state exists`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns null |
||||
|
||||
expectThat(oAuth.isAuthorized(context)).isFalse() |
||||
} |
||||
|
||||
@Test |
||||
fun `isAuthorized() should return false if existing state is not authorized and token is not refreshed`() { |
||||
mockkStatic(PowerPreference::class) |
||||
mockkStatic(AuthState::class) |
||||
|
||||
val authState = mockk<AuthState>() |
||||
every { AuthState.jsonDeserialize(any<String>()) } returns authState |
||||
every { authState.isAuthorized } returns false |
||||
every { authState.needsTokenRefresh } returns false |
||||
|
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns "{}" |
||||
|
||||
expectThat(oAuth.isAuthorized(context)).isFalse() |
||||
} |
||||
|
||||
@Test |
||||
fun `isAuthorized() should return true if existing state is authorized`() { |
||||
mockkStatic(PowerPreference::class) |
||||
mockkStatic(AuthState::class) |
||||
|
||||
val authState = mockk<AuthState>() |
||||
every { AuthState.jsonDeserialize(any<String>()) } returns authState |
||||
every { authState.isAuthorized } returns true |
||||
|
||||
val mockPref = mockk<Preference>() |
||||
every { PowerPreference.getFileByName(any()) } returns mockPref |
||||
every { mockPref.getString(any()) } returns "{}" |
||||
|
||||
expectThat(oAuth.isAuthorized(context)).isTrue() |
||||
} |
||||
|
||||
@Test |
||||
fun `tryRefreshAccessToken() should perform token refresh request if accessToken needs refresh and refreshToken exists`() { |
||||
mockkStatic(PowerPreference::class) |
||||
mockkStatic(AuthState::class) |
||||
|
||||
val authState = mockk<AuthState>() |
||||
every { AuthState.jsonDeserialize(any<String>()) } returns authState |
||||
every { authState.isAuthorized } returns false |
||||
every { authState.needsTokenRefresh } returns true |
||||
every { authState.refreshToken } returns "refreshToken" |
||||
every { authState.createTokenRefreshRequest() } returns mockk() |
||||
every { authState.clientSecret } returns "clientSecret" |
||||
every { authServiceFactory.create(any()) } returns authService |
||||
every { authService.performTokenRequest(any(), any<ClientSecretPost>(), any()) } returns mockk() |
||||
|
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns "{}" |
||||
|
||||
oAuth.tryRefreshAccessToken(context) |
||||
|
||||
verify { authService.performTokenRequest(any(), any(), any()) } |
||||
} |
||||
|
||||
@Test |
||||
fun `tryRefreshAccessToken() should not perform token refresh request if accessToken doesn't need refresh`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns "{}" |
||||
|
||||
mockkStatic(AuthState::class) |
||||
|
||||
val authState = mockk<AuthState>() |
||||
every { AuthState.jsonDeserialize(any<String>()) } returns authState |
||||
every { authState.isAuthorized } returns false |
||||
every { authState.needsTokenRefresh } returns false |
||||
|
||||
oAuth.tryRefreshAccessToken(context) |
||||
|
||||
verify(exactly = 0) { authService.performTokenRequest(any(), any(), any()) } |
||||
} |
||||
|
||||
@Test |
||||
fun `init() should setup correct endpoints`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.setString(any(), any()) } returns true |
||||
|
||||
val result = oAuth.init("hostname") |
||||
|
||||
expectThat(result.authorizationServiceConfiguration?.authorizationEndpoint.toString()) |
||||
.isEqualTo("hostname/authorize") |
||||
expectThat(result.authorizationServiceConfiguration?.tokenEndpoint.toString()) |
||||
.isEqualTo("hostname/api/v1/oauth/token/") |
||||
expectThat(result.authorizationServiceConfiguration?.registrationEndpoint.toString()) |
||||
.isEqualTo("hostname/api/v1/oauth/apps/") |
||||
} |
||||
|
||||
@Test |
||||
fun `register() should not initiate http request if configuration is missing`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns "{}" |
||||
|
||||
mockkStatic(AuthState::class) |
||||
val authState = mockk<AuthState>() |
||||
every { AuthState.jsonDeserialize(any<String>()) } returns authState |
||||
every { authState.authorizationServiceConfiguration } returns null |
||||
|
||||
val mockkClient = mockk<Client>() |
||||
FuelManager.instance.client = mockkClient |
||||
|
||||
oAuth.register {} |
||||
|
||||
verify(exactly = 0) { mockkClient.executeRequest(any()) } |
||||
} |
||||
|
||||
@Test |
||||
fun `register() should initiate correct HTTP request to registration endpoint`() { |
||||
mockkStatic(PowerPreference::class) |
||||
every { PowerPreference.getFileByName(any()) } returns mockPreference |
||||
every { mockPreference.getString(any()) } returns "{}" |
||||
every { mockPreference.setString(any(), any()) } returns true |
||||
|
||||
mockkStatic(AuthState::class) |
||||
val authState = mockk<AuthState>() |
||||
every { AuthState.jsonDeserialize(any<String>()) } returns authState |
||||
val mockConfig = mockk<AuthorizationServiceConfiguration>() |
||||
every { authState.authorizationServiceConfiguration } returns mockConfig |
||||
|
||||
val mockkClient = mockk<Client>() |
||||
|
||||
FuelManager.instance.client = mockkClient |
||||
|
||||
val state = oAuth.init("https://example.com") |
||||
oAuth.register(state) { } |
||||
|
||||
val requestSlot = slot<com.github.kittinunf.fuel.core.Request>() |
||||
|
||||
coVerify { mockkClient.awaitRequest(capture(requestSlot)) } |
||||
|
||||
val capturedRequest = requestSlot.captured |
||||
expectThat(capturedRequest.url.toString()) |
||||
.isEqualTo("https://example.com/api/v1/oauth/apps/") |
||||
|
||||
expectThat(deserializeJson<Map<String, String>>(capturedRequest)).isEqualTo( |
||||
mapOf( |
||||
"name" to "Funkwhale for Android (null)", |
||||
"redirect_uris" to "urn:/audio.funkwhale.funkwhale-android/oauth/callback", |
||||
"scopes" to "read write" |
||||
) |
||||
) |
||||
} |
||||
|
||||
private fun <T> deserializeJson( |
||||
capturedRequest: Request |
||||
): T { |
||||
return Gson().fromJson( |
||||
capturedRequest.body.asString("application/json"), |
||||
object : TypeToken<T>() {}.type |
||||
) |
||||
} |
||||
|
||||
} |
||||
|
@ -1,58 +0,0 @@
@@ -1,58 +0,0 @@
|
||||
package audio.funkwhale.util |
||||
|
||||
import io.mockk.MockKAnnotations |
||||
import io.mockk.clearAllMocks |
||||
import org.junit.Test |
||||
import org.junit.runner.Description |
||||
import org.junit.runner.Runner |
||||
import org.junit.runner.notification.Failure |
||||
import org.junit.runner.notification.RunNotifier |
||||
import java.lang.reflect.Method |
||||
|
||||
class MockKJUnitRunner(private val testClass: Class<*>) : Runner() { |
||||
|
||||
private val methodDescriptions: MutableMap<Method, Description> = mutableMapOf() |
||||
|
||||
init { |
||||
// Build method/descriptions map |
||||
testClass.methods |
||||
.map { method -> |
||||
val annotation: Annotation? = method.getAnnotation(Test::class.java) |
||||
method to annotation |
||||
} |
||||
.filter { (_, annotation) -> |
||||
annotation != null |
||||
} |
||||
.map { (method, annotation) -> |
||||
val desc = Description.createTestDescription(testClass, method.name, annotation) |
||||
method to desc |
||||
} |
||||
.forEach { (method, desc) -> methodDescriptions[method] = desc } |
||||
} |
||||
|
||||
override fun getDescription(): Description { |
||||
val description = Description.createSuiteDescription( |
||||
testClass.name, *testClass.annotations |
||||
) |
||||
methodDescriptions.values.forEach { description.addChild(it) } |
||||
return description |
||||
} |
||||
|
||||
override fun run(notifier: RunNotifier?) { |
||||
val testObject = testClass.newInstance() |
||||
MockKAnnotations.init(testObject, relaxUnitFun = true) |
||||
|
||||
methodDescriptions |
||||
.onEach { (_, _) -> clearAllMocks() } |
||||
.onEach { (_, desc) -> notifier!!.fireTestStarted(desc) } |
||||
.forEach { (method, desc) -> |
||||
try { |
||||
method.invoke(testObject) |
||||
} catch (e: Throwable) { |
||||
notifier!!.fireTestFailure(Failure(desc, e.cause)) |
||||
} finally { |
||||
notifier!!.fireTestFinished(desc) |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue