mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-21 16:13:27 -04:00
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:
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
}
|
||||
|
||||
// No error, add success message.
|
||||
GlobalNotificationService.AddSuccessMessage("Credentials created successfully.");
|
||||
GlobalNotificationService.AddSuccessMessage("Credential created successfully.");
|
||||
|
||||
NavigationManager.NavigateTo("/credentials/" + id);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Test attachment content
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user