Monday, April 11, 2011

curl + 2xml for simple http get post web test command line scripting

Web programming can get complicated pretty quickly. Everybody wishes they could automatically test their website when they make a change to ensure they haven't broken anything. For many websites this is a large and tedious task that screams out for automation. It would be nice if you could separate your web-application's functionality from the details of browser compatibility. Great strides have been taken to improve compatibility between browser versions but quirks still (and will probably always) exist.
Some web automation tests really can and should be agnostic of the web browser being used at that point in time. Whichever version of Firefox, Chrome, Safari, Opera, IE , Lynx :), etc.. people are into at that moment.

So to ensure your test automation doesn't rely on some test framework version or browser quirks, you must take the long view, you have to ensure you use only methods from the w3c http spec, get && post with or without cookies. These are the web operations most likely to not change and remain compatible for the lifetime of your application.

Two interesting ways of doing this are either via some curl library in your favorite programming language or alternatively via the handy curl executable compiled for your operating system, which is available on 99.99% of computers today.

Let me get this out of the way. "You obviously cannot execute or test all client side JavaScript via curl." Nevertheless when any regular Javascript or even Ajax function communicates with the server it does so using standard http commands and curl can handle all of those operations. Here are some very useful sample commands that might surprise you by their simplicity and power. A consequence of building your website to be testable in this manner enforces a restful and simple design, regardless of whether you continue to test this way in future. Most higher level web testing frameworks are compatible with the operations I'm going to show you in curl, so once you've spiked out your test at the command line you'll see its trivial to convert this into another language/framework since these basic operations are universal to the web itself.

If you add xml2 and the grep command you can easily prototype some test scenarios that could subsequently be scripted. The reason I'm doing this is that I often want to prototype new website designs and functions and I don't really want to have to manually log in and navigate to the page in question if this can be automated at the command-line.


If you followed along with my previous post on federated login with spring-roo you'll have a copy of the same app I'm running my commands against.
We'll assume you did and your server and app are:

myserver=http://localhost:8080
appname=openidlogin


Example:
Most pages need redirection capability so this means add the -L flag to follow the location as it changes.
Additionally we're gonna need to know where we've been redirected to, we'll also add the -v for verbose flag.

myflags="-L -v"

curl $myflags "$myserver/$appname/"

\\lots of html in here returned\\

So that's a lot of xhtml that came back lets make that human friendly with xml2/html2 and grep
xml2/html2 gives you an xpath representation of each xml element on a separate line which makes them grepable.

So for example:

curl $myflags "$myserver/$appname" | xml2

on my app i get back something like this:

/html/body/div/div/a/@title=Home
/html/body/div/div/a/@href=/openidlogin/;jsessionid=F5E83F4ABC01CB1B0C8D222E6AE22B29
/html/body/div/div/ul/li/ul/li/a/@title=Create new Employee
/html/body/div/div/ul/li/ul/li/a/@href=/openidlogin/employees;jsessionid=F5E83F4ABC01CB1B0C8D222E6AE22B29?form
/html/body/div/div/ul/li/ul/li/a/@title=List all Employees
/html/body/div/div/ul/li/ul/li/a/@href=/openidlogin/employees;jsessionid=F5E83F4ABC01CB1B0C8D222E6AE22B29?page=1&size=10
/html/body/div/div/ul/li/ul/li/a/@title=Find by Open Id Identifier
/html/body/div/div/ul/li/ul/li/a/@href=/openidlogin/employees;jsessionid=F5E83F4ABC01CB1B0C8D222E6AE22B29?find=ByOpenIdIdentifier&form
/html/body/div/div/div/div/@id=_title_title_id

This is not too bad actually. Right? And you were worried about not needing a web browser :). So you see here some link titles and hrefs for the basic CRUD operations on my app as well as a spring roo finder.
We also received a
Set-Cookie: JSESSIONID=F5E83F4ABC01CB1B0C8D222E6AE22B29; Path=/openidlogin
command from the server. This is our unique session token, it's been appended to our urls. Yours would be different.

To move forward from here, as is the case for many situations in life, we are going to need some cookies! Else every time we hit our server its going to think we are a different person. Curl gives us this functionality with two flags, one to store cookies, the other to send cookies. We store with a cookiejar -c flag. And we send with the -b flag. lets add those flags now.

myflags="-L -v -b cookiejar.txt -c cookiejar.txt"

Now if hit the site we only get a cookie the first time and it gets saved and sent along when necessary.

curl $myflags "$myserver/$appname/" | xml2

cat cookiejar.txt
beastinator.local FALSE /openidlogin FALSE 0 JSESSIONID 89CF82D8C4211D5C6913B9B911B67C2B

