Vladimir Dyuzhev, author of MockMotor

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.