From 375f5071ae510f0e9b44d3af79ed225e61da72df Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 6 Mar 2025 13:20:11 +0000 Subject: [PATCH] Improve test coverage by adding tests and removing unused code (#240) * maybe all objects need to be false * fmt * remove extra code form hosts * fix build * add speckle serializer exception test * add DownloadObjects and DownloadSingleObject test * fmt * add HasObjects test * BaseItem tests * adjust code and add test for UploadObjects * Remove try/catch * can't test compressed? use lib to parse multipart * remove unused verify snapshots --- Directory.Packages.props | 4 +- src/Speckle.Sdk/Host/Attributes.cs | 54 +---- src/Speckle.Sdk/Host/HostApplications.cs | 177 -------------- .../Serialisation/V2/Send/BaseItem.cs | 5 +- .../Serialisation/V2/Send/EmptyDictionary.cs | 47 ---- .../Serialisation/V2/ServerObjectManager.cs | 19 +- .../V2/ServerObjectManagerFactory.cs | 11 +- .../ExceptionTests.cs | 20 ++ .../SerializationTypeTests.cs | 36 +++ ...ManagerTests.DownloadObjects.verified.json | 10 + ...erTests.DownloadSingleObject.verified.json | 4 + ...bjectManagerTests.HasObjects.verified.json | 4 + .../ServerObjectManagerTests.cs | 229 ++++++++++++++++++ .../Speckle.Sdk.Serialization.Tests.csproj | 5 +- .../packages.lock.json | 49 ++++ .../DummyReceiveServerObjectManager.cs | 4 +- .../Framework/DummySendServerObjectManager.cs | 5 +- 17 files changed, 389 insertions(+), 294 deletions(-) delete mode 100644 src/Speckle.Sdk/Serialisation/V2/Send/EmptyDictionary.cs create mode 100644 tests/Speckle.Sdk.Serialization.Tests/SerializationTypeTests.cs create mode 100644 tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.DownloadObjects.verified.json create mode 100644 tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.DownloadSingleObject.verified.json create mode 100644 tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.HasObjects.verified.json create mode 100644 tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 3f6002d6..a9f44a4e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + @@ -21,6 +22,7 @@ + @@ -35,4 +37,4 @@ - + \ No newline at end of file diff --git a/src/Speckle.Sdk/Host/Attributes.cs b/src/Speckle.Sdk/Host/Attributes.cs index 5887f6af..b30e6989 100644 --- a/src/Speckle.Sdk/Host/Attributes.cs +++ b/src/Speckle.Sdk/Host/Attributes.cs @@ -1,63 +1,15 @@ namespace Speckle.Sdk.Host; -[AttributeUsage(AttributeTargets.Constructor)] -public sealed class SchemaInfoAttribute : Attribute -{ - public SchemaInfoAttribute(string name, string description) - : this(name, description, null, null) { } - - public SchemaInfoAttribute(string name, string description, string? category, string? subcategory) - { - Name = name; - Description = description; - Category = category; - Subcategory = subcategory; - } - - public string? Subcategory { get; } - - public string? Category { get; } - - public string Description { get; } - - public string Name { get; } -} - -[AttributeUsage(AttributeTargets.Constructor)] -public sealed class SchemaDeprecatedAttribute : Attribute { } - -[AttributeUsage(AttributeTargets.Parameter)] -public sealed class SchemaParamInfoAttribute : Attribute -{ - public SchemaParamInfoAttribute(string description) - { - Description = description; - } - - public string Description { get; } -} - -/// -/// Used to indicate which is the main input parameter of the schema builder component. Schema info will be attached to this object. -/// -[AttributeUsage(AttributeTargets.Parameter)] -public sealed class SchemaMainParamAttribute : Attribute { } - // TODO: this could be nuked, as it's only used to hide props on Base, // which we might want to expose anyways... /// /// Used to ignore properties from expand objects etc /// [AttributeUsage(AttributeTargets.Property)] -public sealed class SchemaIgnoreAttribute : Attribute { } +public sealed class SchemaIgnoreAttribute : Attribute; [AttributeUsage(AttributeTargets.Method)] -public sealed class SchemaComputedAttribute : Attribute +public sealed class SchemaComputedAttribute(string name) : Attribute { - public SchemaComputedAttribute(string name) - { - Name = name; - } - - public string Name { get; } + public string Name { get; } = name; } diff --git a/src/Speckle.Sdk/Host/HostApplications.cs b/src/Speckle.Sdk/Host/HostApplications.cs index fc38d763..2abced0a 100644 --- a/src/Speckle.Sdk/Host/HostApplications.cs +++ b/src/Speckle.Sdk/Host/HostApplications.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.Contracts; - namespace Speckle.Sdk.Host; /// @@ -42,179 +40,4 @@ public static class HostApplications Navisworks = new("Navisworks", "navisworks"), AdvanceSteel = new("Advance Steel", "advancesteel"), Other = new("Other", "other"); - - /// - /// Gets a HostApplication form a string. It could be the versioned name or a string coming from a process running. - /// - /// String with the name of the app - /// - [Pure] - public static HostApplication GetHostAppFromString(string? appname) - { - if (appname == null) - { - return Other; - } - - appname = appname.ToLowerInvariant().Replace(" ", ""); - if (appname.Contains("dynamo")) - { - return Dynamo; - } - - if (appname.Contains("revit")) - { - return Revit; - } - - if (appname.Contains("autocad")) - { - return AutoCAD; - } - if (appname.Contains("civil3d")) - { - return Civil3D; - } - if (appname.Contains("civil")) - { - return Civil; - } - - if (appname.Contains("rhino")) - { - return Rhino; - } - - if (appname.Contains("grasshopper")) - { - return Grasshopper; - } - - if (appname.Contains("unity")) - { - return Unity; - } - - if (appname.Contains("gsa")) - { - return GSA; - } - - if (appname.Contains("microstation")) - { - return MicroStation; - } - - if (appname.Contains("openroads")) - { - return OpenRoads; - } - - if (appname.Contains("openrail")) - { - return OpenRail; - } - - if (appname.Contains("openbuildings")) - { - return OpenBuildings; - } - - if (appname.Contains("etabs")) - { - return ETABS; - } - - if (appname.Contains("sap")) - { - return SAP2000; - } - - if (appname.Contains("csibridge")) - { - return CSiBridge; - } - - if (appname.Contains("safe")) - { - return SAFE; - } - - if (appname.Contains("teklastructures")) - { - return TeklaStructures; - } - - if (appname.Contains("dxf")) - { - return Dxf; - } - - if (appname.Contains("excel")) - { - return Excel; - } - - if (appname.Contains("unreal")) - { - return Unreal; - } - - if (appname.Contains("powerbi")) - { - return PowerBI; - } - - if (appname.Contains("blender")) - { - return Blender; - } - - if (appname.Contains("qgis")) - { - return QGIS; - } - - if (appname.Contains("arcgis")) - { - return ArcGIS; - } - - if (appname.Contains("sketchup")) - { - return SketchUp; - } - - if (appname.Contains("archicad")) - { - return Archicad; - } - - if (appname.Contains("topsolid")) - { - return TopSolid; - } - - if (appname.Contains("python")) - { - return Python; - } - - if (appname.Contains("net")) - { - return NET; - } - - if (appname.Contains("navisworks")) - { - return Navisworks; - } - - if (appname.Contains("advancesteel")) - { - return AdvanceSteel; - } - - return new HostApplication(appname, appname); - } } diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs b/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs index f5c39f4b..2a88e3db 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs @@ -2,8 +2,7 @@ using System.Text; namespace Speckle.Sdk.Serialisation.V2.Send; -public readonly record struct BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary? Closures) - : IHasByteSize +public sealed record BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary? Closures) : IHasByteSize { public int ByteSize { get; } = Encoding.UTF8.GetByteCount(Json.Value); @@ -13,7 +12,7 @@ public readonly record struct BaseItem(Id Id, Json Json, bool NeedsStorage, Dict { return false; } - return string.Equals(Id.Value, other.Value.Id.Value, StringComparison.OrdinalIgnoreCase); + return string.Equals(Id.Value, other.Id.Value, StringComparison.OrdinalIgnoreCase); } public override int GetHashCode() => Id.GetHashCode(); diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/EmptyDictionary.cs b/src/Speckle.Sdk/Serialisation/V2/Send/EmptyDictionary.cs deleted file mode 100644 index 7059231a..00000000 --- a/src/Speckle.Sdk/Serialisation/V2/Send/EmptyDictionary.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections; -using System.Diagnostics.CodeAnalysis; - -namespace Speckle.Sdk.Serialisation.V2.Send; - -public class EmptyDictionary : IDictionary -{ - public IEnumerator> GetEnumerator() => throw new NotImplementedException(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public void Add(KeyValuePair item) { } - - public void Clear() => throw new NotImplementedException(); - - public bool Contains(KeyValuePair item) => false; - - public void CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotImplementedException(); - - public bool Remove(KeyValuePair item) => false; - - public int Count => 0; - public bool IsReadOnly => false; - - public void Add(TKey key, TValue value) { } - - public bool ContainsKey(TKey key) => false; - - public bool Remove(TKey key) => false; - - public bool TryGetValue(TKey key, [UnscopedRef] out TValue value) - { - value = default!; - return false; - } - - public TValue this[TKey key] - { -#pragma warning disable CA1065 - get => throw new NotImplementedException(); -#pragma warning restore CA1065 - set { } - } - - public ICollection Keys { get; } - public ICollection Values { get; } -} diff --git a/src/Speckle.Sdk/Serialisation/V2/ServerObjectManager.cs b/src/Speckle.Sdk/Serialisation/V2/ServerObjectManager.cs index 895a4a6f..e196c890 100644 --- a/src/Speckle.Sdk/Serialisation/V2/ServerObjectManager.cs +++ b/src/Speckle.Sdk/Serialisation/V2/ServerObjectManager.cs @@ -13,6 +13,12 @@ using Speckle.Sdk.Transports.ServerUtils; namespace Speckle.Sdk.Serialisation.V2; +public class ServerObjectManagerOptions(TimeSpan? timeout = null, string? boundary = null) +{ + public TimeSpan Timeout => timeout ?? TimeSpan.FromSeconds(120); + public string Boundary => boundary ??= Guid.NewGuid().ToString(); +} + [GenerateAutoInterface] public class ServerObjectManager : IServerObjectManager { @@ -21,6 +27,7 @@ public class ServerObjectManager : IServerObjectManager private readonly ISdkActivityFactory _activityFactory; private readonly HttpClient _client; private readonly string _streamId; + private readonly ServerObjectManagerOptions _serverObjectManagerOptions; public ServerObjectManager( ISpeckleHttp speckleHttp, @@ -28,13 +35,14 @@ public class ServerObjectManager : IServerObjectManager Uri baseUri, string streamId, string? authorizationToken, - int timeoutSeconds = 120 + ServerObjectManagerOptions? options = null ) { + _serverObjectManagerOptions = options ?? new(); _activityFactory = activityFactory; _client = speckleHttp.CreateHttpClient( new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip }, - timeoutSeconds: timeoutSeconds, + timeoutSeconds: (int)_serverObjectManagerOptions.Timeout.TotalSeconds, authorizationToken: authorizationToken ); _client.BaseAddress = baseUri; @@ -139,10 +147,8 @@ public class ServerObjectManager : IServerObjectManager CancellationToken cancellationToken ) { + using var _ = _activityFactory.Start(); cancellationToken.ThrowIfCancellationRequested(); - - // Stopwatch sw = new Stopwatch(); sw.Start(); - string objectsPostParameter = JsonConvert.SerializeObject(objectIds); var payload = new Dictionary { { "objects", objectsPostParameter } }; string serializedPayload = JsonConvert.SerializeObject(payload); @@ -169,6 +175,7 @@ public class ServerObjectManager : IServerObjectManager CancellationToken cancellationToken ) { + using var _ = _activityFactory.Start(); cancellationToken.ThrowIfCancellationRequested(); using HttpRequestMessage message = new() @@ -177,7 +184,7 @@ public class ServerObjectManager : IServerObjectManager Method = HttpMethod.Post, }; - MultipartFormDataContent multipart = new(); + MultipartFormDataContent multipart = new(_serverObjectManagerOptions.Boundary); int mpId = 0; var ctBuilder = new StringBuilder("["); diff --git a/src/Speckle.Sdk/Serialisation/V2/ServerObjectManagerFactory.cs b/src/Speckle.Sdk/Serialisation/V2/ServerObjectManagerFactory.cs index 071a62b5..0fba82bf 100644 --- a/src/Speckle.Sdk/Serialisation/V2/ServerObjectManagerFactory.cs +++ b/src/Speckle.Sdk/Serialisation/V2/ServerObjectManagerFactory.cs @@ -8,6 +8,13 @@ namespace Speckle.Sdk.Serialisation.V2; public class ServerObjectManagerFactory(ISpeckleHttp speckleHttp, ISdkActivityFactory activityFactory) : IServerObjectManagerFactory { - public IServerObjectManager Create(Uri url, string streamId, string? authorizationToken, int timeoutSeconds = 120) => - new ServerObjectManager(speckleHttp, activityFactory, url, streamId, authorizationToken, timeoutSeconds); + public IServerObjectManager Create(Uri url, string streamId, string? authorizationToken, TimeSpan? timeout = null) => + new ServerObjectManager( + speckleHttp, + activityFactory, + url, + streamId, + authorizationToken, + new ServerObjectManagerOptions(timeout: timeout) + ); } diff --git a/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs index a9315b8a..ff22c2b8 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/ExceptionTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Speckle.Objects.Geometry; using Speckle.Sdk.Host; using Speckle.Sdk.Models; +using Speckle.Sdk.Serialisation; using Speckle.Sdk.Serialisation.V2.Receive; using Speckle.Sdk.Serialisation.V2.Send; using Speckle.Sdk.Serialization.Tests.Framework; @@ -144,4 +145,23 @@ public class ExceptionTests } await Verify(ex).UseParameters(hasObject); } + + [SpeckleType("Objects.Geometry.BadBase")] + public class BadBase : Base + { +#pragma warning disable CA1065 + public string BadProp => throw new NotImplementedException(); +#pragma warning restore CA1065 + } + + [Fact] + public void Test_SpeckleSerializerException() + { + var factory = new ObjectSerializerFactory(new BasePropertyGatherer()); + var serializer = factory.Create(new Dictionary(), default); + Assert.Throws(() => + { + var _ = serializer.Serialize(new BadBase()).ToList(); + }); + } } diff --git a/tests/Speckle.Sdk.Serialization.Tests/SerializationTypeTests.cs b/tests/Speckle.Sdk.Serialization.Tests/SerializationTypeTests.cs new file mode 100644 index 00000000..432e5103 --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/SerializationTypeTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using Speckle.Sdk.Serialisation; +using Speckle.Sdk.Serialisation.V2.Send; + +namespace Speckle.Sdk.Serialization.Tests; + +public class SerializationTypeTests +{ + [Fact] + public void Json() + { + var json = new Json("{}"); + json.ToString().Should().Be("{}"); + } + + [Fact] + public void Id() + { + var id = new Id("id"); + id.ToString().Should().Be("id"); + id.Equals(new Id("id")).Should().BeTrue(); + id.Equals(new Id("id2")).Should().BeFalse(); + id.GetHashCode().Should().Be("id".GetHashCode()); + } + + [Fact] + public void BaseItem() + { + var id = new Id("id"); + var json = new Json("{}"); + var baseItem = new BaseItem(id, json, false, new Dictionary()); + baseItem.Equals(new BaseItem(id, json, false, new Dictionary())).Should().BeTrue(); + baseItem.Equals(new BaseItem(new Id("id2"), json, false, new Dictionary())).Should().BeFalse(); + baseItem.GetHashCode().Should().Be(id.GetHashCode()); + } +} diff --git a/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.DownloadObjects.verified.json b/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.DownloadObjects.verified.json new file mode 100644 index 00000000..488ea578 --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.DownloadObjects.verified.json @@ -0,0 +1,10 @@ +{ + "6f422a35-6183-48b9-8021-d22ec97e8674": { + "id": "6f422a35-6183-48b9-8021-d22ec97e8674", + "value": true + }, + "ef2f7ea0-495a-46af-a9ad-f18a8a298597": { + "id": "ef2f7ea0-495a-46af-a9ad-f18a8a298597", + "value": true + } +} diff --git a/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.DownloadSingleObject.verified.json b/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.DownloadSingleObject.verified.json new file mode 100644 index 00000000..d9bb684e --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.DownloadSingleObject.verified.json @@ -0,0 +1,4 @@ +{ + "id": "6f422a35-6183-48b9-8021-d22ec97e8674", + "value": true +} diff --git a/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.HasObjects.verified.json b/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.HasObjects.verified.json new file mode 100644 index 00000000..4295906d --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.HasObjects.verified.json @@ -0,0 +1,4 @@ +{ + "6f422a35-6183-48b9-8021-d22ec97e8674": true, + "ef2f7ea0-495a-46af-a9ad-f18a8a298597": false +} diff --git a/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.cs b/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.cs new file mode 100644 index 00000000..d31c6441 --- /dev/null +++ b/tests/Speckle.Sdk.Serialization.Tests/ServerObjectManagerTests.cs @@ -0,0 +1,229 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Text; +using FluentAssertions; +using HttpMultipartParser; +using Moq; +using RichardSzalay.MockHttp; +using Speckle.Newtonsoft.Json; +using Speckle.Newtonsoft.Json.Linq; +using Speckle.Sdk.Common; +using Speckle.Sdk.Helpers; +using Speckle.Sdk.Logging; +using Speckle.Sdk.Serialisation; +using Speckle.Sdk.Serialisation.V2; +using Speckle.Sdk.Serialisation.V2.Send; + +namespace Speckle.Sdk.Serialization.Tests; + +[ExcludeFromCodeCoverage] +public abstract class MoqTest : IDisposable +{ + protected MoqTest() => Repository = new(MockBehavior.Strict); + + public void Dispose() => Repository.VerifyAll(); + + protected MockRepository Repository { get; private set; } = new(MockBehavior.Strict); + + protected Mock Create(MockBehavior behavior = MockBehavior.Strict) + where T : class => Repository.Create(behavior); +} + +public class ServerObjectManagerTests : MoqTest +{ + [Fact] + public async Task DownloadObjects() + { + var id = Guid.Parse("6f422a35-6183-48b9-8021-d22ec97e8674").ToString(); + var id2 = Guid.Parse("ef2f7ea0-495a-46af-a9ad-f18a8a298597").ToString(); + var ct = new CancellationToken(); + var token = "token"; + var timeout = 2; + var uri = new Uri("http://localhost"); + var streamId = "streamId"; + var jObject = new JObject { { "id", id }, { "value", true } }; + var jObject2 = new JObject { { "id", id2 }, { "value", true } }; + var mockHttp = new MockHttpMessageHandler(); + Dictionary postParameters = new() + { + { "objects", JsonConvert.SerializeObject(new List { id, id2 }) }, + }; + + string serializedPayload = JsonConvert.SerializeObject(postParameters); + mockHttp + .When(HttpMethod.Post, $"http://localhost/api/getobjects/{streamId}") + .WithContent(serializedPayload) + .Respond( + "application/json", + $"{id}\t{jObject.ToString(Formatting.None)}\n{id2}\t{jObject2.ToString(Formatting.None)}\n" + ); + var httpClient = mockHttp.ToHttpClient(); + var http = Create(); + http.Setup(x => x.CreateHttpClient(It.IsAny(), timeout, token)).Returns(httpClient); + + var activityFactory = Create(); + activityFactory.Setup(x => x.Start(null, "DownloadObjects")).Returns((ISdkActivity?)null); + + var serverObjectManager = new ServerObjectManager( + http.Object, + activityFactory.Object, + uri, + streamId, + token, + new(timeout: TimeSpan.FromSeconds(timeout)) + ); + var results = serverObjectManager.DownloadObjects(new List { id, id2 }, null, ct); + var objects = new JObject(); + await foreach (var (x, json) in results) + { + objects.Add(x, JToken.Parse(json)); + } + + await VerifyJson(objects.ToString(Formatting.Indented)); + } + + [Fact] + public async Task DownloadSingleObject() + { + var id = Guid.Parse("6f422a35-6183-48b9-8021-d22ec97e8674").ToString(); + var ct = new CancellationToken(); + var token = "token"; + var timeout = 2; + var uri = new Uri("http://localhost"); + var streamId = "streamId"; + var jObject = new JObject { { "id", id }, { "value", true } }; + var mockHttp = new MockHttpMessageHandler(); + mockHttp + .When(HttpMethod.Get, $"http://localhost/objects/{streamId}/{id}/single") + .Respond("application/json", $"{jObject.ToString(Formatting.None)}\n"); + var httpClient = mockHttp.ToHttpClient(); + var http = Create(); + http.Setup(x => x.CreateHttpClient(It.IsAny(), timeout, token)).Returns(httpClient); + + var activityFactory = Create(); + activityFactory.Setup(x => x.Start(null, "DownloadSingleObject")).Returns((ISdkActivity?)null); + + var serverObjectManager = new ServerObjectManager( + http.Object, + activityFactory.Object, + uri, + streamId, + token, + new(timeout: TimeSpan.FromSeconds(timeout)) + ); + var json = await serverObjectManager.DownloadSingleObject(id, null, ct); + await VerifyJson(json); + } + + [Fact] + public async Task HasObjects() + { + var id = Guid.Parse("6f422a35-6183-48b9-8021-d22ec97e8674").ToString(); + var id2 = Guid.Parse("ef2f7ea0-495a-46af-a9ad-f18a8a298597").ToString(); + var ct = new CancellationToken(); + var token = "token"; + var timeout = 2; + var uri = new Uri("http://localhost"); + var streamId = "streamId"; + var mockHttp = new MockHttpMessageHandler(); + Dictionary postParameters = new() + { + { "objects", JsonConvert.SerializeObject(new List { id, id2 }) }, + }; + + Dictionary responseParameters = new() { { id, true }, { id2, false } }; + string serializedPayload = JsonConvert.SerializeObject(postParameters); + mockHttp + .When(HttpMethod.Post, $"http://localhost/api/diff/{streamId}") + .WithContent(serializedPayload) + .Respond("application/json", JsonConvert.SerializeObject(responseParameters)); + var httpClient = mockHttp.ToHttpClient(); + var http = Create(); + http.Setup(x => x.CreateHttpClient(It.IsAny(), timeout, token)).Returns(httpClient); + + var activityFactory = Create(); + activityFactory.Setup(x => x.Start(null, "HasObjects")).Returns((ISdkActivity?)null); + + var serverObjectManager = new ServerObjectManager( + http.Object, + activityFactory.Object, + uri, + streamId, + token, + new(timeout: TimeSpan.FromSeconds(timeout)) + ); + var results = await serverObjectManager.HasObjects(new List { id, id2 }, ct); + + await Verify(results); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task UploadObjects(bool compressed) + { + var id = Guid.Parse("6f422a35-6183-48b9-8021-d22ec97e8674").ToString(); + var id2 = Guid.Parse("ef2f7ea0-495a-46af-a9ad-f18a8a298597").ToString(); + var ct = new CancellationToken(); + var token = "token"; + var timeout = 2; + var uri = new Uri("http://localhost"); + var streamId = "streamId"; + var jObject = new JObject { { "id", id }, { "value", true } }; + var jObject2 = new JObject { { "id", id2 }, { "value", true } }; + + var mockHttp = new MockHttpMessageHandler(); + mockHttp + .When(HttpMethod.Post, $"http://localhost/objects/{streamId}") + .Respond(x => HandleUploadRequest(x, compressed, new List { jObject, jObject2 })); + var httpClient = mockHttp.ToHttpClient(); + var http = Create(); + http.Setup(x => x.CreateHttpClient(It.IsAny(), timeout, token)).Returns(httpClient); + + var activityFactory = Create(); + activityFactory.Setup(x => x.Start(null, "UploadObjects")).Returns((ISdkActivity?)null); + + var serverObjectManager = new ServerObjectManager( + http.Object, + activityFactory.Object, + uri, + streamId, + token, + new(timeout: TimeSpan.FromSeconds(timeout), boundary: "boundary") + ); + await serverObjectManager.UploadObjects( + new List + { + new(new(id), new Json(jObject.ToString()), true, null), + new(new(id2), new Json(jObject2.ToString()), true, null), + }, + compressed, + null, + ct + ); + } + + private async Task HandleUploadRequest( + HttpRequestMessage req, + bool compressed, + List objects + ) + { + var content = await req.Content.NotNull().ReadAsStringAsync().ConfigureAwait(false); + var stream = new MemoryStream(Encoding.Default.GetBytes(content)); + var formData = await MultipartFormDataParser.ParseAsync(stream, "boundary"); + formData.Files.Count.Should().Be(1); + var file = formData.Files[0]; + file.FileName.Should().Be("batch-0"); + // file.ContentType.Should().Be("application/json"); + if (!compressed) + { + var dataStream = file.Data; + using var reader = new StreamReader(dataStream); + var s = await reader.ReadToEndAsync(); + JsonConvert.DeserializeObject>(s).Should().BeEquivalentTo(objects); + } + + return new HttpResponseMessage(HttpStatusCode.OK); + } +} diff --git a/tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj b/tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj index 47352da9..3f3689e8 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj +++ b/tests/Speckle.Sdk.Serialization.Tests/Speckle.Sdk.Serialization.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -9,7 +9,10 @@ + + + diff --git a/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json b/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json index d2993f2e..3e40f9b6 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json +++ b/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json @@ -20,6 +20,17 @@ "resolved": "5.12.0", "contentHash": "dJuigXycpJNOiLT9or7mkHSkGFHgGW3/p6cNNYEKZBa7Hhp1FdX/cvqYWWYhRLpfoZOedeA7aRbYiOB3vW/dvA==" }, + "HttpMultipartParser": { + "type": "Direct", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "WeGKDK62waagwo9nokRY62DrLukwVCNXOPVjrzi/qSS73D+B6zPOBkImsFqzhvRNweIobR74js9M2HTV+PTeIg==", + "dependencies": { + "Microsoft.CSharp": "4.7.0", + "Microsoft.IO.RecyclableMemoryStream": "3.0.1", + "System.Buffers": "4.6.0" + } + }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[17.13.0, )", @@ -40,12 +51,27 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, + "Moq": { + "type": "Direct", + "requested": "[4.20.70, )", + "resolved": "4.20.70", + "contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, "PolySharp": { "type": "Direct", "requested": "[1.15.0, )", "resolved": "1.15.0", "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" }, + "RichardSzalay.MockHttp": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" + }, "Speckle.InterfaceGenerator": { "type": "Direct", "requested": "[0.9.6, )", @@ -69,6 +95,14 @@ "resolved": "0.26.0", "contentHash": "n7btGXdtRyprGnpLMpBs6rLScxlvPtVWwmTR8h7CtJvpZXBGhGvibEdZxRjeTZNrwf403jJ0ZPpt35Pz/NaNsw==" }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, "DiffEngine": { "type": "Transitive", "resolved": "15.9.0", @@ -175,6 +209,11 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.1" } }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Transitive", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "8.0.0", @@ -246,6 +285,11 @@ "SQLitePCLRaw.core": "2.1.4" } }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" + }, "System.CodeDom": { "type": "Transitive", "resolved": "9.0.1", @@ -256,6 +300,11 @@ "resolved": "4.5.0", "contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg==" }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, "System.IO.Hashing": { "type": "Transitive", "resolved": "9.0.1", diff --git a/tests/Speckle.Sdk.Testing/Framework/DummyReceiveServerObjectManager.cs b/tests/Speckle.Sdk.Testing/Framework/DummyReceiveServerObjectManager.cs index 75787dc7..e038f33c 100644 --- a/tests/Speckle.Sdk.Testing/Framework/DummyReceiveServerObjectManager.cs +++ b/tests/Speckle.Sdk.Testing/Framework/DummyReceiveServerObjectManager.cs @@ -37,14 +37,14 @@ public class DummyReceiveServerObjectManager(IReadOnlyDictionary ) => throw new NotImplementedException(); public Task UploadObjects( - IReadOnlyList objects, + IReadOnlyList objectsToUpload, bool compressPayloads, IProgress? progress, CancellationToken cancellationToken ) { long totalBytes = 0; - foreach (var item in objects) + foreach (var item in objectsToUpload) { totalBytes += Encoding.Default.GetByteCount(item.Json.Value); } diff --git a/tests/Speckle.Sdk.Testing/Framework/DummySendServerObjectManager.cs b/tests/Speckle.Sdk.Testing/Framework/DummySendServerObjectManager.cs index 0ae163c4..ca46a915 100644 --- a/tests/Speckle.Sdk.Testing/Framework/DummySendServerObjectManager.cs +++ b/tests/Speckle.Sdk.Testing/Framework/DummySendServerObjectManager.cs @@ -22,10 +22,7 @@ public class DummySendServerObjectManager(ConcurrentDictionary s public Task> HasObjects( IReadOnlyCollection objectIds, CancellationToken cancellationToken - ) - { - return Task.FromResult(objectIds.Distinct().ToDictionary(x => x, savedObjects.ContainsKey)); - } + ) => Task.FromResult(objectIds.Distinct().ToDictionary(x => x, _ => false)); public Task UploadObjects( IReadOnlyList objects,