That's our session. simple. And since we stored our cookies properly the jsessionid isn't appended to the url anymore which cleans things up a lot:

/html/body/div/div/ul/@id=_menu
/html/body/div/div/ul/li/@id=c_employee
/html/body/div/div/ul/li/h2=Employee
/html/body/div/div/ul/li/ul/li/@id=i_employee_new
/html/body/div/div/ul/li/ul/li/a/@title=Create new Employee
/html/body/div/div/ul/li/ul/li/a/@href=/openidlogin/employees?form
/html/body/div/div/ul/li/ul/li/a=Create new Employee
/html/body/div/div/ul/li/ul/li
/html/body/div/div/ul/li/ul/li/@id=i_employee_list
/html/body/div/div/ul/li/ul/li/a/@title=List all Employees
/html/body/div/div/ul/li/ul/li/a/@href=/openidlogin/employees?page=1&size=10
/html/body/div/div/ul/li/ul/li/a=List all Employees
/html/body/div/div/ul/li/ul/li
/html/body/div/div/ul/li/ul/li/@id=fi_employee_openididentifier
/html/body/div/div/ul/li/ul/li/a/@title=Find by Open Id Identifier
/html/body/div/div/ul/li/ul/li/a/@href=/openidlogin/employees?find=ByOpenIdIdentifier&form
/html/body/div/div/ul/li/ul/li/a=Find by Open Id Identifier


Now lets try to go from here to the List operation on my Employee entity. namely the following link.

curl $myflags "$myserver/$appname/employees" | xml2

So here, based on the security settings for this app, we get redirected to a login page. As you can see in the header:

< HTTP/1.1 302 Moved Temporarily
< Server: Apache-Coyote/1.1
< Location: http://localhost:8080/openidlogin/login

Now things get interesting. We have three options to log in.

1. User/pass combo. In this case I've pre set up user1/password.
2. Sign in with Google Federated Login. So we'll have to sign in via a prelinked google openid.
3. Sign in with a more standard, Non-google openid provider. In this case I'll show you how to use myopenid.com.

1. User/pass combo method using user1/password. Lets look the first form action.

curl $myflags "$myserver/$appname/login" | xml2 | grep -i form

/html/body/div/div/div/div/form/@method=POST
/html/body/div/div/div/div/form/@action=/openidlogin/resources/j_spring_security_check
/html/body/div/div/div/div/form/@name=f
/html/body/div/div/div/div/form/div/label/@for=j_username
/html/body/div/div/div/div/form/div/label=Name
/html/body/div/div/div/div/form/div/input/@style=width:150px
/html/body/div/div/div/div/form/div/input/@name=j_username
/html/body/div/div/div/div/form/div/input/@type=text
/html/body/div/div/div/div/form/div/input/@id=j_username
/html/body/div/div/div/div/form/div/script/@type=text/javascript
/html/body/div/div/div/div/form/div/script=
/html/body/div/div/div/div/form/div/script= Spring.addDecoration(new Spring.ElementDecoration({elementId : "j_username", widgetType : "dijit.form.ValidationTextBox", widgetAttrs : {promptMessage: "Enter your name", required : true}}));
/html/body/div/div/div/div/form/div/script=
/html/body/div/div/div/div/form/br
/html/body/div/div/div/div/form/div/label/@for=j_password
/html/body/div/div/div/div/form/div/label=Password
/html/body/div/div/div/div/form/div/input/@style=width:150px
/html/body/div/div/div/div/form/div/input/@name=j_password
/html/body/div/div/div/div/form/div/input/@type=password
/html/body/div/div/div/div/form/div/input/@id=j_password
/html/body/div/div/div/div/form/div/script/@type=text/javascript
/html/body/div/div/div/div/form/div/script=
/html/body/div/div/div/div/form/div/script= Spring.addDecoration(new Spring.ElementDecoration({elementId : "j_password", widgetType : "dijit.form.ValidationTextBox", widgetAttrs : {promptMessage: "Enter your password", required : true}}));
/html/body/div/div/div/div/form/div/script=
/html/body/div/div/div/div/form/br
/html/body/div/div/div/div/form/div/@class=submit
/html/body/div/div/div/div/form/div/script/@type=text/javascript
/html/body/div/div/div/div/form/div/script=Spring.addDecoration(new Spring.ValidateAllDecoration({elementId:'proceed', event:'onclick'}));
/html/body/div/div/div/div/form/div/input/@value=Submit
/html/body/div/div/div/div/form/div/input/@type=submit
/html/body/div/div/div/div/form/div/input/@id=proceed
/html/body/div/div/div/div/form/div/input
/html/body/div/div/div/div/form/div/input/@value=Reset
/html/body/div/div/div/div/form/div/input/@type=reset
/html/body/div/div/div/div/form/div/input/@id=reset


