Add multi-url import support to all importers (#1590)

This commit is contained in:
Leendert de Borst
2026-02-02 23:12:57 +01:00
committed by Leendert de Borst
parent 16f44a6f00
commit 572998bb7c
21 changed files with 267 additions and 72 deletions

View File

@@ -411,12 +411,12 @@
FaviconExtractionCancellation = new CancellationTokenSource();
StateHasChanged();
var credentialsWithUrls = ImportedCredentials.Where(c => !string.IsNullOrEmpty(c.ServiceUrl)).ToList();
var credentialsWithUrls = ImportedCredentials.Where(c => c.ServiceUrls?.FirstOrDefault() != null).ToList();
// Group credentials by normalized domain to avoid duplicate fetches
var processedDomains = new HashSet<string>();
TotalFaviconsToExtract = credentialsWithUrls.Count;
TotalFaviconsToExtract = credentialsWithUrls.Count();
foreach (var credential in credentialsWithUrls)
{
@@ -428,7 +428,7 @@
// Extract normalized domain for deduplication
try
{
var domain = new Uri(credential.ServiceUrl!).Host.ToLowerInvariant();
var domain = new Uri(credential.ServiceUrls!.First()).Host.ToLowerInvariant();
if (domain.StartsWith("www."))
{
domain = domain[4..];
@@ -460,13 +460,14 @@
try
{
// Extract normalized domain for storage key
var domain = new Uri(credential.ServiceUrl!).Host.ToLowerInvariant();
var url = credential.ServiceUrls!.First();
var domain = new Uri(url).Host.ToLowerInvariant();
if (domain.StartsWith("www."))
{
domain = domain[4..];
}
var apiReturn = await HttpClient.GetFromJsonAsync<FaviconExtractModel>($"v1/Favicon/Extract?url={Uri.EscapeDataString(credential.ServiceUrl!)}");
var apiReturn = await HttpClient.GetFromJsonAsync<FaviconExtractModel>($"v1/Favicon/Extract?url={Uri.EscapeDataString(url)}");
if (apiReturn?.Image is not null)
{
// Store by normalized domain for deduplication

View File

@@ -33,6 +33,7 @@
<ImportServiceLastPass />
<ImportServiceNordPass />
<ImportServiceProtonPass />
<ImportServiceRoboform />
<ImportServiceStrongbox />
<ImportServiceAliasVault />
<ImportServiceGenericCsv />

View File

@@ -248,6 +248,19 @@
<value>In order to import your Dropbox Passwords, you need to export them as a CSV file. You can do this by opening Dropbox Passwords, going to 'Account' > 'Export' (to .CSV).</value>
<comment>Dropbox export instructions part 1</comment>
</data>
<!-- RoboForm -->
<data name="RoboformDescription" xml:space="preserve">
<value>Import passwords from RoboForm</value>
<comment>Description for RoboForm import service</comment>
</data>
<data name="RoboformInstructionsPart1" xml:space="preserve">
<value>In order to import your RoboForm passwords, you need to export them as a CSV file. You can do this by opening RoboForm, going to 'RoboForm' menu > 'Options' > 'Account &amp; Data' > 'Export' and selecting CSV format.</value>
<comment>RoboForm export instructions part 1</comment>
</data>
<data name="RoboformInstructionsPart2" xml:space="preserve">
<value>Once you have exported the file, you can upload it below.</value>
<comment>RoboForm export instructions part 2</comment>
</data>
<!-- Common text that can be reused -->
<data name="UploadFileInstructionCommon" xml:space="preserve">
<value>Once you have exported the file, you can upload it below.</value>

View File

@@ -4,6 +4,6 @@ Business,,login,Item for business folder,,,0,,crisply,4CSp43uhSZri8A,
,,login,Test,,,0,,test2,asdasd,
Business,,login,TutaNota,,,0,,avtest2@tutamail.com,blabla,otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&algorithm=SHA1&digits=6&period=30
Business,,login,Aliasvault.net,,,0,https://www.aliasvault.net,root,toor,
Business,,login,TutaNota3,,,0,,avtest3@tutamail.com,blabla,otpauth://totp/Test%20name%3Atest%40test.org?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&issuer=Alias%20Vault
Business,,login,TutaNota3,,,0,"https://www.aliasvault.net,https://app.aliasvault.net,https://downloads.aliasvault.net",avtest3@tutamail.com,blabla,otpauth://totp/Test%20name%3Atest%40test.org?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&issuer=Alias%20Vault
Business,,login,TutaNota4,,,0,,avtest4@tutamail.com,blabla,otpauth://totp/my.email@gmail.com?secret=TEST!INVALID&issuer=Alias%20Vault&algorithm=SHA1&digits=6&period=30
Business,,login,TutaNota5,,,0,,avtest5@tutamail.com,blabla,incorrectstring
1 folder favorite type name notes fields reprompt login_uri login_username login_password login_totp
4 login Test 0 test2 asdasd
5 Business login TutaNota 0 avtest2@tutamail.com blabla otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&algorithm=SHA1&digits=6&period=30
6 Business login Aliasvault.net 0 https://www.aliasvault.net root toor
7 Business login TutaNota3 0 https://www.aliasvault.net,https://app.aliasvault.net,https://downloads.aliasvault.net avtest3@tutamail.com blabla otpauth://totp/Test%20name%3Atest%40test.org?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&issuer=Alias%20Vault
8 Business login TutaNota4 0 avtest4@tutamail.com blabla otpauth://totp/my.email@gmail.com?secret=TEST!INVALID&issuer=Alias%20Vault&algorithm=SHA1&digits=6&period=30
9 Business login TutaNota5 0 avtest5@tutamail.com blabla incorrectstring

View File

@@ -61,7 +61,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(importedCredential.ServiceName, Is.EqualTo(item.Name));
Assert.That(importedCredential.ServiceUrl, Is.EqualTo("https://testservice.com"));
Assert.That(importedCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://testservice.com"));
Assert.That(importedCredential.Username, Is.EqualTo("testuser"));
Assert.That(importedCredential.Notes, Is.EqualTo("Test notes"));
Assert.That(importedCredential.CreatedAt?.Date, Is.EqualTo(item.CreatedAt.Date));
@@ -109,10 +109,36 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(aliasVaultCredential.ServiceName, Is.EqualTo("Aliasvault.net"));
Assert.That(aliasVaultCredential.ServiceUrl, Is.EqualTo("https://www.aliasvault.net"));
Assert.That(aliasVaultCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://www.aliasvault.net"));
Assert.That(aliasVaultCredential.Username, Is.EqualTo("root"));
Assert.That(aliasVaultCredential.Password, Is.EqualTo("toor"));
});
// Test entry with multiple URLs (TutaNota3)
var multiUrlCredential = importedCredentials.First(c => c.ServiceName == "TutaNota3");
Assert.Multiple(() =>
{
Assert.That(multiUrlCredential.ServiceName, Is.EqualTo("TutaNota3"));
Assert.That(multiUrlCredential.ServiceUrls, Has.Count.EqualTo(3));
Assert.That(multiUrlCredential.ServiceUrls![0], Is.EqualTo("https://www.aliasvault.net"));
Assert.That(multiUrlCredential.ServiceUrls[1], Is.EqualTo("https://app.aliasvault.net"));
Assert.That(multiUrlCredential.ServiceUrls[2], Is.EqualTo("https://downloads.aliasvault.net"));
Assert.That(multiUrlCredential.Username, Is.EqualTo("avtest3@tutamail.com"));
});
// Verify multiple URLs get converted to multiple FieldValues
var multiUrlItem = convertedItems.First(i => i.Name == "TutaNota3");
var urlFieldValues = multiUrlItem.FieldValues.Where(fv => fv.FieldKey == FieldKey.LoginUrl).OrderBy(fv => fv.Weight).ToList();
Assert.Multiple(() =>
{
Assert.That(urlFieldValues, Has.Count.EqualTo(3));
Assert.That(urlFieldValues[0].Value, Is.EqualTo("https://www.aliasvault.net"));
Assert.That(urlFieldValues[1].Value, Is.EqualTo("https://app.aliasvault.net"));
Assert.That(urlFieldValues[2].Value, Is.EqualTo("https://downloads.aliasvault.net"));
Assert.That(urlFieldValues[0].Weight, Is.EqualTo(0));
Assert.That(urlFieldValues[1].Weight, Is.EqualTo(1));
Assert.That(urlFieldValues[2].Weight, Is.EqualTo(2));
});
}
/// <summary>
@@ -146,7 +172,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(sampleCredential.ServiceName, Is.EqualTo("Sample"));
Assert.That(sampleCredential.ServiceUrl, Is.EqualTo("https://strongboxsafe.com"));
Assert.That(sampleCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://strongboxsafe.com"));
Assert.That(sampleCredential.Username, Is.EqualTo("username"));
Assert.That(sampleCredential.Password, Is.EqualTo("&3V_$z?Aiw-_x+nbYj"));
});
@@ -183,7 +209,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(onePasswordAccount.ServiceName, Is.EqualTo("1Password Account (dpatel)"));
Assert.That(onePasswordAccount.ServiceUrl, Is.EqualTo("https://my.1password.com"));
Assert.That(onePasswordAccount.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://my.1password.com"));
Assert.That(onePasswordAccount.Username, Is.EqualTo("derekpatel@aliasvault.net"));
Assert.That(onePasswordAccount.Password, Is.EqualTo("passwordexample"));
Assert.That(onePasswordAccount.Notes, Is.EqualTo("You can use this login to sign in to your account on 1password.com."));
@@ -211,7 +237,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(exampleCredential.ServiceName, Is.EqualTo("example.com"));
Assert.That(exampleCredential.ServiceUrl, Is.EqualTo("https://example.com/"));
Assert.That(exampleCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://example.com/"));
Assert.That(exampleCredential.Username, Is.EqualTo("usernamegoogle"));
Assert.That(exampleCredential.Password, Is.EqualTo("passwordgoogle"));
Assert.That(exampleCredential.Notes, Is.EqualTo("Note for example password from Google"));
@@ -221,7 +247,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(facebookCredential.ServiceName, Is.EqualTo("facebook.com"));
Assert.That(facebookCredential.ServiceUrl, Is.EqualTo("https://facebook.com/"));
Assert.That(facebookCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://facebook.com/"));
Assert.That(facebookCredential.Username, Is.EqualTo("facebookuser"));
Assert.That(facebookCredential.Password, Is.EqualTo("facebookpass"));
Assert.That(facebookCredential.Notes, Is.EqualTo("Facebook comment"));
@@ -249,7 +275,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(exampleCredential.ServiceName, Is.EqualTo("example.com"));
Assert.That(exampleCredential.ServiceUrl, Is.EqualTo("https://example.com"));
Assert.That(exampleCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://example.com"));
Assert.That(exampleCredential.Username, Is.EqualTo("username-example"));
Assert.That(exampleCredential.Password, Is.EqualTo("examplepassword"));
});
@@ -258,7 +284,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(youtubeCredential.ServiceName, Is.EqualTo("youtube.com"));
Assert.That(youtubeCredential.ServiceUrl, Is.EqualTo("https://youtube.com"));
Assert.That(youtubeCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://youtube.com"));
Assert.That(youtubeCredential.Username, Is.EqualTo("youtubeusername"));
Assert.That(youtubeCredential.Password, Is.EqualTo("youtubepassword"));
});
@@ -285,7 +311,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(sampleEntry.ServiceName, Is.EqualTo("Sample Entry"));
Assert.That(sampleEntry.ServiceUrl, Is.EqualTo("https://keepass.info/"));
Assert.That(sampleEntry.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://keepass.info/"));
Assert.That(sampleEntry.Username, Is.EqualTo("User Name"));
Assert.That(sampleEntry.Password, Is.EqualTo("Password"));
Assert.That(sampleEntry.Notes, Is.EqualTo("Notes"));
@@ -295,7 +321,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(sampleEntry2.ServiceName, Is.EqualTo("Sample Entry #2"));
Assert.That(sampleEntry2.ServiceUrl, Is.EqualTo("https://keepass.info/help/kb/testform.html"));
Assert.That(sampleEntry2.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://keepass.info/help/kb/testform.html"));
Assert.That(sampleEntry2.Username, Is.EqualTo("Michael321"));
Assert.That(sampleEntry2.Password, Is.EqualTo("12345"));
Assert.That(sampleEntry2.Notes, Is.Empty);
@@ -323,7 +349,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(specialEntry.ServiceName, Is.EqualTo("Entry with \"notes\" special chars"));
Assert.That(specialEntry.ServiceUrl, Is.Empty);
Assert.That(specialEntry.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(specialEntry.Username, Is.Empty);
Assert.That(specialEntry.Password, Is.EqualTo("DVfIsb4TGkL7oKCwyiet"));
Assert.That(specialEntry.Notes, Does.Contain("\"with quotes\""));
@@ -336,7 +362,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(sampleEntry.ServiceName, Is.EqualTo("Sample Entry"));
Assert.That(sampleEntry.ServiceUrl, Is.EqualTo("https://keepass.info/"));
Assert.That(sampleEntry.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://keepass.info/"));
Assert.That(sampleEntry.Username, Is.EqualTo("User Name"));
Assert.That(sampleEntry.Password, Is.EqualTo("Password"));
Assert.That(sampleEntry.Notes, Is.EqualTo("Notes"));
@@ -364,7 +390,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(sampleEntry.ServiceName, Is.EqualTo("Sample Entry"));
Assert.That(sampleEntry.ServiceUrl, Is.EqualTo("https://keepass.info/"));
Assert.That(sampleEntry.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://keepass.info/"));
Assert.That(sampleEntry.Username, Is.EqualTo("User Name"));
Assert.That(sampleEntry.Password, Is.EqualTo("Password"));
Assert.That(sampleEntry.Notes, Is.EqualTo("Notes"));
@@ -375,7 +401,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(sampleEntry2.ServiceName, Is.EqualTo("Sample Entry #2"));
Assert.That(sampleEntry2.ServiceUrl, Is.EqualTo("https://keepass.info/help/kb/testform.html"));
Assert.That(sampleEntry2.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://keepass.info/help/kb/testform.html"));
Assert.That(sampleEntry2.Username, Is.EqualTo("Michael321"));
Assert.That(sampleEntry2.Password, Is.EqualTo("12345"));
Assert.That(sampleEntry2.Notes, Is.Empty);
@@ -404,7 +430,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(testProton1Credential.ServiceName, Is.EqualTo("Test proton 1"));
Assert.That(testProton1Credential.ServiceUrl, Is.EqualTo("https://www.website.com/"));
Assert.That(testProton1Credential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://www.website.com/"));
Assert.That(testProton1Credential.Username, Is.EqualTo("user1"));
Assert.That(testProton1Credential.Password, Is.EqualTo("pass1"));
Assert.That(testProton1Credential.TwoFactorSecret, Is.EqualTo("otpauth://totp/Strongbox?secret=PLW4SB3PQ7MKVXY2MXF4NEXS6Y&algorithm=SHA1&digits=6&period=30"));
@@ -455,7 +481,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(testCredential.ServiceName, Is.EqualTo("Test"));
Assert.That(testCredential.ServiceUrl, Is.EqualTo("https://Test"));
Assert.That(testCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://Test"));
Assert.That(testCredential.Username, Is.EqualTo("Test username"));
Assert.That(testCredential.Password, Is.EqualTo("password123"));
Assert.That(testCredential.Notes, Is.Null);
@@ -465,7 +491,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(googleCredential.ServiceName, Is.EqualTo("Google"));
Assert.That(googleCredential.ServiceUrl, Is.EqualTo("https://www.google.com"));
Assert.That(googleCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://www.google.com"));
Assert.That(googleCredential.Username, Is.EqualTo("googleuser"));
Assert.That(googleCredential.Password, Is.EqualTo("googlepassword"));
Assert.That(googleCredential.Notes, Is.Null);
@@ -475,7 +501,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(localCredential.ServiceName, Is.EqualTo("Local"));
Assert.That(localCredential.ServiceUrl, Is.EqualTo("https://www.testwebsite.local"));
Assert.That(localCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://www.testwebsite.local"));
Assert.That(localCredential.Username, Is.EqualTo("testusername"));
Assert.That(localCredential.Password, Is.EqualTo("testpassword"));
Assert.That(localCredential.Notes, Is.EqualTo("testnote\nAlternative username 1: testusernamealternative"));
@@ -503,7 +529,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(exampleCredential.ServiceName, Is.EqualTo("Examplename"));
Assert.That(exampleCredential.ServiceUrl, Is.EqualTo("https://example.com"));
Assert.That(exampleCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://example.com"));
Assert.That(exampleCredential.Username, Is.EqualTo("Exampleusername"));
Assert.That(exampleCredential.Password, Is.EqualTo("examplepassword"));
Assert.That(exampleCredential.Notes, Is.EqualTo("Examplenotes"));
@@ -515,7 +541,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(userWithoutUrlCredential.ServiceName, Is.EqualTo("Userwithouturlornotes"));
Assert.That(userWithoutUrlCredential.ServiceUrl, Is.Null);
Assert.That(userWithoutUrlCredential.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(userWithoutUrlCredential.Username, Is.EqualTo("userwithouturlornotes"));
Assert.That(userWithoutUrlCredential.Password, Is.EqualTo("userpass"));
Assert.That(userWithoutUrlCredential.Notes, Is.Empty);
@@ -527,7 +553,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(secureNoteCredential.ServiceName, Is.EqualTo("securenote1"));
Assert.That(secureNoteCredential.ServiceUrl, Is.Null);
Assert.That(secureNoteCredential.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(secureNoteCredential.Username, Is.Empty);
Assert.That(secureNoteCredential.Password, Is.Empty);
Assert.That(secureNoteCredential.Notes, Is.EqualTo("Securenotecontent here"));
@@ -539,7 +565,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(creditCardCredential.ServiceName, Is.EqualTo("Paymentcard1"));
Assert.That(creditCardCredential.ServiceUrl, Is.Null); // Should be normalized to null
Assert.That(creditCardCredential.ServiceUrls?.FirstOrDefault(), Is.Null); // Should be normalized to null
Assert.That(creditCardCredential.Username, Is.Empty);
Assert.That(creditCardCredential.Password, Is.Empty);
Assert.That(creditCardCredential.ItemType, Is.EqualTo(ImportedItemType.Creditcard));
@@ -573,7 +599,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(gmailCredential.ServiceName, Is.EqualTo("Gmail"));
Assert.That(gmailCredential.ServiceUrl, Is.EqualTo("https://gmail.com"));
Assert.That(gmailCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://gmail.com"));
Assert.That(gmailCredential.Username, Is.EqualTo("your.email@gmail.com"));
Assert.That(gmailCredential.Password, Is.EqualTo("your_password"));
Assert.That(gmailCredential.Notes, Is.EqualTo("Important email account"));
@@ -585,7 +611,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(facebookCredential.ServiceName, Is.EqualTo("Facebook"));
Assert.That(facebookCredential.ServiceUrl, Is.EqualTo("https://facebook.com"));
Assert.That(facebookCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://facebook.com"));
Assert.That(facebookCredential.Username, Is.EqualTo("your.username"));
Assert.That(facebookCredential.Password, Is.EqualTo("your_password"));
Assert.That(facebookCredential.Notes, Is.EqualTo("Social media account"));
@@ -597,7 +623,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(githubCredential.ServiceName, Is.EqualTo("GitHub"));
Assert.That(githubCredential.ServiceUrl, Is.EqualTo("https://github.com"));
Assert.That(githubCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://github.com"));
Assert.That(githubCredential.Username, Is.EqualTo("developer_username"));
Assert.That(githubCredential.Password, Is.EqualTo("your_password"));
Assert.That(githubCredential.Notes, Is.EqualTo("Development platform"));
@@ -609,7 +635,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(secureNoteCredential.ServiceName, Is.EqualTo("Secure Note"));
Assert.That(secureNoteCredential.ServiceUrl, Is.Null);
Assert.That(secureNoteCredential.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(secureNoteCredential.Username, Is.Empty);
Assert.That(secureNoteCredential.Password, Is.Empty);
Assert.That(secureNoteCredential.Notes, Is.EqualTo("Important information or notes without login credentials"));
@@ -638,7 +664,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(gmailCredential.ServiceName, Is.EqualTo("Gmail"));
Assert.That(gmailCredential.ServiceUrl, Is.EqualTo("https://gmail.com"));
Assert.That(gmailCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://gmail.com"));
Assert.That(gmailCredential.Username, Is.EqualTo("testuser@gmail.com"));
Assert.That(gmailCredential.Password, Is.EqualTo("gmailpass123"));
Assert.That(gmailCredential.Notes, Is.EqualTo("Important email account"));
@@ -649,7 +675,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(githubCredential.ServiceName, Is.EqualTo("GitHub"));
Assert.That(githubCredential.ServiceUrl, Is.EqualTo("https://github.com"));
Assert.That(githubCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://github.com"));
Assert.That(githubCredential.Username, Is.EqualTo("devuser"));
Assert.That(githubCredential.Password, Is.EqualTo("devpass789"));
Assert.That(githubCredential.Notes, Is.EqualTo("Development platform"));
@@ -660,7 +686,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(secureNoteCredential.ServiceName, Is.EqualTo("Secure Note"));
Assert.That(secureNoteCredential.ServiceUrl, Is.Null);
Assert.That(secureNoteCredential.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(secureNoteCredential.Username, Is.Empty);
Assert.That(secureNoteCredential.Password, Is.Empty);
Assert.That(secureNoteCredential.Notes, Is.EqualTo("Important information stored securely"));
@@ -688,7 +714,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(passwordCredential.ServiceName, Is.EqualTo("Password title"));
Assert.That(passwordCredential.ServiceUrl, Is.EqualTo("http://google.nl"));
Assert.That(passwordCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("http://google.nl"));
Assert.That(passwordCredential.Username, Is.EqualTo("email@example.tld"));
Assert.That(passwordCredential.Password, Is.EqualTo("password"));
Assert.That(passwordCredential.FolderPath, Is.EqualTo("Business"));
@@ -701,7 +727,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(secureNote.ServiceName, Is.EqualTo("SecureNote1"));
Assert.That(secureNote.ServiceUrl, Is.Null);
Assert.That(secureNote.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(secureNote.Username, Is.Empty);
Assert.That(secureNote.Password, Is.Empty);
Assert.That(secureNote.ItemType, Is.EqualTo(ImportedItemType.Note));
@@ -817,7 +843,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(credential3.ServiceName, Is.EqualTo("credential3"));
Assert.That(credential3.ServiceUrl, Is.Empty);
Assert.That(credential3.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(credential3.Username, Is.EqualTo("username3"));
Assert.That(credential3.Password, Is.Empty);
Assert.That(credential3.Notes, Is.EqualTo("without password"));
@@ -836,7 +862,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(service2Credential.ServiceName, Is.EqualTo("service2"));
Assert.That(service2Credential.ServiceUrl, Is.EqualTo("https://service2.com"));
Assert.That(service2Credential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://service2.com"));
Assert.That(service2Credential.Username, Is.EqualTo("username2"));
Assert.That(service2Credential.Password, Is.EqualTo("password2"));
Assert.That(service2Credential.Notes, Is.Empty);
@@ -853,7 +879,7 @@ public class ImportExportTests
Assert.Multiple(() =>
{
Assert.That(service1Credential.ServiceName, Is.EqualTo("service1"));
Assert.That(service1Credential.ServiceUrl, Is.Empty);
Assert.That(service1Credential.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(service1Credential.Username, Is.EqualTo("username1"));
Assert.That(service1Credential.Password, Is.EqualTo("password1"));
Assert.That(service1Credential.Notes, Is.EqualTo("notes1"));
@@ -1126,6 +1152,118 @@ public class ImportExportTests
Assert.That(folderNames, Is.Not.Null, "Should return a set (even if empty)");
}
/// <summary>
/// Test case for importing credentials from RoboForm CSV and ensuring all values are present.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task ImportCredentialsFromRoboformCsv()
{
// Arrange
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.roboform.csv");
// Act
var importedCredentials = await RoboformImporter.ImportFromCsvAsync(fileContent);
// Assert - Should import 4 records
Assert.That(importedCredentials, Has.Count.EqualTo(4));
// Test regular login credential
var comCredential = importedCredentials.First(c => c.ServiceName == "Com");
Assert.Multiple(() =>
{
Assert.That(comCredential.ServiceName, Is.EqualTo("Com"));
Assert.That(comCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://www.example.com.com"));
Assert.That(comCredential.Username, Is.EqualTo("username1"));
Assert.That(comCredential.Password, Is.EqualTo("password1"));
Assert.That(comCredential.Notes, Is.Null.Or.Empty);
Assert.That(comCredential.FolderPath, Is.Null);
Assert.That(comCredential.ItemType, Is.EqualTo(ImportedItemType.Login));
});
// Test credential with note
var exampleCredential = importedCredentials.First(c => c.ServiceName == "Example");
Assert.Multiple(() =>
{
Assert.That(exampleCredential.ServiceName, Is.EqualTo("Example"));
Assert.That(exampleCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://www.example.com"));
Assert.That(exampleCredential.Username, Is.EqualTo("exampleusername"));
Assert.That(exampleCredential.Password, Is.EqualTo("examplepassword"));
Assert.That(exampleCredential.Notes, Is.EqualTo("Examplenote"));
Assert.That(exampleCredential.FolderPath, Is.Null);
Assert.That(exampleCredential.ItemType, Is.EqualTo(ImportedItemType.Login));
});
// Test secure note (no URL, login, or password)
var safeNoteCredential = importedCredentials.First(c => c.ServiceName == "Safenotename");
Assert.Multiple(() =>
{
Assert.That(safeNoteCredential.ServiceName, Is.EqualTo("Safenotename"));
Assert.That(safeNoteCredential.ServiceUrls?.FirstOrDefault(), Is.Null);
Assert.That(safeNoteCredential.Username, Is.Null.Or.Empty);
Assert.That(safeNoteCredential.Password, Is.Null.Or.Empty);
Assert.That(safeNoteCredential.Notes, Is.EqualTo("Safenote content example here"));
Assert.That(safeNoteCredential.ItemType, Is.EqualTo(ImportedItemType.Note));
});
// Test credential in folder
var businessCredential = importedCredentials.First(c => c.ServiceName == "Business");
Assert.Multiple(() =>
{
Assert.That(businessCredential.ServiceName, Is.EqualTo("Business"));
Assert.That(businessCredential.ServiceUrls?.FirstOrDefault(), Is.EqualTo("https://www.business.com"));
Assert.That(businessCredential.Username, Is.EqualTo("businessusername"));
Assert.That(businessCredential.Password, Is.EqualTo("businesspassword"));
Assert.That(businessCredential.FolderPath, Is.EqualTo("Business"));
Assert.That(businessCredential.ItemType, Is.EqualTo(ImportedItemType.Login));
});
}
/// <summary>
/// Test case for RoboForm folder import.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task RoboformFolderImport()
{
// Arrange
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.roboform.csv");
// Act
var importedCredentials = await RoboformImporter.ImportFromCsvAsync(fileContent);
// Assert - verify folder path is extracted (leading slash removed)
var folderNames = BaseImporter.CollectUniqueFolderNames(importedCredentials);
Assert.That(folderNames, Does.Contain("Business"));
var credentialWithFolder = importedCredentials.First(c => c.FolderPath == "Business");
Assert.That(credentialWithFolder.ServiceName, Is.EqualTo("Business"));
}
/// <summary>
/// Test case for RoboForm secure note detection.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task RoboformSecureNoteDetection()
{
// Arrange
var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.roboform.csv");
// Act
var importedCredentials = await RoboformImporter.ImportFromCsvAsync(fileContent);
var items = BaseImporter.ConvertToItem(importedCredentials);
// Assert - verify secure note is detected
var secureNoteItem = items.FirstOrDefault(i => i.Name == "Safenotename");
Assert.That(secureNoteItem, Is.Not.Null, "Should find Safenotename");
Assert.That(secureNoteItem!.ItemType, Is.EqualTo(ItemType.Note), "Secure note should have Note item type");
// Verify the note content is preserved
var notesFieldValue = secureNoteItem.FieldValues.FirstOrDefault(fv => fv.FieldKey == FieldKey.NotesContent);
Assert.That(notesFieldValue?.Value, Is.EqualTo("Safenote content example here"));
}
/// <summary>
/// Helper method to add a field value to an item.
/// </summary>

View File

@@ -138,6 +138,29 @@ public static class BaseImporter
return decoded;
}
/// <summary>
/// Parses a URL string that may contain multiple comma-separated URLs.
/// Many password managers export multiple URIs as comma-separated values within quotes.
/// </summary>
/// <param name="url">The URL string to parse.</param>
/// <returns>A list of URLs, or null if the input is empty.</returns>
public static List<string>? ParseUrls(string? url)
{
if (string.IsNullOrWhiteSpace(url))
{
return null;
}
// Split by comma and filter out empty entries
var urls = url.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(u => u.Trim())
.Where(u => !string.IsNullOrWhiteSpace(u))
.ToList();
return urls.Count > 0 ? urls : null;
}
/// <summary>
/// Converts a list of imported credentials to a list of AliasVault Items.
/// </summary>
@@ -185,7 +208,7 @@ public static class BaseImporter
else
{
// Add standard field values for non-empty fields (Login, Alias, Note types)
AddFieldValueIfNotEmpty(item, FieldKey.LoginUrl, importedCredential.ServiceUrl, createdAt, updatedAt);
AddUrlFieldValues(item, importedCredential.ServiceUrls, createdAt, updatedAt);
AddFieldValueIfNotEmpty(item, FieldKey.LoginUsername, importedCredential.Username, createdAt, updatedAt);
AddFieldValueIfNotEmpty(item, FieldKey.LoginPassword, importedCredential.Password, createdAt, updatedAt);
AddFieldValueIfNotEmpty(item, FieldKey.LoginEmail, importedCredential.Email, createdAt, updatedAt);
@@ -354,6 +377,39 @@ public static class BaseImporter
AddFieldValueIfNotEmpty(item, FieldKey.CardExpiryYear, card.ExpiryYear, createdAt, updatedAt);
}
/// <summary>
/// Adds URL field values to an item, supporting multiple URLs with proper weight ordering.
/// </summary>
/// <param name="item">The item to add the field values to.</param>
/// <param name="urls">The list of URLs to add.</param>
/// <param name="createdAt">The created timestamp.</param>
/// <param name="updatedAt">The updated timestamp.</param>
private static void AddUrlFieldValues(Item item, List<string>? urls, DateTime createdAt, DateTime updatedAt)
{
if (urls == null || urls.Count == 0)
{
return;
}
var weight = 0;
foreach (var url in urls)
{
if (!string.IsNullOrEmpty(url))
{
item.FieldValues.Add(new FieldValue
{
Id = Guid.NewGuid(),
ItemId = item.Id,
FieldKey = FieldKey.LoginUrl,
Value = url,
Weight = weight++,
CreatedAt = createdAt,
UpdatedAt = updatedAt,
});
}
}
}
/// <summary>
/// Adds a field value to an item if the value is not empty.
/// </summary>

View File

@@ -33,7 +33,7 @@ public static class BitwardenImporter
var credential = new ImportedCredential
{
ServiceName = record.Title,
ServiceUrl = record.URL,
ServiceUrls = BaseImporter.ParseUrls(record.URL),
Username = record.Username,
Password = record.Password,
TwoFactorSecret = record.OTPAuth,

View File

@@ -33,7 +33,7 @@ public static class ChromeImporter
var credential = new ImportedCredential
{
ServiceName = record.Name,
ServiceUrl = record.Url,
ServiceUrls = BaseImporter.ParseUrls(record.Url),
Username = record.Username,
Password = record.Password,
Notes = record.Note

View File

@@ -30,7 +30,7 @@ public static class DashlaneImporter
var credential = new ImportedCredential
{
ServiceName = record.Title,
ServiceUrl = record.URL,
ServiceUrls = BaseImporter.ParseUrls(record.URL),
Username = record.Username,
Password = record.Password,
TwoFactorSecret = record.OTPUrl,

View File

@@ -36,7 +36,7 @@ public static class DropboxImporter
var credential = new ImportedCredential
{
ServiceName = record.Name,
ServiceUrl = NormalizeUrl(record.Url),
ServiceUrls = BaseImporter.ParseUrls(record.Url),
Username = record.Username,
Password = record.Password,
Notes = record.Notes
@@ -47,19 +47,4 @@ public static class DropboxImporter
return credentials;
}
/// <summary>
/// Normalizes URL values from Dropbox CSV format.
/// </summary>
/// <param name="url">The URL from the CSV record.</param>
/// <returns>The normalized URL or null if it's empty or invalid.</returns>
private static string? NormalizeUrl(string? url)
{
if (string.IsNullOrWhiteSpace(url))
{
return null;
}
return url;
}
}

View File

@@ -37,7 +37,7 @@ public static class FirefoxImporter
var credential = new ImportedCredential
{
ServiceName = serviceName,
ServiceUrl = record.Url,
ServiceUrls = BaseImporter.ParseUrls(record.Url),
Username = record.Username,
Password = record.Password
};

View File

@@ -66,7 +66,7 @@ public static class GenericCsvImporter
var credential = new ImportedCredential
{
ServiceName = record.ServiceName.Trim(),
ServiceUrl = NormalizeUrl(record.Url),
ServiceUrls = BaseImporter.ParseUrls(NormalizeUrl(record.Url)),
Username = record.Username?.Trim(),
Password = record.Password?.Trim(),
TwoFactorSecret = record.TotpSecret?.Trim(),

View File

@@ -81,7 +81,7 @@ public static class KeePassImporter
var credential = new ImportedCredential
{
ServiceName = record.Account ?? string.Empty,
ServiceUrl = record.Website,
ServiceUrls = BaseImporter.ParseUrls(record.Website),
Username = record.LoginName,
Password = record.Password,
Notes = record.Comments

View File

@@ -30,7 +30,7 @@ public static class KeePassXcImporter
var credential = new ImportedCredential
{
ServiceName = record.Title,
ServiceUrl = record.URL,
ServiceUrls = BaseImporter.ParseUrls(record.URL),
Username = record.Username,
Password = record.Password,
TwoFactorSecret = record.TOTP,

View File

@@ -61,7 +61,7 @@ public static class LastPassImporter
var credential = new ImportedCredential
{
ServiceName = record.Title,
ServiceUrl = normalizedUrl,
ServiceUrls = BaseImporter.ParseUrls(normalizedUrl),
Username = record.Username,
Password = record.Password,
TwoFactorSecret = record.TwoFactorSecret,

View File

@@ -55,7 +55,7 @@ public static class NordPassImporter
var credential = new ImportedCredential
{
ServiceName = record.Name,
ServiceUrl = string.IsNullOrWhiteSpace(record.Url) ? null : record.Url,
ServiceUrls = BaseImporter.ParseUrls(record.Url),
Email = record.Email,
Username = record.Username,
Password = record.Password,

View File

@@ -30,7 +30,7 @@ public static class OnePasswordImporter
var credential = new ImportedCredential
{
ServiceName = record.Title,
ServiceUrl = record.Url,
ServiceUrls = BaseImporter.ParseUrls(record.Url),
Username = record.Username,
Password = record.Password,
TwoFactorSecret = record.OTPAuth,

View File

@@ -30,7 +30,7 @@ public static class ProtonPassImporter
var credential = new ImportedCredential
{
ServiceName = record.Name,
ServiceUrl = record.Url,
ServiceUrls = BaseImporter.ParseUrls(record.Url),
Email = record.Email,
Username = record.Username,
Password = record.Password,

View File

@@ -30,7 +30,7 @@ public static class StrongboxImporter
var credential = new ImportedCredential
{
ServiceName = record.Title,
ServiceUrl = record.URL,
ServiceUrls = BaseImporter.ParseUrls(record.URL),
Username = record.Username,
Password = record.Password,
TwoFactorSecret = record.OTPAuth,

View File

@@ -9,6 +9,7 @@ namespace AliasVault.ImportExport;
using AliasClientDb;
using AliasClientDb.Models;
using AliasVault.ImportExport.Importers;
using AliasVault.ImportExport.Models;
using CsvHelper;
using CsvHelper.Configuration;
@@ -98,7 +99,7 @@ public static class ItemCsvService
var credential = new ImportedCredential
{
ServiceName = record.ServiceName,
ServiceUrl = record.ServiceUrl,
ServiceUrls = BaseImporter.ParseUrls(record.ServiceUrl),
Username = record.Username,
Password = record.CurrentPassword,
Email = record.AliasEmail,

View File

@@ -20,9 +20,9 @@ public class ImportedCredential
public string? ServiceName { get; set; }
/// <summary>
/// Gets or sets the service URL.
/// Gets or sets the service URLs.
/// </summary>
public string? ServiceUrl { get; set; }
public List<string>? ServiceUrls { get; set; }
/// <summary>
/// Gets or sets the username or email.