package org.edx.mobile.test.http;
import android.text.TextUtils;
import com.google.gson.JsonObject;
import com.google.inject.Injector;
import org.edx.mobile.authentication.AuthResponse;
import org.edx.mobile.authentication.LoginAPI;
import org.edx.mobile.authentication.LoginService;
import org.edx.mobile.http.Api;
import org.edx.mobile.http.HttpStatus;
import org.edx.mobile.http.IApi;
import org.edx.mobile.model.api.ProfileModel;
import org.edx.mobile.services.ServiceManager;
import org.edx.mobile.test.BaseTestCase;
import org.edx.mobile.test.util.MockDataUtil;
import org.edx.mobile.util.Config;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Ignore;
import org.robolectric.RuntimeEnvironment;
import java.io.IOException;
import java.util.Locale;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import static org.junit.Assert.assertNotNull;
/**
* use MockWebService for Api test
*/
@Ignore
public class HttpBaseTestCase extends BaseTestCase {
private static final int DELAY_MS = 2000; // Network calls will take 2 seconds.
private static final int VARIANCE_PCT = 40; // Network delay varies by ±40%.
private static final int ERROR_PCT = 3; // 3% of network calls will fail.
private static final int ERROR_DELAY_FACTOR = 3; // Network errors will be scaled by this value.
private static final Random random = new Random(); // Random instance for determining delays
private static final String API_HOST_URL = "API_HOST_URL"; // Config key for API host url
// Use a mock server to serve fixed responses
protected MockWebServer server;
protected Api api;
// Per-test configuration for whether the mock web server should create artificial delays
// before sending the response.
protected boolean useArtificialDelay = false;
protected ServiceManager serviceManager;
protected LoginAPI loginAPI;
protected LoginService loginService;
/**
* Returns the base url used by the mock server
*/
private String getBaseMockUrl() {
return "http://" + server.getHostName() + ":" + server.getPort();
}
@Override
public void setUp() throws Exception {
server = new MockWebServer();
server.setDispatcher(new MockResponseDispatcher());
server.start();
api = new Api(RuntimeEnvironment.application);
super.setUp();
}
@Override
protected JsonObject generateConfigProperties() throws IOException {
// Add the mock host url in the test config properties
JsonObject properties = super.generateConfigProperties();
properties.addProperty(API_HOST_URL, getBaseMockUrl());
return properties;
}
@Override
public void addBindings() {
super.addBindings();
module.addBinding(IApi.class, api);
}
@Override
protected void inject(Injector injector) throws Exception {
super.inject(injector);
injector.injectMembers(api);
serviceManager = injector.getInstance(ServiceManager.class);
loginAPI = injector.getInstance(LoginAPI.class);
loginService = injector.getInstance(LoginService.class);
}
/**
* Utility method to be used as a prerequisite for testing most API
*
* @throws Exception If an exception was encountered during login or
* verification
*/
protected void login() throws Exception {
// The credentials given here don't matter, we will always get the same mock response
AuthResponse res = loginAPI.logInUsingEmail("example@example.com", "password");
assertNotNull(res);
assertNotNull(res.access_token);
assertNotNull(res.token_type);
assertNotNull(res.refresh_token);
print(res.toString());
assertNotNull(res.profile);
}
@Override
public void tearDown() throws Exception {
super.tearDown();
server.shutdown();
}
/**
* Randomly determine whether this call should result in a network failure.
*/
private static boolean calculateIsFailure() {
int randomValue = random.nextInt(100) + 1;
return randomValue <= ERROR_PCT;
}
// The delay randomizing methods below are copied from the Retrofit
// MockRestAdapter implementation which is distributed under the Apache 2.0
// License
/**
* Get the delay (in milliseconds) that should be used for triggering a
* network error.
* <p/>
* Because we are triggering an error, use a random delay between 0 and
* three times the normal network delay to simulate a flaky connection
* failing anywhere from quickly to slowly.
*/
private static int calculateDelayForError() {
return random.nextInt(DELAY_MS * ERROR_DELAY_FACTOR);
}
/**
* Get the delay (in milliseconds) that should be used for delaying
* a successful network call response.
*/
private static int calculateDelayForSuccess() {
float errorPercent = VARIANCE_PCT / 100f; // e.g., 20 / 100f == 0.2f
float lowerBound = 1f - errorPercent; // 0.2f --> 0.8f
float upperBound = 1f + errorPercent; // 0.2f --> 1.2f
float bound = upperBound - lowerBound; // 1.2f - 0.8f == 0.4f
float delayPercent = (random.nextFloat() * bound) +
lowerBound; // 0.8 + (rnd * 0.4)
return (int) (DELAY_MS * delayPercent);
}
/**
* Get the delay (in milliseconds) that should be used for delaying
* a network call response.
*/
private static int calculateDelayForCall() {
// Commenting out the random failure mode delay since we want our
// tests to be reproducible
return //calculateIsFailure() ? calculateDelayForError() :
calculateDelayForSuccess();
}
/**
* Match url to a regex template while allowing extra slash and query
* strings at the end
*/
private static boolean urlMatches(String url, String template) {
if (TextUtils.isEmpty(url) || TextUtils.isEmpty(template)) {
return false;
}
String pattern = '^' + template;
if (template.charAt(template.length() - 1) != '/') {
pattern += "/?";
}
pattern += "(\\?.*)?$";
return url.matches(pattern);
}
private MockResponse generateMockResponse(RecordedRequest request) {
final String method = request.getMethod();
final String path = request.getPath();
final String body = request.getBody().readUtf8();
MockResponse response = new MockResponse();
response.addHeader("Set-Cookie", "csrftoken=dummy; Max-Age=31449600; Path=/");
response.setResponseCode(HttpStatus.NOT_FOUND);
try {
if ("POST".equals(method)) {
if (urlMatches(path, "/oauth2/access_token")) {
response.setBody(MockDataUtil.getMockResponse("post_oauth2_access_token"));
response.setResponseCode(HttpStatus.OK);
} else if (urlMatches(path, "/api/mobile/v0.5/users/staff/course_status_info/[^/]+/[^/]+/[^/]+")) {
try {
JSONObject jsonObject = new JSONObject(request.getBody().readUtf8());
String moduleId = jsonObject.getString("last_visited_module_id");
response.setBody(String.format(Locale.US, MockDataUtil.getMockResponse("post_course_status_info"), moduleId));
response.setResponseCode(HttpStatus.OK);
} catch (JSONException e) {
e.printStackTrace();
}
} else if (urlMatches(path, "/api/enrollment/v1/enrollment")) {
try {
JSONObject jsonObject = new JSONObject(request.getBody().readUtf8());
response.setBody(String.format(Locale.US, MockDataUtil.getMockResponse("post_enrollment"),
jsonObject.getJSONObject("course_details").getString("course_id")));
response.setResponseCode(HttpStatus.OK);
} catch (JSONException e) {
e.printStackTrace();
}
} else if (urlMatches(path, "/password_reset")) {
response.setBody(MockDataUtil.getMockResponse("post_password_reset"));
response.setResponseCode(HttpStatus.OK);
}
} else if ("GET".equals(method)) {
if (urlMatches(path, "/api/mobile/v0.5/my_user_info")) {
String baseMockUrl = getBaseMockUrl();
response.setBody(String.format(Locale.US, MockDataUtil.getMockResponse("get_my_user_info"), baseMockUrl));
response.setResponseCode(HttpStatus.OK);
} else if (urlMatches(path, "/api/mobile/v0.5/users/[^/]+/course_enrollments")) {
String baseMockUrl = getBaseMockUrl();
response.setBody(String.format(Locale.US, MockDataUtil.getMockResponse("get_course_enrollments"), baseMockUrl));
response.setResponseCode(HttpStatus.OK);
} else if (urlMatches(path, "/api/mobile/v0.5/video_outlines/courses/[^/]+/[^/]+/[^/]+")) {
response.setBody(MockDataUtil.getMockResponse("get_video_outlines_courses"));
response.setResponseCode(HttpStatus.OK);
} else if (urlMatches(path, "/api/mobile/v0.5/course_info/[^/]+/[^/]+/[^/]+/updates")) {
response.setBody(MockDataUtil.getMockResponse("get_course_info_updates"));
response.setResponseCode(HttpStatus.OK);
} else if (urlMatches(path, "/api/mobile/v0.5/users/staff/course_status_info/[^/]+/[^/]+/[^/]+")) {
Matcher matcher = Pattern.compile(
"/api/mobile/v0.5/users/staff/course_status_info/([^/]+)/([^/]+)/([^/]+)", 0).matcher(path);
matcher.matches();
String moduleId = "i4x://" + matcher.group(1) + '/' + matcher.group(2) + "/course/" + matcher.group(3);
response.setBody(String.format(Locale.US, MockDataUtil.getMockResponse("get_course_status_info"), moduleId));
response.setResponseCode(HttpStatus.OK);
} else if (urlMatches(path, "/api/mobile/v0.5/course_info/[^/]+/[^/]+/[^/]+/handouts")) {
// TODO: Find out if this is a wrong API call or server issue
response.setResponseCode(HttpStatus.NOT_FOUND);
response.setBody("{\"detail\": \"Not found\"}");
} else if (urlMatches(path, "/api/courses/v1/blocks/")) {
// TODO: Return different responses based on the parameters?
response.setBody(MockDataUtil.getMockResponse("get_course_structure"));
response.setResponseCode(HttpStatus.OK);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
/**
* Handler for requests on the mock server that will send mock responses
* according to the request urls
*/
private class MockResponseDispatcher extends Dispatcher {
@Override
public MockResponse dispatch(RecordedRequest recordedRequest)
throws InterruptedException {
if (useArtificialDelay) {
Thread.sleep(calculateDelayForCall());
}
return generateMockResponse(recordedRequest);
}
}
}