Pretty simple. We have j_username and j_password fields with submit and reset buttons and the http method is post to this url /openidlogin/resources/j_spring_security_check.

So in curl to make a post command all we have to do is add the data flag -d and point to the action url, like the following.

curl $myflags -d "j_username=user1&j_password=password" "$myserver/$appname/resources/j_spring_security_check" | xml2

thats it! we're in. we don't have to send the username and password again so we don't need to add this to $myflags since our session cookie keeps us authenticated.

lets log out now and try the next method.
curl $myflags "$myserver/$appname/resources/j_spring_security_logout" | html2

2. lets try it for google. this is the part of the form we're interested in:
visit google sign in page to get the GALX cookie
curl $myflags http://gmail.com

grab the GALX token from the cookie:
GALX=`cat cookiejar.txt | grep www.google.com | grep GALX | awk '{print $NF}'`

set a google user and pass
goog_user=your_goog_email
goog_pass=your_goog_pass

login to google:
curl $myflags -d "Email=$goog_user&GALX=$GALX&Passwd=$goog_pass&asts=&continue=https://mail.google.com/mail/?&dnConn=&dsh=$dsh&hl=en<mpl=default<mplcache=2&pstMsg=&rm=false&rmShown=1&scc=1&secTok&service=mail&signIn=Sign in&ss=1&timeStmp=" https://www.google.com/accounts/ServiceLoginAuth | html2

you now have all the fancy google cookies you need for oauth. (also google is tracking you :) )

So here's how we use the fancy cookies to log into our site. I'm going to assume you already granted permissions on the google account in question for this app.

so lets look at the part of our form that does google oauth:

curl $myflags "$myserver/$appname/login" | html2 | grep -i form

/html/body/div/div/div/div/form/@method=post
/html/body/div/div/div/div/form/@action=/openidlogin/j_spring_openid_security_check;jsessionid=00FD152C5918A248CD10CEC030C4F864
/html/body/div/div/div/div/form/input/@value=http://www.google.com/accounts/o8/id
/html/body/div/div/div/div/form/input/@type=hidden
/html/body/div/div/div/div/form/input/@maxlength=100
/html/body/div/div/div/div/form/input/@size=50
/html/body/div/div/div/div/form/input/@name=openid_identifier
/html/body/div/div/div/div/form/div/@class=submit
/html/body/div/div/div/div/form/div/input/@value=Sign in with Google
/html/body/div/div/div/div/form/div/input/@type=submit
/html/body/div/div/div/div/form/div/input/@id=proceed_google


looks like all we need to do is send on the hard coded openid_identifier http://www.google.com/accounts/o8/id

curl $myflags -d "openid_identifier=http://www.google.com/accounts/o8/id" "$myserver/$appname/j_spring_openid_security_check"

let all the fancy oauth redirecting do its thing with your cookies, then you can query employees since you are logged in!

curl $myflags "$myserver/$appname/employees" | html2 | grep "/html/body/div/div/div/div/table/tr/td="

/html/body/div/div/div/div/table/tr/td=ACTIVE
/html/body/div/div/div/div/table/tr/td=ROLE_ADMIN
/html/body/div/div/div/div/table/tr/td=5e884898da
/html/body/div/div/div/div/table/tr/td=user1
/html/body/div/div/div/div/table/tr/td=ACTIVE
/html/body/div/div/div/div/table/tr/td=ROLE_USER
/html/body/div/div/div/div/table/tr/td=5e884898da
/html/body/div/div/div/div/table/tr/td=user2
/html/body/div/div/div/div/table/tr/td=Peter
/html/body/div/div/div/div/table/tr/td=RESIGNED
/html/body/div/div/div/div/table/tr/td=ROLE_USER
/html/body/div/div/div/div/table/tr/td=5e884898da
/html/body/div/div/div/div/table/tr/td=user3
/html/body/div/div/div/div/table/tr/td=Christina
/html/body/div/div/div/div/table/tr/td=


lets log out now and try the next method.
curl $myflags "$myserver/$appname/resources/j_spring_security_logout" | html2


3. Sign in with a more standard, Non-google openid provider. In this case I'll show you how to use myopenid.com.
head back to the login page and examine the third form action.

curl $myflags "$myserver/$appname/login" | xml2 | grep -i form

