Merge pull request #296 from lanedirt/287-saving-existing-credential-with-one-or-more-attachments-fails

Saving existing credential with one or more attachments fails
This commit is contained in:
Leendert de Borst
2024-10-14 09:58:30 +02:00
committed by GitHub
18 changed files with 347 additions and 89 deletions

View File

@@ -1,4 +1,5 @@
@using System.IO
@inject ILogger<AttachmentUploader> Logger
<div class="col-span-6 sm:col-span-3">
<InputFile OnChange="@HandleFileSelection" class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400" />
@@ -6,12 +7,12 @@
{
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">@StatusMessage</p>
}
@if (Attachments.Any())
@if (Attachments.Exists(x => !x.IsDeleted))
{
<div class="mt-4">
<h4 class="mb-2 text-lg font-semibold dark:text-white">Attachments:</h4>
<ul class="list-disc list-inside">
@foreach (var attachment in Attachments)
@foreach (var attachment in Attachments.Where(x => !x.IsDeleted))
{
<li class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
<span>@attachment.Filename</span>
@@ -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).
/// </summary>
private List<Guid> OriginalAttachmentsIds = [];
private List<Guid> OriginalAttachmentsIds { get; set; } = [];
private string StatusMessage = string.Empty;
/// <summary>
/// Status message to display.
/// </summary>
private string StatusMessage { get; set; } = string.Empty;
/// <inheritdoc />
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.");
}
}

View File

@@ -2,7 +2,7 @@
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Attachments</h3>
@if (Attachments.Any())
@if (Attachments.Any(x => !x.IsDeleted))
{
<div class="overflow-x-auto">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
@@ -13,7 +13,7 @@
</tr>
</thead>
<tbody>
@foreach (var attachment in Attachments)
@foreach (var attachment in Attachments.Where(x => !x.IsDeleted))
{
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">

View File

@@ -2,10 +2,10 @@
@inject JsInteropService JsInteropService
@implements IDisposable
<label for="@_inputId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
<div class="relative">
<input type="text" id="@_inputId" class="outline-0 shadow-sm bg-gray-50 border @(_copied ? "border-green-500 border-2" : "border-gray-300") text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-@(_copied ? "green-500" : "gray-600") dark:placeholder-gray-400 dark:text-white" value="@Value" @onclick="CopyToClipboard" readonly>
@if (_copied)
<input type="text" id="@Id" class="outline-0 shadow-sm bg-gray-50 border @(Copied ? "border-green-500 border-2" : "border-gray-300") text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-@(Copied ? "green-500" : "gray-600") dark:placeholder-gray-400 dark:text-white" value="@Value" @onclick="CopyToClipboard" readonly>
@if (Copied)
{
<span class="absolute inset-y-0 right-0 flex items-center pr-3 text-green-500 dark:text-green-400">
Copied!
@@ -14,6 +14,12 @@
</div>
@code {
/// <summary>
/// Id for the input field. Defaults to a random GUID if not provided.
/// </summary>
[Parameter]
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The label for the input.
/// </summary>
@@ -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;
/// <inheritdoc />
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);
}

View File

@@ -2,12 +2,12 @@
@inject JsInteropService JsInteropService
@implements IDisposable
<label for="@_inputId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
<div class="relative">
<input type="@(_isPasswordVisible ? "text" : "password")" id="@_inputId" class="outline-0 shadow-sm bg-gray-50 border @(_copied ? "border-green-500 border-2" : "border-gray-300") text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-20 dark:bg-gray-700 dark:border-@(_copied ? "green-500" : "gray-600") dark:placeholder-gray-400 dark:text-white" value="@Value" @onclick="CopyToClipboard" readonly>
<input type="@(IsPasswordVisible ? "text" : "password")" id="@Id" class="outline-0 shadow-sm bg-gray-50 border @(Copied ? "border-green-500 border-2" : "border-gray-300") text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-20 dark:bg-gray-700 dark:border-@(Copied ? "green-500" : "gray-600") dark:placeholder-gray-400 dark:text-white" value="@Value" @onclick="CopyToClipboard" readonly>
<button type="button" class="absolute inset-y-1 right-1 flex items-center justify-center w-10 h-8 text-gray-500 bg-gray-200 rounded-md shadow-sm hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors duration-200 dark:bg-gray-600 dark:hover:bg-gray-500 dark:text-gray-300" @onclick="TogglePasswordVisibility">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@if (_isPasswordVisible)
@if (IsPasswordVisible)
{
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
@@ -18,7 +18,7 @@
}
</svg>
</button>
@if (_copied)
@if (Copied)
{
<span class="absolute inset-y-0 right-10 flex items-center pr-3 text-green-500 dark:text-green-400">
Copied!
@@ -27,6 +27,12 @@
</div>
@code {
/// <summary>
/// Id for the input field. Defaults to a random GUID if not provided.
/// </summary>
[Parameter]
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The label for the input.
/// </summary>
@@ -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; }
/// <inheritdoc />
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)

View File

@@ -159,7 +159,7 @@
}
// No error, add success message.
GlobalNotificationService.AddSuccessMessage("Credentials created successfully.");
GlobalNotificationService.AddSuccessMessage("Credential created successfully.");
NavigationManager.NavigateTo("/credentials/" + id);

View File

@@ -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);
}
/// <summary>
/// Helper method to convert a Credential object to a CredentialEdit object.
/// </summary>
@@ -390,7 +350,7 @@ else
/// </summary>
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();
}
/// <summary>
/// Save the alias to the database.
/// </summary>
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);
}
}

