Some more tests, move values into xml

This commit is contained in:
Matthieu 2020-03-07 18:13:26 +01:00
parent 1405d69861
commit b79ebca03a
7 changed files with 158 additions and 103 deletions

View File

@ -4,9 +4,10 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'jacoco' apply plugin: 'jacoco'
android { android {
compileSdkVersion 29 compileSdkVersion 29
buildToolsVersion "29.0.3" buildToolsVersion "29.0.3"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@ -18,6 +19,7 @@ compileOptions {
versionName "1.0" versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
} }
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/java' main.java.srcDirs += 'src/main/java'
@ -36,6 +38,7 @@ compileOptions {
} }
testOptions { testOptions {
animationsDisabled true animationsDisabled true
} }
} }
@ -53,16 +56,17 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.16' implementation 'io.reactivex.rxjava2:rxjava:2.2.16'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "androidx.browser:browser:1.2.0" implementation "androidx.browser:browser:1.2.0"
implementation 'com.google.android.material:material:1.1.0'
testImplementation "com.github.tomakehurst:wiremock-jre8:2.26.2"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.github.tomakehurst:wiremock-jre8:2.26.3"
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.google.android.material:material:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
androidTestImplementation 'androidx.test:rules:1.3.0-alpha04'
}
}
tasks.withType(Test) { tasks.withType(Test) {
jacoco.includeNoLocationClasses = true jacoco.includeNoLocationClasses = true

View File

@ -1,26 +1,28 @@
package com.h.pixeldroid package com.h.pixeldroid
import android.view.View import android.app.Activity
import android.view.ViewGroup import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasDataString
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.Description import androidx.test.rule.ActivityTestRule
import org.hamcrest.CoreMatchers.*
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers import org.junit.Before
import org.hamcrest.TypeSafeMatcher import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
import org.junit.Rule
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
@ -30,27 +32,69 @@ import org.junit.Rule
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class LoginInstrumentedTest { class LoginInstrumentedTest {
@get:Rule @get:Rule
var activityRule: ActivityScenarioRule<MainActivity> var activityRule: ActivityScenarioRule<LoginActivity>
= ActivityScenarioRule(MainActivity::class.java) = ActivityScenarioRule(LoginActivity::class.java)
@Test @Test
fun clickConnect() { fun clickConnect() {
onView(withId(R.id.button_start_login)).perform(click())
onView(withId(R.id.connect_instance_button)).check(matches(withText("Connect"))) onView(withId(R.id.connect_instance_button)).check(matches(withText("Connect")))
} }
@Test @Test
fun invalidURL() { fun invalidURL() {
onView(withId(R.id.button_start_login)).perform(click())
onView(withId(R.id.editText)).perform(ViewActions.replaceText("/jdi"), ViewActions.closeSoftKeyboard()) onView(withId(R.id.editText)).perform(ViewActions.replaceText("/jdi"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(click()) onView(withId(R.id.connect_instance_button)).perform(click())
onView(withId(R.id.editText)).check(matches(hasErrorText("Invalid domain"))) onView(withId(R.id.editText)).check(matches(hasErrorText("Invalid domain")))
} }
@Test @Test
fun notPixelfedInstance() { fun notPixelfedInstance() {
onView(withId(R.id.button_start_login)).perform(click())
onView(withId(R.id.editText)).perform(ViewActions.replaceText("localhost"), ViewActions.closeSoftKeyboard()) onView(withId(R.id.editText)).perform(ViewActions.replaceText("localhost"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(click()) onView(withId(R.id.connect_instance_button)).perform(click())
onView(withId(R.id.editText)).check(matches(hasErrorText("Could not register the application with this server"))) onView(withId(R.id.editText)).check(matches(hasErrorText("Could not register the application with this server")))
}
}
@RunWith(AndroidJUnit4::class)
class LoginCheckIntent {
@get:Rule
val intentsTestRule = IntentsTestRule(LoginActivity::class.java)
@Test
fun launchesIntent() {
val expectedIntent: Matcher<Intent> = allOf(
hasAction(ACTION_VIEW),
hasDataString(containsString("pixelfed.social"))
)
onView(withId(R.id.editText)).perform(ViewActions.replaceText("pixelfed.social"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(click())
Thread.sleep(5000)
intended(expectedIntent)
}
}
@RunWith(AndroidJUnit4::class)
class AfterIntent {
@get:Rule
val rule = ActivityTestRule(LoginActivity::class.java)
private var launchedActivity: Activity? = null
@Before
fun setup() {
val intent = Intent(ACTION_VIEW, Uri.parse("oauth2redirect://com.h.pixeldroid?code=sdfdqsf"))
launchedActivity = rule.launchActivity(intent)
}
@Test
fun usesIntent() {
Thread.sleep(5000)
onView(withId(R.id.editText)).check(matches(
anyOf(hasErrorText("Error getting token"),
hasErrorText("Could not authenticate"))))
} }
} }

View File

@ -19,7 +19,7 @@
<data <data
android:host="${applicationId}" android:host="${applicationId}"
android:scheme="oauth2redirect" /> android:scheme="@string/auth_scheme" />
</intent-filter> </intent-filter>
</activity> <activity android:name=".MainActivity"> </activity> <activity android:name=".MainActivity">
<intent-filter> <intent-filter>

View File

@ -20,10 +20,10 @@ import retrofit2.Response
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
private val OAUTH_SCHEME = "oauth2redirect" private lateinit var OAUTH_SCHEME: String
private val PACKAGE_ID = "com.h.pixeldroid" private val PACKAGE_ID = BuildConfig.APPLICATION_ID
private val SCOPE = "read write follow" private val SCOPE = "read write follow"
private val APP_NAME = "PixelDroid" private lateinit var APP_NAME: String
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -31,12 +31,11 @@ class LoginActivity : AppCompatActivity() {
setContentView(R.layout.activity_login) setContentView(R.layout.activity_login)
connect_instance_button.setOnClickListener { onClickConnect() } connect_instance_button.setOnClickListener { onClickConnect() }
APP_NAME = getString(R.string.app_name)
OAUTH_SCHEME = getString(R.string.auth_scheme)
preferences = getSharedPreferences( preferences = getSharedPreferences(
"$PACKAGE_ID.pref", Context.MODE_PRIVATE "$PACKAGE_ID.pref", Context.MODE_PRIVATE
) )
} }
override fun onStart(){ override fun onStart(){
@ -44,69 +43,40 @@ class LoginActivity : AppCompatActivity() {
val url = intent.data val url = intent.data
if (url != null && url.toString().startsWith("$OAUTH_SCHEME://$PACKAGE_ID")) { if (url == null || !url.toString().startsWith("$OAUTH_SCHEME://$PACKAGE_ID")) return
val code = url.getQueryParameter("code") val code = url.getQueryParameter("code")
val error = url.getQueryParameter("error") authenticate(code)
// Restore previous values from preferences
val domain = preferences.getString("domain", "")
val clientId = preferences.getString("clientID", "")
val clientSecret = preferences.getString("clientSecret", "")
if (code != null && !domain.isNullOrEmpty() && !clientId.isNullOrEmpty() && !clientSecret.isNullOrEmpty()) {
//Successful authorization
val callback = object : Callback<Token> {
override fun onResponse(call: Call<Token>, response: Response<Token>) {
if (response.isSuccessful) {
authenticationSuccessful(domain, response.body()?.access_token)
} else {
failedRegistration("Error getting token")
}
}
override fun onFailure(call: Call<Token>, t: Throwable) {
failedRegistration("Error getting token")
}
}
val pixelfedAPI = PixelfedAPI.create("https://$domain")
pixelfedAPI.obtainToken(clientId, clientSecret, "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, code,
"authorization_code").enqueue(callback)
} else if (error != null) {
failedRegistration("Authentication denied")
} else {
failedRegistration("Unknown response")
}
}
}
private fun authenticationSuccessful(domain: String, accessToken: String?) {
Log.e("Token", accessToken!!)
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
} }
private fun onClickConnect() { private fun onClickConnect() {
connect_instance_button.isEnabled = false connect_instance_button.isEnabled = false
val domain = editText.text.toString() val normalizedDomain = normalizeDomain(editText.text.toString())
val normalizedDomain = normalizeDomain(domain)
try{ try{
HttpUrl.Builder().host(domain).scheme("https").build() HttpUrl.Builder().host(normalizedDomain).scheme("https").build()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
failedRegistration("Invalid domain") failedRegistration(getString(R.string.invalid_domain))
return return
} }
preferences.edit() preferences.edit()
.putString("domain", normalizedDomain) .putString("domain", normalizedDomain)
.apply() .apply()
registerAppToServer(normalizedDomain)
}
private fun normalizeDomain(domain: String): String {
var d = domain.replace("http://", "")
d = d.replace("https://", "")
return d.trim(Char::isWhitespace)
}
private fun registerAppToServer(normalizedDomain: String) {
val callback = object : Callback<Application> { val callback = object : Callback<Application> {
override fun onResponse(call: Call<Application>, response: Response<Application>) { override fun onResponse(call: Call<Application>, response: Response<Application>) {
if (!response.isSuccessful) { if (!response.isSuccessful) {
@ -118,13 +88,12 @@ class LoginActivity : AppCompatActivity() {
val clientId = credentials?.client_id ?: return failedRegistration() val clientId = credentials?.client_id ?: return failedRegistration()
val clientSecret = credentials.client_secret val clientSecret = credentials.client_secret
preferences.edit() preferences.edit()
.putString("clientID", clientId) .putString("clientID", clientId)
.putString("clientSecret", clientSecret) .putString("clientSecret", clientSecret)
.apply() .apply()
redirect(normalizedDomain, clientId) promptOAuth(normalizedDomain, clientId)
} }
override fun onFailure(call: Call<Application>, t: Throwable) { override fun onFailure(call: Call<Application>, t: Throwable) {
@ -132,28 +101,12 @@ class LoginActivity : AppCompatActivity() {
return return
} }
} }
PixelfedAPI.create("https://$normalizedDomain").registerApplication(
val pixelfedAPI = PixelfedAPI.create("https://$normalizedDomain") APP_NAME,"$OAUTH_SCHEME://$PACKAGE_ID", SCOPE
pixelfedAPI.registerApplication( ).enqueue(callback)
APP_NAME,
"$OAUTH_SCHEME://$PACKAGE_ID", SCOPE
)
.enqueue(callback)
} }
private fun failedRegistration(message: String = private fun promptOAuth(normalizedDomain: String, client_id: String) {
"Could not register the application with this server"){
connect_instance_button.isEnabled = true
editText.error = message
}
private fun normalizeDomain(domain: String): String {
var d = domain.replace("http://", "")
d = d.replace("https://", "")
return d.trim(Char::isWhitespace)
}
fun redirect(normalizedDomain: String, client_id: String) {
val url = "https://$normalizedDomain/oauth/authorize?" + val url = "https://$normalizedDomain/oauth/authorize?" +
"client_id" + "=" + client_id + "&" + "client_id" + "=" + client_id + "&" +
@ -161,23 +114,67 @@ class LoginActivity : AppCompatActivity() {
"response_type=code" + "&" + "response_type=code" + "&" +
"scope=$SCOPE" "scope=$SCOPE"
browser(this, url)
}
private fun browser(context: Context, url: String) {
val intent = CustomTabsIntent.Builder().build() val intent = CustomTabsIntent.Builder().build()
try { try {
intent.launchUrl(context, Uri.parse(url)) intent.launchUrl(this, Uri.parse(url))
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
if (browserIntent.resolveActivity(packageManager) != null) { if (browserIntent.resolveActivity(packageManager) != null) {
startActivity(browserIntent) startActivity(browserIntent)
} else { } else {
failedRegistration(message="Could not launch a browser, do you have one?") failedRegistration(getString(R.string.browser_launch_failed))
return return
} }
} }
connect_instance_button.isEnabled = true connect_instance_button.isEnabled = true
} }
private fun authenticate(code: String?) {
// Get previous values from preferences
val domain = preferences.getString("domain", "")
val clientId = preferences.getString("clientID", "")
val clientSecret = preferences.getString("clientSecret", "")
if (code == null || domain.isNullOrEmpty() || clientId.isNullOrEmpty() || clientSecret.isNullOrEmpty()) {
failedRegistration(getString(R.string.auth_failed))
return
}
//Successful authorization
val callback = object : Callback<Token> {
override fun onResponse(call: Call<Token>, response: Response<Token>) {
if (!response.isSuccessful || response.body() == null) {
failedRegistration(getString(R.string.token_error))
return
}
authenticationSuccessful(domain, response.body()!!.access_token)
}
override fun onFailure(call: Call<Token>, t: Throwable) {
failedRegistration(getString(R.string.token_error))
}
}
val pixelfedAPI = PixelfedAPI.create("https://$domain")
pixelfedAPI.obtainToken(
clientId, clientSecret, "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE, code,
"authorization_code"
).enqueue(callback)
}
private fun authenticationSuccessful(domain: String, accessToken: String) {
preferences.edit().putString("accessToken", accessToken).apply()
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
private fun failedRegistration(message: String =
getString(R.string.registration_failed)){
connect_instance_button.isEnabled = true
editText.error = message
}
} }

View File

@ -9,4 +9,11 @@
<string name="invalid_username">Not a valid username</string> <string name="invalid_username">Not a valid username</string>
<string name="invalid_password">Password must be >5 characters</string> <string name="invalid_password">Password must be >5 characters</string>
<string name="login_failed">"Login failed"</string> <string name="login_failed">"Login failed"</string>
<string name="invalid_domain">"Invalid domain"</string>
<string name="registration_failed">"Could not register the application with this server"</string>
<string name="browser_launch_failed">"Could not launch a browser, do you have one?"</string>
<string name="auth_failed">"Could not authenticate"</string>
<string name="token_error">"Error getting token"</string>
</resources> </resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="auth_scheme" translatable="false">oauth2redirect</string>
</resources>

View File

@ -5,7 +5,6 @@ import com.github.tomakehurst.wiremock.junit.WireMockRule
import com.h.pixeldroid.api.PixelfedAPI import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.objects.* import com.h.pixeldroid.objects.*
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import retrofit2.Call import retrofit2.Call