package org.syftkog.web.test.framework;
import com.saucelabs.saucerest.SecurityUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.DateUtils;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.cookie.*;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.cookie.BestMatchSpecFactory;
import org.apache.http.impl.cookie.BrowserCompatSpec;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.apache.commons.codec.binary.Base64;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.rmi.UnexpectedException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.simple.JSONArray;
import org.json.simple.JSONValue;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
/**
* Java API that invokes the Sauce REST API. Adapted https://github.com/saucelabs/saucerest-java/blob/master/src/main/java/com/saucelabs/saucerest/SauceREST.java
*
*/
public class SauceRESTApi {
private static final Logger logger = Logger.getLogger(SauceRESTApi.class.getName());
private static final long HTTP_READ_TIMEOUT_SECONDS = TimeUnit.SECONDS.toMillis(10);
private static final long HTTP_CONNECT_TIMEOUT_SECONDS = TimeUnit.SECONDS.toMillis(10);
/**
*
*/
protected String username;
/**
*
*/
protected String accessKey;
/**
*
*/
public static final String RESTURL = "https://saucelabs.com/rest/v1/%1$s";
private static final String USER_RESULT_FORMAT = RESTURL + "/%2$s";
private static final String JOBS_FORMAT = RESTURL + "/jobs?limit=%2$s";
private static final String JOB_RESULT_FORMAT = RESTURL + "/jobs/%2$s";
private static final String STOP_JOB_FORMAT = JOB_RESULT_FORMAT + "/stop";
private static final String DOWNLOAD_VIDEO_FORMAT = JOB_RESULT_FORMAT + "/assets/video.flv";
private static final String DOWNLOAD_LOG_FORMAT = JOB_RESULT_FORMAT + "/assets/selenium-server.log";
private static final String GET_TUNNEL_FORMAT = RESTURL + "/tunnels";
private static final String DELETE_TUNNEL_FORMAT = GET_TUNNEL_FORMAT + "/%2$s";
private static final String DATE_FORMAT = "yyyyMMdd_HHmmSS";
/**
*
* @param username
* @param accessKey
*/
public SauceRESTApi(String username, String accessKey) {
this.username = username;
this.accessKey = accessKey;
}
/**
* Marks a Sauce Job as 'passed'.
*
* @param jobId the Sauce Job Id, typically equal to the Selenium/WebDriver
* sessionId
*/
public void jobPassed(String jobId) {
Map<String, Object> updates = new HashMap<String, Object>();
updates.put("passed", true);
updateJobInfo(jobId, updates);
}
/**
* Marks a Sauce Job as 'failed'.
*
* @param jobId the Sauce Job Id, typically equal to the Selenium/WebDriver
* sessionId
*/
public void jobFailed(String jobId) {
Map<String, Object> updates = new HashMap<String, Object>();
updates.put("passed", false);
updateJobInfo(jobId, updates);
}
/**
* Downloads the video for a Sauce Job to the filesystem. The file will be
* stored in a directory specified by the <code>location</code> field.
*
* @param jobId the Sauce Job Id, typically equal to the Selenium/WebDriver
* sessionId
* @param location
*/
public void downloadVideo(String jobId, String location) {
URL restEndpoint = null;
try {
restEndpoint = new URL(String.format(DOWNLOAD_VIDEO_FORMAT, username, jobId));
} catch (MalformedURLException e) {
logger.log(Level.WARNING, "Error constructing Sauce URL", e);
}
downloadFile(jobId, location, restEndpoint);
}
/**
* Downloads the log file for a Sauce Job to the filesystem. The file will be
* stored in a directory specified by the <code>location</code> field.
*
* @param jobId the Sauce Job Id, typically equal to the Selenium/WebDriver
* sessionId
* @param location
*/
public void downloadLog(String jobId, String location) {
URL restEndpoint = null;
try {
restEndpoint = new URL(String.format(DOWNLOAD_LOG_FORMAT, username, jobId));
} catch (MalformedURLException e) {
logger.log(Level.WARNING, "Error constructing Sauce URL", e);
}
downloadFile(jobId, location, restEndpoint);
}
/**
*
* @param path
* @return
*/
public String retrieveResults(String path) {
URL restEndpoint = null;
try {
restEndpoint = new URL(String.format(USER_RESULT_FORMAT, username, path));
} catch (MalformedURLException e) {
logger.log(Level.WARNING, "Error constructing Sauce URL", e);
}
return retrieveResults(restEndpoint);
}
/**
*
* @param jobId
* @return
*/
public String getJobInfo(String jobId) {
URL restEndpoint = null;
try {
restEndpoint = new URL(String.format(JOB_RESULT_FORMAT, username, jobId));
} catch (MalformedURLException e) {
logger.log(Level.WARNING, "Error constructing Sauce URL", e);
}
return retrieveResults(restEndpoint);
}
/**
*
* @param restEndpoint
* @return
*/
public String retrieveResults(URL restEndpoint) {
BufferedReader reader = null;
StringBuilder builder = new StringBuilder();
try {
HttpURLConnection connection = openConnection(restEndpoint);
connection.setDoOutput(true);
addAuthenticationProperty(connection);
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
while ((inputLine = reader.readLine()) != null) {
builder.append(inputLine);
}
} catch (IOException e) {
logger.log(Level.WARNING, "Error retrieving Sauce Results", e);
}
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Error closing Sauce input stream", e);
}
return builder.toString();
}
private void downloadFile(String jobId, String location, URL restEndpoint) {
try {
HttpURLConnection connection = openConnection(restEndpoint);
connection.setDoOutput(true);
connection.setRequestMethod("GET");
addAuthenticationProperty(connection);
InputStream stream = connection.getInputStream();
BufferedInputStream in = new BufferedInputStream(stream);
SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);
String saveName = jobId + format.format(new Date());
if (restEndpoint.getPath().endsWith(".flv")) {
saveName = saveName + ".flv";
} else {
saveName = saveName + ".log";
}
FileOutputStream file = new FileOutputStream(new File(location, saveName));
BufferedOutputStream out = new BufferedOutputStream(file);
int i;
while ((i = in.read()) != -1) {
out.write(i);
}
out.flush();
} catch (IOException e) {
logger.log(Level.WARNING, "Error downloading Sauce Results");
}
}
/**
*
* @param connection
*/
protected void addAuthenticationProperty(HttpURLConnection connection) {
if (username != null && accessKey != null) {
String auth = encodeAuthentication();
connection.setRequestProperty("Authorization", auth);
}
}
/**
*
* @return
*/
public String getJSON() {
try {
URL url = new URL("http://localhost:8080/RESTfulExample/json/product/get");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
if (conn.getResponseCode() != 200) {
throw new RuntimeException("Failed : HTTP error code : " + conn.getResponseCode());
}
BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));
String output;
System.out.println("Output from Server .... \n");
while ((output = br.readLine()) != null) {
System.out.println(output);
}
conn.disconnect();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
*
* @param jobId
* @return
*/
public boolean deleteJob(String jobId) {
HttpURLConnection conn = null;
try {
URL restEndpoint = new URL(String.format(JOB_RESULT_FORMAT, username, jobId));
conn = openConnection(restEndpoint);
conn.setDoOutput(true);
conn.setRequestMethod("DELETE");
addAuthenticationProperty(conn);
int responseCode = conn.getResponseCode();
return responseCode == 200;
} catch (IOException e) {
logger.log(Level.WARNING, "Error deleting Sauce Job", e);
}
try {
if (conn != null) {
conn.getInputStream().close();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Error deleting result stream", e);
}
return false;
}
/**
*
* @param limit
* @return
*/
public JSONArray getJobs(int limit) {
HttpURLConnection conn = null;
try {
URL restEndpoint = new URL(String.format(JOBS_FORMAT, username, limit));
conn = openConnection(restEndpoint);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
addAuthenticationProperty(conn);
if (conn.getResponseCode() != 200) {
throw new RuntimeException("Failed : HTTP error code : " + conn.getResponseCode());
}
BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));
// String output;
//System.out.println("Output from Server .... \n");
// while ((output = br.readLine()) != null) {
// System.out.println(output);
// }
JSONParser parser = new JSONParser();
//Object obj = parser.parse(br);
JSONArray list = (JSONArray) parser.parse(br);
return list;
} catch (IOException | ParseException e) {
logger.log(Level.WARNING, "Error reading Sauce Jobs", e);
}
try {
if (conn != null) {
conn.getInputStream().close();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Error closing result stream", e);
}
return null;
}
/**
*
* @param jobId
* @param updates
*/
public void updateJobInfo(String jobId, Map<String, Object> updates) {
HttpURLConnection postBack = null;
try {
URL restEndpoint = new URL(String.format(JOB_RESULT_FORMAT, username, jobId));
postBack = openConnection(restEndpoint);
postBack.setDoOutput(true);
postBack.setRequestMethod("PUT");
addAuthenticationProperty(postBack);
String jsonText = JSONValue.toJSONString(updates);
postBack.getOutputStream().write(jsonText.getBytes());
} catch (IOException e) {
logger.log(Level.WARNING, "Error updating Sauce Results", e);
}
try {
if (postBack != null) {
postBack.getInputStream().close();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Error closing result stream", e);
}
}
/**
*
* @param jobId
*/
public void stopJob(String jobId) {
HttpURLConnection postBack = null;
try {
URL restEndpoint = new URL(String.format(STOP_JOB_FORMAT, username, jobId));
postBack = openConnection(restEndpoint);
postBack.setDoOutput(true);
postBack.setRequestMethod("PUT");
addAuthenticationProperty(postBack);
postBack.getOutputStream().write("".getBytes());
} catch (IOException e) {
logger.log(Level.WARNING, "Error stopping Sauce Job", e);
}
try {
if (postBack != null) {
postBack.getInputStream().close();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Error closing result stream", e);
}
}
/**
*
* @param url
* @return
* @throws IOException
*/
public HttpURLConnection openConnection(URL url) throws IOException {
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setReadTimeout((int) HTTP_READ_TIMEOUT_SECONDS);
con.setConnectTimeout((int) HTTP_CONNECT_TIMEOUT_SECONDS);
return con;
}
/**
* Uploads a file to Sauce storage.
*
* @param file the file to upload -param fileName uses file.getName() to store
* in sauce -param overwrite set to true
* @return the md5 hash returned by sauce of the file
* @throws IOException
*/
public String uploadFile(File file) throws IOException {
return uploadFile(file, file.getName());
}
/**
* Uploads a file to Sauce storage.
*
* @param file the file to upload
* @param fileName name of the file in sauce storage -param overwrite set to
* true
* @return the md5 hash returned by sauce of the file
* @throws IOException
*/
public String uploadFile(File file, String fileName) throws IOException {
return uploadFile(file, fileName, true);
}
/**
* Uploads a file to Sauce storage.
*
* @param file the file to upload
* @param fileName name of the file in sauce storage
* @param overwrite boolean flag to overwrite file in sauce storage if it
* exists
* @return the md5 hash returned by sauce of the file
* @throws IOException
*/
public String uploadFile(File file, String fileName, Boolean overwrite) throws IOException {
CookieSpecProvider customSpecProvider = new CookieSpecProvider() {
public CookieSpec create(HttpContext context) {
return new BrowserCompatSpec(new String[]{DateUtils.PATTERN_RFC1123,
DateUtils.PATTERN_RFC1036,
DateUtils.PATTERN_ASCTIME,
"\"EEE, dd-MMM-yyyy HH:mm:ss z\""});
}
};
Registry<CookieSpecProvider> r = RegistryBuilder.<CookieSpecProvider>create()
.register(CookieSpecs.BEST_MATCH,
new BestMatchSpecFactory())
.register("custom", customSpecProvider)
.build();
RequestConfig requestConfig = RequestConfig.custom()
.setCookieSpec("custom")
.build();
CloseableHttpClient client = HttpClients.custom()
.setDefaultCookieSpecRegistry(r)
.setDefaultRequestConfig(requestConfig)
.build();
HttpClientContext context = HttpClientContext.create();
context.setCookieSpecRegistry(r);
HttpPost post = new HttpPost("http://saucelabs.com/rest/v1/storage/"
+ username + "/" + fileName + "?overwrite=" + overwrite.toString());
FileEntity entity = new FileEntity(file);
entity.setContentType(new BasicHeader("Content-Type",
"application/octet-stream"));
post.setEntity(entity);
post.setHeader("Content-Type", "application/octet-stream");
post.setHeader("Authorization", encodeAuthentication());
HttpResponse response = client.execute(post, context);
BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
String line;
StringBuilder builder = new StringBuilder();
while ((line = rd.readLine()) != null) {
builder.append(line);
}
try {
JSONObject sauceUploadResponse = new JSONObject(builder.toString());
if (sauceUploadResponse.has("error")) {
throw new UnexpectedException("Failed to upload to sauce-storage: "
+ sauceUploadResponse.getString("error"));
}
return sauceUploadResponse.getString("md5");
} catch (JSONException j) {
throw new UnexpectedException("Failed to parse json response.", j);
}
}
/**
* Generates a link to the job page on Saucelabs.com, which can be accessed
* without the user's credentials. Auth token is HMAC/MD5 of the job ID with
* the key <username>:<api key>
* (see {@link http://saucelabs.com/docs/integration#public-job-links}).
*
* @param jobId the Sauce Job Id, typically equal to the Selenium/WebDriver
* sessionId
* @return link to the job page with authorization token
*/
public String getPublicJobLink(String jobId) {
try {
String key = username + ":" + accessKey;
String auth_token = SecurityUtils.hmacEncode("HmacMD5", jobId, key);
String link = "https://saucelabs.com/jobs/" + jobId + "?auth=" + auth_token;
return link;
} catch (IllegalArgumentException ex) {
// someone messed up on the algorithm to hmacEncode
// For available algorithms see {@link http://docs.oracle.com/javase/7/docs/api/javax/crypto/Mac.html}
// we only want to use 'HmacMD5'
System.err.println("Unable to create an authenticated public link to job:");
System.err.println(ex);
return "";
}
}
// TODO: Test this
//http://stackoverflow.com/questions/13109588/base64-encoding-in-java
/**
*
* @return
*/
protected String encodeAuthentication() {
String auth = username + ":" + accessKey;
auth = "Basic " + new String(Base64.encodeBase64(auth.getBytes()));
return auth;
}
/**
*
* @param tunnelId
*/
public void deleteTunnel(String tunnelId) {
HttpURLConnection postBack = null;
try {
URL restEndpoint = new URL(String.format(DELETE_TUNNEL_FORMAT, username, tunnelId));
postBack = openConnection(restEndpoint);
postBack.setDoOutput(true);
postBack.setRequestMethod("DELETE");
addAuthenticationProperty(postBack);
postBack.getOutputStream().write("".getBytes());
} catch (IOException e) {
logger.log(Level.WARNING, "Error stopping Sauce Job", e);
}
try {
if (postBack != null) {
postBack.getInputStream().close();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Error closing result stream", e);
}
}
/**
*
* @return
*/
public String getTunnels() {
URL restEndpoint = null;
try {
restEndpoint = new URL(String.format(GET_TUNNEL_FORMAT, username));
} catch (MalformedURLException e) {
logger.log(Level.WARNING, "Error constructing Sauce URL", e);
}
return retrieveResults(restEndpoint);
}
}