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.
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
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
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 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
Name the new property
SessionId, provide some description and click
Then do the same for
Now we can use
$account/SessionExp in the responses.
How Do We Create a Session?
We have to create a new session every time a client calls
Below is the response:
1 The response is matched when the top XML element is
2 We're selecting an account that has
SessionId equal to a random value of
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
is populated with the value we were comparing it with, i.e.
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
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
to the value from the request (
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.
Now we only need to place all business operations below the
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.