You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@esme.apache.org by es...@apache.org on 2010/01/01 00:47:38 UTC

svn commit: r894955 - in /incubator/esme/trunk/server/src: main/resources/props/ main/scala/bootstrap/liftweb/ main/scala/org/apache/esme/api/ main/scala/org/apache/esme/model/ test/scala/org/apache/esme/api/

Author: esjewett
Date: Thu Dec 31 23:47:38 2009
New Revision: 894955

URL: http://svn.apache.org/viewvc?rev=894955&view=rev
Log:
Big-ish commit including new API2 tests and methods, plus the introduction of role-based authorization to certain API2 methods

Added:
    incubator/esme/trunk/server/src/main/resources/props/test.default.props
Modified:
    incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala
    incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/API2.scala
    incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/ApiHelper.scala
    incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/XMLHelper.scala
    incubator/esme/trunk/server/src/main/scala/org/apache/esme/model/User.scala
    incubator/esme/trunk/server/src/test/scala/org/apache/esme/api/API2Test.scala

Added: incubator/esme/trunk/server/src/main/resources/props/test.default.props
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/resources/props/test.default.props?rev=894955&view=auto
==============================================================================
--- incubator/esme/trunk/server/src/main/resources/props/test.default.props (added)
+++ incubator/esme/trunk/server/src/main/resources/props/test.default.props Thu Dec 31 23:47:38 2009
@@ -0,0 +1 @@
+role.api_test=integration-admin
\ No newline at end of file

Modified: incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala?rev=894955&r1=894954&r2=894955&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/bootstrap/liftweb/Boot.scala Thu Dec 31 23:47:38 2009
@@ -142,18 +142,21 @@
 
     LiftRules.setSiteMap(SiteMap(entries: _*))
 
-    S.addAround(ExtSession.requestLoans)
+    S.addAround(ExtSession.requestLoans)          
+
+    // API security rules
+    LiftRules.dispatch.append(API2.authorizationRules)
 
     // REST APIs (new and old)
     LiftRules.dispatch.prepend(RestAPI.dispatch)
-    LiftRules.dispatch.prepend(API2.dispatch)
+    LiftRules.dispatch.append(API2.dispatch)
 
     LiftRules.httpAuthProtectedResource.prepend {
       case ParsePath(l, _, _, _) if l startsWith TwitterAPI.ApiPath => Full(AuthRole("user"))
     }
 
     LiftRules.authentication = TwitterAPI.twitterAuth
-
+                               
     LiftRules.dispatch.append(TwitterXmlAPI.dispatch)
     LiftRules.dispatch.append(TwitterJsonAPI.dispatch)
 

Modified: incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/API2.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/API2.scala?rev=894955&r1=894954&r2=894955&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/API2.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/API2.scala Thu Dec 31 23:47:38 2009
@@ -20,16 +20,33 @@
  */
 
 /*
- * API2.scala
+ * API2.scala                        
+ *
+ * The structure of the API2 object is a dispatch rule table,
+ * which is a match against the Lift request object Req(). The match
+ * determines a function to call. The convention in our case is that
+ * the function has a return type of
+ * Box[Tuple3[Int,Map[String,String],Box[Elem]]]
+ * 
+ * This return type is inspired by Rack/WSGI and is implicitly
+ * converted to the correct type of Lift Response by the ApiHelper
+ * trait.
+ *
+ * The semantic structure of the response type
+ * Box[Tuple3[Int,Map[String,String],Box[Elem]]]
+ * is a Box (an Empty is converted to a 500 response) containing
+ * a 3-tuple, containing in order:
+ *   1. An Int representing the response code
+ *   2. A Map(String,String) representing response headers
+ *   3. A Box[Elem] containing the response body.
  *
- * To change this template, choose Tools | Template Manager
- * and open the template in the editor.
  */            
 
 package org.apache.esme.api
 
 import net.liftweb._
-import http._
+import http._  
+import auth._
 import actor._
 import rest._
 import util._