View File

@@ -59,13 +59,13 @@ else
<form action="#">
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<CopyPasteFormRow Label="Email" Value="@Alias.Alias.Email"></CopyPasteFormRow>
<CopyPasteFormRow Id="email" Label="Email" Value="@Alias.Alias.Email"></CopyPasteFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<CopyPasteFormRow Label="Username" Value="@(Alias.Username)"></CopyPasteFormRow>
<CopyPasteFormRow Id="username" Label="Username" Value="@(Alias.Username)"></CopyPasteFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<CopyPastePasswordFormRow Label="Password" Value="@(Alias.Passwords.FirstOrDefault()?.Value ?? string.Empty)"></CopyPastePasswordFormRow>
<CopyPastePasswordFormRow Id="password" Label="Password" Value="@(Alias.Passwords.FirstOrDefault()?.Value ?? string.Empty)"></CopyPastePasswordFormRow>
</div>
</div>
</form>

View File

@@ -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);
}
}

View File

@@ -23,6 +23,7 @@
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
<EmbeddedResource Include="TestData\AliasClientDb_encrypted_base64_1.0.0.txt" />
<EmbeddedResource Include="TestData\TestAttachment.txt" />
</ItemGroup>
<ItemGroup>

View File

@@ -175,8 +175,9 @@ public class ClientPlaywrightTest : PlaywrightTest
/// Create new credential entry.
/// </summary>
/// <param name="formValues">Dictionary with html element ids and values to input as field value.</param>
/// <param name="customLogic">Optional custom logic to execute after filling input fields.</param>
/// <returns>Async task.</returns>
protected async Task CreateCredentialEntry(Dictionary<string, string>? formValues = null)
protected async Task CreateCredentialEntry(Dictionary<string, string>? formValues = null, Func<Task>? 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.");
}
/// <summary>

View File

@@ -7,7 +7,9 @@
namespace AliasVault.E2ETests.Common;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
/// <summary>
/// Utility for reading strings from project embedded resources used in tests.
@@ -20,7 +22,7 @@ public static class ResourceReaderUtility
/// <param name="resourceName">Name of the embedded resource.</param>
/// <returns>Contents of embedded resource as string.</returns>
/// <exception cref="InvalidOperationException">Thrown when resource is not found with that name.</exception>
public static async Task<string> ReadEmbeddedResourceAsync(string resourceName)
public static async Task<string> ReadEmbeddedResourceStringAsync(string resourceName)
{
var assembly = Assembly.GetExecutingAssembly();
@@ -34,6 +36,27 @@ public static class ResourceReaderUtility
return await reader.ReadToEndAsync();
}
/// <summary>
/// Reads byte array from embedded resource.
/// </summary>
/// <param name="resourceName">Name of the embedded resource.</param>
/// <returns>Contents of embedded resource as byte array.</returns>
/// <exception cref="InvalidOperationException">Thrown when resource is not found with that name.</exception>
public static async Task<byte[]> 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();
}
/// <summary>
/// Get all embedded resource names in current assembly.
/// </summary>

View File

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

View File

@@ -0,0 +1 @@
Test attachment content

View File

@@ -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.");
}

View File

@@ -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",
});

View File

@@ -0,0 +1,205 @@
//-----------------------------------------------------------------------
// <copyright file="AttachmentTests.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.E2ETests.Tests.Client.Shard5;
/// <summary>
/// End-to-end tests for uploading and downloading attachments.
/// </summary>
[Parallelizable(ParallelScope.Self)]
[Category("ClientTests")]
[TestFixture]
public class AttachmentTests : ClientPlaywrightTest
{
/// <summary>
/// Test that adding an attachment works correctly and can be downloaded afterwards.
/// </summary>
/// <returns>Async task.</returns>
[Test]
[Order(1)]
public async Task UploadAndDownloadAttachment()
{
// Create a new alias with service name = "Test Service".
var serviceName = "Test Service";
await CreateCredentialEntry(
new Dictionary<string, string>
{
{ "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);
}
/// <summary>
/// Test that updating a credential with an existing attachment works correctly.
/// </summary>
/// <returns>Async task.</returns>
[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<string, string>
{
{ "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<string, string>
{
{ "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);
}
/// <summary>
/// Test that uploading and deleting an attachment works correctly.
/// </summary>
/// <returns>Async task.</returns>
[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<string, string>
{
{ "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.");
}
}

View File

@@ -1,5 +1,5 @@
//-----------------------------------------------------------------------
// <copyright file="AuthPasswordChangeTest.cs" company="lanedirt">
// <copyright file="AuthPasswordChangeTests.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
@@ -14,7 +14,7 @@ namespace AliasVault.E2ETests.Tests.Client.Shard5;
[Category("ClientTests")]
[TestFixture]
public class AuthPasswordChangeTest : ClientPlaywrightTest
public class AuthPasswordChangeTests : ClientPlaywrightTest
{
/// <summary>
/// Test if changing password works correctly.

View File

@@ -5,7 +5,7 @@
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.E2ETests.Tests.Client;
namespace AliasVault.E2ETests.Tests.Client.Shard5;
/// <summary>
/// End-to-end tests for the database unlock functionality.