Compare commits

...

5 Commits

Author SHA1 Message Date
Jedd Morgan b1b6e0399b experiment with lower values 2026-02-05 17:00:09 +00:00
Jedd Morgan 899d9e6fc7 Force InitializeWebsocket 2026-02-05 16:47:41 +00:00
Jedd Morgan 2cd1f1bbde correct exception type 2026-01-29 13:36:33 +00:00
Jedd Morgan 76d3b8279c assert this way 2026-01-29 13:18:16 +00:00
Jedd Morgan 2cd7b29fca CanCreateModelIngestion check 2026-01-29 13:11:50 +00:00
7 changed files with 92 additions and 15 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
- name: 🔨 Integration Tests against Internal Server
if: ${{ inputs.use-internal-image }}
run: dotnet test ${{ env.Solution }} --filter "Category=Integration" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Public)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
+20
View File
@@ -87,6 +87,26 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
catch (Exception ex) when (!ex.IsFatal()) { }
}
/// <summary>
/// Ensure the <see cref="GQLClient"/>'s websocket is fully initialized.
/// <br/>
/// You don't <i>need</i> to call this function, if you don't, then it will be setup for you when you call <see cref="SubscribeTo"/> (e.g. when you create a <see cref="Subscription"/>),
/// but due to <see cref="GraphQL"/>'s WebSocket implementation, it's not awaited (deferred) thus the subscription make take a while to actually be setup.
/// </summary>
/// <remarks>
/// We only use websockets for GraphQL subscriptions, so if you're not using subscriptions, don't call this
///
/// Note. due to other sources (potentially on the GraphQL side) you still need a ~100ms delay between setting up the subscription, and being able to relaibly trigger it
/// This should only really negatively affect test projects.
/// </remarks>
public async Task InitializeWebsocket()
{
if (GQLClient.WebSocketSubProtocol is null)
{
await GQLClient.InitializeWebsocketConnection().ConfigureAwait(false);
}
}
internal async Task<T> ExecuteWithResiliencePolicies<T>(Func<Task<T>> func) =>
await GraphQLRetry
.ExecuteAsync<T, SpeckleGraphQLInternalErrorException>(
@@ -6,7 +6,7 @@ using Speckle.Sdk.Api.GraphQL.Models.Responses;
namespace Speckle.Sdk.Api.GraphQL.Resources;
/// <remarks>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
public sealed class ModelIngestionResource
{
@@ -23,7 +23,7 @@ public sealed class ModelIngestionResource
/// <remarks>
/// The model ingestion created will have a <c>processing</c> state (not <c>queued</c>). This mutation is designed to be used
/// by client/connectors that are immediately processing
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
@@ -72,7 +72,7 @@ public sealed class ModelIngestionResource
}
/// <remarks>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
/// <param name="modelIngestionId"></param>
/// <param name="projectId"></param>
@@ -121,7 +121,7 @@ public sealed class ModelIngestionResource
/// For File Import / Cloud integrations only
/// </summary>
/// <remarks>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
@@ -173,7 +173,7 @@ public sealed class ModelIngestionResource
/// For File Import / Cloud integrations only
/// </summary>
/// <remarks>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
@@ -222,7 +222,7 @@ public sealed class ModelIngestionResource
}
/// <remarks>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
@@ -277,7 +277,7 @@ public sealed class ModelIngestionResource
/// If successful, the job will be in a terminal "successful" state.
/// </summary>
/// <remarks>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
/// <seealso cref="FailWithError"/>
/// <seealso cref="FailWithCancel"/>
@@ -320,7 +320,7 @@ public sealed class ModelIngestionResource
/// </summary>
/// <remarks>
/// For requested user cancellation, use <see cref="FailWithCancel"/> instead<br/>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
/// <seealso cref="FailWithCancel"/>
/// <seealso cref="Complete"/>
@@ -375,7 +375,7 @@ public sealed class ModelIngestionResource
/// This should only be done if the user has explicitly requested cancellation
/// Other forms of cancellation use <see cref="FailWithError"/>.
/// The ingestion should then enter a terminal "canceled" state.<br/>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </summary>
/// <seealso cref="FailWithError"/>
/// <seealso cref="Complete"/>
@@ -434,7 +434,7 @@ public sealed class ModelIngestionResource
/// via <see cref="SubscriptionResource.CreateProjectModelIngestionCancellationRequestedSubscription"/>
/// and report it as canceled via <see cref="ModelIngestionResource.FailWithCancel"/>
/// See "cooperative cancellation pattern"<br/>
/// Model Ingestion API is available for server versions <c>3.0.3-alpha.583</c> and above
/// Model Ingestion API is available for server versions <c>3.0.3</c> and above
/// </remarks>
/// <seealso cref="FailWithError"/>
/// <seealso cref="Complete"/>
@@ -359,4 +359,41 @@ public sealed class ModelResource
.ConfigureAwait(false);
return response.data.data.data;
}
/// <param name="projectId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="SpeckleGraphQLBadInputException">server versions &lt;3.0.11 do not have <c>canCreateIngestion</c> and will throw this exception</exception>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<PermissionCheckResult> CanCreateModelIngestion(
string projectId,
string modelId,
CancellationToken cancellationToken = default
)
{
//language=graphql
const string QUERY = """
query ModelPermissions($projectId: String!, $modelId: String!) {
data:project(id: $projectId) {
data:model(id: $modelId) {
data:permissions {
data:canCreateIngestion {
authorized
code
message
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { projectId, modelId } };
var response = await _client
.ExecuteGraphQLRequest<
RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<PermissionCheckResult>>>>
>(request, cancellationToken)
.ConfigureAwait(false);
return response.data.data.data.data;
}
}
@@ -1,2 +1,2 @@
schema: https://app.speckle.systems/graphql
schema: https://latest.speckle.systems/graphql
documents: '**/*.graphql'
@@ -141,4 +141,23 @@ public class ModelResourceTests : IAsyncLifetime
guestResult.canCreateVersion.authorized.Should().Be(false);
guestResult.canDelete.authorized.Should().Be(false);
}
[Fact]
[Trait("Server", "Internal")]
public async Task TestCanCreateModelIngestion_InternalServer()
{
var ownerResult = await Sut.CanCreateModelIngestion(_project.id, _model.id);
ownerResult.authorized.Should().Be(true);
}
[Fact]
[Trait("Server", "Public")]
public async Task TestCanCreateModelIngestion_PublicServer_Throws()
{
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
await Sut.CanCreateModelIngestion(_project.id, _model.id)
);
ex.InnerExceptions.Should().HaveCount(1);
ex.InnerExceptions.Should().AllBeOfType<SpeckleGraphQLInvalidQueryException>();
}
}
@@ -11,11 +11,11 @@ namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
public class SubscriptionResourceTests : IAsyncLifetime
{
#if DEBUG
private const int WAIT_PERIOD = 4000; // WSL is slow AF, so for local runs, we're being extra generous
private const int WAIT_PERIOD = 2000; // WSL is slow AF, so for local runs, we're being extra generous
#else
private const int WAIT_PERIOD = 500; // For CI runs, a much smaller wait time is acceptable
private const int WAIT_PERIOD = 300; // For CI runs, a much smaller wait time is acceptable
#endif
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 500;
private const int TIMEOUT = WAIT_PERIOD + 1000;
private IClient _testUser;
private Project _testProject;
private Model _testModel;
@@ -32,6 +32,7 @@ public class SubscriptionResourceTests : IAsyncLifetime
public async Task InitializeAsync()
{
_testUser = await Fixtures.SeedUserWithClient();
await _testUser.InitializeWebsocket();
_testProject = await _testUser.Project.Create(new("test project123", "desc", null));
_testModel = await _testUser.Model.Create(new("test model", "desc", _testProject.id));
_testVersion = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.id);