@@ -49,13 +66,24 @@
 object API2 extends ApiHelper with XmlHelper {
   val logger: Logger = Logger.getLogger("org.apache.esme.api")
 
+  def authorizationRules: LiftRules.DispatchPF = {
+    case Req("api2" :: "users" :: Nil, _, PostRequest)
+      if !User.checkRole("integration-admin") => unAuthorized  
+    case Req("api2" :: "users" :: _ :: tokens :: Nil, _, GetRequest)
+      if !User.checkRole("integration-admin") => unAuthorized
+    case Req("api2" :: "users" :: _ :: tokens :: Nil, _, PostRequest)
+      if !User.checkRole("integration-admin") => unAuthorized
+  }
+
   def dispatch: LiftRules.DispatchPF = {
     case Req("api2" :: "session" :: Nil, _, GetRequest) => allSessions
     case Req("api2" :: "session" :: Nil, _, PostRequest) => addSession          
     case Req("api2" :: "session" :: Nil, _, DeleteRequest) => removeSession                      
 	
-    case Req("api2" :: "users" :: Nil, _, GetRequest) => allUsers
-// Add a method to get detail for a specific user
+    case Req("api2" :: "users" :: Nil, _, GetRequest) => allUsers 
+    case Req("api2" :: "users" :: Nil, _, PostRequest) => addUser  
+    case Req("api2" :: "users" :: id :: "tokens" :: Nil, _, GetRequest) => () => allTokens(id)
+    case Req("api2" :: "users" :: id :: "tokens" :: Nil, _, PostRequest) => () => addToken(id)
                                                                           
     case Req("api2" :: "user" :: "messages" :: Nil, _, GetRequest)
  	  if S.param("timeout").isDefined => waitForMsgs
@@ -64,8 +92,8 @@
     case Req("api2" :: "user" :: "messages" :: Nil, _, GetRequest) => getNewMsgs    
     case Req("api2" :: "user" :: "messages" :: Nil, _, PostRequest) => () => addMsg
 
-    case Req("api2" :: "user" :: "tags" :: tag :: "messages" :: Nil, _, GetRequest)
-  		    => () => allUserMsgs(tag)                                                 
+    case Req("api2" :: "tags" :: tag :: "messages" :: Nil, _, GetRequest)
+  		    => () => allTagMsgs(tag)                                                 
 
     case Req("api2" :: "user" :: "followees" :: Nil, _, GetRequest) => allFollowees         
     case Req("api2" :: "user" :: "followees" :: Nil, _, PostRequest) => addFollowee
@@ -76,13 +104,11 @@
 
     case Req("api2" :: "user" :: "tracks" :: Nil, _, GetRequest) => allTracking
     case Req("api2" :: "user" :: "tracks" :: Nil, _, PostRequest) => addTracking
-// Add a method to get detail for a specific track (or messages for the track?)
     case Req("api2" :: "user" :: "tracks" :: trackId :: Nil, _, DeleteRequest) => () 
 			=> removeTracking(Box(List(trackId)))
 
     case Req("api2" :: "user" :: "actions" :: Nil, _, GetRequest) => allActions 
     case Req("api2" :: "user" :: "actions" :: Nil, _, PostRequest) => addAction
-// Add a method to get detail of a specific action
     case Req("api2" :: "user" :: "actions" :: actionId :: Nil, _, PutRequest) => () 
 			=> changeAction(Box(List(actionId)))
     case Req("api2" :: "user" :: "actions" :: actionId :: Nil, _, DeleteRequest) => () 
@@ -90,19 +116,11 @@
                                                                                 
     case Req("api2" :: "pools" :: Nil, _, GetRequest) => allPools 
     case Req("api2" :: "pools" :: Nil, _, PostRequest) => () => addPool 
-// Add a method to delete pool
-// Add a method to get the detail for a pool
-// Add a method to get the list of users in a pool
     case Req("api2" :: "pools" :: poolId :: "users" :: Nil, _, PostRequest) => () 
 			=> addUserToPool(Box(List(poolId))) 
-// Add a method to delete a user from a pool   
-// Add a method to get the messages from a pool
-// Add a method to post a new message to a pool
     
-// Add a method to get list of conversations
     case Req("api2" :: "conversations" :: conversationId :: Nil, _, GetRequest) => () 
 			=> getConversation(Box(List(conversationId)))
-// Add a method to post a message to a conversation??                          
   }
 
   def allSessions(): LiftResponse = {
@@ -128,7 +146,8 @@
       } yield {
         User.logUserIn(user)
         val myActor = buildActor(user.id)
-        restActor(Full(myActor))
+        messageRestActor(Full(myActor))   
+        userRoles(AuthRole("integration-admin"))
         (200,Map(),Full(user_xml))     
       }
 
@@ -156,10 +175,67 @@
 
 	r
   }
