package com.gfk.senbot.framework.services; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.security.cert.CertificateException; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.json.JSONException; import org.json.JSONObject; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gfk.senbot.framework.BaseServiceHub; import com.gfk.senbot.framework.context.SenBotContext; import com.gfk.senbot.framework.cucumber.stepdefinitions.ScenarioGlobals; import com.gfk.senbot.framework.data.GenericUser; import cucumber.api.Scenario; /** * A generic service for accessing a remote WebService API * @author joostschouten * */ public class APIAccessService extends BaseServiceHub{ /** * setup the default coockie handeling so all API requests will function as expected */ // static { // CookieHandler.setDefault(new CookieManager()); // ((CookieManager)CookieHandler.getDefault()).setCookiePolicy(CookiePolicy.ACCEPT_ALL); // } private static Logger log = LoggerFactory.getLogger(APIAccessService.class); /** * Static keys used to store Cucumber {@link Scenario} session scoped variables */ public static final String LAST_SCENARIO_HTTP_RESPONSE_CODE_KEY = "LAST_SCENARIO_HTTP_RESPONSE_CODE_KEY"; public static final String API_AUTHENTICATION_USER = "API_AUTHENTICATION_USER"; public static final String API_ACCESS_MODE_KEY = "API_ACCESS_MODE_KEY"; /** * Static keys used to store Scenarion scoped values in the {@link ScenarioGlobals} */ public static final String LAST_SCENARIO_JSON_RESPONSE_KEY = "LAST_SCENARIO_JSON_RESPONSE_KEY"; /** * Constants indicating if the API should be accessed directly or though the browser using selenium */ public static final String BROWSER_API_ACCESS_MODE = "browser"; public static final String HTTP_API_ACCESS_MODE = "http"; public JSONObject getLastJSONResponse() { return (JSONObject) getScenarioGlobals().getAttribute(APIAccessService.LAST_SCENARIO_JSON_RESPONSE_KEY); } /** * When the api is accessed though the browser selenium is used, though straight HTTP calls we can choose to POST or GET the api * * @return the api access mode being either {@link APIAccessService#HTTP_API_ACCESS_MODE} (default) or {@link APIAccessService#BROWSER_API_ACCESS_MODE} */ public String getAPIAccessMode() { String apiAccessMode = (String) getScenarioGlobals().getAttribute(API_ACCESS_MODE_KEY); return apiAccessMode == null ? HTTP_API_ACCESS_MODE : apiAccessMode; } public Integer getLastHTTPResponseCode() { return (Integer) getScenarioGlobals().getAttribute(LAST_SCENARIO_HTTP_RESPONSE_CODE_KEY); } /** * assert that the {@link JSONObject} response is not null and does not hold any error messages * @param response * @throws JSONException */ public void assertCorrectResonse(JSONObject response) throws JSONException { Integer lastHTTPResponseCode = getLastHTTPResponseCode(); if(lastHTTPResponseCode != null) { //if a response code is set, check it. Selenium requests do not let you query this which is why we can't check this in case of a selenium API call assertEquals("The https response code should be 200 (OK)", new Integer(HttpURLConnection.HTTP_OK), lastHTTPResponseCode); } assertNotNull("The request should result in a response", response); } public void assertNoError(JSONObject response) throws JSONException { if(response.has("error") ) { Object errorObj = response.get("error"); if(!JSONObject.NULL.equals(errorObj)) { fail("The request resulted in a response with error: " + errorObj.toString()); } } } public void assertErrorMessage(String expectedMessage) throws JSONException { JSONObject response = getLastJSONResponse(); if(!response.has("error")) { fail("The request holds no error while it is expected"); } else { Object errorObj = response.get("error"); String foundErrorMessage = null; if(errorObj instanceof JSONObject) { JSONObject errorJSON = (JSONObject) errorObj; foundErrorMessage = errorJSON.getString("message"); } else if (errorObj instanceof String) { foundErrorMessage = (String) errorObj; } assertEquals(expectedMessage, foundErrorMessage); } } /** * Process the {@link JSONObject} request * * @param urlString * @param request * @param requestType * @param requestHedaers * @return {@link JSONObject} * @throws CertificateException * @throws IOException * @throws JSONException */ public JSONObject processAPIRequest( String urlString, JSONObject request, String requestType, String[] requestParameters, String... requestHedaers) throws CertificateException, IOException, JSONException { resetRequestScenarioVariables(); String response = null; GenericUser authenticationUser = (GenericUser) getScenarioGlobals().getAttribute(API_AUTHENTICATION_USER); if(BROWSER_API_ACCESS_MODE.equals(getAPIAccessMode())) { response = processBrowserAPIRequest(urlString, request, requestType, requestParameters); } else { response = processHTTPAPIRequest(urlString, request, authenticationUser, requestType, requestParameters, requestHedaers); } JSONObject responseJSON = null; try{ if(!StringUtils.isBlank(response)) { responseJSON = new JSONObject(response); log.debug("API JSON response: " + responseJSON.toString(4)); } } catch(JSONException ex) { fail("Contructing the JSON respose failed with: " + ex.getLocalizedMessage() + "\n\nResponse: " + response); } getScenarioGlobals().setAttribute(APIAccessService.LAST_SCENARIO_JSON_RESPONSE_KEY, responseJSON); return responseJSON; } public String processBrowserAPIRequest(String urlString, JSONObject request, String requestType, String[] requestParameters) throws UnsupportedEncodingException { StringBuilder builder = new StringBuilder(); builder.append(urlString + "?json=" + URLEncoder.encode(request.toString(), "UTF-8")); for(int i = 0 ; i < requestParameters.length; i = i + 2) { builder.append("&" + requestParameters[i] + "=" + requestParameters[i+1]); } SenBotContext.getSeleniumDriver().get(builder.toString()); String response = null; try{ //some browsers add some markup to the returned json. If so, clean it up by getText() on the root element WebElement rootElement = SenBotContext.getSeleniumDriver().findElement(By.xpath("*")); if(rootElement != null) { response = rootElement.getText(); } else { response = SenBotContext.getSeleniumDriver().getPageSource(); } } catch (Exception e) { // TODO: handle exception } return response; } private void resetRequestScenarioVariables() { //clear the request variables getScenarioGlobals().setAttribute(LAST_SCENARIO_HTTP_RESPONSE_CODE_KEY, null); getScenarioGlobals().setAttribute(APIAccessService.LAST_SCENARIO_JSON_RESPONSE_KEY, null); } /** * Process the json API request * * @param request * @param requestType POST or GET * @return the {@link JSONObject} representation of the http response * @throws CertificateException * @throws IOException * @throws JSONException */ public String processHTTPAPIRequest( String urlString, JSONObject request, GenericUser authenticationUser, String requestType, String[] requestParameters, String... requestHedaers) throws CertificateException, IOException, JSONException { log.debug("API request: " + request.toString(4)); String jsonRequest = request.toString(); HttpURLConnection ucon = null; if("POST".equals(requestType)) { URL url = new URL(urlString); log.debug("API request: " + url); ucon = (HttpURLConnection) url.openConnection(); ucon.setRequestMethod("POST"); ucon.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); setRequestHeaders(ucon, authenticationUser, requestHedaers); for(int i = 0 ; i < requestParameters.length; i = i + 2) { ucon.setRequestProperty(getReferenceService().namespaceString(requestParameters[i]), getReferenceService().namespaceString(requestParameters[i+1])); } ucon.setUseCaches (false); ucon.setDoInput(true); ucon.setDoOutput(true); //Send request String encodedPostRequest = "json=" + URLEncoder.encode(jsonRequest, "UTF-8"); ucon.setRequestProperty("Content-Length", Integer.toString(encodedPostRequest.getBytes().length)); DataOutputStream wr = new DataOutputStream (ucon.getOutputStream ()); wr.writeBytes(encodedPostRequest); wr.flush (); wr.close (); } else if ("GET".equals(requestType)) { log.debug("API request: " + request.toString()); StringBuilder builder = new StringBuilder(); builder.append(urlString + "?json=" + URLEncoder.encode(request.toString(), "UTF-8").replaceAll("\"", "%22")); for(int i = 0 ; i < requestParameters.length; i = i + 2) { builder.append("&" + getReferenceService().namespaceString(requestParameters[i]) + "=" + getReferenceService().namespaceString(requestParameters[i+1])); } URL url = new URL(builder.toString()); log.debug("API request: " + url); ucon = (HttpURLConnection) url.openConnection(); setRequestHeaders(ucon, authenticationUser, requestHedaers); ucon.setConnectTimeout(5000); ucon.setFollowRedirects(true); ucon.setDoOutput(true); } else { throw new IllegalArgumentException("Only POST and GET API requests are supported at this stage"); } //Obtain the response StringBuilder responseBuilder = new StringBuilder(); try{ InputStream inputStream = ucon.getInputStream(); BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); String pre = null; while ((pre = in.readLine()) != null){ responseBuilder.append(pre); responseBuilder.append("\n"); } in.close(); } catch (IOException e) { //on error codes we should not fail but just capture the response code log.debug("Genarating the API call response caused: ", e); } String response = responseBuilder.toString(); log.debug("The generated API call response: " + response); getScenarioGlobals().setAttribute(LAST_SCENARIO_HTTP_RESPONSE_CODE_KEY, ucon.getResponseCode()); return response; } /** * Add the request headers to the request * * @param ucon the connection to append the http headers to * @param authenticationUser The username:password combination to pass along as basic http authentication * @param requestHedaers the https header name:value pairs. This means the headers should always come in an even number (name + value) * n */ private void setRequestHeaders(HttpURLConnection ucon, GenericUser authenticationUser, String[] requestHedaers) { if(authenticationUser != null) { String authenticationString = authenticationUser.getUserName() + ":" + authenticationUser.getPassword(); byte[] encodedAuthentication = Base64.encodeBase64(authenticationString.getBytes()); String authenticationHeaderValue = "Basic " + new String(encodedAuthentication); ucon.setRequestProperty("Authorization", authenticationHeaderValue); } for(int i = 0;i<requestHedaers.length;i = i+2) { ucon.setRequestProperty(getReferenceService().namespaceString(requestHedaers[i]), getReferenceService().namespaceString(requestHedaers[i+1])); } } }