Moved the CSRF token client over into main commons code (#5471)

This commit is contained in:
Paul Hawke 2024-01-23 19:36:43 -06:00 committed by GitHub
parent 3d0e65c92c
commit 8b8eb84fae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 252 additions and 37 deletions

View file

@ -25,6 +25,8 @@ dependencies {
// Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies // Ref: https://docs.gradle.org/7.5/userguide/upgrading_version_5.html#forced_dependencies
//force = true //API 19 support //force = true //API 19 support
} }
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-gson:2.8.1"
implementation 'com.squareup.okio:okio:2.2.2' implementation 'com.squareup.okio:okio:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3' implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
@ -99,6 +101,7 @@ dependencies {
testImplementation 'com.facebook.soloader:soloader:0.10.5' testImplementation 'com.facebook.soloader:soloader:0.10.5'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
debugImplementation("androidx.fragment:fragment-testing:1.6.2") debugImplementation("androidx.fragment:fragment-testing:1.6.2")
testImplementation "commons-io:commons-io:2.6"
// Android testing // Android testing
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04'
@ -134,7 +137,6 @@ dependencies {
implementation "androidx.room:room-rxjava2:$ROOM_VERSION" implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION" kapt "androidx.room:room-compiler:$ROOM_VERSION"
// For Kotlin use kapt instead of annotationProcessor // For Kotlin use kapt instead of annotationProcessor
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "androidx.arch.core:core-testing:2.1.0"
// Pref // Pref

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.actions
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import org.wikipedia.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
/** /**
* This class acts as a Client to facilitate wiki page editing * This class acts as a Client to facilitate wiki page editing

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.actions
import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
import io.reactivex.Observable import io.reactivex.Observable
import org.wikipedia.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton

View file

@ -1,4 +1,4 @@
package org.wikipedia.csrf; package fr.free.nrw.commons.auth.csrf;
import android.text.TextUtils; import android.text.TextUtils;

View file

@ -35,7 +35,7 @@ import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.HttpLoggingInterceptor;
import okhttp3.logging.HttpLoggingInterceptor.Level; import okhttp3.logging.HttpLoggingInterceptor.Level;
import org.wikipedia.csrf.CsrfTokenClient; import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service; import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory; import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite; import org.wikipedia.dataclient.WikiSite;

View file

@ -5,7 +5,7 @@ import fr.free.nrw.commons.notification.models.Notification
import fr.free.nrw.commons.notification.models.NotificationType import fr.free.nrw.commons.notification.models.NotificationType
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import org.wikipedia.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.util.DateUtil import org.wikipedia.util.DateUtil
import javax.inject.Inject import javax.inject.Inject

View file

@ -26,7 +26,7 @@ import javax.inject.Singleton;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import org.wikipedia.csrf.CsrfTokenClient; import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.mwapi.MwException; import org.wikipedia.dataclient.mwapi.MwException;
import timber.log.Timber; import timber.log.Timber;

View file

@ -9,7 +9,7 @@ import io.reactivex.Observable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.wikipedia.csrf.CsrfTokenClient; import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.mwapi.MwPostResponse; import org.wikipedia.dataclient.mwapi.MwPostResponse;
import timber.log.Timber; import timber.log.Timber;

View file

@ -0,0 +1,111 @@
package fr.free.nrw.commons;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import okhttp3.mockwebserver.MockResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.wikipedia.AppAdapter;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.json.GsonUtil;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
@RunWith(RobolectricTestRunner.class)
public abstract class MockWebServerTest {
private OkHttpClient okHttpClient;
private final TestWebServer server = new TestWebServer();
@Before public void setUp() throws Throwable {
AppAdapter.set(new TestAppAdapter());
OkHttpClient.Builder builder = AppAdapter.get().getOkHttpClient(new WikiSite(Service.WIKIPEDIA_URL)).newBuilder();
okHttpClient = builder.dispatcher(new Dispatcher(new ImmediateExecutorService())).build();
server.setUp();
}
@After public void tearDown() throws Throwable {
server.tearDown();
}
@NonNull protected TestWebServer server() {
return server;
}
protected void enqueueFromFile(@NonNull String filename) throws Throwable {
String json = TestFileUtil.readRawFile(filename);
server.enqueue(json);
}
protected void enqueue404() {
final int code = 404;
server.enqueue(new MockResponse().setResponseCode(code).setBody("Not Found"));
}
protected void enqueueMalformed() {
server.enqueue("(╯°□°)╯︵ ┻━┻");
}
protected void enqueueEmptyJson() {
server.enqueue(new MockResponse().setBody("{}"));
}
@NonNull protected OkHttpClient okHttpClient() {
return okHttpClient;
}
@NonNull protected <T> T service(Class<T> clazz) {
return service(clazz, server().getUrl());
}
@NonNull protected <T> T service(Class<T> clazz, @NonNull String url) {
return new Retrofit.Builder()
.baseUrl(url)
.callbackExecutor(new ImmediateExecutor())
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson()))
.build()
.create(clazz);
}
public final class ImmediateExecutorService extends AbstractExecutorService {
@Override public void shutdown() {
throw new UnsupportedOperationException();
}
@NonNull @Override public List<Runnable> shutdownNow() {
throw new UnsupportedOperationException();
}
@Override public boolean isShutdown() {
throw new UnsupportedOperationException();
}
@Override public boolean isTerminated() {
throw new UnsupportedOperationException();
}
@Override public boolean awaitTermination(long l, @NonNull TimeUnit timeUnit)
throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override public void execute(@NonNull Runnable runnable) {
runnable.run();
}
}
public class ImmediateExecutor implements Executor {
@Override
public void execute(@NonNull Runnable runnable) {
runnable.run();
}
}
}

View file

@ -0,0 +1,37 @@
package fr.free.nrw.commons;
import android.annotation.TargetApi;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
public final class TestFileUtil {
private static final String RAW_DIR = "src/test/res/raw/";
public static File getRawFile(@NonNull String rawFileName) {
return new File(RAW_DIR + rawFileName);
}
public static String readRawFile(String basename) throws IOException {
return readFile(getRawFile(basename));
}
@TargetApi(19)
private static String readFile(File file) throws IOException {
return FileUtils.readFileToString(file, StandardCharsets.UTF_8);
}
@TargetApi(19)
public static String readStream(InputStream stream) throws IOException {
StringWriter writer = new StringWriter();
IOUtils.copy(stream, writer, StandardCharsets.UTF_8);
return writer.toString();
}
private TestFileUtil() { }
}

View file

@ -0,0 +1,56 @@
package fr.free.nrw.commons;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
public class TestWebServer {
public static final int TIMEOUT_DURATION = 5;
public static final TimeUnit TIMEOUT_UNIT = TimeUnit.SECONDS;
private final MockWebServer server;
public TestWebServer() {
server = new MockWebServer();
}
public void setUp() throws IOException {
server.start();
}
public void tearDown() throws IOException {
server.shutdown();
}
public String getUrl() {
return getUrl("");
}
public String getUrl(String path) {
return server.url(path).url().toString();
}
public int getRequestCount() {
return server.getRequestCount();
}
public void enqueue(@NonNull String body) {
enqueue(new MockResponse().setBody(body));
}
public void enqueue(MockResponse response) {
server.enqueue(response);
}
@NonNull public RecordedRequest takeRequest() throws InterruptedException {
RecordedRequest req = server.takeRequest(TIMEOUT_DURATION,
TIMEOUT_UNIT);
if (req == null) {
throw new InterruptedException("Timeout elapsed.");
}
return req;
}
}

View file

@ -9,8 +9,7 @@ import org.mockito.ArgumentMatchers
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.wikipedia.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import org.wikipedia.dataclient.Service
import org.wikipedia.edit.Edit import org.wikipedia.edit.Edit
class PageEditClientTest { class PageEditClientTest {

View file

@ -12,12 +12,9 @@ import org.mockito.MockedStatic
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.wikipedia.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import org.wikipedia.dataclient.Service
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@PrepareForTest(CommonsApplication::class) @PrepareForTest(CommonsApplication::class)

View file

@ -1,27 +1,22 @@
package org.wikipedia.csrf; package fr.free.nrw.commons.auth.csrf;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.gson.stream.MalformedJsonException; 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.junit.Test;
import org.wikipedia.csrf.CsrfTokenClient.Callback; import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.wikipedia.dataclient.Service; import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.WikiSite; import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.mwapi.MwException; import org.wikipedia.dataclient.mwapi.MwException;
import org.wikipedia.dataclient.mwapi.MwQueryResponse; import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.okhttp.HttpStatusException; import org.wikipedia.dataclient.okhttp.HttpStatusException;
import org.wikipedia.test.MockWebServerTest;
import retrofit2.Call; import retrofit2.Call;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
public class CsrfTokenClientTest extends MockWebServerTest { public class CsrfTokenClientTest extends MockWebServerTest {
private static final WikiSite TEST_WIKI = new WikiSite("test.wikipedia.org"); private static final WikiSite TEST_WIKI = new WikiSite("test.wikipedia.org");
@NonNull private final CsrfTokenClient subject = new CsrfTokenClient(TEST_WIKI, TEST_WIKI); @NonNull private final CsrfTokenClient subject = new CsrfTokenClient(TEST_WIKI, TEST_WIKI);
@ -30,7 +25,7 @@ public class CsrfTokenClientTest extends MockWebServerTest {
String expected = "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\"; String expected = "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\";
enqueueFromFile("csrf_token.json"); enqueueFromFile("csrf_token.json");
Callback cb = mock(Callback.class); Callback cb = Mockito.mock(Callback.class);
request(cb); request(cb);
server().takeRequest(); server().takeRequest();
@ -40,7 +35,7 @@ public class CsrfTokenClientTest extends MockWebServerTest {
@Test public void testRequestResponseApiError() throws Throwable { @Test public void testRequestResponseApiError() throws Throwable {
enqueueFromFile("api_error.json"); enqueueFromFile("api_error.json");
Callback cb = mock(Callback.class); Callback cb = Mockito.mock(Callback.class);
request(cb); request(cb);
server().takeRequest(); server().takeRequest();
@ -50,7 +45,7 @@ public class CsrfTokenClientTest extends MockWebServerTest {
@Test public void testRequestResponseFailure() throws Throwable { @Test public void testRequestResponseFailure() throws Throwable {
enqueue404(); enqueue404();
Callback cb = mock(Callback.class); Callback cb = Mockito.mock(Callback.class);
request(cb); request(cb);
server().takeRequest(); server().takeRequest();
@ -60,7 +55,7 @@ public class CsrfTokenClientTest extends MockWebServerTest {
@Test public void testRequestResponseMalformed() throws Throwable { @Test public void testRequestResponseMalformed() throws Throwable {
enqueueMalformed(); enqueueMalformed();
Callback cb = mock(Callback.class); Callback cb = Mockito.mock(Callback.class);
request(cb); request(cb);
server().takeRequest(); server().takeRequest();
@ -69,16 +64,16 @@ public class CsrfTokenClientTest extends MockWebServerTest {
private void assertCallbackSuccess(@NonNull Callback cb, private void assertCallbackSuccess(@NonNull Callback cb,
@NonNull String expected) { @NonNull String expected) {
verify(cb).success(eq(expected)); verify(cb).success(ArgumentMatchers.eq(expected));
//noinspection unchecked //noinspection unchecked
verify(cb, never()).failure(any(Throwable.class)); verify(cb, never()).failure(ArgumentMatchers.any(Throwable.class));
} }
private void assertCallbackFailure(@NonNull Callback cb, private void assertCallbackFailure(@NonNull Callback cb,
@NonNull Class<? extends Throwable> throwable) { @NonNull Class<? extends Throwable> throwable) {
//noinspection unchecked //noinspection unchecked
verify(cb, never()).success(any(String.class)); verify(cb, never()).success(ArgumentMatchers.any(String.class));
verify(cb).failure(isA(throwable)); verify(cb).failure(ArgumentMatchers.isA(throwable));
} }
private Call<MwQueryResponse> request(@NonNull Callback cb) { private Call<MwQueryResponse> request(@NonNull Callback cb) {

View file

@ -17,7 +17,7 @@ import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode import org.robolectric.annotation.LooperMode
import org.wikipedia.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.json.GsonUtil import org.wikipedia.json.GsonUtil

View file

@ -6,7 +6,7 @@ import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.wikipedia.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
class WikiBaseClientUnitTest { class WikiBaseClientUnitTest {

View file

@ -0,0 +1,10 @@
{
"errors": [
{
"code": "unknown_action",
"text": "Unrecognized value for parameter \"action\": oscillate."
}
],
"docref": "See https://en.wikipedia.org/w/api.php for API usage.",
"servedby": "mw1286"
}

View file

@ -0,0 +1,8 @@
{
"batchcomplete": true,
"query": {
"tokens": {
"csrftoken": "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\"
}
}
}