diff --git a/apps/server/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj b/apps/server/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj
index a6eb0e3af..d3f2ad8c9 100644
--- a/apps/server/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj
+++ b/apps/server/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj
@@ -29,6 +29,8 @@
+
+
diff --git a/apps/server/Tests/AliasVault.E2ETests/Common/PlaywrightInputHelper.cs b/apps/server/Tests/AliasVault.E2ETests/Common/PlaywrightInputHelper.cs
index a78a5f0f5..e894931c9 100644
--- a/apps/server/Tests/AliasVault.E2ETests/Common/PlaywrightInputHelper.cs
+++ b/apps/server/Tests/AliasVault.E2ETests/Common/PlaywrightInputHelper.cs
@@ -88,6 +88,21 @@ public class PlaywrightInputHelper(IPage page)
// 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))
{
+ // For service-url fields, check if the input already contains a prefilled "https://"
+ // If it does, and our value also starts with "https://", clear the field first to avoid "https://https://..." duplication
+ // Playwright's FillAsync doesn't clear the field by default if there's already a value
+ if (inputId.StartsWith("service-url-"))
+ {
+ var currentValue = await input.InputValueAsync();
+
+ // If the current value is just "https://" (the prefill) and our value starts with "https://",
+ // we need to clear first, otherwise FillAsync appends and we get "https://https://..."
+ if (!string.IsNullOrEmpty(currentValue) && currentValue == "https://" && fieldValue.StartsWith("https://"))
+ {
+ await input.ClearAsync();
+ }
+ }
+
await input.FillAsync(fieldValue);
}
}
diff --git a/apps/server/Tests/AliasVault.E2ETests/TestData/README.md b/apps/server/Tests/AliasVault.E2ETests/TestData/README.md
index 69914d3be..cd87a0123 100644
--- a/apps/server/Tests/AliasVault.E2ETests/TestData/README.md
+++ b/apps/server/Tests/AliasVault.E2ETests/TestData/README.md
@@ -4,5 +4,6 @@ using the `AliasVault.UnitTests.ResourceReaderUtility` class.
Index:
- `AliasClientDb_encrypted_base64_1.0.0` - Encrypted vault blob with client db version 1.0.0 used to test client db upgrade paths. This vault contains two test credentials that are checked in the tests after local client db upgrade.
-- `TestVault.avux` - .Avux file with predetermined set of credentials, used to test if Avux import works as expected.
+- `TestVault.avux` - Unencrypted .avux file with predetermined set of credentials, used to test if .avux import works as expected. Generated by `GenerateAvuxTestFile` test.
+- `TestVault.avex` - Encrypted .avex file with the same credentials as TestVault.avux, encrypted with password "testexportpass123". Used to test if .avex import with password decryption works as expected. Generated by `GenerateAvuxTestFile` test.
- `TestAttachment.txt` - Test attachment file that is uploaded during test.
diff --git a/apps/server/Tests/AliasVault.E2ETests/TestData/TestVault.avex b/apps/server/Tests/AliasVault.E2ETests/TestData/TestVault.avex
new file mode 100644
index 000000000..4074668c2
Binary files /dev/null and b/apps/server/Tests/AliasVault.E2ETests/TestData/TestVault.avex differ
diff --git a/apps/server/Tests/AliasVault.E2ETests/TestData/TestVault.avux b/apps/server/Tests/AliasVault.E2ETests/TestData/TestVault.avux
index 25906bbd9..c97074f0e 100644
Binary files a/apps/server/Tests/AliasVault.E2ETests/TestData/TestVault.avux and b/apps/server/Tests/AliasVault.E2ETests/TestData/TestVault.avux differ
diff --git a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/AvexExportImportTests.cs b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/AvexExportImportTests.cs
new file mode 100644
index 000000000..7478f2c58
--- /dev/null
+++ b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/AvexExportImportTests.cs
@@ -0,0 +1,154 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) aliasvault. All rights reserved.
+// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+namespace AliasVault.E2ETests.Tests.Client.Shard3;
+
+///
+/// End-to-end tests for importing vault data using the .avex (encrypted) format.
+/// These tests use a pre-generated .avex file to ensure backward compatibility
+/// and that encrypted exports continue to work with new versions.
+///
+[Parallelizable(ParallelScope.Self)]
+[Category("ClientTests")]
+[TestFixture]
+public class AvexExportImportTests : VaultImportTestsBase
+{
+ ///
+ /// Gets the test password used for .avex encryption (must match the password used in GenerateAvuxAvexTestFile test).
+ ///
+ private const string TestAvexPassword = "testexportpass123";
+
+ ///
+ /// Test that importing an encrypted .avex file works correctly and all data is preserved.
+ /// This test uses a pre-generated .avex file (TestVault.avex) that contains the same items as TestVault.avux,
+ /// but encrypted with a password. This ensures backward compatibility with existing .avex exports
+ /// and validates the password-based decryption flow.
+ ///
+ /// Async task.
+ [Test]
+ [Order(1)]
+ public async Task ImportAvexFile()
+ {
+ // Get the .avex test file from embedded resources
+ var avexBytes = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync("AliasVault.E2ETests.TestData.TestVault.avex");
+
+ Assert.That(avexBytes, Is.Not.Null);
+ Assert.That(avexBytes.Length, Is.GreaterThan(0), ".avex file should not be empty");
+
+ // Navigate to import/export settings page
+ await NavigateUsingBlazorRouter("settings/import-export");
+ await WaitForUrlAsync("settings/import-export", "Import / Export");
+
+ // Click on the AliasVault import card
+ await Page.ClickAsync("[data-import-service='AliasVault']");
+
+ // Wait for modal to appear by waiting for the file input
+ await Page.WaitForSelectorAsync("input[type='file']", new() { State = WaitForSelectorState.Visible });
+
+ // Create a temporary file with the .avex content
+ var tempFilePath = Path.Combine(Path.GetTempPath(), $"test-import-{Guid.NewGuid()}.avex");
+ await File.WriteAllBytesAsync(tempFilePath, avexBytes);
+
+ try
+ {
+ // Set the file input using the temporary file
+ var fileInput = Page.Locator("input[type='file']");
+ await fileInput.SetInputFilesAsync(tempFilePath);
+
+ // Wait for password input to appear (encrypted .avex requires password)
+ await Page.WaitForSelectorAsync("input[type='password']", new() { State = WaitForSelectorState.Visible, Timeout = 10000 });
+
+ // Enter the decryption password
+ await Page.FillAsync("input[type='password']", TestAvexPassword);
+
+ // Click Decrypt/Next button
+ await Page.ClickAsync("button:has-text('Decrypt')");
+
+ // Wait for the verification screen to load
+ await Page.WaitForSelectorAsync("button:has-text('Next')", new() { State = WaitForSelectorState.Visible, Timeout = 10000 });
+
+ // Click Next in the verify screen
+ await Page.ClickAsync("text=Next");
+
+ // Wait for Import button to be visible
+ await Page.WaitForSelectorAsync("button:has-text('Import')");
+
+ // Click Import button to import the items
+ await Page.ClickAsync("button:has-text('Import')");
+ await Page.WaitForSelectorAsync("text=Successfully imported");
+
+ // Verify all items were imported using shared verification logic
+ await VerifyImportedItems();
+ }
+ finally
+ {
+ // Cleanup: delete the temporary file
+ if (File.Exists(tempFilePath))
+ {
+ File.Delete(tempFilePath);
+ }
+ }
+ }
+
+ ///
+ /// Test that importing a .avex file with an incorrect password fails gracefully.
+ ///
+ /// Async task.
+ [Test]
+ [Order(2)]
+ public async Task ImportAvexFileWithWrongPassword()
+ {
+ // Get the .avex test file from embedded resources
+ var avexBytes = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync("AliasVault.E2ETests.TestData.TestVault.avex");
+
+ // Navigate to import/export settings page
+ await NavigateUsingBlazorRouter("settings/import-export");
+ await WaitForUrlAsync("settings/import-export", "Import / Export");
+
+ // Click on the AliasVault import card
+ await Page.ClickAsync("[data-import-service='AliasVault']");
+
+ // Wait for modal to appear by waiting for the file input
+ await Page.WaitForSelectorAsync("input[type='file']", new() { State = WaitForSelectorState.Visible });
+
+ // Create a temporary file with the .avex content
+ var tempFilePath = Path.Combine(Path.GetTempPath(), $"test-import-{Guid.NewGuid()}.avex");
+ await File.WriteAllBytesAsync(tempFilePath, avexBytes);
+
+ try
+ {
+ // Set the file input using the temporary file
+ var fileInput = Page.Locator("input[type='file']");
+ await fileInput.SetInputFilesAsync(tempFilePath);
+
+ // Wait for password input to appear
+ await Page.WaitForSelectorAsync("input[type='password']", new() { State = WaitForSelectorState.Visible, Timeout = 10000 });
+
+ // Enter an INCORRECT password
+ await Page.FillAsync("input[type='password']", "wrongpassword123");
+
+ // Click Decrypt button
+ await Page.ClickAsync("button:has-text('Decrypt')");
+
+ // Wait for error message to appear
+ await Page.WaitForTimeoutAsync(2000); // Give some time for error to show
+ var pageContent = await Page.TextContentAsync("body");
+ Assert.That(
+ pageContent,
+ Does.Contain("password").Or.Contains("incorrect").Or.Contains("decrypt").Or.Contains("failed"),
+ "Should show error message for incorrect password");
+ }
+ finally
+ {
+ // Cleanup: delete the temporary file
+ if (File.Exists(tempFilePath))
+ {
+ File.Delete(tempFilePath);
+ }
+ }
+ }
+}
diff --git a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/AvuxExportImportTests.cs b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/AvuxExportImportTests.cs
index 668255273..298ebde2f 100644
--- a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/AvuxExportImportTests.cs
+++ b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/AvuxExportImportTests.cs
@@ -10,17 +10,17 @@ namespace AliasVault.E2ETests.Tests.Client.Shard3;
using System.IO.Compression;
///
-/// End-to-end tests for importing vault data using the .avux format.
+/// End-to-end tests for importing vault data using the .avux (unencrypted) format.
/// These tests use a pre-generated .avux file to ensure backward compatibility
-/// and that old exports continue to work with new versions.
+/// and that unencrypted exports continue to work with new versions.
///
[Parallelizable(ParallelScope.Self)]
[Category("ClientTests")]
[TestFixture]
-public class AvuxExportImportTests : ClientPlaywrightTest
+public class AvuxExportImportTests : VaultImportTestsBase
{
///
- /// Test that importing a .avux file works correctly and all data is preserved.
+ /// Test that importing an unencrypted .avux file works correctly and all data is preserved.
/// This test uses a pre-generated .avux file (TestVault.avux) that contains:
/// - Basic login credential (username, password, URL, notes)
/// - Login with 2FA/TOTP
@@ -37,8 +37,7 @@ public class AvuxExportImportTests : ClientPlaywrightTest
public async Task ImportAvuxFile()
{
// Get the .avux test file from embedded resources
- var avuxBytes = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync(
- "AliasVault.E2ETests.TestData.TestVault.avux");
+ var avuxBytes = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync("AliasVault.E2ETests.TestData.TestVault.avux");
// Verify the .avux file structure before importing
VerifyAvuxFileStructure(avuxBytes);
@@ -73,7 +72,7 @@ public class AvuxExportImportTests : ClientPlaywrightTest
await Page.ClickAsync("button:has-text('Import')");
await Page.WaitForSelectorAsync("text=Successfully imported");
- // Verify all items were imported
+ // Verify all items were imported using shared verification logic
await VerifyImportedItems();
}
finally
@@ -110,280 +109,4 @@ public class AvuxExportImportTests : ClientPlaywrightTest
Assert.That(manifestJson, Does.Contain("\"items\""), "manifest should contain items");
Assert.That(manifestJson, Does.Contain("\"folders\""), "manifest should contain folders");
}
-
- ///
- /// Verifies that all expected items from the .avux file were imported correctly.
- ///
- private async Task VerifyImportedItems()
- {
- // Navigate to items page
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
-
- // Wait for items to load
- await Page.WaitForTimeoutAsync(1000);
-
- var pageContent = await Page.TextContentAsync("body");
-
- // Verify all expected items are present
- var expectedItems = new Dictionary
- {
- { "Basic Login Test", "Basic login credential" },
- { "Login with 2FA", "Login with 2FA/TOTP" },
- { "Login with Attachment", "Login with file attachment" },
- { "Test Credit Card", "Credit card entry" },
- { "Test Secure Note", "Secure note entry" },
- { "Multi-URL Login", "Credential with multiple URLs" },
- };
-
- foreach (var (itemName, description) in expectedItems)
- {
- Assert.That(pageContent, Does.Contain(itemName), $"{description} should be imported");
- }
-
- // Verify the folder was imported
- Assert.That(pageContent, Does.Contain("Test Folder"), "Test Folder should be imported");
-
- // Verify individual items in detail
- await VerifyBasicLoginTest();
- await VerifyLoginWith2FA();
- await VerifyLoginWithAttachment();
- await VerifyCreditCard();
- await VerifySecureNote();
- await VerifyMultiUrlLogin();
- await VerifyCredentialInFolder();
-
- // Verify that timestamps were preserved and items appear in creation order
- await VerifyItemsOrderPreserved();
- }
-
- ///
- /// Verifies the "Basic Login Test" item details.
- ///
- private async Task VerifyBasicLoginTest()
- {
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
- await Page.ClickAsync("text=Basic Login Test");
-
- // Wait for navigation to item view page
- await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
- await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- // Get input values - fields are displayed in readonly inputs on view page
- var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
- var pageContent = await Page.TextContentAsync("body");
-
- Assert.Multiple(() =>
- {
- Assert.That(usernameValue, Is.EqualTo("testuser"), "Username should be preserved");
- Assert.That(pageContent, Does.Contain("https://example.com"), "URL should be preserved");
- Assert.That(pageContent, Does.Contain("This is a test note for basic login"), "Notes should be preserved");
- });
- }
-
- ///
- /// Verifies the "Login with 2FA" item details.
- ///
- private async Task VerifyLoginWith2FA()
- {
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
- await Page.ClickAsync("text=Login with 2FA");
-
- await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
- await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
- var pageContent = await Page.TextContentAsync("body");
-
- Assert.Multiple(() =>
- {
- Assert.That(usernameValue, Is.EqualTo("user2fa"), "2FA username should be preserved");
- Assert.That(
- pageContent,
- Does.Contain("Test TOTP").Or.Contains("Two-Factor").Or.Contains("2FA").Or.Contains("TOTP"),
- "TOTP should be preserved");
- });
- }
-
- ///
- /// Verifies the "Login with Attachment" item details.
- ///
- private async Task VerifyLoginWithAttachment()
- {
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
- await Page.ClickAsync("text=Login with Attachment");
-
- await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
- await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
- var pageContent = await Page.TextContentAsync("body");
-
- Assert.Multiple(() =>
- {
- Assert.That(usernameValue, Is.EqualTo("userattachment"), "Attachment username should be preserved");
- Assert.That(
- pageContent,
- Does.Contain("test-attachment.txt").Or.Contains("Attachment"),
- "Attachment should be preserved");
- });
- }
-
- ///
- /// Verifies the "Test Credit Card" item details.
- ///
- private async Task VerifyCreditCard()
- {
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
- await Page.ClickAsync("text=Test Credit Card");
-
- await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
- await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- // Read credit card field values - View page uses full field keys with dots replaced by hyphens
- var cardNumberValue = await Page.Locator("input#card-number").InputValueAsync();
- var cardholderValue = await Page.Locator("input#card-cardholder_name").InputValueAsync();
- var expiryMonthValue = await Page.Locator("input#card-expiry_month").InputValueAsync();
- var expiryYearValue = await Page.Locator("input#card-expiry_year").InputValueAsync();
- var cvvValue = await Page.Locator("input#card-cvv").InputValueAsync();
- var pinValue = await Page.Locator("input#card-pin").InputValueAsync();
-
- Assert.Multiple(() =>
- {
- Assert.That(cardNumberValue, Is.EqualTo("4111111111111111"), "Card number should be preserved");
- Assert.That(cardholderValue, Is.EqualTo("Test Cardholder"), "Cardholder name should be preserved");
- Assert.That(expiryMonthValue, Is.EqualTo("12"), "Expiry month should be preserved");
- Assert.That(expiryYearValue, Is.EqualTo("2025"), "Expiry year should be preserved");
- Assert.That(cvvValue, Is.EqualTo("123"), "CVV should be preserved");
- Assert.That(pinValue, Is.EqualTo("1234"), "PIN should be preserved");
- });
- }
-
- ///
- /// Verifies the "Test Secure Note" item details.
- ///
- private async Task VerifySecureNote()
- {
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
- await Page.ClickAsync("text=Test Secure Note");
-
- await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
- await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- var pageContent = await Page.TextContentAsync("body");
- Assert.That(
- pageContent,
- Does.Contain("secure note").Or.Contains("important information"),
- "Note content should be preserved");
- }
-
- ///
- /// Verifies the "Multi-URL Login" item details.
- ///
- private async Task VerifyMultiUrlLogin()
- {
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
- await Page.ClickAsync("text=Multi-URL Login");
-
- await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
- await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- // Read username from input and URLs from page body (URLs are displayed as text, not input fields)
- var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
- var pageContent = await Page.TextContentAsync("body");
-
- Assert.Multiple(() =>
- {
- Assert.That(usernameValue, Is.EqualTo("multiurluser"), "Multi-URL username should be preserved");
- Assert.That(pageContent, Does.Contain("app.example.com"), "First URL should be preserved");
- Assert.That(pageContent, Does.Contain("www.example.com"), "Second URL should be preserved");
- Assert.That(pageContent, Does.Contain("admin.example.com"), "Third URL should be preserved");
- });
- }
-
- ///
- /// Verifies the "Credential in Folder" item details.
- ///
- private async Task VerifyCredentialInFolder()
- {
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
-
- // Navigate to the folder first
- await Page.ClickAsync("text=Test Folder");
- await Page.WaitForTimeoutAsync(500);
-
- // Now click on the credential within the folder
- await Page.ClickAsync("text=Credential in Folder");
-
- await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
- await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
-
- var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
- Assert.That(usernameValue, Is.EqualTo("folderuser"), "Folder credential username should be preserved");
- }
-
- ///
- /// Verifies that imported items appear in the correct order based on their creation timestamps.
- /// The web app uses "oldest first" sorting by default, so we verify that the first few items
- /// appear in the order they were originally created.
- ///
- private async Task VerifyItemsOrderPreserved()
- {
- await NavigateUsingBlazorRouter("items");
- await WaitForUrlAsync("items", "Find all of your items");
-
- // Wait for items to load
- await Page.WaitForTimeoutAsync(1000);
-
- // Get all item cards in the order they appear on the page
- // Items are displayed in cards with the service name
- var itemCards = await Page.Locator("[data-testid='item-card'], .item-card, [class*='item']").AllAsync();
-
- // Get text content from the page to find item positions
- var pageContent = await Page.TextContentAsync("body");
-
- // The expected order based on creation timestamps in the test data
- // These are the first 4 items that were created in the GenerateComprehensiveAvuxTestFile test
- var expectedOrder = new[]
- {
- "Basic Login Test",
- "Login with 2FA",
- "Login with Attachment",
- "Test Credit Card",
- };
-
- // Find the positions of each expected item in the page content
- var positions = new List<(string ItemName, int Position)>();
- foreach (var itemName in expectedOrder)
- {
- var position = pageContent?.IndexOf(itemName) ?? -1;
- if (position >= 0)
- {
- positions.Add((itemName, position));
- }
- }
-
- // Verify we found all expected items
- Assert.That(positions.Count, Is.EqualTo(expectedOrder.Length), "Not all expected items were found on the page");
-
- // Verify the items appear in the expected order (oldest first)
- // Each item should appear before the next one in the list
- for (int i = 0; i < positions.Count - 1; i++)
- {
- var currentItem = positions[i];
- var nextItem = positions[i + 1];
-
- var errorMessage = $"Item '{currentItem.ItemName}' should appear before '{nextItem.ItemName}' in oldest-first order. " +
- $"This indicates that timestamps from the .avux import were not preserved correctly.";
-
- Assert.That(currentItem.Position, Is.LessThan(nextItem.Position), errorMessage);
- }
- }
}
diff --git a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/VaultImportTestsBase.cs b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/VaultImportTestsBase.cs
new file mode 100644
index 000000000..c8113525f
--- /dev/null
+++ b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/VaultImportTestsBase.cs
@@ -0,0 +1,300 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) aliasvault. All rights reserved.
+// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+namespace AliasVault.E2ETests.Tests.Client.Shard3;
+
+///
+/// Base class for vault import tests that provides shared verification methods.
+/// Both .avux and .avex import tests can inherit from this to reuse verification logic.
+///
+public abstract class VaultImportTestsBase : ClientPlaywrightTest
+{
+ ///
+ /// Verifies that all expected items from the vault file were imported correctly.
+ ///
+ /// Async task.
+ protected async Task VerifyImportedItems()
+ {
+ // Navigate to items page
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+
+ // Wait for items to load
+ await Page.WaitForTimeoutAsync(1000);
+
+ var pageContent = await Page.TextContentAsync("body");
+
+ // Verify all expected items are present
+ var expectedItems = new Dictionary
+ {
+ { "Basic Login Test", "Basic login credential" },
+ { "Login with 2FA", "Login with 2FA/TOTP" },
+ { "Login with Attachment", "Login with file attachment" },
+ { "Test Credit Card", "Credit card entry" },
+ { "Test Secure Note", "Secure note entry" },
+ { "Multi-URL Login", "Credential with multiple URLs" },
+ };
+
+ foreach (var (itemName, description) in expectedItems)
+ {
+ Assert.That(pageContent, Does.Contain(itemName), $"{description} should be imported");
+ }
+
+ // Verify the folder was imported
+ Assert.That(pageContent, Does.Contain("Test Folder"), "Test Folder should be imported");
+
+ // Verify individual items in detail
+ await VerifyBasicLoginTest();
+ await VerifyLoginWith2FA();
+ await VerifyLoginWithAttachment();
+ await VerifyCreditCard();
+ await VerifySecureNote();
+ await VerifyMultiUrlLogin();
+ await VerifyCredentialInFolder();
+
+ // Verify that timestamps were preserved and items appear in creation order
+ await VerifyItemsOrderPreserved();
+ }
+
+ ///
+ /// Verifies the "Basic Login Test" item details.
+ ///
+ /// Async task.
+ protected async Task VerifyBasicLoginTest()
+ {
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+ await Page.ClickAsync("text=Basic Login Test");
+
+ // Wait for navigation to item view page
+ await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
+ await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ // Get input values - fields are displayed in readonly inputs on view page
+ var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
+ var pageContent = await Page.TextContentAsync("body");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(usernameValue, Is.EqualTo("testuser"), "Username should be preserved");
+ Assert.That(pageContent, Does.Contain("https://google.com"), "URL should be preserved");
+ Assert.That(pageContent, Does.Contain("This is a test note for basic login"), "Notes should be preserved");
+ });
+ }
+
+ ///
+ /// Verifies the "Login with 2FA" item details.
+ ///
+ /// Async task.
+ protected async Task VerifyLoginWith2FA()
+ {
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+ await Page.ClickAsync("text=Login with 2FA");
+
+ await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
+ await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
+ var pageContent = await Page.TextContentAsync("body");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(usernameValue, Is.EqualTo("user2fa"), "2FA username should be preserved");
+ Assert.That(
+ pageContent,
+ Does.Contain("Test TOTP").Or.Contains("Two-Factor").Or.Contains("2FA").Or.Contains("TOTP"),
+ "TOTP should be preserved");
+ });
+ }
+
+ ///
+ /// Verifies the "Login with Attachment" item details.
+ ///
+ /// Async task.
+ protected async Task VerifyLoginWithAttachment()
+ {
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+ await Page.ClickAsync("text=Login with Attachment");
+
+ await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
+ await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
+ var pageContent = await Page.TextContentAsync("body");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(usernameValue, Is.EqualTo("userattachment"), "Attachment username should be preserved");
+ Assert.That(
+ pageContent,
+ Does.Contain("test-attachment.txt").Or.Contains("Attachment"),
+ "Attachment should be preserved");
+ });
+ }
+
+ ///
+ /// Verifies the "Test Credit Card" item details.
+ ///
+ /// Async task.
+ protected async Task VerifyCreditCard()
+ {
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+ await Page.ClickAsync("text=Test Credit Card");
+
+ await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
+ await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ // Read credit card field values - View page uses full field keys with dots replaced by hyphens
+ var cardNumberValue = await Page.Locator("input#card-number").InputValueAsync();
+ var cardholderValue = await Page.Locator("input#card-cardholder_name").InputValueAsync();
+ var expiryMonthValue = await Page.Locator("input#card-expiry_month").InputValueAsync();
+ var expiryYearValue = await Page.Locator("input#card-expiry_year").InputValueAsync();
+ var cvvValue = await Page.Locator("input#card-cvv").InputValueAsync();
+ var pinValue = await Page.Locator("input#card-pin").InputValueAsync();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(cardNumberValue, Is.EqualTo("4111111111111111"), "Card number should be preserved");
+ Assert.That(cardholderValue, Is.EqualTo("Test Cardholder"), "Cardholder name should be preserved");
+ Assert.That(expiryMonthValue, Is.EqualTo("12"), "Expiry month should be preserved");
+ Assert.That(expiryYearValue, Is.EqualTo("2025"), "Expiry year should be preserved");
+ Assert.That(cvvValue, Is.EqualTo("123"), "CVV should be preserved");
+ Assert.That(pinValue, Is.EqualTo("1234"), "PIN should be preserved");
+ });
+ }
+
+ ///
+ /// Verifies the "Test Secure Note" item details.
+ ///
+ /// Async task.
+ protected async Task VerifySecureNote()
+ {
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+ await Page.ClickAsync("text=Test Secure Note");
+
+ await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
+ await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ var pageContent = await Page.TextContentAsync("body");
+ Assert.That(
+ pageContent,
+ Does.Contain("secure note").Or.Contains("important information"),
+ "Note content should be preserved");
+ }
+
+ ///
+ /// Verifies the "Multi-URL Login" item details.
+ ///
+ /// Async task.
+ protected async Task VerifyMultiUrlLogin()
+ {
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+ await Page.ClickAsync("text=Multi-URL Login");
+
+ await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
+ await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ // Read username from input and URLs from page body (URLs are displayed as text, not input fields)
+ var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
+ var pageContent = await Page.TextContentAsync("body");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(usernameValue, Is.EqualTo("multiurluser"), "Multi-URL username should be preserved");
+ Assert.That(pageContent, Does.Contain("app.example.com"), "First URL should be preserved");
+ Assert.That(pageContent, Does.Contain("www.example.com"), "Second URL should be preserved");
+ Assert.That(pageContent, Does.Contain("admin.example.com"), "Third URL should be preserved");
+ });
+ }
+
+ ///
+ /// Verifies the "Credential in Folder" item details.
+ ///
+ /// Async task.
+ protected async Task VerifyCredentialInFolder()
+ {
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+
+ // Navigate to the folder first
+ await Page.ClickAsync("text=Test Folder");
+ await Page.WaitForTimeoutAsync(500);
+
+ // Now click on the credential within the folder
+ await Page.ClickAsync("text=Credential in Folder");
+
+ await Page.WaitForURLAsync("**/items/*", new() { Timeout = 5000 });
+ await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ var usernameValue = await Page.Locator("input#login-username").InputValueAsync();
+ Assert.That(usernameValue, Is.EqualTo("folderuser"), "Folder credential username should be preserved");
+ }
+
+ ///
+ /// Verifies that imported items appear in the correct order based on their creation timestamps.
+ /// The web app uses "oldest first" sorting by default, so we verify that the first few items
+ /// appear in the order they were originally created.
+ ///
+ /// Async task.
+ protected async Task VerifyItemsOrderPreserved()
+ {
+ await NavigateUsingBlazorRouter("items");
+ await WaitForUrlAsync("items", "Find all of your items");
+
+ // Wait for items to load
+ await Page.WaitForTimeoutAsync(1000);
+
+ // Get all item cards in the order they appear on the page
+ // Items are displayed in cards with the service name
+ var itemCards = await Page.Locator("[data-testid='item-card'], .item-card, [class*='item']").AllAsync();
+
+ // Get text content from the page to find item positions
+ var pageContent = await Page.TextContentAsync("body");
+
+ // The expected order based on creation timestamps in the test data
+ // These are the first 4 items that were created in the GenerateAvuxAvexTestFile test
+ var expectedOrder = new[]
+ {
+ "Basic Login Test",
+ "Login with 2FA",
+ "Login with Attachment",
+ "Test Credit Card",
+ };
+
+ // Find the positions of each expected item in the page content
+ var positions = new List<(string ItemName, int Position)>();
+ foreach (var itemName in expectedOrder)
+ {
+ var position = pageContent?.IndexOf(itemName) ?? -1;
+ if (position >= 0)
+ {
+ positions.Add((itemName, position));
+ }
+ }
+
+ // Verify we found all expected items
+ Assert.That(positions.Count, Is.EqualTo(expectedOrder.Length), "Not all expected items were found on the page");
+
+ // Verify the items appear in the expected order (oldest first)
+ // Each item should appear before the next one in the list
+ for (int i = 0; i < positions.Count - 1; i++)
+ {
+ var currentItem = positions[i];
+ var nextItem = positions[i + 1];
+
+ var errorMessage = $"Item '{currentItem.ItemName}' should appear before '{nextItem.ItemName}' in oldest-first order. " +
+ $"This indicates that timestamps from the import were not preserved correctly.";
+
+ Assert.That(currentItem.Position, Is.LessThan(nextItem.Position), errorMessage);
+ }
+ }
+}
diff --git a/apps/server/Tests/AliasVault.E2ETests/Tests/Extensions/TestVaultGeneratorTests.cs b/apps/server/Tests/AliasVault.E2ETests/Tests/Extensions/TestVaultGeneratorTests.cs
index 03819b7db..73f19bbd0 100644
--- a/apps/server/Tests/AliasVault.E2ETests/Tests/Extensions/TestVaultGeneratorTests.cs
+++ b/apps/server/Tests/AliasVault.E2ETests/Tests/Extensions/TestVaultGeneratorTests.cs
@@ -34,6 +34,11 @@ using Microsoft.EntityFrameworkCore;
[TestFixture]
public class TestVaultGeneratorTests : BrowserExtensionPlaywrightTest
{
+ ///
+ /// Gets the test password used for .avex encryption.
+ ///
+ private const string TestAvexPassword = "testexportpass123";
+
///
/// Gets or sets user email (override).
///
@@ -176,13 +181,13 @@ public class TestVaultGeneratorTests : BrowserExtensionPlaywrightTest
}
///
- /// Creates a comprehensive test vault with all item types and exports it to .avux format.
- /// This test should be run manually when you need to generate a new .avux test file.
- /// The generated .avux file can be used for backward compatibility testing.
+ /// Creates a test vault with all item types and exports it to .avux/.avex format.
+ /// This test should be run manually when you need to generate a new .avux/.avex test file.
+ /// The generated .avux and .avex files can be used for backward compatibility testing.
///
/// Async task.
[Test]
- public async Task GenerateComprehensiveAvuxTestFile()
+ public async Task GenerateAvuxAvexTestFile()
{
// 1. Create a basic login credential
await CreateItemEntry(new Dictionary
@@ -190,7 +195,7 @@ public class TestVaultGeneratorTests : BrowserExtensionPlaywrightTest
{ "service-name", "Basic Login Test" },
{ "username", "testuser" },
{ "password", "testpassword123" },
- { "service-url-0", "https://example.com" },
+ { "service-url-0", "https://google.com" },
{ "notes", "This is a test note for basic login" },
});
@@ -373,17 +378,21 @@ public class TestVaultGeneratorTests : BrowserExtensionPlaywrightTest
await NavigateUsingBlazorRouter("settings/import-export");
await WaitForUrlAsync("settings/import-export", "Import / Export");
- // Click the "Export Full Vault (.avux)" button
- var exportButton = Page.Locator("button").Filter(new() { HasText = "Export Full Vault (.avux)" });
+ // Click the "Export unencrypted vault (.avux)" button
+ var exportButton = Page.Locator("button").Filter(new() { HasText = "Export unencrypted vault (.avux)" });
await exportButton.ClickAsync();
- // Confirm the export warning
- await Page.WaitForSelectorAsync("button:has-text('Confirm')");
+ // Confirm the export warning in the confirmation modal
+ await Page.WaitForSelectorAsync("button:has-text('Confirm')", new() { State = WaitForSelectorState.Visible });
await Page.ClickAsync("button:has-text('Confirm')");
+ // Wait for the password confirmation modal to appear
+ await Page.WaitForSelectorAsync("input[type='password']", new() { State = WaitForSelectorState.Visible });
+
// Enter password for verification
- await Page.WaitForSelectorAsync("input[type='password']");
await Page.FillAsync("input[type='password']", TestUserPassword);
+
+ // Click the confirm button in the password modal
await Page.ClickAsync("button:has-text('Confirm')");
// Wait for download
@@ -409,8 +418,57 @@ public class TestVaultGeneratorTests : BrowserExtensionPlaywrightTest
var testDataAvuxPath = Path.Combine(testDataDir, "TestVault.avux");
File.Copy(avuxFilePath, testDataAvuxPath, overwrite: true);
- Console.WriteLine("\n=== AVUX TEST FILE GENERATION COMPLETE ===");
- Console.WriteLine("A .avux file has been generated with the following items:");
+ // Now export the same vault as .avex (encrypted)
+ Console.WriteLine("\n=== Starting .avex (encrypted) export ===");
+
+ // Navigate back to import/export page
+ await Page.BringToFrontAsync();
+ await NavigateUsingBlazorRouter("settings/import-export");
+ await WaitForUrlAsync("settings/import-export", "Import / Export");
+
+ // Click the "Export encrypted vault (.avex)" button
+ var exportAvexButton = Page.Locator("button").Filter(new() { HasText = "Export encrypted vault (.avex)" });
+ await exportAvexButton.ClickAsync();
+
+ // Confirm the export warning in the confirmation modal
+ await Page.WaitForSelectorAsync("button:has-text('Confirm')", new() { State = WaitForSelectorState.Visible });
+ await Page.ClickAsync("button:has-text('Confirm')");
+
+ // Wait for the master password confirmation modal to appear
+ await Page.WaitForSelectorAsync("input[type='password']", new() { State = WaitForSelectorState.Visible });
+
+ // Enter master password for verification
+ await Page.FillAsync("input[type='password']", TestUserPassword);
+
+ // Click the confirm button in the master password modal
+ await Page.ClickAsync("button:has-text('Confirm')");
+
+ // Wait for the export password modal to appear
+ await Page.WaitForSelectorAsync("input#exportPassword", new() { State = WaitForSelectorState.Visible, Timeout = 10000 });
+
+ // Enter export password
+ await Page.FillAsync("input#exportPassword", TestAvexPassword);
+
+ // Enter export password confirmation
+ await Page.FillAsync("input#confirmExportPassword", TestAvexPassword);
+
+ // Click the "Create Encrypted Export" button in the export password modal
+ await Page.ClickAsync("button:has-text('Create Encrypted Export')");
+
+ // Wait for download
+ var avexDownloadPromise = Page.WaitForDownloadAsync();
+ var avexDownload = await avexDownloadPromise;
+
+ // Save .avex to output directory
+ var avexFilePath = Path.Combine(vaultOutputDir, "TestVault.avex");
+ await avexDownload.SaveAsAsync(avexFilePath);
+
+ // Also save to TestData directory
+ var testDataAvexPath = Path.Combine(testDataDir, "TestVault.avex");
+ File.Copy(avexFilePath, testDataAvexPath, overwrite: true);
+
+ Console.WriteLine("\n=== AVUX AND AVEX TEST FILE GENERATION COMPLETE ===");
+ Console.WriteLine("Both .avux and .avex files have been generated with the following items:");
Console.WriteLine("1. Basic Login Test - basic login credential with username, password, URL, notes");
Console.WriteLine("2. Login with 2FA - login with TOTP/2FA enabled");
Console.WriteLine("3. Login with Attachment - login with file attachment");
@@ -419,11 +477,16 @@ public class TestVaultGeneratorTests : BrowserExtensionPlaywrightTest
Console.WriteLine("6. Multi-URL Login - credential with multiple URLs");
Console.WriteLine("7. Credential in Folder - credential organized in 'Test Folder'");
Console.WriteLine("\nFiles saved to:");
- Console.WriteLine($" - Output directory: {avuxFilePath}");
- Console.WriteLine($" - TestData directory: {testDataAvuxPath}");
- Console.WriteLine("\nThis .avux file can now be used for:");
+ Console.WriteLine($" - Output directory (.avux): {avuxFilePath}");
+ Console.WriteLine($" - Output directory (.avex): {avexFilePath}");
+ Console.WriteLine($" - TestData directory (.avux): {testDataAvuxPath}");
+ Console.WriteLine($" - TestData directory (.avex): {testDataAvexPath}");
+ Console.WriteLine("\nTest Credentials:");
+ Console.WriteLine($" - Master Password: {TestUserPassword}");
+ Console.WriteLine($" - .avex Export Password: {TestAvexPassword}");
+ Console.WriteLine("\nThese files can now be used for:");
Console.WriteLine(" - Backward compatibility testing");
- Console.WriteLine(" - CI/CD automated import tests");
+ Console.WriteLine(" - CI/CD automated import tests (.avux unencrypted, .avex encrypted)");
Console.WriteLine(" - Validation that old exports work with new versions");
// Open file explorer at the output location