Question
· Feb 16, 2018

Using UnitTest for a RestService

I am trying to use the automated unittest class for a %CSP.Rest service.

So my Unittest code set's %request and %response from %CSP.Req/resp respectively

I build a tProxy with the fields I need for the post and set it %ToJson... I have tried seting %request.Content = tProxy (and not)

I call the method for the post url directly... 

  When that method calls %fromJson to set an object with the info. I passed... 

      - when I don't set %request.content.. I get a error '5035 - premature end of data code 12'

      - when I set %request.content=tProxy.. I get an errror Method Does not exist 

Any tips on what may be going on?

Discussion (7)2
Log in or sign up to continue

George, Gordon
Interesting info on Upgrade2016.1  This seems to by some fake news.
Since the class is there om 2017.2 and even in IRIS2018.1
http://docs.intersystems.com/iris20181/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25CSP.Stream

The Type set fails because %CSP.Stream is just an abstract class. 
Like %CSP.Page you have to create the real object yourself as %CSP.CharacterStream or %CSP.BinaryStream
 

a little bit simplified for retyping in terminal:

set req = ##class(%CSP.Request).%New()
set cont=req.Content zw cont   ; MO AUTOMATIC OBJECT BEAUSE ABSTRACT !!
cont=""
set cont=##class(%CSP.CharacterStream).%New()  zw cont  ; NOW WE HAVE AN OBJECT
cont=<OBJECT REFERENCE>[5@%CSP.CharacterStream]
+----------------- general information ---------------
|      oref value: 5
|      class name: %CSP.CharacterStream
| reference count: 2
+----------------- attribute values ------------------
|     (%Concurrency) = 1
|    (%LastModified) = ""
|          %Location = ""  <Get,Set>
|         (%LockRef) = ""
|          (%Locked) = 0
|              AtEnd = 0
|           (Buffer) = ""
|           (IOSize) = 0
|                 Id = ""
|     LineTerminator = $c(13,10)  <Set>
|        (MaxNodeNo) = 0
|             (Mode) = 0
|           (NodeNo) = 0
|         (Position) = 1
|        (StoreNode) = ""
|        (StoreRoot) = "^CacheStream"
|         (TempNode) = ""
+--------------- calculated references ---------------
|            CharSet   <Get,Set>
|        ContentType   <Get,Set>
|            Expires   <Get,Set>
|           FileName   <Get,Set>
|            Headers   <Get,Set>
|       LastModified   <Get>
|        MimeSection   <Get,Set>
|               Size   <Get>
|        (StoreGlvn)   <Get>
|         (TempGlvn)   <Get>
+-----------------------------------------------------
set cont.ContentType=
"application/json"

; and so on ....

HTH

Hi George,

I'm most interested in your Question on Unit Testing a REST service.

The approach I've taken seems similar to yours. Unfortunately I've not yet cracked a working Unit Test implementation for this scenario.

Equally I had already studied the documentation that 'Robert Cemper' suggested in his answer links. Also I note in the following link:
 http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=...

that a bunch of the %CSP.Stream methods are being deprecated from 2016.1 on. I have been unable to locate their replacement or alternative approach.

If you, Robert or anyone in the community can supply a working code sample of how to set the parameters of a %CSP.Request object would be much appreciated. 

The following code compiles but fails on execution.

    #dim status As %Status

    #dim cspRequest As %CSP.Request = ##class(%CSP.Request).%New()

    set status = cspRequest.Content.ContentTypeSet("application/json") => Fails => OREF Failure => I assume to do with %CSP.Stream


My aim with the Unit Test is to somehow have the class that Extends %CSP.REST be instansiated or ingest the cspRequest As %CSP.Request. This would allow for directly debugging the ClassMethods of the %CSP.REST class.

Much appreciate your assistance.

Cheers,

Gordon
 

Hi George / Gordon,

Feels like too many potential trip hazards getting this right, and the possibility of creating false positives.

I would recommend using an HTTP client instead.

Here is a quick and dirty example, it uses %Net.HttpRequest to make the REST calls. Take a look at the documentation here http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls as there are many other settings (e.g. credentials) that you might also want to set.

The example has both a GET and a POST...

Class Foo.Rest1 Extends %CSP.REST
{

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap]
{
    <Routes>
    <Route Url="/book" Method="POST" Call="PostBook" />
    <Route Url="/book/:ibn" Method="GET" Call="GetBook" />
    </Routes>
}

ClassMethod PostBook() As %Status
{
    set obj=##class(%DynamicObject).%FromJSON(%request.Content)
    set ^Foo.Books(obj.ibn,"name")=obj.name
    set ^Foo.Books(obj.ibn,"author")=obj.author
    quit 1
}

ClassMethod GetBook(pIBN) As %Status
{
    if '$Data(^Foo.Books(pIBN)) set %response.Status = 404 return 1
    set book = {
        "ibn" (pIBN),
        "name" (^Foo.Books(pIBN,"name")),
        "author" (^Foo.Books(pIBN,"author"))
    }
    write book.%ToJSON()
    return 1
}