+
+  def addUser(): LiftResponse = {
+    val moduleName: String = "upw"
+    
+    val r: Box[Tuple3[Int,Map[String,String],Box[Elem]]] = {
+      for{
+        nickName <- S.param("nickname")
+        passWord <- S.param("password")      
+      } yield {
+       	User.findByNickname(nickName) match {
+          case user :: _ => (200,Map(),Full(userToXml(user)))
+          case _ =>
+            val user = User.createAndPopulate.nickname(nickName).saveMe                                
+            val salt = randomString(10)
+	        val md5 = Helpers.md5(salt + passWord)
+	        UserAuth.create
+	                .user(user)
+	                .authType(moduleName)
+	                .authKey(nickName)
+	                .authData(salt+";"+md5)
+	                .save
+            (200,Map(),Full(userToXml(user)))
+        }
+      }
+    }
+
+    r
+  } 
+
+  def allTokens(userId: String): LiftResponse = {
+    val r: Box[Tuple3[Int,Map[String,String],Box[Elem]]] = {
+      for{
+        user <- User.find(userId)
+      } yield {                  
+        val tokens: NodeSeq = user.authTokens.map(t => tokenToXml(t)) 
+        (200,Map(),Full(<tokens>{tokens}</tokens>))
+      }
+    }
+
+    r
+  }
+
+  def addToken(userId: String): LiftResponse = {
+    val r: Box[Tuple3[Int,Map[String,String],Box[Elem]]] = {
+      for{
+        user <- User.find(userId)
+      } yield {
+        val token: AuthToken = AuthToken.create
+          .user(user)
+          .description(S.param("description"))
+          .saveMe
+        (200,Map(),Full(tokenToXml(token)))
+      }
+    }
+
+    r
+  }
        
   def allUserMsgs(): LiftResponse = {
     val ret: Box[Tuple3[Int,Map[String,String],Box[Elem]]] =
-      for (user <- calcUser ?~  S.?("base_rest_api_err_param_not_found", "User");
+      for (user <- User.currentUser;
 		val num = S.param("history").map(_.toInt) openOr 40;
         val lst = Mailbox.mostRecentMessagesFor(user.id, num))
       yield (200,Map(),Full(<messages>{lst.flatMap{ case (msg, reason, _) => msgToXml(msg) }}</messages>))
@@ -177,11 +253,11 @@
       future.get(60L * 1000L)
 
     val ret: Box[Tuple3[Int,Map[String,String],Box[Elem]]] = 
-      for (act <- restActor.is ?~ S.?("base_rest_api_err_no_rest_actor");
+      for (act <- messageRestActor.is ?~ S.?("base_rest_api_err_no_rest_actor");
 		   val ignore = act ! ListenFor(future, 0 seconds);
 	       answer <- waitForAnswer ?~ S.?("base_rest_api_err_no_answer")) 
       yield { 
-        if(answer.isEmpty) (204,Map(),Empty)          
+        if(answer.isEmpty) (304,Map(),Empty)          
         else (200,Map(),Full(<messages>{answer.flatMap{ case (msg, reason) => msgToXml(msg) }}</messages>))
       }
 
@@ -198,12 +274,12 @@
       future.get(6L * 60L * 1000L)
 
     val ret: Box[Tuple3[Int,Map[String,String],Box[Elem]]] =  
-      for (act <- restActor.is ?~ "No REST actor";
+      for (act <- messageRestActor.is ?~ "No REST actor";
 		length <- S.param("timeout").map(_.toInt * 1000);
         val ignore = act ! ListenFor(future, TimeSpan(length));
         answer <- waitForAnswer ?~ "Didn't get an answer")
       yield {
-        if(answer.isEmpty) (204,Map(),Empty)          
+        if(answer.isEmpty) (304,Map(),Empty)          
         else (200,Map(),Full(<messages>{answer.flatMap{ case (msg, reason) => msgToXml(msg) }}</messages>))
       }
 
@@ -213,7 +289,7 @@
     r
   }
 
-  def allUserMsgs(tag: String): LiftResponse = {
+  def allTagMsgs(tag: String): LiftResponse = {
     val ret: Box[Tuple3[Int,Map[String,String],Box[Elem]]] =  
       for (user <- User.currentUser;
            tagName <- Box(List(tag));
@@ -229,12 +305,10 @@
     r	
   } 
 
-  def addMsg(): LiftResponse = {
-// Should return the created message
-
+  def addMsg(): LiftResponse = {      
     val ret: Box[Tuple3[Int,Map[String,String],Box[Elem]]] = 
-      for (user <- calcUser.map(_.id.is) ?~ S.?("base_rest_api_err_param_not_found", "User");
-        msg <- S.param("message") ?~ S.?("base_rest_api_err_missing_param", "message"))
+      for (user <- User.currentUser.map(_.id.is);
+           msg <- S.param("message"))
       yield {
         val from: String = S.param("via") openOr "api2"
         val pool = for (poolName <- S.param("pool");
@@ -270,9 +344,9 @@
       for(user <- User.currentUser)
       yield {
         val followees: NodeSeq = 
-          calcUser.map(_.following)
-		                .map(_.map(userToXml(_)))
-		                .openOr(<no_followees/>)
+          User.currentUser.map(_.following)
+		                  .map(_.map(userToXml(_)))
+		                  .openOr(<no_followees/>)
 		(200,Map(),Full(<followees>{followees}</followees>))
 	  }
 
@@ -320,9 +394,9 @@
       for(user <- User.currentUser)
       yield {
         val followees: NodeSeq = 
-          calcUser.map(_.following)
-		                .map(_.map(userToXml(_)))
-		                .openOr(<no_followees/>)
+          User.currentUser.map(_.following)
+		                  .map(_.map(userToXml(_)))
+		                  .openOr(<no_followees/>)
 		(200,Map(),Full(<followees>{followees}</followees>))
 	  }
 
@@ -514,13 +588,17 @@
         if(messages.isEmpty)
           (404,Map(),Empty)
         else
-          (200,Map(),Full(<conversation id={id.toString}>{messages.map(_.toXml)}</conversation>))
+          (200,Map(),Full(<conversation id={id.toString}>{messages.map(msgToXml(_))}</conversation>))
       }
 
 	val r: Box[Tuple3[Int,Map[String,String],Box[Elem]]] =
 	  if(ret.isDefined) ret else Full((403,Map(),Empty))
 
 	r
+  }   
+
+  def unAuthorized(): LiftResponse = {
+    Full((403,Map(),Empty))
   }
 
   private def findAction(actionId: Box[String]): Box[Action] =
@@ -530,23 +608,25 @@
                              By(Action.id, id.toLong),
                              By(Action.removed, false))) yield action
 
-  
-  private def calcUser: Box[User] =
-  	S.param("user").flatMap(User.findFromWeb) or
-  	User.currentUser 
-
   def createTag(in: NodeSeq) = <api>{in}</api>
   
   private def buildActor(userId: Long): RestActor = {
     val ret = new RestActor
     ret ! StartUp(userId)
     ret
-  }
+  }                    
 
-  object restActor extends SessionVar[Box[RestActor]](Empty) {
+  object messageRestActor extends SessionVar[Box[RestActor]](Empty) {
     override def onShutdown(session: LiftSession) = this.is.map(_ ! ByeBye)
   }
   
+//  object tagRestActors extends SessionVar[Map[String,Box[RestActor]]](Map()) {
+//    override def onShutdown(session: LiftSession) = this.is.values.map(_ ! ByeBye)
+
+//    def findOrCreate(tag: String): Box[RestActor] => this.is.getOrElseUpdate(tag, createNew(tag))
+
+//    def createNew(tag: String): (Box[RestActor]) => ("placeholder",buildActor) 
+//  }
 
   class RestActor extends LiftActor {
     private var userId: Long = _
@@ -602,7 +682,18 @@
 }                                                          
 
 // TODO:
-// 2. Fix errors so that they properly indicate a missing parameter or 404
+// 1. Make addMsg() return the created message when successful. 
+// 2. Add a method to get detail for a specific user                                                              
 // 3. Change changeAction so that if the "enabled" parameter doesn't show up it will simply use
 //    the current value for the action, not throw an error.
-// 4. Match based on the return content type header to determine what to return (default to XML) 
+// 4. Match based on the return content type header to determine what to return (default to XML)   
+// 5. Add a method to get detail for a specific track (or messages for the track?)    
+// 6. Add a method to get detail of a specific action
+// 7. Add a method to delete pool
+// 8. Add a method to get the detail for a pool
+// 9. Add a method to get the list of users in a pool
+// 10. Add a method to delete a user from a pool   
+// 11. Add a method to get the messages from a pool
+// 12. Add a method to post a new message to a pool        
+// 13. Add a method to get list of conversations
+// 14. Add a method to post a message to a conversation??                          

Modified: incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/ApiHelper.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/ApiHelper.scala?rev=894955&r1=894954&r2=894955&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/ApiHelper.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/ApiHelper.scala Thu Dec 31 23:47:38 2009
@@ -48,7 +48,7 @@
 
   implicit def rackResponse(in: Box[Tuple3[Int,Map[String,String],Box[Elem]]]): LiftResponse = in match {
     case Full((200,_,xml)) => buildResponse(true, Empty, xml openOr Text(""))
-    case Full((204,_,_)) => NoContentResponse()
+    case Full((304,_,_)) => NoContentResponse()
     case Full((403,_,_)) => ForbiddenResponse()
     case Full((404,_,_)) => NotFoundResponse()
     case _ => InternalServerErrorResponse()   

Modified: incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/XMLHelper.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/XMLHelper.scala?rev=894955&r1=894954&r2=894955&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/XMLHelper.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/org/apache/esme/api/XMLHelper.scala Thu Dec 31 23:47:38 2009
@@ -52,10 +52,25 @@
   <id>{msg.id.toString}</id>
   <date>{toInternetDate(msg.when.is)}</date>
   <source>{msg.source.sourceAttr.getOrElse(Text(""))}</source>
-  <body>{msg.body}</body>
+  <body>{msg.body}</body>  
+  {
+    msg.author.obj.map(u =>
+      <author><nickname>{u.niceName}</nickname><id>{u.id.toString}</id></author>
+    ) openOr Text("")
+  }
   <tags>{msg.tags}</tags>{replyToTag}
 </message>
 
    ret        
+  } 
+
+  protected def tokenToXml(t: AuthToken): Elem = {
+    <token><id>{t.uniqueId}</id><description>{t.description}</description></token>
   }
-}
+}         
+
+// TODO:
+//  1. Add user to the msgToXml() function
+//  2. Add trackToXml()
+//  3. Add actionToXml()
+//  4. Add poolToXml()

Modified: incubator/esme/trunk/server/src/main/scala/org/apache/esme/model/User.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/main/scala/org/apache/esme/model/User.scala?rev=894955&r1=894954&r2=894955&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/main/scala/org/apache/esme/model/User.scala (original)
+++ incubator/esme/trunk/server/src/main/scala/org/apache/esme/model/User.scala Thu Dec 31 23:47:38 2009
@@ -170,7 +170,13 @@
   def currentUserId: Box[String] = curUserId.is
 
   private object curUser extends RequestVar[Box[User]](currentUserId.flatMap(id => getSingleton.find(id)))
+  
+  private object currentRole extends RequestVar[Box[String]](currentUser.flatMap(u => Props.get("role."+u.niceName)))
 
+  def checkRole(role: String): Boolean = {
+    val userRole:String = currentRole.openOr("")
+    userRole.equals(role)
+  }
 
   def currentUser: Box[User] = curUser.is
 }

Modified: incubator/esme/trunk/server/src/test/scala/org/apache/esme/api/API2Test.scala
URL: http://svn.apache.org/viewvc/incubator/esme/trunk/server/src/test/scala/org/apache/esme/api/API2Test.scala?rev=894955&r1=894954&r2=894955&view=diff
==============================================================================
--- incubator/esme/trunk/server/src/test/scala/org/apache/esme/api/API2Test.scala (original)
+++ incubator/esme/trunk/server/src/test/scala/org/apache/esme/api/API2Test.scala Thu Dec 31 23:47:38 2009
@@ -47,17 +47,29 @@
 object Api2Specs extends Specification with TestKit {
   JettyTestServer.start
 
-  val baseUrl = JettyTestServer.urlFor("/api2/")
+  val baseUrl = JettyTestServer.urlFor("/api2/")  
 
-  val session = new LiftSession(Helpers.randomString(20), "", Empty)    
-
-  val theUser = S.initIfUninitted(session) {User.createAndPopulate.nickname("api_test").saveMe}    
+  // Note "api_test" user is special. It has been set up with the integration-admin
+  // role in the test.default.props file.
+                                                                    
+  val theUser = find_or_create_user("api_test")    
   val token = {
     val toke = AuthToken.create.user(theUser).saveMe
     toke.uniqueId.is
   }           
 
-  val post_session = post("session", "token" -> token)
+  val post_session = post("session", "token" -> token) 
+
+  def find_or_create_user(userName: String): User = {
+    val users = User.findByNickname(userName)
+
+    if(users.length > 0)
+      users.head
+    else { 
+      val session = new LiftSession(Helpers.randomString(20), "", Empty)                          
+      S.initIfUninitted(session) {User.createAndPopulate.nickname(userName).saveMe}
+    }
+  }
 
   def sleep(wait: Long): Box[Boolean] = {
     Thread.sleep(wait)
@@ -71,6 +83,7 @@
           session <- post_session
         } {
           (session.xml \ "session" \ "user" \ "id").text must be equalTo (theUser.id.toString)
+          (session.xml \ "session" \ "user" \ "nickname").text must be equalTo (theUser.niceName) 
           session.code must be equalTo 200
         }
       }
@@ -126,7 +139,7 @@
         for{
           session <- post_session
           users <- session.get("users")
-        } {
+        } {                         
           users.code must be equalTo 200
         }
       }
@@ -136,6 +149,123 @@
           session_res.code must be equalTo 403
         }
       }
