using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using FrostFS.SDK.Client; using FrostFS.SDK.Cryptography; using Microsoft.Extensions.Options; namespace FrostFS.SDK.SmokeTests; [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] [SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] public class PoolSmokeTests : SmokeTestsBase { private static readonly PrmWait lightWait = new(100, 1); private InitParameters GetDefaultParams() { return new InitParameters { Key = keyString.LoadWif(), NodeParams = [new(1, this.url, 100.0f)], ClientBuilder = null, GracefulCloseOnSwitchTimeout = 30_000_000, Logger = null }; } [Fact] public async void NetworkMapTest() { var options = GetDefaultParams(); using var pool = new Pool(options); var error = await pool.Dial(new CallContext()); Assert.Null(error); var result = await pool.GetNetmapSnapshotAsync(default); Assert.True(result.Epoch > 0); Assert.Single(result.NodeInfoCollection); var item = result.NodeInfoCollection[0]; Assert.Equal(2, item.Version.Major); Assert.Equal(13, item.Version.Minor); Assert.Equal(NodeState.Online, item.State); Assert.True(item.PublicKey.Length > 0); Assert.Single(item.Addresses); Assert.Equal(9, item.Attributes.Count); } [Fact] public async void NodeInfoTest() { var options = GetDefaultParams(); using var pool = new Pool(options); var error = await pool.Dial(new CallContext()); Assert.Null(error); var result = await pool.GetNodeInfoAsync(); Assert.Equal(2, result.Version.Major); Assert.Equal(13, result.Version.Minor); Assert.Equal(NodeState.Online, result.State); Assert.Equal(33, result.PublicKey.Length); Assert.Single(result.Addresses); Assert.Equal(9, result.Attributes.Count); } [Fact] public async void NodeInfoStatisticsTwoNodesTest() { var callbackText = string.Empty; var options = new InitParameters { Key = keyString.LoadWif(), NodeParams = [ new(1, this.url, 100.0f), new(2, this.url.Replace('0', '1'), 100.0f) ], ClientBuilder = null, GracefulCloseOnSwitchTimeout = 30_000_000, Logger = null, Callback = (cs) => callbackText = $"{cs.MethodName} took {cs.ElapsedMicroSeconds} microseconds" }; using var pool = new Pool(options); var ctx = new CallContext { }; var error = await pool.Dial(ctx).ConfigureAwait(true); Assert.Null(error); using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); var result = await client.GetNodeInfoAsync(); var statistics = pool.Statistic(); Assert.False(string.IsNullOrEmpty(callbackText)); Assert.Contains(" took ", callbackText, StringComparison.Ordinal); } [Fact] public async void NodeInfoStatisticsTest() { var options = GetDefaultParams(); var callbackText = string.Empty; options.Callback = (cs) => callbackText = $"{cs.MethodName} took {cs.ElapsedMicroSeconds} microseconds"; using var pool = new Pool(options); var ctx = new CallContext(); var error = await pool.Dial(ctx).ConfigureAwait(true); Assert.Null(error); using var client = FrostFSClient.GetSingleOwnerInstance(GetSingleOwnerOptions(this.keyString, this.url)); var result = await client.GetNodeInfoAsync(); Assert.False(string.IsNullOrEmpty(callbackText)); Assert.Contains(" took ", callbackText, StringComparison.Ordinal); } [Fact] public async void GetSessionTest() { var options = GetDefaultParams(); using var pool = new Pool(options); var error = await pool.Dial(new CallContext()).ConfigureAwait(true); Assert.Null(error); var prm = new PrmSessionCreate(100); var token = await pool.CreateSessionAsync(prm).ConfigureAwait(true); var ownerHash = Base58.Decode(OwnerId!.Value); Assert.NotNull(token); Assert.NotEqual(Guid.Empty, token.Id); Assert.Equal(33, token.SessionKey.Length); } [Fact] public async void CreateObjectWithSessionToken() { var options = GetDefaultParams(); using var pool = new Pool(options); var error = await pool.Dial(new CallContext()).ConfigureAwait(true); Assert.Null(error); await Cleanup(pool); var token = await pool.CreateSessionAsync(new PrmSessionCreate(int.MaxValue)); var createContainerParam = new PrmContainerCreate( new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))); createContainerParam.XHeaders.Add("key1", "value1"); var containerId = await pool.CreateContainerAsync(createContainerParam); var bytes = GetRandomBytes(1024); var param = new PrmObjectPut { Header = new FrostFsObjectHeader( containerId: containerId, type: FrostFsObjectType.Regular, [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), ClientCut = false, SessionToken = token }; var objectId = await pool.PutObjectAsync(param).ConfigureAwait(true); var @object = await pool.GetObjectAsync(new PrmObjectGet(containerId, objectId)); var downloadedBytes = new byte[@object.Header.PayloadLength]; MemoryStream ms = new(downloadedBytes); ReadOnlyMemory? chunk = null; while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) { ms.Write(chunk.Value.Span); } Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); await Cleanup(pool); } [Fact] public async void FilterTest() { var options = GetDefaultParams(); using var pool = new Pool(options); var error = await pool.Dial(new CallContext()).ConfigureAwait(true); Assert.Null(error); await Cleanup(pool); var createContainerParam = new PrmContainerCreate( new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) { WaitParams = lightWait }; var containerId = await pool.CreateContainerAsync(createContainerParam); var bytes = new byte[] { 1, 2, 3 }; var ParentHeader = new FrostFsObjectHeader( containerId: containerId, type: FrostFsObjectType.Regular) { PayloadLength = 3 }; var param = new PrmObjectPut { Header = new FrostFsObjectHeader( containerId: containerId, type: FrostFsObjectType.Regular, [new FrostFsAttributePair("fileName", "test")], new FrostFsSplit()), Payload = new MemoryStream(bytes), ClientCut = false }; var objectId = await pool.PutObjectAsync(param); var head = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objectId)); var ecdsaKey = this.keyString.LoadWif(); var networkInfo = await pool.GetNetmapSnapshotAsync(); await CheckFilter(pool, containerId, new FilterByContainerId(FrostFsMatchType.Equals, containerId)); await CheckFilter(pool, containerId, new FilterByOwnerId(FrostFsMatchType.Equals, FrostFsOwner.FromKey(ecdsaKey))); await CheckFilter(pool, containerId, new FilterBySplitId(FrostFsMatchType.Equals, param.Header.Split!.SplitId)); await CheckFilter(pool, containerId, new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test")); await CheckFilter(pool, containerId, new FilterByObjectId(FrostFsMatchType.Equals, objectId)); await CheckFilter(pool, containerId, new FilterByVersion(FrostFsMatchType.Equals, networkInfo.NodeInfoCollection[0].Version)); await CheckFilter(pool, containerId, new FilterByEpoch(FrostFsMatchType.Equals, networkInfo.Epoch)); await CheckFilter(pool, containerId, new FilterByPayloadLength(FrostFsMatchType.Equals, 3)); var checkSum = CheckSum.CreateCheckSum(bytes); await CheckFilter(pool, containerId, new FilterByPayloadHash(FrostFsMatchType.Equals, checkSum)); await CheckFilter(pool, containerId, new FilterByPhysicallyStored()); } private static async Task CheckFilter(Pool pool, FrostFsContainerId containerId, IObjectFilter filter) { var resultObjectsCount = 0; PrmObjectSearch searchParam = new(containerId) { Filters = [filter] }; await foreach (var objId in pool.SearchObjectsAsync(searchParam)) { resultObjectsCount++; var objHeader = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objId)); } Assert.True(0 < resultObjectsCount, $"Filter for {filter.Key} doesn't work"); } [Theory] [InlineData(1)] [InlineData(3 * 1024 * 1024)] // exactly one chunk size - 3MB [InlineData(6 * 1024 * 1024 + 100)] public async void SimpleScenarioTest(int objectSize) { bool callbackInvoked = false; var options = GetDefaultParams(); options.Callback = new((CallStatistics cs) => { callbackInvoked = true; Assert.True(cs.ElapsedMicroSeconds > 0); }); using var pool = new Pool(options); var error = await pool.Dial(new CallContext()).ConfigureAwait(true); Assert.Null(error); await Cleanup(pool); var ctx = new CallContext { }; var createContainerParam = new PrmContainerCreate( new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)), [new("testKey", "testValue")]), ctx); var createdContainer = await pool.CreateContainerAsync(createContainerParam); var container = await pool.GetContainerAsync(new PrmContainerGet(createdContainer)); Assert.NotNull(container); Assert.True(callbackInvoked); var bytes = GetRandomBytes(objectSize); var param = new PrmObjectPut() { Header = new FrostFsObjectHeader( containerId: createdContainer, type: FrostFsObjectType.Regular, [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), ClientCut = false }; var objectId = await pool.PutObjectAsync(param); var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); bool hasObject = false; await foreach (var objId in pool.SearchObjectsAsync(new PrmObjectSearch(createdContainer) { Filters = [filter] })) { hasObject = true; var objHeader = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(createdContainer, objectId)); Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); Assert.NotNull(objHeader.Attributes); Assert.Single(objHeader.Attributes!); Assert.Equal("fileName", objHeader.Attributes!.First().Key); Assert.Equal("test", objHeader.Attributes!.First().Value); } Assert.True(hasObject); var @object = await pool.GetObjectAsync(new PrmObjectGet(createdContainer, objectId)); var downloadedBytes = new byte[@object.Header.PayloadLength]; MemoryStream ms = new(downloadedBytes); ReadOnlyMemory? chunk = null; while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) { ms.Write(chunk.Value.Span); } Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); await Cleanup(pool); await foreach (var _ in pool.ListContainersAsync()) { Assert.Fail("Containers exist"); } } [Theory] [InlineData(1)] [InlineData(3 * 1024 * 1024)] // exactly one chunk size - 3MB [InlineData(6 * 1024 * 1024 + 100)] public async void SimpleScenarioWithSessionTest(int objectSize) { var options = GetDefaultParams(); using var pool = new Pool(options); options.Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)); var error = await pool.Dial(new CallContext()).ConfigureAwait(true); Assert.Null(error); var token = await pool.CreateSessionAsync(new PrmSessionCreate(int.MaxValue)); await Cleanup(pool); var ctx = new CallContext { Timeout = TimeSpan.FromSeconds(20) }; var createContainerParam = new PrmContainerCreate( new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1))), ctx); var container = await pool.CreateContainerAsync(createContainerParam); var containerInfo = await pool.GetContainerAsync(new PrmContainerGet(container, ctx)); Assert.NotNull(containerInfo); var bytes = GetRandomBytes(objectSize); var param = new PrmObjectPut(new CallContext()) { Header = new FrostFsObjectHeader( containerId: container, type: FrostFsObjectType.Regular, [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), ClientCut = false, SessionToken = token }; var objectId = await pool.PutObjectAsync(param); var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); bool hasObject = false; await foreach (var objId in pool.SearchObjectsAsync(new PrmObjectSearch(container) { Filters = [filter], SessionToken = token })) { hasObject = true; var objHeader = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(container, objectId) { SessionToken = token }); Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); Assert.NotNull(objHeader.Attributes); Assert.Single(objHeader.Attributes); Assert.Equal("fileName", objHeader.Attributes.First().Key); Assert.Equal("test", objHeader.Attributes.First().Value); } Assert.True(hasObject); var @object = await pool.GetObjectAsync(new PrmObjectGet(container, objectId) { SessionToken = token }); var downloadedBytes = new byte[@object.Header.PayloadLength]; MemoryStream ms = new(downloadedBytes); ReadOnlyMemory? chunk = null; while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) { ms.Write(chunk.Value.Span); } Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); await Cleanup(pool); await foreach (var _ in pool.ListContainersAsync()) { Assert.Fail("Containers exist"); } } [Theory] [InlineData(1)] [InlineData(64 * 1024 * 1024)] // exactly 1 block size - 64MB [InlineData(64 * 1024 * 1024 - 1)] [InlineData(64 * 1024 * 1024 + 1)] [InlineData(2 * 64 * 1024 * 1024 + 256)] [InlineData(200)] public async void ClientCutScenarioTest(int objectSize) { var options = GetDefaultParams(); using var pool = new Pool(options); var error = await pool.Dial(new CallContext()).ConfigureAwait(true); Assert.Null(error); await Cleanup(pool); var createContainerParam = new PrmContainerCreate(new FrostFsContainerInfo(new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))) { WaitParams = lightWait }; var containerId = await pool.CreateContainerAsync(createContainerParam); var ctx = new CallContext { Timeout = TimeSpan.FromSeconds(10), }; // ctx.Interceptors.Add(new CallbackInterceptor()); var container = await pool.GetContainerAsync(new PrmContainerGet(containerId, ctx)); Assert.NotNull(container); byte[] bytes = GetRandomBytes(objectSize); var param = new PrmObjectPut { Header = new FrostFsObjectHeader( containerId: containerId, type: FrostFsObjectType.Regular, [new FrostFsAttributePair("fileName", "test")]), Payload = new MemoryStream(bytes), ClientCut = true }; var objectId = await pool.PutObjectAsync(param); var filter = new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test"); bool hasObject = false; await foreach (var objId in pool.SearchObjectsAsync(new PrmObjectSearch(containerId, null, filter))) { hasObject = true; var objHeader = await pool.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objectId)); Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); Assert.NotNull(objHeader.Attributes); Assert.Single(objHeader.Attributes); Assert.Equal("fileName", objHeader.Attributes[0].Key); Assert.Equal("test", objHeader.Attributes[0].Value); } Assert.True(hasObject); var @object = await pool.GetObjectAsync(new PrmObjectGet(containerId, objectId)); var downloadedBytes = new byte[@object.Header.PayloadLength]; MemoryStream ms = new(downloadedBytes); ReadOnlyMemory? chunk = null; while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) { ms.Write(chunk.Value.Span); } Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(downloadedBytes)); await CheckFilter(pool, containerId, new FilterByRootObject()); await Cleanup(pool); await foreach (var cid in pool.ListContainersAsync()) { Assert.Fail($"Container {cid.GetValue()} exist"); } } private static byte[] GetRandomBytes(int size) { Random rnd = new(); var bytes = new byte[size]; rnd.NextBytes(bytes); return bytes; } private static IOptions GetSingleOwnerOptions(string key, string url) { return Options.Create(new ClientSettings { Key = key, Host = url }); } private static IOptions GetOptions(string url) { return Options.Create(new ClientSettings { Host = url }); } static async Task Cleanup(Pool pool) { await foreach (var cid in pool.ListContainersAsync()) { await pool.DeleteContainerAsync(new PrmContainerDelete(cid) { WaitParams = lightWait }).ConfigureAwait(true); } } }