ClassMethod GetNewRestHelper() As %Net.HttpRequest
{
    set httpRequest=##class(%Net.HttpRequest).%New()
    set httpRequest.Server="localhost"
    set httpRequest.Port=57772
    set httpRequest.Timeout=2
    quit httpRequest
}

ClassMethod Tests() As %Boolean
{
    //basic assert, replace with actual unit tester implementation
    #define assert(%a,%b) write !,"Test ",$increment(count)," ",$select(%a=%b:"PASSED",1:"FAILED, expected """_%b_""" recieved"""_%a_"""")

    //TEST 1 : should save ok
    set rest=..GetNewRestHelper()
    set book = {
        "ibn" 1234,
        "name" "Lord Of The Rings",
        "author" "J.R.R. Tolkien"
    }
    set json = book.%ToJSON()
    do rest.EntityBody.Write(json)
    do rest.Post("/foo/book")
    $$$assert(200,rest.HttpResponse.StatusCode)

    //TEST 2 : should find book 1234
    set rest=..GetNewRestHelper()
    do rest.Get("/foo/book/1234")
    $$$assert(200,rest.HttpResponse.StatusCode)
    set json=rest.HttpResponse.Data.Read(32000)
    set book=##class(%DynamicObject).%FromJSON(json)
    $$$assert(book.ibn,1234)
    $$$assert(book.name,"Lord Of The Rings")
    $$$assert(book.author,"J.R.R. Tolkien")

    //TEST 3 : should NOT find book 5678, test should fail
    set rest=..GetNewRestHelper()
    do rest.Get("/foo/book/5678")
    $$$assert(200,rest.HttpResponse.StatusCode)
}

}

Hi George, Robert, Sean,

Thanks all for responding so promptly.

All comments have combined into what I needed...

Cheers,

Gordon

Code follows:

Class RestApi.Ris Extends %CSP.REST
{

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap]
{
<Routes>

<Route Url="/(.*)" Method="GET" Call="RestApi.Ris:ProcessUrl"/>
<Route Url="/(.*)" Method="POST" Call="RestApi.Ris:ProcessUrl"/>
<Route Url="/(.*)" Method="PUT" Call="RestApi.Ris:ProcessUrl"/>
<Route Url="/(.*)" Method="DELETE" Call="RestApi.Ris:ProcessUrl"/>

</Routes>
}

