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?

 

  • 0
  • 0
  • 354
  • 5
  • 2

Answers

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)
}

}

Comments

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
 

Gordon,

Thanks for your reply.. after hours of researching what Robert posted, I could not get it to work either.  I thought I had it solved by instantiating a new object content=##Class(%Library.Stream.Object).%New()... and building that.. then setting into %Request.Content=Content but it doesn't like that either.. so I am at a loss as well.

George

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, 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...