mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 23:52:31 -04:00
Add attachment delete test and fix bug with IsDeleted flag (#287)
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)
|
||||
|
||||
@@ -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("Credential updated successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalNotificationService.AddSuccessMessage("Credential 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,8 +118,11 @@ public class AttachmentTests : ClientPlaywrightTest
|
||||
});
|
||||
|
||||
// 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(updatedUsername), "Updated username does not appear on alias page.");
|
||||
Assert.That(pageContent, Does.Contain("TestAttachment.txt"), "Attachment name does not appear on alias page after update.");
|
||||
|
||||
// Download the attachment
|
||||
@@ -140,4 +143,63 @@ public class AttachmentTests : ClientPlaywrightTest
|
||||
// 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.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user