Created
February 25, 2015 09:23
-
-
Save sadikovi/55c3cad72304397f8fc7 to your computer and use it in GitHub Desktop.
Example of Gatling scenario that uses complex authentication with response processing (asking for auth-token, encrypting it, sending back, verifying logon). Each "browsing" request is sent, and based on response several sub-requests are generated, imitating drill-down into some piece of data on a website.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package systemsimulation.general | |
object AuthDataConst { | |
// constants | |
val DEFAULT_ITERATIONS = 1024; | |
val DEFAULT_BLOCK_SIZE = 256; | |
val DEFAULT_SALT = "Adfas245766&#N%^BW,.|%^&*"; | |
// indicates whether the encryption is on or off | |
// affects key encryption and user token | |
val MOBILE_ENCRYPTION = true; | |
} | |
class AuthData(token: String, numberOfIterations: Int, blockSize: Int, MobileEncryption: Boolean, salt: String) { | |
val _token = token; | |
val _iterations = numberOfIterations; | |
val _blockSize = blockSize; | |
val _salt = salt; | |
val _encryption = MobileEncryption; | |
def this(token: String, iterations: Int, blocksize: Int, MobileEncryption: Boolean) | |
= this(token, iterations, blocksize, MobileEncryption, AuthDataConst.DEFAULT_SALT); | |
def this(token: String, iterations: Int, blocksize: Int) | |
= this(token, iterations, blocksize, AuthDataConst.MOBILE_ENCRYPTION, AuthDataConst.DEFAULT_SALT); | |
def this(token: String) | |
= this(token, AuthDataConst.DEFAULT_ITERATIONS, AuthDataConst.DEFAULT_BLOCK_SIZE, AuthDataConst.MOBILE_ENCRYPTION, AuthDataConst.DEFAULT_SALT); | |
def this(token: String, iterations: Int, encryption: Boolean) | |
= this(token, iterations, AuthDataConst.DEFAULT_BLOCK_SIZE, encryption, AuthDataConst.DEFAULT_SALT); | |
def getToken(): String = { | |
return _token; | |
} | |
def getIterations(): Int = { | |
return _iterations; | |
} | |
def getBlockSize(): Int = { | |
return _blockSize; | |
} | |
def getSalt(): String = { | |
return _salt; | |
} | |
def getEncryption(): Boolean = { | |
return _encryption; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package systemsimulation.general | |
import java.util.UUID | |
object DeviceDataConst { | |
val DEFAULT_TYPE:String = "Firefox"; | |
val DEFAULT_VERSION:Int = 35; | |
val DEFAULT_INFO:String = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0"; | |
def randomId() : String = { | |
return UUID.randomUUID().toString(); | |
} | |
} | |
class DeviceData(cid: String, ctype: String, cversion: Int, caddInfo: String) { | |
// device id | |
val _id: String = cid; | |
// device type | |
val _type: String = ctype; | |
// device version | |
val _version: Int = cversion; | |
// device additional information | |
val _addInfo: String = caddInfo; | |
def this(cid: String, ctype: String, cversion: Int) | |
= this(cid, ctype, cversion, DeviceDataConst.DEFAULT_INFO); | |
def this(cid: String) | |
= this(cid, DeviceDataConst.DEFAULT_TYPE, DeviceDataConst.DEFAULT_VERSION); | |
def this() | |
= this(DeviceDataConst.randomId(), DeviceDataConst.DEFAULT_TYPE, DeviceDataConst.DEFAULT_VERSION); | |
def getId() : String = { | |
//return _id; | |
// generate random id every time method is called | |
return DeviceDataConst.randomId(); | |
} | |
def getType() : String = { | |
return _type; | |
} | |
def getVersion() : Int = { | |
return _version; | |
} | |
def getAddInfo() : String = { | |
return _addInfo; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package systemsimulation.general | |
object DomainProxy { | |
val host = "localhost" | |
val port = 8080 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package systemsimulation.general | |
class UserToken(username: Option[String], password: Option[String]) { | |
var _username = username match { | |
case None => "unknown" | |
case _ => username.get | |
}; | |
var _password = password match { | |
case None => "unknown" | |
case _ => password.get | |
}; | |
// data type for the user token | |
val _datatype = "BasicAuthenticationUserToken"; | |
def changePassword(newPassword: String) { | |
_password = newPassword; | |
} | |
def getUsername() : String = { | |
return _username; | |
} | |
def getPassword() : String = { | |
return _password; | |
} | |
def getDatatype() : String = { | |
return _datatype; | |
} | |
def getJsonString() : String = { | |
var json = """{"DataType":"""" + _datatype + """",""" + | |
""""Username":"""" + _username + """",""" + | |
""""Password":"""" + _password + """"}"""; | |
return json; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package systemsimulation.general | |
import scala.util.parsing.json._ | |
import scala.util.Random | |
object Util { | |
def _createPair(key: String, value: Any) : String = value match { | |
case a:String => "\"" + key + "\"" + ":" + "\"" + a + "\""; | |
case b:Map[String,Any] => "\"" + key + "\"" + ":" + jsonFromMap(b); | |
case _ => "\"" + key + "\"" + ":" + "\"" + "_unknown_" + "\""; | |
} | |
def _wrapJsonString(json: String) : String = { | |
return "{ " + json + " }"; | |
} | |
def jsonFromMap(map: Map[String,Any]) : String = { | |
var jsonList:List[String] = List(); | |
map.keys.foreach { | |
key => | |
var pair:String = _createPair(key, map(key)); | |
jsonList = jsonList ::: List(pair) | |
} | |
var jsonBody:String = jsonList.mkString(","); | |
return _wrapJsonString(jsonBody); | |
} | |
def mapFromJsonString(jsonString: String) : Map[String, Any] = { | |
var option:Option[Any] = JSON.parseFull(jsonString); | |
var json = option.get.asInstanceOf[Map[String, Any]]; | |
return json; | |
} | |
// generate random string based on ASCII table | |
def _randomStringFromASCII(length: Int, minCharCode: Int, maxCharCode: Int) : String = { | |
val (min, max) = (minCharCode, maxCharCode); | |
def nextDigit = Random.nextInt(max - min) + min | |
return new String(Array.fill(length)(nextDigit.toByte), "ASCII"); | |
} | |
// generate random string, sort of password | |
def randomString(length: Int) : String = { | |
return _randomStringFromASCII(length, 33, 126); | |
} | |
// generate random string using only letters | |
def randomName(length: Int) : String = { | |
return _randomStringFromASCII(length, 97, 122); | |
} | |
// random index | |
def randomIndex(min:Int, max:Int): Int = { | |
if (min >= max) | |
return min; | |
return Random.nextInt(max - min) + min; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package systemsimulation | |
import io.gatling.core.Predef._ | |
import io.gatling.core.session._ | |
import io.gatling.http.Predef._ | |
import scala.concurrent.duration._ | |
import general._ | |
class SystemSimulation extends Simulation { | |
// configure proxy | |
val httpProxy = Proxy(DomainProxy.host, DomainProxy.port) | |
// json file feeder | |
val JSON_FEEDER = "login.json" | |
// number of threads (users) | |
val SIMULATION_THREADS = 100 | |
// number of times to try login process | |
var LOGIN_ATTEMPTS = 1 | |
// number of seconds user spends on webpage | |
object WebpageViewTime { | |
val min = 5; | |
val max = 10; | |
} | |
// set headers | |
val commonHeaders = Map( | |
"Host" -> "unwired.mysuperworld.com", | |
"User-Agent" -> "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0", | |
"Accept" -> "application/json, text/javascript, */*; q=0.01", | |
"Accept-Language" -> "en-US,en;q=0.5", | |
"DNT" -> "1", | |
"X-Requested-With" -> "XMLHttpRequest", | |
"Referer" -> "https://unwired.mysuperworld.com/mrddres_residential/", | |
"Connection" -> "keep-alive" | |
) | |
// configure basic http | |
val httpConf = http | |
.proxy(httpProxy) | |
.baseURL("https://unwired.mysuperworld.com/mrddres_residential") | |
.headers(commonHeaders) | |
// object Actions to parse response and return new AuthData | |
object Actions { | |
// fetch token key and number of iterations and store them back in session | |
def retrieveAuthData(session: Session, datakey: String) : AuthData = { | |
// get response as a string | |
var t:String = session.get(datakey).as[String]; | |
// remove everything from the beginning of the string to reach first symbol "{" | |
var altered:String = t.slice(t.indexOfSlice("{"), t.length); | |
var a:Map[String, Any] = Util.mapFromJsonString(altered); | |
// retrieve parameters from json | |
var token:String = a("Token").asInstanceOf[String]; | |
var iterations:Int = a("KeyStrengtheningIterations").asInstanceOf[Double].toInt; | |
var mobileEncryption:Boolean = a("MobileEncryption").asInstanceOf[Boolean]; | |
return new AuthData(token, iterations, mobileEncryption); | |
} | |
// extract json map from session for datakey | |
def getJsonMap(session: Session, datakey: String) : Map[String, Any] = { | |
var t:String = session.get(datakey).as[String] | |
var altered:String = t.slice(t.indexOfSlice("{"), t.length); | |
var a:Map[String, Any] = Util.mapFromJsonString(altered); | |
return a; | |
} | |
def extractLocations(session:Session, datakey:String) : List[String] = { | |
var _list:List[String] = List(); | |
var a = Actions.getJsonMap(session, datakey); | |
var icps = a("Icps").asInstanceOf[List[Any]]; | |
for (x <- icps) { | |
var cn:String = (x.asInstanceOf[Map[String, String]])("IcpNumber"); | |
if (cn != null) { _list = _list :+ cn; } | |
} | |
return _list; | |
} | |
def extractCurrentPeriodDate(session:Session, datakey:String) : String = { | |
var a = Actions.getJsonMap(session, datakey); | |
var currentPeriodDate = a("CurrentPeriodDate").asInstanceOf[String]; | |
return currentPeriodDate; | |
} | |
def extractDatesList(session:Session, datakey:String) : List[String] = { | |
var _list:List[String] = List(); | |
var a = Actions.getJsonMap(session, datakey); | |
var usage = a("Usage").asInstanceOf[Map[String, String]]; | |
var lDate:String = usage("LastDataReceivedDate"); | |
var fDate:String = usage("FirstDataReceivedDate"); | |
if (lDate != null) {_list = _list :+ lDate;} | |
if (fDate != null) { _list = _list :+ fDate;} | |
return _list; | |
} | |
// extract dates for drill down | |
def extractDrills(session:Session, datakey:String) : List[String] = { | |
var _list:List[String] = List(); | |
var a = Actions.getJsonMap(session, datakey); | |
var b = a("GraphInUnits").asInstanceOf[Map[String, Any]]; | |
var graph = b("Graph").asInstanceOf[Map[String, Any]]; | |
var graphBars = graph("GraphBars").asInstanceOf[List[Map[String, Any]]]; | |
// fill list with possible drills | |
for (key <- graphBars) { | |
var action = key("ViewActionPeriod").asInstanceOf[Map[String, Any]]; | |
if (action != null && action.contains("DocumentToPost")) { | |
var documentToPost = Util.jsonFromMap(action("DocumentToPost").asInstanceOf[Map[String, String]]); | |
if (documentToPost != null) { _list = _list :+ documentToPost; } | |
} | |
} | |
return Actions._randomlyFilterList(_list); | |
} | |
// extract feature actions | |
def extractFeatureActions(session:Session, datakey:String) : List[String] = { | |
var _list:List[String] = List(); | |
var a = Actions.getJsonMap(session, datakey); | |
var features = a("ChangeGraphFeaturesActions").asInstanceOf[List[Map[String, Any]]]; | |
// fill list with possible drills | |
for (key <- features) { | |
var action = key("DocumentToPost").asInstanceOf[Map[String, Any]]; | |
if (action != null) { | |
var documentToPost = Util.jsonFromMap(action.asInstanceOf[Map[String, String]]); | |
if (documentToPost != null) { _list = _list :+ documentToPost; } | |
} | |
} | |
return Actions._randomlyFilterList(_list); | |
} | |
// randmoly filter list that is passed and return a new list | |
def _randomlyFilterList(list: List[String]) : List[String] = { | |
var _selectedList:List[String] = List(); | |
if (list.length > 0) { | |
// get upper bound as a number between 1 and 3 | |
var n:Int = Util.randomIndex(1, 3); | |
// check if n is in list range | |
if (list.length < n) { n = list.length; } | |
var bound:Int = list.length-1; | |
for (i <- 0 to n) { | |
var j:Int = Util.randomIndex(0, bound); | |
_selectedList = _selectedList :+ list(j); | |
} | |
} | |
return _selectedList; | |
} | |
} | |
// create feeder with credentials | |
val feeder = jsonFile(JSON_FEEDER).queue; | |
// Auth actions | |
object Auth { | |
var device = new DeviceData(); | |
// pick user from feeder | |
// and set username, password, and deviceId | |
var pickUser = feed(feeder) | |
// different actions for authentication | |
// 1. check if session is on, that we have logged in | |
val checkSessionIsOn = exec( | |
http("check_session_is_on") | |
.get("/webservices/mobile") | |
.check(status.is(200)) | |
) | |
// 2. check if session is off, that we are not logged in | |
val checkSessionIsOff = exec( | |
http("check_session_is_off") | |
.get("/webservices/mobile") | |
.check(status.is(401)) | |
) | |
// 3. login chain | |
// login chain of requests | |
val login = tryMax(LOGIN_ATTEMPTS) { | |
exec( | |
http("requesting_onetime_token") | |
.post("/webservices/mobile/core/oneTimeLoginToken") | |
.headers(Map( | |
"Accept" -> "application/json, text/javascript, */*; q=0.01", | |
"Content-Type" -> "application/x-www-form-urlencoded; charset=UTF-8" | |
) | |
) | |
.body(StringBody( | |
Util.jsonFromMap(Map( | |
"DocumentType" -> "OneTimeLoginTokenRequestDocument", | |
"Username" -> "${username}", | |
"DeviceId" -> "${deviceId}", | |
"DeviceType" -> device.getType() | |
)) | |
)) | |
.check(status.is(200)) | |
.check(bodyString.saveAs("mobile_response")) | |
) | |
.exec{ session => | |
var userToken = new UserToken( | |
session("username").asOption[String], | |
session("password").asOption[String] | |
); | |
//println(session("username").as[String] + "->" + session("password").as[String]) | |
val authdata = Actions.retrieveAuthData(session, "mobile_response"); | |
val encToken = MobileEncryption.encryptUserToken(userToken, authdata); | |
session.set("mobile_encryptedUserToken", encToken); | |
} | |
.exec( | |
http("sending_login_token") | |
.post("/webservices/mobile/core/sessions") | |
.body(StringBody( | |
Util.jsonFromMap(Map( | |
"DocumentType" -> "SessionCreationDocument", | |
"Username" -> "${username}", | |
"UserToken" -> "${mobile_encryptedUserToken}", | |
"DeviceInformation" -> Map( | |
"AdditionalInformation" -> device.getAddInfo(), | |
"DeviceModel" -> device.getVersion().toString, | |
"DeviceType" -> device.getType(), | |
"DeviceId" -> "${deviceId}" | |
) | |
)) | |
)) | |
.check(status.is(200)) | |
) | |
.exec( | |
http("checking_login_success") | |
.get("/webservices/mobile") | |
.check(status.is(200)) | |
) | |
}.exitHereIfFailed | |
// logout - server side logging out + clearing all the cookies | |
var logout = exec{ session => | |
var usertoken = new UserToken( | |
session("username").asOption[String], | |
session("password").asOption[String] | |
); | |
val authdata = new AuthData(""); | |
val encToken = MobileEncryption.encryptUserToken(usertoken, authdata); | |
session.set("mobile_logoutToken", encToken); | |
} | |
.exec( | |
http("logging_out") | |
.post("/webservices/mobile/core/sessions/current") | |
.body(StringBody("${mobile_logoutToken}")) | |
.check(status.is(200)) | |
) | |
.exec(flushSessionCookies) | |
} | |
// additional super login flow | |
object SuperLoginFlow { | |
val sendCustomerNumber = exec( | |
http("customer_send_number") | |
.post("/webservices/arc/residential/users/all") | |
.headers(Map( | |
"Accept" -> "application/json, text/javascript, */*; q=0.01", | |
"Content-Type" -> "application/x-www-form-urlencoded; charset=UTF-8" | |
)) | |
.body(StringBody( | |
Util.jsonFromMap(Map( | |
"CustomerId" -> "${customerNumber}", | |
"DocumentType" -> "ConsumerUsageSummaryRequest" | |
)) | |
)) | |
.check(status.is(200)) | |
) | |
val getUserInfo = exec( | |
http("super_get_user_info") | |
.get("/webservices/arc/residential/users/byid/${customerNumber}") | |
.check(status.is(200)) | |
) | |
/* call for start browsing */ | |
// it will automatically call every method for a particular location/date | |
val startBrowsing = exec( | |
http("super_get_start_browsing") | |
.get("/webservices/arc/residential/powerusage/all") | |
.check(status.is(200)) | |
.check(bodyString.saveAs("super_powerusage_all")) | |
) | |
.pause(WebpageViewTime.min, WebpageViewTime.max) | |
.exec { session => | |
var loc = Actions.extractLocations(session, "super_powerusage_all") | |
var date = Actions.extractCurrentPeriodDate(session, "super_powerusage_all") | |
session | |
.set("super_locations", loc) | |
.set("super_current_date", date) | |
} | |
.foreach("${super_locations}", "location") { | |
ViewLocation.run | |
} | |
} | |
object ViewLocation { | |
var run = exec( | |
http("super_customer_location") | |
.post("/webservices/arc/residential/powerusage/all") | |
.check(status.is(200)) | |
.body(StringBody(Util.jsonFromMap(Map( | |
"ConsumerName" -> "${location}", | |
"Mode" -> "Cumulative", | |
"RequestDate" -> "${super_current_date}", | |
"TimePeriod" -> "Month", | |
"FeatureMode" -> "ShowTotalUsage", | |
"DocumentType" -> "ConsumerDetailRequest" | |
)))) | |
.check(bodyString.saveAs("super_powerusage_location")) | |
) | |
.pause(WebpageViewTime.min, WebpageViewTime.max) | |
.exec { session => | |
val ses = "super_powerusage_location" | |
var dates = Actions.extractDatesList(session, ses) | |
session | |
.set("super_dates_list", dates) | |
.set("super_powerusage_location", "1") | |
} | |
.foreach("${super_dates_list}", "super_search_date") { | |
ViewYear.run | |
} | |
} | |
// | |
object ViewYear { | |
var run = exec( | |
http("super_customer_year_view") | |
.post("/webservices/arc/residential/powerusage/all") | |
.body(StringBody(Util.jsonFromMap(Map( | |
"ConsumerName" -> "${location}", | |
"Mode" -> "Cumulative", | |
"RequestDate" -> "${super_search_date}", | |
"TimePeriod" -> "Year", | |
"FeatureMode" -> "ShowTotalUsage", | |
"DocumentType" -> "ConsumerDetailRequest" | |
)))) | |
.check(bodyString.saveAs("super_powerusage_location_year")) | |
) | |
.pause(WebpageViewTime.min, WebpageViewTime.max) | |
.exec { session => | |
val ses = "super_powerusage_location_year" | |
var bars = Actions.extractDrills(session, ses) | |
val features = Actions.extractFeatureActions(session, ses) | |
session | |
.set("super_temp_post", bars) | |
.set("super_temp_new_features", features) | |
.set("super_powerusage_location_year", "1") | |
} | |
.foreach("${super_temp_post}", "super_document_to_post_month") { | |
ViewMonth.run | |
} | |
.foreach("${super_temp_new_features}", "super_document_to_post_year_new_feature") { | |
ChangeFeature.run | |
} | |
} | |
object ViewMonth { | |
val run = exec( | |
http("super_customer_month_view") | |
.post("/webservices/arc/residential/powerusage/all") | |
.check(status.is(200)) | |
.body(StringBody("${super_document_to_post_month}")) | |
.check(bodyString.saveAs("super_powerusage_location_month")) | |
) | |
.pause(WebpageViewTime.min, WebpageViewTime.max) | |
.exec { session => | |
val ses = "super_powerusage_location_month" | |
var bars = Actions.extractDrills(session, ses) | |
session | |
.set("super_temp_post_month", bars) | |
.set("super_powerusage_location_month", "1") | |
} | |
.foreach("${super_temp_post_month}", "super_document_to_post_day") { | |
ViewDay.run | |
} | |
} | |
object ViewDay { | |
val run = exec( | |
http("super_customer_day_view") | |
.post("/webservices/arc/residential/powerusage/all") | |
.check(status.is(200)) | |
.body(StringBody("${super_document_to_post_day}")) | |
) | |
.pause(WebpageViewTime.min, WebpageViewTime.max) | |
} | |
object ChangeFeature { | |
val run = exec( | |
http("super_customer_year_new_feature") | |
.post("/webservices/arc/residential/powerusage/all") | |
.check(status.is(200)) | |
.body(StringBody("${super_document_to_post_year_new_feature}")) | |
) | |
.pause(WebpageViewTime.min, WebpageViewTime.max) | |
} | |
// create scenario for one user | |
val superAdmin = scenario("Super Admin") | |
.exec( | |
Auth.pickUser, | |
Auth.checkSessionIsOff, | |
Auth.login, | |
Auth.checkSessionIsOn, | |
SuperLoginFlow.sendCustomerNumber, | |
SuperLoginFlow.getUserInfo, | |
SuperLoginFlow.startBrowsing, | |
Auth.logout, | |
Auth.checkSessionIsOff | |
) | |
// set it all up | |
setUp( | |
superAdmin.inject(atOnceUsers(SIMULATION_THREADS)) | |
).protocols(httpConf) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks @sadikovi this really very helpful. I'm facing similar kind of problem, could you please check
Here is my scenario.
But I'm getting error as