From 1102017ed63039304e2bae34f9fc3fd685d65219 Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Fri, 17 Oct 2025 11:01:27 +0300 Subject: [PATCH 01/14] Do not log if new value equals old one --- Core/Internal/Models/Account.cs | 10 ++++++- Core/Internal/Models/AutoSave.cs | 28 ++++++++----------- Core/Internal/Models/Database.cs | 4 +-- Core/Internal/Models/Service.cs | 3 ++ Core/Internal/Models/User.cs | 9 ++++-- Core/Internal/Utils/LogCenter.cs | 8 +----- Core/Public/Interfaces/ILog.cs | 6 ++-- Core/Public/Interfaces/IPasswordFactory.cs | 12 ++++---- .../Public/Interfaces/ISerializationCenter.cs | 24 ++++++++++++++++ Core/Public/Utils/ClipboardManager.cs | 8 +++--- Core/Public/Utils/CryptographyCenter.cs | 18 ++++++------ UnitTests/Models/AccountUnitTests.cs | 14 ++++++++-- UnitTests/Models/DatabaseUnitTests.cs | 6 ++-- UnitTests/Models/ServiceUnitTests.cs | 6 ++++ UnitTests/Models/UserUnitTests.cs | 10 +++++++ UnitTests/UnitTests.csproj | 10 +++++++ UnitTests/UnitTestsHelper.cs | 4 +-- .../Utils/CryptographyCenterUnitTexts.cs | 19 +++++++++++++ 18 files changed, 140 insertions(+), 59 deletions(-) diff --git a/Core/Internal/Models/Account.cs b/Core/Internal/Models/Account.cs index a96fd67..d09fd78 100644 --- a/Core/Internal/Models/Account.cs +++ b/Core/Internal/Models/Account.cs @@ -19,6 +19,7 @@ string IAccount.Label itemName: ToString(), fieldName: nameof(Label), needsReview: false, + oldValue: Label, value: value, readableValue: value); } @@ -30,6 +31,7 @@ string[] IAccount.Identifiants itemName: ToString(), fieldName: nameof(Identifiants), needsReview: true, + oldValue: Identifiants, value: value, readableValue: $"({string.Join(", ", value)})"); } @@ -39,8 +41,10 @@ string IAccount.Password get => Database.Get(Password); set { - if (!string.IsNullOrEmpty(value)) + if (!string.IsNullOrEmpty(value) + && Password != value) { + Dictionary oldPasswords = ISerializationCenter.Clone(Database.SerializationCenter, Passwords); Passwords[DateTime.Now] = Password = value; if (_service != null) @@ -49,6 +53,7 @@ string IAccount.Password itemName: ToString(), fieldName: nameof(Password), needsReview: true, + oldValue: oldPasswords, value: Passwords, readableValue: string.Empty); } @@ -65,6 +70,7 @@ string IAccount.Notes itemName: ToString(), fieldName: nameof(Notes), needsReview: false, + oldValue: Notes, value: value, readableValue: value); } @@ -76,6 +82,7 @@ int IAccount.PasswordUpdateReminderDelay itemName: ToString(), fieldName: nameof(PasswordUpdateReminderDelay), needsReview: false, + oldValue: PasswordUpdateReminderDelay, value: value, readableValue: value.ToString()); } @@ -87,6 +94,7 @@ AccountOption IAccount.Options itemName: ToString(), fieldName: nameof(Options), needsReview: false, + oldValue: Options, value: value, readableValue: value.ToString()); } diff --git a/Core/Internal/Models/AutoSave.cs b/Core/Internal/Models/AutoSave.cs index e1c73b1..a50404e 100644 --- a/Core/Internal/Models/AutoSave.cs +++ b/Core/Internal/Models/AutoSave.cs @@ -1,4 +1,5 @@ using Upsilon.Apps.PassKey.Core.Internal.Utils; +using Upsilon.Apps.PassKey.Core.Public.Interfaces; namespace Upsilon.Apps.PassKey.Core.Internal.Models { @@ -13,9 +14,12 @@ internal Database Database public Queue Changes { get; set; } = new(); - internal T UpdateValue(string itemId, string itemName, string fieldName, bool needsReview, T value, string readableValue) where T : notnull + internal T UpdateValue(string itemId, string itemName, string fieldName, bool needsReview, T oldValue, T value, string readableValue) where T : notnull { - _addChange(itemId, itemName, string.Empty, fieldName, Database.SerializationCenter.Serialize(value), readableValue, needsReview, Change.Type.Update); + if (ISerializationCenter.AreDifferent(Database.SerializationCenter, oldValue, value)) + { + _addChange(itemId, itemName, string.Empty, fieldName, Database.SerializationCenter.Serialize(value), readableValue, needsReview, Change.Type.Update); + } return value; } @@ -50,22 +54,12 @@ private void _addChange(string itemId, string itemName, string containerName, st } Database.AutoSaveFileLocker.Save(this, Database.Passkeys); - string logMessage; - - switch (action) + string logMessage = action switch { - case Change.Type.Add: - logMessage = $"{itemName} has been added to {containerName}"; - break; - case Change.Type.Delete: - logMessage = $"{itemName} has been removed from {containerName}"; - break; - case Change.Type.Update: - default: - logMessage = $"{itemName}'s {fieldName.ToSentenceCase().ToLower()} has been {(string.IsNullOrWhiteSpace(readableValue) ? $"updated" : $"set to {readableValue}")}"; - break; - } - + Change.Type.Add => $"{itemName} has been added to {containerName}", + Change.Type.Delete => $"{itemName} has been removed from {containerName}", + _ => $"{itemName}'s {fieldName.ToSentenceCase().ToLower()} has been {(string.IsNullOrWhiteSpace(readableValue) ? $"updated" : $"set to {readableValue}")}", + }; Database.Logs.AddLog(logMessage, needsReview); } diff --git a/Core/Internal/Models/Database.cs b/Core/Internal/Models/Database.cs index 2979a2b..9acd1b2 100644 --- a/Core/Internal/Models/Database.cs +++ b/Core/Internal/Models/Database.cs @@ -374,7 +374,7 @@ private Warning[] _lookAtPasswordUpdateReminderWarnings() .Where(x => x.PasswordExpired) .ToArray(); - return accounts.Length != 0 ? ([new Warning(WarningType.PasswordUpdateReminderWarning, accounts)]) : ([]); + return accounts.Length != 0 ? [new Warning(WarningType.PasswordUpdateReminderWarning, accounts)] : []; } private Warning[] _lookAtPasswordLeakedWarnings() @@ -386,7 +386,7 @@ private Warning[] _lookAtPasswordLeakedWarnings() .Where(x => x.PasswordLeaked) .ToArray(); - return accounts.Length != 0 ? ([new Warning(WarningType.PasswordLeakedWarning, accounts)]) : ([]); + return accounts.Length != 0 ? [new Warning(WarningType.PasswordLeakedWarning, accounts)] : []; } private Warning[] _lookAtDuplicatedPasswordsWarnings() diff --git a/Core/Internal/Models/Service.cs b/Core/Internal/Models/Service.cs index 8ba94e8..f40d93a 100644 --- a/Core/Internal/Models/Service.cs +++ b/Core/Internal/Models/Service.cs @@ -18,6 +18,7 @@ string IService.ServiceName itemName: ToString(), fieldName: nameof(ServiceName), needsReview: true, + oldValue: ServiceName, value: value, readableValue: value); } @@ -29,6 +30,7 @@ string IService.Url itemName: ToString(), fieldName: nameof(Url), needsReview: false, + oldValue: Url, value: value, readableValue: value); } @@ -40,6 +42,7 @@ string IService.Notes itemName: ToString(), fieldName: nameof(Notes), needsReview: false, + oldValue: Notes, value: value, readableValue: value); } diff --git a/Core/Internal/Models/User.cs b/Core/Internal/Models/User.cs index 893bde0..97cfc88 100644 --- a/Core/Internal/Models/User.cs +++ b/Core/Internal/Models/User.cs @@ -19,6 +19,7 @@ string IUser.Username itemName: ToString(), fieldName: nameof(Username), needsReview: true, + oldValue: Username, value: value, readableValue: value); } @@ -30,6 +31,7 @@ string[] IUser.Passkeys itemName: ToString(), fieldName: nameof(Passkeys), needsReview: true, + oldValue: Passkeys, value: value, readableValue: string.Empty); } @@ -41,6 +43,7 @@ int IUser.LogoutTimeout itemName: ToString(), fieldName: nameof(LogoutTimeout), needsReview: false, + oldValue: LogoutTimeout, value: value, readableValue: value.ToString()); } @@ -54,6 +57,7 @@ int IUser.CleaningClipboardTimeout itemName: ToString(), fieldName: nameof(CleaningClipboardTimeout), needsReview: false, + oldValue: CleaningClipboardTimeout, value: value, readableValue: value.ToString()); } @@ -65,6 +69,7 @@ WarningType IUser.WarningsToNotify itemName: ToString(), fieldName: nameof(WarningsToNotify), needsReview: true, + oldValue: WarningsToNotify, value: value, readableValue: value.ToString()); } @@ -169,9 +174,9 @@ private void _timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) private void _cleanClipboard() { - var passwords = Services.SelectMany(x => x.Accounts).Select(x => x.Password).ToArray(); + string[] passwords = [.. Services.SelectMany(x => x.Accounts).Select(x => x.Password)]; - var cleanedPasswordsCount = ClipboardManager.RemoveAllOccurence(passwords); + int cleanedPasswordsCount = ClipboardManager.RemoveAllOccurence(passwords); if (cleanedPasswordsCount != 0) { diff --git a/Core/Internal/Utils/LogCenter.cs b/Core/Internal/Utils/LogCenter.cs index 816f7c5..b8b226f 100644 --- a/Core/Internal/Utils/LogCenter.cs +++ b/Core/Internal/Utils/LogCenter.cs @@ -14,11 +14,7 @@ internal Database Database } [JsonIgnore] - public ILog[]? Logs - { - get - { - return Database.User == null + public ILog[]? Logs => Database.User == null ? null : LogList.Select(x => { @@ -27,8 +23,6 @@ public ILog[]? Logs }) .OrderByDescending(x => x.DateTime) .ToArray(); - } - } public List LogList { get; set; } = []; public string Username { get; set; } = string.Empty; diff --git a/Core/Public/Interfaces/ILog.cs b/Core/Public/Interfaces/ILog.cs index 60d3639..232ed72 100644 --- a/Core/Public/Interfaces/ILog.cs +++ b/Core/Public/Interfaces/ILog.cs @@ -8,16 +8,16 @@ public interface ILog /// /// The date and time the event occured. /// - public DateTime DateTime { get; } + DateTime DateTime { get; } /// /// The event message. /// - public string Message { get; } + string Message { get; } /// /// Indicate if the current log needs review. /// - public bool NeedsReview { get; set; } + bool NeedsReview { get; set; } } } diff --git a/Core/Public/Interfaces/IPasswordFactory.cs b/Core/Public/Interfaces/IPasswordFactory.cs index 504f633..45b562f 100644 --- a/Core/Public/Interfaces/IPasswordFactory.cs +++ b/Core/Public/Interfaces/IPasswordFactory.cs @@ -8,17 +8,17 @@ public interface IPasswordFactory /// /// The letters used by the factory. /// - public string Alphabetic { get; } + string Alphabetic { get; } /// /// The digits used by the factory. /// - public string Numeric { get; } + string Numeric { get; } /// /// The special characters used by the factory. /// - public string SpecialChars { get; } + string SpecialChars { get; } /// /// Generate a random password. @@ -31,7 +31,7 @@ public interface IPasswordFactory /// Exclude some specific characters. /// Ensure that the generated password has been already leaked. /// The random geenrated password. - public string GeneratePassword(int length, + string GeneratePassword(int length, bool includeUpperCaseAlphabeticChars = true, bool includeLowerCaseAlphabeticChars = true, bool includeNumericChars = true, @@ -46,7 +46,7 @@ public string GeneratePassword(int length, /// The alphabet used. /// Ensure that the generated password has been already leaked. /// The random geenrated password. - public string GeneratePassword(int length, + string GeneratePassword(int length, string alphabet, bool checkIfLeaked = true); @@ -55,6 +55,6 @@ public string GeneratePassword(int length, /// /// The password to check. /// Returns true if the password has been leaked. - public bool PasswordLeaked(string password); + bool PasswordLeaked(string password); } } diff --git a/Core/Public/Interfaces/ISerializationCenter.cs b/Core/Public/Interfaces/ISerializationCenter.cs index b76e8ac..c39e24d 100644 --- a/Core/Public/Interfaces/ISerializationCenter.cs +++ b/Core/Public/Interfaces/ISerializationCenter.cs @@ -20,5 +20,29 @@ public interface ISerializationCenter /// The serialised string. /// The deserialized object. T Deserialize(string toDeserialize) where T : notnull; + + /// + /// Check if two objects are different or not. + /// + /// The Serialization Center. + /// The first object. + /// The second object. + /// True if the two objects are different, False else. + static bool AreDifferent(ISerializationCenter serializationCenter, object object1, object object2) + { + return serializationCenter.Serialize(object1) != serializationCenter.Serialize(object2); + } + + /// + /// Clone the given object. + /// + /// The type of the object to clone. + /// The Serialization Center. + /// The object to clone. + /// The clone of the object. + static T Clone(ISerializationCenter serializationCenter, T source) where T : notnull + { + return serializationCenter.Deserialize(serializationCenter.Serialize(source)); + } } } diff --git a/Core/Public/Utils/ClipboardManager.cs b/Core/Public/Utils/ClipboardManager.cs index 170c649..09f9452 100644 --- a/Core/Public/Utils/ClipboardManager.cs +++ b/Core/Public/Utils/ClipboardManager.cs @@ -8,18 +8,18 @@ public static int RemoveAllOccurence(string[] removeList) { int cleanedPasswordCount = 0; - var clipboardHistory = Clipboard.GetHistoryItemsAsync().AsTask().GetAwaiter().GetResult().Items; + IReadOnlyList clipboardHistory = Clipboard.GetHistoryItemsAsync().AsTask().GetAwaiter().GetResult().Items; - foreach (var item in clipboardHistory) + foreach (ClipboardHistoryItem? item in clipboardHistory) { - var content = item.Content; + DataPackageView content = item.Content; if (content.Contains(StandardDataFormats.Text)) { string text = content.GetTextAsync().AsTask().GetAwaiter().GetResult(); if (removeList.Any(x => x == text)) { - Clipboard.DeleteItemFromHistory(item); + _ = Clipboard.DeleteItemFromHistory(item); cleanedPasswordCount++; } } diff --git a/Core/Public/Utils/CryptographyCenter.cs b/Core/Public/Utils/CryptographyCenter.cs index a40faf6..d870141 100644 --- a/Core/Public/Utils/CryptographyCenter.cs +++ b/Core/Public/Utils/CryptographyCenter.cs @@ -17,7 +17,7 @@ public string GetHash(string source) public string GetSlowHash(string source) { - long realTimeFactor = (long)Math.Pow(0b1000, 5); + long realTimeFactor = (long)Math.Pow(0b1001, 6); for (int i = 0; i < realTimeFactor; i++) { @@ -113,7 +113,7 @@ public string EncryptAsymmetrically(string source, string key) string aesKey = Encoding.UTF8.GetString(randomBytes); source = EncryptSymmetrically(source, [aesKey]); aesKey = _encryptRsa(aesKey, csp); - var s = new KeyValuePair(aesKey, source); + KeyValuePair s = new(aesKey, source); source = JsonSerializer.Serialize(s); Sign(ref source); @@ -137,7 +137,7 @@ public string DecryptAsymmetrically(string source, string key) csp.ImportParameters(privKey); - var s = JsonSerializer.Deserialize>(source); + KeyValuePair s = JsonSerializer.Deserialize>(source); string aesKey = _decryptRsa(s.Key, 0, csp); source = DecryptSymmetrically(s.Value, [aesKey]); @@ -151,11 +151,9 @@ private string _cipherAes(string plainText, string key) return plainText; } - MD5 mD5 = MD5.Create(); - - key = Encoding.ASCII.GetString(mD5.ComputeHash(Encoding.ASCII.GetBytes(key))); - key += Encoding.ASCII.GetString(mD5.ComputeHash(Encoding.ASCII.GetBytes(key))); - key += Encoding.ASCII.GetString(mD5.ComputeHash(Encoding.ASCII.GetBytes(key))); + key = Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + key += Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + key += Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); byte[] _key = Encoding.ASCII.GetBytes(key[..32]); byte[] IV = Encoding.ASCII.GetBytes(key.Substring(32, 16)); @@ -220,7 +218,7 @@ private string _uncitherAes(byte[] cipherText, byte[] key, byte[] IV) private string _encryptAes(string source, string[] passwords) { - passwords = passwords.Select(x => GetHash(x)).ToArray(); + passwords = passwords.Select(GetHash).ToArray(); for (int i = passwords.Length - 1; i >= 0; i--) { @@ -236,7 +234,7 @@ private string _encryptAes(string source, string[] passwords) private string _decryptAes(string source, string[] passwords) { - passwords = passwords.Select(x => GetHash(x)).ToArray(); + passwords = passwords.Select(GetHash).ToArray(); try { diff --git a/UnitTests/Models/AccountUnitTests.cs b/UnitTests/Models/AccountUnitTests.cs index 4b687ed..c30cefa 100644 --- a/UnitTests/Models/AccountUnitTests.cs +++ b/UnitTests/Models/AccountUnitTests.cs @@ -29,7 +29,7 @@ public void Case01_AddAccountUpdateSaved() string newPassword = UnitTestsHelper.GetRandomString(); string notes = UnitTestsHelper.GetRandomString(); int passwordUpdateReminderDelay = UnitTestsHelper.GetRandomInt(12); - AccountOption options = AccountOption.WarnIfPasswordLeaked; + AccountOption options = AccountOption.None; Stack expectedLogs = new(); Stack expectedLogWarnings = new(); @@ -46,18 +46,23 @@ public void Case01_AddAccountUpdateSaved() // When account.Label = newAccountLabel; + account.Label = newAccountLabel; expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiants)})'s label has been set to {newAccountLabel}"); account.Identifiants = newIdentifiants; + account.Identifiants = newIdentifiants; expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiants)})'s identifiants has been set to ({string.Join(", ", newIdentifiants)})"); expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiants)})'s identifiants has been set to ({string.Join(", ", newIdentifiants)})"); account.Password = newPassword; + account.Password = newPassword; expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password has been updated"); expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password has been updated"); account.Notes = notes; + account.Notes = notes; expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s notes has been set to {notes}"); account.PasswordUpdateReminderDelay = passwordUpdateReminderDelay; expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password update reminder delay has been set to {passwordUpdateReminderDelay}"); account.Options = options; + account.Options = options; expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s options has been set to {options}"); databaseCreated.Save(); @@ -116,7 +121,7 @@ public void Case02_AddAccountUpdateAutoSave() string newPassword = UnitTestsHelper.GetRandomString(); string notes = UnitTestsHelper.GetRandomString(); int passwordUpdateReminderDelay = UnitTestsHelper.GetRandomInt(12); - AccountOption options = AccountOption.WarnIfPasswordLeaked; + AccountOption options = AccountOption.None; Stack expectedLogs = new(); Stack expectedLogWarnings = new(); @@ -133,18 +138,23 @@ public void Case02_AddAccountUpdateAutoSave() // When account.Label = newAccountLabel; + account.Label = newAccountLabel; expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiants)})'s label has been set to {newAccountLabel}"); account.Identifiants = newIdentifiants; + account.Identifiants = newIdentifiants; expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiants)})'s identifiants has been set to ({string.Join(", ", newIdentifiants)})"); expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiants)})'s identifiants has been set to ({string.Join(", ", newIdentifiants)})"); account.Password = newPassword; + account.Password = newPassword; expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password has been updated"); expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password has been updated"); account.Notes = notes; + account.Notes = notes; expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s notes has been set to {notes}"); account.PasswordUpdateReminderDelay = passwordUpdateReminderDelay; expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password update reminder delay has been set to {passwordUpdateReminderDelay}"); account.Options = options; + account.Options = options; expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s options has been set to {options}"); databaseCreated.Close(); diff --git a/UnitTests/Models/DatabaseUnitTests.cs b/UnitTests/Models/DatabaseUnitTests.cs index e4c6fac..8e7f359 100644 --- a/UnitTests/Models/DatabaseUnitTests.cs +++ b/UnitTests/Models/DatabaseUnitTests.cs @@ -286,7 +286,7 @@ public void Case05_DatabaseAutoLogout() Thread.Sleep(500); } - int elapsedTime = (int)(DateTime.Now - start).TotalMinutes; + int elapsedTime = (int)(DateTime.Now - start).TotalSeconds; // Then _ = closedDueToTimeout.Should().BeTrue(); @@ -295,8 +295,8 @@ public void Case05_DatabaseAutoLogout() database = UnitTestsHelper.OpenTestDatabase(passkeys, out _); // Then - elapsedTime.Should().Be(database.User.LogoutTimeout); - database.Logs.FirstOrDefault(x => x.Message == $"User {username}'s login session timeout reached" && x.NeedsReview).Should().NotBeNull(); + _ = elapsedTime.Should().BeLessThanOrEqualTo(database.User.LogoutTimeout * 60); + _ = database.Logs.FirstOrDefault(x => x.Message == $"User {username}'s login session timeout reached" && x.NeedsReview).Should().NotBeNull(); // Finaly database.Close(); diff --git a/UnitTests/Models/ServiceUnitTests.cs b/UnitTests/Models/ServiceUnitTests.cs index cee41f5..1d1e797 100644 --- a/UnitTests/Models/ServiceUnitTests.cs +++ b/UnitTests/Models/ServiceUnitTests.cs @@ -36,11 +36,14 @@ public void Case01_AddServiceUpdateSaved() // When service.ServiceName = newServiceName; + service.ServiceName = newServiceName; expectedLogs.Push($"Warning : Service {oldServiceName}'s service name has been set to {newServiceName}"); expectedLogWarnings.Push($"Warning : Service {oldServiceName}'s service name has been set to {newServiceName}"); service.Url = url; + service.Url = url; expectedLogs.Push($"Information : Service {newServiceName}'s url has been set to {url}"); service.Notes = notes; + service.Notes = notes; expectedLogs.Push($"Information : Service {newServiceName}'s notes has been set to {notes}"); databaseCreated.Save(); @@ -101,11 +104,14 @@ public void Case02_AddServiceUpdateAutoSave() // When service.ServiceName = newServiceName; + service.ServiceName = newServiceName; expectedLogs.Push($"Warning : Service {oldServiceName}'s service name has been set to {newServiceName}"); expectedLogWarnings.Push($"Warning : Service {oldServiceName}'s service name has been set to {newServiceName}"); service.Url = url; + service.Url = url; expectedLogs.Push($"Information : Service {newServiceName}'s url has been set to {url}"); service.Notes = notes; + service.Notes = notes; expectedLogs.Push($"Information : Service {newServiceName}'s notes has been set to {notes}"); databaseCreated.Close(); diff --git a/UnitTests/Models/UserUnitTests.cs b/UnitTests/Models/UserUnitTests.cs index 91a7fc0..91684e7 100644 --- a/UnitTests/Models/UserUnitTests.cs +++ b/UnitTests/Models/UserUnitTests.cs @@ -65,16 +65,21 @@ public void Case02_UserUpdateThenSaved() // When databaseCreated.User.Username = newUsername; + databaseCreated.User.Username = newUsername; expectedLogs.Push($"Warning : User {oldUsername}'s username has been set to {newUsername}"); expectedLogWarnings.Push($"Warning : User {oldUsername}'s username has been set to {newUsername}"); databaseCreated.User.Passkeys = newPasskeys; + databaseCreated.User.Passkeys = newPasskeys; expectedLogs.Push($"Warning : User {oldUsername}'s passkeys has been updated"); expectedLogWarnings.Push($"Warning : User {oldUsername}'s passkeys has been updated"); databaseCreated.User.LogoutTimeout = logoutTimeout; + databaseCreated.User.LogoutTimeout = logoutTimeout; expectedLogs.Push($"Information : User {oldUsername}'s logout timeout has been set to {logoutTimeout}"); databaseCreated.User.CleaningClipboardTimeout = cleaningClipboardTimeout; + databaseCreated.User.CleaningClipboardTimeout = cleaningClipboardTimeout; expectedLogs.Push($"Information : User {oldUsername}'s cleaning clipboard timeout has been set to {cleaningClipboardTimeout}"); databaseCreated.User.WarningsToNotify = WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning; + databaseCreated.User.WarningsToNotify = WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning; expectedLogs.Push($"Warning : User {oldUsername}'s warnings to notify has been set to {WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning}"); expectedLogWarnings.Push($"Warning : User {oldUsername}'s warnings to notify has been set to {WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning}"); @@ -150,16 +155,21 @@ public void Case03_UserUpdateButNotSaved() // When databaseCreated.User.Username = newUsername; + databaseCreated.User.Username = newUsername; expectedLogs.Push($"Warning : User {oldUsername}'s username has been set to {newUsername}"); expectedLogWarnings.Push($"Warning : User {oldUsername}'s username has been set to {newUsername}"); databaseCreated.User.Passkeys = newPasskeys; + databaseCreated.User.Passkeys = newPasskeys; expectedLogs.Push($"Warning : User {oldUsername}'s passkeys has been updated"); expectedLogWarnings.Push($"Warning : User {oldUsername}'s passkeys has been updated"); databaseCreated.User.LogoutTimeout = logoutTimeout; + databaseCreated.User.LogoutTimeout = logoutTimeout; expectedLogs.Push($"Information : User {oldUsername}'s logout timeout has been set to {logoutTimeout}"); databaseCreated.User.CleaningClipboardTimeout = cleaningClipboardTimeout; + databaseCreated.User.CleaningClipboardTimeout = cleaningClipboardTimeout; expectedLogs.Push($"Information : User {oldUsername}'s cleaning clipboard timeout has been set to {cleaningClipboardTimeout}"); databaseCreated.User.WarningsToNotify = WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning; + databaseCreated.User.WarningsToNotify = WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning; expectedLogs.Push($"Warning : User {oldUsername}'s warnings to notify has been set to {WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning}"); expectedLogWarnings.Push($"Warning : User {oldUsername}'s warnings to notify has been set to {WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning}"); diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 7f61773..1ca8dd9 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -7,6 +7,16 @@ enable + + + 0 + + + + + 0 + + diff --git a/UnitTests/UnitTestsHelper.cs b/UnitTests/UnitTestsHelper.cs index cc085b3..e11e399 100644 --- a/UnitTests/UnitTestsHelper.cs +++ b/UnitTests/UnitTestsHelper.cs @@ -1,6 +1,6 @@ -using System.Runtime.CompilerServices; +using FluentAssertions; +using System.Runtime.CompilerServices; using System.Security.Cryptography; -using FluentAssertions; using Upsilon.Apps.PassKey.Core.Public.Enums; using Upsilon.Apps.PassKey.Core.Public.Interfaces; using Upsilon.Apps.PassKey.Core.Public.Utils; diff --git a/UnitTests/Utils/CryptographyCenterUnitTexts.cs b/UnitTests/Utils/CryptographyCenterUnitTexts.cs index 67a1c09..75c2075 100644 --- a/UnitTests/Utils/CryptographyCenterUnitTexts.cs +++ b/UnitTests/Utils/CryptographyCenterUnitTexts.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using System.Diagnostics; using Upsilon.Apps.PassKey.Core.Public.Utils; namespace Upsilon.Apps.PassKey.UnitTests.Utils @@ -6,6 +7,24 @@ namespace Upsilon.Apps.PassKey.UnitTests.Utils [TestClass] public sealed class CryptographyCenterUnitTexts { + [TestMethod] + /* + * Signing an empty string returns the hash code of that empty string, + * Then checking the signature returns the empty string. + */ + public void Case00_SlowHash() + { + // Given + Stopwatch _stopwatch = Stopwatch.StartNew(); + + // When + _ = UnitTestsHelper.CryptographicCenter.GetSlowHash(string.Empty); + _stopwatch.Stop(); + + // Then + _ = _stopwatch.ElapsedMilliseconds.Should().BeGreaterThan(500); + } + [TestMethod] /* * Signing an empty string returns the hash code of that empty string, From c398fbf8beed80eb5da1b27ae8aa5b9501a01a3d Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Mon, 1 Dec 2025 10:55:10 +0300 Subject: [PATCH 02/14] Align Core with GUI --- Core/Internal/Models/Account.cs | 65 +- Core/Internal/Models/AutoSave.cs | 145 ++++- Core/Internal/Models/Change.cs | 6 +- Core/Internal/Models/Database.cs | 223 +++++-- Core/Internal/Models/Log.cs | 4 +- Core/Internal/Models/Service.cs | 59 +- Core/Internal/Models/User.cs | 135 ++-- Core/Internal/Models/Warning.cs | 6 +- Core/Internal/Utils/FileLocker.cs | 47 +- Core/Internal/Utils/ImportExportHelper.cs | 199 ++++++ Core/Internal/Utils/LogCenter.cs | 21 +- Core/Internal/Utils/StaticMethods.cs | 9 +- Core/Public/Enums/AccountOption.cs | 2 +- Core/Public/Enums/AutoSaveMergeBehavior.cs | 10 +- Core/Public/Enums/WarningType.cs | 2 +- .../Events/AutoSaveDetectedEventArgs.cs | 6 +- Core/Public/Events/LogoutEventArgs.cs | 2 +- .../Public/Events/WarningDetectedEventArgs.cs | 4 +- Core/Public/Interfaces/IAccount.cs | 6 +- Core/Public/Interfaces/ICryptographyCenter.cs | 2 +- Core/Public/Interfaces/IDatabase.cs | 44 +- Core/Public/Interfaces/IItem.cs | 7 +- Core/Public/Interfaces/ILog.cs | 2 +- Core/Public/Interfaces/IPasswordFactory.cs | 2 +- .../Public/Interfaces/ISerializationCenter.cs | 8 +- Core/Public/Interfaces/IService.cs | 29 +- Core/Public/Interfaces/IUser.cs | 17 +- Core/Public/Interfaces/IWarning.cs | 4 +- Core/Public/Interfaces/Interfaces.cd | 40 +- Core/Public/Utils/ClipboardManager.cs | 2 +- Core/Public/Utils/CryptographyCenter.cs | 86 +-- Core/Public/Utils/Exceptions.cs | 2 +- Core/Public/Utils/JsonSerializationCenter.cs | 17 +- Core/Public/Utils/PasswordFactory.cs | 11 +- Core/Public/Utils/StaticMethods.cs | 17 + Core/Upsilon.Apps.Passkey.Core.csproj | 2 +- UnitTests/Models/AccountUnitTests.cs | 123 ++-- UnitTests/Models/DatabaseUnitTests.cs | 68 +- UnitTests/Models/ImportExportUnitTests.cs | 598 ++++++++++++++++++ UnitTests/Models/ServiceUnitTests.cs | 35 +- UnitTests/Models/UserUnitTests.cs | 142 ++++- UnitTests/TestFiles/import.csv | 5 + UnitTests/TestFiles/import.json | 78 +++ UnitTests/TestFiles/import_MissingCollumn.csv | 5 + UnitTests/TestFiles/import_MissingHearder.csv | 5 + UnitTests/TestFiles/import_WrongFormat.json | 52 ++ .../TestFiles/import_blanckIdentifier.json | 52 ++ UnitTests/TestFiles/import_blanckService.csv | 5 + UnitTests/TestFiles/import_noData.csv | 1 + UnitTests/UnitTests.csproj | 10 +- UnitTests/UnitTestsHelper.cs | 48 +- .../Utils/CryptographyCenterUnitTexts.cs | 41 +- 52 files changed, 2079 insertions(+), 432 deletions(-) create mode 100644 Core/Internal/Utils/ImportExportHelper.cs create mode 100644 Core/Public/Utils/StaticMethods.cs create mode 100644 UnitTests/Models/ImportExportUnitTests.cs create mode 100644 UnitTests/TestFiles/import.csv create mode 100644 UnitTests/TestFiles/import.json create mode 100644 UnitTests/TestFiles/import_MissingCollumn.csv create mode 100644 UnitTests/TestFiles/import_MissingHearder.csv create mode 100644 UnitTests/TestFiles/import_WrongFormat.json create mode 100644 UnitTests/TestFiles/import_blanckIdentifier.json create mode 100644 UnitTests/TestFiles/import_blanckService.csv create mode 100644 UnitTests/TestFiles/import_noData.csv diff --git a/Core/Internal/Models/Account.cs b/Core/Internal/Models/Account.cs index d09fd78..1b45e72 100644 --- a/Core/Internal/Models/Account.cs +++ b/Core/Internal/Models/Account.cs @@ -1,15 +1,18 @@ using System.ComponentModel; -using Upsilon.Apps.PassKey.Core.Internal.Utils; -using Upsilon.Apps.PassKey.Core.Public.Enums; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Internal.Utils; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Internal.Models { internal sealed class Account : IAccount { #region IAccount interface explicit Internal string IItem.ItemId => Database.Get(ItemId); + + IDatabase IItem.Database => Database; + IService IAccount.Service => Database.Get(Service); string IAccount.Label @@ -20,19 +23,19 @@ string IAccount.Label fieldName: nameof(Label), needsReview: false, oldValue: Label, - value: value, + newValue: value, readableValue: value); } - string[] IAccount.Identifiants + string[] IAccount.Identifiers { - get => Database.Get(Identifiants); - set => Identifiants = Database.AutoSave.UpdateValue(ItemId, + get => Database.Get(Identifiers); + set => Identifiers = Database.AutoSave.UpdateValue(ItemId, itemName: ToString(), - fieldName: nameof(Identifiants), + fieldName: nameof(Identifiers), needsReview: true, - oldValue: Identifiants, - value: value, + oldValue: Identifiers, + newValue: value, readableValue: $"({string.Join(", ", value)})"); } @@ -49,12 +52,26 @@ string IAccount.Password if (_service != null) { + if (Service.User.NumberOfOldPasswordToKeep != 0) + { + DateTime[] datesToRemove = [.. Passwords.Keys + .OrderBy(x => x) + .Take(Passwords.Count > Service.User.NumberOfOldPasswordToKeep + ? Passwords.Count - Service.User.NumberOfOldPasswordToKeep + : 0)]; + + foreach (DateTime dateToRemove in datesToRemove) + { + _ = Passwords.Remove(dateToRemove); + } + } + _ = Database.AutoSave.UpdateValue(ItemId, itemName: ToString(), fieldName: nameof(Password), needsReview: true, oldValue: oldPasswords, - value: Passwords, + newValue: Passwords, readableValue: string.Empty); } } @@ -71,7 +88,7 @@ string IAccount.Notes fieldName: nameof(Notes), needsReview: false, oldValue: Notes, - value: value, + newValue: value, readableValue: value); } @@ -83,7 +100,7 @@ int IAccount.PasswordUpdateReminderDelay fieldName: nameof(PasswordUpdateReminderDelay), needsReview: false, oldValue: PasswordUpdateReminderDelay, - value: value, + newValue: value, readableValue: value.ToString()); } @@ -95,7 +112,7 @@ AccountOption IAccount.Options fieldName: nameof(Options), needsReview: false, oldValue: Options, - value: value, + newValue: value, readableValue: value.ToString()); } @@ -113,7 +130,7 @@ internal Service Service } public string Label { get; set; } = string.Empty; - public string[] Identifiants { get; set; } = []; + public string[] Identifiers { get; set; } = []; public string Password { get; set; } = string.Empty; public Dictionary Passwords { get; set; } = []; public string Notes { get; set; } = string.Empty; @@ -144,23 +161,23 @@ public void Apply(Change change) switch (change.FieldName) { case nameof(Label): - Label = Database.SerializationCenter.Deserialize(change.Value); + Label = change.NewValue.DeserializeTo(Database.SerializationCenter); break; - case nameof(Identifiants): - Identifiants = Database.SerializationCenter.Deserialize(change.Value); + case nameof(Identifiers): + Identifiers = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(Notes): - Notes = Database.SerializationCenter.Deserialize(change.Value); + Notes = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(Password): - Passwords = Database.SerializationCenter.Deserialize>(change.Value); + Passwords = change.NewValue.DeserializeTo>(Database.SerializationCenter); Password = Passwords.Count != 0 ? Passwords[Passwords.Keys.Max()] : string.Empty; break; case nameof(PasswordUpdateReminderDelay): - PasswordUpdateReminderDelay = Database.SerializationCenter.Deserialize(change.Value); + PasswordUpdateReminderDelay = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(Options): - Options = Database.SerializationCenter.Deserialize(change.Value); + Options = change.NewValue.DeserializeTo(Database.SerializationCenter); break; default: throw new InvalidDataException("FieldName not valid"); @@ -180,7 +197,7 @@ public override string ToString() account += $"{Label} "; } - return account + $"({string.Join(", ", Identifiants)})"; + return account + $"({string.Join(", ", Identifiers)})"; } } } \ No newline at end of file diff --git a/Core/Internal/Models/AutoSave.cs b/Core/Internal/Models/AutoSave.cs index a50404e..1a34baa 100644 --- a/Core/Internal/Models/AutoSave.cs +++ b/Core/Internal/Models/AutoSave.cs @@ -1,52 +1,111 @@ -using Upsilon.Apps.PassKey.Core.Internal.Utils; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Internal.Utils; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Internal.Models { internal sealed class AutoSave { - private Database? _database; internal Database Database { - get => _database ?? throw new NullReferenceException(nameof(Database)); - set => _database = value; + get => field ?? throw new NullReferenceException(nameof(Database)); + set; } - public Queue Changes { get; set; } = new(); + public Dictionary> Changes { get; set; } = []; - internal T UpdateValue(string itemId, string itemName, string fieldName, bool needsReview, T oldValue, T value, string readableValue) where T : notnull + internal T UpdateValue(string itemId, + string itemName, + string fieldName, + bool needsReview, + T oldValue, + T newValue, + string readableValue) where T : notnull { - if (ISerializationCenter.AreDifferent(Database.SerializationCenter, oldValue, value)) + if (ISerializationCenter.AreDifferent(Database.SerializationCenter, oldValue, newValue)) { - _addChange(itemId, itemName, string.Empty, fieldName, Database.SerializationCenter.Serialize(value), readableValue, needsReview, Change.Type.Update); + _addChange(itemId, + itemName, + string.Empty, + fieldName, + oldValue.SerializeWith(Database.SerializationCenter), + newValue.SerializeWith(Database.SerializationCenter), + readableValue, + needsReview, + Change.Type.Update); } - return value; + return newValue; } - internal T AddValue(string itemId, string itemName, string containerName, bool needsReview, T value) where T : notnull + internal T AddValue(string itemId, + string itemName, + string containerName, + bool needsReview, + T value) where T : notnull { - _addChange(itemId, itemName, containerName, string.Empty, Database.SerializationCenter.Serialize(value), string.Empty, needsReview, Change.Type.Add); + _addChange(itemId, itemName, containerName, string.Empty, value.SerializeWith(Database.SerializationCenter), string.Empty, needsReview, Change.Type.Add); return value; } - internal T DeleteValue(string itemId, string itemName, string containerName, bool needsReview, T value) where T : notnull + internal T DeleteValue(string itemId, + string itemName, + string containerName, + bool needsReview, + T value) where T : notnull { - _addChange(itemId, itemName, containerName, string.Empty, Database.SerializationCenter.Serialize(value), string.Empty, needsReview, Change.Type.Delete); + _addChange(itemId, itemName, containerName, string.Empty, value.SerializeWith(Database.SerializationCenter), string.Empty, needsReview, Change.Type.Delete); return value; } - private void _addChange(string itemId, string itemName, string containerName, string fieldName, string value, string readableValue, bool needsReview, Change.Type action) + private void _addChange(string itemId, + string itemName, + string containerName, + string fieldName, + string newValue, + string readableValue, + bool needsReview, + Change.Type action) + { + _addChange(itemId, + itemName, + containerName, + fieldName, + null, + newValue, + readableValue, + needsReview, + action); + } + + private void _addChange(string itemId, + string itemName, + string containerName, + string fieldName, + string? oldValue, + string newValue, + string readableValue, + bool needsReview, + Change.Type action) { - Changes.Enqueue(new Change + string changeKey = $"{itemId}\t{fieldName}"; + if (!Changes.ContainsKey(changeKey)) + { + Changes[changeKey] = []; + } + + Change currentChange = new() { + Index = DateTime.Now.Ticks, ActionType = action, ItemId = itemId, FieldName = fieldName, - Value = value, - }); + OldValue = oldValue, + NewValue = newValue, + }; + + _mergeChanges(changeKey, currentChange); if (Database.AutoSaveFileLocker == null) { @@ -63,24 +122,60 @@ private void _addChange(string itemId, string itemName, string containerName, st Database.Logs.AddLog(logMessage, needsReview); } - internal void MergeChange() + private void _mergeChanges(string changeKey, Change currentChange) { - while (Changes.Count != 0) + Change? lastUpdate = Changes[changeKey].LastOrDefault(x => x.ActionType == Change.Type.Update); + + if (currentChange.ActionType != Change.Type.Update + || lastUpdate is null) { - Database.User?.Apply(Changes.Dequeue()); + Changes[changeKey].Add(currentChange); + return; } - Clear(); + _ = Changes[changeKey].Remove(lastUpdate); + currentChange.OldValue = lastUpdate.OldValue; + + if (currentChange.OldValue != currentChange.NewValue) + { + Changes[changeKey].Add(currentChange); + } + else if (Changes[changeKey].Count == 0) + { + _ = Changes.Remove(changeKey); + } } - internal void Clear() + internal void ApplyChanges(bool deleteFile) + { + List changes = Changes.Values.SelectMany(x => x).OrderBy(x => x.Index).ToList(); + + foreach (Change change in changes) + { + Database.User?.Apply(change); + } + + if (deleteFile) + { + Clear(deleteFile: true); + } + } + + internal bool Any() => Any(string.Empty); + + internal bool Any(string itemId) => Changes.Any(x => x.Key.StartsWith(itemId)); + + internal bool Any(string itemId, string fieldName) => Changes.Any(x => x.Key == $"{itemId}\t{fieldName}"); + + internal void Clear(bool deleteFile) { Changes.Clear(); Database.AutoSaveFileLocker?.Dispose(); Database.AutoSaveFileLocker = null; - if (File.Exists(Database.AutoSaveFile)) + if (deleteFile + && File.Exists(Database.AutoSaveFile)) { File.Delete(Database.AutoSaveFile); } diff --git a/Core/Internal/Models/Change.cs b/Core/Internal/Models/Change.cs index ac79abc..eb15356 100644 --- a/Core/Internal/Models/Change.cs +++ b/Core/Internal/Models/Change.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Internal.Models { internal sealed class Change { @@ -10,9 +10,11 @@ public enum Type Delete = 3, } + public long Index { get; set; } = long.MaxValue; public Type ActionType { get; set; } = Type.None; public string ItemId { get; set; } = string.Empty; public string FieldName { get; set; } = string.Empty; - public string Value { get; set; } = string.Empty; + public string? OldValue { get; set; } = null; + public string NewValue { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/Core/Internal/Models/Database.cs b/Core/Internal/Models/Database.cs index 9acd1b2..f3ef8f9 100644 --- a/Core/Internal/Models/Database.cs +++ b/Core/Internal/Models/Database.cs @@ -1,10 +1,10 @@ -using Upsilon.Apps.PassKey.Core.Internal.Utils; -using Upsilon.Apps.PassKey.Core.Public.Enums; -using Upsilon.Apps.PassKey.Core.Public.Events; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; -using Upsilon.Apps.PassKey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Internal.Utils; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Events; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Utils; -namespace Upsilon.Apps.PassKey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Internal.Models { internal sealed class Database : IDatabase { @@ -15,6 +15,7 @@ internal sealed class Database : IDatabase public string LogFile { get; set; } IUser? IDatabase.User => Get(User); + int? IDatabase.SessionLeftTime => User?.SessionLeftTime; ILog[]? IDatabase.Logs => Get(Logs.Logs); @@ -31,7 +32,7 @@ internal sealed class Database : IDatabase public void Delete() { - if (User == null) throw new NullReferenceException(nameof(User)); + if (User is null) throw new NullReferenceException(nameof(User)); DatabaseFileLocker?.Delete(); LogFileLocker?.Delete(); @@ -80,21 +81,7 @@ public void Delete() _handleAutoSave(eventArg.MergeBehavior); } - Warning[] logWarnings = _lookAtLogWarnings(); - Warning[] passwordUpdateReminderWarnings = _lookAtPasswordUpdateReminderWarnings(); - Warning[] passwordLeakedWarnings = _lookAtPasswordLeakedWarnings(); - Warning[] duplicatedPasswordsWarnings = _lookAtDuplicatedPasswordsWarnings(); - - Warnings = [..logWarnings, - ..passwordUpdateReminderWarnings, - ..passwordLeakedWarnings, - ..duplicatedPasswordsWarnings]; - - WarningDetected?.Invoke(this, new WarningDetectedEventArgs( - [..User.WarningsToNotify.ContainsFlag(WarningType.LogReviewWarning) ? logWarnings : [], - ..User.WarningsToNotify.ContainsFlag(WarningType.PasswordUpdateReminderWarning) ? passwordUpdateReminderWarnings : [], - ..User.WarningsToNotify.ContainsFlag(WarningType.PasswordLeakedWarning) ? passwordLeakedWarnings : [], - ..User.WarningsToNotify.ContainsFlag(WarningType.DuplicatedPasswordsWarning) ? duplicatedPasswordsWarnings : []])); + _ = Task.Run(_lookAtWarnings); User.ResetTimer(); } @@ -104,6 +91,90 @@ public void Delete() public void Close() => Dispose(); + public bool HasChanged() => User is not null && HasChanged(User.ItemId); + + public bool HasChanged(string itemId) => AutoSave.Any(itemId); + + public bool HasChanged(string itemId, string fieldName) => AutoSave.Any(itemId, fieldName); + + public bool ImportFromFile(string filePath) + { + _save(logSaveEvent: true); + Logs.AddLog($"Importing data from file : '{filePath}'", needsReview: true); + + string importContent = string.Empty; + string errorLog = string.Empty; + + try + { + importContent = File.ReadAllText(filePath); + } + catch + { + errorLog = $"import file is not accessible"; + } + + if (string.IsNullOrWhiteSpace(errorLog)) + { + string extention = Path.GetExtension(filePath); + + errorLog = extention switch + { + ".json" => this.ImportJson(importContent), + ".csv" => this.ImportCSV(importContent), + _ => $"'{extention}' extention type is not handled", + }; + } + + if (string.IsNullOrWhiteSpace(errorLog)) + { + Logs.AddLog($"Import completed successfully", needsReview: true); + _save(logSaveEvent: true); + } + else + { + Logs.AddLog($"Import failed because {errorLog}", needsReview: true); + } + + return string.IsNullOrWhiteSpace(errorLog); + } + + public bool ExportToFile(string filePath) + { + _save(logSaveEvent: true); + Logs.AddLog($"Exporting data to file : '{filePath}'", needsReview: true); + + string errorLog = string.Empty; + + if (File.Exists(filePath)) + { + errorLog = $"export file already exists"; + } + + if (string.IsNullOrWhiteSpace(errorLog)) + { + string extention = Path.GetExtension(filePath); + + errorLog = extention switch + { + ".json" => this.ExportJson(filePath), + ".csv" => this.ExportCSV(filePath), + _ => $"'{extention}' extention type is not handled", + }; + } + + if (string.IsNullOrWhiteSpace(errorLog)) + { + Logs.AddLog($"Export completed successfully", needsReview: true); + } + else + { + Logs.AddLog($"Export failed because {errorLog}", needsReview: true); + } + + return string.IsNullOrWhiteSpace(errorLog); + } + #endregion internal User? User; @@ -212,15 +283,7 @@ internal static IDatabase Create(ICryptographyCenter cryptographicCenter, database._save(logSaveEvent: false); - database.Close(logCloseEvent: false, loginTimeoutReached: false); - - return Open(cryptographicCenter, - serializationCenter, - passwordFactory, - databaseFile, - autoSaveFile, - logFile, - username); + return database; } internal static IDatabase Open(ICryptographyCenter cryptographicCenter, @@ -254,28 +317,39 @@ internal T Get(T value) private void _save(bool logSaveEvent) { - if (User == null) throw new NullReferenceException(nameof(User)); + _saveLogs(); + _saveDatabase(logSaveEvent); + } + + private void _saveDatabase(bool logSaveEvent) + { + if (User is null) throw new NullReferenceException(nameof(User)); if (DatabaseFileLocker == null) throw new NullReferenceException(nameof(DatabaseFileLocker)); Username = User.Username; - Passkeys = [CryptographyCenter.GetHash(User.Username), .. User.Passkeys.Select(x => CryptographyCenter.GetSlowHash(x))]; + Passkeys = [CryptographyCenter.GetHash(User.Username), .. User.Passkeys.Select(CryptographyCenter.GetSlowHash)]; DatabaseFileLocker.Save(User, Passkeys); - Logs.Username = Username; - LogFileLocker?.Save(Logs, [CryptographyCenter.GetHash(User.Username)]); - if (logSaveEvent) { Logs.AddLog($"User {Username}'s database saved", needsReview: false); } - AutoSave.Clear(); + AutoSave.Clear(deleteFile: true); User.ResetTimer(); DatabaseSaved?.Invoke(this, EventArgs.Empty); } + private void _saveLogs() + { + if (User is null) throw new NullReferenceException(nameof(User)); + + Logs.Username = User.Username; + LogFileLocker?.Save(Logs, [CryptographyCenter.GetHash(User.Username)]); + } + internal void Close(bool logCloseEvent, bool loginTimeoutReached) { if (logCloseEvent) @@ -283,12 +357,16 @@ internal void Close(bool logCloseEvent, bool loginTimeoutReached) if (User != null) { string logoutLog = $"User {Username} logged out"; - bool needsReview = AutoSave.Changes.Count != 0; + bool needsReview = AutoSave.Any(); if (needsReview) { logoutLog += " without saving"; } + else + { + AutoSave.Clear(deleteFile: true); + } Logs.AddLog(logoutLog, needsReview); } @@ -297,7 +375,6 @@ internal void Close(bool logCloseEvent, bool loginTimeoutReached) } User = null; - AutoSave.Changes.Clear(); Username = string.Empty; Passkeys = []; Warnings = null; @@ -308,8 +385,7 @@ internal void Close(bool logCloseEvent, bool loginTimeoutReached) LogFileLocker?.Dispose(); LogFileLocker = null; - AutoSaveFileLocker?.Dispose(); - AutoSaveFileLocker = null; + AutoSave.Clear(deleteFile: false); DatabaseFile = string.Empty; AutoSaveFile = string.Empty; @@ -320,7 +396,7 @@ internal void Close(bool logCloseEvent, bool loginTimeoutReached) private void _handleAutoSave(AutoSaveMergeBehavior mergeAutoSave) { - if (User == null) throw new NullReferenceException(nameof(User)); + if (User is null) throw new NullReferenceException(nameof(User)); if (!File.Exists(AutoSaveFile)) { @@ -329,13 +405,18 @@ private void _handleAutoSave(AutoSaveMergeBehavior mergeAutoSave) switch (mergeAutoSave) { - case AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile: - AutoSave.MergeChange(); - Logs.AddLog($"User {Username}'s autosave merged", needsReview: true); + case AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile: + AutoSave.ApplyChanges(deleteFile: true); + Logs.AddLog($"User {Username}'s autosave merged and saved", needsReview: true); _save(logSaveEvent: false); break; + case AutoSaveMergeBehavior.MergeWithoutSavingAndKeepAutoSaveFile: + AutoSave.ApplyChanges(deleteFile: false); + Logs.AddLog($"User {Username}'s autosave merged without saving", needsReview: true); + _saveLogs(); + break; case AutoSaveMergeBehavior.DontMergeAndRemoveAutoSaveFile: - AutoSave.Clear(); + AutoSave.Clear(deleteFile: true); Logs.AddLog($"User {Username}'s autosave not merged and removed", needsReview: true); break; case AutoSaveMergeBehavior.DontMergeAndKeepAutoSaveFile: @@ -345,12 +426,37 @@ private void _handleAutoSave(AutoSaveMergeBehavior mergeAutoSave) } } + private void _lookAtWarnings() + { + if (User is null) return; + + try + { + Warning[] logWarnings = _lookAtLogWarnings(); + Warning[] passwordUpdateReminderWarnings = _lookAtPasswordUpdateReminderWarnings(); + Warning[] passwordLeakedWarnings = _lookAtPasswordLeakedWarnings(); + Warning[] duplicatedPasswordsWarnings = _lookAtDuplicatedPasswordsWarnings(); + + Warnings = [..logWarnings, + ..passwordUpdateReminderWarnings, + ..passwordLeakedWarnings, + ..duplicatedPasswordsWarnings]; + + WarningDetected?.Invoke(this, new WarningDetectedEventArgs( + [..User.WarningsToNotify.ContainsFlag(WarningType.LogReviewWarning) ? logWarnings : [], + ..User.WarningsToNotify.ContainsFlag(WarningType.PasswordUpdateReminderWarning) ? passwordUpdateReminderWarnings : [], + ..User.WarningsToNotify.ContainsFlag(WarningType.PasswordLeakedWarning) ? passwordLeakedWarnings : [], + ..User.WarningsToNotify.ContainsFlag(WarningType.DuplicatedPasswordsWarning) ? duplicatedPasswordsWarnings : []])); + } + catch { } + } + private Warning[] _lookAtLogWarnings() { - if (User == null) throw new NullReferenceException(nameof(User)); + if (User is null) throw new NullReferenceException(nameof(User)); if (Logs.Logs == null) throw new NullReferenceException(nameof(Logs.Logs)); - List logs = Logs.Logs.Cast().ToList(); + List logs = [.. Logs.Logs.Cast()]; for (int i = 0; i < logs.Count && logs[i].Message != $"User {Username} logged in"; i++) { @@ -367,37 +473,34 @@ private Warning[] _lookAtLogWarnings() private Warning[] _lookAtPasswordUpdateReminderWarnings() { - if (User == null) throw new NullReferenceException(nameof(User)); + if (User is null) return []; - Account[] accounts = User.Services + Account[] accounts = [.. User.Services .SelectMany(x => x.Accounts) - .Where(x => x.PasswordExpired) - .ToArray(); + .Where(x => x.PasswordExpired)]; return accounts.Length != 0 ? [new Warning(WarningType.PasswordUpdateReminderWarning, accounts)] : []; } private Warning[] _lookAtPasswordLeakedWarnings() { - if (User == null) throw new NullReferenceException(nameof(User)); + if (User is null) return []; - Account[] accounts = User.Services + Account[] accounts = [.. User.Services .SelectMany(x => x.Accounts) - .Where(x => x.PasswordLeaked) - .ToArray(); + .Where(x => x.PasswordLeaked)]; return accounts.Length != 0 ? [new Warning(WarningType.PasswordLeakedWarning, accounts)] : []; } private Warning[] _lookAtDuplicatedPasswordsWarnings() { - if (User == null) throw new NullReferenceException(nameof(User)); + if (User is null) return []; - IGrouping[] duplicatedPasswords = User.Services + IGrouping[] duplicatedPasswords = [.. User.Services .SelectMany(x => x.Accounts) .GroupBy(x => x.Password) - .Where(x => x.Count() > 1) - .ToArray(); + .Where(x => x.Count() > 1)]; List warnings = []; diff --git a/Core/Internal/Models/Log.cs b/Core/Internal/Models/Log.cs index 0f6bf63..852c8ed 100644 --- a/Core/Internal/Models/Log.cs +++ b/Core/Internal/Models/Log.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Internal.Models { internal class Log : ILog { diff --git a/Core/Internal/Models/Service.cs b/Core/Internal/Models/Service.cs index f40d93a..cdbc462 100644 --- a/Core/Internal/Models/Service.cs +++ b/Core/Internal/Models/Service.cs @@ -1,13 +1,17 @@ using System.ComponentModel; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Internal.Utils; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Internal.Models { internal sealed class Service : IService { #region IService interface explicit Internal string IItem.ItemId => Database.Get(ItemId); + + IDatabase IItem.Database => Database; + IUser IService.User => Database.Get(User); IAccount[] IService.Accounts => [.. Database.Get(Accounts)]; @@ -19,7 +23,7 @@ string IService.ServiceName fieldName: nameof(ServiceName), needsReview: true, oldValue: ServiceName, - value: value, + newValue: value, readableValue: value); } @@ -31,7 +35,7 @@ string IService.Url fieldName: nameof(Url), needsReview: false, oldValue: Url, - value: value, + newValue: value, readableValue: value); } @@ -43,27 +47,49 @@ string IService.Notes fieldName: nameof(Notes), needsReview: false, oldValue: Notes, - value: value, + newValue: value, readableValue: value); } - IAccount IService.AddAccount(string label, IEnumerable identifiants, string password) + public IAccount AddAccount(string label, IEnumerable identifiers, string password) { Account account = new() { Service = this, - ItemId = ItemId + Database.CryptographyCenter.GetHash(label + string.Join(string.Empty, identifiants)), + ItemId = ItemId + Database.CryptographyCenter.GetHash(label + string.Join(string.Empty, identifiers)), Label = label, - Identifiants = identifiants.ToArray(), + Identifiers = [.. identifiers], Password = password, }; - account.Passwords[DateTime.Now] = password; Accounts.Add(Database.AutoSave.AddValue(ItemId, itemName: account.ToString(), containerName: ToString(), needsReview: false, account)); + account.Passwords[DateTime.Now] = Database.AutoSave.UpdateValue(account.ItemId, + itemName: account.ToString(), + fieldName: nameof(account.Password), + needsReview: true, + oldValue: string.Empty, + newValue: account.Password, + readableValue: string.Empty); + return account; } + public IAccount AddAccount(string label, IEnumerable identifiers) + { + return AddAccount(label, identifiers, password: string.Empty); + } + + public IAccount AddAccount(IEnumerable identifiers, string password) + { + return AddAccount(label: string.Empty, identifiers, password); + } + + public IAccount AddAccount(IEnumerable identifiers) + { + return AddAccount(label: string.Empty, identifiers, password: string.Empty); + } + void IService.DeleteAccount(IAccount account) { Account accountToRemove = Accounts.FirstOrDefault(x => x.ItemId == account.ItemId) @@ -79,13 +105,12 @@ void IService.DeleteAccount(IAccount account) public string ItemId { get; set; } = string.Empty; - private User? _user; internal User User { - get => _user ?? throw new NullReferenceException(nameof(User)); + get => field ?? throw new NullReferenceException(nameof(User)); set { - _user = value; + field = value; foreach (Account account in Accounts) { @@ -126,25 +151,25 @@ private void _apply(Change change) switch (change.FieldName) { case nameof(ServiceName): - ServiceName = Database.SerializationCenter.Deserialize(change.Value); + ServiceName = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(Url): - Url = Database.SerializationCenter.Deserialize(change.Value); + Url = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(Notes): - Notes = Database.SerializationCenter.Deserialize(change.Value); + Notes = change.NewValue.DeserializeTo(Database.SerializationCenter); break; default: throw new InvalidDataException("FieldName not valid"); } break; case Change.Type.Add: - Account accountToAdd = Database.SerializationCenter.Deserialize(change.Value); + Account accountToAdd = change.NewValue.DeserializeTo(Database.SerializationCenter); accountToAdd.Service = this; Accounts.Add(accountToAdd); break; case Change.Type.Delete: - Account accountToDelete = Database.SerializationCenter.Deserialize(change.Value); + Account accountToDelete = change.NewValue.DeserializeTo(Database.SerializationCenter); _ = Accounts.RemoveAll(x => x.ItemId == accountToDelete.ItemId); break; default: diff --git a/Core/Internal/Models/User.cs b/Core/Internal/Models/User.cs index 97cfc88..e86d56d 100644 --- a/Core/Internal/Models/User.cs +++ b/Core/Internal/Models/User.cs @@ -1,39 +1,53 @@ using System.ComponentModel; -using Upsilon.Apps.PassKey.Core.Public.Enums; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; -using Upsilon.Apps.PassKey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Internal.Utils; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Utils; -namespace Upsilon.Apps.PassKey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Internal.Models { internal sealed class User : IUser { #region IUser interface explicit Internal string IItem.ItemId => Database.Get(ItemId); + + IDatabase IItem.Database => Database; + IService[] IUser.Services => [.. Database.Get(Services)]; string IUser.Username { get => Database.Get(Username); - set => Username = Database.AutoSave.UpdateValue(ItemId, - itemName: ToString(), - fieldName: nameof(Username), - needsReview: true, - oldValue: Username, - value: value, - readableValue: value); + set + { + CredentialChanged |= Username != value; + + Username = Database.AutoSave.UpdateValue(ItemId, + itemName: ToString(), + fieldName: nameof(Username), + needsReview: true, + oldValue: Username, + newValue: value, + readableValue: value); + } } string[] IUser.Passkeys { get => Database.Get(Passkeys); - set => Passkeys = Database.AutoSave.UpdateValue(ItemId, - itemName: ToString(), - fieldName: nameof(Passkeys), - needsReview: true, - oldValue: Passkeys, - value: value, - readableValue: string.Empty); + set + { + CredentialChanged |= ISerializationCenter.AreDifferent(Database.SerializationCenter, Passkeys, value); + + Passkeys = Database.AutoSave.UpdateValue(ItemId, + itemName: ToString(), + fieldName: nameof(Passkeys), + needsReview: true, + oldValue: Passkeys, + newValue: value, + readableValue: string.Empty); + } } int IUser.LogoutTimeout @@ -44,12 +58,10 @@ int IUser.LogoutTimeout fieldName: nameof(LogoutTimeout), needsReview: false, oldValue: LogoutTimeout, - value: value, + newValue: value, readableValue: value.ToString()); } - int IUser.SessionLeftTime => _sessionLeftTime; - int IUser.CleaningClipboardTimeout { get => Database.Get(CleaningClipboardTimeout); @@ -58,10 +70,53 @@ int IUser.CleaningClipboardTimeout fieldName: nameof(CleaningClipboardTimeout), needsReview: false, oldValue: CleaningClipboardTimeout, - value: value, + newValue: value, readableValue: value.ToString()); } + int IUser.ShowPasswordDelay + { + get => Database.Get(ShowPasswordDelay); + set => ShowPasswordDelay = Database.AutoSave.UpdateValue(ItemId, + itemName: ToString(), + fieldName: nameof(ShowPasswordDelay), + needsReview: false, + oldValue: ShowPasswordDelay, + newValue: value, + readableValue: value.ToString()); + } + + int IUser.NumberOfOldPasswordToKeep + { + get => Database.Get(NumberOfOldPasswordToKeep); + set + { + NumberOfOldPasswordToKeep = Database.AutoSave.UpdateValue(ItemId, + itemName: ToString(), + fieldName: nameof(NumberOfOldPasswordToKeep), + needsReview: true, + oldValue: NumberOfOldPasswordToKeep, + newValue: value, + readableValue: value.ToString()); + + if (NumberOfOldPasswordToKeep == 0) return; + + Account[] accounts = [.. Services.SelectMany(x => x.Accounts).Where(x => x.Passwords.Count > NumberOfOldPasswordToKeep)]; + + foreach (Account account in accounts) + { + DateTime[] datesToRemove = [.. account.Passwords.Keys + .OrderBy(x => x) + .Take(account.Passwords.Count - NumberOfOldPasswordToKeep)]; + + foreach (DateTime dateToRemove in datesToRemove) + { + _ = account.Passwords.Remove(dateToRemove); + } + } + } + } + WarningType IUser.WarningsToNotify { get => Database.Get(WarningsToNotify); @@ -70,7 +125,7 @@ WarningType IUser.WarningsToNotify fieldName: nameof(WarningsToNotify), needsReview: true, oldValue: WarningsToNotify, - value: value, + newValue: value, readableValue: value.ToString()); } @@ -98,13 +153,12 @@ void IUser.DeleteService(IService service) #endregion - private Database? _database; internal Database Database { - get => _database ?? throw new NullReferenceException(nameof(Database)); + get => field ?? throw new NullReferenceException(nameof(Database)); set { - _database = value; + field = value; foreach (Service service in Services) { @@ -120,8 +174,11 @@ internal Database Database public string Username { get; set; } = string.Empty; public string[] Passkeys { get; set; } = []; + public bool CredentialChanged { get; set; } = false; public int LogoutTimeout { get; set; } = 0; public int CleaningClipboardTimeout { get; set; } = 0; + public int ShowPasswordDelay { get; set; } = 0; + public int NumberOfOldPasswordToKeep { get; set; } = 0; public WarningType WarningsToNotify { get; set; } = WarningType.LogReviewWarning | WarningType.PasswordUpdateReminderWarning @@ -135,7 +192,7 @@ internal Database Database Interval = 1000, }; - private int _sessionLeftTime = 0; + public int SessionLeftTime = 0; private int _clipboardLeftTime = 0; public User() @@ -147,9 +204,9 @@ private void _timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { if (LogoutTimeout != 0) { - _sessionLeftTime--; + SessionLeftTime--; - if (_sessionLeftTime == 0) + if (SessionLeftTime == 0) { Database.Logs.AddLog($"User {Username}'s login session timeout reached", needsReview: true); Database.Close(logCloseEvent: true, loginTimeoutReached: true); @@ -174,7 +231,7 @@ private void _timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) private void _cleanClipboard() { - string[] passwords = [.. Services.SelectMany(x => x.Accounts).Select(x => x.Password)]; + string[] passwords = [.. Services.SelectMany(x => x.Accounts).SelectMany(x => x.Passwords.Values)]; int cleanedPasswordsCount = ClipboardManager.RemoveAllOccurence(passwords); @@ -186,7 +243,7 @@ private void _cleanClipboard() public void ResetTimer() { - _sessionLeftTime = LogoutTimeout * 60; + SessionLeftTime = LogoutTimeout * 60; _clipboardLeftTime = CleaningClipboardTimeout; } @@ -217,31 +274,33 @@ private void _apply(Change change) switch (change.FieldName) { case nameof(Username): - Username = Database.SerializationCenter.Deserialize(change.Value); + CredentialChanged = true; + Username = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(Passkeys): - Passkeys = Database.SerializationCenter.Deserialize(change.Value); + CredentialChanged = true; + Passkeys = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(LogoutTimeout): - LogoutTimeout = Database.SerializationCenter.Deserialize(change.Value); + LogoutTimeout = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(CleaningClipboardTimeout): - CleaningClipboardTimeout = Database.SerializationCenter.Deserialize(change.Value); + CleaningClipboardTimeout = change.NewValue.DeserializeTo(Database.SerializationCenter); break; case nameof(WarningsToNotify): - WarningsToNotify = Database.SerializationCenter.Deserialize(change.Value); + WarningsToNotify = change.NewValue.DeserializeTo(Database.SerializationCenter); break; default: throw new InvalidDataException("FieldName not valid"); } break; case Change.Type.Add: - Service serviceToAdd = Database.SerializationCenter.Deserialize(change.Value); + Service serviceToAdd = change.NewValue.DeserializeTo(Database.SerializationCenter); serviceToAdd.User = this; Services.Add(serviceToAdd); break; case Change.Type.Delete: - Service serviceToDelete = Database.SerializationCenter.Deserialize(change.Value); + Service serviceToDelete = change.NewValue.DeserializeTo(Database.SerializationCenter); _ = Services.RemoveAll(x => x.ItemId == serviceToDelete.ItemId); break; default: diff --git a/Core/Internal/Models/Warning.cs b/Core/Internal/Models/Warning.cs index 7bacef9..968b664 100644 --- a/Core/Internal/Models/Warning.cs +++ b/Core/Internal/Models/Warning.cs @@ -1,7 +1,7 @@ -using Upsilon.Apps.PassKey.Core.Public.Enums; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Internal.Models { internal class Warning : IWarning { diff --git a/Core/Internal/Utils/FileLocker.cs b/Core/Internal/Utils/FileLocker.cs index 06efaf2..c3f071f 100644 --- a/Core/Internal/Utils/FileLocker.cs +++ b/Core/Internal/Utils/FileLocker.cs @@ -1,6 +1,9 @@ -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using System.IO.Compression; +using System.Text; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Utils; -namespace Upsilon.Apps.PassKey.Core.Internal.Utils +namespace Upsilon.Apps.Passkey.Core.Internal.Utils { internal class FileLocker : IDisposable { @@ -39,7 +42,7 @@ internal string ReadAllText() { Unlock(); - string text = File.ReadAllText(FilePath); + string text = _decompressString(File.ReadAllText(FilePath)); Lock(); @@ -55,14 +58,14 @@ internal string ReadAllText(string[] passkeys) internal T Open(string[] passkeys) where T : notnull { - return _serializationCenter.Deserialize(ReadAllText(passkeys)); + return ReadAllText(passkeys).DeserializeTo(_serializationCenter); } internal void WriteAllText(string text) { Unlock(); - File.WriteAllText(FilePath, text); + File.WriteAllText(FilePath, _compressString(text)); Lock(); } @@ -76,7 +79,7 @@ internal void WriteAllText(string text, string[] passkeys) internal void Save(T obj, string[] passkeys) where T : notnull { - WriteAllText(_serializationCenter.Serialize(obj), passkeys); + WriteAllText(obj.SerializeWith(_serializationCenter), passkeys); } internal void Delete() @@ -94,5 +97,37 @@ public void Dispose() Unlock(); FilePath = string.Empty; } + + private static string _compressString(string text) + { + byte[] bytes = Encoding.UTF8.GetBytes(text); + using MemoryStream msi = new(bytes); + using MemoryStream mso = new(); + using (GZipStream gs = new(mso, CompressionLevel.SmallestSize)) + { + msi.CopyTo(gs); + } + return Convert.ToBase64String(mso.ToArray()); + } + + private static string _decompressString(string compressedText) + { + try + { + byte[] bytes = Convert.FromBase64String(compressedText); + using MemoryStream msi = new(bytes); + using MemoryStream mso = new(); + using (GZipStream gs = new(msi, CompressionMode.Decompress)) + { + gs.CopyTo(mso); + } + return Encoding.UTF8.GetString(mso.ToArray()); + } + catch + { + throw new CorruptedSourceException(); + } + } + } } diff --git a/Core/Internal/Utils/ImportExportHelper.cs b/Core/Internal/Utils/ImportExportHelper.cs new file mode 100644 index 0000000..b06fc5f --- /dev/null +++ b/Core/Internal/Utils/ImportExportHelper.cs @@ -0,0 +1,199 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Upsilon.Apps.Passkey.Core.Internal.Models; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; + +namespace Upsilon.Apps.Passkey.Core.Internal.Utils +{ + internal static class ImportExportHelper + { + private enum Headers + { + ServiceName, + ServiceUrl, + ServiceNotes, + AccountLabel, + Identifiers, + Password, + AccountNotes, + AccountOptions, + PasswordUpdateReminderDelay, + } + + private static string _jsonSerialize(T obj) + => JsonSerializer.Serialize(obj, _options); + + private static T _jsonDeserializeAs(string json) + => JsonSerializer.Deserialize(json, _options) ?? throw new NullReferenceException(); + + private static readonly JsonSerializerOptions _options = new() { Converters = { new JsonStringEnumConverter() }, WriteIndented = true, }; + + public static string ImportCSV(this IDatabase database, string importContent) + { + List services = []; + + try + { + string[] csvLines = [.. importContent.Split('\n').Select(x => x.Replace("\r", "")).Where(x => !string.IsNullOrWhiteSpace(x))]; + + string[] headers = csvLines[0].Split("\t"); + + Dictionary headersIndexes = []; + + headersIndexes[Headers.ServiceName] = headers.IndexOf(Headers.ServiceName.ToString()); + headersIndexes[Headers.ServiceUrl] = headers.IndexOf(Headers.ServiceUrl.ToString()); + headersIndexes[Headers.ServiceNotes] = headers.IndexOf(Headers.ServiceNotes.ToString()); + headersIndexes[Headers.AccountLabel] = headers.IndexOf(Headers.AccountLabel.ToString()); + headersIndexes[Headers.Identifiers] = headers.IndexOf(Headers.Identifiers.ToString()); + headersIndexes[Headers.Password] = headers.IndexOf(Headers.Password.ToString()); + headersIndexes[Headers.AccountNotes] = headers.IndexOf(Headers.AccountNotes.ToString()); + headersIndexes[Headers.AccountOptions] = headers.IndexOf(Headers.AccountOptions.ToString()); + headersIndexes[Headers.PasswordUpdateReminderDelay] = headers.IndexOf(Headers.PasswordUpdateReminderDelay.ToString()); + + if (headersIndexes.Values.Any(x => x == -1)) return $"the CSV headers should be : {string.Join(", ", headersIndexes.Keys.Select(x => $"'{x}'"))}"; + + Service? service = null; + + for (int i = 1; i < csvLines.Length; i++) + { + string csvLine = csvLines[i]; + string[] csvColumns = csvLine.Split('\t'); + string serviceName = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.ServiceName]]); + string serviceUrl = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.ServiceUrl]]); + string serviceNotes = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.ServiceNotes]]); + string accountLabel = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.AccountLabel]]); + string identifiers = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.Identifiers]]); + string password = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.Password]]); + string accountNotes = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.AccountNotes]]); + AccountOption accountOptions = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.AccountOptions]]); + int passwordUpdateReminderDelay = _jsonDeserializeAs(csvColumns[headersIndexes[Headers.PasswordUpdateReminderDelay]]); + + if (service is null + || service.ServiceName != serviceName) + { + service = new() + { + ServiceName = serviceName, + Url = serviceUrl, + Notes = serviceNotes, + }; + + services.Add(service); + } + + Account account = new() + { + Label = accountLabel, + Identifiers = [.. identifiers.Split('|').Select(x => x.Trim())], + Password = password, + Notes = accountNotes, + Options = accountOptions, + PasswordUpdateReminderDelay = passwordUpdateReminderDelay + }; + + service.Accounts.Add(account); + } + } + catch + { + return "the CSV data format is incorrect"; + } + + return _importServices(database, services); + } + + public static string ImportJson(this IDatabase database, string importContent) + { + Service[] services; + + try + { + services = _jsonDeserializeAs(importContent); + } + catch + { + return "import file deserialization failed"; + } + + return _importServices(database, services); + } + + private static string _importServices(IDatabase database, IEnumerable services) + { + if (database.User is null) return string.Empty; + + if (!services.Any()) return "there is no data to import"; + + Service? s0 = services.FirstOrDefault(x => database.User.Services.Any(y => y.ServiceName == x.ServiceName)); + if (s0 is not null) + { + return $"service '{s0.ServiceName}' already exists"; + } + + s0 = services.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.ServiceName)); + if (s0 is not null) + { + return $"service name cannot be blank"; + } + + foreach (Service s in services) + { + IService service = database.User.AddService(s.ServiceName); + service.Url = s.Url; + service.Notes = s.Notes; + + foreach (Account a in s.Accounts) + { + IAccount account = service.AddAccount(a.Label, a.Identifiers, a.Password); + account.Notes = a.Notes; + account.Options = a.Options; + account.PasswordUpdateReminderDelay = a.PasswordUpdateReminderDelay; + } + } + + return string.Empty; + } + + public static string ExportCSV(this Database database, string filePath) + { + if (database.User is null) return string.Empty; + + StringBuilder sb = new(string.Join("\t", Enum.GetNames()) + "\n"); + + foreach (Service service in database.User.Services) + { + string serviceLine = $"{_jsonSerialize(service.ServiceName.Trim())}\t" + + $"{_jsonSerialize(service.Url.Trim())}\t" + + $"{_jsonSerialize(service.Notes.Trim())}\t"; + + foreach (Account account in service.Accounts) + { + string identifiers = string.Join("|", account.Identifiers.Where(x => !string.IsNullOrWhiteSpace(x))); + + _ = sb.Append(serviceLine); + _ = sb.Append($"{_jsonSerialize(account.Label.Trim())}\t" + + $"{_jsonSerialize(identifiers)}\t" + + $"{_jsonSerialize(account.Password.Trim())}\t" + + $"{_jsonSerialize(account.Notes.Trim())}\t" + + $"{_jsonSerialize(account.Options)}\t" + + $"{_jsonSerialize(account.PasswordUpdateReminderDelay)}\n"); + } + } + + File.WriteAllText(filePath, sb.ToString()); + + return string.Empty; + } + + public static string ExportJson(this Database database, string filePath) + { + if (database.User is null) return string.Empty; + + File.WriteAllText(filePath, _jsonSerialize(database.User.Services)); + + return string.Empty; + } + } +} diff --git a/Core/Internal/Utils/LogCenter.cs b/Core/Internal/Utils/LogCenter.cs index b8b226f..b799df7 100644 --- a/Core/Internal/Utils/LogCenter.cs +++ b/Core/Internal/Utils/LogCenter.cs @@ -1,26 +1,23 @@ using System.Text.Json.Serialization; -using Upsilon.Apps.PassKey.Core.Internal.Models; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Internal.Models; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Internal.Utils +namespace Upsilon.Apps.Passkey.Core.Internal.Utils { internal class LogCenter { - private Database? _database; internal Database Database { - get => _database ?? throw new NullReferenceException(nameof(Database)); - set => _database = value; + get => field ?? throw new NullReferenceException(nameof(Database)); + set; } [JsonIgnore] public ILog[]? Logs => Database.User == null ? null - : LogList.Select(x => - { - string textLog = Database.CryptographyCenter.DecryptAsymmetrically(x, Database.User.PrivateKey); - return Database.SerializationCenter.Deserialize(textLog); - }) + : LogList.Select(x => Database.CryptographyCenter + .DecryptAsymmetrically(x, Database.User.PrivateKey) + .DeserializeTo(Database.SerializationCenter)) .OrderByDescending(x => x.DateTime) .ToArray(); @@ -37,7 +34,7 @@ public void AddLog(string message, bool needsReview) NeedsReview = needsReview, }; - string textLog = Database.SerializationCenter.Serialize(log); + string textLog = log.SerializeWith(Database.SerializationCenter); LogList.Add(Database.CryptographyCenter.EncryptAsymmetrically(textLog, PublicKey)); _save(); diff --git a/Core/Internal/Utils/StaticMethods.cs b/Core/Internal/Utils/StaticMethods.cs index 01c9852..9a5b37e 100644 --- a/Core/Internal/Utils/StaticMethods.cs +++ b/Core/Internal/Utils/StaticMethods.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Internal.Utils +namespace Upsilon.Apps.Passkey.Core.Internal.Utils { internal static class StaticMethods { @@ -17,5 +18,11 @@ public static bool ContainsFlag(this T value, T lookingForFlag) where T : Enu int intLookingForFlag = (int)(object)lookingForFlag; return (intValue & intLookingForFlag) == intLookingForFlag; } + + public static string SerializeWith(this T obj, ISerializationCenter serializationCenter) where T : notnull + => serializationCenter.Serialize(obj); + + public static T DeserializeTo(this string serializedString, ISerializationCenter serializationCenter) where T : notnull + => serializationCenter.Deserialize(serializedString); } } diff --git a/Core/Public/Enums/AccountOption.cs b/Core/Public/Enums/AccountOption.cs index 98702bd..4f30873 100644 --- a/Core/Public/Enums/AccountOption.cs +++ b/Core/Public/Enums/AccountOption.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Enums +namespace Upsilon.Apps.Passkey.Core.Public.Enums { /// /// Represent an account option. diff --git a/Core/Public/Enums/AutoSaveMergeBehavior.cs b/Core/Public/Enums/AutoSaveMergeBehavior.cs index ee77167..9b5823e 100644 --- a/Core/Public/Enums/AutoSaveMergeBehavior.cs +++ b/Core/Public/Enums/AutoSaveMergeBehavior.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Enums +namespace Upsilon.Apps.Passkey.Core.Public.Enums { /// /// Represent the behavior of auto-save handling. @@ -6,9 +6,13 @@ public enum AutoSaveMergeBehavior { /// - /// The auto-save will be merged into the database then the auto-save file will be removed. + /// The auto-save will be merged into the database and saved then the auto-save file will be removed. /// - MergeThenRemoveAutoSaveFile, + MergeAndSaveThenRemoveAutoSaveFile, + /// + /// The auto-save will be merged into the database without saving and the auto-save file will be keeped. + /// + MergeWithoutSavingAndKeepAutoSaveFile, /// /// The auto-save will not be merged into the database but the auto-save file will be removed. /// diff --git a/Core/Public/Enums/WarningType.cs b/Core/Public/Enums/WarningType.cs index 3fe60bb..9722ee7 100644 --- a/Core/Public/Enums/WarningType.cs +++ b/Core/Public/Enums/WarningType.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Enums +namespace Upsilon.Apps.Passkey.Core.Public.Enums { /// /// Represent a type of warning. diff --git a/Core/Public/Events/AutoSaveDetectedEventArgs.cs b/Core/Public/Events/AutoSaveDetectedEventArgs.cs index c099ba3..73239d4 100644 --- a/Core/Public/Events/AutoSaveDetectedEventArgs.cs +++ b/Core/Public/Events/AutoSaveDetectedEventArgs.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.PassKey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Enums; -namespace Upsilon.Apps.PassKey.Core.Public.Events +namespace Upsilon.Apps.Passkey.Core.Public.Events { /// /// Represent the behavior of auto-save handling event argument. @@ -11,6 +11,6 @@ public class AutoSaveDetectedEventArgs : EventArgs /// The behavior selected. /// By default it will merge then remove the auto-save file. /// - public AutoSaveMergeBehavior MergeBehavior { get; set; } = AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile; + public AutoSaveMergeBehavior MergeBehavior { get; set; } = AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile; } } diff --git a/Core/Public/Events/LogoutEventArgs.cs b/Core/Public/Events/LogoutEventArgs.cs index f8e812d..6a0f879 100644 --- a/Core/Public/Events/LogoutEventArgs.cs +++ b/Core/Public/Events/LogoutEventArgs.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Events +namespace Upsilon.Apps.Passkey.Core.Public.Events { /// /// Represent a loggout event argument. diff --git a/Core/Public/Events/WarningDetectedEventArgs.cs b/Core/Public/Events/WarningDetectedEventArgs.cs index 36831b6..c9bce8f 100644 --- a/Core/Public/Events/WarningDetectedEventArgs.cs +++ b/Core/Public/Events/WarningDetectedEventArgs.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Public.Events +namespace Upsilon.Apps.Passkey.Core.Public.Events { /// /// Represent a warning detected event argument. diff --git a/Core/Public/Interfaces/IAccount.cs b/Core/Public/Interfaces/IAccount.cs index 35a2853..b602c18 100644 --- a/Core/Public/Interfaces/IAccount.cs +++ b/Core/Public/Interfaces/IAccount.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.PassKey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Enums; -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent an account. @@ -25,7 +25,7 @@ public interface IAccount : IItem /// /// The identifants. /// - string[] Identifiants { get; set; } + string[] Identifiers { get; set; } /// /// The actual password. diff --git a/Core/Public/Interfaces/ICryptographyCenter.cs b/Core/Public/Interfaces/ICryptographyCenter.cs index d28c73c..d5d8595 100644 --- a/Core/Public/Interfaces/ICryptographyCenter.cs +++ b/Core/Public/Interfaces/ICryptographyCenter.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent a cryptographic center. diff --git a/Core/Public/Interfaces/IDatabase.cs b/Core/Public/Interfaces/IDatabase.cs index 2e03f04..19d7832 100644 --- a/Core/Public/Interfaces/IDatabase.cs +++ b/Core/Public/Interfaces/IDatabase.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.PassKey.Core.Public.Events; +using Upsilon.Apps.Passkey.Core.Public.Events; -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent a database. @@ -27,6 +27,11 @@ public interface IDatabase : IDisposable /// IUser? User { get; } + /// + /// The number of seconds left before the session ended. + /// + int? SessionLeftTime { get; } + /// /// The logs. /// @@ -96,6 +101,41 @@ public interface IDatabase : IDisposable /// void Close(); + /// + /// Check if the database has changed. + /// + /// True if the database changed, False else. + bool HasChanged(); + + /// + /// Check if the given item has changed. + /// + /// The item id to check. + /// True if the item changed, False else. + bool HasChanged(string itemId); + + /// + /// Check if the field of the given item has changed. + /// + /// The item id to check. + /// The field name to check. + /// True if the field changed, False else. + bool HasChanged(string itemId, string fieldName); + + /// + /// Import services and/or accounts from a file. + /// + /// The file path. + /// True if the import succeded, False else. + bool ImportFromFile(string filePath); + + /// + /// Export services and accounts to a file. + /// + /// The file path. + /// True if the export succeded, False else. + bool ExportToFile(string filePath); + /// /// Create a new user database and returns the database. /// After creating, the User should be loaded with the Login method. diff --git a/Core/Public/Interfaces/IItem.cs b/Core/Public/Interfaces/IItem.cs index eeb48ea..920cc17 100644 --- a/Core/Public/Interfaces/IItem.cs +++ b/Core/Public/Interfaces/IItem.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent an item. @@ -9,5 +9,10 @@ public interface IItem /// The Id of the item. /// string ItemId { get; } + + /// + /// The database that contains the item. + /// + IDatabase Database { get; } } } diff --git a/Core/Public/Interfaces/ILog.cs b/Core/Public/Interfaces/ILog.cs index 232ed72..ca0f5dd 100644 --- a/Core/Public/Interfaces/ILog.cs +++ b/Core/Public/Interfaces/ILog.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent an event log. diff --git a/Core/Public/Interfaces/IPasswordFactory.cs b/Core/Public/Interfaces/IPasswordFactory.cs index 45b562f..ad8e4d9 100644 --- a/Core/Public/Interfaces/IPasswordFactory.cs +++ b/Core/Public/Interfaces/IPasswordFactory.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent a Password factory engine. diff --git a/Core/Public/Interfaces/ISerializationCenter.cs b/Core/Public/Interfaces/ISerializationCenter.cs index c39e24d..d5702ba 100644 --- a/Core/Public/Interfaces/ISerializationCenter.cs +++ b/Core/Public/Interfaces/ISerializationCenter.cs @@ -1,4 +1,6 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +using Upsilon.Apps.Passkey.Core.Internal.Utils; + +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent a serialization center. @@ -30,7 +32,7 @@ public interface ISerializationCenter /// True if the two objects are different, False else. static bool AreDifferent(ISerializationCenter serializationCenter, object object1, object object2) { - return serializationCenter.Serialize(object1) != serializationCenter.Serialize(object2); + return object1.SerializeWith(serializationCenter) != object2.SerializeWith(serializationCenter); } /// @@ -42,7 +44,7 @@ static bool AreDifferent(ISerializationCenter serializationCenter, object object /// The clone of the object. static T Clone(ISerializationCenter serializationCenter, T source) where T : notnull { - return serializationCenter.Deserialize(serializationCenter.Serialize(source)); + return source.SerializeWith(serializationCenter).DeserializeTo(serializationCenter); } } } diff --git a/Core/Public/Interfaces/IService.cs b/Core/Public/Interfaces/IService.cs index fe3437f..da2ed18 100644 --- a/Core/Public/Interfaces/IService.cs +++ b/Core/Public/Interfaces/IService.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent a service. @@ -34,10 +34,33 @@ public interface IService : IItem /// Add a new account to this service. /// /// The label of the account. - /// The identifiants. + /// The identifiers. /// The password. /// The created account. - IAccount AddAccount(string label, IEnumerable identifiants, string password); + IAccount AddAccount(string label, IEnumerable identifiers, string password); + + /// + /// Add a new account to this service. + /// + /// The label of the account. + /// The identifiers. + /// The created account. + IAccount AddAccount(string label, IEnumerable identifiers); + + /// + /// Add a new account to this service. + /// + /// The identifiers. + /// The password. + /// The created account. + IAccount AddAccount(IEnumerable identifiers, string password); + + /// + /// Add a new account to this service. + /// + /// The identifiers. + /// The created account. + IAccount AddAccount(IEnumerable identifiers); /// /// Delete the given account from this service. diff --git a/Core/Public/Interfaces/IUser.cs b/Core/Public/Interfaces/IUser.cs index caa7cd3..3674763 100644 --- a/Core/Public/Interfaces/IUser.cs +++ b/Core/Public/Interfaces/IUser.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.PassKey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Enums; -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent an user. @@ -23,14 +23,19 @@ public interface IUser : IItem int LogoutTimeout { get; set; } /// - /// The number of seconds left before the session ended. + /// The number of second to keep existing passwords in the clipboard. /// - int SessionLeftTime { get; } + int CleaningClipboardTimeout { get; set; } /// - /// The number of second to keep existing passwords in the clipboard. + /// The delay to keep password visible. /// - int CleaningClipboardTimeout { get; set; } + int ShowPasswordDelay { get; set; } + + /// + /// The number of old paswords to keep. + /// + int NumberOfOldPasswordToKeep { get; set; } /// /// The warnings types which will be notified if detected. diff --git a/Core/Public/Interfaces/IWarning.cs b/Core/Public/Interfaces/IWarning.cs index 9e9ed5a..478f829 100644 --- a/Core/Public/Interfaces/IWarning.cs +++ b/Core/Public/Interfaces/IWarning.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.PassKey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Enums; -namespace Upsilon.Apps.PassKey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Core.Public.Interfaces { /// /// Represent a warning. diff --git a/Core/Public/Interfaces/Interfaces.cd b/Core/Public/Interfaces/Interfaces.cd index 0108aab..a1d979e 100644 --- a/Core/Public/Interfaces/Interfaces.cd +++ b/Core/Public/Interfaces/Interfaces.cd @@ -1,27 +1,27 @@  - + AAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA= Public\Events\AutoSaveDetectedEventArgs.cs - + AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Public\Events\WarningDetectedEventArgs.cs - + AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Public\Events\LogoutEventArgs.cs - + AAAgAAAAEAAAAAAAAAAAAAAAAAAAQAACAAAAAABAAAA= @@ -32,16 +32,16 @@ - + AAACAAAAAAAAABAAAMAAAAAAAAAAAAASAAAAAABABAE= Public\Interfaces\ICryptographyCenter.cs - + - + @@ -49,7 +49,7 @@ - + @@ -60,7 +60,7 @@ - + @@ -86,28 +86,28 @@ - + AAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA= Public\Interfaces\IItem.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAIAIAAAAAA= Public\Interfaces\ILog.cs - + AAAAAAAAAAAAAAAAAAAAAAAAACBAAAAAAAAAAAAAAAA= Public\Interfaces\ISerializationCenter.cs - + AAAhAAAAAAAAAAAAAAAAAAAAAAAAAACACAACAQAABAA= @@ -120,9 +120,9 @@ - + - + @@ -138,9 +138,9 @@ - + - + @@ -160,21 +160,21 @@ - + AAAAAQAAAAAAAAAAAAAAAAACAAAAAAQAgABAAAAAAAA= Public\Interfaces\IPasswordFactory.cs - + AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA= Public\Enums\AccountOption.cs - + AAAAQAAAAABgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff --git a/Core/Public/Utils/ClipboardManager.cs b/Core/Public/Utils/ClipboardManager.cs index 09f9452..d3bddd8 100644 --- a/Core/Public/Utils/ClipboardManager.cs +++ b/Core/Public/Utils/ClipboardManager.cs @@ -1,6 +1,6 @@ using Windows.ApplicationModel.DataTransfer; -namespace Upsilon.Apps.PassKey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Public.Utils { public static class ClipboardManager { diff --git a/Core/Public/Utils/CryptographyCenter.cs b/Core/Public/Utils/CryptographyCenter.cs index d870141..2bf8ff7 100644 --- a/Core/Public/Utils/CryptographyCenter.cs +++ b/Core/Public/Utils/CryptographyCenter.cs @@ -1,18 +1,18 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Public.Utils { public class CryptographyCenter : ICryptographyCenter { public string GetHash(string source) { - string md5Hash = Convert.ToBase64String(MD5.HashData(Encoding.Unicode.GetBytes(source))).TrimEnd('='); - string sha1Hash = Convert.ToBase64String(SHA1.HashData(Encoding.Unicode.GetBytes(source))).TrimEnd('='); + string md5Hash = Convert.ToBase64String(MD5.HashData(Encoding.Unicode.GetBytes(source))); + string sha1Hash = Convert.ToBase64String(SHA1.HashData(Encoding.Unicode.GetBytes(source))); - return md5Hash + sha1Hash; + return (md5Hash + sha1Hash).Replace("/", "-"); } public string GetSlowHash(string source) @@ -81,38 +81,20 @@ public string DecryptSymmetrically(string source, string[] passwords) public void GenerateRandomKeys(out string publicKey, out string privateKey) { - RSACryptoServiceProvider csp = new(2048); + using RSA rsa = RSA.Create(4096); - StringWriter sw = new(); - System.Xml.Serialization.XmlSerializer xs = new(typeof(RSAParameters)); - - xs.Serialize(sw, csp.ExportParameters(includePrivateParameters: false)); - publicKey = sw.ToString(); - - sw = new System.IO.StringWriter(); - xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters)); - - xs.Serialize(sw, csp.ExportParameters(includePrivateParameters: true)); - privateKey = sw.ToString(); + privateKey = rsa.ExportRSAPrivateKeyPem(); + publicKey = rsa.ExportRSAPublicKeyPem(); } public string EncryptAsymmetrically(string source, string key) { - RSACryptoServiceProvider csp = new(); - - StringReader sr = new(key); - System.Xml.Serialization.XmlSerializer xs = new(typeof(RSAParameters)); - - RSAParameters pubKey = (RSAParameters?)xs.Deserialize(sr) ?? throw new WrongPasswordException(0); - - csp.ImportParameters(pubKey); - Random random = new((int)DateTime.Now.Ticks); byte[] randomBytes = new byte[100]; random.NextBytes(randomBytes); string aesKey = Encoding.UTF8.GetString(randomBytes); source = EncryptSymmetrically(source, [aesKey]); - aesKey = _encryptRsa(aesKey, csp); + aesKey = _encryptRsa(aesKey, key); KeyValuePair s = new(aesKey, source); source = JsonSerializer.Serialize(s); @@ -128,23 +110,14 @@ public string DecryptAsymmetrically(string source, string key) throw new CheckSignFailedException(); } - RSACryptoServiceProvider csp = new(); - - StringReader sr = new(key); - System.Xml.Serialization.XmlSerializer xs = new(typeof(RSAParameters)); - - RSAParameters privKey = (RSAParameters?)xs.Deserialize(sr) ?? throw new Exception(); - - csp.ImportParameters(privKey); - KeyValuePair s = JsonSerializer.Deserialize>(source); - string aesKey = _decryptRsa(s.Key, 0, csp); + string aesKey = _decryptRsa(s.Key, 0, key); source = DecryptSymmetrically(s.Value, [aesKey]); return source; } - private string _cipherAes(string plainText, string key) + private static string _cipherAes(string plainText, string key) { if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(plainText)) { @@ -160,10 +133,10 @@ private string _cipherAes(string plainText, string key) byte[] bytes = _cipherAes(plainText, _key, IV); - return new string(bytes.Select(x => (char)x).ToArray()); + return new string([.. bytes.Select(x => (char)x)]); } - private byte[] _cipherAes(string plainText, byte[] key, byte[] IV) + private static byte[] _cipherAes(string plainText, byte[] key, byte[] IV) { using Aes aesAlg = Aes.Create(); aesAlg.Key = key; @@ -181,27 +154,25 @@ private byte[] _cipherAes(string plainText, byte[] key, byte[] IV) return msEncrypt.ToArray(); } - private string _uncipherAes(string cipherText, string key) + private static string _uncipherAes(string cipherText, string key) { if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(cipherText)) { return cipherText; } - - MD5 mD5 = MD5.Create(); - key = Encoding.ASCII.GetString(mD5.ComputeHash(Encoding.ASCII.GetBytes(key))); - key += Encoding.ASCII.GetString(mD5.ComputeHash(Encoding.ASCII.GetBytes(key))); - key += Encoding.ASCII.GetString(mD5.ComputeHash(Encoding.ASCII.GetBytes(key))); + key = Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + key += Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); + key += Encoding.ASCII.GetString(MD5.HashData(Encoding.ASCII.GetBytes(key))); byte[] _key = Encoding.ASCII.GetBytes(key[..32]); byte[] IV = Encoding.ASCII.GetBytes(key.Substring(32, 16)); - byte[] bytes = cipherText.Select(x => (byte)x).ToArray(); + byte[] bytes = [.. cipherText.Select(x => (byte)x)]; return _uncitherAes(bytes, _key, IV); } - private string _uncitherAes(byte[] cipherText, byte[] key, byte[] IV) + private static string _uncitherAes(byte[] cipherText, byte[] key, byte[] IV) { using Aes aesAlg = Aes.Create(); aesAlg.Key = key; @@ -218,7 +189,7 @@ private string _uncitherAes(byte[] cipherText, byte[] key, byte[] IV) private string _encryptAes(string source, string[] passwords) { - passwords = passwords.Select(GetHash).ToArray(); + passwords = [.. passwords.Select(GetHash)]; for (int i = passwords.Length - 1; i >= 0; i--) { @@ -234,7 +205,7 @@ private string _encryptAes(string source, string[] passwords) private string _decryptAes(string source, string[] passwords) { - passwords = passwords.Select(GetHash).ToArray(); + passwords = [.. passwords.Select(GetHash)]; try { @@ -270,23 +241,28 @@ private string _decryptAes(string source, string[] passwords) return source; } - private string _encryptRsa(string source, RSACryptoServiceProvider csp) + private static string _encryptRsa(string source, string publicKeyPem) { + using RSA rsa = RSA.Create(); + rsa.ImportFromPem(publicKeyPem); + byte[] bytesPlainTextData = Encoding.Unicode.GetBytes(source); - byte[] bytesCypherText = csp.Encrypt(bytesPlainTextData, true); + byte[] bytesCypherText = rsa.Encrypt(bytesPlainTextData, RSAEncryptionPadding.OaepSHA256); source = Convert.ToBase64String(bytesCypherText); return source; } - private string _decryptRsa(string source, int level, RSACryptoServiceProvider csp) + private static string _decryptRsa(string source, int level, string privateKeyPem) { try { - byte[] bytesCypherText = Convert.FromBase64String(source); - byte[] bytesPlainTextData = csp.Decrypt(bytesCypherText, true); + using RSA rsa = RSA.Create(); + rsa.ImportFromPem(privateKeyPem); + byte[] bytesCypherText = Convert.FromBase64String(source); + byte[] bytesPlainTextData = rsa.Decrypt(bytesCypherText, RSAEncryptionPadding.OaepSHA256); return Encoding.Unicode.GetString(bytesPlainTextData); } catch diff --git a/Core/Public/Utils/Exceptions.cs b/Core/Public/Utils/Exceptions.cs index 2239cda..9086f78 100644 --- a/Core/Public/Utils/Exceptions.cs +++ b/Core/Public/Utils/Exceptions.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.PassKey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Public.Utils { public sealed class CheckSignFailedException : Exception { diff --git a/Core/Public/Utils/JsonSerializationCenter.cs b/Core/Public/Utils/JsonSerializationCenter.cs index f00e3bc..73db29f 100644 --- a/Core/Public/Utils/JsonSerializationCenter.cs +++ b/Core/Public/Utils/JsonSerializationCenter.cs @@ -1,20 +1,17 @@ using System.Text.Json; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using System.Text.Json.Serialization; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Public.Utils { public class JsonSerializationCenter : ISerializationCenter { + private static readonly JsonSerializerOptions _options = new() { Converters = { new JsonStringEnumConverter() }, }; + public string Serialize(T toSerialize) where T : notnull - { - return JsonSerializer.Serialize(toSerialize); - } + => JsonSerializer.Serialize(toSerialize, _options); public T Deserialize(string toDeserialize) where T : notnull - { - T? obj = JsonSerializer.Deserialize(toDeserialize); - - return obj ?? throw new NullReferenceException(nameof(obj)); - } + => JsonSerializer.Deserialize(toDeserialize, _options) ?? throw new NullReferenceException(nameof(toDeserialize)); } } diff --git a/Core/Public/Utils/PasswordFactory.cs b/Core/Public/Utils/PasswordFactory.cs index 691eed7..eba02d5 100644 --- a/Core/Public/Utils/PasswordFactory.cs +++ b/Core/Public/Utils/PasswordFactory.cs @@ -1,9 +1,8 @@ -using System.Net; -using System.Security.Cryptography; +using System.Security.Cryptography; using System.Text; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Public.Utils { public class PasswordFactory : IPasswordFactory { @@ -75,10 +74,6 @@ public bool PasswordLeaked(string password) { string hash = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(password))); - ServicePointManager.Expect100Continue = true; - ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; - ServicePointManager.DefaultConnectionLimit = 9999; - using HttpClient httpClient = new(); HttpRequestMessage request = new(HttpMethod.Get, $"https://api.pwnedpasswords.com/range/{hash[..5]}"); HttpResponseMessage response = httpClient.Send(request); diff --git a/Core/Public/Utils/StaticMethods.cs b/Core/Public/Utils/StaticMethods.cs new file mode 100644 index 0000000..37a7a93 --- /dev/null +++ b/Core/Public/Utils/StaticMethods.cs @@ -0,0 +1,17 @@ +using Upsilon.Apps.Passkey.Core.Public.Interfaces; + +namespace Upsilon.Apps.Passkey.Core.Public.Utils +{ + public static class StaticMethods + { + public static bool HasChanged(this IItem item) + { + return item.Database.HasChanged(item.ItemId); + } + + public static bool HasChanged(this IItem item, string fieldName) + { + return item.Database.HasChanged(item.ItemId, fieldName); + } + } +} diff --git a/Core/Upsilon.Apps.Passkey.Core.csproj b/Core/Upsilon.Apps.Passkey.Core.csproj index ca9a385..af86099 100644 --- a/Core/Upsilon.Apps.Passkey.Core.csproj +++ b/Core/Upsilon.Apps.Passkey.Core.csproj @@ -1,7 +1,7 @@  - net8.0-windows10.0.18362.0 + net10.0-windows10.0.18362.0 enable enable $(AssemblyName) diff --git a/UnitTests/Models/AccountUnitTests.cs b/UnitTests/Models/AccountUnitTests.cs index c30cefa..56d42a8 100644 --- a/UnitTests/Models/AccountUnitTests.cs +++ b/UnitTests/Models/AccountUnitTests.cs @@ -1,8 +1,9 @@ using FluentAssertions; -using Upsilon.Apps.PassKey.Core.Public.Enums; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Utils; -namespace Upsilon.Apps.PassKey.UnitTests.Models +namespace Upsilon.Apps.Passkey.UnitTests.Models { [TestClass] public sealed class AccountUnitTests @@ -23,8 +24,8 @@ public void Case01_AddAccountUpdateSaved() IService service = databaseCreated.User.AddService("Service_" + UnitTestsHelper.GetUsername()); string oldAccountLabel = "Account_" + UnitTestsHelper.GetUsername(); string newAccountLabel = "new_" + oldAccountLabel; - string[] oldIdentifiants = UnitTestsHelper.GetRandomStringArray(); - string[] newIdentifiants = UnitTestsHelper.GetRandomStringArray(); + string[] oldIdentifiers = UnitTestsHelper.GetRandomStringArray(); + string[] newIdentifiers = UnitTestsHelper.GetRandomStringArray(); string oldPassword = UnitTestsHelper.GetRandomString(); string newPassword = UnitTestsHelper.GetRandomString(); string notes = UnitTestsHelper.GetRandomString(); @@ -34,37 +35,55 @@ public void Case01_AddAccountUpdateSaved() Stack expectedLogWarnings = new(); // When - IAccount account = service.AddAccount(oldAccountLabel, oldIdentifiants, oldPassword); - expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiants)}) has been added to Service {service.ServiceName}"); + IAccount account = service.AddAccount(oldAccountLabel, oldIdentifiers, oldPassword); + expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiers)}) has been added to Service {service.ServiceName}"); + expectedLogs.Push($"Warning : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiers)})'s password has been updated"); + expectedLogWarnings.Push($"Warning : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiers)})'s password has been updated"); // Then + databaseCreated.User.HasChanged().Should().BeTrue(); + service.HasChanged().Should().BeTrue(); + account.HasChanged().Should().BeTrue(); _ = service.Accounts.Length.Should().Be(1); _ = account.Label.Should().Be(oldAccountLabel); - _ = account.Identifiants.Should().BeEquivalentTo(oldIdentifiants); + _ = account.Identifiers.Should().BeEquivalentTo(oldIdentifiers); _ = account.Password.Should().Be(oldPassword); _ = account.Passwords.Values.Should().BeEquivalentTo([oldPassword]); // When account.Label = newAccountLabel; account.Label = newAccountLabel; - expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiants)})'s label has been set to {newAccountLabel}"); - account.Identifiants = newIdentifiants; - account.Identifiants = newIdentifiants; - expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiants)})'s identifiants has been set to ({string.Join(", ", newIdentifiants)})"); - expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiants)})'s identifiants has been set to ({string.Join(", ", newIdentifiants)})"); + expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiers)})'s label has been set to {newAccountLabel}"); + account.Identifiers = newIdentifiers; + account.Identifiers = newIdentifiers; + expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiers)})'s identifiers has been set to ({string.Join(", ", newIdentifiers)})"); + expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiers)})'s identifiers has been set to ({string.Join(", ", newIdentifiers)})"); account.Password = newPassword; account.Password = newPassword; - expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password has been updated"); - expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password has been updated"); + expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s password has been updated"); + expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s password has been updated"); account.Notes = notes; account.Notes = notes; - expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s notes has been set to {notes}"); + expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s notes has been set to {notes}"); account.PasswordUpdateReminderDelay = passwordUpdateReminderDelay; - expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password update reminder delay has been set to {passwordUpdateReminderDelay}"); + expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s password update reminder delay has been set to {passwordUpdateReminderDelay}"); account.Options = options; account.Options = options; - expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s options has been set to {options}"); + expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s options has been set to {options}"); + // Then + databaseCreated.User.HasChanged().Should().BeTrue(); + service.HasChanged().Should().BeTrue(); + service.HasChanged().Should().BeTrue(); + account.HasChanged().Should().BeTrue(); + account.HasChanged(nameof(account.Label)).Should().BeTrue(); + account.HasChanged(nameof(account.Identifiers)).Should().BeTrue(); + account.HasChanged(nameof(account.Password)).Should().BeTrue(); + account.HasChanged(nameof(account.Notes)).Should().BeTrue(); + account.HasChanged(nameof(account.PasswordUpdateReminderDelay)).Should().BeTrue(); + account.HasChanged(nameof(account.Options)).Should().BeTrue(); + + // When databaseCreated.Save(); expectedLogs.Push($"Information : User {username}'s database saved"); databaseCreated.Close(); @@ -84,7 +103,7 @@ public void Case01_AddAccountUpdateSaved() // Then _ = account.Label.Should().Be(newAccountLabel); - _ = account.Identifiants.Should().BeEquivalentTo(newIdentifiants); + _ = account.Identifiers.Should().BeEquivalentTo(newIdentifiers); _ = account.Password.Should().Be(newPassword); _ = account.Passwords.OrderByDescending(x => x.Key).Select(x => x.Value).Should().BeEquivalentTo([newPassword, oldPassword]); _ = account.Notes.Should().Be(notes); @@ -103,7 +122,7 @@ public void Case01_AddAccountUpdateSaved() /* * Service.AddAccount adds the new account, * Then updating the account without saving will create the autosave file, - * Then Database.Open with AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile loads correctly the updated database file with the updated account. + * Then Database.Open with AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile loads correctly the updated database file with the updated account. */ public void Case02_AddAccountUpdateAutoSave() { @@ -115,8 +134,8 @@ public void Case02_AddAccountUpdateAutoSave() IService service = databaseCreated.User.AddService("Service_" + UnitTestsHelper.GetUsername()); string oldAccountLabel = "Account_" + UnitTestsHelper.GetUsername(); string newAccountLabel = "new_" + oldAccountLabel; - string[] oldIdentifiants = UnitTestsHelper.GetRandomStringArray(); - string[] newIdentifiants = UnitTestsHelper.GetRandomStringArray(); + string[] oldIdentifiers = UnitTestsHelper.GetRandomStringArray(); + string[] newIdentifiers = UnitTestsHelper.GetRandomStringArray(); string oldPassword = UnitTestsHelper.GetRandomString(); string newPassword = UnitTestsHelper.GetRandomString(); string notes = UnitTestsHelper.GetRandomString(); @@ -126,47 +145,49 @@ public void Case02_AddAccountUpdateAutoSave() Stack expectedLogWarnings = new(); // When - IAccount account = service.AddAccount(oldAccountLabel, oldIdentifiants, oldPassword); - expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiants)}) has been added to Service {service.ServiceName}"); + IAccount account = service.AddAccount(oldAccountLabel, oldIdentifiers, oldPassword); + expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiers)}) has been added to Service {service.ServiceName}"); + expectedLogs.Push($"Warning : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiers)})'s password has been updated"); + expectedLogWarnings.Push($"Warning : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiers)})'s password has been updated"); // Then _ = service.Accounts.Length.Should().Be(1); _ = account.Label.Should().Be(oldAccountLabel); - _ = account.Identifiants.Should().BeEquivalentTo(oldIdentifiants); + _ = account.Identifiers.Should().BeEquivalentTo(oldIdentifiers); _ = account.Password.Should().Be(oldPassword); _ = account.Passwords.Values.Should().BeEquivalentTo([oldPassword]); // When account.Label = newAccountLabel; account.Label = newAccountLabel; - expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiants)})'s label has been set to {newAccountLabel}"); - account.Identifiants = newIdentifiants; - account.Identifiants = newIdentifiants; - expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiants)})'s identifiants has been set to ({string.Join(", ", newIdentifiants)})"); - expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiants)})'s identifiants has been set to ({string.Join(", ", newIdentifiants)})"); + expectedLogs.Push($"Information : Account {oldAccountLabel} ({string.Join(", ", oldIdentifiers)})'s label has been set to {newAccountLabel}"); + account.Identifiers = newIdentifiers; + account.Identifiers = newIdentifiers; + expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiers)})'s identifiers has been set to ({string.Join(", ", newIdentifiers)})"); + expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", oldIdentifiers)})'s identifiers has been set to ({string.Join(", ", newIdentifiers)})"); account.Password = newPassword; account.Password = newPassword; - expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password has been updated"); - expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password has been updated"); + expectedLogs.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s password has been updated"); + expectedLogWarnings.Push($"Warning : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s password has been updated"); account.Notes = notes; account.Notes = notes; - expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s notes has been set to {notes}"); + expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s notes has been set to {notes}"); account.PasswordUpdateReminderDelay = passwordUpdateReminderDelay; - expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s password update reminder delay has been set to {passwordUpdateReminderDelay}"); + expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s password update reminder delay has been set to {passwordUpdateReminderDelay}"); account.Options = options; account.Options = options; - expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiants)})'s options has been set to {options}"); + expectedLogs.Push($"Information : Account {newAccountLabel} ({string.Join(", ", newIdentifiers)})'s options has been set to {options}"); databaseCreated.Close(); expectedLogs.Push($"Warning : User {username} logged out without saving"); expectedLogWarnings.Push($"Warning : User {username} logged out without saving"); expectedLogs.Push($"Information : User {username}'s database closed"); - IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile); + IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile); expectedLogs.Push($"Information : User {username}'s database opened"); expectedLogs.Push($"Information : User {username} logged in"); - expectedLogs.Push($"Warning : User {username}'s autosave merged"); - expectedLogWarnings.Push($"Warning : User {username}'s autosave merged"); + expectedLogs.Push($"Warning : User {username}'s autosave merged and saved"); + expectedLogWarnings.Push($"Warning : User {username}'s autosave merged and saved"); IService serviceLoaded = databaseLoaded.User.Services.First(); // Then @@ -177,7 +198,7 @@ public void Case02_AddAccountUpdateAutoSave() // Then _ = account.Label.Should().Be(newAccountLabel); - _ = account.Identifiants.Should().BeEquivalentTo(newIdentifiants); + _ = account.Identifiers.Should().BeEquivalentTo(newIdentifiers); _ = account.Password.Should().Be(newPassword); _ = account.Passwords.OrderByDescending(x => x.Key).Select(x => x.Value).Should().BeEquivalentTo([newPassword, oldPassword]); _ = account.Notes.Should().Be(notes); @@ -206,9 +227,9 @@ public void Case03_DeleteAccountUpdateSaved() IDatabase databaseCreated = UnitTestsHelper.CreateTestDatabase(passkeys); IService service = databaseCreated.User.AddService("Service_" + UnitTestsHelper.GetUsername()); string accountLabel = "Account_" + UnitTestsHelper.GetUsername(); - string[] identifiants = UnitTestsHelper.GetRandomStringArray(); + string[] identifiers = UnitTestsHelper.GetRandomStringArray(); string password = UnitTestsHelper.GetRandomString(); - _ = service.AddAccount(accountLabel, identifiants, password); + _ = service.AddAccount(accountLabel, identifiers, password); databaseCreated.Save(); databaseCreated.Close(); Stack expectedLogs = new(); @@ -220,8 +241,8 @@ public void Case03_DeleteAccountUpdateSaved() // When serviceLoaded.DeleteAccount(accountLoaded); - expectedLogs.Push($"Warning : Account {accountLabel} ({string.Join(", ", identifiants)}) has been removed from Service {service.ServiceName}"); - expectedLogWarnings.Push($"Warning : Account {accountLabel} ({string.Join(", ", identifiants)}) has been removed from Service {service.ServiceName}"); + expectedLogs.Push($"Warning : Account {accountLabel} ({string.Join(", ", identifiers)}) has been removed from Service {service.ServiceName}"); + expectedLogWarnings.Push($"Warning : Account {accountLabel} ({string.Join(", ", identifiers)}) has been removed from Service {service.ServiceName}"); // Then _ = serviceLoaded.Accounts.Length.Should().Be(0); @@ -253,7 +274,7 @@ public void Case03_DeleteAccountUpdateSaved() [TestMethod] /* * Service.DeleteAccount adeletes the account, - * Then Database.Open with AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile loads correctly the updated database file with the updated account. + * Then Database.Open with AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile loads correctly the updated database file with the updated account. */ public void Case04_DeleteAccountUpdateAutoSave() { @@ -264,9 +285,9 @@ public void Case04_DeleteAccountUpdateAutoSave() IDatabase databaseCreated = UnitTestsHelper.CreateTestDatabase(passkeys); IService service = databaseCreated.User.AddService("Service_" + UnitTestsHelper.GetUsername()); string accountLabel = "Account_" + UnitTestsHelper.GetUsername(); - string[] identifiants = UnitTestsHelper.GetRandomStringArray(); + string[] identifiers = UnitTestsHelper.GetRandomStringArray(); string password = UnitTestsHelper.GetRandomString(); - _ = service.AddAccount(accountLabel, identifiants, password); + _ = service.AddAccount(accountLabel, identifiers, password); databaseCreated.Save(); databaseCreated.Close(); Stack expectedLogs = new(); @@ -278,8 +299,8 @@ public void Case04_DeleteAccountUpdateAutoSave() // When serviceLoaded.DeleteAccount(accountLoaded); - expectedLogs.Push($"Warning : Account {accountLabel} ({string.Join(", ", identifiants)}) has been removed from Service {service.ServiceName}"); - expectedLogWarnings.Push($"Warning : Account {accountLabel} ({string.Join(", ", identifiants)}) has been removed from Service {service.ServiceName}"); + expectedLogs.Push($"Warning : Account {accountLabel} ({string.Join(", ", identifiers)}) has been removed from Service {service.ServiceName}"); + expectedLogWarnings.Push($"Warning : Account {accountLabel} ({string.Join(", ", identifiers)}) has been removed from Service {service.ServiceName}"); // Then _ = serviceLoaded.Accounts.Length.Should().Be(0); @@ -290,11 +311,11 @@ public void Case04_DeleteAccountUpdateAutoSave() expectedLogWarnings.Push($"Warning : User {username} logged out without saving"); expectedLogs.Push($"Information : User {username}'s database closed"); - databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile); + databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile); expectedLogs.Push($"Information : User {username}'s database opened"); expectedLogs.Push($"Information : User {username} logged in"); - expectedLogs.Push($"Warning : User {username}'s autosave merged"); - expectedLogWarnings.Push($"Warning : User {username}'s autosave merged"); + expectedLogs.Push($"Warning : User {username}'s autosave merged and saved"); + expectedLogWarnings.Push($"Warning : User {username}'s autosave merged and saved"); serviceLoaded = databaseLoaded.User.Services.First(); diff --git a/UnitTests/Models/DatabaseUnitTests.cs b/UnitTests/Models/DatabaseUnitTests.cs index 8e7f359..2994843 100644 --- a/UnitTests/Models/DatabaseUnitTests.cs +++ b/UnitTests/Models/DatabaseUnitTests.cs @@ -1,11 +1,65 @@ using FluentAssertions; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; -namespace Upsilon.Apps.PassKey.UnitTests.Models +namespace Upsilon.Apps.Passkey.UnitTests.Models { [TestClass] public sealed class DatabaseUnitTests { + [TestMethod, Ignore] + public void Case00_GenerateNewDatabase() + { + IDatabase database = UnitTestsHelper.CreateTestDatabase(["a", "b"], "_"); + IUser user = database.User; + user.LogoutTimeout = 10; + user.CleaningClipboardTimeout = 15; + user.WarningsToNotify = (Core.Public.Enums.WarningType)0; + + for (int i = 0; i < 50; i++) + { + IService service = user.AddService($"Service{i} ({UnitTestsHelper.GetRandomString(min: 10, max: 15)})"); + service.Url = $"www.service{i}.xyz"; + int random = UnitTestsHelper.GetRandomInt(100) % 10; + service.Notes = random == 0 ? $"Service{i} notes : \n{UnitTestsHelper.GetRandomString(min: 10, max: 150)}" : ""; + + int accountNumber = UnitTestsHelper.GetRandomInt(min: 1, max: 5); + + for (int j = 0; j < accountNumber; j++) + { + random = UnitTestsHelper.GetRandomInt(10) + 1; + + IAccount account; + switch (random % 4) + { + case 1: + account = service.AddAccount(label: $"Account{j}", + identifiers: UnitTestsHelper.GetRandomStringArray(random / 2).Select(x => x + "@test.te")); + break; + case 2: + account = service.AddAccount(identifiers: UnitTestsHelper.GetRandomStringArray(random / 2).Select(x => x + "@test.te"), + password: UnitTestsHelper.GetRandomString(min: 20, max: 25)); + break; + case 3: + account = service.AddAccount(identifiers: UnitTestsHelper.GetRandomStringArray(random / 2).Select(x => x + "@test.te")); + break; + default: + account = service.AddAccount(label: $"Account{j}", + identifiers: UnitTestsHelper.GetRandomStringArray(random / 2).Select(x => x + "@test.te"), + password: UnitTestsHelper.GetRandomString(min: 20, max: 25)); + break; + } + + random = UnitTestsHelper.GetRandomInt(100); + account.Notes = random % 10 == 0 ? $"Service{i}'s Account{j} notes : \n{UnitTestsHelper.GetRandomString(min: 10, max: 150)}" : ""; + account.PasswordUpdateReminderDelay = random < 10 ? random : 0; + account.Options = random % 2 == 0 ? Core.Public.Enums.AccountOption.WarnIfPasswordLeaked : Core.Public.Enums.AccountOption.None; + } + } + + database.Save(); + database.Close(); + } + [TestMethod] /* * Database.Create creates an empty database file, @@ -28,8 +82,6 @@ public void Case01_DatabaseCreationOpenDelete() // When IDatabase databaseCreated = UnitTestsHelper.CreateTestDatabase(passkeys); expectedLogs.Push($"Information : User {username}'s database created"); - expectedLogs.Push($"Information : User {username}'s database opened"); - expectedLogs.Push($"Information : User {username} logged in"); // Then _ = databaseCreated.DatabaseFile.Should().Be(databaseFile); @@ -271,11 +323,6 @@ public void Case05_DatabaseAutoLogout() database.DatabaseClosed += (s, e) => { closedDueToTimeout = e.LoginTimeoutReached; }; - foreach (string passkey in passkeys) - { - _ = database.Login(passkey); - } - database.User.LogoutTimeout = 1; database.Save(); DateTime start = DateTime.Now; @@ -286,8 +333,6 @@ public void Case05_DatabaseAutoLogout() Thread.Sleep(500); } - int elapsedTime = (int)(DateTime.Now - start).TotalSeconds; - // Then _ = closedDueToTimeout.Should().BeTrue(); @@ -295,7 +340,6 @@ public void Case05_DatabaseAutoLogout() database = UnitTestsHelper.OpenTestDatabase(passkeys, out _); // Then - _ = elapsedTime.Should().BeLessThanOrEqualTo(database.User.LogoutTimeout * 60); _ = database.Logs.FirstOrDefault(x => x.Message == $"User {username}'s login session timeout reached" && x.NeedsReview).Should().NotBeNull(); // Finaly diff --git a/UnitTests/Models/ImportExportUnitTests.cs b/UnitTests/Models/ImportExportUnitTests.cs new file mode 100644 index 0000000..466cbe4 --- /dev/null +++ b/UnitTests/Models/ImportExportUnitTests.cs @@ -0,0 +1,598 @@ +using ABI.System; +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Text; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.UnitTests; +using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext; + +namespace Upsilon.Apps.Passkey.UnitTests.Models +{ + [TestClass] + public class ImportExportUnitTests + { + [TestMethod] + public void Case01_Import_MissingFile() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath("missing_import.csv"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because import file is not accessible"); + + // Then + database.User.Services.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case02_Import_WrongExtention() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"{username}/import.txt", createIfNotExists: true); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because '.txt' extention type is not handled"); + + // Then + database.User.Services.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case03_Import_NoData() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import_noData.csv"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because there is no data to import"); + + // Then + database.User.Services.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case04_Import_ServiceAlreadyExists() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import.csv"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + database.User.AddService("Service1"); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because service 'Service1' already exists"); + + // Then + database.User.Services.Length.Should().Be(1); + database.User.Services[0].Url.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case05_ImportBlanckService() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import_blanckService.csv"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because service name or account identifier cannot be blank"); + + // Then + database.User.Services.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case06_ImportBlanckIdentifier() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import_blanckIdentifier.json"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because service name or account identifier cannot be blank"); + + // Then + database.User.Services.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case07_ImportCSV_OK() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath("import.csv"); + string exportFile = UnitTestsHelper.GetTestFilePath($"{username}/export.csv"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + + expectedLogs.Push($"Information : Service Service0 has been added to User {username}"); + expectedLogs.Push($"Information : Service Service0's url has been set to www.service0.xyz"); + expectedLogs.Push($"Information : Service Service0's notes has been set to Service0's notes"); + + expectedLogs.Push($"Information : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz) has been added to Service Service0"); + expectedLogs.Push($"Warning : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz)'s password has been updated"); + expectedLogs.Push($"Information : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz)'s notes has been set to Service0's Account0's notes"); + expectedLogs.Push($"Information : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz)'s options has been set to None"); + expectedLogs.Push($"Information : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz)'s password update reminder delay has been set to 3"); + + expectedLogs.Push($"Information : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz) has been added to Service Service0"); + expectedLogs.Push($"Warning : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz)'s password has been updated"); + expectedLogs.Push($"Information : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz)'s notes has been set to Service0's Account1's notes"); + expectedLogs.Push($"Information : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz)'s options has been set to None"); + expectedLogs.Push($"Information : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz)'s password update reminder delay has been set to 3"); + + expectedLogs.Push($"Information : Service Service1 has been added to User {username}"); + expectedLogs.Push($"Information : Service Service1's url has been set to www.service1.xyz"); + expectedLogs.Push($"Information : Service Service1's notes has been set to Service1's notes"); + + expectedLogs.Push($"Information : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz) has been added to Service Service1"); + expectedLogs.Push($"Warning : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz)'s password has been updated"); + expectedLogs.Push($"Information : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz)'s notes has been set to Service1's Account0's notes"); + expectedLogs.Push($"Information : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz)'s options has been set to None"); + expectedLogs.Push($"Information : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz)'s password update reminder delay has been set to 3"); + + expectedLogs.Push($"Information : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz) has been added to Service Service1"); + expectedLogs.Push($"Warning : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz)'s password has been updated"); + expectedLogs.Push($"Information : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz)'s notes has been set to Service1's Account1's notes"); + expectedLogs.Push($"Information : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz)'s options has been set to None"); + expectedLogs.Push($"Information : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz)'s password update reminder delay has been set to 3"); + + expectedLogs.Push($"Warning : Import completed successfully"); + expectedLogs.Push($"Information : User {username}'s database saved"); + + // Then + database.User.Services.Length.Should().Be(2); + + database.User.Services[0].ServiceName.Should().Be("Service0"); + database.User.Services[0].Url.Should().Be("www.service0.xyz"); + database.User.Services[0].Notes.Should().Be("Service0's notes"); + + database.User.Services[0].Accounts.Length.Should().Be(2); + + database.User.Services[0].Accounts[0].Label.Should().Be("Account0"); + database.User.Services[0].Accounts[0].Identifiers.Should().BeEquivalentTo(new[] { "account0@service0.xyz", "account0_backup@service0.xyz" }); + database.User.Services[0].Accounts[0].Password.Should().Be("0000"); + database.User.Services[0].Accounts[0].Notes.Should().Be("Service0's Account0's notes"); + database.User.Services[0].Accounts[0].Options.Should().Be(AccountOption.None); + database.User.Services[0].Accounts[0].PasswordUpdateReminderDelay.Should().Be(3); + + database.User.Services[0].Accounts[1].Label.Should().Be("Account1"); + database.User.Services[0].Accounts[1].Identifiers.Should().BeEquivalentTo(new[] { "account1@service0.xyz", "account1_backup@service0.xyz" }); + database.User.Services[0].Accounts[1].Password.Should().Be("1111"); + database.User.Services[0].Accounts[1].Notes.Should().Be("Service0's Account1's notes"); + database.User.Services[0].Accounts[1].Options.Should().Be(AccountOption.None); + database.User.Services[0].Accounts[1].PasswordUpdateReminderDelay.Should().Be(3); + + database.User.Services[1].ServiceName.Should().Be("Service1"); + database.User.Services[1].Url.Should().Be("www.service1.xyz"); + database.User.Services[1].Notes.Should().Be("Service1's notes"); + + database.User.Services[1].Accounts.Length.Should().Be(2); + + database.User.Services[1].Accounts[0].Label.Should().Be("Account0"); + database.User.Services[1].Accounts[0].Identifiers.Should().BeEquivalentTo(new[] { "account0@service1.xyz", "account0_backup@service1.xyz" }); + database.User.Services[1].Accounts[0].Password.Should().Be("AAAA"); + database.User.Services[1].Accounts[0].Notes.Should().Be("Service1's Account0's notes"); + database.User.Services[1].Accounts[0].Options.Should().Be(AccountOption.None); + database.User.Services[1].Accounts[0].PasswordUpdateReminderDelay.Should().Be(3); + + database.User.Services[1].Accounts[1].Label.Should().Be("Account1"); + database.User.Services[1].Accounts[1].Identifiers.Should().BeEquivalentTo(new[] { "account1@service1.xyz", "account1_backup@service1.xyz" }); + database.User.Services[1].Accounts[1].Password.Should().Be("BBBB"); + database.User.Services[1].Accounts[1].Notes.Should().Be("Service1's Account1's notes"); + database.User.Services[1].Accounts[1].Options.Should().Be(AccountOption.None); + database.User.Services[1].Accounts[1].PasswordUpdateReminderDelay.Should().Be(3); + + // When + database.ExportToFile(exportFile); + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Exporting data to file : '{exportFile}'"); + expectedLogs.Push($"Warning : Export completed successfully"); + + // Then + File.ReadAllText(importFile).Should().Be(File.ReadAllText(exportFile)); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case08_ImportCSV_MissingHeader() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import_MissingHearder.csv"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because the CSV headers should be : 'ServiceName', 'ServiceUrl', 'ServiceNotes', 'AccountLabel', 'Identifiers', 'Password', 'AccountNotes', 'AccountOptions', 'PasswordUpdateReminderDelay'"); + + // Then + database.User.Services.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case09_ImportCSV_MissingCollumn() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import_MissingCollumn.csv"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because the CSV data format is incorrect"); + + // Then + database.User.Services.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case10_ImportJson_OK() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath("import.json"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + + expectedLogs.Push($"Information : Service Service0 has been added to User {username}"); + expectedLogs.Push($"Information : Service Service0's url has been set to www.service0.xyz"); + expectedLogs.Push($"Information : Service Service0's notes has been set to Service0's notes"); + + expectedLogs.Push($"Information : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz) has been added to Service Service0"); + expectedLogs.Push($"Warning : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz)'s password has been updated"); + expectedLogs.Push($"Information : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz)'s notes has been set to Service0's Account0's notes"); + expectedLogs.Push($"Information : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz)'s options has been set to None"); + expectedLogs.Push($"Information : Account Account0 (account0@service0.xyz, account0_backup@service0.xyz)'s password update reminder delay has been set to 3"); + + expectedLogs.Push($"Information : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz) has been added to Service Service0"); + expectedLogs.Push($"Warning : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz)'s password has been updated"); + expectedLogs.Push($"Information : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz)'s notes has been set to Service0's Account1's notes"); + expectedLogs.Push($"Information : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz)'s options has been set to None"); + expectedLogs.Push($"Information : Account Account1 (account1@service0.xyz, account1_backup@service0.xyz)'s password update reminder delay has been set to 3"); + + expectedLogs.Push($"Information : Service Service1 has been added to User {username}"); + expectedLogs.Push($"Information : Service Service1's url has been set to www.service1.xyz"); + expectedLogs.Push($"Information : Service Service1's notes has been set to Service1's notes"); + + expectedLogs.Push($"Information : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz) has been added to Service Service1"); + expectedLogs.Push($"Warning : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz)'s password has been updated"); + expectedLogs.Push($"Information : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz)'s notes has been set to Service1's Account0's notes"); + expectedLogs.Push($"Information : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz)'s options has been set to None"); + expectedLogs.Push($"Information : Account Account0 (account0@service1.xyz, account0_backup@service1.xyz)'s password update reminder delay has been set to 3"); + + expectedLogs.Push($"Information : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz) has been added to Service Service1"); + expectedLogs.Push($"Warning : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz)'s password has been updated"); + expectedLogs.Push($"Information : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz)'s notes has been set to Service1's Account1's notes"); + expectedLogs.Push($"Information : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz)'s options has been set to None"); + expectedLogs.Push($"Information : Account Account1 (account1@service1.xyz, account1_backup@service1.xyz)'s password update reminder delay has been set to 3"); + + expectedLogs.Push($"Warning : Import completed successfully"); + expectedLogs.Push($"Information : User {username}'s database saved"); + + // Then + database.User.Services.Length.Should().Be(2); + + database.User.Services[0].ServiceName.Should().Be("Service0"); + database.User.Services[0].Url.Should().Be("www.service0.xyz"); + database.User.Services[0].Notes.Should().Be("Service0's notes"); + + database.User.Services[0].Accounts.Length.Should().Be(2); + + database.User.Services[0].Accounts[0].Label.Should().Be("Account0"); + database.User.Services[0].Accounts[0].Identifiers.Should().BeEquivalentTo(new[] { "account0@service0.xyz", "account0_backup@service0.xyz" }); + database.User.Services[0].Accounts[0].Password.Should().Be("0000"); + database.User.Services[0].Accounts[0].Notes.Should().Be("Service0's Account0's notes"); + database.User.Services[0].Accounts[0].Options.Should().Be(AccountOption.None); + database.User.Services[0].Accounts[0].PasswordUpdateReminderDelay.Should().Be(3); + + database.User.Services[0].Accounts[1].Label.Should().Be("Account1"); + database.User.Services[0].Accounts[1].Identifiers.Should().BeEquivalentTo(new[] { "account1@service0.xyz", "account1_backup@service0.xyz" }); + database.User.Services[0].Accounts[1].Password.Should().Be("1111"); + database.User.Services[0].Accounts[1].Notes.Should().Be("Service0's Account1's notes"); + database.User.Services[0].Accounts[1].Options.Should().Be(AccountOption.None); + database.User.Services[0].Accounts[1].PasswordUpdateReminderDelay.Should().Be(3); + + database.User.Services[1].ServiceName.Should().Be("Service1"); + database.User.Services[1].Url.Should().Be("www.service1.xyz"); + database.User.Services[1].Notes.Should().Be("Service1's notes"); + + database.User.Services[1].Accounts.Length.Should().Be(2); + + database.User.Services[1].Accounts[0].Label.Should().Be("Account0"); + database.User.Services[1].Accounts[0].Identifiers.Should().BeEquivalentTo(new[] { "account0@service1.xyz", "account0_backup@service1.xyz" }); + database.User.Services[1].Accounts[0].Password.Should().Be("AAAA"); + database.User.Services[1].Accounts[0].Notes.Should().Be("Service1's Account0's notes"); + database.User.Services[1].Accounts[0].Options.Should().Be(AccountOption.None); + database.User.Services[1].Accounts[0].PasswordUpdateReminderDelay.Should().Be(3); + + database.User.Services[1].Accounts[1].Label.Should().Be("Account1"); + database.User.Services[1].Accounts[1].Identifiers.Should().BeEquivalentTo(new[] { "account1@service1.xyz", "account1_backup@service1.xyz" }); + database.User.Services[1].Accounts[1].Password.Should().Be("BBBB"); + database.User.Services[1].Accounts[1].Notes.Should().Be("Service1's Account1's notes"); + database.User.Services[1].Accounts[1].Options.Should().Be(AccountOption.None); + database.User.Services[1].Accounts[1].PasswordUpdateReminderDelay.Should().Be(3); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + public void Case11_ImportJson_WrongFormat() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import_WrongFormat.json"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + + // When + database.ImportFromFile(importFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); + expectedLogs.Push($"Warning : Import failed because import file deserialization failed"); + + // Then + database.User.Services.Should().BeEmpty(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + + [TestMethod] + public void Case12_Export_FileAlreadyExists() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import.json"); + string exportFile = UnitTestsHelper.GetTestFilePath($"{username}/export.json", createIfNotExists: true); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + database.ImportFromFile(importFile); + + // When + database.ExportToFile(exportFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Exporting data to file : '{exportFile}'"); + expectedLogs.Push($"Warning : Export failed because export file already exists"); + + // Then + File.Exists(exportFile).Should().BeTrue(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + + [TestMethod] + public void Case13_Export_FileAlreadyExists() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + + string username = UnitTestsHelper.GetUsername(); + string[] passkeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + string importFile = UnitTestsHelper.GetTestFilePath($"import.json"); + string exportFile = UnitTestsHelper.GetTestFilePath($"{username}/export.txt"); + IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); + Stack expectedLogs = new(); + database.ImportFromFile(importFile); + + // When + database.ExportToFile(exportFile); + + expectedLogs.Push($"Information : User {username}'s database saved"); + expectedLogs.Push($"Warning : Exporting data to file : '{exportFile}'"); + expectedLogs.Push($"Warning : Export failed because '.txt' extention type is not handled"); + + // Then + File.Exists(exportFile).Should().BeFalse(); + + UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); + + // Finaly + database.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + } +} diff --git a/UnitTests/Models/ServiceUnitTests.cs b/UnitTests/Models/ServiceUnitTests.cs index 1d1e797..75bbe4d 100644 --- a/UnitTests/Models/ServiceUnitTests.cs +++ b/UnitTests/Models/ServiceUnitTests.cs @@ -1,8 +1,9 @@ using FluentAssertions; -using Upsilon.Apps.PassKey.Core.Public.Enums; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Utils; -namespace Upsilon.Apps.PassKey.UnitTests.Models +namespace Upsilon.Apps.Passkey.UnitTests.Models { [TestClass] public sealed class ServiceUnitTests @@ -32,6 +33,8 @@ public void Case01_AddServiceUpdateSaved() expectedLogs.Push($"Information : Service {oldServiceName} has been added to User {username}"); // Then + databaseCreated.User.HasChanged().Should().BeTrue(); + service.HasChanged().Should().BeFalse(); _ = databaseCreated.User.Services.Length.Should().Be(1); // When @@ -46,6 +49,14 @@ public void Case01_AddServiceUpdateSaved() service.Notes = notes; expectedLogs.Push($"Information : Service {newServiceName}'s notes has been set to {notes}"); + // Then + databaseCreated.User.HasChanged().Should().BeTrue(); + service.HasChanged().Should().BeTrue(); + service.HasChanged(nameof(service.ServiceName)).Should().BeTrue(); + service.HasChanged(nameof(service.Url)).Should().BeTrue(); + service.HasChanged(nameof(service.Notes)).Should().BeTrue(); + + // When databaseCreated.Save(); expectedLogs.Push($"Information : User {username}'s database saved"); databaseCreated.Close(); @@ -79,7 +90,7 @@ public void Case01_AddServiceUpdateSaved() /* * User.AddService adds the new service, * Then updating the service without saving will create the autosave file, - * Then Database.Open with AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile loads correctly the updated database file with the updated service. + * Then Database.Open with AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile loads correctly the updated database file with the updated service. */ public void Case02_AddServiceUpdateAutoSave() { @@ -119,11 +130,11 @@ public void Case02_AddServiceUpdateAutoSave() expectedLogWarnings.Push($"Warning : User {username} logged out without saving"); expectedLogs.Push($"Information : User {username}'s database closed"); - IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile); + IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile); expectedLogs.Push($"Information : User {username}'s database opened"); expectedLogs.Push($"Information : User {username} logged in"); - expectedLogs.Push($"Warning : User {username}'s autosave merged"); - expectedLogWarnings.Push($"Warning : User {username}'s autosave merged"); + expectedLogs.Push($"Warning : User {username}'s autosave merged and saved"); + expectedLogWarnings.Push($"Warning : User {username}'s autosave merged and saved"); // Then _ = databaseLoaded.User.Services.Length.Should().Be(1); @@ -199,7 +210,7 @@ public void Case03_DeleteServiceUpdateSaved() [TestMethod] /* * User.DeleteService adeletes the service, - * Then Database.Open with AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile loads correctly the updated database file with the updated service. + * Then Database.Open with AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile loads correctly the updated database file with the updated service. */ public void Case04_DeleteServiceUpdateAutoSave() { @@ -214,7 +225,7 @@ public void Case04_DeleteServiceUpdateAutoSave() Stack expectedLogs = new(); Stack expectedLogWarnings = new(); - IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile); + IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile); IService serviceLoaded = databaseLoaded.User.Services.First(); // When @@ -231,11 +242,11 @@ public void Case04_DeleteServiceUpdateAutoSave() expectedLogWarnings.Push($"Warning : User {username} logged out without saving"); expectedLogs.Push($"Information : User {username}'s database closed"); - databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile); + databaseLoaded = UnitTestsHelper.OpenTestDatabase(passkeys, out _, AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile); expectedLogs.Push($"Information : User {username}'s database opened"); expectedLogs.Push($"Information : User {username} logged in"); - expectedLogs.Push($"Warning : User {username}'s autosave merged"); - expectedLogWarnings.Push($"Warning : User {username}'s autosave merged"); + expectedLogs.Push($"Warning : User {username}'s autosave merged and saved"); + expectedLogWarnings.Push($"Warning : User {username}'s autosave merged and saved"); // Then _ = databaseLoaded.User.Services.Length.Should().Be(0); diff --git a/UnitTests/Models/UserUnitTests.cs b/UnitTests/Models/UserUnitTests.cs index 91684e7..e9852ae 100644 --- a/UnitTests/Models/UserUnitTests.cs +++ b/UnitTests/Models/UserUnitTests.cs @@ -1,8 +1,9 @@ using FluentAssertions; -using Upsilon.Apps.PassKey.Core.Public.Enums; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Utils; -namespace Upsilon.Apps.PassKey.UnitTests.Models +namespace Upsilon.Apps.Passkey.UnitTests.Models { [TestClass] public sealed class UserUnitTests @@ -31,6 +32,15 @@ public void Case01_UserUpdateWithoutSaving() databaseLoaded.User.Passkeys = newPasskeys; databaseLoaded.User.LogoutTimeout = logoutTimeout; databaseLoaded.User.CleaningClipboardTimeout = cleaningClipboardTimeout; + + // Then + databaseLoaded.User.HasChanged().Should().BeTrue(); + databaseLoaded.User.HasChanged(nameof(databaseLoaded.User.Username)).Should().BeTrue(); + databaseLoaded.User.HasChanged(nameof(databaseLoaded.User.Passkeys)).Should().BeTrue(); + databaseLoaded.User.HasChanged(nameof(databaseLoaded.User.LogoutTimeout)).Should().BeTrue(); + databaseLoaded.User.HasChanged(nameof(databaseLoaded.User.CleaningClipboardTimeout)).Should().BeTrue(); + + // When databaseLoaded.Close(); // Then @@ -119,11 +129,11 @@ public void Case02_UserUpdateThenSaved() _ = File.Exists(autoSaveFile).Should().BeFalse(); - _ = databaseLoaded.Warnings.Should().NotBeEmpty(); - UnitTestsHelper.LastLogsShouldMatch(databaseLoaded, [.. expectedLogs]); UnitTestsHelper.LastLogWarningsShouldMatch(databaseLoaded, [.. expectedLogWarnings]); + _ = databaseLoaded.Warnings.Should().NotBeEmpty(); + // Finaly databaseLoaded.Close(); UnitTestsHelper.ClearTestEnvironment(); @@ -136,7 +146,7 @@ public void Case02_UserUpdateThenSaved() * Then HandleAutoSave updates the database object and the database file, * Then Database.Open loads correctly the updated database file. */ - public void Case03_UserUpdateButNotSaved() + public void Case03_UserUpdateButNotSaved_CaseMergeAndSave() { // Given UnitTestsHelper.ClearTestEnvironment(); @@ -182,20 +192,20 @@ public void Case03_UserUpdateButNotSaved() _ = File.Exists(autoSaveFile).Should().BeTrue(); // When - IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(oldPasskeys, out IWarning[] warnings, AutoSaveMergeBehavior.MergeThenRemoveAutoSaveFile); + IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(oldPasskeys, out IWarning[] warnings, AutoSaveMergeBehavior.MergeAndSaveThenRemoveAutoSaveFile); expectedLogs.Push($"Information : User {oldUsername}'s database opened"); expectedLogs.Push($"Information : User {oldUsername} logged in"); - expectedLogs.Push($"Warning : User {oldUsername}'s autosave merged"); - expectedLogWarnings.Push($"Warning : User {oldUsername}'s autosave merged"); + expectedLogs.Push($"Warning : User {oldUsername}'s autosave merged and saved"); + expectedLogWarnings.Push($"Warning : User {oldUsername}'s autosave merged and saved"); // Then _ = File.Exists(autoSaveFile).Should().BeFalse(); + _ = databaseLoaded.User.HasChanged().Should().BeFalse(); _ = databaseLoaded.User.Username.Should().Be(newUsername); _ = databaseLoaded.User.Passkeys.Should().BeEquivalentTo(newPasskeys); _ = databaseLoaded.User.LogoutTimeout.Should().Be(logoutTimeout); _ = databaseLoaded.User.CleaningClipboardTimeout.Should().Be(cleaningClipboardTimeout); - _ = databaseLoaded.Warnings.Should().NotBeEmpty(); _ = warnings.Should().BeEmpty(); // When @@ -223,11 +233,123 @@ public void Case03_UserUpdateButNotSaved() _ = databaseLoaded.User.Passkeys.Should().BeEquivalentTo(newPasskeys); _ = databaseLoaded.User.LogoutTimeout.Should().Be(logoutTimeout); _ = databaseLoaded.User.CleaningClipboardTimeout.Should().Be(cleaningClipboardTimeout); + + UnitTestsHelper.LastLogsShouldMatch(databaseLoaded, [.. expectedLogs]); + UnitTestsHelper.LastLogWarningsShouldMatch(databaseLoaded, [.. expectedLogWarnings]); + _ = databaseLoaded.Warnings.Should().NotBeEmpty(); + // Finaly + databaseLoaded.Close(); + UnitTestsHelper.ClearTestEnvironment(); + } + + [TestMethod] + /* + * Updating User creates an autosave file, + * Then Database.Open loads the database file without the updated data, + * Then HandleAutoSave updates the database object but not save the database file, + * Then Database.Open loads correctly the updated database file but tag the data "unsaved". + */ + public void Case04_UserUpdateButNotSaved_CaseMergeWithoutSaving() + { + // Given + UnitTestsHelper.ClearTestEnvironment(); + string oldUsername = UnitTestsHelper.GetUsername(); + string[] oldPasskeys = UnitTestsHelper.GetRandomStringArray(); + string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); + string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); + string logFile = UnitTestsHelper.ComputeLogFilePath(); + IDatabase databaseCreated = UnitTestsHelper.CreateTestDatabase(oldPasskeys); + string newUsername = "new_" + oldUsername; + string[] newPasskeys = UnitTestsHelper.GetRandomStringArray(); + int logoutTimeout = UnitTestsHelper.GetRandomInt(1, 60); + int cleaningClipboardTimeout = UnitTestsHelper.GetRandomInt(1, 60); + Stack expectedLogs = new(); + Stack expectedLogWarnings = new(); + + // When + databaseCreated.User.Username = newUsername; + databaseCreated.User.Username = newUsername; + expectedLogs.Push($"Warning : User {oldUsername}'s username has been set to {newUsername}"); + expectedLogWarnings.Push($"Warning : User {oldUsername}'s username has been set to {newUsername}"); + databaseCreated.User.Passkeys = newPasskeys; + databaseCreated.User.Passkeys = newPasskeys; + expectedLogs.Push($"Warning : User {oldUsername}'s passkeys has been updated"); + expectedLogWarnings.Push($"Warning : User {oldUsername}'s passkeys has been updated"); + databaseCreated.User.LogoutTimeout = logoutTimeout; + databaseCreated.User.LogoutTimeout = logoutTimeout; + expectedLogs.Push($"Information : User {oldUsername}'s logout timeout has been set to {logoutTimeout}"); + databaseCreated.User.CleaningClipboardTimeout = cleaningClipboardTimeout; + databaseCreated.User.CleaningClipboardTimeout = cleaningClipboardTimeout; + expectedLogs.Push($"Information : User {oldUsername}'s cleaning clipboard timeout has been set to {cleaningClipboardTimeout}"); + databaseCreated.User.WarningsToNotify = WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning; + databaseCreated.User.WarningsToNotify = WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning; + expectedLogs.Push($"Warning : User {oldUsername}'s warnings to notify has been set to {WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning}"); + expectedLogWarnings.Push($"Warning : User {oldUsername}'s warnings to notify has been set to {WarningType.DuplicatedPasswordsWarning | WarningType.PasswordUpdateReminderWarning}"); + + databaseCreated.Close(); + expectedLogs.Push($"Warning : User {oldUsername} logged out without saving"); + expectedLogWarnings.Push($"Warning : User {oldUsername} logged out without saving"); + expectedLogs.Push($"Information : User {oldUsername}'s database closed"); + + // Then + _ = File.Exists(autoSaveFile).Should().BeTrue(); + + // When + IDatabase databaseLoaded = UnitTestsHelper.OpenTestDatabase(oldPasskeys, out IWarning[] warnings, AutoSaveMergeBehavior.MergeWithoutSavingAndKeepAutoSaveFile); + expectedLogs.Push($"Information : User {oldUsername}'s database opened"); + expectedLogs.Push($"Information : User {oldUsername} logged in"); + expectedLogs.Push($"Warning : User {oldUsername}'s autosave merged without saving"); + expectedLogWarnings.Push($"Warning : User {oldUsername}'s autosave merged without saving"); + + // Then + _ = File.Exists(autoSaveFile).Should().BeTrue(); + _ = databaseLoaded.User.HasChanged().Should().BeTrue(); + _ = databaseLoaded.User.HasChanged(nameof(databaseLoaded.User.Username)).Should().BeTrue(); + _ = databaseLoaded.User.Username.Should().Be(newUsername); + _ = databaseLoaded.User.HasChanged(nameof(databaseLoaded.User.Passkeys)).Should().BeTrue(); + _ = databaseLoaded.User.Passkeys.Should().BeEquivalentTo(newPasskeys); + _ = databaseLoaded.User.HasChanged(nameof(databaseLoaded.User.LogoutTimeout)).Should().BeTrue(); + _ = databaseLoaded.User.LogoutTimeout.Should().Be(logoutTimeout); + _ = databaseLoaded.User.HasChanged(nameof(databaseLoaded.User.CleaningClipboardTimeout)).Should().BeTrue(); + _ = databaseLoaded.User.CleaningClipboardTimeout.Should().Be(cleaningClipboardTimeout); + + _ = warnings.Should().BeEmpty(); + + // When + databaseLoaded.Save(); + expectedLogs.Push($"Information : User {newUsername}'s database saved"); + databaseLoaded.Close(); + expectedLogs.Push($"Information : User {newUsername} logged out"); + expectedLogs.Push($"Information : User {newUsername}'s database closed"); + + databaseLoaded = IDatabase.Open(UnitTestsHelper.CryptographicCenter, + UnitTestsHelper.SerializationCenter, + UnitTestsHelper.PasswordFactory, + databaseFile, + autoSaveFile, + logFile, + newUsername); + expectedLogs.Push($"Information : User {newUsername}'s database opened"); + foreach (string passkey in newPasskeys) + { + _ = databaseLoaded.Login(passkey); + } + expectedLogs.Push($"Information : User {newUsername} logged in"); + + // Then + _ = File.Exists(autoSaveFile).Should().BeFalse(); + _ = databaseLoaded.User.Username.Should().Be(newUsername); + _ = databaseLoaded.User.Passkeys.Should().BeEquivalentTo(newPasskeys); + _ = databaseLoaded.User.LogoutTimeout.Should().Be(logoutTimeout); + _ = databaseLoaded.User.CleaningClipboardTimeout.Should().Be(cleaningClipboardTimeout); + UnitTestsHelper.LastLogsShouldMatch(databaseLoaded, [.. expectedLogs]); UnitTestsHelper.LastLogWarningsShouldMatch(databaseLoaded, [.. expectedLogWarnings]); + _ = databaseLoaded.Warnings.Should().NotBeEmpty(); + // Finaly databaseLoaded.Close(); UnitTestsHelper.ClearTestEnvironment(); diff --git a/UnitTests/TestFiles/import.csv b/UnitTests/TestFiles/import.csv new file mode 100644 index 0000000..0f7b27b --- /dev/null +++ b/UnitTests/TestFiles/import.csv @@ -0,0 +1,5 @@ +ServiceName ServiceUrl ServiceNotes AccountLabel Identifiers Password AccountNotes AccountOptions PasswordUpdateReminderDelay +"Service0" "www.service0.xyz" "Service0\u0027s notes" "Account0" "account0@service0.xyz|account0_backup@service0.xyz" "0000" "Service0\u0027s Account0\u0027s notes" "None" 3 +"Service0" "www.service0.xyz" "Service0\u0027s notes" "Account1" "account1@service0.xyz|account1_backup@service0.xyz" "1111" "Service0\u0027s Account1\u0027s notes" "None" 3 +"Service1" "www.service1.xyz" "Service1\u0027s notes" "Account0" "account0@service1.xyz|account0_backup@service1.xyz" "AAAA" "Service1\u0027s Account0\u0027s notes" "None" 3 +"Service1" "www.service1.xyz" "Service1\u0027s notes" "Account1" "account1@service1.xyz|account1_backup@service1.xyz" "BBBB" "Service1\u0027s Account1\u0027s notes" "None" 3 diff --git a/UnitTests/TestFiles/import.json b/UnitTests/TestFiles/import.json new file mode 100644 index 0000000..cbe5543 --- /dev/null +++ b/UnitTests/TestFiles/import.json @@ -0,0 +1,78 @@ +[ + { + "ItemId": "eXZ6qTu9LwbPIRGL0YgYZQ==7Fo5YgcgzaCQbBFmigk1WbNgUwU=37RLlpzi9VX85YalKXG0cA==sbuNCB\u002B7gEqqz55oh0i3U9mTDPs=", + "Accounts": [ + { + "ItemId": "eXZ6qTu9LwbPIRGL0YgYZQ==7Fo5YgcgzaCQbBFmigk1WbNgUwU=37RLlpzi9VX85YalKXG0cA==sbuNCB\u002B7gEqqz55oh0i3U9mTDPs=jHeUM0yDZ3766A6Q\u002BBuRGQ==b\u002B6nLTM-xoUZGm6mH8aLxJ5R1Ok=", + "Label": "Account0", + "Identifiers": [ + "account0@service0.xyz", + "account0_backup@service0.xyz" + ], + "Password": "0000", + "Passwords": { + "2025-11-28T14:48:28.6023277+03:00": "0000" + }, + "Notes": "Service0\u0027s Account0\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + }, + { + "ItemId": "eXZ6qTu9LwbPIRGL0YgYZQ==7Fo5YgcgzaCQbBFmigk1WbNgUwU=37RLlpzi9VX85YalKXG0cA==sbuNCB\u002B7gEqqz55oh0i3U9mTDPs=WA6YxI54pKPhOViHyC37RA==talAus-d2TCHuNMcKrWtpgSmsss=", + "Label": "Account1", + "Identifiers": [ + "account1@service0.xyz", + "account1_backup@service0.xyz" + ], + "Password": "1111", + "Passwords": { + "2025-11-28T14:48:28.9224604+03:00": "1111" + }, + "Notes": "Service0\u0027s Account1\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + } + ], + "ServiceName": "Service0", + "Url": "www.service0.xyz", + "Notes": "Service0\u0027s notes" + }, + { + "ItemId": "eXZ6qTu9LwbPIRGL0YgYZQ==7Fo5YgcgzaCQbBFmigk1WbNgUwU=jrxkc\u002BsJFH\u002BIFczozfFImQ==HjTPTnu9u2anbu8mhoHj2YSnGJY=", + "Accounts": [ + { + "ItemId": "eXZ6qTu9LwbPIRGL0YgYZQ==7Fo5YgcgzaCQbBFmigk1WbNgUwU=jrxkc\u002BsJFH\u002BIFczozfFImQ==HjTPTnu9u2anbu8mhoHj2YSnGJY=TPIjH-MHbiYdF0c3smzJHQ==4N-tDXIlas1bY47AMMGvU1GtZjw=", + "Label": "Account0", + "Identifiers": [ + "account0@service1.xyz", + "account0_backup@service1.xyz" + ], + "Password": "AAAA", + "Passwords": { + "2025-11-28T14:48:29.5464925+03:00": "AAAA" + }, + "Notes": "Service1\u0027s Account0\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + }, + { + "ItemId": "eXZ6qTu9LwbPIRGL0YgYZQ==7Fo5YgcgzaCQbBFmigk1WbNgUwU=jrxkc\u002BsJFH\u002BIFczozfFImQ==HjTPTnu9u2anbu8mhoHj2YSnGJY=34tP0ZDF29WU5eVpYJR0qw==ZBW4YjgfwYfRrpGaaTqmsLfcQNU=", + "Label": "Account1", + "Identifiers": [ + "account1@service1.xyz", + "account1_backup@service1.xyz" + ], + "Password": "BBBB", + "Passwords": { + "2025-11-28T14:48:30.0815746+03:00": "BBBB" + }, + "Notes": "Service1\u0027s Account1\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + } + ], + "ServiceName": "Service1", + "Url": "www.service1.xyz", + "Notes": "Service1\u0027s notes" + } +] \ No newline at end of file diff --git a/UnitTests/TestFiles/import_MissingCollumn.csv b/UnitTests/TestFiles/import_MissingCollumn.csv new file mode 100644 index 0000000..ec8e5db --- /dev/null +++ b/UnitTests/TestFiles/import_MissingCollumn.csv @@ -0,0 +1,5 @@ +ServiceName ServiceUrl ServiceNotes AccountLabel Identifiers Password AccountNotes AccountOptions PasswordUpdateReminderDelay +"Service0" "www.service0.xyz" "Service0's notes" "Account0" "account0@service0.xyz|account0_backup@service0.xyz" "0000" "Service0's Account0's notes" 0 3 +"Service0" "www.service0.xyz" "Service0's notes" "Account1" "account1@service0.xyz|account1_backup@service0.xyz" "1111" "Service0's Account1's notes" 0 3 +"Service1" "www.service1.xyz" "Service1's notes" "Account0" "account0@service1.xyz|account0_backup@service1.xyz" "AAAA" "Service1's Account0's notes" 3 +"Service1" "www.service1.xyz" "Service1's notes" "Account1" "account1@service1.xyz|account1_backup@service1.xyz" "BBBB" "Service1's Account1's notes" 0 3 \ No newline at end of file diff --git a/UnitTests/TestFiles/import_MissingHearder.csv b/UnitTests/TestFiles/import_MissingHearder.csv new file mode 100644 index 0000000..2c6a7fd --- /dev/null +++ b/UnitTests/TestFiles/import_MissingHearder.csv @@ -0,0 +1,5 @@ +ServiceName ServiceUrl ServiceNotes AccountLabel Identifiers AccountNotes AccountOptions PasswordUpdateReminderDelay +"Service0" "www.service0.xyz" "Service0\u0027s notes" "Account0" "account0@service0.xyz|account0_backup@service0.xyz" "Service0\u0027s Account0\u0027s notes" "None" 3 +"Service0" "www.service0.xyz" "Service0\u0027s notes" "Account1" "account1@service0.xyz|account1_backup@service0.xyz" "Service0\u0027s Account1\u0027s notes" "None" 3 +"Service1" "www.service1.xyz" "Service1\u0027s notes" "Account0" "account0@service1.xyz|account0_backup@service1.xyz" "Service1\u0027s Account0\u0027s notes" "None" 3 +"Service1" "www.service1.xyz" "Service1\u0027s notes" "Account1" "account1@service1.xyz|account1_backup@service1.xyz" "Service1\u0027s Account1\u0027s notes" "None" 3 diff --git a/UnitTests/TestFiles/import_WrongFormat.json b/UnitTests/TestFiles/import_WrongFormat.json new file mode 100644 index 0000000..44f28ca --- /dev/null +++ b/UnitTests/TestFiles/import_WrongFormat.json @@ -0,0 +1,52 @@ +[ + { + "ServiceName": "Service0", + "Url": "www.service0.xyz", + "Notes": "Service0\u0027s notes", + "Accounts": [ + { + "Label": "Account0", + "Identifiers": [ "account0@service0.xyz", "account0_backup@service0.xyz" ], + "Password": "0000", + "Passwords": { "2025-11-26T21:33:12.4829309+03:00": "0000" }, + "Notes": "Service0\u0027s Account0\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + } + { + "Label": "Account1", + "Identifiers": [ "account1@service0.xyz", "account1_backup@service0.xyz" ], + "Password": "1111", + "Passwords": { "2025-11-26T21:33:12.7380781+03:00": "1111" }, + "Notes": "Service0\u0027s Account1\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + } + ] + }, + { + "ServiceName": "Service1", + "Url": "www.service1.xyz", + "Notes": "Service1\u0027s notes", + "Accounts": [ + { + "Label": "Account0", + "Identifiers": [ "account0@service1.xyz", "account0_backup@service1.xyz" ], + "Password": "AAAA", + "Passwords": { "2025-11-26T21:33:13.1589472+03:00": "AAAA" }, + "Notes": "Service1\u0027s Account0\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + }, + { + "Label": "Account1", + "Identifiers": [ "account1@service1.xyz", "account1_backup@service1.xyz" ], + "Password": "BBBB", + "Passwords": { "2025-11-26T21:33:13.4643103+03:00": "BBBB" }, + "Notes": "Service1\u0027s Account1\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + } + ] + } +] \ No newline at end of file diff --git a/UnitTests/TestFiles/import_blanckIdentifier.json b/UnitTests/TestFiles/import_blanckIdentifier.json new file mode 100644 index 0000000..0773473 --- /dev/null +++ b/UnitTests/TestFiles/import_blanckIdentifier.json @@ -0,0 +1,52 @@ +[ + { + "ServiceName": "Service0", + "Url": "www.service0.xyz", + "Notes": "Service0\u0027s notes", + "Accounts": [ + { + "Label": "Account0", + "Identifiers": [ "account0@service0.xyz", "account0_backup@service0.xyz" ], + "Password": "0000", + "Passwords": { "2025-11-26T21:33:12.4829309+03:00": "0000" }, + "Notes": "Service0\u0027s Account0\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + }, + { + "Label": "Account1", + "Identifiers": [ "account1@service0.xyz", "account1_backup@service0.xyz" ], + "Password": "1111", + "Passwords": { "2025-11-26T21:33:12.7380781+03:00": "1111" }, + "Notes": "Service0\u0027s Account1\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + } + ] + }, + { + "ServiceName": "Service1", + "Url": "www.service1.xyz", + "Notes": "Service1\u0027s notes", + "Accounts": [ + { + "Label": "Account0", + "Identifiers": [ "account0@service1.xyz", "account0_backup@service1.xyz" ], + "Password": "AAAA", + "Passwords": { "2025-11-26T21:33:13.1589472+03:00": "AAAA" }, + "Notes": "Service1\u0027s Account0\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + }, + { + "Label": "Account1", + "Identifiers": [ "account1@service1.xyz", "" ], + "Password": "BBBB", + "Passwords": { "2025-11-26T21:33:13.4643103+03:00": "BBBB" }, + "Notes": "Service1\u0027s Account1\u0027s notes", + "PasswordUpdateReminderDelay": 3, + "Options": "None" + } + ] + } +] \ No newline at end of file diff --git a/UnitTests/TestFiles/import_blanckService.csv b/UnitTests/TestFiles/import_blanckService.csv new file mode 100644 index 0000000..0207639 --- /dev/null +++ b/UnitTests/TestFiles/import_blanckService.csv @@ -0,0 +1,5 @@ +ServiceName ServiceUrl ServiceNotes AccountLabel Identifiers Password AccountNotes AccountOptions PasswordUpdateReminderDelay +"Service0" "www.service0.xyz" "Service0's notes" "Account0" "account0@service0.xyz|account0_backup@service0.xyz" "0000" "Service0's Account0's notes" 0 3 +"Service0" "www.service0.xyz" "Service0's notes" "Account1" "account1@service0.xyz|account1_backup@service0.xyz" "1111" "Service0's Account1's notes" 0 3 +"" "www.service1.xyz" "Service1's notes" "Account0" "account0@service1.xyz|account0_backup@service1.xyz" "AAAA" "Service1's Account0's notes" 0 3 +"" "www.service1.xyz" "Service1's notes" "Account1" "account1@service1.xyz|account1_backup@service1.xyz" "BBBB" "Service1's Account1's notes" 0 3 \ No newline at end of file diff --git a/UnitTests/TestFiles/import_noData.csv b/UnitTests/TestFiles/import_noData.csv new file mode 100644 index 0000000..6bf25b9 --- /dev/null +++ b/UnitTests/TestFiles/import_noData.csv @@ -0,0 +1 @@ +ServiceName ServiceUrl ServiceNotes AccountLabel Identifiers Password AccountNotes AccountOptions PasswordUpdateReminderDelay \ No newline at end of file diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 1ca8dd9..c5088d6 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -1,7 +1,7 @@ - + - net8.0-windows10.0.18362.0 + net10.0-windows10.0.18362.0 latest enable enable @@ -31,4 +31,10 @@ + + + Always + + + diff --git a/UnitTests/UnitTestsHelper.cs b/UnitTests/UnitTestsHelper.cs index e11e399..4840019 100644 --- a/UnitTests/UnitTestsHelper.cs +++ b/UnitTests/UnitTestsHelper.cs @@ -1,11 +1,11 @@ using FluentAssertions; using System.Runtime.CompilerServices; using System.Security.Cryptography; -using Upsilon.Apps.PassKey.Core.Public.Enums; -using Upsilon.Apps.PassKey.Core.Public.Interfaces; -using Upsilon.Apps.PassKey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Public.Utils; -namespace Upsilon.Apps.PassKey.UnitTests +namespace Upsilon.Apps.Passkey.UnitTests { internal static class UnitTestsHelper { @@ -15,10 +15,30 @@ internal static class UnitTestsHelper public static readonly ISerializationCenter SerializationCenter = new JsonSerializationCenter(); public static readonly IPasswordFactory PasswordFactory = new PasswordFactory(); - public static string ComputeDatabaseFileDirectory([CallerMemberName] string username = "") => $"./TestFiles/{username}"; - public static string ComputeDatabaseFilePath([CallerMemberName] string username = "") => $"{ComputeDatabaseFileDirectory(username)}/{username}.pku"; - public static string ComputeAutoSaveFilePath([CallerMemberName] string username = "") => $"{ComputeDatabaseFileDirectory(username)}/{username}.pka"; - public static string ComputeLogFilePath([CallerMemberName] string username = "") => $"{ComputeDatabaseFileDirectory(username)}/{username}.pkl"; + public static string ComputeTestDirectory([CallerMemberName] string username = "") => $"./TestFiles/{username}"; + public static string ComputeDatabaseFileDirectory([CallerMemberName] string username = "") => $"{ComputeTestDirectory(username)}/{CryptographicCenter.GetHash(username)}"; + public static string ComputeDatabaseFilePath([CallerMemberName] string username = "") => $"{ComputeDatabaseFileDirectory(username)}/{CryptographicCenter.GetHash(username)}.pku"; + public static string ComputeAutoSaveFilePath([CallerMemberName] string username = "") => $"{ComputeDatabaseFileDirectory(username)}/{CryptographicCenter.GetHash(username)}.pka"; + public static string ComputeLogFilePath([CallerMemberName] string username = "") => $"{ComputeDatabaseFileDirectory(username)}/{CryptographicCenter.GetHash(username)}.pkl"; + + public static string GetTestFilePath(string fileName, bool createIfNotExists = false) + { + string filePath = $"./TestFiles/{fileName}"; + + if (!File.Exists(filePath) + && createIfNotExists) + { + string fileDirectory = Path.GetDirectoryName(filePath); + if (!Directory.Exists(fileDirectory)) + { + Directory.CreateDirectory(fileDirectory); + } + + File.Create(filePath).Close(); + } + + return filePath; + } public static IDatabase CreateTestDatabase(string[] passkeys = null, [CallerMemberName] string username = "") { @@ -37,11 +57,6 @@ public static IDatabase CreateTestDatabase(string[] passkeys = null, [CallerMemb username, passkeys); - foreach (string passkey in passkeys) - { - _ = database.Login(passkey); - } - return database; } @@ -76,7 +91,7 @@ public static IDatabase OpenTestDatabase(string[] passkeys, out IWarning[] detec public static void ClearTestEnvironment([CallerMemberName] string username = "") { - string directory = ComputeDatabaseFileDirectory(username); + string directory = ComputeTestDirectory(username); if (Directory.Exists(directory)) { @@ -144,6 +159,11 @@ public static void LastLogsShouldMatch(IDatabase database, string[] expectedLogs public static void LastLogWarningsShouldMatch(IDatabase database, string[] expectedLogs) { + while (database.Warnings == null) + { + Thread.Sleep(200); + } + IWarning logWarning = database.Warnings.First(x => x.WarningType == WarningType.LogReviewWarning); string[] actualLogs = logWarning.Logs diff --git a/UnitTests/Utils/CryptographyCenterUnitTexts.cs b/UnitTests/Utils/CryptographyCenterUnitTexts.cs index 75c2075..2458713 100644 --- a/UnitTests/Utils/CryptographyCenterUnitTexts.cs +++ b/UnitTests/Utils/CryptographyCenterUnitTexts.cs @@ -1,8 +1,8 @@ using FluentAssertions; using System.Diagnostics; -using Upsilon.Apps.PassKey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Public.Utils; -namespace Upsilon.Apps.PassKey.UnitTests.Utils +namespace Upsilon.Apps.Passkey.UnitTests.Utils { [TestClass] public sealed class CryptographyCenterUnitTexts @@ -12,7 +12,7 @@ public sealed class CryptographyCenterUnitTexts * Signing an empty string returns the hash code of that empty string, * Then checking the signature returns the empty string. */ - public void Case00_SlowHash() + public void Case01_SlowHash() { // Given Stopwatch _stopwatch = Stopwatch.StartNew(); @@ -25,12 +25,31 @@ public void Case00_SlowHash() _ = _stopwatch.ElapsedMilliseconds.Should().BeGreaterThan(500); } + [TestMethod] + /* + * The length of any should be constantly equal to `HashLength`. + */ + public void Case02_HashLength() + { + for (int i = 0; i < UnitTestsHelper.RANDOMIZED_TESTS_LOOP; i++) + { + // Given + string source = UnitTestsHelper.GetRandomString(); + + // When + string hash = UnitTestsHelper.CryptographicCenter.GetHash(source); + + // Then + _ = hash.Length.Should().Be(UnitTestsHelper.CryptographicCenter.HashLength); + } + } + [TestMethod] /* * Signing an empty string returns the hash code of that empty string, * Then checking the signature returns the empty string. */ - public void Case01_SignEmptyString() + public void Case03_SignEmptyString() { // Given string source = string.Empty; @@ -54,7 +73,7 @@ public void Case01_SignEmptyString() /* * Signing a random string then check the sign should rise no error. */ - public void Case02_SignRandomString() + public void Case04_SignRandomString() { for (int i = 0; i < UnitTestsHelper.RANDOMIZED_TESTS_LOOP; i++) { @@ -77,7 +96,7 @@ public void Case02_SignRandomString() * Encrypting symmetrically a random string then decrypting it should rise no error, * Then the decrypted string should be the same as the source. */ - public void Case03_SymmetricEncryptionRandomString() + public void Case05_SymmetricEncryptionRandomString() { for (int i = 0; i < UnitTestsHelper.RANDOMIZED_TESTS_LOOP; i++) { @@ -98,7 +117,7 @@ public void Case03_SymmetricEncryptionRandomString() /* * Decrypting symmetrically a corrupted string should rise an error. */ - public void Case04_SymmetricEncryptionDecryptingCorruptedRandomString() + public void Case06_SymmetricEncryptionDecryptingCorruptedRandomString() { for (int i = 0; i < UnitTestsHelper.RANDOMIZED_TESTS_LOOP; i++) { @@ -133,7 +152,7 @@ public void Case04_SymmetricEncryptionDecryptingCorruptedRandomString() /* * Decrypting symmetrically a random string with a wrong passkey should rise an error. */ - public void Case05_SymmetricEncryptionDecryptingRandomStringWithWrongPasskey() + public void Case07_SymmetricEncryptionDecryptingRandomStringWithWrongPasskey() { for (int i = 0; i < UnitTestsHelper.RANDOMIZED_TESTS_LOOP; i++) { @@ -171,7 +190,7 @@ public void Case05_SymmetricEncryptionDecryptingRandomStringWithWrongPasskey() * Encrypting a random string then decrypting it should rise no error, * Then the decrypted string should be the same as the source. */ - public void Case06_AsymmetricEncryptionRandomString() + public void Case08_AsymmetricEncryptionRandomString() { for (int i = 0; i < UnitTestsHelper.RANDOMIZED_TESTS_LOOP; i++) { @@ -192,7 +211,7 @@ public void Case06_AsymmetricEncryptionRandomString() /* * Decrypting a corrupted string should rise an error. */ - public void Case07_AsymmetricEncryptionDecryptingCorruptedRandomString() + public void Case09_AsymmetricEncryptionDecryptingCorruptedRandomString() { for (int i = 0; i < UnitTestsHelper.RANDOMIZED_TESTS_LOOP; i++) { @@ -227,7 +246,7 @@ public void Case07_AsymmetricEncryptionDecryptingCorruptedRandomString() /* * Decrypting a random string with a wrong passkey should rise an error. */ - public void Case08_AsymmetricEncryptionDecryptingRandomStringWithWrongPasskey() + public void Case10_AsymmetricEncryptionDecryptingRandomStringWithWrongPasskey() { for (int i = 0; i < UnitTestsHelper.RANDOMIZED_TESTS_LOOP; i++) { From 98d20fcd4cf5fd7ce00a280f512d4c3830d71084 Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Mon, 1 Dec 2025 11:14:10 +0300 Subject: [PATCH 03/14] Updating build --- .github/workflows/csharp-dotnet.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/csharp-dotnet.yml b/.github/workflows/csharp-dotnet.yml index e89950a..d0f671b 100644 --- a/.github/workflows/csharp-dotnet.yml +++ b/.github/workflows/csharp-dotnet.yml @@ -1,4 +1,4 @@ -name: C# Build with CMake +name: C# Build with Dot Net on: push: @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore Upsilon.Apps.Passkey.sln From 2fc0cc5835c82f72e44a9654ab514dfcc686f32c Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Mon, 1 Dec 2025 12:52:14 +0300 Subject: [PATCH 04/14] Fixing tests --- UnitTests/Models/ImportExportUnitTests.cs | 50 ++++-------------- .../TestFiles/import_blanckIdentifier.json | 52 ------------------- 2 files changed, 9 insertions(+), 93 deletions(-) delete mode 100644 UnitTests/TestFiles/import_blanckIdentifier.json diff --git a/UnitTests/Models/ImportExportUnitTests.cs b/UnitTests/Models/ImportExportUnitTests.cs index 466cbe4..fb1dccb 100644 --- a/UnitTests/Models/ImportExportUnitTests.cs +++ b/UnitTests/Models/ImportExportUnitTests.cs @@ -163,7 +163,7 @@ public void Case05_ImportBlanckService() expectedLogs.Push($"Information : User {username}'s database saved"); expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); - expectedLogs.Push($"Warning : Import failed because service name or account identifier cannot be blank"); + expectedLogs.Push($"Warning : Import failed because service name cannot be blank"); // Then database.User.Services.Should().BeEmpty(); @@ -176,39 +176,7 @@ public void Case05_ImportBlanckService() } [TestMethod] - public void Case06_ImportBlanckIdentifier() - { - // Given - UnitTestsHelper.ClearTestEnvironment(); - - string username = UnitTestsHelper.GetUsername(); - string[] passkeys = UnitTestsHelper.GetRandomStringArray(); - string databaseFile = UnitTestsHelper.ComputeDatabaseFilePath(); - string autoSaveFile = UnitTestsHelper.ComputeAutoSaveFilePath(); - string logFile = UnitTestsHelper.ComputeLogFilePath(); - string importFile = UnitTestsHelper.GetTestFilePath($"import_blanckIdentifier.json"); - IDatabase database = UnitTestsHelper.CreateTestDatabase(passkeys); - Stack expectedLogs = new(); - - // When - database.ImportFromFile(importFile); - - expectedLogs.Push($"Information : User {username}'s database saved"); - expectedLogs.Push($"Warning : Importing data from file : '{importFile}'"); - expectedLogs.Push($"Warning : Import failed because service name or account identifier cannot be blank"); - - // Then - database.User.Services.Should().BeEmpty(); - - UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); - - // Finaly - database.Close(); - UnitTestsHelper.ClearTestEnvironment(); - } - - [TestMethod] - public void Case07_ImportCSV_OK() + public void Case06_ImportCSV_OK() { // Given UnitTestsHelper.ClearTestEnvironment(); @@ -314,7 +282,7 @@ public void Case07_ImportCSV_OK() expectedLogs.Push($"Warning : Export completed successfully"); // Then - File.ReadAllText(importFile).Should().Be(File.ReadAllText(exportFile)); + File.ReadAllText(importFile).Replace("\r", "").Should().Be(File.ReadAllText(exportFile).Replace("\r", "")); UnitTestsHelper.LastLogsShouldMatch(database, [.. expectedLogs]); @@ -324,7 +292,7 @@ public void Case07_ImportCSV_OK() } [TestMethod] - public void Case08_ImportCSV_MissingHeader() + public void Case07_ImportCSV_MissingHeader() { // Given UnitTestsHelper.ClearTestEnvironment(); @@ -356,7 +324,7 @@ public void Case08_ImportCSV_MissingHeader() } [TestMethod] - public void Case09_ImportCSV_MissingCollumn() + public void Case08_ImportCSV_MissingCollumn() { // Given UnitTestsHelper.ClearTestEnvironment(); @@ -388,7 +356,7 @@ public void Case09_ImportCSV_MissingCollumn() } [TestMethod] - public void Case10_ImportJson_OK() + public void Case09_ImportJson_OK() { // Given UnitTestsHelper.ClearTestEnvironment(); @@ -494,7 +462,7 @@ public void Case10_ImportJson_OK() } [TestMethod] - public void Case11_ImportJson_WrongFormat() + public void Case10_ImportJson_WrongFormat() { // Given UnitTestsHelper.ClearTestEnvironment(); @@ -527,7 +495,7 @@ public void Case11_ImportJson_WrongFormat() [TestMethod] - public void Case12_Export_FileAlreadyExists() + public void Case11_Export_FileAlreadyExists() { // Given UnitTestsHelper.ClearTestEnvironment(); @@ -562,7 +530,7 @@ public void Case12_Export_FileAlreadyExists() [TestMethod] - public void Case13_Export_FileAlreadyExists() + public void Case12_Export_FileExtensionNotHandled() { // Given UnitTestsHelper.ClearTestEnvironment(); diff --git a/UnitTests/TestFiles/import_blanckIdentifier.json b/UnitTests/TestFiles/import_blanckIdentifier.json deleted file mode 100644 index 0773473..0000000 --- a/UnitTests/TestFiles/import_blanckIdentifier.json +++ /dev/null @@ -1,52 +0,0 @@ -[ - { - "ServiceName": "Service0", - "Url": "www.service0.xyz", - "Notes": "Service0\u0027s notes", - "Accounts": [ - { - "Label": "Account0", - "Identifiers": [ "account0@service0.xyz", "account0_backup@service0.xyz" ], - "Password": "0000", - "Passwords": { "2025-11-26T21:33:12.4829309+03:00": "0000" }, - "Notes": "Service0\u0027s Account0\u0027s notes", - "PasswordUpdateReminderDelay": 3, - "Options": "None" - }, - { - "Label": "Account1", - "Identifiers": [ "account1@service0.xyz", "account1_backup@service0.xyz" ], - "Password": "1111", - "Passwords": { "2025-11-26T21:33:12.7380781+03:00": "1111" }, - "Notes": "Service0\u0027s Account1\u0027s notes", - "PasswordUpdateReminderDelay": 3, - "Options": "None" - } - ] - }, - { - "ServiceName": "Service1", - "Url": "www.service1.xyz", - "Notes": "Service1\u0027s notes", - "Accounts": [ - { - "Label": "Account0", - "Identifiers": [ "account0@service1.xyz", "account0_backup@service1.xyz" ], - "Password": "AAAA", - "Passwords": { "2025-11-26T21:33:13.1589472+03:00": "AAAA" }, - "Notes": "Service1\u0027s Account0\u0027s notes", - "PasswordUpdateReminderDelay": 3, - "Options": "None" - }, - { - "Label": "Account1", - "Identifiers": [ "account1@service1.xyz", "" ], - "Password": "BBBB", - "Passwords": { "2025-11-26T21:33:13.4643103+03:00": "BBBB" }, - "Notes": "Service1\u0027s Account1\u0027s notes", - "PasswordUpdateReminderDelay": 3, - "Options": "None" - } - ] - } -] \ No newline at end of file From 7da9cacefa7a16a529e3fc4848d6584241eff780 Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Fri, 5 Dec 2025 15:16:58 +0300 Subject: [PATCH 05/14] Separating Core and Interfaces projects --- Core/{Internal => }/Models/Account.cs | 12 +-- Core/{Internal => }/Models/AutoSave.cs | 8 +- Core/{Internal => }/Models/Change.cs | 2 +- Core/{Internal => }/Models/Database.cs | 18 ++-- Core/{Internal => }/Models/Log.cs | 4 +- Core/{Internal => }/Models/Service.cs | 6 +- Core/{Internal => }/Models/User.cs | 11 +- Core/{Internal => }/Models/Warning.cs | 6 +- .../Public/Interfaces/ISerializationCenter.cs | 50 --------- Core/Upsilon.Apps.Passkey.Core.csproj | 4 + Core/{Public => }/Utils/ClipboardManager.cs | 2 +- Core/{Public => }/Utils/CryptographyCenter.cs | 5 +- Core/{Internal => }/Utils/FileLocker.cs | 7 +- .../Utils/ImportExportHelper.cs | 8 +- .../Utils/JsonSerializationCenter.cs | 4 +- Core/{Internal => }/Utils/LogCenter.cs | 6 +- Core/{Public => }/Utils/PasswordFactory.cs | 4 +- Core/{Internal => }/Utils/StaticMethods.cs | 16 ++- .../Enums/AccountOption.cs | 2 +- .../Enums/AutoSaveMergeBehavior.cs | 2 +- .../Enums/WarningType.cs | 2 +- .../Events/AutoSaveDetectedEventArgs.cs | 4 +- .../Events/LogoutEventArgs.cs | 2 +- .../Events/WarningDetectedEventArgs.cs | 4 +- .../Interfaces/IAccount.cs | 4 +- .../Interfaces/ICryptographyCenter.cs | 2 +- .../Interfaces/IDatabase.cs | 61 +---------- .../Public => Interfaces}/Interfaces/IItem.cs | 2 +- .../Public => Interfaces}/Interfaces/ILog.cs | 2 +- .../Interfaces/IPasswordFactory.cs | 2 +- Interfaces/Interfaces/ISerializationCenter.cs | 24 +++++ .../Interfaces/IService.cs | 2 +- .../Public => Interfaces}/Interfaces/IUser.cs | 4 +- .../Interfaces/IWarning.cs | 4 +- .../Interfaces/Interfaces.cd | 100 ++++++++---------- .../Upsilon.Apps.Passkey.Interfaces.csproj | 22 ++++ .../Public => Interfaces}/Utils/Exceptions.cs | 2 +- .../Utils/StaticMethods.cs | 4 +- UnitTests/Models/AccountUnitTests.cs | 6 +- UnitTests/Models/DatabaseUnitTests.cs | 10 +- UnitTests/Models/ImportExportUnitTests.cs | 4 +- UnitTests/Models/ServiceUnitTests.cs | 6 +- UnitTests/Models/UserUnitTests.cs | 13 +-- UnitTests/UnitTestsHelper.cs | 11 +- .../Utils/CryptographyCenterUnitTexts.cs | 2 +- Upsilon.Apps.Passkey.sln | 10 +- 46 files changed, 216 insertions(+), 270 deletions(-) rename Core/{Internal => }/Models/Account.cs (95%) rename Core/{Internal => }/Models/AutoSave.cs (95%) rename Core/{Internal => }/Models/Change.cs (90%) rename Core/{Internal => }/Models/Database.cs (97%) rename Core/{Internal => }/Models/Log.cs (64%) rename Core/{Internal => }/Models/Service.cs (97%) rename Core/{Internal => }/Models/User.cs (96%) rename Core/{Internal => }/Models/Warning.cs (75%) delete mode 100644 Core/Public/Interfaces/ISerializationCenter.cs rename Core/{Public => }/Utils/ClipboardManager.cs (94%) rename Core/{Public => }/Utils/CryptographyCenter.cs (98%) rename Core/{Internal => }/Utils/FileLocker.cs (95%) rename Core/{Internal => }/Utils/ImportExportHelper.cs (97%) rename Core/{Public => }/Utils/JsonSerializationCenter.cs (85%) rename Core/{Internal => }/Utils/LogCenter.cs (90%) rename Core/{Public => }/Utils/PasswordFactory.cs (96%) rename Core/{Internal => }/Utils/StaticMethods.cs (62%) rename {Core/Public => Interfaces}/Enums/AccountOption.cs (86%) rename {Core/Public => Interfaces}/Enums/AutoSaveMergeBehavior.cs (94%) rename {Core/Public => Interfaces}/Enums/WarningType.cs (93%) rename {Core/Public => Interfaces}/Events/AutoSaveDetectedEventArgs.cs (81%) rename {Core/Public => Interfaces}/Events/LogoutEventArgs.cs (86%) rename {Core/Public => Interfaces}/Events/WarningDetectedEventArgs.cs (78%) rename {Core/Public => Interfaces}/Interfaces/IAccount.cs (91%) rename {Core/Public => Interfaces}/Interfaces/ICryptographyCenter.cs (98%) rename {Core/Public => Interfaces}/Interfaces/IDatabase.cs (58%) rename {Core/Public => Interfaces}/Interfaces/IItem.cs (85%) rename {Core/Public => Interfaces}/Interfaces/ILog.cs (88%) rename {Core/Public => Interfaces}/Interfaces/IPasswordFactory.cs (97%) create mode 100644 Interfaces/Interfaces/ISerializationCenter.cs rename {Core/Public => Interfaces}/Interfaces/IService.cs (97%) rename {Core/Public => Interfaces}/Interfaces/IUser.cs (94%) rename {Core/Public => Interfaces}/Interfaces/IWarning.cs (82%) rename {Core/Public => Interfaces}/Interfaces/Interfaces.cd (55%) create mode 100644 Interfaces/Upsilon.Apps.Passkey.Interfaces.csproj rename {Core/Public => Interfaces}/Utils/Exceptions.cs (95%) rename {Core/Public => Interfaces}/Utils/StaticMethods.cs (75%) diff --git a/Core/Internal/Models/Account.cs b/Core/Models/Account.cs similarity index 95% rename from Core/Internal/Models/Account.cs rename to Core/Models/Account.cs index 1b45e72..6dcb6be 100644 --- a/Core/Internal/Models/Account.cs +++ b/Core/Models/Account.cs @@ -1,9 +1,9 @@ -using System.ComponentModel; -using Upsilon.Apps.Passkey.Core.Internal.Utils; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Core.Utils; +using System.ComponentModel; -namespace Upsilon.Apps.Passkey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Models { internal sealed class Account : IAccount { @@ -47,7 +47,7 @@ string IAccount.Password if (!string.IsNullOrEmpty(value) && Password != value) { - Dictionary oldPasswords = ISerializationCenter.Clone(Database.SerializationCenter, Passwords); + Dictionary oldPasswords = Passwords.CloneWith(Database.SerializationCenter); Passwords[DateTime.Now] = Password = value; if (_service != null) diff --git a/Core/Internal/Models/AutoSave.cs b/Core/Models/AutoSave.cs similarity index 95% rename from Core/Internal/Models/AutoSave.cs rename to Core/Models/AutoSave.cs index 1a34baa..8d86ad4 100644 --- a/Core/Internal/Models/AutoSave.cs +++ b/Core/Models/AutoSave.cs @@ -1,7 +1,7 @@ -using Upsilon.Apps.Passkey.Core.Internal.Utils; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces; -namespace Upsilon.Apps.Passkey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Models { internal sealed class AutoSave { @@ -21,7 +21,7 @@ internal T UpdateValue(string itemId, T newValue, string readableValue) where T : notnull { - if (ISerializationCenter.AreDifferent(Database.SerializationCenter, oldValue, newValue)) + if (Database.SerializationCenter.AreDifferent(oldValue, newValue)) { _addChange(itemId, itemName, diff --git a/Core/Internal/Models/Change.cs b/Core/Models/Change.cs similarity index 90% rename from Core/Internal/Models/Change.cs rename to Core/Models/Change.cs index eb15356..67ba50c 100644 --- a/Core/Internal/Models/Change.cs +++ b/Core/Models/Change.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Models { internal sealed class Change { diff --git a/Core/Internal/Models/Database.cs b/Core/Models/Database.cs similarity index 97% rename from Core/Internal/Models/Database.cs rename to Core/Models/Database.cs index f3ef8f9..3331768 100644 --- a/Core/Internal/Models/Database.cs +++ b/Core/Models/Database.cs @@ -1,12 +1,12 @@ -using Upsilon.Apps.Passkey.Core.Internal.Utils; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Events; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; -using Upsilon.Apps.Passkey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Events; +using Upsilon.Apps.Passkey.Interfaces.Utils; -namespace Upsilon.Apps.Passkey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Models { - internal sealed class Database : IDatabase + public sealed class Database : IDatabase { #region IUser interface explicit Internal @@ -236,7 +236,7 @@ private Database(ICryptographyCenter cryptographicCenter, Logs.Database = this; } - internal static IDatabase Create(ICryptographyCenter cryptographicCenter, + public static IDatabase Create(ICryptographyCenter cryptographicCenter, ISerializationCenter serializationCenter, IPasswordFactory passwordFactory, string databaseFile, @@ -286,7 +286,7 @@ internal static IDatabase Create(ICryptographyCenter cryptographicCenter, return database; } - internal static IDatabase Open(ICryptographyCenter cryptographicCenter, + public static IDatabase Open(ICryptographyCenter cryptographicCenter, ISerializationCenter serializationCenter, IPasswordFactory passwordFactory, string databaseFile, diff --git a/Core/Internal/Models/Log.cs b/Core/Models/Log.cs similarity index 64% rename from Core/Internal/Models/Log.cs rename to Core/Models/Log.cs index 852c8ed..15837fd 100644 --- a/Core/Internal/Models/Log.cs +++ b/Core/Models/Log.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces; -namespace Upsilon.Apps.Passkey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Models { internal class Log : ILog { diff --git a/Core/Internal/Models/Service.cs b/Core/Models/Service.cs similarity index 97% rename from Core/Internal/Models/Service.cs rename to Core/Models/Service.cs index cdbc462..13f1dab 100644 --- a/Core/Internal/Models/Service.cs +++ b/Core/Models/Service.cs @@ -1,8 +1,8 @@ using System.ComponentModel; -using Upsilon.Apps.Passkey.Core.Internal.Utils; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces; -namespace Upsilon.Apps.Passkey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Models { internal sealed class Service : IService { diff --git a/Core/Internal/Models/User.cs b/Core/Models/User.cs similarity index 96% rename from Core/Internal/Models/User.cs rename to Core/Models/User.cs index e86d56d..4c67047 100644 --- a/Core/Internal/Models/User.cs +++ b/Core/Models/User.cs @@ -1,10 +1,9 @@ using System.ComponentModel; -using Upsilon.Apps.Passkey.Core.Internal.Utils; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; -using Upsilon.Apps.Passkey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; -namespace Upsilon.Apps.Passkey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Models { internal sealed class User : IUser { @@ -38,7 +37,7 @@ string[] IUser.Passkeys get => Database.Get(Passkeys); set { - CredentialChanged |= ISerializationCenter.AreDifferent(Database.SerializationCenter, Passkeys, value); + CredentialChanged |= Database.SerializationCenter.AreDifferent(Passkeys, value); Passkeys = Database.AutoSave.UpdateValue(ItemId, itemName: ToString(), diff --git a/Core/Internal/Models/Warning.cs b/Core/Models/Warning.cs similarity index 75% rename from Core/Internal/Models/Warning.cs rename to Core/Models/Warning.cs index 968b664..06ac3d2 100644 --- a/Core/Internal/Models/Warning.cs +++ b/Core/Models/Warning.cs @@ -1,7 +1,7 @@ -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; -namespace Upsilon.Apps.Passkey.Core.Internal.Models +namespace Upsilon.Apps.Passkey.Core.Models { internal class Warning : IWarning { diff --git a/Core/Public/Interfaces/ISerializationCenter.cs b/Core/Public/Interfaces/ISerializationCenter.cs deleted file mode 100644 index d5702ba..0000000 --- a/Core/Public/Interfaces/ISerializationCenter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Upsilon.Apps.Passkey.Core.Internal.Utils; - -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces -{ - /// - /// Represent a serialization center. - /// - public interface ISerializationCenter - { - /// - /// Serialize the given object to a string. - /// - /// The type of the object. - /// The object to serialize. - /// The serialised string. - string Serialize(T toSerialize) where T : notnull; - - /// - /// Deserialize the given string to the given type object. - /// - /// The type of the object. - /// The serialised string. - /// The deserialized object. - T Deserialize(string toDeserialize) where T : notnull; - - /// - /// Check if two objects are different or not. - /// - /// The Serialization Center. - /// The first object. - /// The second object. - /// True if the two objects are different, False else. - static bool AreDifferent(ISerializationCenter serializationCenter, object object1, object object2) - { - return object1.SerializeWith(serializationCenter) != object2.SerializeWith(serializationCenter); - } - - /// - /// Clone the given object. - /// - /// The type of the object to clone. - /// The Serialization Center. - /// The object to clone. - /// The clone of the object. - static T Clone(ISerializationCenter serializationCenter, T source) where T : notnull - { - return source.SerializeWith(serializationCenter).DeserializeTo(serializationCenter); - } - } -} diff --git a/Core/Upsilon.Apps.Passkey.Core.csproj b/Core/Upsilon.Apps.Passkey.Core.csproj index af86099..8b51e4d 100644 --- a/Core/Upsilon.Apps.Passkey.Core.csproj +++ b/Core/Upsilon.Apps.Passkey.Core.csproj @@ -19,4 +19,8 @@ 9999 + + + + diff --git a/Core/Public/Utils/ClipboardManager.cs b/Core/Utils/ClipboardManager.cs similarity index 94% rename from Core/Public/Utils/ClipboardManager.cs rename to Core/Utils/ClipboardManager.cs index d3bddd8..c12bfdc 100644 --- a/Core/Public/Utils/ClipboardManager.cs +++ b/Core/Utils/ClipboardManager.cs @@ -1,6 +1,6 @@ using Windows.ApplicationModel.DataTransfer; -namespace Upsilon.Apps.Passkey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Utils { public static class ClipboardManager { diff --git a/Core/Public/Utils/CryptographyCenter.cs b/Core/Utils/CryptographyCenter.cs similarity index 98% rename from Core/Public/Utils/CryptographyCenter.cs rename to Core/Utils/CryptographyCenter.cs index 2bf8ff7..ee83b9d 100644 --- a/Core/Public/Utils/CryptographyCenter.cs +++ b/Core/Utils/CryptographyCenter.cs @@ -1,9 +1,10 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Utils; -namespace Upsilon.Apps.Passkey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Utils { public class CryptographyCenter : ICryptographyCenter { diff --git a/Core/Internal/Utils/FileLocker.cs b/Core/Utils/FileLocker.cs similarity index 95% rename from Core/Internal/Utils/FileLocker.cs rename to Core/Utils/FileLocker.cs index c3f071f..c9e99bc 100644 --- a/Core/Internal/Utils/FileLocker.cs +++ b/Core/Utils/FileLocker.cs @@ -1,9 +1,10 @@ using System.IO.Compression; using System.Text; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; -using Upsilon.Apps.Passkey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Utils; -namespace Upsilon.Apps.Passkey.Core.Internal.Utils +namespace Upsilon.Apps.Passkey.Core.Utils { internal class FileLocker : IDisposable { diff --git a/Core/Internal/Utils/ImportExportHelper.cs b/Core/Utils/ImportExportHelper.cs similarity index 97% rename from Core/Internal/Utils/ImportExportHelper.cs rename to Core/Utils/ImportExportHelper.cs index b06fc5f..e63bf98 100644 --- a/Core/Internal/Utils/ImportExportHelper.cs +++ b/Core/Utils/ImportExportHelper.cs @@ -1,11 +1,11 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using Upsilon.Apps.Passkey.Core.Internal.Models; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Models; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; -namespace Upsilon.Apps.Passkey.Core.Internal.Utils +namespace Upsilon.Apps.Passkey.Core.Utils { internal static class ImportExportHelper { diff --git a/Core/Public/Utils/JsonSerializationCenter.cs b/Core/Utils/JsonSerializationCenter.cs similarity index 85% rename from Core/Public/Utils/JsonSerializationCenter.cs rename to Core/Utils/JsonSerializationCenter.cs index 73db29f..50d2619 100644 --- a/Core/Public/Utils/JsonSerializationCenter.cs +++ b/Core/Utils/JsonSerializationCenter.cs @@ -1,8 +1,8 @@ using System.Text.Json; using System.Text.Json.Serialization; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces; -namespace Upsilon.Apps.Passkey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Utils { public class JsonSerializationCenter : ISerializationCenter { diff --git a/Core/Internal/Utils/LogCenter.cs b/Core/Utils/LogCenter.cs similarity index 90% rename from Core/Internal/Utils/LogCenter.cs rename to Core/Utils/LogCenter.cs index b799df7..22c931a 100644 --- a/Core/Internal/Utils/LogCenter.cs +++ b/Core/Utils/LogCenter.cs @@ -1,8 +1,8 @@ using System.Text.Json.Serialization; -using Upsilon.Apps.Passkey.Core.Internal.Models; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Models; +using Upsilon.Apps.Passkey.Interfaces; -namespace Upsilon.Apps.Passkey.Core.Internal.Utils +namespace Upsilon.Apps.Passkey.Core.Utils { internal class LogCenter { diff --git a/Core/Public/Utils/PasswordFactory.cs b/Core/Utils/PasswordFactory.cs similarity index 96% rename from Core/Public/Utils/PasswordFactory.cs rename to Core/Utils/PasswordFactory.cs index eba02d5..8cf6ba6 100644 --- a/Core/Public/Utils/PasswordFactory.cs +++ b/Core/Utils/PasswordFactory.cs @@ -1,8 +1,8 @@ using System.Security.Cryptography; using System.Text; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces; -namespace Upsilon.Apps.Passkey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Core.Utils { public class PasswordFactory : IPasswordFactory { diff --git a/Core/Internal/Utils/StaticMethods.cs b/Core/Utils/StaticMethods.cs similarity index 62% rename from Core/Internal/Utils/StaticMethods.cs rename to Core/Utils/StaticMethods.cs index 9a5b37e..f868257 100644 --- a/Core/Internal/Utils/StaticMethods.cs +++ b/Core/Utils/StaticMethods.cs @@ -1,9 +1,9 @@ using System.Text.RegularExpressions; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces; -namespace Upsilon.Apps.Passkey.Core.Internal.Utils +namespace Upsilon.Apps.Passkey.Core.Utils { - internal static class StaticMethods + public static class StaticMethods { public static string ToSentenceCase(this string str) => Regex.Replace(str, "[a-z][A-Z]", m => $"{m.Value[0]} {char.ToLower(m.Value[1])}"); @@ -24,5 +24,15 @@ public static string SerializeWith(this T obj, ISerializationCenter serializa public static T DeserializeTo(this string serializedString, ISerializationCenter serializationCenter) where T : notnull => serializationCenter.Deserialize(serializedString); + + public static T CloneWith(this T source, ISerializationCenter serializationCenter) where T : notnull + { + return source.SerializeWith(serializationCenter).DeserializeTo(serializationCenter); + } + + public static bool AreDifferent(this ISerializationCenter serializationCenter, object object1, object object2) + { + return object1.SerializeWith(serializationCenter) != object2.SerializeWith(serializationCenter); + } } } diff --git a/Core/Public/Enums/AccountOption.cs b/Interfaces/Enums/AccountOption.cs similarity index 86% rename from Core/Public/Enums/AccountOption.cs rename to Interfaces/Enums/AccountOption.cs index 4f30873..0677387 100644 --- a/Core/Public/Enums/AccountOption.cs +++ b/Interfaces/Enums/AccountOption.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Enums +namespace Upsilon.Apps.Passkey.Interfaces.Enums { /// /// Represent an account option. diff --git a/Core/Public/Enums/AutoSaveMergeBehavior.cs b/Interfaces/Enums/AutoSaveMergeBehavior.cs similarity index 94% rename from Core/Public/Enums/AutoSaveMergeBehavior.cs rename to Interfaces/Enums/AutoSaveMergeBehavior.cs index 9b5823e..b4d32a9 100644 --- a/Core/Public/Enums/AutoSaveMergeBehavior.cs +++ b/Interfaces/Enums/AutoSaveMergeBehavior.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Enums +namespace Upsilon.Apps.Passkey.Interfaces.Enums { /// /// Represent the behavior of auto-save handling. diff --git a/Core/Public/Enums/WarningType.cs b/Interfaces/Enums/WarningType.cs similarity index 93% rename from Core/Public/Enums/WarningType.cs rename to Interfaces/Enums/WarningType.cs index 9722ee7..587de34 100644 --- a/Core/Public/Enums/WarningType.cs +++ b/Interfaces/Enums/WarningType.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Enums +namespace Upsilon.Apps.Passkey.Interfaces.Enums { /// /// Represent a type of warning. diff --git a/Core/Public/Events/AutoSaveDetectedEventArgs.cs b/Interfaces/Events/AutoSaveDetectedEventArgs.cs similarity index 81% rename from Core/Public/Events/AutoSaveDetectedEventArgs.cs rename to Interfaces/Events/AutoSaveDetectedEventArgs.cs index 73239d4..ef06808 100644 --- a/Core/Public/Events/AutoSaveDetectedEventArgs.cs +++ b/Interfaces/Events/AutoSaveDetectedEventArgs.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Interfaces.Enums; -namespace Upsilon.Apps.Passkey.Core.Public.Events +namespace Upsilon.Apps.Passkey.Interfaces.Events { /// /// Represent the behavior of auto-save handling event argument. diff --git a/Core/Public/Events/LogoutEventArgs.cs b/Interfaces/Events/LogoutEventArgs.cs similarity index 86% rename from Core/Public/Events/LogoutEventArgs.cs rename to Interfaces/Events/LogoutEventArgs.cs index 6a0f879..9d7c068 100644 --- a/Core/Public/Events/LogoutEventArgs.cs +++ b/Interfaces/Events/LogoutEventArgs.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Events +namespace Upsilon.Apps.Passkey.Interfaces.Events { /// /// Represent a loggout event argument. diff --git a/Core/Public/Events/WarningDetectedEventArgs.cs b/Interfaces/Events/WarningDetectedEventArgs.cs similarity index 78% rename from Core/Public/Events/WarningDetectedEventArgs.cs rename to Interfaces/Events/WarningDetectedEventArgs.cs index c9bce8f..a0f7013 100644 --- a/Core/Public/Events/WarningDetectedEventArgs.cs +++ b/Interfaces/Events/WarningDetectedEventArgs.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces; -namespace Upsilon.Apps.Passkey.Core.Public.Events +namespace Upsilon.Apps.Passkey.Interfaces.Events { /// /// Represent a warning detected event argument. diff --git a/Core/Public/Interfaces/IAccount.cs b/Interfaces/Interfaces/IAccount.cs similarity index 91% rename from Core/Public/Interfaces/IAccount.cs rename to Interfaces/Interfaces/IAccount.cs index b602c18..9bdafd7 100644 --- a/Core/Public/Interfaces/IAccount.cs +++ b/Interfaces/Interfaces/IAccount.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Interfaces.Enums; -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent an account. diff --git a/Core/Public/Interfaces/ICryptographyCenter.cs b/Interfaces/Interfaces/ICryptographyCenter.cs similarity index 98% rename from Core/Public/Interfaces/ICryptographyCenter.cs rename to Interfaces/Interfaces/ICryptographyCenter.cs index d5d8595..7291e54 100644 --- a/Core/Public/Interfaces/ICryptographyCenter.cs +++ b/Interfaces/Interfaces/ICryptographyCenter.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent a cryptographic center. diff --git a/Core/Public/Interfaces/IDatabase.cs b/Interfaces/Interfaces/IDatabase.cs similarity index 58% rename from Core/Public/Interfaces/IDatabase.cs rename to Interfaces/Interfaces/IDatabase.cs index 19d7832..236f85e 100644 --- a/Core/Public/Interfaces/IDatabase.cs +++ b/Interfaces/Interfaces/IDatabase.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.Passkey.Core.Public.Events; +using Upsilon.Apps.Passkey.Interfaces.Events; -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent a database. @@ -135,62 +135,5 @@ public interface IDatabase : IDisposable /// The file path. /// True if the export succeded, False else. bool ExportToFile(string filePath); - - /// - /// Create a new user database and returns the database. - /// After creating, the User should be loaded with the Login method. - /// - /// An implementation of the cryptographic center. - /// An implementation of the serialization center. - /// An implementation of the password factory. - /// The path to the database file. - /// The path to the autosave file. - /// The path to the log file. - /// The username. - /// The passkeys. - /// The database created. - static IDatabase Create(ICryptographyCenter cryptographicCenter, - ISerializationCenter serializationCenter, - IPasswordFactory passwordFactory, - string databaseFile, - string autoSaveFile, - string logFile, - string username, - string[] passkeys) - => Internal.Models.Database.Create(cryptographicCenter, - serializationCenter, - passwordFactory, - databaseFile, - autoSaveFile, - logFile, - username, - passkeys); - - /// - /// Open an user and returns the database. - /// After opening, the User should be loaded with the Login method. - /// - /// An implementation of the cryptographic center. - /// An implementation of the serialization center. - /// An implementation of the password factory. - /// The path to the database file. - /// The path to the autosave file. - /// The path to the log file. - /// The username. - /// The database opened. - static IDatabase Open(ICryptographyCenter cryptographicCenter, - ISerializationCenter serializationCenter, - IPasswordFactory passwordFactory, - string databaseFile, - string autoSaveFile, - string logFile, - string username) - => Internal.Models.Database.Open(cryptographicCenter, - serializationCenter, - passwordFactory, - databaseFile, - autoSaveFile, - logFile, - username); } } \ No newline at end of file diff --git a/Core/Public/Interfaces/IItem.cs b/Interfaces/Interfaces/IItem.cs similarity index 85% rename from Core/Public/Interfaces/IItem.cs rename to Interfaces/Interfaces/IItem.cs index 920cc17..caaa06e 100644 --- a/Core/Public/Interfaces/IItem.cs +++ b/Interfaces/Interfaces/IItem.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent an item. diff --git a/Core/Public/Interfaces/ILog.cs b/Interfaces/Interfaces/ILog.cs similarity index 88% rename from Core/Public/Interfaces/ILog.cs rename to Interfaces/Interfaces/ILog.cs index ca0f5dd..9e3dd57 100644 --- a/Core/Public/Interfaces/ILog.cs +++ b/Interfaces/Interfaces/ILog.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent an event log. diff --git a/Core/Public/Interfaces/IPasswordFactory.cs b/Interfaces/Interfaces/IPasswordFactory.cs similarity index 97% rename from Core/Public/Interfaces/IPasswordFactory.cs rename to Interfaces/Interfaces/IPasswordFactory.cs index ad8e4d9..398ec20 100644 --- a/Core/Public/Interfaces/IPasswordFactory.cs +++ b/Interfaces/Interfaces/IPasswordFactory.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent a Password factory engine. diff --git a/Interfaces/Interfaces/ISerializationCenter.cs b/Interfaces/Interfaces/ISerializationCenter.cs new file mode 100644 index 0000000..6ead7d2 --- /dev/null +++ b/Interfaces/Interfaces/ISerializationCenter.cs @@ -0,0 +1,24 @@ +namespace Upsilon.Apps.Passkey.Interfaces +{ + /// + /// Represent a serialization center. + /// + public interface ISerializationCenter + { + /// + /// Serialize the given object to a string. + /// + /// The type of the object. + /// The object to serialize. + /// The serialised string. + string Serialize(T toSerialize) where T : notnull; + + /// + /// Deserialize the given string to the given type object. + /// + /// The type of the object. + /// The serialised string. + /// The deserialized object. + T Deserialize(string toDeserialize) where T : notnull; + } +} diff --git a/Core/Public/Interfaces/IService.cs b/Interfaces/Interfaces/IService.cs similarity index 97% rename from Core/Public/Interfaces/IService.cs rename to Interfaces/Interfaces/IService.cs index da2ed18..e95535d 100644 --- a/Core/Public/Interfaces/IService.cs +++ b/Interfaces/Interfaces/IService.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent a service. diff --git a/Core/Public/Interfaces/IUser.cs b/Interfaces/Interfaces/IUser.cs similarity index 94% rename from Core/Public/Interfaces/IUser.cs rename to Interfaces/Interfaces/IUser.cs index 3674763..5b1b3b5 100644 --- a/Core/Public/Interfaces/IUser.cs +++ b/Interfaces/Interfaces/IUser.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Interfaces.Enums; -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent an user. diff --git a/Core/Public/Interfaces/IWarning.cs b/Interfaces/Interfaces/IWarning.cs similarity index 82% rename from Core/Public/Interfaces/IWarning.cs rename to Interfaces/Interfaces/IWarning.cs index 478f829..aaa584d 100644 --- a/Core/Public/Interfaces/IWarning.cs +++ b/Interfaces/Interfaces/IWarning.cs @@ -1,6 +1,6 @@ -using Upsilon.Apps.Passkey.Core.Public.Enums; +using Upsilon.Apps.Passkey.Interfaces.Enums; -namespace Upsilon.Apps.Passkey.Core.Public.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent a warning. diff --git a/Core/Public/Interfaces/Interfaces.cd b/Interfaces/Interfaces/Interfaces.cd similarity index 55% rename from Core/Public/Interfaces/Interfaces.cd rename to Interfaces/Interfaces/Interfaces.cd index a1d979e..0a0674e 100644 --- a/Core/Public/Interfaces/Interfaces.cd +++ b/Interfaces/Interfaces/Interfaces.cd @@ -1,68 +1,60 @@  - + AAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA= - Public\Events\AutoSaveDetectedEventArgs.cs + Events\AutoSaveDetectedEventArgs.cs - + AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Public\Events\WarningDetectedEventArgs.cs + Events\WarningDetectedEventArgs.cs - + AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Public\Events\LogoutEventArgs.cs + Events\LogoutEventArgs.cs - + - AAAgAAAAEAAAAAAAAAAAAAAAAAAAQAACAAAAAABAAAA= - Public\Interfaces\IAccount.cs + AAAgAAAAEAAAAAAAAAAAAAAAAAAAQAACAAAAAAJAAAA= + Interfaces\IAccount.cs - + AAACAAAAAAAAABAAAMAAAAAAAAAAAAASAAAAAABABAE= - Public\Interfaces\ICryptographyCenter.cs + Interfaces\ICryptographyCenter.cs - - - + + + - - - - - - - - - + - - + + - + - + @@ -72,8 +64,8 @@ - AgABAIgQAAAAoAAAQAAACAAggAGEAUAAAAAQAACAAAA= - Public\Interfaces\IDatabase.cs + AgABAIgQAAAAgAAAYAAACCAggAGEAQAAAAARAACABAA= + Interfaces\IDatabase.cs @@ -86,32 +78,32 @@ - + - AAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA= - Public\Interfaces\IItem.cs + AAAAAAAAAEAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA= + Interfaces\IItem.cs - + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAIAIAAAAAA= - Public\Interfaces\ILog.cs + Interfaces\ILog.cs - + AAAAAAAAAAAAAAAAAAAAAAAAACBAAAAAAAAAAAAAAAA= - Public\Interfaces\ISerializationCenter.cs + Interfaces\ISerializationCenter.cs - + AAAhAAAAAAAAAAAAAAAAAAAAAAAAAACACAACAQAABAA= - Public\Interfaces\IService.cs + Interfaces\IService.cs @@ -120,27 +112,19 @@ - + - - - - - - - - - gABAAAEAAAAAAAAAAQAAAAAAAAAACAAAAAgAECAAAAA= - Public\Interfaces\IUser.cs + gABAAAEAAAAAAAAAAQAAAAAAAAAACAAIAAkAECAAAAA= + Interfaces\IUser.cs - + - + @@ -150,7 +134,7 @@ AAAAAAAQgAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAA= - Public\Interfaces\IWarning.cs + Interfaces\IWarning.cs @@ -160,25 +144,25 @@ - + AAAAAQAAAAAAAAAAAAAAAAACAAAAAAQAgABAAAAAAAA= - Public\Interfaces\IPasswordFactory.cs + Interfaces\IPasswordFactory.cs - + AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA= - Public\Enums\AccountOption.cs + Enums\AccountOption.cs - + AAAAQAAAAABgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Public\Enums\WarningType.cs + Enums\WarningType.cs diff --git a/Interfaces/Upsilon.Apps.Passkey.Interfaces.csproj b/Interfaces/Upsilon.Apps.Passkey.Interfaces.csproj new file mode 100644 index 0000000..9355386 --- /dev/null +++ b/Interfaces/Upsilon.Apps.Passkey.Interfaces.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + $(AssemblyName) + Yassin Lokhat + A local stored Password Manager Interfaces. + + + + True + 9999 + + + + True + 9999 + + + diff --git a/Core/Public/Utils/Exceptions.cs b/Interfaces/Utils/Exceptions.cs similarity index 95% rename from Core/Public/Utils/Exceptions.cs rename to Interfaces/Utils/Exceptions.cs index 9086f78..47df615 100644 --- a/Core/Public/Utils/Exceptions.cs +++ b/Interfaces/Utils/Exceptions.cs @@ -1,4 +1,4 @@ -namespace Upsilon.Apps.Passkey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Interfaces.Utils { public sealed class CheckSignFailedException : Exception { diff --git a/Core/Public/Utils/StaticMethods.cs b/Interfaces/Utils/StaticMethods.cs similarity index 75% rename from Core/Public/Utils/StaticMethods.cs rename to Interfaces/Utils/StaticMethods.cs index 37a7a93..91aa374 100644 --- a/Core/Public/Utils/StaticMethods.cs +++ b/Interfaces/Utils/StaticMethods.cs @@ -1,6 +1,4 @@ -using Upsilon.Apps.Passkey.Core.Public.Interfaces; - -namespace Upsilon.Apps.Passkey.Core.Public.Utils +namespace Upsilon.Apps.Passkey.Interfaces.Utils { public static class StaticMethods { diff --git a/UnitTests/Models/AccountUnitTests.cs b/UnitTests/Models/AccountUnitTests.cs index 56d42a8..b725bec 100644 --- a/UnitTests/Models/AccountUnitTests.cs +++ b/UnitTests/Models/AccountUnitTests.cs @@ -1,7 +1,7 @@ using FluentAssertions; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; -using Upsilon.Apps.Passkey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Utils; namespace Upsilon.Apps.Passkey.UnitTests.Models { diff --git a/UnitTests/Models/DatabaseUnitTests.cs b/UnitTests/Models/DatabaseUnitTests.cs index 2994843..15eda7d 100644 --- a/UnitTests/Models/DatabaseUnitTests.cs +++ b/UnitTests/Models/DatabaseUnitTests.cs @@ -1,5 +1,7 @@ using FluentAssertions; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Core.Models; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; namespace Upsilon.Apps.Passkey.UnitTests.Models { @@ -13,7 +15,7 @@ public void Case00_GenerateNewDatabase() IUser user = database.User; user.LogoutTimeout = 10; user.CleaningClipboardTimeout = 15; - user.WarningsToNotify = (Core.Public.Enums.WarningType)0; + user.WarningsToNotify = (WarningType)0; for (int i = 0; i < 50; i++) { @@ -52,7 +54,7 @@ public void Case00_GenerateNewDatabase() random = UnitTestsHelper.GetRandomInt(100); account.Notes = random % 10 == 0 ? $"Service{i}'s Account{j} notes : \n{UnitTestsHelper.GetRandomString(min: 10, max: 150)}" : ""; account.PasswordUpdateReminderDelay = random < 10 ? random : 0; - account.Options = random % 2 == 0 ? Core.Public.Enums.AccountOption.WarnIfPasswordLeaked : Core.Public.Enums.AccountOption.None; + account.Options = random % 2 == 0 ? AccountOption.WarnIfPasswordLeaked : AccountOption.None; } } @@ -312,7 +314,7 @@ public void Case05_DatabaseAutoLogout() Stack expectedLogWarnings = new(); UnitTestsHelper.ClearTestEnvironment(); - IDatabase database = IDatabase.Create(UnitTestsHelper.CryptographicCenter, + IDatabase database = Database.Create(UnitTestsHelper.CryptographicCenter, UnitTestsHelper.SerializationCenter, UnitTestsHelper.PasswordFactory, databaseFile, diff --git a/UnitTests/Models/ImportExportUnitTests.cs b/UnitTests/Models/ImportExportUnitTests.cs index fb1dccb..b5867a2 100644 --- a/UnitTests/Models/ImportExportUnitTests.cs +++ b/UnitTests/Models/ImportExportUnitTests.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; using System.Text; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; using Upsilon.Apps.Passkey.UnitTests; using static Microsoft.ApplicationInsights.MetricDimensionNames.TelemetryContext; diff --git a/UnitTests/Models/ServiceUnitTests.cs b/UnitTests/Models/ServiceUnitTests.cs index 75bbe4d..7bc631d 100644 --- a/UnitTests/Models/ServiceUnitTests.cs +++ b/UnitTests/Models/ServiceUnitTests.cs @@ -1,7 +1,7 @@ using FluentAssertions; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; -using Upsilon.Apps.Passkey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Utils; namespace Upsilon.Apps.Passkey.UnitTests.Models { diff --git a/UnitTests/Models/UserUnitTests.cs b/UnitTests/Models/UserUnitTests.cs index e9852ae..b5aa55c 100644 --- a/UnitTests/Models/UserUnitTests.cs +++ b/UnitTests/Models/UserUnitTests.cs @@ -1,7 +1,8 @@ using FluentAssertions; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; -using Upsilon.Apps.Passkey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Models; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; +using Upsilon.Apps.Passkey.Interfaces.Utils; namespace Upsilon.Apps.Passkey.UnitTests.Models { @@ -107,7 +108,7 @@ public void Case02_UserUpdateThenSaved() _ = File.Exists(autoSaveFile).Should().BeFalse(); // When - IDatabase databaseLoaded = IDatabase.Open(UnitTestsHelper.CryptographicCenter, + IDatabase databaseLoaded = Database.Open(UnitTestsHelper.CryptographicCenter, UnitTestsHelper.SerializationCenter, UnitTestsHelper.PasswordFactory, databaseFile, @@ -213,7 +214,7 @@ public void Case03_UserUpdateButNotSaved_CaseMergeAndSave() expectedLogs.Push($"Information : User {newUsername} logged out"); expectedLogs.Push($"Information : User {newUsername}'s database closed"); - databaseLoaded = IDatabase.Open(UnitTestsHelper.CryptographicCenter, + databaseLoaded = Database.Open(UnitTestsHelper.CryptographicCenter, UnitTestsHelper.SerializationCenter, UnitTestsHelper.PasswordFactory, databaseFile, @@ -324,7 +325,7 @@ public void Case04_UserUpdateButNotSaved_CaseMergeWithoutSaving() expectedLogs.Push($"Information : User {newUsername} logged out"); expectedLogs.Push($"Information : User {newUsername}'s database closed"); - databaseLoaded = IDatabase.Open(UnitTestsHelper.CryptographicCenter, + databaseLoaded = Database.Open(UnitTestsHelper.CryptographicCenter, UnitTestsHelper.SerializationCenter, UnitTestsHelper.PasswordFactory, databaseFile, diff --git a/UnitTests/UnitTestsHelper.cs b/UnitTests/UnitTestsHelper.cs index 4840019..afea47a 100644 --- a/UnitTests/UnitTestsHelper.cs +++ b/UnitTests/UnitTestsHelper.cs @@ -1,9 +1,10 @@ using FluentAssertions; using System.Runtime.CompilerServices; using System.Security.Cryptography; -using Upsilon.Apps.Passkey.Core.Public.Enums; -using Upsilon.Apps.Passkey.Core.Public.Interfaces; -using Upsilon.Apps.Passkey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Core.Models; +using Upsilon.Apps.Passkey.Core.Utils; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; namespace Upsilon.Apps.Passkey.UnitTests { @@ -48,7 +49,7 @@ public static IDatabase CreateTestDatabase(string[] passkeys = null, [CallerMemb passkeys ??= GetRandomStringArray(); - IDatabase database = IDatabase.Create(CryptographicCenter, + IDatabase database = Database.Create(CryptographicCenter, SerializationCenter, PasswordFactory, databaseFile, @@ -68,7 +69,7 @@ public static IDatabase OpenTestDatabase(string[] passkeys, out IWarning[] detec IWarning[] warnings = []; - IDatabase database = IDatabase.Open(CryptographicCenter, + IDatabase database = Database.Open(CryptographicCenter, SerializationCenter, PasswordFactory, databaseFile, diff --git a/UnitTests/Utils/CryptographyCenterUnitTexts.cs b/UnitTests/Utils/CryptographyCenterUnitTexts.cs index 2458713..45597c2 100644 --- a/UnitTests/Utils/CryptographyCenterUnitTexts.cs +++ b/UnitTests/Utils/CryptographyCenterUnitTexts.cs @@ -1,6 +1,6 @@ using FluentAssertions; using System.Diagnostics; -using Upsilon.Apps.Passkey.Core.Public.Utils; +using Upsilon.Apps.Passkey.Interfaces.Utils; namespace Upsilon.Apps.Passkey.UnitTests.Utils { diff --git a/Upsilon.Apps.Passkey.sln b/Upsilon.Apps.Passkey.sln index 5d4ab3b..d9655d7 100644 --- a/Upsilon.Apps.Passkey.sln +++ b/Upsilon.Apps.Passkey.sln @@ -1,12 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36429.23 d17.14 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11222.15 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Upsilon.Apps.Passkey.Core", "Core\Upsilon.Apps.Passkey.Core.csproj", "{E8918B8B-3457-14F3-B592-18B9C1C7AB82}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Upsilon.Apps.Passkey.Interfaces", "Interfaces\Upsilon.Apps.Passkey.Interfaces.csproj", "{D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}.Debug|Any CPU.Build.0 = Debug|Any CPU {BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}.Release|Any CPU.ActiveCfg = Release|Any CPU {BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}.Release|Any CPU.Build.0 = Release|Any CPU + {D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 0bf415ace11d1a747c57add1b204cd32a088d49c Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Fri, 5 Dec 2025 15:38:17 +0300 Subject: [PATCH 06/14] Migrating solution from sln to slnx --- .github/workflows/csharp-dotnet.yml | 8 +++---- Upsilon.Apps.Passkey.sln | 37 ----------------------------- Upsilon.Apps.Passkey.slnx | 5 ++++ 3 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 Upsilon.Apps.Passkey.sln create mode 100644 Upsilon.Apps.Passkey.slnx diff --git a/.github/workflows/csharp-dotnet.yml b/.github/workflows/csharp-dotnet.yml index d0f671b..e268702 100644 --- a/.github/workflows/csharp-dotnet.yml +++ b/.github/workflows/csharp-dotnet.yml @@ -18,16 +18,16 @@ jobs: dotnet-version: 10.0.x - name: Restore dependencies - run: dotnet restore Upsilon.Apps.Passkey.sln + run: dotnet restore Upsilon.Apps.Passkey.slnx - name: Build Debug - run: dotnet build Upsilon.Apps.Passkey.sln --no-restore --configuration Debug + run: dotnet build Upsilon.Apps.Passkey.slnx --no-restore --configuration Debug - name: Build Relesae - run: dotnet build Upsilon.Apps.Passkey.sln --no-restore --configuration Release + run: dotnet build Upsilon.Apps.Passkey.slnx --no-restore --configuration Release - name: Test - run: dotnet test --no-build --verbosity normal Upsilon.Apps.Passkey.sln + run: dotnet test --no-build --verbosity normal Upsilon.Apps.Passkey.slnx permissions: diff --git a/Upsilon.Apps.Passkey.sln b/Upsilon.Apps.Passkey.sln deleted file mode 100644 index d9655d7..0000000 --- a/Upsilon.Apps.Passkey.sln +++ /dev/null @@ -1,37 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.0.11222.15 d18.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Upsilon.Apps.Passkey.Core", "Core\Upsilon.Apps.Passkey.Core.csproj", "{E8918B8B-3457-14F3-B592-18B9C1C7AB82}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Upsilon.Apps.Passkey.Interfaces", "Interfaces\Upsilon.Apps.Passkey.Interfaces.csproj", "{D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {E8918B8B-3457-14F3-B592-18B9C1C7AB82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8918B8B-3457-14F3-B592-18B9C1C7AB82}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8918B8B-3457-14F3-B592-18B9C1C7AB82}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8918B8B-3457-14F3-B592-18B9C1C7AB82}.Release|Any CPU.Build.0 = Release|Any CPU - {BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BAE0DFAB-D0D6-4020-B9B1-73CCF5CD3678}.Release|Any CPU.Build.0 = Release|Any CPU - {D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D21D9B5C-C624-CE2D-FA6A-59C80C24EC37}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E52E3DBD-718B-4004-B585-42D919FFD3C8} - EndGlobalSection -EndGlobal diff --git a/Upsilon.Apps.Passkey.slnx b/Upsilon.Apps.Passkey.slnx new file mode 100644 index 0000000..5ac3ca7 --- /dev/null +++ b/Upsilon.Apps.Passkey.slnx @@ -0,0 +1,5 @@ + + + + + From 4ea6eff5d74f9ebc62b519e15f1fec49dcc1cb53 Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Fri, 5 Dec 2025 15:44:49 +0300 Subject: [PATCH 07/14] Updating Readme - Part 1 --- README.md | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 161 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1a332e6..f8aeb8a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ - - -**Upsilon.Apps.Passkey.Core** +**Upsilon.Apps.Passkey** ============================================= **Overview** ------------ -This is a C# implementation of a local stored password manager core API. The API provides a secure way to store and manage passwords locally on a user's device. +This is a C# implementation of a local stored password manager. The application provides a secure way to store and manage passwords locally on the user's device. **Features** ------------ @@ -28,7 +26,164 @@ This is a C# implementation of a local stored password manager core API. The API ---------- ### Class diagram -![Interfaces](https://github.com/user-attachments/assets/a4ad591c-1334-426f-86de-b7d264ea904b) +```mermaid +classDiagram + direction TB + + %% Main Interfaces + class IDatabase { + <> + +DatabaseFile : string + +AutoSaveFile : string + +LogFile : string + +User : IUser + +SessionLeftTime : int + +Logs : IEnumerable~ILog~ + +Warnings : IEnumerable~IWarning~ + +SerializationCenter : ISerializationCenter + +CryptographyCenter : ICryptographyCenter + +PasswordFactory : IPasswordFactory + +WarningDetected : EventHandler~WarningDetectedEventArgs~ + +AutoSaveDetected : EventHandler~AutoSaveDetectedEventArgs~ + +DatabaseSaved : EventHandler + +DatabaseClosed : EventHandler~LogoutEventArgs~ + +Login(in passkey string) IUser + +Save() void + +Delete() void + +Close() void + +HasChanged() bool + +HasChanged(in itemId string) bool + +HasChanged(in itemId string, in fieldName string) bool + +ImportFromFile(in filePath string) bool + +ExportToFile(in filePath string) bool + +Create(in cryptographicCenter ICryptographyCenter, in serializationCenter ISerializationCenter, in passwordFactory IPasswordFactory, in databaseFile string, in autoSaveFile string, in logFile string, in username string, in passkeys IEnumerable~string~) IDatabase + } + + class IItem { + <> + +ItemId : string + +Database : IDatabase + } + + class IUser { + <> + +Name + +Email + +MasterPassword + +CreatedDate + +LastModifiedDate + +IsLocked + +Services : IEnumerable~IService~ + } + + class IService { + <> + +Name + +Url + +IconPath + +CreatedDate + +LastModifiedDate + +User : IUser + +Accounts : IEnumerable~IAccount~ + } + + class IAccount { + <> + +Username + +Password + +Email + +Notes + +Service : IService + +Options : AccountOption + } + + class ILog { + <> + +Timestamp + +Message + +LogType + } + + class IWarning { + <> + +WarningType : WarningType + +Accounts : IEnumerable~IAccount~ + +Logs : IEnumerable~ILog~ + +Severity + } + + class ICryptographyCenter { + <> + +Encrypt() + +Decrypt() + +Hash() + +GenerateSalt() + +VerifyHash() + +GenerateKey() + } + + class ISerializationCenter { + <> + +Serialize() + +Deserialize() + } + + class IPasswordFactory { + <> + +GeneratePassword() + +ValidatePassword() + +GetPasswordStrength() + +GeneratePassphrase() + } + + %% Enums + class AccountOption { + <> + AutoSave + TwoFactorAuth + } + + class WarningType { + <> + WeakPassword + ReusedPassword + OldPassword + } + + %% Event Args Classes + class AutoSaveDetectedEventArgs { + +Account + } + + class LogoutEventArgs { + +Reason + } + + class WarningDetectedEventArgs { + +Warning + } + + %% Inheritance Relations + IUser --|> IItem + + %% Link Relations + IUser "1" --> "*" IService : Services + IService "1" --> "*" IAccount : Accounts + IService --> IUser : User + IAccount --> IService : Service + IAccount --> AccountOption : Options + + IDatabase --> IUser : User + IDatabase --> "*" ILog : Logs + IDatabase --> "*" IWarning : Warnings + IDatabase --> ISerializationCenter : SerializationCenter + IDatabase --> ICryptographyCenter : CryptographyCenter + IDatabase --> IPasswordFactory : PasswordFactory + + IWarning --> WarningType : WarningType + IWarning --> "*" ILog : Logs + IWarning --> "*" IAccount : Accounts +``` **Example Use Cases** @@ -119,7 +274,7 @@ database.Close(); **Getting Started** ------------------- -1. Clone the repository: `git clone https://github.com/YassinLokhat/Upsilon.Apps.Passkey.Core.git` +1. Clone the repository: `git clone https://github.com/YassinLokhat/Upsilon.Apps.Passkey.git` 2. Build the solution: `dotnet build` 3. Run the API: `dotnet run` From 7851f429e001df705500a4aa0af2f57f41353d65 Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Sat, 6 Dec 2025 06:07:02 +0300 Subject: [PATCH 08/14] Isolating ClipboardManages as OS specific implementation --- Core/Models/Database.cs | 7 ++++++ Core/Models/User.cs | 2 +- Core/Upsilon.Apps.Passkey.Core.csproj | 2 +- Interfaces/Interfaces/IClipboardManager.cs | 19 +++++++++++++++ Interfaces/Interfaces/IDatabase.cs | 5 ++++ Interfaces/Interfaces/Interfaces.cd | 24 ++++++++++++------- {Core/Utils => UnitTests}/ClipboardManager.cs | 7 +++--- UnitTests/Models/DatabaseUnitTests.cs | 1 + UnitTests/Models/UserUnitTests.cs | 3 +++ UnitTests/UnitTestsHelper.cs | 3 +++ 10 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 Interfaces/Interfaces/IClipboardManager.cs rename {Core/Utils => UnitTests}/ClipboardManager.cs (80%) diff --git a/Core/Models/Database.cs b/Core/Models/Database.cs index 3331768..f211829 100644 --- a/Core/Models/Database.cs +++ b/Core/Models/Database.cs @@ -24,6 +24,7 @@ public sealed class Database : IDatabase public ICryptographyCenter CryptographyCenter { get; private set; } public ISerializationCenter SerializationCenter { get; private set; } public IPasswordFactory PasswordFactory { get; private set; } + public IClipboardManager ClipboardManager { get; private set; } public event EventHandler? WarningDetected; public event EventHandler? AutoSaveDetected; @@ -192,6 +193,7 @@ public bool ExportToFile(string filePath) private Database(ICryptographyCenter cryptographicCenter, ISerializationCenter serializationCenter, IPasswordFactory passwordFactory, + IClipboardManager clipboardManager, string databaseFile, string autoSaveFile, string logFile, @@ -207,6 +209,7 @@ private Database(ICryptographyCenter cryptographicCenter, CryptographyCenter = cryptographicCenter; SerializationCenter = serializationCenter; PasswordFactory = passwordFactory; + ClipboardManager = clipboardManager; Username = username; Passkeys = [CryptographyCenter.GetHash(username)]; @@ -239,6 +242,7 @@ private Database(ICryptographyCenter cryptographicCenter, public static IDatabase Create(ICryptographyCenter cryptographicCenter, ISerializationCenter serializationCenter, IPasswordFactory passwordFactory, + IClipboardManager clipboardManager, string databaseFile, string autoSaveFile, string logFile, @@ -262,6 +266,7 @@ public static IDatabase Create(ICryptographyCenter cryptographicCenter, Database database = new(cryptographicCenter, serializationCenter, passwordFactory, + clipboardManager, databaseFile, autoSaveFile, logFile, @@ -289,6 +294,7 @@ public static IDatabase Create(ICryptographyCenter cryptographicCenter, public static IDatabase Open(ICryptographyCenter cryptographicCenter, ISerializationCenter serializationCenter, IPasswordFactory passwordFactory, + IClipboardManager clipboardManager, string databaseFile, string autoSaveFile, string logFile, @@ -297,6 +303,7 @@ public static IDatabase Open(ICryptographyCenter cryptographicCenter, Database database = new(cryptographicCenter, serializationCenter, passwordFactory, + clipboardManager, databaseFile, autoSaveFile, logFile, diff --git a/Core/Models/User.cs b/Core/Models/User.cs index 4c67047..023aa6d 100644 --- a/Core/Models/User.cs +++ b/Core/Models/User.cs @@ -232,7 +232,7 @@ private void _cleanClipboard() { string[] passwords = [.. Services.SelectMany(x => x.Accounts).SelectMany(x => x.Passwords.Values)]; - int cleanedPasswordsCount = ClipboardManager.RemoveAllOccurence(passwords); + int cleanedPasswordsCount = Database.ClipboardManager.RemoveAllOccurence(passwords); if (cleanedPasswordsCount != 0) { diff --git a/Core/Upsilon.Apps.Passkey.Core.csproj b/Core/Upsilon.Apps.Passkey.Core.csproj index 8b51e4d..e3e95b6 100644 --- a/Core/Upsilon.Apps.Passkey.Core.csproj +++ b/Core/Upsilon.Apps.Passkey.Core.csproj @@ -1,7 +1,7 @@  - net10.0-windows10.0.18362.0 + net10.0 enable enable $(AssemblyName) diff --git a/Interfaces/Interfaces/IClipboardManager.cs b/Interfaces/Interfaces/IClipboardManager.cs new file mode 100644 index 0000000..5077e86 --- /dev/null +++ b/Interfaces/Interfaces/IClipboardManager.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Upsilon.Apps.Passkey.Interfaces +{ + /// + /// Represent a OS specific Clipboard manager. + /// + public interface IClipboardManager + { + /// + /// Remove any occurence of elements in a the given list from the clipboard history. + /// + /// The list of eleemnts to remove. + /// The number of item removed. + int RemoveAllOccurence(string[] removeList); + } +} diff --git a/Interfaces/Interfaces/IDatabase.cs b/Interfaces/Interfaces/IDatabase.cs index 236f85e..e7698d8 100644 --- a/Interfaces/Interfaces/IDatabase.cs +++ b/Interfaces/Interfaces/IDatabase.cs @@ -57,6 +57,11 @@ public interface IDatabase : IDisposable /// IPasswordFactory PasswordFactory { get; } + /// + /// The OS specific Clipboard manager implementation. + /// + IClipboardManager ClipboardManager { get; } + /// /// Occurs when a warning is detected. /// diff --git a/Interfaces/Interfaces/Interfaces.cd b/Interfaces/Interfaces/Interfaces.cd index 0a0674e..472b83a 100644 --- a/Interfaces/Interfaces/Interfaces.cd +++ b/Interfaces/Interfaces/Interfaces.cd @@ -1,21 +1,21 @@  - + AAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA= Events\AutoSaveDetectedEventArgs.cs - + AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Events\WarningDetectedEventArgs.cs - + AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Events\LogoutEventArgs.cs @@ -33,7 +33,7 @@ - + AAACAAAAAAAAABAAAMAAAAAAAAAAAAASAAAAAABABAE= Interfaces\ICryptographyCenter.cs @@ -56,15 +56,15 @@ - - + + - AgABAIgQAAAAgAAAYAAACCAggAGEAQAAAAARAACABAA= + AgABAIgQAAAAgAAAYAAACCAggAGEAQAAEAARAACABAA= Interfaces\IDatabase.cs @@ -72,6 +72,7 @@ + @@ -145,12 +146,19 @@ - + AAAAAQAAAAAAAAAAAAAAAAACAAAAAAQAgABAAAAAAAA= Interfaces\IPasswordFactory.cs + + + + AAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Interfaces\IClipboardManager.cs + + diff --git a/Core/Utils/ClipboardManager.cs b/UnitTests/ClipboardManager.cs similarity index 80% rename from Core/Utils/ClipboardManager.cs rename to UnitTests/ClipboardManager.cs index c12bfdc..46d1d92 100644 --- a/Core/Utils/ClipboardManager.cs +++ b/UnitTests/ClipboardManager.cs @@ -1,10 +1,11 @@ -using Windows.ApplicationModel.DataTransfer; +using Upsilon.Apps.Passkey.Interfaces; +using Windows.ApplicationModel.DataTransfer; namespace Upsilon.Apps.Passkey.Core.Utils { - public static class ClipboardManager + public class ClipboardManager : IClipboardManager { - public static int RemoveAllOccurence(string[] removeList) + public int RemoveAllOccurence(string[] removeList) { int cleanedPasswordCount = 0; diff --git a/UnitTests/Models/DatabaseUnitTests.cs b/UnitTests/Models/DatabaseUnitTests.cs index 15eda7d..99098c7 100644 --- a/UnitTests/Models/DatabaseUnitTests.cs +++ b/UnitTests/Models/DatabaseUnitTests.cs @@ -317,6 +317,7 @@ public void Case05_DatabaseAutoLogout() IDatabase database = Database.Create(UnitTestsHelper.CryptographicCenter, UnitTestsHelper.SerializationCenter, UnitTestsHelper.PasswordFactory, + UnitTestsHelper.ClipboardManager, databaseFile, autoSaveFile, logFile, diff --git a/UnitTests/Models/UserUnitTests.cs b/UnitTests/Models/UserUnitTests.cs index b5aa55c..19bad4e 100644 --- a/UnitTests/Models/UserUnitTests.cs +++ b/UnitTests/Models/UserUnitTests.cs @@ -111,6 +111,7 @@ public void Case02_UserUpdateThenSaved() IDatabase databaseLoaded = Database.Open(UnitTestsHelper.CryptographicCenter, UnitTestsHelper.SerializationCenter, UnitTestsHelper.PasswordFactory, + UnitTestsHelper.ClipboardManager, databaseFile, autoSaveFile, logFile, @@ -217,6 +218,7 @@ public void Case03_UserUpdateButNotSaved_CaseMergeAndSave() databaseLoaded = Database.Open(UnitTestsHelper.CryptographicCenter, UnitTestsHelper.SerializationCenter, UnitTestsHelper.PasswordFactory, + UnitTestsHelper.ClipboardManager, databaseFile, autoSaveFile, logFile, @@ -328,6 +330,7 @@ public void Case04_UserUpdateButNotSaved_CaseMergeWithoutSaving() databaseLoaded = Database.Open(UnitTestsHelper.CryptographicCenter, UnitTestsHelper.SerializationCenter, UnitTestsHelper.PasswordFactory, + UnitTestsHelper.ClipboardManager, databaseFile, autoSaveFile, logFile, diff --git a/UnitTests/UnitTestsHelper.cs b/UnitTests/UnitTestsHelper.cs index afea47a..b9334ee 100644 --- a/UnitTests/UnitTestsHelper.cs +++ b/UnitTests/UnitTestsHelper.cs @@ -15,6 +15,7 @@ internal static class UnitTestsHelper public static readonly ICryptographyCenter CryptographicCenter = new CryptographyCenter(); public static readonly ISerializationCenter SerializationCenter = new JsonSerializationCenter(); public static readonly IPasswordFactory PasswordFactory = new PasswordFactory(); + public static readonly IClipboardManager ClipboardManager = new ClipboardManager(); public static string ComputeTestDirectory([CallerMemberName] string username = "") => $"./TestFiles/{username}"; public static string ComputeDatabaseFileDirectory([CallerMemberName] string username = "") => $"{ComputeTestDirectory(username)}/{CryptographicCenter.GetHash(username)}"; @@ -52,6 +53,7 @@ public static IDatabase CreateTestDatabase(string[] passkeys = null, [CallerMemb IDatabase database = Database.Create(CryptographicCenter, SerializationCenter, PasswordFactory, + ClipboardManager, databaseFile, autoSaveFile, logFile, @@ -72,6 +74,7 @@ public static IDatabase OpenTestDatabase(string[] passkeys, out IWarning[] detec IDatabase database = Database.Open(CryptographicCenter, SerializationCenter, PasswordFactory, + ClipboardManager, databaseFile, autoSaveFile, logFile, From c89e26efeff3054b50ec3b433cafcf108fa0d43e Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Sun, 7 Dec 2025 09:14:40 +0300 Subject: [PATCH 09/14] Moving Interfaces --- Interfaces/{Interfaces => }/IAccount.cs | 0 Interfaces/{Interfaces => }/IClipboardManager.cs | 0 Interfaces/{Interfaces => }/ICryptographyCenter.cs | 0 Interfaces/{Interfaces => }/IDatabase.cs | 0 Interfaces/{Interfaces => }/IItem.cs | 0 Interfaces/{Interfaces => }/ILog.cs | 0 Interfaces/{Interfaces => }/IPasswordFactory.cs | 0 Interfaces/{Interfaces => }/ISerializationCenter.cs | 0 Interfaces/{Interfaces => }/IService.cs | 0 Interfaces/{Interfaces => }/IUser.cs | 0 Interfaces/{Interfaces => }/IWarning.cs | 0 Interfaces/{Interfaces => }/Interfaces.cd | 2 +- 12 files changed, 1 insertion(+), 1 deletion(-) rename Interfaces/{Interfaces => }/IAccount.cs (100%) rename Interfaces/{Interfaces => }/IClipboardManager.cs (100%) rename Interfaces/{Interfaces => }/ICryptographyCenter.cs (100%) rename Interfaces/{Interfaces => }/IDatabase.cs (100%) rename Interfaces/{Interfaces => }/IItem.cs (100%) rename Interfaces/{Interfaces => }/ILog.cs (100%) rename Interfaces/{Interfaces => }/IPasswordFactory.cs (100%) rename Interfaces/{Interfaces => }/ISerializationCenter.cs (100%) rename Interfaces/{Interfaces => }/IService.cs (100%) rename Interfaces/{Interfaces => }/IUser.cs (100%) rename Interfaces/{Interfaces => }/IWarning.cs (100%) rename Interfaces/{Interfaces => }/Interfaces.cd (98%) diff --git a/Interfaces/Interfaces/IAccount.cs b/Interfaces/IAccount.cs similarity index 100% rename from Interfaces/Interfaces/IAccount.cs rename to Interfaces/IAccount.cs diff --git a/Interfaces/Interfaces/IClipboardManager.cs b/Interfaces/IClipboardManager.cs similarity index 100% rename from Interfaces/Interfaces/IClipboardManager.cs rename to Interfaces/IClipboardManager.cs diff --git a/Interfaces/Interfaces/ICryptographyCenter.cs b/Interfaces/ICryptographyCenter.cs similarity index 100% rename from Interfaces/Interfaces/ICryptographyCenter.cs rename to Interfaces/ICryptographyCenter.cs diff --git a/Interfaces/Interfaces/IDatabase.cs b/Interfaces/IDatabase.cs similarity index 100% rename from Interfaces/Interfaces/IDatabase.cs rename to Interfaces/IDatabase.cs diff --git a/Interfaces/Interfaces/IItem.cs b/Interfaces/IItem.cs similarity index 100% rename from Interfaces/Interfaces/IItem.cs rename to Interfaces/IItem.cs diff --git a/Interfaces/Interfaces/ILog.cs b/Interfaces/ILog.cs similarity index 100% rename from Interfaces/Interfaces/ILog.cs rename to Interfaces/ILog.cs diff --git a/Interfaces/Interfaces/IPasswordFactory.cs b/Interfaces/IPasswordFactory.cs similarity index 100% rename from Interfaces/Interfaces/IPasswordFactory.cs rename to Interfaces/IPasswordFactory.cs diff --git a/Interfaces/Interfaces/ISerializationCenter.cs b/Interfaces/ISerializationCenter.cs similarity index 100% rename from Interfaces/Interfaces/ISerializationCenter.cs rename to Interfaces/ISerializationCenter.cs diff --git a/Interfaces/Interfaces/IService.cs b/Interfaces/IService.cs similarity index 100% rename from Interfaces/Interfaces/IService.cs rename to Interfaces/IService.cs diff --git a/Interfaces/Interfaces/IUser.cs b/Interfaces/IUser.cs similarity index 100% rename from Interfaces/Interfaces/IUser.cs rename to Interfaces/IUser.cs diff --git a/Interfaces/Interfaces/IWarning.cs b/Interfaces/IWarning.cs similarity index 100% rename from Interfaces/Interfaces/IWarning.cs rename to Interfaces/IWarning.cs diff --git a/Interfaces/Interfaces/Interfaces.cd b/Interfaces/Interfaces.cd similarity index 98% rename from Interfaces/Interfaces/Interfaces.cd rename to Interfaces/Interfaces.cd index 472b83a..5374964 100644 --- a/Interfaces/Interfaces/Interfaces.cd +++ b/Interfaces/Interfaces.cd @@ -152,7 +152,7 @@ Interfaces\IPasswordFactory.cs - + AAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= From e3283f79af15639737f51867524620b62db33efd Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Sun, 7 Dec 2025 19:41:44 +0300 Subject: [PATCH 10/14] Removing unused method --- Core/Utils/PasswordFactory.cs | 35 ---------------------------------- Interfaces/IPasswordFactory.cs | 19 ------------------ 2 files changed, 54 deletions(-) diff --git a/Core/Utils/PasswordFactory.cs b/Core/Utils/PasswordFactory.cs index 8cf6ba6..f591cc6 100644 --- a/Core/Utils/PasswordFactory.cs +++ b/Core/Utils/PasswordFactory.cs @@ -10,41 +10,6 @@ public class PasswordFactory : IPasswordFactory public string Numeric => "0123456789"; public string SpecialChars => "~!@#$%^&*()_-+={[}]\\|'\";:,<.>/?"; - public string GeneratePassword(int length, - bool includeUpperCaseAlphabeticChars = true, - bool includeLowerCaseAlphabeticChars = true, - bool includeNumericChars = true, - bool includeSpecialChars = true, - string excludedChars = "", - bool checkIfLeaked = true) - { - string alphabet = ""; - - if (includeUpperCaseAlphabeticChars) - { - alphabet += Alphabetic.ToUpper(); - } - - if (includeLowerCaseAlphabeticChars) - { - alphabet += Alphabetic.ToLower(); - } - - if (includeNumericChars) - { - alphabet += Numeric; - } - - if (includeSpecialChars) - { - alphabet += SpecialChars; - } - - alphabet = string.Join("", alphabet.ToCharArray().Except(excludedChars.ToCharArray())); - - return GeneratePassword(length, alphabet, checkIfLeaked); - } - public string GeneratePassword(int length, string alphabet, bool checkIfLeaked = true) { if (string.IsNullOrWhiteSpace(alphabet) diff --git a/Interfaces/IPasswordFactory.cs b/Interfaces/IPasswordFactory.cs index 398ec20..231333c 100644 --- a/Interfaces/IPasswordFactory.cs +++ b/Interfaces/IPasswordFactory.cs @@ -20,25 +20,6 @@ public interface IPasswordFactory /// string SpecialChars { get; } - /// - /// Generate a random password. - /// - /// The length of the password. - /// Include the upper case letters. - /// Include the lower case letters. - /// Include the digits. - /// Include the special characters. - /// Exclude some specific characters. - /// Ensure that the generated password has been already leaked. - /// The random geenrated password. - string GeneratePassword(int length, - bool includeUpperCaseAlphabeticChars = true, - bool includeLowerCaseAlphabeticChars = true, - bool includeNumericChars = true, - bool includeSpecialChars = true, - string excludedChars = "", - bool checkIfLeaked = true); - /// /// Generate a random password. /// From 761bc6c5430408d4a11061f2719f02a307d91094 Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Mon, 8 Dec 2025 06:13:53 +0300 Subject: [PATCH 11/14] Updating Readme - Part 2 --- README.md | 180 ++++++++++++++++-------------------------------------- 1 file changed, 54 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index f8aeb8a..949c248 100644 --- a/README.md +++ b/README.md @@ -28,112 +28,70 @@ This is a C# implementation of a local stored password manager. The application ### Class diagram ```mermaid classDiagram - direction TB - - %% Main Interfaces - class IDatabase { - <> - +DatabaseFile : string - +AutoSaveFile : string - +LogFile : string - +User : IUser - +SessionLeftTime : int - +Logs : IEnumerable~ILog~ - +Warnings : IEnumerable~IWarning~ - +SerializationCenter : ISerializationCenter - +CryptographyCenter : ICryptographyCenter - +PasswordFactory : IPasswordFactory - +WarningDetected : EventHandler~WarningDetectedEventArgs~ - +AutoSaveDetected : EventHandler~AutoSaveDetectedEventArgs~ - +DatabaseSaved : EventHandler - +DatabaseClosed : EventHandler~LogoutEventArgs~ - +Login(in passkey string) IUser - +Save() void - +Delete() void - +Close() void - +HasChanged() bool - +HasChanged(in itemId string) bool - +HasChanged(in itemId string, in fieldName string) bool - +ImportFromFile(in filePath string) bool - +ExportToFile(in filePath string) bool - +Create(in cryptographicCenter ICryptographyCenter, in serializationCenter ISerializationCenter, in passwordFactory IPasswordFactory, in databaseFile string, in autoSaveFile string, in logFile string, in username string, in passkeys IEnumerable~string~) IDatabase - } - - class IItem { - <> - +ItemId : string - +Database : IDatabase - } - - class IUser { - <> - +Name - +Email - +MasterPassword - +CreatedDate - +LastModifiedDate - +IsLocked - +Services : IEnumerable~IService~ - } - - class IService { - <> - +Name - +Url - +IconPath - +CreatedDate - +LastModifiedDate - +User : IUser - +Accounts : IEnumerable~IAccount~ - } - - class IAccount { + direction TB + + %% Main Interfaces + class ISerializationCenter { <> - +Username - +Password - +Email - +Notes - +Service : IService - +Options : AccountOption + +Serialize~T~(in toSerialize T) string + +Deserialize~T~(in toDeserialize string) T } - - class ILog { + + class IClipboardManager { <> - +Timestamp - +Message - +LogType + +RemoveAllOccurence(in removeList IEnumerable~string~) int } - - class IWarning { + + class IPasswordFactory { <> - +WarningType : WarningType - +Accounts : IEnumerable~IAccount~ - +Logs : IEnumerable~ILog~ - +Severity + +Alphabetic : string + +Numeric : string + +SpecialChars : string + + +GeneratePassword(in length int, in alphabet string, in checkIfLeaked bool) string + +PasswordLeaked(in password string) bool } - + class ICryptographyCenter { <> - +Encrypt() - +Decrypt() - +Hash() - +GenerateSalt() - +VerifyHash() - +GenerateKey() - } - - class ISerializationCenter { - <> - +Serialize() - +Deserialize() + +HashLength : int + + +GetHash(in source string) string + +GetSlowHash(in source string) string + +Sign(inout source string) void + +CheckSign(inout source string) bool + +EncryptSymmetrically(inout source string, in passwords IEnumerable~string~) string + +DecryptSymmetrically(inout source string, in passwords IEnumerable~string~) string + +GenerateRandomKeys(out publicKey string, out privateKey string) void + +EncryptAsymmetrically(inout source string, in key string) string + +DecryptAsymmetrically(inout source string, in key string) string } - - class IPasswordFactory { + + class IDatabase { <> - +GeneratePassword() - +ValidatePassword() - +GetPasswordStrength() - +GeneratePassphrase() + +DatabaseFile : string + +AutoSaveFile : string + +LogFile : string + +User : IUser + +SessionLeftTime : int + +Logs : IEnumerable~ILog~ + +Warnings : IEnumerable~IWarning~ + +SerializationCenter : ISerializationCenter + +CryptographyCenter : ICryptographyCenter + +PasswordFactory : IPasswordFactory + +WarningDetected : EventHandler~WarningDetectedEventArgs~ + +AutoSaveDetected : EventHandler~AutoSaveDetectedEventArgs~ + +DatabaseSaved : EventHandler + +DatabaseClosed : EventHandler~LogoutEventArgs~ + +Login(in passkey string) IUser + +Save() void + +Delete() void + +Close() void + +HasChanged() bool + +HasChanged(in itemId string) bool + +HasChanged(in itemId string, in fieldName string) bool + +ImportFromFile(in filePath string) bool + +ExportToFile(in filePath string) bool } %% Enums @@ -143,46 +101,16 @@ classDiagram TwoFactorAuth } - class WarningType { - <> - WeakPassword - ReusedPassword - OldPassword - } - %% Event Args Classes class AutoSaveDetectedEventArgs { +Account } - class LogoutEventArgs { - +Reason - } - - class WarningDetectedEventArgs { - +Warning - } - %% Inheritance Relations IUser --|> IItem %% Link Relations IUser "1" --> "*" IService : Services - IService "1" --> "*" IAccount : Accounts - IService --> IUser : User - IAccount --> IService : Service - IAccount --> AccountOption : Options - - IDatabase --> IUser : User - IDatabase --> "*" ILog : Logs - IDatabase --> "*" IWarning : Warnings - IDatabase --> ISerializationCenter : SerializationCenter - IDatabase --> ICryptographyCenter : CryptographyCenter - IDatabase --> IPasswordFactory : PasswordFactory - - IWarning --> WarningType : WarningType - IWarning --> "*" ILog : Logs - IWarning --> "*" IAccount : Accounts ``` **Example Use Cases** From 02f505760e82b12b8c2f41f6dc16148bd8b63c46 Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Sat, 13 Dec 2025 11:53:01 +0300 Subject: [PATCH 12/14] Updating Readme - Part 3 --- Interfaces/Interfaces.cd | 57 +++++++++------ README.md | 147 ++++++++++++++++++++++++++++++++++----- 2 files changed, 163 insertions(+), 41 deletions(-) diff --git a/Interfaces/Interfaces.cd b/Interfaces/Interfaces.cd index 5374964..acb986d 100644 --- a/Interfaces/Interfaces.cd +++ b/Interfaces/Interfaces.cd @@ -1,31 +1,34 @@  - + AAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA= Events\AutoSaveDetectedEventArgs.cs + + + - + AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Events\WarningDetectedEventArgs.cs - + AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Events\LogoutEventArgs.cs - + AAAgAAAAEAAAAAAAAAAAAAAAAAAAQAACAAAAAAJAAAA= - Interfaces\IAccount.cs + IAccount.cs @@ -36,7 +39,7 @@ AAACAAAAAAAAABAAAMAAAAAAAAAAAAASAAAAAABABAE= - Interfaces\ICryptographyCenter.cs + ICryptographyCenter.cs @@ -65,7 +68,7 @@ AgABAIgQAAAAgAAAYAAACCAggAGEAQAAEAARAACABAA= - Interfaces\IDatabase.cs + IDatabase.cs @@ -80,31 +83,34 @@ - + AAAAAAAAAEAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA= - Interfaces\IItem.cs + IItem.cs + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAIAIAAAAAA= - Interfaces\ILog.cs + ILog.cs AAAAAAAAAAAAAAAAAAAAAAAAACBAAAAAAAAAAAAAAAA= - Interfaces\ISerializationCenter.cs + ISerializationCenter.cs - + AAAhAAAAAAAAAAAAAAAAAAAAAAAAAACACAACAQAABAA= - Interfaces\IService.cs + IService.cs @@ -114,10 +120,10 @@ - + gABAAAEAAAAAAAAAAQAAAAAAAAAACAAIAAkAECAAAAA= - Interfaces\IUser.cs + IUser.cs @@ -129,13 +135,13 @@ - - + + AAAAAAAQgAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAA= - Interfaces\IWarning.cs + IWarning.cs @@ -149,29 +155,36 @@ AAAAAQAAAAAAAAAAAAAAAAACAAAAAAQAgABAAAAAAAA= - Interfaces\IPasswordFactory.cs + IPasswordFactory.cs AAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - Interfaces\IClipboardManager.cs + IClipboardManager.cs - + AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA= Enums\AccountOption.cs - + AAAAQAAAAABgAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= Enums\WarningType.cs + + + + AAAAIBAgAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAA= + Enums\AutoSaveMergeBehavior.cs + + \ No newline at end of file diff --git a/README.md b/README.md index 949c248..145c876 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,52 @@ classDiagram +DecryptAsymmetrically(inout source string, in key string) string } + class IItem { + <> + +ItemId : string + +Database : IDatabase + } + + class IAccount { + <> + +Service : IService + +Label : string + +Notes : string + +Identifiers : IEnumerable~string~ + +Password : string + +Passwords : IDictionary~DateTime, string~ + +PasswordUpdateReminderDelay : int + +Options : AccountOption + } + + class IService { + <> + +User : IUser + +ServiceName : string + +Url : string + +Notes : string + +Accounts : IEnumerable~IAccount~ + +AddAccount(in label string, in identifiers IEnumerable~string~, in password string) IAccount + +AddAccount(in label string, in identifiers IEnumerable~string~) IAccount + +AddAccount(in identifiers IEnumerable~string~, in password string) IAccount + +AddAccount(in identifiers IEnumerable~string~) IAccount + +DeleteAccount(in account IAccount) void + } + + class IUser { + <> + +Username : string + +Passkeys : IEnumerable~string~ + +LogoutTimeout : int + +CleaningClipboardTimeout : int + +ShowPasswordDelay : int + +NumberOfOldPasswordToKeep : int + +WarningsToNotify : WarningType + +Services : IEnumerable~IService~ + +AddService(in serviceName string) IService + +DeleteService(in service IService) void + } + class IDatabase { <> +DatabaseFile : string @@ -79,38 +125,99 @@ classDiagram +SerializationCenter : ISerializationCenter +CryptographyCenter : ICryptographyCenter +PasswordFactory : IPasswordFactory + +ClipboardManager : IClipboardManager +WarningDetected : EventHandler~WarningDetectedEventArgs~ +AutoSaveDetected : EventHandler~AutoSaveDetectedEventArgs~ +DatabaseSaved : EventHandler +DatabaseClosed : EventHandler~LogoutEventArgs~ +Login(in passkey string) IUser - +Save() void - +Delete() void - +Close() void - +HasChanged() bool + +Save(void) void + +Delete(void) void + +Close(void) void + +HasChanged(void) bool +HasChanged(in itemId string) bool +HasChanged(in itemId string, in fieldName string) bool +ImportFromFile(in filePath string) bool +ExportToFile(in filePath string) bool } + + class ILog { + <> + +DateTime : DateTime + +Message : string + +NeedsReview : bool + } + + class IWarning { + <> + +WarningType : WarningType + +Logs : IEnumerable~ILog~ + +Accounts : IEnumerable~IAccount~ + } %% Enums class AccountOption { <> - AutoSave - TwoFactorAuth + None + WarnIfPasswordLeaked + } + + class WarningType { + <> + LogReviewWarning + PasswordUpdateReminderWarning + DuplicatedPasswordsWarning + PasswordLeakedWarning + } + + class AutoSaveMergeBehavior { + <> + MergeAndSaveThenRemoveAutoSaveFile + MergeWithoutSavingAndKeepAutoSaveFile + DontMergeAndRemoveAutoSaveFile + DontMergeAndKeepAutoSaveFile } %% Event Args Classes class AutoSaveDetectedEventArgs { - +Account + +MergeBehavior : AutoSaveMergeBehavior + } + + class WarningDetectedEventArgs { + +Warnings : IEnumerable~IWarning~ + } + + class LogoutEventArgs { + +LoginTimeoutReached : bool } %% Inheritance Relations IUser --|> IItem + IService --|> IItem + IAccount --|> IItem %% Link Relations - IUser "1" --> "*" IService : Services + IItem --> IDatabase : Database + IAccount --> IService : Service + IAccount --> AccountOption : Options + IService "0" --> "*" IAccount : Accounts + IService --> IUser : User + IUser "0" --> "*" IService : Services + IDatabase --> ISerializationCenter : SerializationCenter + IDatabase --> ICryptographyCenter : CryptographyCenter + IDatabase --> IPasswordFactory : PasswordFactory + IDatabase --> IClipboardManager : ClipboardManager + IDatabase --> IUser : User + IDatabase "0" --> "*" IWarning : Warnings + IDatabase "0" --> "*" ILog : Logs + IDatabase --> WarningDetectedEventArgs : WarningDetected + IDatabase --> AutoSaveDetectedEventArgs : AutoSaveDetected + IDatabase --> LogoutEventArgs : DatabaseClosed + IWarning --> WarningType : WarningType + IWarning "0" --> "*" ILog : Logs + IWarning "0" --> "*" IAccount : Accounts + AutoSaveDetectedEventArgs --> AutoSaveMergeBehavior : MergeBehavior + WarningDetectedEventArgs "0" --> "*" IWarning : Warnings ``` **Example Use Cases** @@ -119,10 +226,10 @@ classDiagram ### Create a new database -To create a new database, use the `IDatabase.Create` static method. +To create a new database, use the `Upsilon.Apps.Passkey.Core.Models.Database.Create` static method. -This method needs an `ICryptographyCenter` implementation, an `ISerializationCenter` implementation and an `IPasswordFactory` implementation. -The namespace `Upsilon.Apps.PassKey.Core.Public.Utils` already contains implementations for all of these intefaces. +This method needs an `ICryptographyCenter` implementation, an `ISerializationCenter` implementation, an `IPasswordFactory` implementation and an `IClipboardManager` implementation. +The namespace `Upsilon.Apps.Passkey.Core.Utils` already contains implementations for all of these intefaces except for the `IClipboardManager` which needs an OS specific implementation. The next parameters are a set of files : the database file itself, the autosave file and the log file. These files will be created during the process. @@ -131,9 +238,10 @@ Finally, the method take the username and the passkeys. Note that the passkeys are used as master passwords to encrypt the database (and the other files). ```csharp -IDatabase database = IDatabase.Create(new Upsilon.Apps.PassKey.Core.Public.Utils.CryptographyCenter(), - new Upsilon.Apps.PassKey.Core.Public.Utils.JsonSerializationCenter(), - new Upsilon.Apps.PassKey.Core.Public.Utils.PasswordFactory(), +IDatabase database = Upsilon.Apps.Passkey.Core.Models.Database.Create(new Upsilon.Apps.Passkey.Core.Utils.CryptographyCenter(), + new Upsilon.Apps.Passkey.Core.Utils.JsonSerializationCenter(), + new Upsilon.Apps.Passkey.Core.Utils.PasswordFactory(), + new OSSpecificClipboardManager(), "./database.pku", "./autosave.pks", "./log.pkl", @@ -146,9 +254,9 @@ So to login, check the **Login to an user** use case. ### Open an existing database -To open an existing database, use the `IDatabase.Open` static method. +To open an existing database, use the `Upsilon.Apps.Passkey.Core.Models.Database.Open` static method. -This method needs the same `ICryptographyCenter` implementation, `ISerializationCenter` implementation and `IPasswordFactory` implementation as in the creation step. +This method needs an `ICryptographyCenter` implementation, an `ISerializationCenter` implementation, an `IPasswordFactory` implementation and an `IClipboardManager` implementation as in the creation step. The next parameters are a set of files : the database file itself, the autosave file and the log file. The database file must, obviously, exist, the autosave file and log files are optional but must be the same as provided during the creating process. @@ -156,9 +264,10 @@ The database file must, obviously, exist, the autosave file and log files are op Finally, the method take the username. ```csharp -IDatabase database = IDatabase.Open(new Upsilon.Apps.PassKey.Core.Public.Utils.CryptographyCenter(), - new Upsilon.Apps.PassKey.Core.Public.Utils.JsonSerializationCenter(), - new Upsilon.Apps.PassKey.Core.Public.Utils.PasswordFactory(), +IDatabase database = Upsilon.Apps.Passkey.Core.Models.Database.Open(new Upsilon.Apps.Passkey.Core.Utils.CryptographyCenter(), + new Upsilon.Apps.Passkey.Core.Utils.JsonSerializationCenter(), + new Upsilon.Apps.Passkey.Core.Utils.PasswordFactory(), + new OSSpecificClipboardManager(), "./database.pku", "./autosave.pks", "./log.pkl", From 6e58b28120bd0e55f4d9493a280a275b1946a483 Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Sat, 13 Dec 2025 12:15:04 +0300 Subject: [PATCH 13/14] Adding Windows and Linux environments --- .github/workflows/csharp-dotnet-linux.yml | 36 +++++++++++++++++++ ...p-dotnet.yml => csharp-dotnet-windows.yml} | 10 +++--- Upsilon.Apps.Passkey.Linux.slnx | 4 +++ ....slnx => Upsilon.Apps.Passkey.Windows.slnx | 2 +- 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/csharp-dotnet-linux.yml rename .github/workflows/{csharp-dotnet.yml => csharp-dotnet-windows.yml} (63%) create mode 100644 Upsilon.Apps.Passkey.Linux.slnx rename Upsilon.Apps.Passkey.slnx => Upsilon.Apps.Passkey.Windows.slnx (100%) diff --git a/.github/workflows/csharp-dotnet-linux.yml b/.github/workflows/csharp-dotnet-linux.yml new file mode 100644 index 0000000..6437575 --- /dev/null +++ b/.github/workflows/csharp-dotnet-linux.yml @@ -0,0 +1,36 @@ +name: C# Build with Dot Net - Linux + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Restore dependencies + run: dotnet restore Upsilon.Apps.Passkey.Linux.slnx + + - name: Build Debug + run: dotnet build Upsilon.Apps.Passkey.Linux.slnx --no-restore --configuration Debug + + - name: Build Relesae + run: dotnet build Upsilon.Apps.Passkey.Linux.slnx --no-restore --configuration Release + + - name: Test + run: dotnet test --no-build --verbosity normal Upsilon.Apps.Passkey.Linux.slnx + + +permissions: + contents: read + issues: write + pull-requests: write \ No newline at end of file diff --git a/.github/workflows/csharp-dotnet.yml b/.github/workflows/csharp-dotnet-windows.yml similarity index 63% rename from .github/workflows/csharp-dotnet.yml rename to .github/workflows/csharp-dotnet-windows.yml index e268702..97ec866 100644 --- a/.github/workflows/csharp-dotnet.yml +++ b/.github/workflows/csharp-dotnet-windows.yml @@ -1,4 +1,4 @@ -name: C# Build with Dot Net +name: C# Build with Dot Net - Windows on: push: @@ -18,16 +18,16 @@ jobs: dotnet-version: 10.0.x - name: Restore dependencies - run: dotnet restore Upsilon.Apps.Passkey.slnx + run: dotnet restore Upsilon.Apps.Passkey.Windows.slnx - name: Build Debug - run: dotnet build Upsilon.Apps.Passkey.slnx --no-restore --configuration Debug + run: dotnet build Upsilon.Apps.Passkey.Windows.slnx --no-restore --configuration Debug - name: Build Relesae - run: dotnet build Upsilon.Apps.Passkey.slnx --no-restore --configuration Release + run: dotnet build Upsilon.Apps.Passkey.Windows.slnx --no-restore --configuration Release - name: Test - run: dotnet test --no-build --verbosity normal Upsilon.Apps.Passkey.slnx + run: dotnet test --no-build --verbosity normal Upsilon.Apps.Passkey.Windows.slnx permissions: diff --git a/Upsilon.Apps.Passkey.Linux.slnx b/Upsilon.Apps.Passkey.Linux.slnx new file mode 100644 index 0000000..561bc69 --- /dev/null +++ b/Upsilon.Apps.Passkey.Linux.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/Upsilon.Apps.Passkey.slnx b/Upsilon.Apps.Passkey.Windows.slnx similarity index 100% rename from Upsilon.Apps.Passkey.slnx rename to Upsilon.Apps.Passkey.Windows.slnx index 5ac3ca7..8ac21f0 100644 --- a/Upsilon.Apps.Passkey.slnx +++ b/Upsilon.Apps.Passkey.Windows.slnx @@ -1,5 +1,5 @@ - + From a78449dba36f5afb45c90a0a0ed89ae93bc7abcb Mon Sep 17 00:00:00 2001 From: Yassin Lokhat Date: Sat, 13 Dec 2025 21:29:39 +0300 Subject: [PATCH 14/14] Code Cleanup --- Core/Models/Account.cs | 6 +++--- Core/Models/AutoSave.cs | 1 - Core/Utils/FileLocker.cs | 1 - Interfaces/Events/WarningDetectedEventArgs.cs | 4 +--- Interfaces/IClipboardManager.cs | 6 +----- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Core/Models/Account.cs b/Core/Models/Account.cs index 6dcb6be..aec8c87 100644 --- a/Core/Models/Account.cs +++ b/Core/Models/Account.cs @@ -1,7 +1,7 @@ -using Upsilon.Apps.Passkey.Interfaces.Enums; -using Upsilon.Apps.Passkey.Interfaces; +using System.ComponentModel; using Upsilon.Apps.Passkey.Core.Utils; -using System.ComponentModel; +using Upsilon.Apps.Passkey.Interfaces; +using Upsilon.Apps.Passkey.Interfaces.Enums; namespace Upsilon.Apps.Passkey.Core.Models { diff --git a/Core/Models/AutoSave.cs b/Core/Models/AutoSave.cs index 8d86ad4..16c9d5d 100644 --- a/Core/Models/AutoSave.cs +++ b/Core/Models/AutoSave.cs @@ -1,5 +1,4 @@ using Upsilon.Apps.Passkey.Core.Utils; -using Upsilon.Apps.Passkey.Interfaces; namespace Upsilon.Apps.Passkey.Core.Models { diff --git a/Core/Utils/FileLocker.cs b/Core/Utils/FileLocker.cs index c9e99bc..a6ad8e7 100644 --- a/Core/Utils/FileLocker.cs +++ b/Core/Utils/FileLocker.cs @@ -1,6 +1,5 @@ using System.IO.Compression; using System.Text; -using Upsilon.Apps.Passkey.Core.Utils; using Upsilon.Apps.Passkey.Interfaces; using Upsilon.Apps.Passkey.Interfaces.Utils; diff --git a/Interfaces/Events/WarningDetectedEventArgs.cs b/Interfaces/Events/WarningDetectedEventArgs.cs index a0f7013..48d06a2 100644 --- a/Interfaces/Events/WarningDetectedEventArgs.cs +++ b/Interfaces/Events/WarningDetectedEventArgs.cs @@ -1,6 +1,4 @@ -using Upsilon.Apps.Passkey.Interfaces; - -namespace Upsilon.Apps.Passkey.Interfaces.Events +namespace Upsilon.Apps.Passkey.Interfaces.Events { /// /// Represent a warning detected event argument. diff --git a/Interfaces/IClipboardManager.cs b/Interfaces/IClipboardManager.cs index 5077e86..f557d2d 100644 --- a/Interfaces/IClipboardManager.cs +++ b/Interfaces/IClipboardManager.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Upsilon.Apps.Passkey.Interfaces +namespace Upsilon.Apps.Passkey.Interfaces { /// /// Represent a OS specific Clipboard manager.