W dzisiejszych czasach chyba żaden software developer nie ma wątpliwości w kwestii potrzeby testowania wytworzonego przez siebie kodu. Podczas prac nad wysyłką wiadomości e-mail potrzebowałem rozwiązania, które pozwoli na weryfikację tematu wiadomości oraz jej zawartości. W krótkim czasie udało się opracować proste rozwiązanie, oparte o platformę chmurową Azure, które chciałbym zaprezentować.
Problem i zastosowane rozwiązania/narzędzia
Problemem było umożliwienie testowania wysyłki oraz zawartości wiadomości e-mail jako elementu testów E2E. Aby znaleźć rozwiązanie skorzystałem z:
- Konta na platformie Azure,
- Microsoft Visual Studio 2019,
- Azure Functions/.NET Core 3.1,
- Azure LogicApp,
- PowerShell,
- Postman,
- Konta Microsoft Outlook Shared Mailbox.
Opis koncepcyjny proponowanego rozwiązania
Platforma chmurowa Azure daje wiele możliwości. Jedną z nich jest tworzenie niewielkich aplikacji w szybki sposób, jednocześnie pozwalając na ich bardzo łatwą i wygodną integrację. Wobec tego przyjrzyjmy się proponowanemu rozwiązaniu opartemu o komponenty Azure (Ryc. 1).
Najważniejszym elementem jest w tym przypadku Azure LogicApp, który ma trzy podstawowe zadania:
- Odpytywanie mailowej skrzynki współdzielonej o nadchodzące wiadomości e-mail.
- Procesowanie otrzymanej wiadomości.
- Zapis wiadomości do Azure Blob Storage.
Azure FunctionApp udostępnia endpoint pozwalający na pobranie zapisanego wcześniej bloba w formie tekstowej.
Rozwiązanie
Pora przyjrzeć się szczegółom technicznym. Opiszę krok po kroku proces tworzenia aplikacji. Dla uproszczenia, wyłączając jednak kroki związane z tworzeniem samych zasobów, gdyż istnieje wiele tutoriali, jak poradzić sobie z tym tematem. Znajdziemy je na przykład w oficjalnej dokumentacji Microsoftu:
- Quickstart: Create an integration workflow with multi-tenant Azure Logic Apps and the Azure portal (zdecydujmy się na Consumption Plan podczas tworzenia zasobu),
- Create your first function in the Azure portal,
- Create a storage account.
Dodatkowo, w końcowej części artykułu opiszę procedurę deploymentu z użyciem ARM template odzwierciedlającego stworzone zasoby. Pozwoli to na szybkie odtworzenie opisanego rozwiązania bez konieczności „wyklikiwania” wszystkiego w portalu.
Implementacja
Po utworzeniu LogicApp przejdźmy do implementacji. Wybierzmy utworzony zasób LogicApp. Następnie z menu po lewej w sekcji Development Tools kliknijmy Logic App Designer oraz, z dostępnych opcji, Blank Logic App. Pierwszym krokiem jest decyzja, co będzie wyzwalało wykonanie utworzonej funkcji (Ryc. 2).
Konfiguracja connectora
Dostępnych opcji są setki, natomiast w naszym przypadku skorzystamy z connectora Office 365 Outlook i akcji „When a new email arrives in a shared mailbox (V2)”. Wybrałem shared mailbox, ponieważ ta skrzynka po ustawieniu odpowiednich uprawnień może być dostępna dla każdego członka zespołu, ułatwiając wspólną pracę.
Konfiguracja connectora powinna być następująca (Ryc. 3):
Original Mailbox Address jest adresem naszej skrzynki współdzielonej. Inne opcje pozwalają na filtrowanie wiadomości e-mail. Według ustawień, skrzynka sprawdzana jest co 20 sekund. Po wprowadzeniu parametrów, musimy uwierzytelnić dostęp do skrzynki poprzez zalogowanie się. Warto wspomnieć, że po skorzystaniu z connectorów w naszej Resource Group zostaną stworzone dodatkowe zasoby typu API Connection.
Inicjalizacja zmiennej
Otrzymywane wiadomości e-mail powinny być możliwe do jednoznacznego zidentyfikowania, wobec tego załóżmy, że w tytule wiadomości znajduje się unikalne OrderId. Użyjemy go również jako nazwy pliku zapisanego w Blob Storage.
Dodajmy wobec tego krok w LogicApp, pozwalający na zainicjalizowanie zmiennej wartością tego Id. Wybierzmy kolejno New Step, Variables, Initialize variable (Ryc. 4).
Jako wartość wyrażenia Expression podajmy:
substring(triggerBody()?['Subject'], add(9, lastIndexOf(triggerBody()?['Subject'], 'ORDERID: ')), 10)
Jest to wyrażenie pozwalające na wyszukanie w temacie wiadomości e-mail ciągu znaków „ORDERID: ” oraz pobranie kolejnych 10 znaków, które będą stanowić właśnie parametr orderId.
substring(triggerBody()?['Subject'], add(9, lastIndexOf(triggerBody()?['Subject'], 'ORDERID: ')), 10)
Jest to wyrażenie pozwalające na wyszukanie w temacie wiadomości e-mail ciągu znaków „ORDERID: ” oraz pobranie kolejnych 10 znaków, które będą stanowić właśnie parametr orderId.
Połączenie do Azure Storage Account
Kolejny krok, który powinniśmy dodać, to połączenia do Azure Storage Account: New Step, Azure Blob Storage, Create New Blob (Ryc. 5).
Następnie skonfigurujmy to połączenie (Ryc. 6):
Parametry to:
- Connection Name – dowolna nazwa połączenia,
- Authentication Type – typ uwierzytelnienia – Access Key, czyli klucz dostępowy,
- Azure Storage Account Name – nazwa Storage Account,
- Azure Storage Account Access Key – klucz dostępowy.
Account Name oraz Access Key możemy odnaleźć we wcześniej stworzonym Storage Account (Ryc. 7):
Implementacja akcji tworzenia bloba
Po uzyskaniu połączenia ze Storage Account musimy zaimplementować akcję tworzenia bloba (Ryc. 8):
Parametry:
- Storage Account Name – wybieramy wcześniej skonfigurowane połączenie,
- Folder Path – nazwa kontenera Blob Storage, gdzie przechowywane będą wiadomości e-mail,
- Blob Name – nazwą bloba będzie unikaly orderId,
- Blob Content – zawartością bloba będzie temat oraz treść wiadomości e-mail oddzielone od siebie separatorem.
Musimy mieć również pewność, że skonfigurowany kontener istnieje, wobec czego dodajmy go w Storage Account (Ryc. 9). Niestety, na chwilę obecną API Azure nie pozwala na tworzenie kontenerów z poziomu LogicApp. Można to jednak zrobić np. poprzez wywołanie FunctionApp, która to zadanie jest w stanie wykonać. Automatyzacja tego procesu jest jednak poza zakresem niniejszego artykułu.
Przetestowanie działania aplikacji
W tym momencie jesteśmy gotowi do przetestowania zapisu wiadomości e-mail poprzez wysłanie maila na wcześniej utworzoną skrzynkę (pamiętajmy jednak, aby tytuł zawierał wyrażenie „ORDERID: ”). Wiadomość powinna znaleźć się we wcześniej utworzonym kontenerze emails.
Poprawne (bądź nie) wykonanie naszej aplikacji możemy podglądnąć w sekcji Overview stworzonej LogicApp. Klikając w konkretny wiersz, uzyskamy dostęp do szczegółów wykonania – jest to również sposób na debuggowanie (Ryc. 10).
Odczytywanie wiadomości w solucji z testami E2E
Na chwilę obecną mamy zapisaną wiadomość w Azure Storage Account, natomiast chcielibyśmy w łatwy i szybki sposób, móc tą wiadomość odczytać w naszej solucji z testami E2E (w solucji testowej wystarczy wykonać najzwyklejsze zapytanie HTTP GET). Wobec tego stwórzmy FunctionApp, które posiadać będzie następujące funkcjonalności:
- Możliwość odczytu bloba na podstawie jego nazwy.
- Czyszczenie kontenera z blobów starszych niż określono.
W Visual Studio stwórzmy nową solucję oraz dodajmy do niej projekt Azure Functions (następnie wybierzmy .NET Core 3 LTS jako framework aplikacji oraz Http Trigger jako sposób wyzwolenia akcji) (Ryc. 11):
Kod funkcji pozwalający na pobranie wiadomości e-mail
Kod funkcji, umożliwiający pobranie wiadomości e-mail z bloba, wygląda następująco:
[FunctionName("get-email")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
[Blob(Constants.EmailContainerName, FileAccess.Read)] CloudBlobContainer myBlobContainer,
ILogger log,
IBinder binder)
{
log.LogInformation("C# HTTP trigger GetEmail function starts processing a request.");
await myBlobContainer.CreateIfNotExistsAsync();
string orderId = req.Query["orderid"];
if (string.IsNullOrEmpty(orderId))
return new BadRequestObjectResult("Please pass an orderId on the query string");
var result = await _blobRepository.ReadEmail(binder, orderId);
return new OkObjectResult(result);
}
Natomiast kod funkcji odpowiedzialnej za czyszczenie kontenera jest następujący (tym razem jest to funkcja wyzwalana poprzez Timer Trigger konfigurowany przy pomocy wyrażenia CRON):
[FunctionName("cleanup-emails")]
public static async Task Run([TimerTrigger("0 50 * * * *")] TimerInfo myTimer,
[Blob(Constants.EmailContainerName, FileAccess.ReadWrite)] CloudBlobContainer myBlobContainer,
ILogger log)
{
log.LogInformation($"{nameof(CleanupEmails)} Timer trigger function executed at: {DateTime.Now}");
await myBlobContainer.CreateIfNotExistsAsync();
BlobContinuationToken blobContinuationToken = null;
var blobList = await myBlobContainer.ListBlobsSegmentedAsync(blobContinuationToken);
var cloudBlobList = blobList.Results
.Select(blb => blb as ICloudBlob)
.Where(blb => blb.Properties.LastModified < DateTime.Now.AddHours(-1));
foreach (var item in cloudBlobList)
{
await item.DeleteIfExistsAsync();
}
}
Myślę, że kod obydwu funkcji jest samoopisujący się, więc nie będę zagłębiał się w jego szczegóły. Pełne rozwiązanie można znaleźć na moim GitHubie.
Ostatnie kroki
Ostatnim krokiem jest stworzenie Publish Profile oraz deploy FunctionApp z Visual Studio do Azure. Kliknijmy prawym klawiszem na projekt zawierający funkcje i wybierzmy Publish oraz kolejno Azure, Azure FunctionApp (Windows) i w okienku konfiguracyjnym Publish zaznaczmy naszą subskrypcję, Resource Group oraz stworzoną wcześniej FunctionApp.
Kliknijmy Finish, co pozwoli nam utworzyć Publish Profile. Wtedy pozostaje już sam deploy, który będzie miał miejsce po kliknięciu przycisku Publish. Przy pomocy narzędzia Postman, które pozwala na wykonanie zapytań m. in. do endpointów FunctionApp, zweryfikujmy poprawność działania aplikacji (Ryc. 12):
Na powyższej grafice widać, że byliśmy w stanie odczytać zapisaną wiadomość e-mail składającą się z tematu, separatora oraz treści wiadomości. Dzięki temu jesteśmy w stanie zweryfikować, co tylko nam się podoba w ramach testów E2E.
Podsumowanie
Platforma Azure udostępnia bardzo dużą ilość prostych rozwiązań, które jesteśmy w stanie użyć bardzo szybko i z łatwością zintegrować między sobą. Jednymi z najbardziej podstawowych są LogicApps oraz FunctionApps, które zostały przedstawione w opisanym wyżej przykładzie.
Bonus
Jako dodatek i ułatwienie, we wspomnianym wcześniej już repozytorium zamieściłem ARM template z opisem infrastruktury użytej w moim przykładzie oraz skrypt PowerShell, pozwalający na łatwy deploy ARM template.
Po sklonowaniu repozytorium, skonfigurujmy odpowiednie parametry w pliku template.parameters.json pamiętając jednocześnie, że w tym przypadku resourceName będzie globalnym adresem FunctionApp, wobec czego musi być unikalne.
"parameters": {
"resourceGroupName": {
"value": "sii-email-notifications"
},
"resourceGroupLocation": {
"value": "westeurope"
},
"resourceName": {
"value": "sii-email-notifications-test"
},
"resourceLocation": {
"value": "westeurope"
}
}
Kolejnym krokiem jest wykonanie skryptu PowerShell (wymagana jest wcześniejsza instalacja modułów Az.Accounts oraz Az.Resources)
.\Deploy.ps1 -useParameters
który pozwoli na deploy ARM template do bieżącej subskrypcji. Pamiętajmy jednak, że sama infrastruktura to w tym przypadku zbyt mało. Trzeba wykonać również dodatkowe kroki w celu poprawnej konfiguracji pełnej aplikacji:
- Uwierzytelnienie połączenia do Outlook 365 w LogicApp (Ryc. 13).
- Uwierzytelnienie połączenia do Azure Storage Account w LogicApp (zgodnie z przedstawionym wcześniej opisem).
- Utworzenie kontenera emails w Blob Storage (zgodnie z przedstawionym wcześniej opisem).
Stworzony skrypt Deploy.ps1 umożliwia również deploy do dowolnej subskrypcji. Wystarczy tylko dodać parametr -subscriptionId <subscriptionId> do wywołania skryptu.
Zostaw komentarz