From 02ce017c98a84548a873b4c0738fcc8e5af4f2b4 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Fri, 26 Jan 2024 21:39:00 -0600 Subject: [PATCH] Convert the LoginClient to kotlin (#5479) * Convert the result classes to kotlin * Convert response and callback to kotlin * Cleanup code-quality warnings before converting * Converted the LoginClient to kotlin * Updated the UserExtendedInfoClientTest to be kotlin, and live in the correct spot --- .../free/nrw/commons/auth/LoginActivity.java | 3 +- .../nrw/commons/auth/csrf/CsrfTokenClient.kt | 6 +- .../nrw/commons/auth/login/LoginCallback.kt | 8 + .../nrw/commons/auth/login/LoginClient.java | 259 ------------------ .../nrw/commons/auth/login/LoginClient.kt | 172 ++++++++++++ .../auth/login/LoginFailedException.kt | 3 + .../nrw/commons/auth/login/LoginInterface.kt | 4 +- .../commons/auth/login/LoginOAuthResult.java | 14 - .../auth/login/LoginResetPasswordResult.java | 13 - .../nrw/commons/auth/login/LoginResponse.kt | 64 +++++ .../nrw/commons/auth/login/LoginResult.java | 73 ----- .../nrw/commons/auth/login/LoginResult.kt | 39 +++ .../auth/login/UserExtendedInfoClientTest.kt | 78 ++++++ .../login/UserExtendedInfoClientTest.java | 84 ------ 14 files changed, 370 insertions(+), 450 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginFailedException.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginOAuthResult.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginResetPasswordResult.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/auth/login/UserExtendedInfoClientTest.kt delete mode 100644 app/src/test/kotlin/fr/free/nrw/commons/login/UserExtendedInfoClientTest.java diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 80b55e7dcf..86e0e033c5 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -31,11 +31,10 @@ import fr.free.nrw.commons.databinding.ActivityLoginBinding; import fr.free.nrw.commons.utils.ActivityUtils; import java.util.Locale; -import org.wikipedia.AppAdapter; import org.wikipedia.dataclient.ServiceFactory; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.dataclient.mwapi.MwQueryResponse; -import fr.free.nrw.commons.auth.login.LoginClient.LoginCallback; +import fr.free.nrw.commons.auth.login.LoginCallback; import javax.inject.Inject; import javax.inject.Named; diff --git a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt index fae982fe10..53e7ded0bd 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt @@ -8,8 +8,8 @@ import org.wikipedia.dataclient.SharedPreferenceCookieManager import org.wikipedia.dataclient.WikiSite import org.wikipedia.dataclient.mwapi.MwQueryResponse import fr.free.nrw.commons.auth.login.LoginClient -import fr.free.nrw.commons.auth.login.LoginClient.LoginCallback -import fr.free.nrw.commons.auth.login.LoginClient.LoginFailedException +import fr.free.nrw.commons.auth.login.LoginCallback +import fr.free.nrw.commons.auth.login.LoginFailedException import fr.free.nrw.commons.auth.login.LoginResult import retrofit2.Call import retrofit2.Response @@ -129,7 +129,7 @@ class CsrfTokenClient( ) = LoginClient() .request(csrfWikiSite, username, password, object : LoginCallback { override fun success(loginResult: LoginResult) { - if (loginResult.pass()) { + if (loginResult.pass) { sessionManager.updateAccount(loginResult) retryCallback() } else { diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt new file mode 100644 index 0000000000..cfaf90b58d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt @@ -0,0 +1,8 @@ +package fr.free.nrw.commons.auth.login + +interface LoginCallback { + fun success(loginResult: LoginResult) + fun twoFactorPrompt(caught: Throwable, token: String?) + fun passwordResetPrompt(token: String?) + fun error(caught: Throwable) +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.java b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.java deleted file mode 100644 index be8661e4d6..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.java +++ /dev/null @@ -1,259 +0,0 @@ -package fr.free.nrw.commons.auth.login; - -import android.annotation.SuppressLint; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -import org.apache.commons.lang3.StringUtils; -import org.wikipedia.dataclient.Service; -import org.wikipedia.dataclient.ServiceFactory; -import org.wikipedia.dataclient.WikiSite; -import org.wikipedia.dataclient.mwapi.ListUserResponse; -import org.wikipedia.dataclient.mwapi.MwQueryResponse; -import org.wikipedia.dataclient.mwapi.MwServiceError; -import org.wikipedia.util.log.L; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -/** - * Responsible for making login related requests to the server. - */ -public class LoginClient { - @Nullable private Call tokenCall; - @Nullable private Call loginCall; - /** - * userLanguage - * It holds the value of the user's device language code. - * For example, if user's device language is English it will hold En - * The value will be fetched when the user clicks Login Button in the LoginActivity - */ - @NonNull private String userLanguage; - - public interface LoginCallback { - void success(@NonNull LoginResult result); - void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token); - void passwordResetPrompt(@Nullable String token); - void error(@NonNull Throwable caught); - } - - public void request(@NonNull final WikiSite wiki, @NonNull final String userName, - @NonNull final String password, @NonNull final LoginCallback cb) { - cancel(); - - tokenCall = ServiceFactory.get(wiki, LoginInterface.class).getLoginToken(); - tokenCall.enqueue(new Callback() { - @Override public void onResponse(@NonNull Call call, - @NonNull Response response) { - login(wiki, userName, password, null, null, response.body().query().loginToken(), - userLanguage, cb); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable caught) { - if (call.isCanceled()) { - return; - } - cb.error(caught); - } - }); - } - - public void login(@NonNull final WikiSite wiki, @NonNull final String userName, @NonNull final String password, - @Nullable final String retypedPassword, @Nullable final String twoFactorCode, - @Nullable final String loginToken, @NonNull final String userLanguage, @NonNull final LoginCallback cb) { - this.userLanguage = userLanguage; - loginCall = TextUtils.isEmpty(twoFactorCode) && TextUtils.isEmpty(retypedPassword) - ? ServiceFactory.get(wiki, LoginInterface.class).postLogIn(userName, password, loginToken, userLanguage, Service.WIKIPEDIA_URL) - : ServiceFactory.get(wiki, LoginInterface.class).postLogIn(userName, password, retypedPassword, twoFactorCode, loginToken, - userLanguage, true); - loginCall.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - LoginResponse loginResponse = response.body(); - LoginResult loginResult = loginResponse.toLoginResult(wiki, password); - if (loginResult != null) { - if (loginResult.pass() && !TextUtils.isEmpty(loginResult.getUserName())) { - // The server could do some transformations on user names, e.g. on some - // wikis is uppercases the first letter. - String actualUserName = loginResult.getUserName(); - getExtendedInfo(wiki, actualUserName, loginResult, cb); - } else if ("UI".equals(loginResult.getStatus())) { - if (loginResult instanceof LoginOAuthResult) { - cb.twoFactorPrompt(new LoginFailedException(loginResult.getMessage()), loginToken); - } else if (loginResult instanceof LoginResetPasswordResult) { - cb.passwordResetPrompt(loginToken); - } else { - cb.error(new LoginFailedException(loginResult.getMessage())); - } - } else { - cb.error(new LoginFailedException(loginResult.getMessage())); - } - } else { - cb.error(new IOException("Login failed. Unexpected response.")); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - if (call.isCanceled()) { - return; - } - cb.error(t); - } - }); - } - - public void loginBlocking(@NonNull final WikiSite wiki, @NonNull final String userName, - @NonNull final String password, @Nullable final String twoFactorCode) throws Throwable { - Response tokenResponse = ServiceFactory.get(wiki, LoginInterface.class).getLoginToken().execute(); - if (tokenResponse.body() == null || TextUtils.isEmpty(tokenResponse.body().query().loginToken())) { - throw new IOException("Unexpected response when getting login token."); - } - String loginToken = tokenResponse.body().query().loginToken(); - - Call tempLoginCall = StringUtils.defaultIfEmpty(twoFactorCode, "").isEmpty() - ? ServiceFactory.get(wiki, LoginInterface.class).postLogIn(userName, password, loginToken, userLanguage, Service.WIKIPEDIA_URL) - : ServiceFactory.get(wiki, LoginInterface.class).postLogIn(userName, password, null, twoFactorCode, loginToken, - userLanguage, true); - Response response = tempLoginCall.execute(); - LoginResponse loginResponse = response.body(); - if (loginResponse == null) { - throw new IOException("Unexpected response when logging in."); - } - LoginResult loginResult = loginResponse.toLoginResult(wiki, password); - if (loginResult == null) { - throw new IOException("Unexpected response when logging in."); - } - if ("UI".equals(loginResult.getStatus())) { - if (loginResult instanceof LoginOAuthResult) { - - // TODO: Find a better way to boil up the warning about 2FA - throw new LoginFailedException(loginResult.getMessage()); - - } else { - throw new LoginFailedException(loginResult.getMessage()); - } - } else if (!loginResult.pass() || TextUtils.isEmpty(loginResult.getUserName())) { - throw new LoginFailedException(loginResult.getMessage()); - } - } - - @SuppressLint("CheckResult") - private void getExtendedInfo(@NonNull final WikiSite wiki, @NonNull String userName, - @NonNull final LoginResult loginResult, @NonNull final LoginCallback cb) { - ServiceFactory.get(wiki, LoginInterface.class).getUserInfo(userName) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(response -> { - ListUserResponse user = response.query().getUserResponse(userName); - int id = response.query().userInfo().id(); - loginResult.setUserId(id); - loginResult.setGroups(user.getGroups()); - cb.success(loginResult); - L.v("Found user ID " + id + " for " + wiki.subdomain()); - }, caught -> { - L.e("Login succeeded but getting group information failed. " + caught); - cb.error(caught); - }); - } - - public void cancel() { - cancelTokenRequest(); - cancelLogin(); - } - - private void cancelTokenRequest() { - if (tokenCall == null) { - return; - } - tokenCall.cancel(); - tokenCall = null; - } - - private void cancelLogin() { - if (loginCall == null) { - return; - } - loginCall.cancel(); - loginCall = null; - } - - public static final class LoginResponse { - @SuppressWarnings("unused") @SerializedName("error") @Nullable - private MwServiceError error; - - @SuppressWarnings("unused") @SerializedName("clientlogin") @Nullable - private ClientLogin clientLogin; - - @Nullable public MwServiceError getError() { - return error; - } - - @Nullable LoginResult toLoginResult(@NonNull WikiSite site, @NonNull String password) { - return clientLogin != null ? clientLogin.toLoginResult(site, password) : null; - } - - private static class ClientLogin { - @SuppressWarnings("unused,NullableProblems") @NonNull private String status; - @SuppressWarnings("unused") @Nullable private List requests; - @SuppressWarnings("unused") @Nullable private String message; - @SuppressWarnings("unused") @SerializedName("username") @Nullable private String userName; - - LoginResult toLoginResult(@NonNull WikiSite site, @NonNull String password) { - String userMessage = message; - if ("UI".equals(status)) { - if (requests != null) { - for (Request req : requests) { - if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest".equals(req.id())) { - return new LoginOAuthResult(site, status, userName, password, message); - } else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest".equals(req.id())) { - return new LoginResetPasswordResult(site, status, userName, password, message); - } - } - } - } else if (!"PASS".equals(status) && !"FAIL".equals(status)) { - //TODO: String resource -- Looks like needed for others in this class too - userMessage = "An unknown error occurred."; - } - return new LoginResult(site, status, userName, password, userMessage); - } - } - - private static class Request { - @SuppressWarnings("unused") @Nullable private String id; - //@SuppressWarnings("unused") @Nullable private JsonObject metadata; - @SuppressWarnings("unused") @Nullable private String required; - @SuppressWarnings("unused") @Nullable private String provider; - @SuppressWarnings("unused") @Nullable private String account; - @SuppressWarnings("unused") @Nullable private Map fields; - - @Nullable String id() { - return id; - } - } - - private static class RequestField { - @SuppressWarnings("unused") @Nullable private String type; - @SuppressWarnings("unused") @Nullable private String label; - @SuppressWarnings("unused") @Nullable private String help; - } - } - - public static class LoginFailedException extends Throwable { - public LoginFailedException(String message) { - super(message); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt new file mode 100644 index 0000000000..22374cc1f1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt @@ -0,0 +1,172 @@ +package fr.free.nrw.commons.auth.login + +import android.text.TextUtils +import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult +import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.wikipedia.dataclient.Service +import org.wikipedia.dataclient.ServiceFactory +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.mwapi.MwQueryResponse +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import timber.log.Timber +import java.io.IOException + +/** + * Responsible for making login related requests to the server. + */ +class LoginClient { + private var tokenCall: Call? = null + private var loginCall: Call? = null + + /** + * userLanguage + * It holds the value of the user's device language code. + * For example, if user's device language is English it will hold En + * The value will be fetched when the user clicks Login Button in the LoginActivity + */ + private var userLanguage = "" + + fun request(wiki: WikiSite, userName: String, password: String, cb: LoginCallback) { + cancel() + + tokenCall = ServiceFactory.get(wiki, LoginInterface::class.java).getLoginToken() + tokenCall!!.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + login(wiki, userName, password, null, null, + response.body()!!.query()!!.loginToken(), userLanguage, cb) + } + + override fun onFailure(call: Call, caught: Throwable) { + if (call.isCanceled) { + return + } + cb.error(caught) + } + }) + } + + fun login( + wiki: WikiSite, userName: String, password: String, retypedPassword: String?, + twoFactorCode: String?, loginToken: String?, userLanguage: String, cb: LoginCallback + ) { + this.userLanguage = userLanguage + + loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { + ServiceFactory.get(wiki, LoginInterface::class.java) + .postLogIn(userName, password, loginToken, userLanguage, Service.WIKIPEDIA_URL) + } else { + ServiceFactory.get(wiki, LoginInterface::class.java).postLogIn( + userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true + ) + } + + loginCall!!.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + val loginResult = response.body()?.toLoginResult(wiki, password) + if (loginResult != null) { + if (loginResult.pass && !loginResult.userName.isNullOrEmpty()) { + // The server could do some transformations on user names, e.g. on some + // wikis is uppercases the first letter. + getExtendedInfo(wiki, loginResult.userName, loginResult, cb) + } else if ("UI" == loginResult.status) { + when (loginResult) { + is OAuthResult -> cb.twoFactorPrompt( + LoginFailedException(loginResult.message), + loginToken + ) + + is ResetPasswordResult -> cb.passwordResetPrompt(loginToken) + + is LoginResult.Result -> cb.error( + LoginFailedException(loginResult.message) + ) + } + } else { + cb.error(LoginFailedException(loginResult.message)) + } + } else { + cb.error(IOException("Login failed. Unexpected response.")) + } + } + + override fun onFailure(call: Call, t: Throwable) { + if (call.isCanceled) { + return + } + cb.error(t) + } + }) + } + + @Throws(Throwable::class) + fun loginBlocking(wiki: WikiSite, userName: String, password: String, twoFactorCode: String?) { + val tokenResponse = ServiceFactory.get(wiki, LoginInterface::class.java).getLoginToken().execute() + if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) { + throw IOException("Unexpected response when getting login token.") + } + + val loginToken = tokenResponse.body()?.query()?.loginToken() + val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) { + ServiceFactory.get(wiki, LoginInterface::class.java).postLogIn( + userName, password, loginToken, userLanguage, Service.WIKIPEDIA_URL + ) + } else { + ServiceFactory.get(wiki, LoginInterface::class.java).postLogIn( + userName, password, null, twoFactorCode, loginToken, userLanguage, true + ) + } + + val response = tempLoginCall.execute() + val loginResponse = response.body() ?: throw IOException("Unexpected response when logging in.") + val loginResult = loginResponse.toLoginResult(wiki, password) ?: throw IOException("Unexpected response when logging in.") + + if ("UI" == loginResult.status) { + if (loginResult is OAuthResult) { + // TODO: Find a better way to boil up the warning about 2FA + throw LoginFailedException(loginResult.message) + } + throw LoginFailedException(loginResult.message) + } + + if (!loginResult.pass || TextUtils.isEmpty(loginResult.userName)) { + throw LoginFailedException(loginResult.message) + } + } + + private fun getExtendedInfo( + wiki: WikiSite, userName: String, loginResult: LoginResult, cb: LoginCallback + ) = ServiceFactory.get(wiki, LoginInterface::class.java).getUserInfo(userName) + .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response: MwQueryResponse? -> + loginResult.userId = response?.query()?.userInfo()?.id() ?: 0 + loginResult.groups = response?.query()?.getUserResponse(userName)?.groups ?: emptySet() + cb.success(loginResult) + Timber.v( + "Found user ID %s for %s", + response?.query()?.userInfo()?.id(), + wiki.subdomain() + ) + }, { caught: Throwable -> + Timber.e(caught, "Login succeeded but getting group information failed. ") + cb.error(caught) + }) + + fun cancel() { + tokenCall?.let { + it.cancel() + tokenCall = null + } + + loginCall?.let { + it.cancel() + loginCall = null + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginFailedException.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginFailedException.kt new file mode 100644 index 0000000000..2f60b2071c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginFailedException.kt @@ -0,0 +1,3 @@ +package fr.free.nrw.commons.auth.login + +class LoginFailedException(message: String?) : Throwable(message) diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt index 49b38db3ad..fbd25dcf06 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt @@ -25,7 +25,7 @@ interface LoginInterface { @Field("logintoken") token: String?, @Field("uselang") userLanguage: String?, @Field("loginreturnurl") url: String? - ): Call + ): Call @Headers("Cache-Control: no-cache") @FormUrlEncoded @@ -38,7 +38,7 @@ interface LoginInterface { @Field("logintoken") token: String?, @Field("uselang") userLanguage: String?, @Field("logincontinue") loginContinue: Boolean - ): Call + ): Call @GET(Service.MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate") fun getUserInfo(@Query("ususers") userName: String): Observable diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginOAuthResult.java b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginOAuthResult.java deleted file mode 100644 index 80a8739259..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginOAuthResult.java +++ /dev/null @@ -1,14 +0,0 @@ -package fr.free.nrw.commons.auth.login; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.wikipedia.dataclient.WikiSite; - -public class LoginOAuthResult extends LoginResult { - - public LoginOAuthResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName, - @Nullable String password, @Nullable String message) { - super(site, status, userName, password, message); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResetPasswordResult.java b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResetPasswordResult.java deleted file mode 100644 index 490e544a1e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResetPasswordResult.java +++ /dev/null @@ -1,13 +0,0 @@ -package fr.free.nrw.commons.auth.login; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.wikipedia.dataclient.WikiSite; - -public class LoginResetPasswordResult extends LoginResult { - public LoginResetPasswordResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName, - @Nullable String password, @Nullable String message) { - super(site, status, userName, password, message); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt new file mode 100644 index 0000000000..01bf1160c0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.auth.login + +import com.google.gson.annotations.SerializedName +import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult +import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult +import fr.free.nrw.commons.auth.login.LoginResult.Result +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.mwapi.MwServiceError + +class LoginResponse { + @SerializedName("error") + val error: MwServiceError? = null + + @SerializedName("clientlogin") + private val clientLogin: ClientLogin? = null + + fun toLoginResult(site: WikiSite, password: String): LoginResult? { + return clientLogin?.toLoginResult(site, password) + } +} + +internal class ClientLogin { + private val status: String? = null + private val requests: List? = null + private val message: String? = null + + @SerializedName("username") + private val userName: String? = null + + fun toLoginResult(site: WikiSite, password: String): LoginResult { + var userMessage = message + if ("UI" == status) { + if (requests != null) { + for (req in requests) { + if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) { + return OAuthResult(site, status, userName, password, message) + } else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) { + return ResetPasswordResult(site, status, userName, password, message) + } + } + } + } else if ("PASS" != status && "FAIL" != status) { + //TODO: String resource -- Looks like needed for others in this class too + userMessage = "An unknown error occurred." + } + return Result(site, status ?: "", userName, password, userMessage) + } +} + +internal class Request { + private val id: String? = null + private val required: String? = null + private val provider: String? = null + private val account: String? = null + private val fields: Map? = null + + fun id(): String? = id +} + +internal class RequestField { + private val type: String? = null + private val label: String? = null + private val help: String? = null +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.java b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.java deleted file mode 100644 index 9900675115..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.java +++ /dev/null @@ -1,73 +0,0 @@ -package fr.free.nrw.commons.auth.login; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.wikipedia.dataclient.WikiSite; - -import java.util.Collections; -import java.util.Set; - -public class LoginResult { - @NonNull private final WikiSite site; - @NonNull private final String status; - @Nullable private final String userName; - @Nullable private final String password; - @Nullable private final String message; - - private int userId; - @NonNull private Set groups = Collections.emptySet(); - - public LoginResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName, - @Nullable String password, @Nullable String message) { - this.site = site; - this.status = status; - this.userName = userName; - this.password = password; - this.message = message; - } - - @NonNull public WikiSite getSite() { - return site; - } - - @NonNull public String getStatus() { - return status; - } - - public boolean pass() { - return "PASS".equals(status); - } - - public boolean fail() { - return "FAIL".equals(status); - } - - @Nullable public String getUserName() { - return userName; - } - - @Nullable public String getPassword() { - return password; - } - - @Nullable public String getMessage() { - return message; - } - - public void setUserId(int id) { - this.userId = id; - } - - public int getUserId() { - return userId; - } - - public void setGroups(@NonNull Set groups) { - this.groups = groups; - } - - @NonNull public Set getGroups() { - return groups; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt new file mode 100644 index 0000000000..f0bbb2107e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.auth.login + +import org.wikipedia.dataclient.WikiSite + +sealed class LoginResult( + val site: WikiSite, + val status: String, + val userName: String?, + val password: String?, + val message: String? +) { + var userId = 0 + var groups = emptySet() + val pass: Boolean get() = "PASS" == status + + class Result( + site: WikiSite, + status: String, + userName: String?, + password: String?, + message: String? + ): LoginResult(site, status, userName, password, message) + + class OAuthResult( + site: WikiSite, + status: String, + userName: String?, + password: String?, + message: String? + ) : LoginResult(site, status, userName, password, message) + + class ResetPasswordResult( + site: WikiSite, + status: String, + userName: String?, + password: String?, + message: String? + ) : LoginResult(site, status, userName, password, message) +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/login/UserExtendedInfoClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/login/UserExtendedInfoClientTest.kt new file mode 100644 index 0000000000..4408cf05a9 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/login/UserExtendedInfoClientTest.kt @@ -0,0 +1,78 @@ +package fr.free.nrw.commons.auth.login + +import android.net.Uri +import com.google.gson.GsonBuilder +import com.google.gson.stream.MalformedJsonException +import fr.free.nrw.commons.MockWebServerTest +import io.reactivex.observers.TestObserver +import org.junit.Before +import org.junit.Test +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.mwapi.MwQueryResponse +import org.wikipedia.json.NamespaceTypeAdapter +import org.wikipedia.json.PostProcessingTypeAdapter +import org.wikipedia.json.UriTypeAdapter +import org.wikipedia.json.WikiSiteTypeAdapter +import org.wikipedia.page.Namespace +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory + +class UserExtendedInfoClientTest : MockWebServerTest() { + private var apiService: LoginInterface? = null + private val observer = TestObserver() + private val gson = GsonBuilder() + .registerTypeHierarchyAdapter(Uri::class.java, UriTypeAdapter().nullSafe()) + .registerTypeHierarchyAdapter(Namespace::class.java, NamespaceTypeAdapter().nullSafe()) + .registerTypeAdapter(WikiSite::class.java, WikiSiteTypeAdapter().nullSafe()) + .registerTypeAdapterFactory(PostProcessingTypeAdapter()) + .create() + + @Before + @Throws(Throwable::class) + override fun setUp() { + super.setUp() + + apiService = Retrofit.Builder() + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .baseUrl(server().url) + .build() + .create(LoginInterface::class.java) + } + + @Test + @Throws(Throwable::class) + fun testRequestSuccess() { + enqueueFromFile("user_extended_info.json") + + apiService!!.getUserInfo("USER").subscribe(observer) + + observer + .assertComplete() + .assertNoErrors() + .assertValue { result: MwQueryResponse -> + result.query()!! + .userInfo()!!.id() == 24531888 && result.query()!!.getUserResponse("USER")!! + .name() == "USER" + } + } + + @Test + fun testRequestResponse404() { + enqueue404() + + apiService!!.getUserInfo("USER").subscribe(observer) + + observer.assertError(Exception::class.java) + } + + @Test + fun testRequestResponseMalformed() { + enqueueMalformed() + + apiService!!.getUserInfo("USER").subscribe(observer) + + observer.assertError(MalformedJsonException::class.java) + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/login/UserExtendedInfoClientTest.java b/app/src/test/kotlin/fr/free/nrw/commons/login/UserExtendedInfoClientTest.java deleted file mode 100644 index 1f2df96d8c..0000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/login/UserExtendedInfoClientTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package fr.free.nrw.commons.login; - -import android.net.Uri; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.stream.MalformedJsonException; -import fr.free.nrw.commons.MockWebServerTest; -import fr.free.nrw.commons.auth.login.LoginInterface; -import io.reactivex.observers.TestObserver; -import org.junit.Before; -import org.junit.Test; -import org.wikipedia.dataclient.WikiSite; -import org.wikipedia.dataclient.mwapi.MwQueryResponse; -import org.wikipedia.json.NamespaceTypeAdapter; -import org.wikipedia.json.PostProcessingTypeAdapter; -import org.wikipedia.json.UriTypeAdapter; -import org.wikipedia.json.WikiSiteTypeAdapter; -import org.wikipedia.page.Namespace; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; - -public class UserExtendedInfoClientTest extends MockWebServerTest { - - private LoginInterface apiService; - - @Override - @Before - public void setUp() throws Throwable { - super.setUp(); - - apiService = new Retrofit.Builder() - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .addConverterFactory(GsonConverterFactory.create(getGson())) - .baseUrl(server().getUrl()) - .build() - .create(LoginInterface.class); - } - - @Test - public void testRequestSuccess() throws Throwable { - enqueueFromFile("user_extended_info.json"); - TestObserver observer = new TestObserver<>(); - - apiService.getUserInfo("USER").subscribe(observer); - - observer - .assertComplete() - .assertNoErrors() - .assertValue( - result -> result.query().userInfo().id() == 24531888 - && result.query().getUserResponse("USER").name().equals("USER") - ); - } - - @Test - public void testRequestResponse404() { - enqueue404(); - TestObserver observer = new TestObserver<>(); - - apiService.getUserInfo("USER").subscribe(observer); - - observer.assertError(Exception.class); - } - - @Test - public void testRequestResponseMalformed() { - enqueueMalformed(); - TestObserver observer = new TestObserver<>(); - - apiService.getUserInfo("USER").subscribe(observer); - - observer.assertError(MalformedJsonException.class); - } - - private Gson getGson() { - return new GsonBuilder() - .registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe()) - .registerTypeHierarchyAdapter(Namespace.class, new NamespaceTypeAdapter().nullSafe()) - .registerTypeAdapter(WikiSite.class, new WikiSiteTypeAdapter().nullSafe()) - .registerTypeAdapterFactory(new PostProcessingTypeAdapter()) - .create(); - } -}