Node.js mit TSOA und Authentifizierung - (Teil 9) Middleware-Header-Test
Im heutigen Artikel lernen wir die it-each-Syntax für Vitest kennen, mit der sich wiederkehrender Code in Tests vermeiden lässt. Zusätzlich bereiten wir den Mock für das JSON Web Token NPM-Paket vor. Ein volles Programm – los geht’s!
Nachdem der Endpunkt getestet wurde, bleibt die Prüfung der Express-Middleware. Ohne Gewissheit über die Funktionsfähigkeit ist die Absicherung nicht viel wert. Als ersten Schritt kümmern wir uns um die Fälle, in denen der Authorization-Header fehlt oder unerwarteten Inhalt enthält. Bevor wir anfangen, empfiehlt es sich, die Vorgänger der Artikelserie zu lesen:
- Teil 1: Die Zielsetzung
- Teil 2: Das Basis-Setup
- Teil 3: Ein JWT erzeugen
- Teil 4: Header-Validierung in der AuthMiddleware
- Teil 5: Error-Middleware für standardkonforme Antworten
- Teil 6: Tokenvalidierung
- Teil 7: Die Unit-Test-Vorbereitung
- Teil 8: Der Unit-Test
Vorbereitung des Mocks
Für die Testfälle wollen wir die Funktionalität des NPM-Pakets jsonwebtoken nicht testen, sondern als gegeben und korrekt annehmen. Entsprechend bereiten wir uns einen Mock des NPM-Pakets vor. An diesen Mock haben wir folgende Anforderungen:
- Wir wollen nur den Aufruf der Methode
verifymanipulieren. Der Rest des Moduls soll sich wie das Original verhalten - Damit wir das Verhalten manipulieren können, benötigen wir in den Tests Zugriff auf die Funktion
vi.fn().
// We need the mock available in the tests to define the implementation per test
const mockJwtVerify = vi.fn()
// We need to mock the whole module to be able to override only the verify function
vi.mock(import('jsonwebtoken'), async (importOriginal) => {
// Therefore, we import the original module implementation
const mod = await importOriginal() // type is inferred
return {
...mod,
// And we override only the verify function with our mock
// Because of hoisting, the mockJwtVerify from above is executed with the parameter passed in.
// Otherwise, you receive: Error: [vitest] There was an error when mocking a module.
// See more details here: https://vitest.dev/api/vi.html#vi-mock
verify: (jwtString, publicKey, callback) => mockJwtVerify(jwtString, publicKey, callback),
}
})
Der Code hat folgende Besonderheiten, auf die ich kurz eingehe:
- Aufgrund des Hoisting können wir
mockJwtVerifynicht direkt zuweisen.verify: mockJwtVerifyim Return-Statement erzeugt einen entsprechenden Fehler bei der Ausführung. Die Lösung ist, den Mock zur Laufzeit zurückzugeben – so wie er in dem Moment je Test konfiguriert ist. - Damit beim Aufruf des Mocks der Callback im Mock verfügbar ist, müssen die Parameter überreichen. Für die Einbindung im Test sieht es wie folgt aus:
mockJwtVerify.mockImplementation((_jwtString: unknown, _publicKey: unknown, callback: (err: Error | null, decodedToken: JwtPayload) => void) => {
callback(new Error('some error'), validToken)
})
Die Mock-Implementierung ignoriert die ersten beiden Parameter und ruft die Callback-Funktion gemäß der Testspezifikation mit einem Fehler auf. In anderen Tests geben wir dort null hinein, um eine erfolgreiche Verifizierung zu simulieren.
Unit-Tests für validateHeader
Die Tests teilen wir in zwei Teile auf. Der erste Teil betrifft die Header-Validierung und kümmert sich darum, den Token auszulesen bzw. zu prüfen, wie sich unser Code verhält, wenn das Auslesen nicht wie erwartet klappt. Der zweite Teil nimmt die Verify-Funktion unter die Lupe.
Als erstes legen wir uns die Datei namens /tests/services/AuthMiddleware.spec.ts an. Für die unterschiedlichen Fälle an gelieferten Header, erstellen wir einen passenden Testfall. Um den Code zu reduzieren, bedienen wir uns der it-each-Syntax. Dazu definieren wir ein Array mit Testfällen. Je Array-Eintrag erfolgt eine Liste von Parametern, die wir hineingeben:
- Parameter 1 – z. B.
missing authorization header– ist in meinem Fall der Name des Testfalls, den ich per%sim Testnamen referenziere. - Parameter 2 – z. B.
emptHeader– ist die Definition des Headers des Express-Requests, der bei der Validierung geprüft wird. - Parameter 3 – z. B.
No authorization header provided– ist die erwartete Fehlermeldung.
const emptyHeader = {
}
describe('AuthMiddleware', () => {
describe('Should fail as expected on', () => {
it.each([
['missing authorization header', emptyHeader, 'No authorization header provided'],
['no bearer prefix in header', {authorization: 'bogus'}, 'No Bearer prefix in token provided'],
['no bearer token in header', {authorization: 'Bearer '}, 'No token in Bearer provided'],
])('%s', async (_, headers, expectedErrorMessage) => {
// arrange
const requiredScopes: string[] = []
const mockRequest = {headers} as ExpressRequest
// act + assert -> no chaining in vitest possible, so we call it twice
await expect(() => expressAuthentication(mockRequest, SECURITY_NAME_JWT, requiredScopes)).rejects.toThrow(UnauthenticatedError)
await expect(() => expressAuthentication(mockRequest, SECURITY_NAME_JWT, requiredScopes)).rejects.toThrow(expectedErrorMessage)
})
})
})
Fazit
Damit haben wir zum einen den ersten Schritt, validateHeader, umgesetzt und zum anderen den Mock für den zweiten Schritt vorbereitet. Später werden wir unterschiedliche Varianten desselben Tests prüfen. Dafür eignet sich die it-each-Syntax. Sobald verstanden ist, wie dieser Aufbau funktioniert, ist er einfach zu schreiben und reduziert repetitiven Code enorm. Damit sind auch unsere Testfälle gut gepflegt und aussagekräftig.
Weiter geht es zum Abschluss der Artikelserie in Teil 10: Middleware-Verify-Test.