Vladimir Dyuzhev
MockMotor Creator
How to Mock a Session-Based Service
Not all services are stateless. But MockMotor can mock the stateful ones just as well.
The classic SOA guidelines tell us that services should be stateless.
That improves the overall system scalability because the services don’t have to keep the state for each of the consumers. In other words, if we don’t have to store consumers' state, we can serve millions of them - as long as they do not hit us all at the same time.
Not All Services are Stateless
Some services, however, are stateful - they keep consumer’s session information in memory or on disk. The session id must be sent with each request so the server could find that session.
A client of such a service needs to obtain the session id first, typically by calling some variant of login operation and providing their credentials.
Once the session is obtained, the client can call other business operations, sending the session id with each request.
Eventually, however, the token expires. The client then gets HTTP 401 instead of the business response and needs to obtain another session id to continue the interaction.
Transparent Re-Login
The session-based authorization is different from other authorization ways. The consumer typically has to implement a transparent re-login in code.
This is because nobody wants to show errors in UI at regular intervals just because some session has expired. Instead, it is expected that the client obtains a new session and retries the same request transparently for the upstream code.
(What the client is doing is creating a virtual stateless service on top of real stateful one. Because the stateless services are so much easier to work with!)
Problems Testing the Transparent Re-Login
As with any tricky functionality, we need to test the transparent re-login logic.
However, testing it against a live service is hard. The live session can be quite long, tens of minutes. That makes the test cycle duration extended beyond reasonable.
It is also impossible to test special cases, such as “what if the re-login failed and gave HTTP 401 again?” or “what if the login call is slow?”.
Mocking Session Service
As usual, these (and other) test scenarios are easily implemented with the help of a mock service. We can configure the session time to be as short as we want. We can configure special responses for particular scenarios, and we can specify any call delays we wish for those scenarios.
Let’s check how we can implement a session-based service.
Sum: a Service We’re Going to Mock
I’ve seen a few live examples of session-based services, but they are all quite complicated. I want to have something simple for this post, so I made up a service that contains all the necessary parts but nothing more.
The service performs a simple math Sum
operation on all Val
values passed in the request. However, each request must have a valid (non-expired) session id
provided. If the session id is missing or the session is expired, the request is rejected with HTTP 401 status.
A request looks like below. You see that it has SessionId
value in the payload:
POST http://127.0.0.1:7080/Examples/SessionBased HTTP/1.1
Content-Type: application/xml
Content-Length: 116
Host: 127.0.0.1:7080
<Add>
<Val>5</Val>
<Val>2</Val>
<Val>3</Val>
<SessionId>2f74ecbc-f844-4530-aa07-84713f711d31</SessionId>
</Add>
If the session is still valid, the response has the Sum
value:
<Sum>10</Sum>
However, if the session id is invalid or the session has expired, the response is HTTP 401 with the error in the payload:
HTTP/1.1 401 Unauthorized
Content-Type: text/xml; charset=UTF-8
<Rejected>
<Expired>1549504803580</Expired>
<Now>1549504805187</Now>
</Rejected>
The value in Expired
field is the number of milliseconds since Jan 1st, 1970 (so-called epoch).
The client then supposed to call a GetSession
operation to obtain the new session id:
POST http://127.0.0.1:7080/Examples/SessionBased HTTP/1.1
Content-Type: application/xml
Content-Length: 85
Host: 127.0.0.1:7080
<GetSession>
<UserName>johndoe</UserName>
<Password>qwerty</Password>
</GetSession>
If the credentials are correct (which I’m not going to check in this mock), the response contains the new SessionId
:
HTTP/1.1 200 OK
Content-Type: text/xml; charset=UTF-8
<Granted>
<SessionId>2f74ecbc-f844-4530-aa07-84713f711d31</SessionId>
<Expires>1549504809393</Expires>
</Granted>
How Do We Store a Session?
A live service has memory, disk and database to store the session information. Where can MockMotor keep it?
The only storage available to MockMotor services is mock accounts. The good news is that they are sufficient for most of the stateful operations.
We’re going to keep every session in its own mock account. The session (and the mock account) has two properties: SessionId
and SessionExp
. SessionId
stores the
session id (the value the client has to provide with each request), and SessionExp
contains the timestamp (the number of milliseconds since epoch) when the session expires.
Creating Account Properties
Let’s create the session account properties.
Navigate to environment accounts, then to properties, and click Add Property
:
Name the new property SessionId
, provide some description and click Save
:
Then do the same for SessionExp
property:
Now we can use $account/SessionId
and $account/SessionExp
in the responses.
How Do We Create a Session?
We have to create a new session every time a client calls GetSession
operation.
Below is the response:
1 The response is matched when the top XML element is GetSession
.
2 We’re selecting an account that has SessionId
equal to a random value of $mockmeta.randomUUID
.
That account does not exist (because the $mockmeta.randomUUID
is random and was never used before).
3 Normally the missing account causes the response to fail, but we also tick the button Create New if not Found
, so the account is instead created, and its SessionId
property
is populated with the value we were comparing it with, i.e. $mockmeta/randomUUID
.
So after step 3, we have created our session storage and assigned its SessionId
property a random session id.
Now let’s complete the response payload:
4 We return the SessionId
value to the client. We could have used $account/SessionId/text()
here, as well - it is the same value.
5 We calculate the expiration timestamp using the built-in value $mockmeta/epochMs
(time since epoch in milliseconds) and add 4s (4000ms) to its value.
You can calculate the same timestamp as `(fn:current-dateTime() - xs:dateTime("1970-01-01T00:00:00-00:00")) div xdt:dayTimeDuration("PT0.001S")`. Yes, XQuery is not very terse when working with dates - hence the `$mockmeta/epochMs` helper value.
6 Finally, we save the calculated expiration timestamp into the mock account under the SessionExp
name. We use the $output
variable that contains the response payload we’ve just generated.
When this response is complete, we have a new mock account containing the new session id and the expiration timestamp 4s in the future. The client can read the session id from the response payload.
How Do We Validate the Session?
Great, now the client has received the session id and begins to call the business method Sum
.
However, we need to implement the session validation, i.e. on every request, we need to check if the session is still valid, and return HTTP 401 if it is not.
Let’s review the Check Session
response that does exactly that. It is executed if the session (mock account) is not found, or the session SessionExp
value is in the past.
1 When MockMotor checks if the response matches the request, it notices that the response uses account values in the matching script. It then tries first to find an account having SessionId
equal
to the value from the request ($input//*:SessionId/text()
).
2 If such an account is not found, this error is ignored (because we specified Ignore & Continue if not found
). The not($account/SessionId)
part of the matching script is then true
, and the response
matches. When executed, it provides HTTP 401 status to the client.
3 If the account is found, the second part of the match script, xs:integer($mockmeta/*:epochMs) gt xs:integer($account/SessionExp)
, compares the current time since epoch with the expiration
time in the account (i.e. in the session). If the current time is bigger, the session has expired, the expression results in true
, the account matches and also returns HTTP 401 to the client.
The `Check Session` response expects to find the `SessionId` element in the request. The `GetSession` response doesn't have one (it is executed without a valid session). To avoid validating the session for `GetSession`, the `GetSession` should be placed in the very first place in the list of responses, and `Check Session` in the second place.
Business Operations
Now we only need to place all business operations below the GetSession
and Check Session
responses. If the Check Session
doesn’t match the request, that means that the session is still valid, and
we can execute any business responses without paying attention to the session value.
See the Whole Example
You can see the complete example of this service in Example Mock Services
mock environment, which is available with every new MockMotor installation and also online on the demo site.