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
//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 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
@ -99,6 +101,7 @@ dependencies {
testImplementation 'com.facebook.soloader:soloader:0.10.5'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
debugImplementation("androidx.fragment:fragment-testing:1.6.2")
testImplementation "commons-io:commons-io:2.6"
// Android testing
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha04'
@ -134,7 +137,6 @@ dependencies {
implementation "androidx.room:room-rxjava2:$ROOM_VERSION"
kapt "androidx.room:room-compiler:$ROOM_VERSION"
// For Kotlin use kapt instead of annotationProcessor
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
testImplementation "androidx.arch.core:core-testing:2.1.0"
// Pref

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.actions
import io.reactivex.Observable
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

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.actions
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
import io.reactivex.Observable
import org.wikipedia.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import javax.inject.Inject
import javax.inject.Named
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;

View file

@ -35,7 +35,7 @@ import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
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.ServiceFactory;
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 io.reactivex.Observable
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.util.DateUtil
import javax.inject.Inject

View file

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

View file

@ -9,7 +9,7 @@ import io.reactivex.Observable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.wikipedia.csrf.CsrfTokenClient;
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
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.Mockito
import org.mockito.MockitoAnnotations
import org.wikipedia.csrf.CsrfTokenClient
import org.wikipedia.dataclient.Service
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import org.wikipedia.edit.Edit
class PageEditClientTest {

View file

@ -12,12 +12,9 @@ import org.mockito.MockedStatic
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.robolectric.RobolectricTestRunner
import org.wikipedia.csrf.CsrfTokenClient
import org.wikipedia.dataclient.Service
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
@RunWith(RobolectricTestRunner::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 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.wikipedia.csrf.CsrfTokenClient.Callback;
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 org.wikipedia.test.MockWebServerTest;
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 {
private static final WikiSite TEST_WIKI = new WikiSite("test.wikipedia.org");
@NonNull private final CsrfTokenClient subject = new CsrfTokenClient(TEST_WIKI, TEST_WIKI);
@ -30,7 +25,7 @@ public class CsrfTokenClientTest extends MockWebServerTest {
String expected = "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\";
enqueueFromFile("csrf_token.json");
Callback cb = mock(Callback.class);
Callback cb = Mockito.mock(Callback.class);
request(cb);
server().takeRequest();
@ -40,7 +35,7 @@ public class CsrfTokenClientTest extends MockWebServerTest {
@Test public void testRequestResponseApiError() throws Throwable {
enqueueFromFile("api_error.json");
Callback cb = mock(Callback.class);
Callback cb = Mockito.mock(Callback.class);
request(cb);
server().takeRequest();
@ -50,7 +45,7 @@ public class CsrfTokenClientTest extends MockWebServerTest {
@Test public void testRequestResponseFailure() throws Throwable {
enqueue404();
Callback cb = mock(Callback.class);
Callback cb = Mockito.mock(Callback.class);
request(cb);
server().takeRequest();
@ -60,7 +55,7 @@ public class CsrfTokenClientTest extends MockWebServerTest {
@Test public void testRequestResponseMalformed() throws Throwable {
enqueueMalformed();
Callback cb = mock(Callback.class);
Callback cb = Mockito.mock(Callback.class);
request(cb);
server().takeRequest();
@ -69,16 +64,16 @@ public class CsrfTokenClientTest extends MockWebServerTest {
private void assertCallbackSuccess(@NonNull Callback cb,
@NonNull String expected) {
verify(cb).success(eq(expected));
verify(cb).success(ArgumentMatchers.eq(expected));
//noinspection unchecked
verify(cb, never()).failure(any(Throwable.class));
verify(cb, never()).failure(ArgumentMatchers.any(Throwable.class));
}
private void assertCallbackFailure(@NonNull Callback cb,
@NonNull Class<? extends Throwable> throwable) {
//noinspection unchecked
verify(cb, never()).success(any(String.class));
verify(cb).failure(isA(throwable));
verify(cb, never()).success(ArgumentMatchers.any(String.class));
verify(cb).failure(ArgumentMatchers.isA(throwable));
}
private Call<MwQueryResponse> request(@NonNull Callback cb) {

View file

@ -17,7 +17,7 @@ import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.robolectric.annotation.Config
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.MwQueryResult
import org.wikipedia.json.GsonUtil

View file

@ -6,7 +6,7 @@ import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.wikipedia.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
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+\\"
}
}
}