+    } 
+
+    "/users POST" in {
+      "with valid session" in {
+        for{
+          session <- post_session        
+          added_user <- session.post("users",
+            "nickname" -> "test_user5",
+            "password" -> "test_password")  
+          all_users <- session.get("users") 
+        } {                                                                                                        
+          added_user.code must be equalTo 200
+          (all_users.xml \ "users") must \\(<nickname>test_user5</nickname>)
+        }
+      }
+
+      "with a valid session but no role authorization returns 403 (forbidden)" in {
+		val new_user = find_or_create_user("tester")
+		val new_toke = AuthToken.create.user(new_user).saveMe
+		val new_token = new_toke.uniqueId.is        
+		
+        for{     
+		  sess <- post("session", "token" -> new_token)
+		  added_user <- sess.post("users",
+            "nickname" -> "test_user3",
+            "password" -> "test_password")
+        } {                 
+          added_user.code must be equalTo 403
+        }
+      } 
+
+      "with no session returns 403 (forbidden)" in {
+        for{
+          added_user <- post("users",
+            "nickname" -> "test_user",
+            "password" -> "test_password")
+        } {
+          added_user.code must be equalTo 403
+        }
+      }
+    }
+
+    "/users/USERID/tokens GET" in {
+      val new_user = find_or_create_user("tester")
+	  val new_toke = AuthToken.create.user(new_user).saveMe
+	  val new_token = new_toke.uniqueId.is
+
+      "with valid session" in {
+        for{
+          session <- post_session          
+          tokens <- session.get("users/"+new_user.id+"/tokens") 
+        } {                                                                                                        
+          tokens.code must be equalTo 200
+          tokens.xml must \\(<id>{new_token}</id>)
+        }
+      }
+
+      "with valid session but no role authorization returns 403 (forbidden)" in {
+		val new_user = find_or_create_user("tester")
+		val new_toke = AuthToken.create.user(new_user).saveMe
+		val new_token = new_toke.uniqueId.is        
+		
+        for{     
+		  sess <- post("session", "token" -> new_token)
+		  tokens <- sess.get("users/"+theUser.id+"/tokens")
+        } {                 
+          tokens.code must be equalTo 403
+        }
+      } 
+
+      "with no session returns 403 (forbidden)" in {
+        for{
+          tokens <- get("users/"+theUser.id+"/tokens")
+        } {                     
+          tokens.code must be equalTo 403
+        }
+      }
+    }        
+
+    "/users/USERID/tokens POST" in {
+      val new_user = find_or_create_user("tester")
+
+      "with valid session" in {
+        for{
+          session <- post_session          
+          new_token <- session.post("users/"+new_user.id+"/tokens",
+            "description" -> "test token")
+          tokens <- session.get("users/"+new_user.id+"/tokens")
+        } {                                                                                                        
+          new_token.code must be equalTo 200
+          new_token.xml must \\(<description>test token</description>) 
+          tokens.xml must \\(<description>test token</description>)
+        }
+      }
+
+      "with valid session but no role authorization returns 403 (forbidden)" in {
+		val new_user = find_or_create_user("tester")
+		val new_toke = AuthToken.create.user(new_user).saveMe
+		val new_token = new_toke.uniqueId.is        
+		
+        for{     
+		  sess <- post("session", "token" -> new_token)
+		  new_token <- sess.post("users/"+theUser.id+"/tokens",
+		    "description" -> "test token 2")
+        } {                 
+          new_token.code must be equalTo 403
+        }
+      } 
+
+      "with no session returns 403 (forbidden)" in {
+        for{
+          new_token <- post("users/"+theUser.id+"/tokens",
+		    "description" -> "test token 2")
+        } {                     
+          new_token.code must be equalTo 403
+        }
+      }
     }
 
     "/user/messages GET" in {
@@ -147,6 +277,9 @@
           mess_res <- session.get("user/messages")
         } {    
           mess_res.code must be equalTo 200
+
+          // Message structure   
+          (mess_res.xml \ "messages" \ "message" \ "author" \ "id").text must be equalTo (theUser.id.toString)
         }
       }
 
