mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-04 06:52:16 -04:00
Fix attachments/totpcodes/passkey mutations in web app to not be committed immediately (#1595)
This commit is contained in:
committed by
Leendert de Borst
parent
9a3189048e
commit
f878afa6be
@@ -86,6 +86,7 @@ public sealed class ItemEdit
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ItemEdit instance from an Item entity.
|
||||
/// Creates clones of Attachments, TotpCodes, and Passkeys to avoid modifying EF-tracked entities.
|
||||
/// </summary>
|
||||
/// <param name="item">The item entity to convert.</param>
|
||||
/// <returns>A new ItemEdit instance.</returns>
|
||||
@@ -99,9 +100,9 @@ public sealed class ItemEdit
|
||||
LogoId = item.LogoId,
|
||||
ServiceLogo = item.Logo?.FileData,
|
||||
FolderId = item.FolderId,
|
||||
Attachments = item.Attachments.Where(a => !a.IsDeleted).ToList(),
|
||||
TotpCodes = item.TotpCodes.Where(t => !t.IsDeleted).ToList(),
|
||||
Passkeys = item.Passkeys.Where(p => !p.IsDeleted).ToList(),
|
||||
Attachments = item.Attachments.Where(a => !a.IsDeleted).Select(CloneAttachment).ToList(),
|
||||
TotpCodes = item.TotpCodes.Where(t => !t.IsDeleted).Select(CloneTotpCode).ToList(),
|
||||
Passkeys = item.Passkeys.Where(p => !p.IsDeleted).Select(ClonePasskey).ToList(),
|
||||
CreateDate = item.CreatedAt,
|
||||
LastUpdate = item.UpdatedAt,
|
||||
};
|
||||
@@ -569,4 +570,66 @@ public sealed class ItemEdit
|
||||
// not yet in the registry - treat as custom for now
|
||||
return FieldCategory.Custom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a clone of an Attachment entity to avoid modifying EF-tracked entities.
|
||||
/// </summary>
|
||||
/// <param name="attachment">The attachment to clone.</param>
|
||||
/// <returns>A new Attachment instance with copied values.</returns>
|
||||
private static Attachment CloneAttachment(Attachment attachment)
|
||||
{
|
||||
return new Attachment
|
||||
{
|
||||
Id = attachment.Id,
|
||||
Filename = attachment.Filename,
|
||||
Blob = attachment.Blob,
|
||||
ItemId = attachment.ItemId,
|
||||
CreatedAt = attachment.CreatedAt,
|
||||
UpdatedAt = attachment.UpdatedAt,
|
||||
IsDeleted = attachment.IsDeleted,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a clone of a TotpCode entity to avoid modifying EF-tracked entities.
|
||||
/// </summary>
|
||||
/// <param name="totpCode">The TOTP code to clone.</param>
|
||||
/// <returns>A new TotpCode instance with copied values.</returns>
|
||||
private static TotpCode CloneTotpCode(TotpCode totpCode)
|
||||
{
|
||||
return new TotpCode
|
||||
{
|
||||
Id = totpCode.Id,
|
||||
Name = totpCode.Name,
|
||||
SecretKey = totpCode.SecretKey,
|
||||
ItemId = totpCode.ItemId,
|
||||
CreatedAt = totpCode.CreatedAt,
|
||||
UpdatedAt = totpCode.UpdatedAt,
|
||||
IsDeleted = totpCode.IsDeleted,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a clone of a Passkey entity to avoid modifying EF-tracked entities.
|
||||
/// </summary>
|
||||
/// <param name="passkey">The passkey to clone.</param>
|
||||
/// <returns>A new Passkey instance with copied values.</returns>
|
||||
private static Passkey ClonePasskey(Passkey passkey)
|
||||
{
|
||||
return new Passkey
|
||||
{
|
||||
Id = passkey.Id,
|
||||
RpId = passkey.RpId,
|
||||
UserHandle = passkey.UserHandle,
|
||||
PublicKey = passkey.PublicKey,
|
||||
PrivateKey = passkey.PrivateKey,
|
||||
PrfKey = passkey.PrfKey,
|
||||
DisplayName = passkey.DisplayName,
|
||||
AdditionalData = passkey.AdditionalData,
|
||||
ItemId = passkey.ItemId,
|
||||
CreatedAt = passkey.CreatedAt,
|
||||
UpdatedAt = passkey.UpdatedAt,
|
||||
IsDeleted = passkey.IsDeleted,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,4 +439,90 @@ public class AttachmentTests : ClientPlaywrightTest
|
||||
File.Delete(downloadedFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that deleting an attachment and then canceling the edit does not persist the deletion.
|
||||
/// This verifies that attachment deletions only happen in-memory until the save button is pressed.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(7)]
|
||||
public async Task DeleteAttachmentCancelDoesNotPersist()
|
||||
{
|
||||
// Create a new alias with an attachment.
|
||||
var serviceName = "Test Service Delete Cancel";
|
||||
await CreateItemEntry(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "service-name", serviceName },
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
// Add the attachments section via the + menu
|
||||
await AddFieldSectionAsync("Attachments");
|
||||
|
||||
// Wait for the file input to appear
|
||||
await Page.WaitForSelectorAsync("input[type='file']");
|
||||
|
||||
// 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 = "TestAttachmentCancel.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);
|
||||
|
||||
// Wait for the file to be uploaded
|
||||
await Page.WaitForSelectorAsync("text=TestAttachmentCancel.txt");
|
||||
|
||||
// Delete the temporary file
|
||||
File.Delete(tempFilePath);
|
||||
});
|
||||
|
||||
// Check that the attachment name appears on the view page.
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("TestAttachmentCancel.txt"), "Uploaded attachment name does not appear on view page.");
|
||||
|
||||
// Click the edit button
|
||||
await Page.ClickAsync("text=Edit");
|
||||
await WaitForUrlAsync("items/**/edit", "Edit the existing item");
|
||||
|
||||
// Verify attachment appears on edit page
|
||||
pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("TestAttachmentCancel.txt"), "Attachment name does not appear on edit page.");
|
||||
|
||||
// Find and click the delete button for the attachment
|
||||
var deleteButton = Page.Locator("button:has-text('Delete')").First;
|
||||
await deleteButton.ClickAsync();
|
||||
|
||||
// Wait a moment for the UI to update
|
||||
await Task.Delay(200);
|
||||
|
||||
// Check that the attachment name no longer appears on the edit page
|
||||
pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Not.Contain("TestAttachmentCancel.txt"), "Deleted attachment name still appears on edit page after deletion.");
|
||||
|
||||
// Click the cancel button instead of save
|
||||
var cancelButton = Page.Locator("text=Cancel").First;
|
||||
await cancelButton.ClickAsync();
|
||||
|
||||
// Wait for the view page to load
|
||||
await WaitForUrlAsync("items/**", "View item");
|
||||
|
||||
// Check that the attachment name STILL appears on the view page (deletion was not persisted)
|
||||
pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("TestAttachmentCancel.txt"), "Attachment was deleted despite canceling the edit. Deletions should only persist on save.");
|
||||
|
||||
// Edit the item again to double-check the attachment is still there
|
||||
await Page.ClickAsync("text=Edit");
|
||||
await WaitForUrlAsync("items/**/edit", "Edit the existing item");
|
||||
|
||||
// Verify attachment still appears on edit page
|
||||
pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("TestAttachmentCancel.txt"), "Attachment name does not appear on edit page after canceling deletion. Deletion was incorrectly persisted.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user