Lqueryvg
Lqueryvg

Reputation: 200

tRPC router introspection for testing

Background

I have an API where I need to authenticate the caller after input validation.

This is because for some endpoints the authorisation depends on information provided in input parameters (e.g. which server side resource the caller wants to query or mutate, and whether the caller has permissions on that specific resource).

I've written some middleware which inserted after the .input() call in the procedure call chain. (See the .use(auth([authMethod1, authMethod2])) in example router below.)

Note, my auth() middleware method takes a list of functions so I can customise which types of auth are required for each endpoint. Some types of auth may depend on input params, others may not. Also, some endpoints need bearer token authorisation, others need API keys. This approach gives me the flexibility I need.

The problem with this approach is that if a developer creates a new endpoint and forgets to add the auth middleware, the endpoint will be wide open.

I'd therefore like to write a unit test which guards against this.

Question

How can I enumerate my router from within a unit test and check that my auth middleware is attached to each endpoint ?

The documentation is scant in this regard and only gives examples of auth middleware done at the top (procedure) level. As I've explained above, applying the same type of authorisation to all my endpoints does not work for my use case.

Example router:

export const accountsRouter = router({
  getOne: apiProcedure
    .input(inputSchema...)
    .use(auth([authMethod1, authMethod2]))
    .mutation(() => {
       // query logic
    }),
  getTwo: apiProcedure
    .use(auth([authMethod1, authMethod3]))
    .output(outputSchema...)
    .query(() => {
       // query logic
    }),

Upvotes: 0

Views: 756

Answers (1)

Brandon Buck
Brandon Buck

Reputation: 7181

How can I enumerate my router from within a unit test and check that my auth middleware is attached to each endpoint ?

If I'm reading this correctly and you want to inspect the procedures, see what auth methods are defined to use and then dynamically iterate and test those cases - Don't. It may not be obvious but this actually defeats the purpose of testing because the goal of well written tests is that if the units under test change (in this case, changing or removing an authentication method) you want the related tests to start failing. This is because the tests are defined based on your expectations of how the code changes and when the expectations change the tests need to evolve as well. If an authentication method is removed/replaced the tests need to be updated in addition to the code change to represent the evolved expectations.

That's not to say you can't try and clean up tests with repeated logic. Find a good way to abstract your methods for testing specific authentication methods and make them generic around which procedure is called and procedure inputs. Then you can easily set up test cases for all the scenarios without significant duplication (as an example):

describe('router', () => {
  // define your caller somewhere for tests to access
  describe('getOne', () => {
    it('allows authentication with method 1', () => {
      expectProcedureToAuthenticateWithMethod1(caller.getOne, procedureInput);
    });

    it('allows authentication with method 2', () => {
      expectProcedureToAuthenticateWithMethod2(caller.getOne, procedureInput);
    });
  });

  describe('getTwo', () => {
    it('allows authentication with method 1', () => {
      expectProcedureToAuthenticateWithMethod1(caller.getTwo, procedureInput);
    });

    it('allows authentication with method 3', () => {
      expectProcedureToAuthenticateWithMethod3(caller.getTwo, procedureInput);
    });
  });
});

Remember, the goal of tests is to ensure the expectations of code. It may be convenient for tests to "evolve" automatically from code changes but that defeats the purpose of having tests (and is also a problem with mocks but that's another issue altogether). You want tests to fail when code changes, you want to have to maintain your tests as you make changes to the applications expectations. That's what tests do, they help you target and fix your application when you make changes and potentially cause something else to break you didn't even expect to be dependent.

Upvotes: 0

Related Questions