diff --git a/.gitignore b/.gitignore index 9db8a680..de571653 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -.vs +.vs obj bin .vscode Tools/AutoClient/datapath .editorconfig -.DS_Store \ No newline at end of file +.DS_Store +dotnet-tools.json diff --git a/ProjectPlugins/LogosStorageClient/ILogosStorageAccess.cs b/ProjectPlugins/LogosStorageClient/ILogosStorageAccess.cs new file mode 100644 index 00000000..f6fcb447 --- /dev/null +++ b/ProjectPlugins/LogosStorageClient/ILogosStorageAccess.cs @@ -0,0 +1,31 @@ +using Logging; +using Utils; + +namespace LogosStorageClient +{ + public interface ILogosStorageAccess + { + string GetName(); + string GetImageName(); + DateTime GetStartUtc(); + DebugInfo GetDebugInfo(); + void SetLogLevel(string logLevel); + string GetSpr(); + DebugPeer GetDebugPeer(string peerId); + void ConnectToPeer(string peerId, string[] peerMultiAddresses); + string UploadFile(UploadInput uploadInput); + Stream DownloadFile(string contentId); + LocalDataset DownloadStreamless(ContentId cid); + LocalDataset DownloadManifestOnly(ContentId cid); + LocalDatasetList LocalFiles(); + LogosStorageSpace Space(); + Address GetDiscoveryEndpoint(); + Address GetApiEndpoint(); + Address GetListenEndpoint(); + Address? GetMetricsEndpoint(); + bool HasCrashed(); + void DeleteDataDirFolder(); + void Stop(bool waitTillStopped); + IDownloadedLog DownloadLog(string additionalName = ""); + } +} diff --git a/ProjectPlugins/LogosStorageClient/LogosStorageAccess.cs b/ProjectPlugins/LogosStorageClient/LogosStorageAccess.cs index 6a53b1f3..cc4853a1 100644 --- a/ProjectPlugins/LogosStorageClient/LogosStorageAccess.cs +++ b/ProjectPlugins/LogosStorageClient/LogosStorageAccess.cs @@ -7,7 +7,7 @@ using WebUtils; namespace LogosStorageClient { - public class LogosStorageAccess + public class LogosStorageAccess : ILogosStorageAccess { public const string API_BASE_URL = "/api/storage/v1"; diff --git a/ProjectPlugins/LogosStorageClient/StorageNode.cs b/ProjectPlugins/LogosStorageClient/StorageNode.cs index 861b08fd..c98722b1 100644 --- a/ProjectPlugins/LogosStorageClient/StorageNode.cs +++ b/ProjectPlugins/LogosStorageClient/StorageNode.cs @@ -53,10 +53,10 @@ namespace LogosStorageClient private readonly TransferSpeeds transferSpeeds; private string peerId = string.Empty; private string nodeId = string.Empty; - private readonly LogosStorageAccess logosStorageAccess; + private readonly ILogosStorageAccess logosStorageAccess; private readonly IFileManager fileManager; - public StorageNode(ILog log, LogosStorageAccess logosStorageAccess, IFileManager fileManager, IStorageNodeHooks hooks) + public StorageNode(ILog log, ILogosStorageAccess logosStorageAccess, IFileManager fileManager, IStorageNodeHooks hooks) { this.logosStorageAccess = logosStorageAccess; this.fileManager = fileManager; @@ -226,13 +226,11 @@ namespace LogosStorageClient public void ConnectToPeer(IStorageNode node) { - var peer = (StorageNode)node; - - Log($"Connecting to peer {peer.GetName()}..."); + Log($"Connecting to peer {node.GetName()}..."); var peerInfo = node.GetDebugInfo(); - logosStorageAccess.ConnectToPeer(peerInfo.Id, GetPeerMultiAddresses(peer, peerInfo)); + logosStorageAccess.ConnectToPeer(peerInfo.Id, GetPeerMultiAddresses(node, peerInfo)); - Log($"Successfully connected to peer {peer.GetName()}."); + Log($"Successfully connected to peer {node.GetName()}."); } public void DeleteDataDirFolder() @@ -307,7 +305,7 @@ namespace LogosStorageClient log.AddStringReplace(LogosStorageUtils.ToShortId(nodeId), nodeName); } - private string[] GetPeerMultiAddresses(StorageNode peer, DebugInfo peerInfo) + private string[] GetPeerMultiAddresses(IStorageNode peer, DebugInfo peerInfo) { var peerId = peer.GetDiscoveryEndpoint().Host .Replace("http://", "") diff --git a/Tests/FrameworkTests/LogosStorageClient/HooksTests.cs b/Tests/FrameworkTests/LogosStorageClient/HooksTests.cs new file mode 100644 index 00000000..fc7602fa --- /dev/null +++ b/Tests/FrameworkTests/LogosStorageClient/HooksTests.cs @@ -0,0 +1,125 @@ +using Moq; +using NUnit.Framework; +using LogosStorageClient; +using LogosStorageClient.Hooks; +using Utils; + +namespace FrameworkTests.LogosStorageClient +{ + [TestFixture] + public class MuxingStorageNodeHooksTests + { + private Mock hookA = null!; + private Mock hookB = null!; + private MuxingStorageNodeHooks mux = null!; + + [SetUp] + public void SetUp() + { + hookA = new Mock(); + hookB = new Mock(); + mux = new MuxingStorageNodeHooks(new[] { hookA.Object, hookB.Object }); + } + + [Test] + public void OnNodeStarting_DispatchesToAllHooks() + { + var start = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + mux.OnNodeStarting(start, "image:tag"); + hookA.Verify(h => h.OnNodeStarting(start, "image:tag"), Times.Once); + hookB.Verify(h => h.OnNodeStarting(start, "image:tag"), Times.Once); + } + + [Test] + public void OnNodeStarted_DispatchesToAllHooks() + { + var mockNode = new Mock(); + mux.OnNodeStarted(mockNode.Object, "peerId-abc", "nodeId-xyz"); + hookA.Verify(h => h.OnNodeStarted(mockNode.Object, "peerId-abc", "nodeId-xyz"), Times.Once); + hookB.Verify(h => h.OnNodeStarted(mockNode.Object, "peerId-abc", "nodeId-xyz"), Times.Once); + } + + [Test] + public void OnNodeStopping_DispatchesToAllHooks() + { + mux.OnNodeStopping(); + hookA.Verify(h => h.OnNodeStopping(), Times.Once); + hookB.Verify(h => h.OnNodeStopping(), Times.Once); + } + + [Test] + public void OnFileUploading_DispatchesToAllHooks() + { + var size = new ByteSize(1024); + mux.OnFileUploading("uid-1", size); + hookA.Verify(h => h.OnFileUploading("uid-1", size), Times.Once); + hookB.Verify(h => h.OnFileUploading("uid-1", size), Times.Once); + } + + [Test] + public void OnFileUploaded_DispatchesToAllHooks() + { + var size = new ByteSize(1024); + var cid = new ContentId("bafybeicid1"); + mux.OnFileUploaded("uid-1", size, cid); + hookA.Verify(h => h.OnFileUploaded("uid-1", size, cid), Times.Once); + hookB.Verify(h => h.OnFileUploaded("uid-1", size, cid), Times.Once); + } + + [Test] + public void OnFileDownloading_DispatchesToAllHooks() + { + var cid = new ContentId("bafybeicid1"); + mux.OnFileDownloading(cid); + hookA.Verify(h => h.OnFileDownloading(cid), Times.Once); + hookB.Verify(h => h.OnFileDownloading(cid), Times.Once); + } + + [Test] + public void OnFileDownloaded_DispatchesToAllHooks() + { + var size = new ByteSize(2048); + var cid = new ContentId("bafybeicid2"); + mux.OnFileDownloaded(size, cid); + hookA.Verify(h => h.OnFileDownloaded(size, cid), Times.Once); + hookB.Verify(h => h.OnFileDownloaded(size, cid), Times.Once); + } + } + + [TestFixture] + public class LogosStorageHooksFactoryTests + { + [Test] + public void CreateHooks_NoProviders_ReturnsDoNothingHooks() + { + var factory = new LogosStorageHooksFactory(); + var hooks = factory.CreateHooks("test-node"); + Assert.That(hooks, Is.InstanceOf()); + } + + [Test] + public void CreateHooks_OneProvider_ReturnsMuxingHooks() + { + var mockHooks = new Mock(); + var mockProvider = new Mock(); + mockProvider.Setup(p => p.CreateHooks("my-node")).Returns(mockHooks.Object); + + var factory = new LogosStorageHooksFactory(); + factory.Providers.Add(mockProvider.Object); + + var result = factory.CreateHooks("my-node"); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void CreateHooks_MultipleProviders_ReturnsMuxingHooks() + { + var factory = new LogosStorageHooksFactory(); + factory.Providers.Add(new DoNothingHooksProvider()); + factory.Providers.Add(new DoNothingHooksProvider()); + + var result = factory.CreateHooks("my-node"); + Assert.That(result, Is.InstanceOf()); + } + } +} diff --git a/Tests/FrameworkTests/LogosStorageClient/LogosStorageLogLineTests.cs b/Tests/FrameworkTests/LogosStorageClient/LogosStorageLogLineTests.cs new file mode 100644 index 00000000..8c335771 --- /dev/null +++ b/Tests/FrameworkTests/LogosStorageClient/LogosStorageLogLineTests.cs @@ -0,0 +1,95 @@ +using NUnit.Framework; +using LogosStorageClient; + +namespace FrameworkTests.LogosStorageClient +{ + [TestFixture] + public class LogosStorageLogLineTests + { + [Test] + public void Parse_NullOrEmpty_ReturnsNull() + { + Assert.That(LogosStorageLogLine.Parse(string.Empty), Is.Null); + Assert.That(LogosStorageLogLine.Parse(null!), Is.Null); + } + + [Test] + public void Parse_TooShort_ReturnsNull() + { + Assert.That(LogosStorageLogLine.Parse("INF 2024-01-01 00:00:00.000"), Is.Null); + } + + [Test] + public void Parse_ValidLine_ReturnsCorrectFields() + { + // Real Nim log format: LVL yyyy-MM-dd HH:mm:ss.fff+00:00 message attrs + // position 3 = space after level, position 33 = space before message + var line = "INF 2024-03-15 12:34:56.789+00:00 Node started successfully topic=storage"; + + var result = LogosStorageLogLine.Parse(line); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.LogLevel, Is.EqualTo("INF")); + Assert.That(result.TimestampUtc, Is.EqualTo(new DateTime(2024, 3, 15, 12, 34, 56, 789, DateTimeKind.Utc))); + Assert.That(result.Message, Is.EqualTo("Node started successfully")); + } + + [Test] + public void Parse_SimpleAttribute_ParsedCorrectly() + { + var line = "ERR 2024-03-15 12:34:56.789+00:00 Upload failed reason=timeout"; + + var result = LogosStorageLogLine.Parse(line); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Attributes.ContainsKey("reason"), Is.True); + Assert.That(result.Attributes["reason"], Is.EqualTo("timeout")); + } + + [Test] + public void Parse_QuotedAttributeValue_ParsedCorrectly() + { + var line = "WRN 2024-03-15 12:34:56.789+00:00 Slow download path=\"/data dir/file.bin\""; + + var result = LogosStorageLogLine.Parse(line); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Attributes["path"], Is.EqualTo("/data dir/file.bin")); + } + + [Test] + public void Parse_MultipleAttributes_AllParsed() + { + var line = "DBG 2024-03-15 12:34:56.789+00:00 Peer discovered peerId=abc nodeId=xyz seen=true"; + + var result = LogosStorageLogLine.Parse(line); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Attributes["peerId"], Is.EqualTo("abc")); + Assert.That(result.Attributes["nodeId"], Is.EqualTo("xyz")); + Assert.That(result.Attributes["seen"], Is.EqualTo("true")); + } + + [Test] + public void Parse_InvalidTimestamp_ReturnsNull() + { + // Passes position-3 and position-33 space checks, but the 23-char timestamp + // substring (positions 4-26) is not a parseable datetime → returns null. + // 29 X's fill positions 4-32, space at 33, then message with attributes. + var line = "INF XXXXXXXXXXXXXXXXXXXXXXXXXXXXX message key=value"; + Assert.That(LogosStorageLogLine.Parse(line), Is.Null); + } + + [Test] + [TestCase("INF")] + [TestCase("ERR")] + [TestCase("WRN")] + [TestCase("DBG")] + public void Parse_LogLevel_IsPreserved(string level) + { + var line = $"{level} 2024-03-15 12:34:56.789+00:00 Message key=value"; + var result = LogosStorageLogLine.Parse(line); + Assert.That(result?.LogLevel, Is.EqualTo(level)); + } + } +} diff --git a/Tests/FrameworkTests/LogosStorageClient/MapperTests.cs b/Tests/FrameworkTests/LogosStorageClient/MapperTests.cs new file mode 100644 index 00000000..dde88fc1 --- /dev/null +++ b/Tests/FrameworkTests/LogosStorageClient/MapperTests.cs @@ -0,0 +1,134 @@ +using NUnit.Framework; +using LogosStorageClient; +using StorageOpenApi; + +namespace FrameworkTests.LogosStorageClient +{ + [TestFixture] + public class MapperTests + { + private Mapper mapper = null!; + + [SetUp] + public void SetUp() + { + mapper = new Mapper(); + } + + [Test] + public void Map_DebugInfo_MapsAllFields() + { + var openApiDebugInfo = new StorageOpenApi.DebugInfo + { + Id = "peer-123", + Spr = "spr-abc", + Addrs = new List { "/ip4/1.2.3.4/tcp/8080" }, + AnnounceAddresses = new List { "/ip4/5.6.7.8/tcp/8080" }, + Storage = new StorageVersion { Version = "1.0.0", Revision = "abc123" }, + Table = new PeersTable + { + LocalNode = new Node { NodeId = "nodeId-1", PeerId = "peerId-1" }, + Nodes = new List() + } + }; + + var result = mapper.Map(openApiDebugInfo); + + Assert.That(result.Id, Is.EqualTo("peer-123")); + Assert.That(result.Spr, Is.EqualTo("spr-abc")); + Assert.That(result.Addrs, Is.EqualTo(new[] { "/ip4/1.2.3.4/tcp/8080" })); + Assert.That(result.AnnounceAddresses, Is.EqualTo(new[] { "/ip4/5.6.7.8/tcp/8080" })); + Assert.That(result.Version.Version, Is.EqualTo("1.0.0")); + Assert.That(result.Version.Revision, Is.EqualTo("abc123")); + Assert.That(result.Table.LocalNode.NodeId, Is.EqualTo("nodeId-1")); + } + + [Test] + public void Map_Space_MapsAllBytes() + { + var openApiSpace = new StorageOpenApi.Space + { + QuotaMaxBytes = 10_000_000, + QuotaUsedBytes = 3_000_000, + QuotaReservedBytes = 1_000_000, + TotalBlocks = 42 + }; + + var result = mapper.Map(openApiSpace); + + Assert.That(result.QuotaMaxBytes, Is.EqualTo(10_000_000)); + Assert.That(result.QuotaUsedBytes, Is.EqualTo(3_000_000)); + Assert.That(result.QuotaReservedBytes, Is.EqualTo(1_000_000)); + Assert.That(result.TotalBlocks, Is.EqualTo(42)); + } + + [Test] + public void Map_DataList_MapsContent() + { + var openApiDataList = new StorageOpenApi.DataList + { + Content = new List + { + new DataItem + { + Cid = "bafybeicid1", + Manifest = new ManifestItem + { + TreeCid = "rootHashXYZ", + DatasetSize = 1024, + BlockSize = 512 + } + } + } + }; + + var result = mapper.Map(openApiDataList); + + Assert.That(result.Content.Length, Is.EqualTo(1)); + Assert.That(result.Content[0].Cid.Id, Is.EqualTo("bafybeicid1")); + Assert.That(result.Content[0].Manifest.RootHash, Is.EqualTo("rootHashXYZ")); + Assert.That(result.Content[0].Manifest.DatasetSize.SizeInBytes, Is.EqualTo(1024)); + Assert.That(result.Content[0].Manifest.BlockSize.SizeInBytes, Is.EqualTo(512)); + } + + [Test] + public void Map_DebugInfoTable_WithPeers_MapsNodes() + { + var openApiDebugInfo = new StorageOpenApi.DebugInfo + { + Id = "x", + Spr = "x", + Addrs = new List(), + AnnounceAddresses = new List(), + Storage = new StorageVersion { Version = "1", Revision = "r" }, + Table = new PeersTable + { + LocalNode = new Node { NodeId = "local-nodeId", PeerId = "local-peerId" }, + Nodes = new List + { + new Node { NodeId = "node-1", PeerId = "peer-1", Seen = true } + } + } + }; + + var result = mapper.Map(openApiDebugInfo); + + Assert.That(result.Table.Nodes.Length, Is.EqualTo(1)); + Assert.That(result.Table.Nodes[0].PeerId, Is.EqualTo("peer-1")); + Assert.That(result.Table.Nodes[0].Seen, Is.True); + } + + [Test] + public void Map_DataList_EmptyContent_ReturnsEmptyArray() + { + var openApiDataList = new StorageOpenApi.DataList + { + Content = new List() + }; + + var result = mapper.Map(openApiDataList); + + Assert.That(result.Content, Is.Empty); + } + } +} diff --git a/Tests/FrameworkTests/LogosStorageClient/StorageNodeTests.cs b/Tests/FrameworkTests/LogosStorageClient/StorageNodeTests.cs new file mode 100644 index 00000000..40a6af07 --- /dev/null +++ b/Tests/FrameworkTests/LogosStorageClient/StorageNodeTests.cs @@ -0,0 +1,207 @@ +using Moq; +using NUnit.Framework; +using LogosStorageClient; +using LogosStorageClient.Hooks; +using FileUtils; +using Logging; +using Utils; + +namespace FrameworkTests.LogosStorageClient +{ + [TestFixture] + public class StorageNodeTests + { + private Mock mockAccess = null!; + private Mock mockHooks = null!; + private Mock mockFileManager = null!; + private StorageNode node = null!; + private NullLog nullLog = null!; + + [SetUp] + public void SetUp() + { + mockAccess = new Mock(); + mockHooks = new Mock(); + mockFileManager = new Mock(); + nullLog = new NullLog(); + + mockAccess.Setup(a => a.GetName()).Returns("test-node"); + mockAccess.Setup(a => a.GetStartUtc()).Returns(DateTime.UtcNow); + mockAccess.Setup(a => a.GetImageName()).Returns("test-image"); + + node = new StorageNode(nullLog, mockAccess.Object, mockFileManager.Object, mockHooks.Object); + } + + // --- GetName --- + + [Test] + public void GetName_DelegatesToAccess() + { + Assert.That(node.GetName(), Is.EqualTo("test-node")); + } + + // --- UploadFile --- + + [Test] + public void UploadFile_ValidResponse_ReturnsCid() + { + using var tmpFile = new TempFile(); + File.WriteAllText(tmpFile.Path, "hello world"); + var trackedFile = new TrackedFile(nullLog, tmpFile.Path, "test-file"); + + mockAccess.Setup(a => a.UploadFile(It.IsAny())).Returns("bafybeicid123"); + + var cid = node.UploadFile(trackedFile); + + Assert.That(cid.Id, Is.EqualTo("bafybeicid123")); + } + + [Test] + public void UploadFile_FiresOnFileUploadingAndOnFileUploaded() + { + using var tmpFile = new TempFile(); + File.WriteAllText(tmpFile.Path, "data"); + var trackedFile = new TrackedFile(nullLog, tmpFile.Path, "test-file"); + mockAccess.Setup(a => a.UploadFile(It.IsAny())).Returns("bafybeicid999"); + + node.UploadFile(trackedFile); + + mockHooks.Verify(h => h.OnFileUploading(It.IsAny(), It.IsAny()), Times.Once); + mockHooks.Verify(h => h.OnFileUploaded( + It.IsAny(), + It.IsAny(), + It.Is(c => c.Id == "bafybeicid999")), Times.Once); + } + + [Test] + public void UploadFile_EmptyResponse_Throws() + { + using var tmpFile = new TempFile(); + File.WriteAllText(tmpFile.Path, "data"); + var trackedFile = new TrackedFile(nullLog, tmpFile.Path, "test-file"); + mockAccess.Setup(a => a.UploadFile(It.IsAny())).Returns(string.Empty); + + Assert.Throws(() => node.UploadFile(trackedFile)); + } + + [Test] + public void UploadFile_StoreBlockFailure_Throws() + { + using var tmpFile = new TempFile(); + File.WriteAllText(tmpFile.Path, "data"); + var trackedFile = new TrackedFile(nullLog, tmpFile.Path, "test-file"); + mockAccess.Setup(a => a.UploadFile(It.IsAny())).Returns("Unable to store block"); + + Assert.Throws(() => node.UploadFile(trackedFile)); + } + + // --- Stop --- + + [Test] + public void Stop_FiresOnNodeStoppingAndCallsAccessStop() + { + node.Stop(waitTillStopped: true); + + mockHooks.Verify(h => h.OnNodeStopping(), Times.Once); + mockAccess.Verify(a => a.Stop(true), Times.Once); + } + + // --- Initialize --- + + [Test] + public void Initialize_ValidDebugInfo_SetsVersion() + { + mockAccess.Setup(a => a.GetDebugInfo()).Returns(new DebugInfo + { + Id = "peer-abc", + Table = new DebugInfoTable { LocalNode = new DebugInfoTableNode { NodeId = "node-xyz" } }, + Version = new DebugInfoVersion { Version = "1.0.0", Revision = "rev123" } + }); + + node.Initialize(); + + Assert.That(node.Version.Version, Is.EqualTo("1.0.0")); + Assert.That(node.Version.Revision, Is.EqualTo("rev123")); + Assert.That(node.GetPeerId(), Is.EqualTo("peer-abc")); + } + + [Test] + public void Initialize_FiresOnNodeStarted() + { + mockAccess.Setup(a => a.GetDebugInfo()).Returns(new DebugInfo + { + Id = "peer-abc", + Table = new DebugInfoTable { LocalNode = new DebugInfoTableNode { NodeId = "node-xyz" } }, + Version = new DebugInfoVersion { Version = "1.0.0", Revision = "rev123" } + }); + + node.Initialize(); + + mockHooks.Verify(h => h.OnNodeStarted(node, "peer-abc", "node-xyz"), Times.Once); + } + + // --- WaitUntilQuotaUsedIncreased --- + + [Test] + public void WaitUntilQuotaUsedIncreased_QuotaAlreadyMet_DoesNotThrow() + { + var startSpace = new LogosStorageSpace { QuotaUsedBytes = 0, QuotaMaxBytes = 1_000_000, QuotaReservedBytes = 0 }; + var afterSpace = new LogosStorageSpace { QuotaUsedBytes = 500, QuotaMaxBytes = 1_000_000, QuotaReservedBytes = 0 }; + mockAccess.Setup(a => a.Space()).Returns(afterSpace); + + Assert.DoesNotThrow(() => + node.WaitUntilQuotaUsedIncreased(startSpace, new ByteSize(400), TimeSpan.FromSeconds(5))); + } + + // --- ConnectToPeer --- + + [Test] + public void ConnectToPeer_ReplacesZeroIPWithPeerDiscoveryHost() + { + var mockPeerNode = new Mock(); + mockPeerNode.Setup(n => n.GetName()).Returns("peer-node"); + mockPeerNode.Setup(n => n.GetDiscoveryEndpoint()) + .Returns(new Address("peer-node", "http://10.0.0.2", 1234)); + mockPeerNode.Setup(n => n.GetDebugInfo(It.IsAny())).Returns(new DebugInfo + { + Id = "peer-id-xyz", + Addrs = new[] { "/ip4/0.0.0.0/tcp/8080", "/ip4/0.0.0.0/tcp/9090" }, + Table = new DebugInfoTable { LocalNode = new DebugInfoTableNode() }, + Version = new DebugInfoVersion { Version = "1.0", Revision = "r1" } + }); + + node.ConnectToPeer(mockPeerNode.Object); + + mockAccess.Verify(a => a.ConnectToPeer( + "peer-id-xyz", + It.Is(addrs => + addrs[0] == "/ip4/10.0.0.2/tcp/8080" && + addrs[1] == "/ip4/10.0.0.2/tcp/9090")), + Times.Once); + } + + // --- TransferSpeeds --- + + [Test] + public void TransferSpeeds_InitiallyEmpty() + { + Assert.That(node.TransferSpeeds.GetUploadSpeed(), Is.Null); + Assert.That(node.TransferSpeeds.GetDownloadSpeed(), Is.Null); + } + + // --- HasCrashed / Space / GetPeerId --- + + [Test] + public void HasCrashed_DelegatesToAccess() + { + mockAccess.Setup(a => a.HasCrashed()).Returns(true); + Assert.That(node.HasCrashed(), Is.True); + } + } + + internal class TempFile : IDisposable + { + public string Path { get; } = System.IO.Path.GetTempFileName(); + public void Dispose() => File.Delete(Path); + } +} diff --git a/Tests/FrameworkTests/LogosStorageClient/StorageTypesTests.cs b/Tests/FrameworkTests/LogosStorageClient/StorageTypesTests.cs new file mode 100644 index 00000000..8e2f50c1 --- /dev/null +++ b/Tests/FrameworkTests/LogosStorageClient/StorageTypesTests.cs @@ -0,0 +1,105 @@ +using NUnit.Framework; +using LogosStorageClient; + +namespace FrameworkTests.LogosStorageClient +{ + [TestFixture] + public class DebugInfoVersionTests + { + [Test] + public void IsValid_BothEmpty_ReturnsFalse() + { + var v = new DebugInfoVersion(); + Assert.That(v.IsValid(), Is.False); + } + + [Test] + public void IsValid_VersionEmptyRevisionSet_ReturnsFalse() + { + var v = new DebugInfoVersion { Version = string.Empty, Revision = "abc123" }; + Assert.That(v.IsValid(), Is.False); + } + + [Test] + public void IsValid_RevisionEmptyVersionSet_ReturnsFalse() + { + var v = new DebugInfoVersion { Version = "1.0.0", Revision = string.Empty }; + Assert.That(v.IsValid(), Is.False); + } + + [Test] + public void IsValid_BothSet_ReturnsTrue() + { + var v = new DebugInfoVersion { Version = "1.0.0", Revision = "abc123" }; + Assert.That(v.IsValid(), Is.True); + } + } + + [TestFixture] + public class LogosStorageSpaceTests + { + [Test] + public void FreeBytes_IsQuotaMax_MinusUsedAndReserved() + { + var space = new LogosStorageSpace + { + QuotaMaxBytes = 1000, + QuotaUsedBytes = 300, + QuotaReservedBytes = 100 + }; + Assert.That(space.FreeBytes, Is.EqualTo(600)); + } + + [Test] + public void FreeBytes_NothingUsed_EqualToMax() + { + var space = new LogosStorageSpace { QuotaMaxBytes = 5000, QuotaUsedBytes = 0, QuotaReservedBytes = 0 }; + Assert.That(space.FreeBytes, Is.EqualTo(5000)); + } + + [Test] + public void FreeBytes_ReservedAloneReducesFreeSpace() + { + var space = new LogosStorageSpace { QuotaMaxBytes = 1000, QuotaUsedBytes = 0, QuotaReservedBytes = 400 }; + Assert.That(space.FreeBytes, Is.EqualTo(600)); + } + } + + [TestFixture] + public class LogosStorageUtilsTests + { + [Test] + public void ToShortId_ShortString_ReturnedUnchanged() + { + Assert.That(LogosStorageUtils.ToShortId("abc"), Is.EqualTo("abc")); + } + + [Test] + public void ToShortId_ExactlyTenChars_ReturnedUnchanged() + { + Assert.That(LogosStorageUtils.ToShortId("abcde12345"), Is.EqualTo("abcde12345")); + } + + [Test] + public void ToShortId_LongString_ReturnsFirst3StarLast6() + { + // "abcde12345678xyz" (16 chars): id[..3]="abc", id[^6..]="678xyz" + var result = LogosStorageUtils.ToShortId("abcde12345678xyz"); + Assert.That(result, Is.EqualTo("abc*678xyz")); + } + + [Test] + public void ToNodeIdShortId_ShortString_ReturnedUnchanged() + { + Assert.That(LogosStorageUtils.ToNodeIdShortId("abc"), Is.EqualTo("abc")); + } + + [Test] + public void ToNodeIdShortId_LongString_ReturnsFirst2StarLast6() + { + // "abcde12345678xyz" (16 chars): id[..2]="ab", id[^6..]="678xyz" + var result = LogosStorageUtils.ToNodeIdShortId("abcde12345678xyz"); + Assert.That(result, Is.EqualTo("ab*678xyz")); + } + } +} diff --git a/Tests/FrameworkTests/LogosStorageClient/TransferSpeedsTests.cs b/Tests/FrameworkTests/LogosStorageClient/TransferSpeedsTests.cs new file mode 100644 index 00000000..f207f2d3 --- /dev/null +++ b/Tests/FrameworkTests/LogosStorageClient/TransferSpeedsTests.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using LogosStorageClient; +using Utils; + +namespace FrameworkTests.LogosStorageClient +{ + [TestFixture] + public class TransferSpeedsTests + { + private TransferSpeeds speeds; + + [SetUp] + public void SetUp() + { + speeds = new TransferSpeeds(); + } + + [Test] + public void GetUploadSpeed_NoSamples_ReturnsNull() + { + Assert.That(speeds.GetUploadSpeed(), Is.Null); + } + + [Test] + public void GetDownloadSpeed_NoSamples_ReturnsNull() + { + Assert.That(speeds.GetDownloadSpeed(), Is.Null); + } + + [Test] + public void GetUploadSpeed_OneSample_ReturnsCorrectSpeed() + { + // 1000 bytes in 2 seconds = 500 bytes/sec + speeds.AddUploadSample(new ByteSize(1000), TimeSpan.FromSeconds(2)); + Assert.That(speeds.GetUploadSpeed()!.SizeInBytes, Is.EqualTo(500)); + } + + [Test] + public void GetDownloadSpeed_OneSample_ReturnsCorrectSpeed() + { + // 3000 bytes in 3 seconds = 1000 bytes/sec + speeds.AddDownloadSample(new ByteSize(3000), TimeSpan.FromSeconds(3)); + Assert.That(speeds.GetDownloadSpeed()!.SizeInBytes, Is.EqualTo(1000)); + } + + [Test] + public void GetUploadSpeed_MultipleSamples_ReturnsAverage() + { + // 1000/1s = 1000, 3000/3s = 1000 → avg = 1000 + speeds.AddUploadSample(new ByteSize(1000), TimeSpan.FromSeconds(1)); + speeds.AddUploadSample(new ByteSize(3000), TimeSpan.FromSeconds(3)); + Assert.That(speeds.GetUploadSpeed()!.SizeInBytes, Is.EqualTo(1000)); + } + + [Test] + public void UploadAndDownload_Independent() + { + speeds.AddUploadSample(new ByteSize(2000), TimeSpan.FromSeconds(1)); + speeds.AddDownloadSample(new ByteSize(500), TimeSpan.FromSeconds(1)); + + Assert.That(speeds.GetUploadSpeed()!.SizeInBytes, Is.EqualTo(2000)); + Assert.That(speeds.GetDownloadSpeed()!.SizeInBytes, Is.EqualTo(500)); + } + + [Test] + public void Combine_WithNull_ReturnsSelf() + { + speeds.AddUploadSample(new ByteSize(1000), TimeSpan.FromSeconds(1)); + var combined = speeds.Combine(null); + Assert.That(combined.GetUploadSpeed()!.SizeInBytes, Is.EqualTo(1000)); + } + + [Test] + public void Combine_TwoSpeeds_MergesAndAverages() + { + speeds.AddUploadSample(new ByteSize(1000), TimeSpan.FromSeconds(1)); // 1000 b/s + var other = new TransferSpeeds(); + other.AddUploadSample(new ByteSize(3000), TimeSpan.FromSeconds(1)); // 3000 b/s + + var combined = speeds.Combine(other); + // avg(1000, 3000) = 2000 + Assert.That(combined.GetUploadSpeed()!.SizeInBytes, Is.EqualTo(2000)); + } + } +}