ClassMethod ProcessUrl(capturedUrlData As %String = "", debugCspRequest As %CSP.Request = "") As %Status
{
#dim %request As %CSP.Request
#dim %response As %CSP.Response

if ((debugCspRequest '= "") && ($IsObject(debugCspRequest)) && ($ClassName(debugCspRequest) = "%CSP.Request"))
{
set %request = debugCspRequest
set %response = ##class(%CSP.Response).%New()
}

Method Implementation continues...

 

Class UnitTests.RestApi.Ris.RisUnitTests Extends Atomic.UnitTest.TestHelper
{

/*

Method OnBeforeOneTest() As %Status
{
/// Code to run Before Test/s
///
/// Quit $$$OK
}

*/
ClassMethod MockCspRequest(requestMethod As %String = "", requestUri As %String = "", jsonPayload As %String = "", authorizationToken As %String = "") As %Net.HttpRequest
{
#dim status As %Status
#dim cspRequest As %CSP.Request = ##class(%CSP.Request).%New()
set cspRequest.CharSet = "UTF-8"

if (('$Data(requestMethod)) || ($IsObject(requestMethod)) || (requestMethod = "")) return ""
if (('$Data(requestUri)) || ($IsObject(requestUri)) || (requestUri = "")) return ""

if (('$Data(jsonPayload)) || ($IsObject(jsonPayload)) || ($zStrip(jsonPayload, "<>", " ") = "")) set jsonPayload = ""

if (('$Data(authorizationToken)) || ($IsObject(authorizationToken)) || ($zStrip(authorizationToken, "<>", " ") = "")) set authorizationToken = ""
if ((authorizationToken '= "") && ('##class(RestApi.Helpers.String).IsValidGuid(authorizationToken))) return ""

#dim patternRequestMethod as %String = "\A(?i)Get|Post|Put|Delete\z"

#dim regex as %Regex.Matcher = ##class(%Regex.Matcher).%New(patternRequestMethod, requestMethod)
#dim isMatch as %Boolean = regex.Locate()

if ('isMatch) return ""

// Request Method
set requestMethod = $zConvert(requestMethod, "U")
set cspRequest.CgiEnvs("REQUEST_METHOD") = requestMethod
set cspRequest.Method = requestMethod


// URI Endpoint validation
#dim patternEndpoint as %String = "(?i)\A(https?|ftp)://([-A-Z0-9.]+)(?::(\d{1,5}))?(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:,.;]*)?\z"
// Standard Naming (G1 Protocol) (G2 Domain ) (G3 Port) (G4 File ) (G5 QueryParameters )
// Caché Doco Naming (G1 Protocol) (G2 Server ) (G3 Port) (G4 Location ) (G5 QueryParameters )


set regex = ##class(%Regex.Matcher).%New(patternEndpoint, requestUri)
set isMatch = regex.Locate()

if ('isMatch) return ""

#dim protocol as %String = $zConvert(regex.GroupGet(1),"L")
#dim server as %String = regex.GroupGet(2)
#dim port as %String = regex.GroupGet(3)
#dim location as %String = regex.GroupGet(4)
#dim queryParams as %String = regex.GroupGet(5)

if (protocol = "ftp") return ""
if (port > 65535) return ""


set cspRequest.CgiEnvs("REQUEST_SCHEME") = protocol

set cspRequest.CgiEnvs("REQUEST_URI") = requestUri
set cspRequest.URL = location

if (authorizationToken '= "")
{
set cspRequest.CgiEnvs("HTTP_AUTHORIZATION") = authorizationToken
}

if ((jsonPayload '= "") && (requestMethod '= "GET"))
{
#dim cspStream As %CSP.Stream = ##class(%CSP.CharacterStream).%New()

set cspStream.CharSet = "utf-8"
set cspStream.ContentType = "application/json"
set status = cspStream.MimeSectionSet(jsonPayload)

set cspRequest.Content = cspStream
}

set queryParams = $zStrip(queryParams, "<>", " ")
if (queryParams = "") return cspRequest


#dim kvpData as %String = queryParams 

#dim kvpElement as %String 
#dim key as %String 
#dim value as %String 

#dim patternKvpElement as %String = "(?i)(\??(?:[a-z0-9]*?=[a-z0-9]*?)(?:&|\z))"
#dim patternKvp as %String = "(?i)(?:\??([a-z0-9]*?)=([a-z0-9]*?)(?:&|\z))"

set regex = ##class(%Regex.Matcher).%New(patternKvpElement, kvpData)
#dim isMatchKvpElement As %Boolean = regex.Locate()

while (isMatchKvpElement)
{
set kvpElement = regex.GroupGet(1)

set regex = ##class(%Regex.Matcher).%New(patternKvp, kvpElement)
set isMatch = regex.Locate()

if (isMatch)
{
set key = regex.GroupGet(1)
set value = regex.GroupGet(2)

Set cspRequest.Data("Name",$Order(cspRequest.Data(key, ""), -1) +1) = value 
}

// Get Next KvpElement
set kvpData = $Extract(kvpData, ($Length(kvpElement) + 1), *-0)

if (kvpData '= "")
{
set regex = ##class(%Regex.Matcher).%New(patternKvpElement, kvpData)
set isMatchKvpElement = regex.Locate()
}
else
{
set isMatchKvpElement = 0
}
}

return cspRequest
}

Method TestProcessUrl()
{
// -------------------------------------------------------------------------------------------------------------------
// Arrange

#dim resultActual As %Status 
#dim resultExpected As %Status

#dim status As %Status

#dim queryString As %String = "?qParam01=123456&qParam02=ABCDEF"
#dim mockUrl As %String = "http://1.2.3.4:80/SomeLocation"_queryString


#dim dq As %String = $Char(34)
#dim message As %String = "Unit Test REST API method without Web Server and complete line by line debug"

#dim jsonPayload As %String = "{"
set jsonPayload = jsonPayload_dq_"source"_dq_":"_dq_"Gordo"_dq _","
set jsonPayload = jsonPayload_dq_"urlMethod"_dq_":"_dq_"UnitTest"_dq _","
set jsonPayload = jsonPayload_dq_"text"_dq_":"_dq_message_dq
set jsonPayload = jsonPayload_"}"


#dim cspRequest As %CSP.Request = ..MockCspRequest("Post", mockUrl, jsonPayload)
  Do $$$AssertTrue($IsObject(cspRequest), "cspRequest instance created")

#dim cspUrl As %String = cspRequest.URL
#dim cspMethod As %String = cspRequest.Method

#dim cspContentType As %String = cspRequest.ContentType



#dim patternRouteCaptue as %String = "/(.*)" // <Route Url="/(.*)"
#dim regex as %Regex.Matcher = ##class(%Regex.Matcher).%New(patternRouteCaptue, cspUrl)
#dim isMatch as %Boolean = regex.Locate()
  Do $$$AssertTrue(isMatch, "Route has capturedUrlData")
 
  #dim capturedUrlData As %String = regex.GroupGet(1)


// -------------------------------------------------------------------------------------------------------------------
// Act / Assert

set resultActual = ##class(RestApi.Ris).ProcessUrl(capturedUrlData, cspRequest)

Method Implementation continues...