mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Moved the CSRF token client over into main commons code (#5471)
This commit is contained in:
parent
3d0e65c92c
commit
8b8eb84fae
18 changed files with 252 additions and 37 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package org.wikipedia.csrf;
|
||||
package fr.free.nrw.commons.auth.csrf;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
111
app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java
Normal file
111
app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/src/test/kotlin/fr/free/nrw/commons/TestFileUtil.java
Normal file
37
app/src/test/kotlin/fr/free/nrw/commons/TestFileUtil.java
Normal 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() { }
|
||||
}
|
||||
56
app/src/test/kotlin/fr/free/nrw/commons/TestWebServer.java
Normal file
56
app/src/test/kotlin/fr/free/nrw/commons/TestWebServer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
10
app/src/test/res/raw/api_error.json
Normal file
10
app/src/test/res/raw/api_error.json
Normal 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"
|
||||
}
|
||||
8
app/src/test/res/raw/csrf_token.json
Normal file
8
app/src/test/res/raw/csrf_token.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"batchcomplete": true,
|
||||
"query": {
|
||||
"tokens": {
|
||||
"csrftoken": "b6f7bd58c013ab30735cb19ecc0aa08258122cba+\\"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue