13. Apr 2020
Lesedauer 13 Min.
Container optimieren
.NET-Core-Microservices entwickeln, Teil 2
Entwickeln, Testen und Debuggen von .NET-Core-Microservices in Docker-Containern.

Der erste Teil dieser Serie [1] hat die Grundlagen von Docker vorgestellt, insbesondere das Anbinden von Laufwerken (Volumes) und die Arbeit mit Docker Compose. Danach wurde mit .NET Core 3.1 ein einfacher Microservice und dazu ein Projekt für Unit-Tests erstellt. Schließlich kamen Docker und Docker Compose zum Einsatz, um den Code des Microservices inklusive Unit-Tests in einem Container laufen zu lassen. Der Container war so konfiguriert, dass er die Unit-Tests jedes Mal erneut ausführt, wenn eine Code-Änderung im Service oder in den Unit-Tests entdeckt wird.In diesem zweiten Teil soll es darum gehen, wie man xUnit, Node.js und Jasmine dazu verwenden kann, sogenannte Outside-in- oder Blackbox-Tests für einen Microservice zu schreiben. Zudem lesen Sie, wie Sie das API des Microservices mittels JWT-Token sichern und wie sich das gesicherte API erfolgreich testen lässt.
Outside-in-Tests
Der zurückliegende erste Teil zeigte, wie Docker-Container dazu beitragen können, die testgetriebene Entwicklung (TDD) so reibungslos wie möglich zu gestalten. Dabei wurden der zu testende Code und die Tests in demselben Container ausgeführt. Jetzt erfahren Sie, wie Sie Outside-in-Tests in einem Container ausführen. Diese Tests werden manchmal auch Blackbox-Tests genannt, da man die zu testende Komponente wie eine Blackbox behandelt. Man schreibt also die Tests so, dass diese nur auf die öffentlichen Schnittstellen der Komponente zugreifen können. Um dieses Prinzip besonders deutlich zu machen, werden die zu testende Komponente und der Test-Code in je einem separaten Container laufen.Den Anfang macht ein mit xUnit in C# geschriebener Test. Haben Sie den ersten Teil der Serie [1] nachvollzogen, dann sollte auf Ihrem Computer der Beispielordner ~/dotnetpro/registration bereits vorhanden sein. Öffnen Sie ein Terminal-Fenster und navigieren Sie in diesen Ordner:
<span class="hljs-variable">$ </span>cd ~<span class="hljs-regexp">/dotnetpro/registration</span>
Dort erstellen Sie den neuen Unterordner OutsideInTests-Node und navigieren zu diesem:
$ mkdir OutsideInTests-<span class="hljs-keyword">Node</span> <span class="hljs-title">&& cd</span> OutsideInTests-<span class="hljs-keyword">Node</span>
Mit npm init initialisieren Sie diesen Ordner als Node-Projekt. Alle vorgeschlagenen Standardwerte werden akzeptiert. Danach installieren und initialisieren Sie Jasmine für dieses Projekt mit folgenden beiden Befehlen:
<span class="hljs-variable">$ </span>npm install jasmine --save
<span class="hljs-variable">$ </span>node_modules/jasmine/bin/jasmine.js init
Der Befehl zum Initialisieren fügt eine Datei namens spec/support/jasmine.json zum Projekt hinzu. Weiter geht’s mit dem Installieren der Bibliothek axios, deren HTTP-Client für die Tests erforderlich ist:
$ npm <span class="hljs-keyword">install</span> axios <span class="hljs-comment">--save </span>
Schließlich wird noch die Bibliothek uuid gebraucht, die ebenfalls mithilfe des Node-Paketmanagers npm hinzugefügt wird:
$ npm <span class="hljs-keyword">install</span> <span class="hljs-keyword">uuid</span> <span class="hljs-comment">--save </span>
Öffnen Sie nun die Datei package.json und ändern Sie den Inhalt des mit scripts überschriebenen Blocks wie in Bild 1 gezeigt.

Die Datei package.jsonanpassen(Bild 1)
Autor
Sie haben nun zwei Skripte: test und watch. Letzteres kommt erst etwas später zum Einsatz. Fügen Sie eine Datei namens registration-spec.js zum Ordner spec hinzu und nehmen Sie die folgenden Codezeilen in die Datei auf:
describe(<span class="hljs-string">"Suite to test the </span>
<span class="hljs-string"> registration API"</span>, () => {
<span class="hljs-keyword">const</span> axios = require(<span class="hljs-string">'axios'</span>);
<span class="hljs-keyword">const</span> baseUrl =
<span class="hljs-string">'http://localhost:5000'</span>;
<span class="hljs-keyword">const</span> client = axios.create({
baseURL: baseUrl
})
<span class="hljs-keyword">const</span> uuidv1 = require(<span class="hljs-string">'uuid/v1'</span>);
describe(<span class="hljs-string">"when calling /health"</span>, () => {
it(<span class="hljs-string">"should return status code OK (200)"</span>,
<span class="hljs-keyword">async</span> () => {
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.<span class="hljs-keyword">get</span>(<span class="hljs-string">'health'</span>);
expect(response.status).toBe(<span class="hljs-number">200</span>);
})
});
});
Das obige Codefragment greift auf den API-Endpunkt
/health zu und erwartet, dass dieser mit OK (200) antwortet. Um diesen Test auszuführen, starten Sie das API mit
/health zu und erwartet, dass dieser mit OK (200) antwortet. Um diesen Test auszuführen, starten Sie das API mit
$ dotnet <span class="hljs-keyword">run</span><span class="bash"> -p applications-api </span>
Dann öffnen Sie ein neues Terminal, navigieren in den Projektordner und starten den Test mittels
$ cd ~/dotnetpro/registration/OutsideInTests-<span class="hljs-keyword">Node</span>
<span class="hljs-title">$</span> npm test
Den Output dieses ersten Outside-in-Tests im Konsolenfenster zeigt Bild 2.

Outputdes ersten Outside-in-Tests(Bild 2)
Autor
Outside-in-Tests mit .NET Core
Auch mit .NET Core lassen sich Outside-in-Tests bauen. Navigieren Sie dazu wieder zurück:
<span class="hljs-variable">$ </span>cd ~<span class="hljs-regexp">/dotnetpro/registration</span>
Erstellen Sie dort ein neues xUnit-Projekt namens OutsideInTests. Das klappt mit folgendem Befehl:
$ dotnet <span class="hljs-keyword">new</span> xunit <span class="hljs-comment">--name OutsideInTests </span>
Öffnen Sie dann die Solution in Ihrem bevorzugten Code Editor, zum Beispiel in Visual Studio Code mittels $ code.Lokalisieren Sie im neuen Ordner OutsideInTests die Datei UnitTests1.cs und öffnen Sie diese im Editor. Löschen Sie den Beispieltest und fügen Sie stattdessen folgenden Code ein:
[Fact]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">when_loading_application</span>(<span class="hljs-params"></span>) </span>
{
<span class="hljs-keyword">var</span> url = <span class="hljs-string">$"http://api:5000/</span>
<span class="hljs-string"> applications/<span class="hljs-subst">{Guid.NewGuid()}</span>"</span>;
<span class="hljs-keyword">var</span> client = <span class="hljs-keyword">new</span> HttpClient();
<span class="hljs-keyword">var</span> response =
<span class="hljs-keyword">await</span> client.GetAsync(url);
Assert.Equal(HttpStatusCode.OK,
response.StatusCode);
Öffnen Sie die Datei launchSettings.json im API-Projekt und ändern Sie die Zeile applicationUrl auf http://+:5000, wie in Bild 3 zu sehen.

Ändern des applicationURL(Bild 3)
Autor
Die Änderung bewirkt, dass der Kestrel Webserver, welcher zum Hosten des APIs genutzt wird, auf allen Endpunkten (und nicht nur auf localhost) auf Port 5000 hört.Damit wird ebenfalls verhindert, dass der Webserver HTTPS-Requests akzeptiert, da diese gültige Zertifikate erfordern, die für die Beispielanwendung nicht berücksichtigt werden. Auch in der Startup-Klasse des APIs entfernen Sie deshalb die Codezeile app.UseHttpsRedirection();.Wie Sie aus der ersten Folge dieser Serie wissen, benutzt das API die Bibliothek MediatR. Diese wurde bislang noch nicht vollständig konfiguriert. Das holen wir jetzt nach. Zuerst werden dafür die MediatR Extensions für Dependency Injection installiert. Das geschieht wie folgt:
$ dotnet add applications_api \
package MediatR<span class="hljs-selector-class">.Extensions</span><span class="hljs-selector-class">.Microsoft</span>.
DependencyInjection
Dann fügen Sie folgende Zeile zur Methode ConfigureServices der Setup-Klasse des APIs hinzu:
<span class="hljs-selector-tag">services</span><span class="hljs-selector-class">.AddMediatR</span>(<span class="hljs-selector-tag">Assembly</span><span class="hljs-selector-class">.GetExecutingAssembly</span>());
Gegebenenfalls müssen Sie auch noch die folgenden beiden using-Statements ergänzen:
<span class="hljs-keyword">using</span> MediatR;
<span class="hljs-keyword">using</span> <span class="hljs-keyword">System</span>.Reflection;
Im Terminal starten Sie nun das API aus dem Solution-Ordner heraus, und zwar mittels
$ dotnet <span class="hljs-keyword">run</span><span class="bash"> -p applications_api </span>
Falls sich die Firewall auf Ihrem System meldet – in Bild 4 sehen Sie diesen Fall unter Windows –, dann erlauben Sie einfach den Zugriff per Klick auf Allow access. Sie sollten dann den Output aus Bild 5 auf Ihrem Bildschirm sehen. In der dritten Zeile wird angezeigt, dass der Webserver nun auf den Endpunkten http://[::]:5000 lauscht.

Windows Firewall:Den Zugriff zulassen(Bild 4)
Autor

Der Webserverhört auf Port 5000(Bild 5)
Autor
Öffnen Sie ein zweites Terminalfenster, navigieren Sie zum Lösungsverzeichnis und starten die Sie die Tests mit dotnet test OutsideInTests. Bild 6 zeigt das erfolgreiche Ergebnis des Tests. Damit sind Sie nun bereit, API und Tests in je einem Container laufen zu lassen.

Test Run Successful(Bild 6)
Autor
Outside-in-Tests im Container
Gebraucht werden dafür zwei Dockerfiles, eines für das API und eines für die Tests. Außerdem ist ein Docker Compose File erforderlich, das deklarativ festlegt, wie die Tests ablaufen sollen. Das klappt so: Fügen Sie zum API-Projekt eine Datei Dockerfile hinzu und füllen Sie diese mit folgendem Code:
<span class="hljs-keyword">FROM</span> mcr.microsoft.com<span class="hljs-regexp">/dotnet/</span>core<span class="hljs-regexp">/sdk:3.1 </span>
<span class="hljs-regexp">WORKDIR /</span>app
<span class="hljs-keyword">COPY</span> .<span class="hljs-regexp">/applications-api/</span>*.csproj .<span class="hljs-regexp">/applications-api/</span>
RUN dotnet restore applications-api
<span class="hljs-keyword">COPY</span> .<span class="hljs-regexp">/applications-api ./</span>applications-api
CMD dotnet run -p applications-api
Auch das Projekt OutsideInTests wird um ein Dockerfile mit nachfolgendem Inhalt erweitert:
FROM mcr.microsoft.com<span class="hljs-regexp">/dotnet/</span>core<span class="hljs-regexp">/sdk:3.1 </span>
<span class="hljs-regexp">WORKDIR /</span>app
COPY .<span class="hljs-regexp">/OutsideInTests/</span>*.csproj .<span class="hljs-regexp">/OutsideInTests/</span>
RUN dotnet restore OutsideInTests
COPY .<span class="hljs-regexp">/OutsideInTests ./</span>OutsideInTests
CMD dotnet test OutsideInTests
Nun erstellen Sie die Datei <em>docker-compose-outsidein.yml</em> im Solution-Ordner und geben folgende Zeilen <span class="hljs-string">ein:</span>
<span class="hljs-string">version:</span> <span class="hljs-string">"2.4"</span>
<span class="hljs-string">services:</span>
<span class="hljs-string">api:</span>
<span class="hljs-string">image:</span> acme.com<span class="hljs-regexp">/applications:dev </span>
<span class="hljs-regexp"> build: </span>
<span class="hljs-regexp"> context: . </span>
<span class="hljs-regexp"> dockerfile: applications-api/</span>Dockerfile
<span class="hljs-string">healthcheck:</span>
<span class="hljs-string">test:</span> curl --fail <span class="hljs-string">localhost:</span><span class="hljs-number">5000</span><span class="hljs-regexp">/health || exit -1 </span>
<span class="hljs-regexp"> interval: 3s </span>
<span class="hljs-regexp"> timeout: 3s </span>
<span class="hljs-regexp"> retries: 3 </span>
<span class="hljs-regexp"> start_period: 5s </span>
<span class="hljs-regexp"> outsidein: </span>
<span class="hljs-regexp"> image: acme.com/</span>applications-<span class="hljs-string">outsideintests:</span>dev
<span class="hljs-string">build:</span>
<span class="hljs-string">context:</span> .
<span class="hljs-string">dockerfile:</span> OutsideInTests/Dockerfile
<span class="hljs-string">depends_on:</span>
<span class="hljs-string">api:</span>
<span class="hljs-string">condition:</span> service_healthy
In diesem Codefragment fällt auf, dass zwei Services definiert sind, nämlich api und outsidein. Ersterer definiert das eigentliche API und Letzterer die Outside-in-Tests. Der api-Service wird durch das Dockerfile im Ordner applications-api und der outsidein-Service durch das Dockerfile im Ordner OutsideInTests definiert. Der api-Service verfügt über eine Gesundheitsprüfung (healthcheck). Diese verwendet das Tool curl,um damit den Endpunkt /health periodisch zu prüfen. Solange das API nicht antwortet, gilt es als ungesund (unhealthy).Der outsidein-Service verfügt über eine Abhängigkeitsprüfung (depends_on), welche sicherstellt, dass der Service erst gestartet wird, wenn der api-Service gesund, also healthy ist. So starten die Tests erst, wenn die zu prüfende Schnittstelle, das API, bereit ist.Obiger Code spricht den Endpunkt /health an, der aber bis jetzt noch gar nicht existiert. Das gilt es jetzt nachzuholen. Fügen Sie also dem API-Projekt einen neuen API-Controller namens HealthController mit diesem Code hinzu:
<span class="hljs-keyword">using</span> Microsoft.AspNetCore.Mvc;
<span class="hljs-keyword">namespace</span> <span class="hljs-title">Applications.Controllers</span>
{
[ApiController]
[Route(<span class="hljs-string">"[controller]"</span>)]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HealthController</span> : <span class="hljs-title">ControllerBase</span>
{
[HttpGet]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">Get</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>; }
}
}
Damit gibt es einen HTTP-GET-Endpunkt, der stets true zurückliefert. Für die Beispielanwendung genügt das. In einer etwas komplexeren Anwendung würde man dort eine aussagekräftigere Logik implementieren, etwa eine Prüfung, ob das API auf eine zugehörige Datenbank zugreifen kann oder Ähnliches.Nun sind Sie bereit, die Tests im Container auszuführen. Im Terminal wechseln Sie dafür ins Solution-Verzeichnis und führen folgenden Befehl aus:
$ docker-compose -f
docker-compose-outsidein<span class="hljs-selector-class">.yml</span> up
In Bild 7 sehen Sie eine gekürzte Fassung des Outputs, den Sie danach in Ihrem Terminal-Fenster sehen sollten. Ganz offensichtlich ist der Test fehlgeschlagen.

Der Testist fehlgeschlagen(Bild 7)
Autor
Der Grund liegt darin, dass im Test der URL zum API hart codiert wurde, nämlich als http://localhost/applications/
<application-id>. Das hat gut geklappt, solange API und Test direkt auf dem Computer gestartet wurden. Jetzt aber laufen sowohl API als auch Tests im Container.Aus der Sicht des Tests läuft das API somit in einem „Remote-Computer“ der über den DNS-Namen api angesprochen wird, weil dies im Docker Compose File so definiert worden ist.Dieses Problem lässt sich leicht beseitigen, indem Sie den URL ändern in http://api:5000/applications/<application-id>. Die zugehörige Zeile im Test-Code sieht nach der Änderung so aus:
<application-id>. Das hat gut geklappt, solange API und Test direkt auf dem Computer gestartet wurden. Jetzt aber laufen sowohl API als auch Tests im Container.Aus der Sicht des Tests läuft das API somit in einem „Remote-Computer“ der über den DNS-Namen api angesprochen wird, weil dies im Docker Compose File so definiert worden ist.Dieses Problem lässt sich leicht beseitigen, indem Sie den URL ändern in http://api:5000/applications/<application-id>. Die zugehörige Zeile im Test-Code sieht nach der Änderung so aus:
<span class="hljs-keyword">var</span> url = <span class="hljs-string">$"http://api:5000/applications/</span>
<span class="hljs-string"> <span class="hljs-subst">{Guid.NewGuid()}</span>"</span>;
Starten Sie einen neuen Testlauf, diesmal mit dem folgenden leicht modifizierten Befehl:
$ docker-compose -f
docker-compose-outsidein<span class="hljs-selector-class">.yml</span> up --build
Der Parameter --build kommt hier zum Einsatz, um sicherzustellen, dass das Docker-Image für den Test-Container neu erstellt wird, da der Code verändert wurde. Fehlt der Parameter, verwendet Docker Compose das alte, im Cache immer noch vorhandene Image. Diesmal sollte der Test erfolgreich durchlaufen.
Sichern und testen mit Auth0
In diesem Abschnitt erfahren Sie, wie Sie ein REST API sichern können, sodass Anwender und Anwendungen, die darauf zugreifen wollen, über ein gültiges JWT-Token verfügen müssen. JWT steht für JSON Web Token [2] und ist eine beliebte Form der Authentifizierung von Web Requests. Das Sichern des APIs ist ganz einfach, da .NET Core über eine eingebaute Unterstützung für JWT-Tokens verfügt. Ein Controller als Ganzes oder auch ein einzelner Endpunkt eines Controllers kann mit dem Attribut AuthorizeAttribute versehen und damit automatisch gesichert werden.Wie aber kommt der Benutzer des APIs zu einem gültigen Token? Dafür ist ein sogenannter Identity Provider erforderlich. Beliebt sind zum Beispiel IdentityServer4 [3] als Open-Source-Variante eines Identity Providers oder kommerzielle Angebote wie etwa Amazon Cognito [4], Microsoft Azure B2C [5], Googles Open ID Connect [6] und Auth0 [7], um nur einige zu nennen. Hier wird Auth0 benutzt.Das API bei Auth0 registrieren
Zunächst gilt es, das API bei Auth0 zu registrieren. Falls Sie noch über keinen Account bei Auth0 verfügen, dann erstellen Sie ein Gratis-Konto unter https://auth0.com/signup. Anschließend melden Sie sich unter https://auth0.auth0.com/login an, navigieren im Dashboard zum Register API und klicken auf den Button + CREATE API. Daraufhin wird ein Formular angezeigt, in das Sie die Daten zum API eingeben:- den Namen für das API, zum Beispiel Registrierungs-API,
- den Identifier, für das Heftbeispiel wählen Sie https://regis trierung.acme.com.
- In der Formularzeile Signing Algorithm behalten Sie den vorgegebenen Wert RS256 bitte bei.
- Identifier (oder audience): https://registrierung.acme.com.
- client_id: <Ihre Client ID>
- client_secret: <Ihr Client Secret>
- Authority: https://<tenant>.auth0.com, wobei <tenant> der Name ist, mit dem Sie sich bei Auth0 authentifizieren.
Sichern des APIs
Wie bereits angedeutet, ist dies der einfachste Teil der anstehenden Aufgabe. Sie müssen lediglich definieren, welche Endpunkte des APIs gesichert werden sollen und welche nicht. Im Beispielfall gibt es zwei Controller, den ApplicationsController und den HealthController. Letzterer muss üblicherweise nicht gesichert werden. Er bleibt deshalb so, wie er ist. Den ApplicationsController gilt es jedoch vor unbefugtem Zugriff zu schützen.Dafür starten Sie ein Terminal-Fenster, wechseln ins Solution-Verzeichnis und nutzen den Befehl dotnet, um weitere NuGet-Pakete zum API hinzuzufügen:
$ dotnet add applications-api \
package Microsoft<span class="hljs-selector-class">.AspNetCore</span>.
Authentication<span class="hljs-selector-class">.JwtBearer</span>
Öffnen Sie den ApplicationsController im Code-Editor und dekorieren Sie die Klasse mit dem Attribut [Authorize]. Der Controller sollte dann so aussehen:
<span class="hljs-comment">[ApiController]</span>
<span class="hljs-comment">[Route("<span class="hljs-comment">[controller]</span>“)]</span>
<span class="hljs-comment">[Authorize]</span>
public class ApplicationsController : ControllerBase
{
...
}
Ergänzen Sie die Datei zudem um die Zeile using Microsoft.AspNetCore.Authorization;. Nun müssen Sie Authentifizierung und Autorisierung mittels JWT noch konfigurieren. Dazu öffnen Sie die Datei Startup.cs und ergänzen am Anfang der Datei folgendes using-Statement:
<span class="hljs-selector-tag">using</span> <span class="hljs-selector-tag">Microsoft</span><span class="hljs-selector-class">.AspNetCore</span><span class="hljs-selector-class">.Authentication</span><span class="hljs-selector-class">.JwtBearer</span>;
Außerdem wird dieses Codefragment zur Methode ConfigureServices hinzugefügt:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority =
$"<span class="hljs-link">https://{Configuration</span>[<span class="hljs-string">"Auth0:Domain"</span>]}/";
options.Audience = Configuration["Auth0:Audience"];
});
Zur Datei appsettings.config fügen Sie folgendes Codefragment hinzu, welches die Definition für Ihre Domain und Audience bei Auth0 enthält:
<span class="hljs-string">"Auth0"</span>: {
<span class="hljs-string">"Domain"</span>: <span class="hljs-string">"<tenant>.auth0.com"</span>,
<span class="hljs-string">"Audience"</span>: <span class="hljs-string">"https://registrierung.acme.com"</span>
}
Ersetzen Sie dabei den Eintrag <tenant> durch den für Sie geltenden Wert, den Sie zuvor in der API-Detailansicht bei Auth0 notiert haben. Zur Methode Configure der Startup-Klasse fügen Sie unmittelbar vor der Codezeile app.UseAuthofrization(); noch die Zeile app.UseAuthentication(); hinzu.Damit ist das API ist nun so konfiguriert, dass es Anfragen nur dann entgegennimmt, wenn diese über ein gültiges Token verfügen. Starten Sie jetzt das API vom Terminal aus:
$ dotnet <span class="hljs-keyword">run</span><span class="bash"> -p applications-api </span>
Manuelles Testen des gesicherten APIs
Mithilfe des Tools Postman [8] soll das gesicherte API manuell getestet werden. Dafür ist zunächst ein gültiges Token von Auth0 erforderlich. Um dieses zu erhalten, können Sie das Admin-API von Auth0 benutzen. Erstellen Sie dazu in Postman einen neuen Request vom Typ POST. Als URL benutzen Sie https://<tenant>.auth0.com/oauth/token und fügen den Header Content-Type: application/json hinzu. Der Body des Requests sieht so aus:
{
<span class="hljs-attr">"client_id"</span>: <span class="hljs-string">"<your client id>"</span>,
<span class="hljs-attr">"client-secret"</span>: <span class="hljs-string">"<your client secret>"</span>,
<span class="hljs-attr">"audience"</span>: <span class="hljs-string">"https://registrierung.acme.com"</span>,
<span class="hljs-attr">"grant_type"</span>: <span class="hljs-string">"client_credentials"</span>
}
Prüfen Sie noch einmal, dass Sie Ihre eigene Client-ID und Ihr eigenes Client-Secret verwenden und klicken Sie dann auf den Button Send.Der Body der Antwort wird das access_token enthalten, zusammen mit dem Token-Typ und dem Verfallsdatum. Kopieren Sie den Token-Wert.Danach erstellen Sie einen Request für den geschützten Endpunkt /application/<application-id>. Wählen Sie dafür den Request-Typ GET und setzen den URL auf http://localhost:5000/application/<application-id>. Anstelle des Platzhalters <application-id> tragen Sie bitte eine gültige GUID ein, zum Beispiel c49ea8bd-6283-467d-8367-e43898c88214. Sie können sich eine neue GUID beispielsweise unter [9] generieren lassen.Fügen Sie den Header Authorization: Bearer <token> hinzu, wobei Sie anstelle von <token> den Wert des zuvor erhaltenen Tokens eintragen, und klicken Sie auf den Button Send.Vergewissern Sie sich, dass ein Status-Code 200 (OK) zurückgegeben wird und ein Application-Objekt im Response-Body angezeigt wird, das die von Ihnen im URL verwendete ID aufweist.Manuelles Testen ist cool, aber nicht skalierbar und auch nicht nachhaltig. Deshalb geht es nun um automatisierte Tests, die auf ein gesichertes API zugreifen.
Das gesicherte API automatisch testen
Um den gerade manuell mit Postman durchgeführten Test zu automatisieren, ist zunächst der Bezug von Tokens von Auth0 zu automatisieren. Dafür ist der Zugriff auf client_id und client_secret des bei Auth0 registrierten APIs erforderlich. Gleichzeitig dürfen die Geheimnisse nicht als Klartext im Code hinterlegt werden, da der dann später auf GitHub öffentlich zugänglich sein wird. Auch wenn man nicht an Hacker denkt, sind im Code hinterlegte Geheimnisse für zu viele Personen sichtbar, die keinen Zugriff darauf haben sollten..NET bietet den Secret Manager, der Geheimnisse verwaltet, die zur Entwicklungszeit gebraucht werden, und verhindert, dass diese im Source-Code erscheinen. Während Anwendung und Tests laufen, werden die Geheimnisse ausgelesen wie andere Konfigurationseinstellungen auch.Benutzergeheimnisse erstellen
Öffnen Sie ein neues Terminal-Fenster und navigieren Sie zum Ordner OutsideInTests der Anwendung:
<span class="hljs-variable">$ </span>cd ~<span class="hljs-regexp">/dotnetpro/registration</span><span class="hljs-regexp">/applications-api </span>
Damit Sie die User-Secrets-Funktionalität benutzen können, fügen Sie die erforderlichen NuGet-Pakete zum Projekt hinzu. Das klappt so:
$ dotnet add package Microsoft<span class="hljs-selector-class">.Extensions</span><span class="hljs-selector-class">.Configuration</span>
$ dotnet add package Microsoft<span class="hljs-selector-class">.Extensions</span>.
Configuration<span class="hljs-selector-class">.UserSecrets</span>
Initialisieren Sie den Secret Manager für das API-Projekt OutsideInTests:
$ dotnet <span class="hljs-keyword">user</span>-secrets init
Definieren Sie die folgenden vier Geheimnisse für den späteren Gebrauch:
$ dotnet user-secrets <span class="hljs-keyword">set</span> <span class="hljs-string">"auth0:url"</span> <span class="hljs-string">"</span>
<span class="hljs-string"> <token-provider-url>"</span>
$ <span class="hljs-keyword">dotnet</span> <span class="hljs-keyword">user</span>-secrets <span class="hljs-keyword">set</span> <span class="hljs-string">"auth0:client_id"</span>
<span class="hljs-string">"<client_id>"</span>
$ <span class="hljs-keyword">dotnet</span> <span class="hljs-keyword">user</span>-secrets <span class="hljs-keyword">set</span> <span class="hljs-string">"auth0:client_secret"</span>
<span class="hljs-string">"<client_secret>"</span>
$ <span class="hljs-keyword">dotnet</span> <span class="hljs-keyword">user</span>-secrets <span class="hljs-keyword">set</span> <span class="hljs-string">"auth0:audience"</span>
<span class="hljs-string">"<audience>"</span>
Wobei <token-provider-url> die Form https://<tenant>.auth0.com/oauth/token hat und <audience> im Beispielfall https://registrierung.acme.com ist, wie es bei der Registrierung der API gewählt wurde. Die Benutzergeheimnisse (User Secrets) werden auf dem lokalen Computer gespeichert und sind weder im Code noch im Code Repository sichtbar, weil sie gar nicht Bestandteil der Lösung sind.
Eine Basisklasse für Tests
Es soll verhindert werden, dass jeder einzelne Test einen langwierigen Remote-Aufruf von Auth0 ausführt. Deshalb wird eine Basisklasse definiert, die diese Aufgabe einmal pro Testsuite durchführt.Fügen Sie dazu eine Datei namens ControllerTestsBase.cs zum Projekt OutsideInTests hinzu. Den Code der Basisklasse sehen Sie in Listing 1; Sie können ihn unter [10] herunterladen. Darin wird ein HTTP-Client erstellt, welcher in den Tests für den Zugriff auf das zu testende API verwendet wird.Listing 1: Die Basisklasse für Tests (Teil 1)
<span class="hljs-keyword">namespace</span> tests <br/>{ <br/> <span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ControllerTestsBase</span> : </span><br/><span class="hljs-class"> <span class="hljs-title">IClassFixture</span>&lt;<span class="hljs-title">WebApiTesterFactory</span>&gt; </span><br/><span class="hljs-class"> </span>{ <br/> <span class="hljs-keyword">protected</span> readonly WebApiTesterFactory factory; <br/> <span class="hljs-keyword">protected</span> readonly JsonElement result; <br/> <span class="hljs-keyword">protected</span> readonly string token; <br/> <span class="hljs-keyword">protected</span> readonly HttpClient client; <br/><br/> <span class="hljs-keyword">public</span> ControllerTestsBase(<br/> WebApiTesterFactory factory) <br/> { <br/> <span class="hljs-keyword">this</span>.factory = factory; <br/> client = factory.CreateClient(); <br/> client.BaseAddress = <br/> <span class="hljs-keyword">new</span> Uri(<span class="hljs-string">"https://localhost:5001/api/"</span>); <br/> <span class="hljs-keyword">var</span> config = factory.Services.GetService( <br/> <span class="hljs-keyword">typeof</span>(IConfiguration)) <span class="hljs-keyword">as</span> IConfiguration; <br/> Assert.NotNull(config); <br/> <span class="hljs-keyword">var</span> httpClient = <span class="hljs-keyword">new</span> HttpClient(); <br/> httpClient.DefaultRequestHeaders.Accept.Add( <br/> <span class="hljs-keyword">new</span> MediaTypeWithQualityHeaderValue(<br/> <span class="hljs-string">"application/json"</span>)); <br/> <span class="hljs-keyword">var</span> url = config[<span class="hljs-string">"auth0:url"</span>]; <br/> <span class="hljs-keyword">var</span> request = <span class="hljs-keyword">new</span> HttpRequestMessage(<br/> HttpMethod.Post, url); <br/> <span class="hljs-keyword">var</span> json = <span class="hljs-keyword">new</span> { <br/> client_id = config[<span class="hljs-string">"auth0:client_id"</span>], <br/> client_secret = config[<span class="hljs-string">"auth0:client_secret"</span>], <br/> audience = config[<span class="hljs-string">"auth0:audience"</span>], <br/> grant_type = <span class="hljs-string">"client_credentials"</span> <br/> }; <br/> <span class="hljs-keyword">var</span> content = <span class="hljs-keyword">new</span> StringContent(JsonSerializer.<br/> Serialize(json, <span class="hljs-keyword">typeof</span>( object)), <br/> Encoding.UTF8, <span class="hljs-string">"application/json"</span>); <br/> request.Content = content; <br/> <span class="hljs-keyword">var</span> response = httpClient.PostAsync(<br/> url, content).Result; <br/><br/> result = <br/> JsonSerializer.Deserialize&lt;JsonElement&gt;( <br/> response.Content.ReadAsStringAsync().Result); <br/> token = result.GetProperty(<br/> <span class="hljs-string">"access_token"</span>).GetString(); <br/> } <br/> } <br/>}
Die Zeile factory.Services.GetService(typeof
(IConfiguration)) as IConfiguration; holt ein Konfigurationsobjekt vom Dependency-Injection-Container, mit dem später auf die Konfigurationsdaten zugegriffen wird.Der übrige Code greift über einen (echten) HTTP-Client auf das Admin-Interface von Auth0 zu und verlangt ein Token. Beginnend mit var url = config[„auth0:url“]; wird das Konfigurationsobjekt genutzt, um die Geheimnisse aus dem Secrets Manager auszulesen. Die Zeile httpClient.PostAsync(url, content).Result; liefert das gewünschte Token, und die Zeile token = result.GetProperty( „access_token“).GetString(); speichert das Token in einer Instanzvariablen.
(IConfiguration)) as IConfiguration; holt ein Konfigurationsobjekt vom Dependency-Injection-Container, mit dem später auf die Konfigurationsdaten zugegriffen wird.Der übrige Code greift über einen (echten) HTTP-Client auf das Admin-Interface von Auth0 zu und verlangt ein Token. Beginnend mit var url = config[„auth0:url“]; wird das Konfigurationsobjekt genutzt, um die Geheimnisse aus dem Secrets Manager auszulesen. Die Zeile httpClient.PostAsync(url, content).Result; liefert das gewünschte Token, und die Zeile token = result.GetProperty( „access_token“).GetString(); speichert das Token in einer Instanzvariablen.
Test für einen gesicherten Endpunkt
Mithilfe der gerade angelegten Basisklasse sind Sie nun bereit zum Schreiben der eigentlichen Tests. Löschen Sie dafür zuerst die Datei UnitTests1.cs aus dem Projekt OutsideInTests. Fügen Sie eine neue Datei namens ApplicationsControllerTests.cs hinzu. Listing 2 zeigt deren Code, den Sie unter [11] abrufen können. Bitte beachten Sie, wie der Test auf das Token zugreift, um den Aufruf des APIs erfolgreich durchzuführen.Listing 2: GlossaryControllerTests.cs
<span class="hljs-keyword">using</span> System.Collections.Generic; <br/><span class="hljs-keyword">using</span> System.Linq; <br/><span class="hljs-keyword">using</span> System.Threading.Tasks; <br/><span class="hljs-keyword">using</span> Newtonsoft.Json; <br/><span class="hljs-keyword">using</span> api; <br/><span class="hljs-keyword">using</span> Xunit; <br/><br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">GlossaryControllerTests</span> : <br/> <span class="hljs-title">ControllerTestsBase</span> <br/>{ <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">GlossaryControllerTests</span>(</span><br/><span class="hljs-function"><span class="hljs-params"> WebApiTesterFactory factory</span>): <span class="hljs-title">base</span>(<span class="hljs-params">factory</span>) </span>{} <br/><br/> [Fact] <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task should_return_list_of_glossary_</span><br/><span class="hljs-function"> <span class="hljs-title">items_without_need_for_token</span>(<span class="hljs-params"></span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-keyword">var</span> response = <span class="hljs-keyword">await</span> client.GetAsync(<span class="hljs-string">"glossary"</span>); <br/> <span class="hljs-keyword">var</span> json = <br/> <span class="hljs-keyword">await</span> response.Content.ReadAsStringAsync(); <br/> <span class="hljs-keyword">var</span> data = JsonConvert.DeserializeObject&lt;<br/> IEnumerable&lt;GlossaryItem&gt;&gt;(json); <br/> Assert.True(data.Count() &gt; <span class="hljs-number">0</span>); <br/> Assert.Equal(<span class="hljs-string">"AccessToken"</span>, data.First().Term); <br/> } <br/>}
Bevor Sie die Tests laufen lassen können, müssen Sie noch die Datei docker-compose-outsidein.yml anpassen, im Testservice die Environment-Variable API_HOST definieren sowie die User Secrets vom Host-Computer in den Container mounten. Bild 8 zeigt, wie es geht. Ändern Sie die Datei und fügen Sie die im Bild rot markierten Zeilen hinzu.

Die rot markiertenZeilen werden hinzugefügt(Bild 8)
Autor
Danach können Sie die Tests starten, indem Sie folgenden Befehl aus dem Solution-Ordner heraus aufrufen:
$ docker-compose -f docker-compose-outsidein up <span class="hljs-comment">--build </span>
Der Parameter --build ist auch hier wieder wichtig, denn er stellt sicher, dass die Docker-Images neu aufgebaut und nicht aus dem Cache entnommen werden. Dieser Testlauf sollte jetzt erfolgreich sein.Fügen Sie nun einen weiteren Test zur Testklasse hinzu, welcher sicherstellt, dass ein Aufruf des Endpunkts
/applications/<application-id> fehlschlägt, sofern kein Token im Header mitgeliefert wird. Bild 9 zeigt den Code dazu.
/applications/<application-id> fehlschlägt, sofern kein Token im Header mitgeliefert wird. Bild 9 zeigt den Code dazu.

Der Zugriff ohne Tokensoll fehlschlagen(Bild 9)
Autor
Fazit
In diesem zweiten Teil der Artikelserie ging es noch einmal um das Thema Testing. Gezeigt wurde, wie Sie Outside-in-, beziehungsweise Blackbox-Tests schreiben – sowohl mit C# und xUnit als auch mit Node.js und Jasmine.Anschließend haben Sie gesehen, wie man ein .NET Core Web API so sichert, dass nur Clients darauf zugreifen können, welche ein gültiges JWT- Token mitbringen. Darüber hinaus wurde erläutert, wie Sie ein gesichertes API mit automatischen Tests prüfen können.Im kommenden dritten Teil der Serie erfahren Sie, wie man den Code per Circuit Breaker Pattern, Logging und Error Handling robust und wartbar gestaltet. Schlussendlich lesen Sie, wie man ein optimales Docker-Image für die Produktion erstellt.Fussnoten
- Gabriel Schenker, .NET-Core-Microservices entwickeln, Teil 1, dotnetpro 4/2020, Seite 14 ff., http://www.dotnetpro.de/A2004NetMicroservice
- JSON Web Token, https://jwt.io
- IdentityServer4, https://identityserver.io
- Amazon Cognito, https://aws.amazon.com/cognito
- Microsoft Azure B2C, http://www.dotnetpro.de/SL2005NETCoreMicroservice1
- Googles Open ID Connect, http://www.dotnetpro.de/SL2005NETCoreMicroservice2
- Auth0, https://auth0.com
- Postman, http://www.postman.com
- GUID-Generator, http://www.dotnetpro.de/SL2005NETCoreMicroservice3
- Code der Basisklasse, http://www.dotnetpro.de/SL2005NETCoreMicroservice4
- GlossaryControllerTests.cs, http://www.dotnetpro.de/SL2005NETCoreMicroservice5