Middleware for the mock-service-worker
The mock-service-worker can be used to mock backends. You can use it in tests, but also in demos. Unlike real http-server frameworks like express.js, the mock-service-worker does not have that notion have "middleware", and I struggled a bit my mock-authentication for the Gachou demo implemented... Here is a solution, and may be feature-suggestion for the Mock Service Worker team.
Motivation
It may sound a bit crazy, but I have used the mock-service-worker library to run a mock-backend for my Gachou demo, which is hosted completely on GitLab Pages. You can not do a lot there, just log in and add users. Everything is stored locally in your browser, but the requests are handled by mock-service-worker so that it even looks real in the dev-tool’s network-tab.
I am using the same mock-endpoints in my test-cases and in the demo. In the demo, the endpoints are “secured” (i.e. check the access token), but in the unit tests, I didn’t want to bother with access tokens and login.
The goal
In express.js
you would register a middleware that performs the
authentication, but mock-service-worker does not support middleware. This is
understandable as it is not a full-featured rest-framework. But I thought it
should be possible to write a function “allowedRoles” that creates a wrapper for
a list of RestHandler
instances. Then I could use it like this:
function allowedRoles(
roles: string[],
restHandlers: RestHandler[],
): RestHandler[] {
// for each restHandler, create a wrapped one that calls its "ResponseResolver"
// after checking the access-token
}
function createUsersHandlers(): RestHander[] {
return [
rest.get("/api/users", (req, res, context) => {
// compute response
}),
rest.post("/api/users", (req, res, context) => {
// store new user
}),
rest.get("/api/users/:username", (req, res, context) => {
// compute response
}),
rest.put("/api/users/:username", (req, res, context) => {
// modify user
}),
rest.delete("/api/users/:username", (req, res, context) => {
// delete user
}),
];
}
// In the demo
setupWorker(...allowedRoles(["ADMIN"], createUsersHandlers()));
// In tests
setupServer(...createusersHandlers());
Unfortunately, in the current version (0.44.2) the RestHandler-class has no way to access the ResponseResolver directly. The class is designed to be created and used directly, not as a “configuration” object that can be wrapped and used in delegates.
I played around with the idea a little, but came to the conclusion that I had to use to many internal and undocumented APIs to achieve my goal.
The solution
Instead, my solution was to not create RestHandler
instances directly, but
first create a configuration object.
export interface RestHandlerConfig<
Resolver = ResponseResolver<
RestRequest,
RestContext,
Record<string, unknown>
>,
> {
path: string;
method: "PUT" | "POST" | "PATCH" | "DELETE" | "GET";
resolver: Resolver;
}
I didn’t use the rest.get
method directly, but created another object with a
custom get
, post
, put
, delete
and patch
method, which created a
RestHandlerConfig
object instead of a RestHandler
. Then I create my own
setupServer
function:
function createHandler(config: RestHandlerConfig): RestHandler {
return new RestHandler(config.method, config.path, config.resolver);
}
export function mySetupServer(...configs: RestHandlerConfig[]): SetupServerApi {
return setupServer(...handlerConfigs.map(createHandler));
}
Finally, I was able to write a wrapper function for my RestHandlerConfig
export function allowedRoles(
allowedRoles: Role[],
handlerConfigs: RestHandlerConfig[],
): RestHandlerConfig[] {
return handlerConfigs.map(({ method, path, resolver }) => {
return {
method,
path,
resolver: (req, res, context) => {
const authorizationHeader = req.headers.get("authorization");
if (authorizationHeader == null) {
return res(context.status(401));
}
const roles = getRolesFromAuthToken(authorizationHeader);
if (!hasAnyRole(roles, allowedRoles)) {
return res(context.status(403));
}
return resolver(req, res, context);
},
};
});
}
Where is the code?
The code is inside my Gachou project. I currently don’t want to invest the time
to publish an npm
-package for this use-case. Contact me, if you are planning
to do something like that.
My source code implements more than this feature. It also provides the code generated from an OpenAPI-spec, as I wrote in my last post.
Conclusion
My use-case certainly is somewhat strange. Nevertheless, my main conclusion is that my workaround should be part of the mock-service-worker in the first place. I think it would be a good design, more concise and more flexible to separate configuration interfaces from the actual request-handling.
I may open a feature request for this in the future.