diff --git a/app/build.gradle b/app/build.gradle index 34910b3e..cf117466 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,9 +4,10 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'jacoco' android { + compileSdkVersion 29 buildToolsVersion "29.0.3" -compileOptions { + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -18,6 +19,7 @@ compileOptions { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments clearPackageData: 'true' } sourceSets { main.java.srcDirs += 'src/main/java' @@ -36,6 +38,7 @@ compileOptions { } testOptions { animationsDisabled true + } } @@ -53,16 +56,17 @@ dependencies { implementation 'io.reactivex.rxjava2:rxjava:2.2.16' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 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' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - implementation 'com.google.android.material:material:1.1.0' - androidTestImplementation 'androidx.test:rules:1.3.0-alpha04' -} + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0' +} tasks.withType(Test) { jacoco.includeNoLocationClasses = true diff --git a/app/src/androidTest/java/com/h/pixeldroid/LoginInstrumentedTest.kt b/app/src/androidTest/java/com/h/pixeldroid/LoginInstrumentedTest.kt index 6f14d346..44868f7f 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/LoginInstrumentedTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/LoginInstrumentedTest.kt @@ -1,26 +1,28 @@ package com.h.pixeldroid -import android.view.View -import android.view.ViewGroup +import android.app.Activity +import android.content.Intent +import android.content.Intent.ACTION_VIEW +import android.net.Uri import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click 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.ext.junit.rules.ActivityScenarioRule -import androidx.test.platform.app.InstrumentationRegistry 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.Matchers -import org.hamcrest.TypeSafeMatcher - +import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* -import org.junit.Rule /** * Instrumented test, which will execute on an Android device. @@ -30,27 +32,69 @@ import org.junit.Rule @RunWith(AndroidJUnit4::class) class LoginInstrumentedTest { @get:Rule - var activityRule: ActivityScenarioRule - = ActivityScenarioRule(MainActivity::class.java) + var activityRule: ActivityScenarioRule + = ActivityScenarioRule(LoginActivity::class.java) + @Test fun clickConnect() { - onView(withId(R.id.button_start_login)).perform(click()) onView(withId(R.id.connect_instance_button)).check(matches(withText("Connect"))) } @Test 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.connect_instance_button)).perform(click()) onView(withId(R.id.editText)).check(matches(hasErrorText("Invalid domain"))) - } @Test 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.connect_instance_button)).perform(click()) 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 = 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")))) + + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 810151b0..624c641a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ + android:scheme="@string/auth_scheme" /> diff --git a/app/src/main/java/com/h/pixeldroid/LoginActivity.kt b/app/src/main/java/com/h/pixeldroid/LoginActivity.kt index 724089b9..72e8a5a3 100644 --- a/app/src/main/java/com/h/pixeldroid/LoginActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/LoginActivity.kt @@ -20,10 +20,10 @@ import retrofit2.Response class LoginActivity : AppCompatActivity() { - private val OAUTH_SCHEME = "oauth2redirect" - private val PACKAGE_ID = "com.h.pixeldroid" + private lateinit var OAUTH_SCHEME: String + private val PACKAGE_ID = BuildConfig.APPLICATION_ID private val SCOPE = "read write follow" - private val APP_NAME = "PixelDroid" + private lateinit var APP_NAME: String private lateinit var preferences: SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { @@ -31,12 +31,11 @@ class LoginActivity : AppCompatActivity() { setContentView(R.layout.activity_login) connect_instance_button.setOnClickListener { onClickConnect() } - + APP_NAME = getString(R.string.app_name) + OAUTH_SCHEME = getString(R.string.auth_scheme) preferences = getSharedPreferences( "$PACKAGE_ID.pref", Context.MODE_PRIVATE ) - - } override fun onStart(){ @@ -44,69 +43,40 @@ class LoginActivity : AppCompatActivity() { 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 error = url.getQueryParameter("error") + val code = url.getQueryParameter("code") + 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 { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - authenticationSuccessful(domain, response.body()?.access_token) - } else { - failedRegistration("Error getting token") - } - } - - override fun onFailure(call: Call, 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() { connect_instance_button.isEnabled = false - val domain = editText.text.toString() + val normalizedDomain = normalizeDomain(editText.text.toString()) - val normalizedDomain = normalizeDomain(domain) try{ - HttpUrl.Builder().host(domain).scheme("https").build() + HttpUrl.Builder().host(normalizedDomain).scheme("https").build() } catch (e: IllegalArgumentException) { - failedRegistration("Invalid domain") + failedRegistration(getString(R.string.invalid_domain)) return } preferences.edit() .putString("domain", normalizedDomain) .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 { override fun onResponse(call: Call, response: Response) { if (!response.isSuccessful) { @@ -118,13 +88,12 @@ class LoginActivity : AppCompatActivity() { val clientId = credentials?.client_id ?: return failedRegistration() val clientSecret = credentials.client_secret - preferences.edit() .putString("clientID", clientId) .putString("clientSecret", clientSecret) .apply() - redirect(normalizedDomain, clientId) + promptOAuth(normalizedDomain, clientId) } override fun onFailure(call: Call, t: Throwable) { @@ -132,28 +101,12 @@ class LoginActivity : AppCompatActivity() { return } } - - val pixelfedAPI = PixelfedAPI.create("https://$normalizedDomain") - pixelfedAPI.registerApplication( - APP_NAME, - "$OAUTH_SCHEME://$PACKAGE_ID", SCOPE - ) - .enqueue(callback) - + PixelfedAPI.create("https://$normalizedDomain").registerApplication( + APP_NAME,"$OAUTH_SCHEME://$PACKAGE_ID", SCOPE + ).enqueue(callback) } - private fun failedRegistration(message: 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) { + private fun promptOAuth(normalizedDomain: String, client_id: String) { val url = "https://$normalizedDomain/oauth/authorize?" + "client_id" + "=" + client_id + "&" + @@ -161,23 +114,67 @@ class LoginActivity : AppCompatActivity() { "response_type=code" + "&" + "scope=$SCOPE" - browser(this, url) - } - - private fun browser(context: Context, url: String) { val intent = CustomTabsIntent.Builder().build() try { - intent.launchUrl(context, Uri.parse(url)) + intent.launchUrl(this, Uri.parse(url)) } catch (e: ActivityNotFoundException) { val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) if (browserIntent.resolveActivity(packageManager) != null) { startActivity(browserIntent) } else { - failedRegistration(message="Could not launch a browser, do you have one?") + failedRegistration(getString(R.string.browser_launch_failed)) return } } 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 { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful || response.body() == null) { + failedRegistration(getString(R.string.token_error)) + return + } + authenticationSuccessful(domain, response.body()!!.access_token) + } + + override fun onFailure(call: Call, 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 + } + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9cb501b..252f133f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,11 @@ Not a valid username Password must be >5 characters "Login failed" + "Invalid domain" + "Could not register the application with this server" + "Could not launch a browser, do you have one?" + "Could not authenticate" + "Error getting token" + + diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml new file mode 100644 index 00000000..02a4ed79 --- /dev/null +++ b/app/src/main/res/values/values.xml @@ -0,0 +1,4 @@ + + + oauth2redirect + \ No newline at end of file diff --git a/app/src/test/java/com/h/pixeldroid/APIUnitTest.kt b/app/src/test/java/com/h/pixeldroid/APIUnitTest.kt index a317fb69..77dbe587 100644 --- a/app/src/test/java/com/h/pixeldroid/APIUnitTest.kt +++ b/app/src/test/java/com/h/pixeldroid/APIUnitTest.kt @@ -5,7 +5,6 @@ import com.github.tomakehurst.wiremock.junit.WireMockRule import com.h.pixeldroid.api.PixelfedAPI import com.h.pixeldroid.objects.* import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Rule import org.junit.Test import retrofit2.Call