Node.js mit TSOA und Authentifizierung - (Teil 10) Middleware-Verify-Test

Zum Abschluss der Artikelserie runden wir die Tests der Express-Middleware ab. Dabei prüfen wir den Verify-Callback in vielen it-each-Blöcken, um eine hohe Testabdeckung zu erreichen. Mit dem Ergebnis haben wir eine Basis für künftige Node.js-Applikationen. Packen wir es an!

6 Minuten

Wir kümmern uns um den zweiten Teil der Unit-Tests für die Express-Middleware. Nachdem die Authorization-Header-Validierung im letzten Teil erfolgreich umgesetzt wurde, kümmern wir uns jetzt um die Behandlung des Verify-Callbacks. Dazu greifen wir auf den vorbereiteten Mock zurück. Bevor wir anfangen, empfiehlt es sich, die Vorgänger der Artikelserie zu lesen:

Prüfung eines Verify-Fehlers

Als Erstes stellen wir die korrekte Behandlung jeglicher Verify-Fehler sicher. Im Produktivcode wird jeder Fehler als UnauthenticatedError quittiert. Entsprechend schlank ist der Test:

describe('Should fail as expected on', () => {
  it('jwtVerify() and reject as UnauthenticatedError', async () => {  
    // arrange  
    const requiredScopes: string[] = []  
    mockJwtVerify.mockImplementation((_token: unknown, _publicKey: unknown, callback: (err: Error | null, decodedToken: JwtPayload) => void) => {  
      callback(new Error('some error'), validToken)  
    })  

    // act + assert  
    await expect(() => expressAuthentication(validHeaderRequest, SECURITY_NAME_JWT, requiredScopes)).rejects.toThrow(UnauthenticatedError)  
    await expect(() => expressAuthentication(validHeaderRequest, SECURITY_NAME_JWT, requiredScopes)).rejects.toThrow('Token verification failed') 
  })
})

In diesem Szenario greifen wir auf den Mock der Verify-Funktion zurück. Der Callback ist der dritte Parameter beim Aufruf. Der Callback selbst hat im Fehlerfall als ersten Parameter den Fehler. Jeglicher Wert, der nicht null ist, bedeutet einen Fehler. In unserem Test simulieren wir den Fehler callback(new Error('some error'), validToken).

Prüfung eines Tokens mit fehlenden Daten

Sobald das NPM-Paket jsonwebtoken den Callback fehlerfrei aufruft – sprich, der erste Parameter ist null –, haben wir als zweiten Parameter das Token. Wir beabsichtigen zuerst abzusichern, dass das Token die Daten enthält, die wir später zurückgeben wollen. Die Definition ist unser Typ JwtData.

Zur Verdeutlichung: Die JSON-Web-Token-Bibliothek bestätigt, dass das Token unverändert ist. Damit könnten wir die Tests schlank halten und viele der folgenden Tests auslassen. Mein Ansatz an so zentraler Stelle wie der Authentifizierungsmiddleware ist eine defensive Programmierweise. D. h., ich prüfe den Parameter decodedToken auf alle Eingangsdatenvarianten. Dafür nutzen wir erneut die it-each-Syntax:

const emptyToken = {
}
it.each([  
  ['string as token', 'some token string', 'Token is not an object'],  
  ['missing userId in token', emptyToken, 'Token missing required fields'],  
  ['missing userId in token', {...validToken, userId: undefined}, 'Token fields with wrong data type'],  
  ['userId as array', {...validToken, userId: []}, 'Token fields with wrong data type'],  
  ['userId as null', {...validToken, userId: null}, 'Token fields with wrong data type'],  
  ['missing email in token', {...validToken, email: undefined}, 'Token fields with wrong data type'],  
  ['email as array', {...validToken, email: []}, 'Token fields with wrong data type'],  
  ['email as null', {...validToken, email: null}, 'Token fields with wrong data type'],  
  ['missing name in token', {...validToken, name: undefined}, 'Token fields with wrong data type'],  
  ['name as array', {...validToken, name: []}, 'Token fields with wrong data type'],  
  ['name as null', {...validToken, name: null}, 'Token fields with wrong data type'],  
  ['missing scopes in token', {...validToken, scopes: undefined}, 'Token fields with wrong data type'],  
  ['scopes as string', {...validToken, scopes: 'bogus'}, 'Token fields with wrong data type'],  
  ['scopes as array with wrong content', {...validToken, scopes: [null]}, 'Token fields with wrong data type'],  
])('%s as UnauthenticatedError', async (_, testToken, expectedErrorMessage) => {  
  // arrange  
  const requiredScopes: string[] = []  
  mockJwtVerify.mockImplementation((_token: unknown, _publicKey: unknown, callback: (err: Error | null, decodedToken: JwtPayload) => void) => {  
    callback(null, testToken as JwtPayload)  
  })  

  // act + assert  
  await expect(() => expressAuthentication(validHeaderRequest, SECURITY_NAME_JWT, requiredScopes)).rejects.toThrow(UnauthenticatedError)  
  await expect(() => expressAuthentication(validHeaderRequest, SECURITY_NAME_JWT, requiredScopes)).rejects.toThrow(expectedErrorMessage)  
})