@@ -315,25 +448,26 @@
       }
     }
 
-    /*
-    *   "/user/tracks/TRACKID DELETE" in {
-    *     "with valid session" in {
-    *       for {
-    *         sess <- post_session
-    *         create_track <- sess.post("user/tracks","track"->".*")
-    *         res <- sess.delete("user/tracks/1")
-    *       } {
-    *         res.code must be equalTo 200
-    *       }
-    *     }
-    *
-    *     "with no session returns 403 (forbidden)" in {
-    *       for(session_res <- delete("user/tracks/1")) {
-    *         session_res.code must be equalTo 403
-    *       }
-    *     }
-    *   }
-    */
+// fragile   
+    "/user/tracks/TRACKID DELETE" in {
+      "with valid session" in {
+        for {
+          sess <- post_session
+          create_track <- sess.post("user/tracks","track"->"hello")
+          res <- sess.delete("user/tracks/2")
+        } {           
+          res.code must be equalTo 200
+        }
+      }
+
+      "with no session returns 403 (forbidden)" in {
+        for {
+          res <- delete("user/tracks/1")
+        } {
+          res.code must be equalTo 403
+        }
+      }
+    }
 
     "/user/actions GET" in {
       "with valid session" in {
@@ -394,25 +528,27 @@
     *   }
     */
 
-    /*
-    *   "/user/actions/ACTIONID DELETE" in {
-    *     "with valid session" in {
-    *       for {
-    *         sess <- post_session
-    *         res <- sess.delete("user/actions/1")
-    *       } {
-    *         res.code must be equalTo 200
-    *       }
-    *     }
-    *
-    *     "with no session returns 403 (forbidden)" in {
-    *       for(res <- delete("user/actions/1")) {
-    *         res.code must be equalTo 403
-    *       }
-    *     }
-    *   }
-    */
-
+// Brittle, brittle, brittle
+    "/user/actions/ACTIONID DELETE" in {
+      "with valid session" in {
+        for {
+          sess <- post_session 
+          create_action <- sess.post("user/actions",
+                                     "name" -> "Test action",
+                                     "test" -> "every 5 mins",
+                                     "action" -> "rss:http://blog.com/feed.rss")
+          res <- sess.delete("user/actions/2")
+        } {                     
+          res.code must be equalTo 200
+        }
+      }
+ 
+      "with no session returns 403 (forbidden)" in {
+        for(res <- delete("user/actions/1")) {
+          res.code must be equalTo 403
+        }
+      }
+    }
 
 // This is very ... shall we say ... brittle
     "conversations/CONVERSATIONID GET" in {
@@ -420,14 +556,12 @@
         for{
           sess <- post_session 
           mess_res <- sess.post("user/messages", "message"->"test")
-          wait <- sleep(1000)
-          messages <- sess.get("user/messages")                    
+          wait <- sleep(1000)                                
           mess_res <- sess.post("user/messages",
                                 "message" -> "test_convo",
-                                "replyto" -> 42)
-          wait2 <- sleep(1000)
-          messages2 <- sess.get("user/messages")
-          res <- sess.get("conversations/42")
+                                "replyto" -> 9)
+          wait2 <- sleep(1000)                 
+          res <- sess.get("conversations/9")
         } {
           res.code must be equalTo 200
         }
@@ -455,7 +589,7 @@
           sess <- post_session
           res <- sess.get("pools")
         } {
-          res.code must be equalTo 200
+          res.code must be equalTo 200      
         }
       }
 
