From 8785e49f7378f8bd9b55549932141937555bc2eb Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:12:12 +0000 Subject: [PATCH] Add send/receive integration tests (#412) --- src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs | 2 +- .../Api/Operations/Operations.Receive.cs | 1 + .../V2/Receive/DeserializeProcess.cs | 2 +- ...ceiveNonExistentObjectThrows.verified.json | 6 + ...eiveNonExistentProjectThrows.verified.json | 6 + ...dReceiveTests.SendAndReceive.verified.json | 4 + ...ReceiveTests.SendInvalidData.verified.json | 18 ++ .../SendReceiveTests.cs | 190 ++++++++++++++++++ 8 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentObjectThrows.verified.json create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentProjectThrows.verified.json create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendAndReceive.verified.json create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendInvalidData.verified.json create mode 100644 tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.cs diff --git a/src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs b/src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs index 01da6610..0da48353 100644 --- a/src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs +++ b/src/Speckle.Sdk/Api/GraphQL/Models/Comment.cs @@ -14,7 +14,7 @@ public sealed class Comment public string rawText { get; init; } public ResourceCollection replies { get; init; } public CommentReplyAuthorCollection replyAuthors { get; init; } - public List resources { get; init; } + public List resources { get; init; } //todo: add resourceIds/baseResourceIds public string? screenshot { get; init; } public DateTime updatedAt { get; init; } public DateTime? viewedAt { get; init; } diff --git a/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs b/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs index 4aa4e00a..1c946b8f 100644 --- a/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs +++ b/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs @@ -16,6 +16,7 @@ public partial class Operations /// No transports were specified /// The was /// Serialization or Send operation was unsuccessful + /// HTTP layer errors /// The requested cancellation public async Task Receive2( Uri url, diff --git a/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs b/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs index 3d3cefad..dd486f20 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Receive/DeserializeProcess.cs @@ -10,7 +10,7 @@ using Speckle.Sdk.Transports; namespace Speckle.Sdk.Serialisation.V2.Receive; public record DeserializeProcessOptions( - bool SkipCache = false, + bool SkipCache = false, //TODO: This appears to be bugged when set to `true`, `LoadId` depends on sqlite bool ThrowOnMissingReferences = true, bool SkipInvalidConverts = false, int? MaxParallelism = null, diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentObjectThrows.verified.json b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentObjectThrows.verified.json new file mode 100644 index 00000000..c48b611b --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentObjectThrows.verified.json @@ -0,0 +1,6 @@ +{ + "Data": {}, + "Message": "Response status code does not indicate success: 404 (Not Found).", + "StatusCode": "NotFound", + "Type": "HttpRequestException" +} diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentProjectThrows.verified.json b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentProjectThrows.verified.json new file mode 100644 index 00000000..c48b611b --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.ReceiveNonExistentProjectThrows.verified.json @@ -0,0 +1,6 @@ +{ + "Data": {}, + "Message": "Response status code does not indicate success: 404 (Not Found).", + "StatusCode": "NotFound", + "Type": "HttpRequestException" +} diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendAndReceive.verified.json b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendAndReceive.verified.json new file mode 100644 index 00000000..a931619e --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendAndReceive.verified.json @@ -0,0 +1,4 @@ +{ + "ConvertedReferences": {}, + "RootId": "5313a8f61e1fa7abe9bf716ddfc767bd" +} diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendInvalidData.verified.json b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendInvalidData.verified.json new file mode 100644 index 00000000..ce2a6547 --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.SendInvalidData.verified.json @@ -0,0 +1,18 @@ +{ + "Data": {}, + "InnerException": { + "$type": "SpeckleSerializeException", + "Data": {}, + "InnerException": { + "$type": "ArgumentException", + "Data": {}, + "Message": "Unsupported value in serialization: System.Text.StringBuilder", + "ParamName": "obj", + "Type": "ArgumentException" + }, + "Message": "Failed to extract (pre-serialize) properties from the Speckle.Sdk.Models.Base", + "Type": "SpeckleSerializeException" + }, + "Message": "Error while sending: Failed to extract (pre-serialize) properties from the Speckle.Sdk.Models.Base", + "Type": "SpeckleException" +} diff --git a/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.cs b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.cs new file mode 100644 index 00000000..78227b1a --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Integration/SendReceiveTests.cs @@ -0,0 +1,190 @@ +using System.Reflection; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Speckle.Sdk.Api; +using Speckle.Sdk.Api.GraphQL.Enums; +using Speckle.Sdk.Api.GraphQL.Models; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; + +namespace Speckle.Sdk.Tests.Integration; + +public sealed class SendReceiveTests : IAsyncLifetime +{ + private Project _project; + private IClient _client; + private IOperations _operations; + private const string NON_EXISTENT_OBJECT_ID = "0a480dfb7aa774f19a82bee9d6320abd"; + private const string NON_EXISTENT_PROJECT_ID = "8cdc651d13"; + + public async Task InitializeAsync() + { + TypeLoader.Reset(); + TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly()); + var serviceProvider = TestServiceSetup.GetServiceProvider(); + _operations = serviceProvider.GetRequiredService(); + ClearCache(); + + _client = await Fixtures.SeedUserWithClient(); + _project = await _client.Project.Create(new("Blobber", "Flobber", ProjectVisibility.Private)); + } + + [Fact] + public async Task SendAndReceive() + { + var myObject = Fixtures.GenerateNestedObject(); + string expectedId = myObject.GetId(true); + + //SEND + var fistSend = await _operations.Send2( + _client.ServerUrl, + _project.id, + _client.Account.token, + myObject, + null, + CancellationToken.None + ); + + Assert.Equal(expectedId, fistSend.RootId); + await Verify(fistSend); + + //RECEIVE + var received = await _operations.Receive2( + _client.ServerUrl, + _project.id, + fistSend.RootId, + _client.Account.token, + null, + CancellationToken.None + ); + + Assert.Equal(expectedId, received.id); + + //SEND AGAIN! + var secondSend = await _operations.Send2( + _client.ServerUrl, + _project.id, + _client.Account.token, + received, + null, + CancellationToken.None + ); + + Assert.Equal(expectedId, secondSend.RootId); + + //RECEIVE AGAIN, but using cache + ClearCache(); + var secondReceive = await _operations.Receive2( + _client.ServerUrl, + _project.id, + fistSend.RootId, + _client.Account.token, + null, + CancellationToken.None + ); + + Assert.Equal(expectedId, secondReceive.id); + } + + private void ClearCache() { } + + [Fact] + public async Task ReceiveNonExistentObjectThrows() + { + var ex = await Assert.ThrowsAsync(async () => + { + _ = await _operations.Receive2( + _client.ServerUrl, + _project.id, + NON_EXISTENT_OBJECT_ID, + _client.Account.token, + null, + CancellationToken.None, + new(true) + ); + }); + await Verify(ex); + } + + [Fact] + public async Task ReceiveNonExistentProjectThrows() + { + var ex = await Assert.ThrowsAsync(async () => + { + _ = await _operations.Receive2( + _client.ServerUrl, + NON_EXISTENT_PROJECT_ID, + NON_EXISTENT_OBJECT_ID, + _client.Account.token, + null, + CancellationToken.None, + new(true) + ); + }); + await Verify(ex); + } + + [Fact] + public async Task SendInvalidData() + { + var myObject = Fixtures.GenerateNestedObject(); + myObject["invalidProp"] = new StringBuilder(); //Serializer does not support serializing this type + + var ex = await Assert.ThrowsAsync(async () => + { + _ = await _operations.Send2( + _client.ServerUrl, + _project.id, + _client.Account.token, + myObject, + null, + CancellationToken.None, + new(SkipCacheRead: true, SkipCacheWrite: true) + ); + }); + await Verify(ex); + } + + [Fact] + public async Task ReceiveNonAuthThrows() + { + using IClient unauthed = Fixtures.Unauthed; + await Assert.ThrowsAsync(async () => + { + _ = await _operations.Receive2( + unauthed.ServerUrl, + _project.id, + NON_EXISTENT_OBJECT_ID, + unauthed.Account.token, + null, + CancellationToken.None, + new(true) + ); + }); + } + + [Fact] + public async Task ReceiveCancellation() + { + using CancellationTokenSource ct = new(); + await ct.CancelAsync(); + await Assert.ThrowsAnyAsync(async () => + { + _ = await _operations.Receive2( + _client.ServerUrl, + _project.id, + NON_EXISTENT_OBJECT_ID, + _client.Account.token, + null, + ct.Token, + new(true) + ); + }); + } + + public Task DisposeAsync() + { + _client?.Dispose(); + return Task.CompletedTask; + } +}