Prüfung der Scopes

Die Prüfung des Scopes erfordert mehrere Betrachtungsweisen:

  1. Keine Scopes werden verlangt → erfolgreiche Authentifizierung
  2. Scopes werden verlangt und fehlen → wir erwarten einen UnauthorizedError (nicht einen UnauthenticatedError!)
  3. Verlangte und gelieferte Scopes passen → erfolgreiche Authentifizierung

1. Keine Required-Scopes

describe('Should succeed JWT verification on', () => {  
  it.each([  
    ['valid token with no required scopes at all', undefined],  
    ['valid token with required scopes as null', undefined],  
    ['valid token with required scopes as empty array', undefined],  
  ])('%s', async (_, requiredScopes) => {  
    // arrange  
    mockJwtVerify.mockImplementation((_token: unknown, _publicKey: unknown, callback: (err: Error | null, decodedToken: JwtPayload) => void) => {  
      callback(null, validToken)  
    })  

    // act  
    const result = await expressAuthentication(validHeaderRequest, SECURITY_NAME_JWT, requiredScopes)  

    // assert  
    expect(result).toEqual(validToken)  
  }) 
})

2. Missing Scopes

describe('Should fail as expected on', () => {  
  it.each([  
    ['empty token, but required scopes', [], [Scope.ADMIN]],  
    ['token scope not in required scopes', ['bogus'], [Scope.ADMIN]],  
    ['token scope USER not in required scopes ADMIN', [Scope.USER], [Scope.ADMIN]],  
  ])('%s', async (_, tokenScopes, requiredScopes) => {  
    // arrange  
    const currentToken = {...validToken, scopes: tokenScopes}  
    mockJwtVerify.mockImplementation((_token: unknown, _publicKey: unknown, callback: (err: Error | null, decodedToken: JwtPayload) => void) => {  
      callback(null, currentToken)  
    })  

    // act + assert  
    await expect(() => expressAuthentication(validHeaderRequest, SECURITY_NAME_JWT, requiredScopes)).rejects.toThrow(UnauthorizedError)  
    await expect(() => expressAuthentication(validHeaderRequest, SECURITY_NAME_JWT, requiredScopes)).rejects.toThrow('Token missing required scopes')  
  })  
})

3. Matching Scopes

describe('Should succeed JWT verification on', () => {  
  it.each([  
    ['token scopes matches required scopes exactly', [Scope.ADMIN], [Scope.ADMIN]],  
    ['token scopes has any matching required scope', [Scope.ADMIN, Scope.USER], [Scope.USER]],  
    ['single token scope is in list of required scope', [Scope.USER], [Scope.ADMIN, Scope.USER]],  
  ])('%s', async (_, tokenScopes, requiredScopes) => {  
    // arrange  
    const currentToken = {...validToken, scopes: tokenScopes}  
    mockJwtVerify.mockImplementation((_token: unknown, _publicKey: unknown, callback: (err: Error | null, decodedToken: JwtPayload) => void) => {  
      callback(null, currentToken)  
    })  

    // act  
    const result = await expressAuthentication(validHeaderRequest, SECURITY_NAME_JWT, requiredScopes)  

    // assert  
    expect(result).toEqual(currentToken)  
  })  
})

Fazit

Damit ist unsere Express-Middleware vollständig geprüft. Aufgrund der wichtigen Funktion ist unsere Testabdeckung hoch. Damit können wir sicherstellen, diese Funktion in zukünftigen Projekten wiederzuverwenden. Des Weiteren ist dieser Baustein Teil der Schnittstellensicherheit – stets ein triftiger Grund, ihn gründlich zu testen.

Damit endet diese Artikelserie. Wir haben uns von der TSOA-Schnittstelle über die Express-Middleware bis hin zur Testung aller Schritte durchgearbeitet. Ich werde diesen Stand als Basis für künftige Artikel und -serien nutzen.

Bei der Ausführung der Tests hast du sicherlich die vielen Console-Log-Ausgaben gesehen. Mich stören diese enorm. Daher wird der nächste Schritt die Einführung eines dedizierten Loggings sein. Bis dahin viel Erfolg beim Nachbauen der bisherigen Express-Applikation.

call to action background image

Abonniere meinen Newsletter

Erhalte einmal im Monat Nachrichten aus den Bereichen Softwareentwicklung und Kommunikation gespikt mit Buch- und Linkempfehlungen.