From 97a208dcfa5e589e3610d268b16d4cc8a55a1a61 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Wed, 24 Jan 2024 07:15:51 -0600 Subject: [PATCH] Refactor CSRF token API to move it into the main commons code base (#5472) * Remove redundent constructor parameter * Converted the CsrfTokenClient and test to kotlin * Moved getCsrfTokenCall() out of the data client --- .../nrw/commons/actions/PageEditClient.kt | 9 +- .../free/nrw/commons/actions/ThanksClient.kt | 2 +- .../commons/auth/csrf/CsrfTokenClient.java | 247 ------------------ .../nrw/commons/auth/csrf/CsrfTokenClient.kt | 163 ++++++++++++ .../commons/auth/csrf/CsrfTokenInterface.kt | 13 + .../free/nrw/commons/di/NetworkingModule.java | 2 +- .../notification/NotificationClient.kt | 2 +- .../nrw/commons/actions/PageEditClientTest.kt | 10 +- .../nrw/commons/actions/ThanksClientTest.kt | 2 +- .../auth/csrf/CsrfTokenClientTest.java | 82 ------ .../commons/auth/csrf/CsrfTokenClientTest.kt | 71 +++++ .../notification/NotificationClientTest.kt | 2 +- .../wikidata/WikiBaseClientUnitTest.kt | 2 +- .../org/wikipedia/dataclient/Service.java | 4 - 14 files changed, 263 insertions(+), 348 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenInterface.kt delete mode 100644 app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt index 88c4ff9a3..077a3a0bb 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/PageEditClient.kt @@ -25,7 +25,7 @@ class PageEditClient( */ fun edit(pageTitle: String, text: String, summary: String): Observable { return try { - pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.tokenBlocking) + pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking()) .map { editResponse -> editResponse.edit()!!.editSucceeded() } } catch (throwable: Throwable) { Observable.just(false) @@ -41,7 +41,7 @@ class PageEditClient( */ fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable { return try { - pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.tokenBlocking) + pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking()) .map { editResponse -> editResponse.edit()!!.editSucceeded() } } catch (throwable: Throwable) { Observable.just(false) @@ -57,7 +57,7 @@ class PageEditClient( */ fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable { return try { - pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.tokenBlocking) + pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking()) .map { editResponse -> editResponse.edit()!!.editSucceeded() } } catch (throwable: Throwable) { Observable.just(false) @@ -76,7 +76,8 @@ class PageEditClient( language: String, value: String) : Observable{ return try { pageEditInterface.postCaptions(summary, title, language, - value, csrfTokenClient.tokenBlocking).map { it.success } + value, csrfTokenClient.getTokenBlocking() + ).map { it.success } } catch (throwable: Throwable) { Observable.just(0) } diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt index 3e798176a..2401c9bad 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt @@ -27,7 +27,7 @@ class ThanksClient @Inject constructor( service.thank( revisionId.toString(), // Rev null, // Log - csrfTokenClient.tokenBlocking, // Token + csrfTokenClient.getTokenBlocking(), // Token CommonsApplication.getInstance().userAgent // Source ).map { mwThankPostResponse -> mwThankPostResponse.result?.success == 1 diff --git a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.java b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.java deleted file mode 100644 index 3d631f8ea..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.java +++ /dev/null @@ -1,247 +0,0 @@ -package fr.free.nrw.commons.auth.csrf; - -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; -import org.wikipedia.AppAdapter; -import org.wikipedia.dataclient.Service; -import org.wikipedia.dataclient.ServiceFactory; -import org.wikipedia.dataclient.SharedPreferenceCookieManager; -import org.wikipedia.dataclient.WikiSite; -import org.wikipedia.dataclient.mwapi.MwQueryResponse; -import org.wikipedia.login.LoginClient; -import org.wikipedia.login.LoginResult; -import org.wikipedia.util.log.L; - -import java.io.IOException; - -import retrofit2.Call; -import retrofit2.Response; - -public class CsrfTokenClient { - private static final String ANON_TOKEN = "+\\"; - private static final int MAX_RETRIES = 1; - private static final int MAX_RETRIES_OF_LOGIN_BLOCKING = 2; - @NonNull private final WikiSite csrfWikiSite; - @NonNull private final WikiSite loginWikiSite; - private int retries = 0; - - @Nullable private Call csrfTokenCall; - @NonNull private LoginClient loginClient = new LoginClient(); - - public CsrfTokenClient(@NonNull WikiSite csrfWikiSite, @NonNull WikiSite loginWikiSite) { - this.csrfWikiSite = csrfWikiSite; - this.loginWikiSite = loginWikiSite; - } - - public void request(@NonNull final Callback callback) { - request(false, callback); - } - - public void request(boolean forceLogin, @NonNull final Callback callback) { - cancel(); - if (forceLogin) { - retryWithLogin(new RuntimeException("Forcing login..."), callback); - return; - } - csrfTokenCall = request(ServiceFactory.get(csrfWikiSite), callback); - } - - public void cancel() { - loginClient.cancel(); - if (csrfTokenCall != null) { - csrfTokenCall.cancel(); - csrfTokenCall = null; - } - } - - @VisibleForTesting - @NonNull - Call request(@NonNull Service service, @NonNull final Callback cb) { - return requestToken(service, new CsrfTokenClient.Callback() { - @Override public void success(@NonNull String token) { - if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) { - retryWithLogin(new RuntimeException("App believes we're logged in, but got anonymous token."), cb); - } else { - cb.success(token); - } - } - - @Override public void failure(@NonNull Throwable caught) { - retryWithLogin(caught, cb); - } - - @Override - public void twoFactorPrompt() { - cb.twoFactorPrompt(); - } - }); - } - - private void retryWithLogin(@NonNull Throwable caught, @NonNull final Callback callback) { - if (retries < MAX_RETRIES - && !TextUtils.isEmpty(AppAdapter.get().getUserName()) - && !TextUtils.isEmpty(AppAdapter.get().getPassword())) { - retries++; - - SharedPreferenceCookieManager.getInstance().clearAllCookies(); - - login(AppAdapter.get().getUserName(), AppAdapter.get().getPassword(), () -> { - L.i("retrying..."); - request(callback); - }, callback); - } else { - callback.failure(caught); - } - } - - private void login(@NonNull final String username, @NonNull final String password, - @NonNull final RetryCallback retryCallback, - @NonNull final Callback callback) { - new LoginClient().request(loginWikiSite, username, password, - new LoginClient.LoginCallback() { - @Override - public void success(@NonNull LoginResult loginResult) { - if (loginResult.pass()) { - AppAdapter.get().updateAccount(loginResult); - retryCallback.retry(); - } else { - callback.failure(new LoginClient.LoginFailedException(loginResult.getMessage())); - } - } - - @Override - public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) { - callback.twoFactorPrompt(); - } - - @Override public void passwordResetPrompt(@Nullable String token) { - // Should not happen here, but call the callback just in case. - callback.failure(new LoginClient.LoginFailedException("Logged in with temporary password.")); - } - - @Override - public void error(@NonNull Throwable caught) { - callback.failure(caught); - } - }); - } - - @NonNull public String getTokenBlocking() throws Throwable { - String token = ""; - Service service = ServiceFactory.get(csrfWikiSite); - - for (int retry = 0; retry < MAX_RETRIES_OF_LOGIN_BLOCKING; retry++) { - try { - if (retry > 0) { - // Log in explicitly - new LoginClient().loginBlocking(loginWikiSite, AppAdapter.get().getUserName(), - AppAdapter.get().getPassword(), ""); - } - - // Get CSRFToken response off the main thread. - Response response = Executors.newSingleThreadExecutor().submit(new CsrfTokenCallExecutor(service)).get(); - - if (response.body() == null || response.body().query() == null - || TextUtils.isEmpty(response.body().query().csrfToken())) { - continue; - } - token = response.body().query().csrfToken(); - if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) { - throw new RuntimeException("App believes we're logged in, but got anonymous token."); - } - break; - } catch (Throwable t) { - L.w(t); - } - } - if (TextUtils.isEmpty(token) || token.equals(ANON_TOKEN)) { - throw new IOException("Invalid token, or login failure."); - } - return token; - } - - @VisibleForTesting @NonNull Call requestToken(@NonNull Service service, - @NonNull final Callback cb) { - Call call = service.getCsrfTokenCall(); - call.enqueue(new retrofit2.Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (call.isCanceled()) { - return; - } - cb.success(response.body().query().csrfToken()); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - if (call.isCanceled()) { - return; - } - cb.failure(t); - } - }); - return call; - } - - public interface Callback { - void success(@NonNull String token); - void failure(@NonNull Throwable caught); - void twoFactorPrompt(); - } - - public static class DefaultCallback implements Callback { - @Override - public void success(@NonNull String token) { - } - - @Override - public void failure(@NonNull Throwable caught) { - L.e(caught); - } - - @Override - public void twoFactorPrompt() { - // TODO: - } - } - - private interface RetryCallback { - void retry(); - } - - /** - * Class CsrfTokenCallExecutor which implement callable interface to get CsrfTokenCall. - */ - class CsrfTokenCallExecutor implements Callable> { - - /** - * Service for token call. - */ - private Service service; - - /** - * Default Constructor. - * @param service - */ - public CsrfTokenCallExecutor(Service service){ - this.service = service; - } - - /** - * Computes a result, or throws an exception if unable to do so. - * - * @return computed result - * @throws Exception if unable to compute a result - */ - @Override - public Response call() throws Exception { - return service.getCsrfTokenCall().execute(); - } - } -} 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 new file mode 100644 index 000000000..e3e45b642 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt @@ -0,0 +1,163 @@ +package fr.free.nrw.commons.auth.csrf + +import androidx.annotation.VisibleForTesting +import org.wikipedia.AppAdapter +import org.wikipedia.dataclient.ServiceFactory +import org.wikipedia.dataclient.SharedPreferenceCookieManager +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.mwapi.MwQueryResponse +import org.wikipedia.login.LoginClient +import org.wikipedia.login.LoginClient.LoginCallback +import org.wikipedia.login.LoginClient.LoginFailedException +import org.wikipedia.login.LoginResult +import retrofit2.Call +import retrofit2.Response +import timber.log.Timber +import java.io.IOException +import java.util.concurrent.Callable +import java.util.concurrent.Executors.newSingleThreadExecutor + +class CsrfTokenClient(private val csrfWikiSite: WikiSite) { + private var retries = 0 + private var csrfTokenCall: Call? = null + private val loginClient = LoginClient() + + @Throws(Throwable::class) + fun getTokenBlocking(): String { + var token = "" + val service = ServiceFactory.get(csrfWikiSite, CsrfTokenInterface::class.java) + val userName = AppAdapter.get().getUserName() + val password = AppAdapter.get().getPassword() + + for (retry in 0 until MAX_RETRIES_OF_LOGIN_BLOCKING) { + try { + if (retry > 0) { + // Log in explicitly + LoginClient().loginBlocking(csrfWikiSite, userName, password, "") + } + + // Get CSRFToken response off the main thread. + val response = newSingleThreadExecutor().submit(Callable { + service.getCsrfTokenCall().execute() + }).get() + + if (response.body()?.query()?.csrfToken().isNullOrEmpty()) { + continue + } + + token = response.body()!!.query()!!.csrfToken()!! + if (AppAdapter.get().isLoggedIn() && token == ANON_TOKEN) { + throw RuntimeException("App believes we're logged in, but got anonymous token.") + } + break + } catch (t: Throwable) { + Timber.w(t) + } + } + + if (token.isEmpty() || token == ANON_TOKEN) { + throw IOException("Invalid token, or login failure.") + } + return token + } + + @VisibleForTesting + fun request(service: CsrfTokenInterface, cb: Callback): Call = + requestToken(service, object : Callback { + override fun success(token: String?) { + if (AppAdapter.get().isLoggedIn() && token == ANON_TOKEN) { + retryWithLogin(cb) { + RuntimeException("App believes we're logged in, but got anonymous token.") + } + } else { + cb.success(token) + } + } + + override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught } + + override fun twoFactorPrompt() = cb.twoFactorPrompt() + }) + + @VisibleForTesting + fun requestToken(service: CsrfTokenInterface, cb: Callback): Call { + val call = service.getCsrfTokenCall() + call.enqueue(object : retrofit2.Callback { + override fun onResponse(call: Call, response: Response) { + if (call.isCanceled) { + return + } + cb.success(response.body()!!.query()!!.csrfToken()) + } + + override fun onFailure(call: Call, t: Throwable) { + if (call.isCanceled) { + return + } + cb.failure(t) + } + }) + return call + } + + private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) { + val userName = AppAdapter.get().getUserName() + val password = AppAdapter.get().getPassword() + if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) { + retries++ + SharedPreferenceCookieManager.getInstance().clearAllCookies() + login(userName, password, callback) { + Timber.i("retrying...") + cancel() + csrfTokenCall = request(ServiceFactory.get(csrfWikiSite, CsrfTokenInterface::class.java), callback) + } + } else { + callback.failure(caught()) + } + } + + private fun login( + username: String, + password: String, + callback: Callback, + retryCallback: () -> Unit + ) = LoginClient().request(csrfWikiSite, username, password, object : LoginCallback { + override fun success(loginResult: LoginResult) { + if (loginResult.pass()) { + AppAdapter.get().updateAccount(loginResult) + retryCallback() + } else { + callback.failure(LoginFailedException(loginResult.message)) + } + } + + override fun twoFactorPrompt(caught: Throwable, token: String?) = + callback.twoFactorPrompt() + + // Should not happen here, but call the callback just in case. + override fun passwordResetPrompt(token: String?) = + callback.failure(LoginFailedException("Logged in with temporary password.")) + + override fun error(caught: Throwable) = callback.failure(caught) + }) + + private fun cancel() { + loginClient.cancel() + if (csrfTokenCall != null) { + csrfTokenCall!!.cancel() + csrfTokenCall = null + } + } + + interface Callback { + fun success(token: String?) + fun failure(caught: Throwable?) + fun twoFactorPrompt() + } + + companion object { + private const val ANON_TOKEN = "+\\" + private const val MAX_RETRIES = 1 + private const val MAX_RETRIES_OF_LOGIN_BLOCKING = 2 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenInterface.kt b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenInterface.kt new file mode 100644 index 000000000..5156de2f5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenInterface.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.auth.csrf + +import org.wikipedia.dataclient.Service +import org.wikipedia.dataclient.mwapi.MwQueryResponse +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Headers + +interface CsrfTokenInterface { + @Headers("Cache-Control: no-cache") + @GET(Service.MW_API_PREFIX + "action=query&meta=tokens&type=csrf") + fun getCsrfTokenCall(): Call +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java index 07195cfa5..d5546ad18 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -106,7 +106,7 @@ public class NetworkingModule { @Provides @Singleton public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) { - return new CsrfTokenClient(commonsWikiSite, commonsWikiSite); + return new CsrfTokenClient(commonsWikiSite); } @Provides diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt index fb4963dcd..c12a74e02 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationClient.kt @@ -34,7 +34,7 @@ class NotificationClient @Inject constructor( fun markNotificationAsRead(notificationId: String?): Observable { return try { service.markRead( - token = csrfTokenClient.tokenBlocking, + token = csrfTokenClient.getTokenBlocking(), readList = notificationId, unreadList = "" ).map(MwQueryResponse::success) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/actions/PageEditClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/actions/PageEditClientTest.kt index ac1534eb2..de25c5425 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/actions/PageEditClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/actions/PageEditClientTest.kt @@ -41,7 +41,7 @@ class PageEditClientTest { */ @Test fun testEdit() { - Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test") + Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test") pageEditClient.edit("test", "test", "test") verify(pageEditInterface).postEdit(eq("test"), eq("test"), eq("test"), eq("test")) } @@ -51,7 +51,7 @@ class PageEditClientTest { */ @Test fun testAppendEdit() { - Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test") + Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test") Mockito.`when`( pageEditInterface.postAppendEdit( ArgumentMatchers.anyString(), @@ -65,7 +65,7 @@ class PageEditClientTest { Mockito.`when`(edit.edit()).thenReturn(editResult) Mockito.`when`(editResult.editSucceeded()).thenReturn(true) pageEditClient.appendEdit("test", "test", "test").test() - verify(csrfTokenClient).tokenBlocking + verify(csrfTokenClient).getTokenBlocking() verify(pageEditInterface).postAppendEdit(eq("test"), eq("test"), eq("test"), eq("test")) verify(edit).edit() verify(editResult).editSucceeded() @@ -76,7 +76,7 @@ class PageEditClientTest { */ @Test fun testPrependEdit() { - Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test") + Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test") pageEditClient.prependEdit("test", "test", "test") verify(pageEditInterface).postPrependEdit(eq("test"), eq("test"), eq("test"), eq("test")) } @@ -86,7 +86,7 @@ class PageEditClientTest { */ @Test fun testSetCaptions() { - Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test") + Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test") pageEditClient.setCaptions("test", "test", "en", "test") verify(pageEditInterface).postCaptions(eq("test"), eq("test"), eq("en"), eq("test"), eq("test")) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt index 0fa4535b4..c3257ddab 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt @@ -47,7 +47,7 @@ class ThanksClientTest { */ @Test fun testThanks() { - `when`(csrfTokenClient.tokenBlocking).thenReturn("test") + `when`(csrfTokenClient.getTokenBlocking()).thenReturn("test") `when`(commonsApplication.userAgent).thenReturn("test") thanksClient.thank(1L) verify(service).thank(ArgumentMatchers.anyString(), ArgumentMatchers.any(), eq("test"), eq("test")) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.java b/app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.java deleted file mode 100644 index 7543086ae..000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package fr.free.nrw.commons.auth.csrf; - -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import androidx.annotation.NonNull; -import com.google.gson.stream.MalformedJsonException; -import fr.free.nrw.commons.MockWebServerTest; -import fr.free.nrw.commons.auth.csrf.CsrfTokenClient.Callback; -import org.junit.Test; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; -import org.wikipedia.dataclient.Service; -import org.wikipedia.dataclient.WikiSite; -import org.wikipedia.dataclient.mwapi.MwException; -import org.wikipedia.dataclient.mwapi.MwQueryResponse; -import org.wikipedia.dataclient.okhttp.HttpStatusException; -import retrofit2.Call; - -public class CsrfTokenClientTest extends MockWebServerTest { - private static final WikiSite TEST_WIKI = new WikiSite("test.wikipedia.org"); - @NonNull private final CsrfTokenClient subject = new CsrfTokenClient(TEST_WIKI, TEST_WIKI); - - @Test public void testRequestSuccess() throws Throwable { - String expected = "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\"; - enqueueFromFile("csrf_token.json"); - - Callback cb = Mockito.mock(Callback.class); - request(cb); - - server().takeRequest(); - assertCallbackSuccess(cb, expected); - } - - @Test public void testRequestResponseApiError() throws Throwable { - enqueueFromFile("api_error.json"); - - Callback cb = Mockito.mock(Callback.class); - request(cb); - - server().takeRequest(); - assertCallbackFailure(cb, MwException.class); - } - - @Test public void testRequestResponseFailure() throws Throwable { - enqueue404(); - - Callback cb = Mockito.mock(Callback.class); - request(cb); - - server().takeRequest(); - assertCallbackFailure(cb, HttpStatusException.class); - } - - @Test public void testRequestResponseMalformed() throws Throwable { - enqueueMalformed(); - - Callback cb = Mockito.mock(Callback.class); - request(cb); - - server().takeRequest(); - assertCallbackFailure(cb, MalformedJsonException.class); - } - - private void assertCallbackSuccess(@NonNull Callback cb, - @NonNull String expected) { - verify(cb).success(ArgumentMatchers.eq(expected)); - //noinspection unchecked - verify(cb, never()).failure(ArgumentMatchers.any(Throwable.class)); - } - - private void assertCallbackFailure(@NonNull Callback cb, - @NonNull Class throwable) { - //noinspection unchecked - verify(cb, never()).success(ArgumentMatchers.any(String.class)); - verify(cb).failure(ArgumentMatchers.isA(throwable)); - } - - private Call request(@NonNull Callback cb) { - return subject.request(service(Service.class), cb); - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.kt new file mode 100644 index 000000000..a53de085b --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/csrf/CsrfTokenClientTest.kt @@ -0,0 +1,71 @@ +package fr.free.nrw.commons.auth.csrf + +import com.google.gson.stream.MalformedJsonException +import fr.free.nrw.commons.MockWebServerTest +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.isA +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.wikipedia.dataclient.Service +import org.wikipedia.dataclient.WikiSite +import org.wikipedia.dataclient.mwapi.MwException +import org.wikipedia.dataclient.okhttp.HttpStatusException + +class CsrfTokenClientTest : MockWebServerTest() { + private val wikiSite = WikiSite("test.wikipedia.org") + private val subject = CsrfTokenClient(wikiSite) + private val cb = mock(CsrfTokenClient.Callback::class.java) + + @Test + @Throws(Throwable::class) + fun testRequestSuccess() { + val expected = "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\" + enqueueFromFile("csrf_token.json") + + performRequest() + + verify(cb).success(eq(expected)) + verify(cb, never()).failure(any(Throwable::class.java)) + } + + @Test + @Throws(Throwable::class) + fun testRequestResponseApiError() { + enqueueFromFile("api_error.json") + + performRequest() + + verify(cb, never()).success(any(String::class.java)) + verify(cb).failure(isA(MwException::class.java)) + } + + @Test + @Throws(Throwable::class) + fun testRequestResponseFailure() { + enqueue404() + + performRequest() + + verify(cb, never()).success(any(String::class.java)) + verify(cb).failure(isA(HttpStatusException::class.java)) + } + + @Test + @Throws(Throwable::class) + fun testRequestResponseMalformed() { + enqueueMalformed() + + performRequest() + + verify(cb, never()).success(any(String::class.java)) + verify(cb).failure(isA(MalformedJsonException::class.java)) + } + + private fun performRequest() { + subject.request(service(CsrfTokenInterface::class.java), cb) + server().takeRequest() + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt index 31c345ab7..35cda96ce 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt @@ -102,7 +102,7 @@ class NotificationClientTest { */ @Test fun markNotificationAsReadTest() { - Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test") + Mockito.`when`(csrfTokenClient.getTokenBlocking()).thenReturn("test") Mockito.`when`(service.markRead(anyString(), anyString(), anyString())) .thenReturn(Observable.just(mQueryResponse)) Mockito.`when`(mQueryResponse.success()).thenReturn(true) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt index 03ed2ebc5..a42082a74 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/wikidata/WikiBaseClientUnitTest.kt @@ -20,7 +20,7 @@ class WikiBaseClientUnitTest { @Throws(Exception::class) fun setUp() { MockitoAnnotations.openMocks(this) - Mockito.`when`(csrfTokenClient!!.tokenBlocking) + Mockito.`when`(csrfTokenClient!!.getTokenBlocking()) .thenReturn("test") } diff --git a/data-client/src/main/java/org/wikipedia/dataclient/Service.java b/data-client/src/main/java/org/wikipedia/dataclient/Service.java index 61273a960..7e93ba055 100644 --- a/data-client/src/main/java/org/wikipedia/dataclient/Service.java +++ b/data-client/src/main/java/org/wikipedia/dataclient/Service.java @@ -192,10 +192,6 @@ public interface Service { // ------- CSRF, Login, and Create Account ------- - @Headers("Cache-Control: no-cache") - @GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf") - @NonNull Call getCsrfTokenCall(); - @Headers("Cache-Control: no-cache") @GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf") @NonNull Observable getCsrfToken();