Whether you use React Router, Angular Router or just a Vanilla Router you are going to need to integrate security into it at some point if you want to develop secure UI components.
Normally our security policy in a Single Page Application is hard-wired to our router because we have to create a relationship between browser URL such as /my-protected-route, our backend calls to /my-protected-data-api-call and the components that load.
This sort of cross-cutting relationship in an app leaves most UI engineers in knots when they are trying to test it because there are so many things to think about, but it doesn’t have to, so read on...
When we create a binding between a protected route and a secure component, we will need conditional logic that will handle redirects and transitions. But this conditional logic is often far too coupled to the way the UI handles updates to the browsers window.location object.
For example, consider the following;
This sort of code looks innocent.
But code like this does not grow. Why?
It is consuming too many external dependencies; it’s calling securityInfo.token (which holds our API issued security token) and router.goTo (which will tell the browser to perform a redirect).
These are both different modules - that do different things.
This means if we ever want to test this code, or we want to test components or routes that need this code to run first (since they are authenticated); we have to mock the securityInfo and the router completely; in case they do actually try and perform actions during our test (such as token manipulation or browser redirects).
Forcing this much code to execute in a single test can be difficult, we should really shield our code from calling so many modules like this by collapsing them all down and putting them behind one single ‘interface’.
We call this interface a ‘boundary’...
A boundary is simply a JS/TS file which houses all of the low-level code which you may need to call in order to run a security process (in our case the login process). The boundary will hide the low level details for browser redirecting and security tokens by wrapping them.
In our course ‘Ultra-Fast Testing and TDD’ we teach engineers to use boundaries to hide other things like HTTP, Fetch or even Local Storage on the browser because it makes the code easier to read, and easier to test because the code becomes more 'encapsulated'.
When you decide to hide details and abstract away low lying code like this and force boundaries around parts of your architecture; it allows you to improve your design because it helps you make a decision on where code lives.
A critical aspect of good architecture is having mental models that help you make decisions...
For example, imagine we re-wrote the code from earlier and we forced ourselves to take those annoying titbits of information about routing/browser redirecting and authentication and then put them behind one single object (called routerBoundary) like this…
Now, this code does not have 2 separate modules to understand anymore, just one… the routerBoundary.
By moving complexity to external boundaries like this we get a big improvement in testability because;
a) now we only have one boundary we need to mock not two.
b) there is half the complexity to understand due to half the dependencies.
Thus, having less complexity by using boundaries helps us test complex scenarios like routing and secure components because we are able to abstract away the low lying detail and simply test the security behaviour in 'isolation'!
Added to this; when we isolate it and enable easier mocking of it we run it independely before our secure components, meaning we can set the pre-conditions of our test and mimic real behaviour more consistently!
Security in a UI app is normally built around the various security and routing modules it has included in it. These are often heavily infused with low level frameworks (UI) or integration tools (browser/IO).
Because of this; we can’t test our routing, or even use our routing in a testing context without setting up complex mocking or even having to run large parts of the UI/Routing framework.
As in many other circumstances; we need to be aware of what we are trying to achieve and base our architectural decisions around it to enable easier understanding and therefore testing. We realise in this instance we are not really testing the router or even the security module - we don't need to. We can assume these tools will work properly. What we are testing is that the redirection takes place to the correct component and route.
When we realise this; we abstract away the low level details so we can isolate the actual rules that govern our security process. That way we get control over our authentication module which will mean if we want to test 'secure' parts of app we get not only easier to understand tests, but the ability to run real but isolated set up code more easily during our tests!