Wyślij zapytanie Dołącz do Sii

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).

Ryc. 1 Proponowane rozwiązanie oparte o komponenty Azure
Ryc. 1 Proponowane rozwiązanie oparte o komponenty Azure

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:

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).

Ryc. 2 Proponowane opcje wyzwolenia aplikacji
Ryc. 2 Proponowane opcje wyzwolenia aplikacji

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):

Ryc. 3 Konfiguracja connectora Outlook
Ryc. 3 Konfiguracja connectora Outlook

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).

Ryc. 4 Inicjalizacja zmiennej
Ryc. 4 Inicjalizacja zmiennej

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).

Ryc. 5 Dodanie połączenia z Storage Account
Ryc. 5 Dodanie połączenia z Storage Account

Następnie skonfigurujmy to połączenie (Ryc. 6):

Ryc. 6 Konfiguracja połączenia z Storage Account
Ryc. 6 Konfiguracja połączenia z Storage Account

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):

Ryc. 7 Parametry niezbędne do skonfigurowania połączenia z Storage Account
Ryc. 7 Parametry niezbędne do skonfigurowania połączenia z Storage Account

Implementacja akcji tworzenia bloba

Po uzyskaniu połączenia ze Storage Account musimy zaimplementować akcję tworzenia bloba (Ryc. 8):

Ryc. 8 Konfiguracja tworzenia bloba z wiadomością e-mail
Ryc. 8 Konfiguracja tworzenia bloba z wiadomością e-mail

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.

Ryc. 9 Dodanie kontenera e-mails do Blob Storage
Ryc. 9 Dodanie kontenera e-mails do Blob Storage

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).

Ryc. 10 Status wykonań aplikacji LogicApp
Ryc. 10 Status wykonań aplikacji LogicApp

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):

Ryc. 11 Tworzenie FunctionApp w Visual Studio
Ryc. 11 Tworzenie FunctionApp w Visual Studio

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):

Ryc. 12 Odpowiedź z endpointa pobierającego zawartość bloba
Ryc. 12 Odpowiedź z endpointa pobierającego zawartość bloba

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).
Ryc. 13 Konfiguracja połączenia Office 365
Ryc. 13 Konfiguracja połączenia Office 365

Stworzony skrypt Deploy.ps1 umożliwia również deploy do dowolnej subskrypcji. Wystarczy tylko dodać parametr -subscriptionId <subscriptionId> do wywołania skryptu.

4.8/5 ( głosy: 24)
Ocena:
4.8/5 ( głosy: 24)
Autor
Avatar
Michał Pyś

Software Developer z ponad 6-letnim doświadczeniem, pracujący głównie w technologiach Microsoft. Prywatnie miłośnik każdego rodzaju sportu oraz dobrego serialu.

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?