From 2bee131ff4dbd154daeffada0a005358d030b5d9 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 25 Jun 2024 23:26:54 +0200 Subject: [PATCH] Add E2E test for unlock mechanism (#60) --- .../Common/PlaywrightInputHelper.cs | 113 ++++++++++++++++++ .../Common/PlaywrightTest.cs | 6 + .../{ => Tests}/AliasTests.cs | 89 +------------- .../{ => Tests}/AuthTests.cs | 2 +- .../AliasVault.E2ETests/Tests/UnlockTests.cs | 49 ++++++++ 5 files changed, 173 insertions(+), 86 deletions(-) create mode 100644 src/Tests/AliasVault.E2ETests/Common/PlaywrightInputHelper.cs rename src/Tests/AliasVault.E2ETests/{ => Tests}/AliasTests.cs (57%) rename src/Tests/AliasVault.E2ETests/{ => Tests}/AuthTests.cs (98%) create mode 100644 src/Tests/AliasVault.E2ETests/Tests/UnlockTests.cs diff --git a/src/Tests/AliasVault.E2ETests/Common/PlaywrightInputHelper.cs b/src/Tests/AliasVault.E2ETests/Common/PlaywrightInputHelper.cs new file mode 100644 index 000000000..b9decb2b8 --- /dev/null +++ b/src/Tests/AliasVault.E2ETests/Common/PlaywrightInputHelper.cs @@ -0,0 +1,113 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.E2ETests.Common; + +/// +/// Playwright input helper class. +/// +/// The IPage instance for the current test. +public class PlaywrightInputHelper(IPage page) +{ + private static readonly Random Random = new(); + + /// + /// Generate a random string of specified length. + /// + /// Length of the string. + /// The random generated string. + public static string GenerateRandomString(int length = 10) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[Random.Next(s.Length)]).ToArray()); + } + + /// + /// Generate a random email address. + /// + /// The random email address. + public static string GenerateRandomEmail() + { + return $"{GenerateRandomString(5)}@example.com"; + } + + /// + /// Generate a random number. + /// + /// The random number. + public static string GenerateRandomNumber() + { + return Random.Next(0, 10000).ToString(); + } + + /// + /// Generate a random password of specified length. + /// + /// The length of the password. + /// The random generated password. + public static string GenerateRandomPassword(int length = 12) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[Random.Next(s.Length)]).ToArray()); + } + + /// + /// Helper method to fill specified input fields on a page with given values. + /// + /// Dictionary with html element ids and values to input as field value. + /// Async task. + public async Task FillInputFields(Dictionary? fieldValues = null) + { + var inputFields = page.Locator("input"); + var count = await inputFields.CountAsync(); + for (int i = 0; i < count; i++) + { + var input = inputFields.Nth(i); + var inputId = await input.GetAttributeAsync("id"); + + // If fieldValues dictionary is provided and the inputId is found in it, fill the input with the value. + if (inputId is not null && fieldValues is not null && fieldValues.TryGetValue(inputId, out var fieldValue)) + { + await input.FillAsync(fieldValue); + } + } + } + + /// + /// Helper method to fill all empty input fields on a page with random data if not provided. + /// + /// Async task. + public async Task FillEmptyInputFieldsWithRandom() + { + var inputFields = page.Locator("input"); + var count = await inputFields.CountAsync(); + for (int i = 0; i < count; i++) + { + var input = inputFields.Nth(i); + var inputType = await input.GetAttributeAsync("type"); + + // If is not empty, skip. + if (!string.IsNullOrEmpty(await input.InputValueAsync())) + { + continue; + } + + // Generate appropriate random data based on input type. + string randomData = inputType switch + { + "email" => GenerateRandomEmail(), + "number" => GenerateRandomNumber(), + "password" => GenerateRandomPassword(), + _ => GenerateRandomString(), // Default for all other types. + }; + + await input.FillAsync(randomData); + } + } +} diff --git a/src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs b/src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs index 2cb49fc6c..b79162188 100644 --- a/src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs +++ b/src/Tests/AliasVault.E2ETests/Common/PlaywrightTest.cs @@ -54,6 +54,11 @@ public class PlaywrightTest /// protected IPage Page { get; private set; } + /// + /// Gets the input helper for Playwright tests. + /// + protected PlaywrightInputHelper InputHelper { get; private set; } = null!; + /// /// One time setup for the Playwright test which runs before all tests in the class. /// @@ -97,6 +102,7 @@ public class PlaywrightTest }); Page = await Context.NewPageAsync(); + InputHelper = new(Page); // Register a new account via the UI await Register(); diff --git a/src/Tests/AliasVault.E2ETests/AliasTests.cs b/src/Tests/AliasVault.E2ETests/Tests/AliasTests.cs similarity index 57% rename from src/Tests/AliasVault.E2ETests/AliasTests.cs rename to src/Tests/AliasVault.E2ETests/Tests/AliasTests.cs index 900c77211..024dc6f75 100644 --- a/src/Tests/AliasVault.E2ETests/AliasTests.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/AliasTests.cs @@ -5,7 +5,7 @@ // //----------------------------------------------------------------------- -namespace AliasVault.E2ETests; +namespace AliasVault.E2ETests.Tests; /// /// End-to-end tests for the alias management. @@ -75,8 +75,7 @@ public class AliasTests : PlaywrightTest // Replace the service name with "Alias service after". var serviceNameAfter = "Alias service after"; - await FillInputFields( - page: Page, + await InputHelper.FillInputFields( fieldValues: new Dictionary { { "service-name", serviceNameAfter }, @@ -91,86 +90,6 @@ public class AliasTests : PlaywrightTest Assert.That(pageContent, Does.Contain(serviceNameAfter), "Alias not updated correctly."); } - /// - /// Helper method to fill specified input fields on a page with given values. - /// - /// IPage instance where to fill the input fields for. - /// Dictionary with html element ids and values to input as field value. - /// Async task. - private static async Task FillInputFields(IPage page, Dictionary? fieldValues = null) - { - var inputFields = page.Locator("input"); - var count = await inputFields.CountAsync(); - for (int i = 0; i < count; i++) - { - var input = inputFields.Nth(i); - var inputId = await input.GetAttributeAsync("id"); - - // If fieldValues dictionary is provided and the inputId is found in it, fill the input with the value. - if (inputId is not null && fieldValues is not null && fieldValues.TryGetValue(inputId, out var fieldValue)) - { - await input.FillAsync(fieldValue); - } - } - } - - /// - /// Helper method to fill all empty input fields on a page with random data if not provided. - /// - /// IPage instance where to fill the input fields for. - /// Async task. - private static async Task FillEmptyInputFieldsWithRandom(IPage page) - { - var inputFields = page.Locator("input"); - var count = await inputFields.CountAsync(); - for (int i = 0; i < count; i++) - { - var input = inputFields.Nth(i); - var inputType = await input.GetAttributeAsync("type"); - - // If is not empty, skip. - if (!string.IsNullOrEmpty(await input.InputValueAsync())) - { - continue; - } - - // Generate appropriate random data based on input type. - string randomData = inputType switch - { - "email" => GenerateRandomEmail(), - "number" => GenerateRandomNumber(), - "password" => GenerateRandomPassword(), - _ => GenerateRandomString(), // Default for all other types. - }; - - await input.FillAsync(randomData); - } - } - - private static string GenerateRandomString(int length = 10) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[Random.Next(s.Length)]).ToArray()); - } - - private static string GenerateRandomEmail() - { - return $"{GenerateRandomString(5)}@example.com"; - } - - private static string GenerateRandomNumber() - { - return Random.Next(0, 10000).ToString(); - } - - private static string GenerateRandomPassword(int length = 12) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"; - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[Random.Next(s.Length)]).ToArray()); - } - /// /// Create new alias. /// @@ -186,8 +105,8 @@ public class AliasTests : PlaywrightTest Assert.That(generateButton, Is.Not.Null, "Generate button not found."); // Fill all input fields with specified values and remaining empty fields with random data. - await FillInputFields(Page, formValues); - await FillEmptyInputFieldsWithRandom(Page); + await InputHelper.FillInputFields(formValues); + await InputHelper.FillEmptyInputFieldsWithRandom(); var submitButton = Page.Locator("text=Save Alias").First; await submitButton.ClickAsync(); diff --git a/src/Tests/AliasVault.E2ETests/AuthTests.cs b/src/Tests/AliasVault.E2ETests/Tests/AuthTests.cs similarity index 98% rename from src/Tests/AliasVault.E2ETests/AuthTests.cs rename to src/Tests/AliasVault.E2ETests/Tests/AuthTests.cs index aa5ccb1a5..9d721c4da 100644 --- a/src/Tests/AliasVault.E2ETests/AuthTests.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/AuthTests.cs @@ -5,7 +5,7 @@ // //----------------------------------------------------------------------- -namespace AliasVault.E2ETests; +namespace AliasVault.E2ETests.Tests; /// /// End-to-end tests for authentication. diff --git a/src/Tests/AliasVault.E2ETests/Tests/UnlockTests.cs b/src/Tests/AliasVault.E2ETests/Tests/UnlockTests.cs new file mode 100644 index 000000000..1b145ed82 --- /dev/null +++ b/src/Tests/AliasVault.E2ETests/Tests/UnlockTests.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.E2ETests.Tests; + +/// +/// End-to-end tests for the database unlock functionality. +/// +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class UnlockTests : PlaywrightTest +{ + private static readonly Random Random = new(); + + /// + /// Test that the unlock page is displayed after hard refresh which should + /// clear the encryption key from memory. + /// + /// Async task. + [Test] + public async Task UnlockPageTest() + { + // Soft navigate to "aliases" page to test the unlock redirect. + var startUrl = "aliases"; + await NavigateUsingBlazorRouter(startUrl); + + // Hard refresh the page. + await Page.ReloadAsync(); + + // Check if the unlock page is displayed. + await WaitForURLAsync("**/unlock", "unlock"); + + // Check if by entering password the unlock page is replaced by the alias listing page. + await InputHelper.FillInputFields(new Dictionary + { + { "password", TestUserPassword }, + }); + + var submitButton = Page.GetByRole(AriaRole.Button, new() { Name = "Unlock" }); + await submitButton.ClickAsync(); + + // Check if we get redirected back to the page we were trying to access. + await WaitForURLAsync("**/" + startUrl, "AliasVault"); + } +}