Compare commits

...

12 Commits

Author SHA1 Message Date
Adam Hathcock 47e72ee1a7 Merge pull request #364 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Main to dev
2025-07-23 14:19:52 +01:00
Adam Hathcock f3de5324db Merge pull request #362 from specklesystems/main
Main to dev
2025-07-23 13:44:14 +01:00
Adam Hathcock 4dd6db886f insert or replace always...don't use ignore or insert (#363)
* SaveObject is always insert or replace.  Never use insert or ignore

* add/fix tests

* always replace even for bulk
2025-07-23 12:16:08 +00:00
Adam Hathcock 4b82db8ea2 Merge pull request #361 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Main to dev
2025-07-23 10:23:04 +01:00
Adam Hathcock 9e7f26f7a6 Add ModelCacheManager class and use it (#356)
* Introduce ModelCacheManager to manage cache and sizes and deletions

* move and abstract

* add tests and format

* Update src/Speckle.Sdk/Caching/ModelCacheManager.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Clean up

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-23 10:07:57 +01:00
Adam Hathcock b19f8c4219 Merge pull request #358 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
(NO SQUASH) Main to dev for release
2025-07-21 15:49:49 +01:00
Adam Hathcock c517e61517 Merge pull request #360 from specklesystems/main-dev
Main to dev
2025-07-21 15:47:38 +01:00
Adam Hathcock b3e0623856 Merge remote-tracking branch 'origin/main' into main-dev 2025-07-21 15:36:30 +01:00
Adam Hathcock e5d1ef2448 Merge pull request #354 from specklesystems/main-dev
Merge pull request #348 from specklesystems/dev
2025-07-21 11:12:54 +01:00
Adam Hathcock 83c3de05fa Merge remote-tracking branch 'origin/dev' into main-dev 2025-07-21 11:02:02 +01:00
Adam Hathcock 507ded7d4a Fix shallow copy allocations and perf (#357)
* add more DynamicBase Tests

* Move ShallowCopy to dynamic and try to be faster with copy

* Correct tests for macOS

* use cache obsolete attribute

* Update src/Speckle.Sdk/Models/DynamicBase.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Speckle.Sdk.Tests.Unit/Models/DynamicBaseTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix AI

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 11:01:00 +01:00
Adam Hathcock 1bcd8ac3a4 Merge branch 'dev' into main-dev 2025-07-03 10:42:05 +01:00
17 changed files with 820 additions and 75 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ To ensure high-quality and consistent commits, please follow these guidelines:
3. **Test your changes**
- Run all unit tests before committing.
- Add or update xUnit tests as needed.
- Use FluentAssertions for assertions and Moq for mocking in tests.
- Use AwesomeAssertions for assertions and Moq for mocking in tests.
4. **Review your changes**
- Double-check for accidental debug code or commented-out code.
+22
View File
@@ -0,0 +1,22 @@
using Speckle.InterfaceGenerator;
namespace Speckle.Sdk.Caching;
/// <summary>
/// This mocks away the file system operations for testing purposes.
/// </summary>
[GenerateAutoInterface]
public class FileSystem : IFileSystem
{
public bool DirectoryExists(string path) => Directory.Exists(path);
public void CreateDirectory(string path) => Directory.CreateDirectory(path);
public IEnumerable<string> EnumerateFiles(string path) => Directory.EnumerateFiles(path);
public void DeleteFile(string path) => File.Delete(path);
public long GetFileSize(string path) => new FileInfo(path).Length;
public string Combine(params string[] paths) => Path.Combine(paths);
}
@@ -0,0 +1,93 @@
using Microsoft.Extensions.Logging;
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Caching;
/// <summary>
/// This class manages the cache for model data, providing methods to get stream paths, clear the cache, and calculate cache size.
/// </summary>
[GenerateAutoInterface]
public class ModelCacheManager(ILogger<ModelCacheManager> logger, IFileSystem fileSystem) : IModelCacheManager
{
private const string DATA_FOLDER = "Projects";
private static readonly string s_basePath = SpecklePathProvider.UserSpeckleFolderPath;
private static string CacheFolder => Path.Combine(s_basePath, DATA_FOLDER);
public string GetStreamPath(string streamId) => GetDbPath(streamId);
public static string GetDbPath(string streamId)
{
var db = Path.Combine(CacheFolder, $"{streamId}.db");
try
{
Directory.CreateDirectory(CacheFolder); //ensure dir is there
return db;
}
catch (Exception ex)
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
{
throw new TransportException($"Path was invalid or could not be created {db}", ex);
}
}
public void ClearCache()
{
try
{
if (!fileSystem.DirectoryExists(CacheFolder))
{
return;
}
foreach (var db in fileSystem.EnumerateFiles(CacheFolder))
{
try
{
fileSystem.DeleteFile(db);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
{
logger.LogWarning(ex, "Failed to delete cache file {filePath}", db);
}
}
}
catch (Exception ex)
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
{
throw new TransportException($"Cache folder could not be cleared: {CacheFolder}", ex);
}
}
public long GetCacheSize()
{
try
{
if (!fileSystem.DirectoryExists(CacheFolder))
{
return 0;
}
long size = 0;
foreach (var file in fileSystem.EnumerateFiles(CacheFolder))
{
try
{
size += fileSystem.GetFileSize(file);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
{
logger.LogWarning(ex, "Failed to get size for cache file {a}", file);
}
}
return size;
}
catch (Exception ex)
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
{
throw new TransportException($"Cache folder size could not be determined: {CacheFolder}", ex);
}
}
}
@@ -174,7 +174,7 @@ public sealed class AccountManager(
account.id = null!; //TODO this is gross so remove when id is nullable
RemoveAccount(id);
_accountStorage.SaveObject(account.id.NotNull(), JsonConvert.SerializeObject(account));
_accountStorage.UpdateObject(account.id.NotNull(), JsonConvert.SerializeObject(account));
}
public IEnumerable<Account> GetAccounts(string serverUrl)
@@ -407,7 +407,7 @@ public sealed class AccountManager(
{
account.isDefault = true;
}
_accountStorage.SaveObject(account.id, JsonConvert.SerializeObject(account));
_accountStorage.UpdateObject(account.id, JsonConvert.SerializeObject(account));
}
}
+4
View File
@@ -15,10 +15,14 @@ internal static class TypeLoader
private static ConcurrentDictionary<string, Type> s_cachedTypes = new();
private static ConcurrentDictionary<Type, string> s_fullTypeStrings = new();
private static ConcurrentDictionary<PropertyInfo, JsonPropertyAttribute?> s_jsonPropertyAttribute = new();
private static readonly ConcurrentDictionary<PropertyInfo, bool> s_obsolete = new();
private static ConcurrentDictionary<Type, IReadOnlyList<PropertyInfo>> s_propInfoCache = new();
public static IEnumerable<LoadedType> Types => s_availableTypes;
public static bool IsObsolete(PropertyInfo property) =>
s_obsolete.GetOrAdd(property, p => p.IsDefined(typeof(ObsoleteAttribute), true));
public static JsonPropertyAttribute? GetJsonPropertyAttribute(PropertyInfo property) =>
s_jsonPropertyAttribute.GetOrAdd(property, p => p.GetCustomAttribute<JsonPropertyAttribute>(true));
+1 -29
View File
@@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Speckle.Newtonsoft.Json;
using Speckle.Newtonsoft.Json.Linq;
using Speckle.Sdk.Common;
using Speckle.Sdk.Helpers;
using Speckle.Sdk.Host;
using Speckle.Sdk.Serialisation;
@@ -92,8 +91,7 @@ public class Base : DynamicBase, ISpeckleObject
var typedProps = @base.GetInstanceMembers();
foreach (var prop in typedProps.Where(p => p.CanRead))
{
bool isIgnored =
prop.IsDefined(typeof(ObsoleteAttribute), true) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
bool isIgnored = TypeLoader.IsObsolete(prop) || prop.IsDefined(typeof(JsonIgnoreAttribute), true);
if (isIgnored)
{
continue;
@@ -193,30 +191,4 @@ public class Base : DynamicBase, ISpeckleObject
return count;
}
}
/// <summary>
/// Creates a shallow copy of the current base object.
/// This operation does NOT copy/duplicate the data inside each prop.
/// The new object's property values will be pointers to the original object's property value.
/// </summary>
/// <returns>A shallow copy of the original object.</returns>
public Base ShallowCopy()
{
Type type = GetType();
Base myDuplicate = (Base)Activator.CreateInstance(type).NotNull();
myDuplicate.id = id;
myDuplicate.applicationId = applicationId;
foreach (var kvp in GetMembers())
{
var propertyInfo = type.GetProperty(kvp.Key);
if (propertyInfo is not null && !propertyInfo.CanWrite)
{
continue;
}
myDuplicate[kvp.Key] = kvp.Value;
}
return myDuplicate;
}
}
+40 -2
View File
@@ -7,7 +7,7 @@ namespace Speckle.Sdk.Models;
/// <summary>
/// Base class implementing a bunch of nice dynamic object methods, like adding and removing props dynamically. Makes c# feel like json.
/// <para>Orginally adapted from Rick Strahl 🤘</para>
/// <para>Originally adapted from Rick Strahl 🤘</para>
/// <para>https://weblog.west-wind.com/posts/2012/feb/08/creating-a-dynamic-extensible-c-expando-object</para>
/// </summary>
public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
@@ -84,6 +84,44 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
}
}
/// <summary>
/// Creates a shallow copy of the current base object.
/// This operation does NOT copy/duplicate the data inside each prop.
/// The new object's property values will be pointers to the original object's property value.
/// </summary>
/// <returns>A shallow copy of the original object.</returns>
public DynamicBase ShallowCopy()
{
Type type = GetType();
DynamicBase myDuplicate = (DynamicBase)(
Activator.CreateInstance(type) ?? throw new SpeckleException($"Failed to create instance of {type.Name}")
);
// Add dynamic members
foreach (var kvp in _properties)
{
myDuplicate._properties[kvp.Key] = kvp.Value;
}
var pinfos = TypeLoader.GetBaseProperties(type).Where(x => !TypeLoader.IsObsolete(x));
foreach (var pi in pinfos)
{
if (pi.CanWrite)
{
try
{
pi.SetValue(myDuplicate, pi.GetValue(this));
}
catch (Exception ex) when (!ex.IsFatal())
{
throw new SpeckleException($"Failed to set value for {type.Name}.{pi.Name}", ex);
}
}
}
return myDuplicate;
}
/// <inheritdoc />
/// <summary>
/// Gets properties via the dot syntax.
@@ -232,7 +270,7 @@ public class DynamicBase : DynamicObject, IDynamicMetaObjectProvider
.GetBaseProperties(GetType())
.Where(x =>
{
var hasObsolete = x.IsDefined(typeof(ObsoleteAttribute), true);
var hasObsolete = TypeLoader.IsObsolete(x);
// If obsolete is false and prop has obsolete attr
// OR
@@ -1,11 +1,11 @@
using Speckle.InterfaceGenerator;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Serialisation.Utilities;
namespace Speckle.Sdk.SQLite;
[GenerateAutoInterface]
public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
public class SqLiteJsonCacheManagerFactory(IModelCacheManager modelCacheManager) : ISqLiteJsonCacheManagerFactory
{
public const int INITIAL_CONCURRENCY = 4;
@@ -16,5 +16,5 @@ public class SqLiteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
Create(Path.Combine(SpecklePathProvider.UserApplicationDataPath(), "Speckle", $"{scope}.db"), 1);
public ISqLiteJsonCacheManager CreateFromStream(string streamId) =>
Create(SqlitePaths.GetDBPath(streamId), INITIAL_CONCURRENCY);
Create(modelCacheManager.GetStreamPath(streamId), INITIAL_CONCURRENCY);
}
@@ -1,30 +0,0 @@
using Speckle.Sdk.Logging;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Serialisation.Utilities;
public static class SqlitePaths
{
private const string APPLICATION_NAME = "Speckle";
private const string DATA_FOLDER = "Projects";
private static readonly string basePath = SpecklePathProvider.UserApplicationDataPath();
public static string BlobStorageFolder =>
SpecklePathProvider.BlobStoragePath(Path.Combine(basePath, APPLICATION_NAME));
public static string GetDBPath(string streamId)
{
var dir = Path.Combine(basePath, APPLICATION_NAME, DATA_FOLDER);
var db = Path.Combine(dir, $"{streamId}.db");
try
{
Directory.CreateDirectory(dir); //ensure dir is there
return db;
}
catch (Exception ex)
when (ex is ArgumentException or IOException or UnauthorizedAccessException or NotSupportedException)
{
throw new TransportException($"Path was invalid or could not be created {db}", ex);
}
}
}
@@ -3,9 +3,9 @@ using System.Diagnostics;
using System.Text;
using System.Timers;
using Microsoft.Data.Sqlite;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Models;
using Speckle.Sdk.Serialisation.Utilities;
using Timer = System.Timers.Timer;
namespace Speckle.Sdk.Transports;
@@ -28,7 +28,7 @@ public sealed class SQLiteTransport2 : IDisposable, ICloneable, ITransport, IBlo
{
_streamId = streamId;
_rootPath = SqlitePaths.GetDBPath(streamId);
_rootPath = ModelCacheManager.GetDbPath(streamId);
_connectionString = $"Data Source={_rootPath};";
@@ -50,7 +50,7 @@ public sealed class SQLiteTransport2 : IDisposable, ICloneable, ITransport, IBlo
private SqliteConnection Connection { get; set; }
private readonly SemaphoreSlim _connectionLock = new(1, 1);
public string BlobStorageFolder => SqlitePaths.BlobStorageFolder;
public string BlobStorageFolder => SpecklePathProvider.UserSpeckleFolderPath;
public void SaveBlob(Blob obj)
{
@@ -0,0 +1,420 @@
using Microsoft.Extensions.Logging;
using Moq;
using Speckle.Newtonsoft.Json;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Credentials;
using Speckle.Sdk.Helpers;
using Speckle.Sdk.SQLite;
using Speckle.Sdk.Testing;
namespace Speckle.Sdk.Tests.Unit.Credentials;
public class AccountManagerTests : MoqTest
{
private class TestAccountFactory : IAccountFactory
{
public Task<Account> CreateAccount(
Uri serverUrl,
string speckleToken,
string? refreshToken = default,
CancellationToken cancellationToken = default
) => throw new NotImplementedException();
public Task<ActiveUserServerInfoResponse> GetUserServerInfo(
Uri serverUrl,
string? authToken,
CancellationToken ct
) => throw new NotImplementedException();
}
private readonly Mock<ISpeckleApplication> _mockApplication;
private readonly Mock<ILogger<AccountManager>> _mockLogger;
private readonly Mock<IGraphQLClientFactory> _mockGraphQLClientFactory;
private readonly Mock<ISpeckleHttp> _mockSpeckleHttp;
private readonly IAccountFactory _mockAccountFactory;
private readonly Mock<ISqLiteJsonCacheManagerFactory> _mockSqLiteJsonCacheManagerFactory;
private readonly Mock<ISqLiteJsonCacheManager> _mockAccountStorage;
private readonly Mock<ISqLiteJsonCacheManager> _mockAccountAddLockStorage;
private readonly AccountManager _accountManager;
public AccountManagerTests()
{
_mockApplication = Create<ISpeckleApplication>();
_mockLogger = Create<ILogger<AccountManager>>(MockBehavior.Loose);
_mockGraphQLClientFactory = Create<IGraphQLClientFactory>();
_mockSpeckleHttp = Create<ISpeckleHttp>();
_mockAccountFactory = new TestAccountFactory();
_mockSqLiteJsonCacheManagerFactory = Create<ISqLiteJsonCacheManagerFactory>();
_mockAccountStorage = Create<ISqLiteJsonCacheManager>();
_mockAccountAddLockStorage = Create<ISqLiteJsonCacheManager>();
_mockSqLiteJsonCacheManagerFactory.Setup(f => f.CreateForUser("Accounts")).Returns(_mockAccountStorage.Object);
_mockSqLiteJsonCacheManagerFactory
.Setup(f => f.CreateForUser("AccountAddFlow"))
.Returns(_mockAccountAddLockStorage.Object);
_accountManager = new AccountManager(
_mockApplication.Object,
_mockLogger.Object,
_mockGraphQLClientFactory.Object,
_mockSpeckleHttp.Object,
_mockAccountFactory,
_mockSqLiteJsonCacheManagerFactory.Object
);
}
[Fact]
public void GetDefaultServerUrl_ReturnsDefaultUrl_WhenNoCustomUrlProvided()
{
// Act
var result = _accountManager.GetDefaultServerUrl();
// Assert
Assert.Equal(new Uri(AccountManager.DEFAULT_SERVER_URL), result);
}
[Fact]
public void GetAccount_ReturnsAccount_WhenExists()
{
// Arrange
var accountId = "test-account-id";
var account = CreateTestAccount(accountId);
_mockAccountStorage
.Setup(s => s.GetAllObjects())
.Returns(new[] { (accountId, JsonConvert.SerializeObject(account)) });
// Act
var result = _accountManager.GetAccount(accountId);
// Assert
Assert.Equal(accountId, result.id);
Assert.Equal(account.userInfo.name, result.userInfo.name);
}
[Fact]
public void GetAccount_ThrowsException_WhenNotExists()
{
// Arrange
var accountId = "non-existent-id";
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns([]);
// Act & Assert
var exception = Assert.Throws<SpeckleAccountManagerException>(() => _accountManager.GetAccount(accountId));
Assert.Equal($"Account {accountId} not found", exception.Message);
}
[Fact]
public void GetAccounts_StringParameter_CallsUriOverload()
{
// Arrange
var serverUrl = "https://test.speckle.systems";
var account = CreateTestAccount("test-account-id");
account.serverInfo.url = serverUrl;
_mockAccountStorage
.Setup(s => s.GetAllObjects())
.Returns(new[] { (account.id, JsonConvert.SerializeObject(account)) });
// Act
var result = _accountManager.GetAccounts(serverUrl).ToList();
// Assert
Assert.Single(result);
Assert.Equal(serverUrl, result[0].serverInfo.url);
}
[Fact]
public void GetAccounts_UriParameter_ReturnsMatchingAccounts()
{
// Arrange
var serverUri = new Uri("https://test.speckle.systems");
var account = CreateTestAccount("test-account-id");
account.serverInfo.url = serverUri.ToString();
_mockAccountStorage
.Setup(s => s.GetAllObjects())
.Returns(new[] { (account.id, JsonConvert.SerializeObject(account)) });
// Act
var result = _accountManager.GetAccounts(serverUri).ToList();
// Assert
Assert.Single(result);
Assert.Equal(serverUri.ToString(), result[0].serverInfo.url);
}
[Fact]
public void GetDefaultAccount_ReturnsMarkedDefaultAccount_WhenExists()
{
// Arrange
var defaultAccount = CreateTestAccount("default-account");
defaultAccount.isDefault = true;
var regularAccount = CreateTestAccount("regular-account");
_mockAccountStorage
.Setup(s => s.GetAllObjects())
.Returns(
new[]
{
(defaultAccount.id, JsonConvert.SerializeObject(defaultAccount)),
(regularAccount.id, JsonConvert.SerializeObject(regularAccount)),
}
);
// Act
var result = _accountManager.GetDefaultAccount();
// Assert
Assert.NotNull(result);
Assert.Equal("default-account", result!.id);
Assert.True(result.isDefault);
}
[Fact]
public void GetDefaultAccount_ReturnsFirstAccount_WhenNoDefaultExists()
{
// Arrange
var account1 = CreateTestAccount("account-1");
var account2 = CreateTestAccount("account-2");
_mockAccountStorage
.Setup(s => s.GetAllObjects())
.Returns(
new[]
{
(account1.id, JsonConvert.SerializeObject(account1)),
(account2.id, JsonConvert.SerializeObject(account2)),
}
);
// Act
var result = _accountManager.GetDefaultAccount();
// Assert
Assert.NotNull(result);
Assert.Equal("account-1", result!.id);
}
[Fact]
public void GetDefaultAccount_ReturnsNull_WhenNoAccounts()
{
// Arrange
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns([]);
// Act
var result = _accountManager.GetDefaultAccount();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAccounts_SkipsInvalidAccounts()
{
// Arrange
var validAccount = CreateTestAccount("valid-account");
validAccount.isDefault = true;
var invalidAccount = new Account { id = "invalid-account" };
var deleteCalled = false;
_mockAccountStorage
.Setup(s => s.GetAllObjects())
.Returns(() =>
{
if (deleteCalled)
{
return [(validAccount.id, JsonConvert.SerializeObject(validAccount))];
}
return
[
(validAccount.id, JsonConvert.SerializeObject(validAccount)),
(invalidAccount.id, JsonConvert.SerializeObject(invalidAccount)),
];
});
_mockAccountStorage.Setup(s => s.DeleteObject(invalidAccount.id)).Callback(() => deleteCalled = true);
// Act
var result = _accountManager.GetAccounts().ToList();
// Assert
Assert.Single(result);
Assert.Equal("valid-account", result[0].id);
_mockAccountStorage.Verify(s => s.DeleteObject(invalidAccount.id), Times.Once);
}
[Fact]
public void RemoveAccount_RemovesAccount()
{
// Arrange
var accountId = "account-to-remove";
_mockAccountStorage.Setup(s => s.DeleteObject(accountId));
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns([]);
// Act
_accountManager.RemoveAccount(accountId);
// Assert
_mockAccountStorage.Verify(s => s.DeleteObject(accountId), Times.Once);
}
[Fact]
public void RemoveAccount_SetsNewDefaultAccount_WhenDefaultRemoved()
{
// Arrange
var defaultAccountId = "default-account";
var regularAccountId = "regular-account";
var regularAccount = CreateTestAccount(regularAccountId);
_mockAccountStorage.Setup(s => s.DeleteObject(defaultAccountId));
_mockAccountStorage
.Setup(s => s.GetAllObjects())
.Returns(new[] { (regularAccountId, JsonConvert.SerializeObject(regularAccount)) });
_mockAccountStorage.Setup(s => s.UpdateObject(regularAccountId, It.IsAny<string>()));
// Act
_accountManager.RemoveAccount(defaultAccountId);
// Assert
_mockAccountStorage.Verify(s => s.DeleteObject(defaultAccountId), Times.Once);
_mockAccountStorage.Verify(
s => s.UpdateObject(regularAccountId, It.Is<string>(json => json.Contains("\"isDefault\":true"))),
Times.Once
);
}
[Fact]
public void ChangeDefaultAccount_UpdatesDefaultAccount()
{
// Arrange
var account1 = CreateTestAccount("account-1");
account1.isDefault = true;
var account2 = CreateTestAccount("account-2");
_mockAccountStorage
.Setup(s => s.GetAllObjects())
.Returns(
new[]
{
(account1.id, JsonConvert.SerializeObject(account1)),
(account2.id, JsonConvert.SerializeObject(account2)),
}
);
_mockAccountStorage.Setup(s => s.UpdateObject(account1.id, It.IsAny<string>()));
_mockAccountStorage.Setup(s => s.UpdateObject(account2.id, It.IsAny<string>()));
// Act
_accountManager.ChangeDefaultAccount(account2.id);
// Assert
_mockAccountStorage.Verify(
s => s.UpdateObject(account1.id, It.Is<string>(json => json.Contains("\"isDefault\":false"))),
Times.Once
);
_mockAccountStorage.Verify(
s => s.UpdateObject(account2.id, It.Is<string>(json => json.Contains("\"isDefault\":true"))),
Times.Once
);
}
[Fact]
public void GetLocalIdentifierForAccount_ReturnsIdentifier_WhenAccountExists()
{
// Arrange
var account = CreateTestAccount("test-account");
var expectedUri = new Uri($"{account.serverInfo.url}?id={account.userInfo.id}");
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns(new[] { ("bad", JsonConvert.SerializeObject(account)) });
// Act
var result = _accountManager.GetLocalIdentifierForAccount(account);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedUri, result);
}
[Fact]
public void GetLocalIdentifierForAccount_ReturnsNull_WhenAccountDoesNotExist()
{
// Arrange
var account = CreateTestAccount("non-existent-account");
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns([]);
// Act
var result = _accountManager.GetLocalIdentifierForAccount(account);
// Assert
Assert.Null(result);
}
[Fact]
public void GetAccountForLocalIdentifier_ReturnsAccount_WhenMatches()
{
// Arrange
var account = CreateTestAccount("test-account");
var localIdentifier = new Uri($"{account.serverInfo.url}?id={account.userInfo.id}");
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns(new[] { ("bad", JsonConvert.SerializeObject(account)) });
// Act
var result = _accountManager.GetAccountForLocalIdentifier(localIdentifier);
// Assert
Assert.NotNull(result);
Assert.Equal(account.id, result!.id);
}
[Fact]
public void GetAccountForLocalIdentifier_ReturnsNull_WhenNoMatch()
{
// Arrange
var account = CreateTestAccount("test-account");
var localIdentifier = new Uri("https://different.url?u=different-user");
_mockAccountStorage.Setup(s => s.GetAllObjects()).Returns(new[] { ("bad", JsonConvert.SerializeObject(account)) });
// Act
var result = _accountManager.GetAccountForLocalIdentifier(localIdentifier);
// Assert
Assert.Null(result);
}
// Helper method to create a test account
private static Account CreateTestAccount(string id)
{
return new Account
{
id = id,
token = "test-token",
refreshToken = "refresh-token",
isDefault = false,
isOnline = true,
userInfo = new UserInfo
{
id = "user-id",
name = "Test User",
email = "test@example.com",
company = "Test Company",
},
serverInfo = new ServerInfo
{
name = "Test Server",
url = "https://test.speckle.systems",
company = "Speckle",
},
};
}
}
+2 -2
View File
@@ -18,7 +18,7 @@ public class SpecklePathTests
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
pattern = @"\/Users\/.*\/\.config";
pattern = @"\/Users\/.*\/Library\/Application Support";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
@@ -57,7 +57,7 @@ public class SpecklePathTests
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
pattern = @"\/Users\/.*\/\.config";
pattern = @"\/Users\/.*\/Library\/Application Support";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
@@ -0,0 +1,83 @@
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Testing;
namespace Speckle.Sdk.Tests.Unit;
public class ModelCacheManagerMockTests : MoqTest
{
private readonly Mock<IFileSystem> _fileSystemMock;
private readonly ModelCacheManager _manager;
public ModelCacheManagerMockTests()
{
Mock<ILogger<ModelCacheManager>> loggerMock = Create<ILogger<ModelCacheManager>>(MockBehavior.Loose);
_fileSystemMock = Create<IFileSystem>();
_manager = new ModelCacheManager(loggerMock.Object, _fileSystemMock.Object);
}
[Fact]
public void ClearCache_ShouldNotDeleteFiles_WhenDirectoryDoesNotExist()
{
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(false);
_manager.ClearCache();
_fileSystemMock.Verify(fs => fs.EnumerateFiles(It.IsAny<string>()), Times.Never);
_fileSystemMock.Verify(fs => fs.DeleteFile(It.IsAny<string>()), Times.Never);
}
[Fact]
public void ClearCache_ShouldDeleteFiles_WhenDirectoryExists()
{
var files = new List<string> { "file1.db", "file2.db" };
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
_fileSystemMock.Setup(fs => fs.EnumerateFiles(It.IsAny<string>())).Returns(files);
foreach (var file in files)
{
_fileSystemMock.Setup(fs => fs.DeleteFile(file));
}
_manager.ClearCache();
}
[Fact]
public void ClearCache_ShouldLogWarning_WhenDeleteFileThrows()
{
var files = new List<string> { "file1.db" };
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
_fileSystemMock.Setup(fs => fs.EnumerateFiles(It.IsAny<string>())).Returns(files);
_fileSystemMock.Setup(fs => fs.DeleteFile(It.IsAny<string>())).Throws<IOException>();
_manager.ClearCache();
}
[Fact]
public void GetCacheSize_ShouldReturnZero_WhenDirectoryDoesNotExist()
{
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(false);
var size = _manager.GetCacheSize();
size.Should().Be(0);
}
[Fact]
public void GetCacheSize_ShouldSumFileSizes()
{
var files = new List<string> { "file1.db", "file2.db" };
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
_fileSystemMock.Setup(fs => fs.EnumerateFiles(It.IsAny<string>())).Returns(files);
_fileSystemMock.Setup(fs => fs.GetFileSize("file1.db")).Returns(10);
_fileSystemMock.Setup(fs => fs.GetFileSize("file2.db")).Returns(20);
var size = _manager.GetCacheSize();
size.Should().Be(30);
}
[Fact]
public void GetCacheSize_ShouldLogWarning_WhenGetFileSizeThrows()
{
var files = new List<string> { "file1.db" };
_fileSystemMock.Setup(fs => fs.DirectoryExists(It.IsAny<string>())).Returns(true);
_fileSystemMock.Setup(fs => fs.EnumerateFiles(It.IsAny<string>())).Returns(files);
_fileSystemMock.Setup(fs => fs.GetFileSize(It.IsAny<string>())).Throws<IOException>();
var size = _manager.GetCacheSize();
size.Should().Be(0);
}
}
@@ -213,7 +213,9 @@ public class BaseTests
[Fact]
public void CanShallowCopy()
{
var sample = new SampleObject();
var sample = new SampleObject { id = "sampleId" };
dynamic x = sample;
x.test = "test";
var copy = sample.ShallowCopy();
var selectedMembers = DynamicBaseMemberType.Dynamic | DynamicBaseMemberType.Instance;
@@ -0,0 +1,141 @@
using FluentAssertions;
using Microsoft.CSharp.RuntimeBinder;
using Speckle.Sdk.Host;
using Speckle.Sdk.Models;
namespace Speckle.Sdk.Tests.Unit.Models;
public class DynamicBaseTests
{
public DynamicBaseTests()
{
TypeLoader.Reset();
TypeLoader.Initialize(typeof(Base).Assembly, typeof(BaseTests).Assembly);
}
[Fact]
public void Indexer_SetAndGet()
{
// Arrange
var dynamicBase = new DynamicBase();
var key = "testProperty";
var value = "testValue";
// Act
dynamicBase[key] = value;
var result = dynamicBase[key];
// Assert
result.Should().Be(value);
}
[Fact]
public void DynamicProperty_SetAndGet()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
var value = "dynamicValue";
// Act
dynamicBase.dynamicProperty = value;
object result = dynamicBase.dynamicProperty;
// Assert
result.Should().Be(value);
}
[Fact]
public void GetMembers_Default()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
dynamicBase.dynamicProp = "hello";
// Act
IDictionary<string, object?> members = dynamicBase.GetMembers();
// Assert
members.Should().ContainKey("dynamicProp");
}
[Fact]
public void GetMembers_Instance()
{
// Arrange
var dynamicBase = new TestDynamicBase();
// Act
var members = dynamicBase.GetMembers(DynamicBaseMemberType.Instance);
// Assert
members.Should().ContainKey(nameof(TestDynamicBase.InstanceProperty));
members.Should().NotContainKey("dynamicProp");
}
[Fact]
public void GetDynamicMemberNames()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
dynamicBase.prop1 = 1;
dynamicBase.prop2 = "test";
// Act
IEnumerable<string> memberNames = dynamicBase.GetDynamicMemberNames();
// Assert
memberNames.Should().BeEquivalentTo(["DynamicPropertyKeys", "prop1", "prop2"]);
}
[Fact]
public void TryGetMember_Existing()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
dynamicBase.existingProp = "I exist";
// Act
var result = dynamicBase.existingProp;
// Assert
((object)result)
.Should()
.Be("I exist");
}
[Fact]
public void TryGetMember_NonExisting()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
// Act
Action act = () =>
{
var result = dynamicBase.nonExistingProp;
};
// Assert
act.Should().Throw<RuntimeBinderException>();
}
[Fact]
public void TrySetMember()
{
// Arrange
dynamic dynamicBase = new DynamicBase();
// Act
dynamicBase.newProp = "newValue";
// Assert
((object)dynamicBase.newProp)
.Should()
.Be("newValue");
}
private class TestDynamicBase : DynamicBase
{
public string InstanceProperty { get; set; } = "instance";
}
}
@@ -1,7 +1,7 @@
using FluentAssertions;
using Microsoft.Data.Sqlite;
using Speckle.Sdk.Caching;
using Speckle.Sdk.Common;
using Speckle.Sdk.Serialisation.Utilities;
using Speckle.Sdk.Transports;
namespace Speckle.Sdk.Tests.Unit.Transports;
@@ -13,7 +13,7 @@ public sealed class SQLiteTransport2Tests : TransportTests, IDisposable
private SQLiteTransport2? _sqlite;
private static readonly string s_name = $"test-{Guid.NewGuid()}";
private static readonly string s_basePath = SqlitePaths.GetDBPath(s_name);
private static readonly string s_basePath = ModelCacheManager.GetDbPath(s_name);
public SQLiteTransport2Tests()
{