/html/body/div/div/div/div/form/@action=/openidlogin/j_spring_openid_security_check
/html/body/div/div/div/div/form/div/label/@for=openid_identifier
/html/body/div/div/div/div/form/div/label=OpenID
/html/body/div/div/div/div/form/div/input/@style=width:150px
/html/body/div/div/div/div/form/div/input/@type=text
/html/body/div/div/div/div/form/div/input/@name=openid_identifier
/html/body/div/div/div/div/form/div/input/@id=openid_identifier
/html/body/div/div/div/div/form/br
/html/body/div/div/div/div/form/div/@class=submit
/html/body/div/div/div/div/form/div/input/@value=Submit
/html/body/div/div/div/div/form/div/input/@type=submit
/html/body/div/div/div/div/form/div/input/@id=proceed_openid

so as before once we have our openid provider's cookie, we just send our openid provider url as a form post parameter and direct curl to the j_spring_openid_security_check action .

example for myopenid.com:

curl $myflags -d "openid_identifier=https://thamster.myopenid.com" "$myserver/$appname/j_spring_openid_security_check" | html2


so to sign in to myopenid.com you do the following:
curl $myflags "https://www.myopenid.com/signin" > myopenidsignin.html
blank=`cat myopenidsignin.html | html2 | grep "=_$" -A 1 | grep value | awk -F"@value=" '{print $2}' | tail -n 1`
tid=`cat myopenidsignin.html | html2 | grep "=tid$" -A 1 | grep value | awk -F"@value=" '{print $2}' | tail -n 1`
token=`cat myopenidsignin.html | html2 | grep "=token$" -A 1 | grep value | awk -F"@value=" '{print $2}' | head -n 1`
myopen_user=your_username
myopen_pass=your_password

curl $myflags -d "user_name=$myopen_user&password=$myopen_pass&_=$blank&tid=$tid&token=$token" "https://www.myopenid.com/signin_submit"

curl $myflags -d "openid_identifier=https://$myopen_user.myopenid.com" "$myserver/$appname/j_spring_openid_security_check" | html2

Thats it you're in. congrats, you didn't even need a browser.

    1 ## summary
2 ## for those of you who read bash better than english.
3
4 myflags="-L -v -b cookiejar.txt -c cookiejar.txt"
5 myserver=http://localhost:8080
6 appname=openidlogin
7
8 #login with username and pass
9 curl $myflags -d "j_username=user1" -d "j_password=password" "$myserver/$appname/resources/j_spring_security_check" | html2
10
11 #login with google oauth
12 #simplified gmail oauth login
13 curl $myflags http://gmail.com
14 GALX=`cat cookiejar.txt | grep www.google.com | grep GALX | awk '{print $NF}'`
15 goog_user=your_goog_user
16 goog_pass=your_goog_pass
17 curl $myflags -d "Email=$goog_user&GALX=$GALX&Passwd=$goog_pass&asts=&continue=https://mail.google.com/mail/?&dnConn=&dsh=$dsh&hl=en&ltmpl=default&ltmplcache=2&pstMsg=&rm=false&rmShown=1&scc=1&secTok&service=mail&signIn=Sign in&ss=1&timeStmp=" https://www.google.com/accounts/ServiceLoginAuth | html2
18 curl $myflags -d "openid_identifier=http://www.google.com/accounts/o8/id" "$myserver/$appname/j_spring_openid_security_check"
19
20
21 #login with myopenid.com
22 curl $myflags "https://www.myopenid.com/signin" > myopenidsignin.html
23 blank=`cat myopenidsignin.html | html2 | grep "=_$" -A 1 | grep value | awk -F"@value=" '{print $2}' | tail -n 1`
24 tid=`cat myopenidsignin.html | html2 | grep "=tid$" -A 1 | grep value | awk -F"@value=" '{print $2}' | tail -n 1`
25 token=`cat myopenidsignin.html | html2 | grep "=token$" -A 1 | grep value | awk -F"@value=" '{print $2}' | head -n 1`
26 myopen_user=username
27 myopen_pass=password
28 curl $myflags -d "user_name=$myopen_user&password=$myopen_pass&_=$blank&tid=$tid&token=$token" "https://www.myopenid.com/signin_submit"
29 curl $myflags -d "openid_identifier=https://$myopen_user.myopenid.com" "$myserver/$appname/j_spring_openid_security_check" | html2
30
31 #logout
32 curl $myflags "$myserver/$appname/resources/j_spring_security_logout" | html2
33
34 #list employees once logged in
35 curl $myflags "$myserver/$appname/employees" | html2 | grep "/html/body/div/div/div/div/table/tr/td="
36
37




more info on curl can be found here:
http://curl.haxx.se/docs/manual.html
http://curl.haxx.se/docs/manpage.html

No comments:

Post a Comment