@@ -485,25 +619,31 @@
 
     "/pools/POOLID/users POST" in {
 
-      /*
-      *     "with valid session" in {
-      *       for {
-      *         sess <- post_session
-      *         res <- sess.post("pools/1/users",{"realm"->"test_realm";
-      *                                                "userId"->1;
-      *                                                "permission"->"Write"})
-      *       } {
-      *         res.code must be equalTo 200
-      *       }
-      *     }
-      */
-
-      "with no session returns 403 (forbidden)" in {
-        for (res <- post("pools/1/users", {
-          "realm" -> "test_realm";
-          "userId" -> 2;
-          "permission" -> "Write"
-        })) {
+      "with valid session" in {
+        for {
+          sess <- post_session
+          pool_res <- sess.post("pools", "poolName" -> "test_pool2")
+          res <- sess.post("pools/test_pool2/users","userId"->2)
+        } {
+          res.code must be equalTo 200
+        }
+      }
+
+//      "a pool name that already exists returns 403 (forbidden)" in {
+//        for {
+//          sess <- post_session
+//          pool_res <- sess.post("pools", "poolName" -> "test_pool2")
+//          res <- sess.post("pools", "poolName" -> "test_pool2")
+//        } {
+//          sess.code must be equalTo 403
+//        }
+//      }
+
+      "with no session returns 403 (forbidden)" in {
+        for (res <- post("pools/1/users",
+                         "realm" -> "test_realm",
+                         "userId" -> 2,
+                         "permission" -> "Write")) {
           res.code must be equalTo 403
         }
       }