diff --git a/src/AliasVault.Client/Main/Components/Attachments/AttachmentUploader.razor b/src/AliasVault.Client/Main/Components/Attachments/AttachmentUploader.razor index 2fa24f1a1..733790c07 100644 --- a/src/AliasVault.Client/Main/Components/Attachments/AttachmentUploader.razor +++ b/src/AliasVault.Client/Main/Components/Attachments/AttachmentUploader.razor @@ -1,4 +1,5 @@ @using System.IO +@inject ILogger Logger
@@ -6,12 +7,12 @@ {

@StatusMessage

} - @if (Attachments.Any()) + @if (Attachments.Exists(x => !x.IsDeleted)) {

Attachments:

    - @foreach (var attachment in Attachments) + @foreach (var attachment in Attachments.Where(x => !x.IsDeleted)) {
  • @attachment.Filename @@ -42,9 +43,12 @@ /// Original attachments that were passed in. This is used to determine if a deleted attachment was part of the original set and /// can be hard deleted (did not exist in the original set) or should be soft deleted (was part of the original set). /// - private List OriginalAttachmentsIds = []; + private List OriginalAttachmentsIds { get; set; } = []; - private string StatusMessage = string.Empty; + /// + /// Status message to display. + /// + private string StatusMessage { get; set; } = string.Empty; /// protected override void OnInitialized() @@ -81,7 +85,7 @@ catch (Exception ex) { StatusMessage = $"Error uploading file: {ex.Message}"; - Console.Error.WriteLine("Error uploading file: {0}", ex.Message); + Logger.LogError(ex, "Error uploading file."); } } diff --git a/src/AliasVault.Client/Main/Components/Attachments/AttachmentViewer.razor b/src/AliasVault.Client/Main/Components/Attachments/AttachmentViewer.razor index af0f63698..e93751003 100644 --- a/src/AliasVault.Client/Main/Components/Attachments/AttachmentViewer.razor +++ b/src/AliasVault.Client/Main/Components/Attachments/AttachmentViewer.razor @@ -2,7 +2,7 @@

    Attachments

    - @if (Attachments.Any()) + @if (Attachments.Any(x => !x.IsDeleted)) {
    @@ -13,7 +13,7 @@ - @foreach (var attachment in Attachments) + @foreach (var attachment in Attachments.Where(x => !x.IsDeleted)) {
    diff --git a/src/AliasVault.Client/Main/Components/Forms/CopyPasteFormRow.razor b/src/AliasVault.Client/Main/Components/Forms/CopyPasteFormRow.razor index df1e5b499..45bd2763a 100644 --- a/src/AliasVault.Client/Main/Components/Forms/CopyPasteFormRow.razor +++ b/src/AliasVault.Client/Main/Components/Forms/CopyPasteFormRow.razor @@ -2,10 +2,10 @@ @inject JsInteropService JsInteropService @implements IDisposable - +
    - - @if (_copied) + + @if (Copied) { Copied! @@ -14,6 +14,12 @@
    @code { + /// + /// Id for the input field. Defaults to a random GUID if not provided. + /// + [Parameter] + public string Id { get; set; } = Guid.NewGuid().ToString(); + /// /// The label for the input. /// @@ -26,8 +32,7 @@ [Parameter] public string Value { get; set; } = string.Empty; - private bool _copied => ClipboardCopyService.GetCopiedId() == _inputId; - private readonly string _inputId = Guid.NewGuid().ToString(); + private bool Copied => ClipboardCopyService.GetCopiedId() == Id; /// protected override void OnInitialized() @@ -38,11 +43,11 @@ private async Task CopyToClipboard() { await JsInteropService.CopyToClipboard(Value); - ClipboardCopyService.SetCopied(_inputId); + ClipboardCopyService.SetCopied(Id); // After 2 seconds, reset the copied state if it's still the same element await Task.Delay(2000); - if (ClipboardCopyService.GetCopiedId() == _inputId) + if (ClipboardCopyService.GetCopiedId() == Id) { ClipboardCopyService.SetCopied(string.Empty); } diff --git a/src/AliasVault.Client/Main/Components/Forms/CopyPastePasswordFormRow.razor b/src/AliasVault.Client/Main/Components/Forms/CopyPastePasswordFormRow.razor index 1fef43d2c..a93f9dcd8 100644 --- a/src/AliasVault.Client/Main/Components/Forms/CopyPastePasswordFormRow.razor +++ b/src/AliasVault.Client/Main/Components/Forms/CopyPastePasswordFormRow.razor @@ -2,12 +2,12 @@ @inject JsInteropService JsInteropService @implements IDisposable - +
    - + - @if (_copied) + @if (Copied) { Copied! @@ -27,6 +27,12 @@
    @code { + /// + /// Id for the input field. Defaults to a random GUID if not provided. + /// + [Parameter] + public string Id { get; set; } = Guid.NewGuid().ToString(); + /// /// The label for the input. /// @@ -39,9 +45,9 @@ [Parameter] public string Value { get; set; } = string.Empty; - private bool _copied => ClipboardCopyService.GetCopiedId() == _inputId; - private readonly string _inputId = Guid.NewGuid().ToString(); - private bool _isPasswordVisible = false; + private bool Copied => ClipboardCopyService.GetCopiedId() == Id; + + private bool IsPasswordVisible { get; set; } /// protected override void OnInitialized() @@ -52,11 +58,11 @@ private async Task CopyToClipboard() { await JsInteropService.CopyToClipboard(Value); - ClipboardCopyService.SetCopied(_inputId); + ClipboardCopyService.SetCopied(Id); // After 2 seconds, reset the copied state if it's still the same element await Task.Delay(2000); - if (ClipboardCopyService.GetCopiedId() == _inputId) + if (ClipboardCopyService.GetCopiedId() == Id) { ClipboardCopyService.SetCopied(string.Empty); } @@ -64,7 +70,7 @@ private void TogglePasswordVisibility() { - _isPasswordVisible = !_isPasswordVisible; + IsPasswordVisible = !IsPasswordVisible; } private void HandleCopy(string copiedElementId) diff --git a/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor b/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor index 923539c22..e4e1735bb 100644 --- a/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor +++ b/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor @@ -159,7 +159,7 @@ } // No error, add success message. - GlobalNotificationService.AddSuccessMessage("Credentials created successfully."); + GlobalNotificationService.AddSuccessMessage("Credential created successfully."); NavigationManager.NavigateTo("/credentials/" + id); diff --git a/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor b/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor index efcb2592c..0e41ceb30 100644 --- a/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor +++ b/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor @@ -305,46 +305,6 @@ else Obj.Password.Value = CredentialService.GenerateRandomPassword(); } - private async Task SaveAlias() - { - GlobalLoadingSpinner.Show(); - StateHasChanged(); - - if (EditMode) - { - if (Id is not null) - { - Id = await CredentialService.UpdateEntryAsync(CredentialEditToCredential(Obj)); - } - } - else - { - Id = await CredentialService.InsertEntryAsync(CredentialEditToCredential(Obj)); - } - - GlobalLoadingSpinner.Hide(); - StateHasChanged(); - - if (Id is null || Id == Guid.Empty) - { - // Error saving. - GlobalNotificationService.AddErrorMessage("Error saving credentials. Please try again.", true); - return; - } - - // No error, add success message. - if (EditMode) - { - GlobalNotificationService.AddSuccessMessage("Credentials updated successfully."); - } - else - { - GlobalNotificationService.AddSuccessMessage("Credentials created successfully."); - } - - NavigationManager.NavigateTo("/credentials/" + Id); - } - /// /// Helper method to convert a Credential object to a CredentialEdit object. /// @@ -390,7 +350,7 @@ else /// private Credential CredentialEditToCredential(CredentialEdit alias) { - var credential = new Credential() + var credential = new Credential { Id = alias.Id, Notes = alias.Notes, @@ -441,4 +401,47 @@ else await SaveAlias(); } + + /// + /// Save the alias to the database. + /// + private async Task SaveAlias() + { + GlobalLoadingSpinner.Show(); + StateHasChanged(); + + if (EditMode) + { + if (Id is not null) + { + Id = await CredentialService.UpdateEntryAsync(CredentialEditToCredential(Obj)); + } + } + else + { + Id = await CredentialService.InsertEntryAsync(CredentialEditToCredential(Obj)); + } + + GlobalLoadingSpinner.Hide(); + StateHasChanged(); + + if (Id is null || Id == Guid.Empty) + { + // Error saving. + GlobalNotificationService.AddErrorMessage("Error saving credentials. Please try again.", true); + return; + } + + // No error, add success message. + if (EditMode) + { + GlobalNotificationService.AddSuccessMessage("Credential updated successfully."); + } + else + { + GlobalNotificationService.AddSuccessMessage("Credential created successfully."); + } + + NavigationManager.NavigateTo("/credentials/" + Id); + } } diff --git a/src/AliasVault.Client/Main/Pages/Credentials/View.razor b/src/AliasVault.Client/Main/Pages/Credentials/View.razor index f2aa12008..95f6de64a 100644 --- a/src/AliasVault.Client/Main/Pages/Credentials/View.razor +++ b/src/AliasVault.Client/Main/Pages/Credentials/View.razor @@ -59,13 +59,13 @@ else
    - +
    - +
    - +
    diff --git a/src/AliasVault.Client/Services/CredentialService.cs b/src/AliasVault.Client/Services/CredentialService.cs index 23adaac54..7340341fc 100644 --- a/src/AliasVault.Client/Services/CredentialService.cs +++ b/src/AliasVault.Client/Services/CredentialService.cs @@ -228,25 +228,27 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService login.Service.UpdatedAt = DateTime.UtcNow; // Remove attachments that are no longer in the list - var existingAttachments = login.Attachments.ToList(); - foreach (var existingAttachment in existingAttachments) + var attachmentsToRemove = login.Attachments.Where(existingAttachment => + !loginObject.Attachments.Any(a => a.Id == existingAttachment.Id)).ToList(); + foreach (var attachmentToRemove in attachmentsToRemove) { - if (!loginObject.Attachments.Any(a => a.Id != Guid.Empty && a.Id == existingAttachment.Id)) - { - context.Entry(existingAttachment).State = EntityState.Deleted; - } + login.Attachments.Remove(attachmentToRemove); + context.Entry(attachmentToRemove).State = EntityState.Deleted; } - // Add new attachments + // Update existing attachments and add new ones foreach (var attachment in loginObject.Attachments) { - if (!login.Attachments.Any(a => attachment.Id != Guid.Empty && a.Id == attachment.Id)) + var existingAttachment = login.Attachments.FirstOrDefault(a => a.Id == attachment.Id); + if (existingAttachment != null) { - login.Attachments.Add(attachment); + // Update existing attachment + context.Entry(existingAttachment).CurrentValues.SetValues(attachment); } else { - context.Entry(attachment).State = EntityState.Modified; + // Add new attachment + login.Attachments.Add(attachment); } } diff --git a/src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj b/src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj index e5b9f6ae6..d98b671d2 100644 --- a/src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj +++ b/src/Tests/AliasVault.E2ETests/AliasVault.E2ETests.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs b/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs index 72703b63a..0ecee2b25 100644 --- a/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs +++ b/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs @@ -175,8 +175,9 @@ public class ClientPlaywrightTest : PlaywrightTest /// Create new credential entry. /// /// Dictionary with html element ids and values to input as field value. + /// Optional custom logic to execute after filling input fields. /// Async task. - protected async Task CreateCredentialEntry(Dictionary? formValues = null) + protected async Task CreateCredentialEntry(Dictionary? formValues = null, Func? customLogic = null) { // Advance the time by 1 second to ensure the credential is created with a unique timestamp. // This is required for certain tests that check for the latest credential and/or latest vault. @@ -193,9 +194,15 @@ public class ClientPlaywrightTest : PlaywrightTest await InputHelper.FillInputFields(formValues); await InputHelper.FillEmptyInputFieldsWithRandom(); + // Execute custom logic if provided + if (customLogic != null) + { + await customLogic(); + } + var submitButton = Page.Locator("text=Save Credentials").First; await submitButton.ClickAsync(); - await WaitForUrlAsync("credentials/**", "Credentials created successfully"); + await WaitForUrlAsync("credentials/**", "Credential created successfully"); // Check if the credential was created var pageContent = await Page.TextContentAsync("body"); @@ -230,11 +237,11 @@ public class ClientPlaywrightTest : PlaywrightTest var submitButton = Page.Locator("text=Save Credentials").First; await submitButton.ClickAsync(); - await WaitForUrlAsync("credentials/**", "Credentials updated successfully"); + await WaitForUrlAsync("credentials/**", "Credential updated successfully"); // Check if the credential was created var pageContent = await Page.TextContentAsync("body"); - Assert.That(pageContent, Does.Contain("Credentials updated successfully"), "Credential not updated successfully."); + Assert.That(pageContent, Does.Contain("Credential updated successfully"), "Credential not updated successfully."); } /// diff --git a/src/Tests/AliasVault.E2ETests/Common/ResourceReaderUtility.cs b/src/Tests/AliasVault.E2ETests/Common/ResourceReaderUtility.cs index 56d3a268e..ef477aac0 100644 --- a/src/Tests/AliasVault.E2ETests/Common/ResourceReaderUtility.cs +++ b/src/Tests/AliasVault.E2ETests/Common/ResourceReaderUtility.cs @@ -7,7 +7,9 @@ namespace AliasVault.E2ETests.Common; +using System.IO; using System.Reflection; +using System.Threading.Tasks; /// /// Utility for reading strings from project embedded resources used in tests. @@ -20,7 +22,7 @@ public static class ResourceReaderUtility /// Name of the embedded resource. /// Contents of embedded resource as string. /// Thrown when resource is not found with that name. - public static async Task ReadEmbeddedResourceAsync(string resourceName) + public static async Task ReadEmbeddedResourceStringAsync(string resourceName) { var assembly = Assembly.GetExecutingAssembly(); @@ -34,6 +36,27 @@ public static class ResourceReaderUtility return await reader.ReadToEndAsync(); } + /// + /// Reads byte array from embedded resource. + /// + /// Name of the embedded resource. + /// Contents of embedded resource as byte array. + /// Thrown when resource is not found with that name. + public static async Task ReadEmbeddedResourceBytesAsync(string resourceName) + { + var assembly = Assembly.GetExecutingAssembly(); + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + throw new InvalidOperationException($"Resource {resourceName} not found in {assembly.FullName}"); + } + + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + return memoryStream.ToArray(); + } + /// /// Get all embedded resource names in current assembly. /// diff --git a/src/Tests/AliasVault.E2ETests/TestData/README.md b/src/Tests/AliasVault.E2ETests/TestData/README.md index 5dd809950..e8feb543b 100644 --- a/src/Tests/AliasVault.E2ETests/TestData/README.md +++ b/src/Tests/AliasVault.E2ETests/TestData/README.md @@ -4,3 +4,4 @@ using the `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. +- `TestAttachment.txt` - Test attachment file that is uploaded during test. diff --git a/src/Tests/AliasVault.E2ETests/TestData/TestAttachment.txt b/src/Tests/AliasVault.E2ETests/TestData/TestAttachment.txt new file mode 100644 index 000000000..40081da24 --- /dev/null +++ b/src/Tests/AliasVault.E2ETests/TestData/TestAttachment.txt @@ -0,0 +1 @@ +Test attachment content diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/CredentialTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/CredentialTests.cs index dd5ea367a..741a6fd41 100644 --- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/CredentialTests.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/CredentialTests.cs @@ -116,7 +116,7 @@ public class CredentialTests : ClientPlaywrightTest await WaitForUrlAsync("credentials/**", "Delete"); pageContent = await Page.TextContentAsync("body"); - Assert.That(pageContent, Does.Contain("Credentials updated"), "Credential update confirmation message not shown."); + Assert.That(pageContent, Does.Contain("Credential updated"), "Credential update confirmation message not shown."); Assert.That(pageContent, Does.Contain(serviceNameAfter), "Credential not updated correctly."); } diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard2/DbUpgradeTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard2/DbUpgradeTests.cs index bf3920b1d..0b67517c7 100644 --- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard2/DbUpgradeTests.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard2/DbUpgradeTests.cs @@ -53,7 +53,7 @@ public class DbUpgradeTests : ClientPlaywrightTest UpdatedAt = DateTime.UtcNow, EncryptionType = "Argon2Id", EncryptionSettings = "{\"DegreeOfParallelism\":4,\"MemorySize\":8192,\"Iterations\":1}", - VaultBlob = await ResourceReaderUtility.ReadEmbeddedResourceAsync("AliasVault.E2ETests.TestData.AliasClientDb_encrypted_base64_1.0.0.txt"), + VaultBlob = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.E2ETests.TestData.AliasClientDb_encrypted_base64_1.0.0.txt"), Salt = "1a73a8ef3a1c6dd891674c415962d87246450f8ca5004ecca24be770a4d7b1f7", Verifier = "ab284d4e6da07a2bc95fb4b9dcd0e192988cc45f51e4c51605e42d4fc1055f8398e579755f4772a045abdbded8ae47ae861faa9ff7cb98155103d7038b9713b12d80dff9134067f02564230ab2f5a550ae293b8b7049516a7dc3f918156cde7190bee7e9c84398b2b5b63aeea763cd776b3e9708fb1f66884340451187ca8aacfced19ea28bc94ae28eefa720aae7a3185b139cf6349c2d43e8147f1edadd249c7e125ce15e775c45694d9796ee3f9b8c5beacd37e777a2ea1e745c781b5c085b7e3826f6abe303a14f539cd8d9519661a91cc4e7d44111b8bc9aac1cf1a51ad76658502b436da746844348dfcfb2581c4e4c340058c116a06f975f57a689df4", }); diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AttachmentTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AttachmentTests.cs new file mode 100644 index 000000000..821557a37 --- /dev/null +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AttachmentTests.cs @@ -0,0 +1,205 @@ +//----------------------------------------------------------------------- +// +// 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.Client.Shard5; + +/// +/// End-to-end tests for uploading and downloading attachments. +/// +[Parallelizable(ParallelScope.Self)] +[Category("ClientTests")] +[TestFixture] +public class AttachmentTests : ClientPlaywrightTest +{ + /// + /// Test that adding an attachment works correctly and can be downloaded afterwards. + /// + /// Async task. + [Test] + [Order(1)] + public async Task UploadAndDownloadAttachment() + { + // Create a new alias with service name = "Test Service". + var serviceName = "Test Service"; + await CreateCredentialEntry( + new Dictionary + { + { "service-name", serviceName }, + }, + async () => + { + // Upload file. + var fileInput = Page.Locator("input[type='file']"); + var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync("AliasVault.E2ETests.TestData.TestAttachment.txt"); + + // Create a temporary file with the content and original file name + var originalFileName = "TestAttachment.txt"; + var tempFilePath = Path.Combine(Path.GetTempPath(), originalFileName); + await File.WriteAllBytesAsync(tempFilePath, fileContent); + + // Set the file input using the temporary file + await fileInput.SetInputFilesAsync(tempFilePath); + + // Delete the temporary file + File.Delete(tempFilePath); + }); + + // Check that the attachment name appears on the alias page. + var pageContent = await Page.TextContentAsync("body"); + Assert.That(pageContent, Does.Contain("TestAttachment.txt"), "Uploaded attachment name does not appear on alias page."); + + // Download the attachment + var downloadPromise = Page.WaitForDownloadAsync(); + await Page.ClickAsync("text=TestAttachment.txt"); + var download = await downloadPromise; + + // Get the path of the downloaded file + var downloadedFilePath = await download.PathAsync(); + + // Read the content of the downloaded file + var downloadedContent = await File.ReadAllBytesAsync(downloadedFilePath); + + // Compare the downloaded content with the original file content + var originalContent = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync("AliasVault.E2ETests.TestData.TestAttachment.txt"); + Assert.That(downloadedContent, Is.EqualTo(originalContent), "Downloaded file content does not match the original file content."); + + // Clean up: delete the downloaded file + File.Delete(downloadedFilePath); + } + + /// + /// Test that updating a credential with an existing attachment works correctly. + /// + /// Async task. + [Test] + [Order(2)] + public async Task UpdateCredentialWithAttachment() + { + // Create a new alias with service name = "Test Service". + var serviceName = "Test Service"; + var initialUsername = "initialuser"; + await CreateCredentialEntry( + new Dictionary + { + { "service-name", serviceName }, + { "username", initialUsername }, + }, + async () => + { + // Upload file. + var fileInput = Page.Locator("input[type='file']"); + var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync("AliasVault.E2ETests.TestData.TestAttachment.txt"); + + // Create a temporary file with the content and original file name + var originalFileName = "TestAttachment.txt"; + var tempFilePath = Path.Combine(Path.GetTempPath(), originalFileName); + await File.WriteAllBytesAsync(tempFilePath, fileContent); + + // Set the file input using the temporary file + await fileInput.SetInputFilesAsync(tempFilePath); + + // Delete the temporary file + File.Delete(tempFilePath); + }); + + // Check that the attachment name appears on the alias page. + var pageContent = await Page.TextContentAsync("body"); + Assert.That(pageContent, Does.Contain("TestAttachment.txt"), "Uploaded attachment name does not appear on alias page."); + + // Update the credential + var updatedUsername = "updateduser"; + await UpdateCredentialEntry(serviceName, new Dictionary + { + { "username", updatedUsername }, + }); + + // Check that the updated username and attachment name still appear on the alias page. + var usernameElement = await Page.QuerySelectorAsync("#username"); + Assert.That(usernameElement, Is.Not.Null, "Username element not found."); + Assert.That(await usernameElement.InputValueAsync(), Is.EqualTo(updatedUsername), "Updated username does not appear on alias page."); + + pageContent = await Page.TextContentAsync("body"); + Assert.That(pageContent, Does.Contain("TestAttachment.txt"), "Attachment name does not appear on alias page after update."); + + // Download the attachment + var downloadPromise = Page.WaitForDownloadAsync(); + await Page.ClickAsync("text=TestAttachment.txt"); + var download = await downloadPromise; + + // Get the path of the downloaded file + var downloadedFilePath = await download.PathAsync(); + + // Read the content of the downloaded file + var downloadedContent = await File.ReadAllBytesAsync(downloadedFilePath); + + // Compare the downloaded content with the original file content + var originalContent = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync("AliasVault.E2ETests.TestData.TestAttachment.txt"); + Assert.That(downloadedContent, Is.EqualTo(originalContent), "Downloaded file content does not match the original file content after update."); + + // Clean up: delete the downloaded file + File.Delete(downloadedFilePath); + } + + /// + /// Test that uploading and deleting an attachment works correctly. + /// + /// Async task. + [Test] + [Order(3)] + public async Task UploadAndDeleteAttachment() + { + // Create a new alias with service name = "Test Service for Deletion". + var serviceName = "Test Service for Deletion"; + await CreateCredentialEntry( + new Dictionary + { + { "service-name", serviceName }, + }, + async () => + { + // Upload file. + var fileInput = Page.Locator("input[type='file']"); + var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceBytesAsync("AliasVault.E2ETests.TestData.TestAttachment.txt"); + + // Create a temporary file with the content and original file name + var originalFileName = "TestAttachment.txt"; + var tempFilePath = Path.Combine(Path.GetTempPath(), originalFileName); + await File.WriteAllBytesAsync(tempFilePath, fileContent); + + // Set the file input using the temporary file + await fileInput.SetInputFilesAsync(tempFilePath); + + // Delete the temporary file + File.Delete(tempFilePath); + }); + + // Check that the attachment name appears on the alias page. + var pageContent = await Page.TextContentAsync("body"); + Assert.That(pageContent, Does.Contain("TestAttachment.txt"), "Uploaded attachment name does not appear on alias page."); + + // Click the edit button + await Page.ClickAsync("text=Edit"); + await WaitForUrlAsync("credentials/**/edit", "Edit the existing credentials"); + + // Find and click the delete button for the attachment + var deleteButton = Page.Locator("button:has-text('Delete')").First; + await deleteButton.ClickAsync(); + + // Check that the attachment name no longer appears on the edit page + pageContent = await Page.TextContentAsync("body"); + Assert.That(pageContent, Does.Not.Contain("TestAttachment.txt"), "Deleted attachment name still appears on edit page."); + + // Save the credential + var saveButton = Page.Locator("text=Save Credentials").First; + await saveButton.ClickAsync(); + await WaitForUrlAsync("credentials/**", "Credential updated successfully"); + + // Check that the attachment name does not appear on the view page + pageContent = await Page.TextContentAsync("body"); + Assert.That(pageContent, Does.Not.Contain("TestAttachment.txt"), "Deleted attachment name appears on view page after saving."); + } +} diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AuthPasswordChangeTest.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AuthPasswordChangeTests.cs similarity index 96% rename from src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AuthPasswordChangeTest.cs rename to src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AuthPasswordChangeTests.cs index be2e207c0..353549fe4 100644 --- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AuthPasswordChangeTest.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/AuthPasswordChangeTests.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// +// // Copyright (c) lanedirt. All rights reserved. // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // @@ -14,7 +14,7 @@ namespace AliasVault.E2ETests.Tests.Client.Shard5; [Category("ClientTests")] [TestFixture] -public class AuthPasswordChangeTest : ClientPlaywrightTest +public class AuthPasswordChangeTests : ClientPlaywrightTest { /// /// Test if changing password works correctly. diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/UnlockTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/UnlockTests.cs index dedad1ec7..549a3c0b7 100644 --- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/UnlockTests.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/UnlockTests.cs @@ -5,7 +5,7 @@ // //----------------------------------------------------------------------- -namespace AliasVault.E2ETests.Tests.Client; +namespace AliasVault.E2ETests.Tests.Client.Shard5; /// /// End-to-end tests for the database unlock functionality.