Compare commits

...

7 Commits

Author SHA1 Message Date
Jedd Morgan 5c40a74f6d fake a release
.NET Build and Publish / build (push) Has been cancelled
2025-12-02 14:45:58 +00:00
Jedd Morgan 21c5a2e163 Fixes 2025-12-02 14:25:17 +00:00
Jedd Morgan 34aa4f3548 subscriptions 2025-12-02 14:24:50 +00:00
Jedd Morgan 3fbd9c17ba format
.NET Build and Publish / build (push) Has been cancelled
2025-11-24 18:41:10 +00:00
Jedd Morgan 937eb94730 First pass 2025-11-24 18:40:17 +00:00
Jedd Morgan b0da4510bf Merge pull request #410 from specklesystems/jrm/subscription-tests
test(subscriptions): Make subscription tests a bit more reliable
2025-11-05 11:28:23 +00:00
Jedd Morgan 96392d0d2f chore(ci): Reliable integration Tests (#418)
* remove bad tests

* add pack to PR workflow
2025-11-05 11:22:07 +00:00
32 changed files with 818 additions and 140 deletions
+3
View File
@@ -33,6 +33,9 @@ jobs:
- name: 🔨 Unit Tests
run: dotnet test ${{ env.Solution }} --configuration Release --filter "Category!=Integration" --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
- name: 🎁 Pack
run: dotnet pack ${{ env.Solution }} --configuration Release --no-build
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
+1
View File
@@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
ProjectSection(SolutionItems) = preProject
.github\workflows\pr.yml = .github\workflows\pr.yml
.github\workflows\release.yml = .github\workflows\release.yml
.github\workflows\integration-test.yml = .github\workflows\integration-test.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Sdk.Tests.Performance", "tests\Speckle.Sdk.Tests.Performance\Speckle.Sdk.Tests.Performance.csproj", "{870E3396-E6F7-43AE-B120-E651FA4F46BD}"
+1 -1
View File
@@ -180,6 +180,6 @@ Target(
}
);
Target("default", dependsOn: [FORMAT, TEST, INTEGRATION], () => Console.WriteLine("Done!"));
Target("default", dependsOn: [FORMAT, TEST], () => Console.WriteLine("Done!"));
await RunTargetsAndExitAsync(args).ConfigureAwait(true);
+2
View File
@@ -35,6 +35,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
public WorkspaceResource Workspace { get; }
public ServerResource Server { get; }
public FileImportResource FileImport { get; }
public ModelIngestionResource Ingestion { get; }
public Uri ServerUrl => new(Account.serverInfo.url);
@@ -71,6 +72,7 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
Workspace = new(this);
Server = new(this);
FileImport = new(this, blobApiFactory.Create(account));
Ingestion = new(this);
}
[AutoInterfaceIgnore]
@@ -1,6 +1,9 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
// ReSharper disable InconsistentNaming
namespace Speckle.Sdk.Api.GraphQL.Enums;
//This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
/// <remarks>
/// This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
/// </remarks>
public enum FileUploadConversionStatus
{
Queued = 0,
@@ -0,0 +1,14 @@
// ReSharper disable InconsistentNaming
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ModelIngestionStatus
{
cancelled,
failed,
processing,
queued,
success,
}
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ProjectCommentsUpdatedMessageType
{
ARCHIVED,
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ProjectFileImportUpdatedMessageType
{
CREATED,
@@ -0,0 +1,13 @@
// ReSharper disable InconsistentNaming
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ProjectModelIngestionUpdatedMessageType
{
cancellationRequested,
created,
deleted,
updated,
}
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ProjectModelsUpdatedMessageType
{
CREATED,
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ProjectPendingModelsUpdatedMessageType
{
CREATED,
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ProjectUpdatedMessageType
{
DELETED,
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ProjectVersionsUpdatedMessageType
{
CREATED,
@@ -1,5 +1,9 @@
// ReSharper disable InconsistentNaming
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ProjectVisibility
{
Private,
@@ -1,5 +1,9 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
// ReSharper disable InconsistentNaming
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum ResourceType
{
commit,
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
/// <remarks>
/// string based enum
/// </remarks>
public enum UserProjectsUpdatedMessageType
{
ADDED,
@@ -0,0 +1,49 @@
using Speckle.Sdk.Api.GraphQL.Enums;
namespace Speckle.Sdk.Api.GraphQL.Inputs;
public record SourceDataInput(
string sourceApplicationSlug,
string sourceApplicationVersion,
string? fileName,
long? fileSizeBytes
);
public record ModelIngestionCreateInput(
string modelId,
string projectId,
string progressMessage,
SourceDataInput sourceData
);
public record ModelIngestionUpdateInput(string ingestionId, string projectId, string progressMessage, double? progress);
public record ModelIngestionSuccessInput(string ingestionId, string projectId, string rootObjectId);
public record ModelIngestionFailedInput(
string ingestionId,
string projectId,
string errorReason,
string? errorStacktrace
)
{
public static ModelIngestionFailedInput FromException(string ingestionId, string projectId, Exception ex)
{
return new ModelIngestionFailedInput(ingestionId, projectId, ex.Message, ex.ToString());
}
}
public record ModelIngestionCancelledInput(string ingestionId, string projectId, string cancellationMessage);
public record ProjectModelIngestionSubscriptionInput(
string projectId,
ModelIngestionReference ingestionReference,
ProjectModelIngestionUpdatedMessageType messageType
);
/// <remarks>
/// <c>@oneOf</c> i.e. server expects <b>either</b> <paramref name="ingestionId"/> or <paramref name="modelId"/>, but not both.
/// </remarks>
/// <param name="ingestionId"></param>
/// <param name="modelId"></param>
public record ModelIngestionReference(string? ingestionId, string? modelId);
@@ -0,0 +1,12 @@
namespace Speckle.Sdk.Api.GraphQL.Models;
public sealed class ModelIngestion
{
public required string id { get; init; }
public required DateTime createdAt { get; init; }
public required DateTime updatedAt { get; init; }
public required string modelId { get; init; }
public required bool cancellationRequested { get; init; }
public required ModelIngestionStatusData statusData { get; init; }
// public required LimitedUser user { get; init; }
}
@@ -0,0 +1,9 @@
using Speckle.Sdk.Api.GraphQL.Enums;
namespace Speckle.Sdk.Api.GraphQL.Models;
public sealed class ModelIngestionStatusData
{
public required ModelIngestionStatus status { get; init; }
public required string? progressMessage { get; init; }
}
@@ -6,10 +6,10 @@ namespace Speckle.Sdk.Api.GraphQL.Models;
public sealed class UserProjectsUpdatedMessage : EventArgs
{
[JsonRequired]
public string id { get; init; }
public required string id { get; init; }
[JsonRequired]
public UserProjectsUpdatedMessageType type { get; init; }
public required UserProjectsUpdatedMessageType type { get; init; }
public Project? project { get; init; }
}
@@ -17,10 +17,10 @@ public sealed class UserProjectsUpdatedMessage : EventArgs
public sealed class ProjectCommentsUpdatedMessage : EventArgs
{
[JsonRequired]
public string id { get; init; }
public required string id { get; init; }
[JsonRequired]
public ProjectCommentsUpdatedMessageType type { get; init; }
public required ProjectCommentsUpdatedMessageType type { get; init; }
public Comment? comment { get; init; }
}
@@ -28,10 +28,10 @@ public sealed class ProjectCommentsUpdatedMessage : EventArgs
public sealed class ProjectFileImportUpdatedMessage : EventArgs
{
[JsonRequired]
public string id { get; init; }
public required string id { get; init; }
[JsonRequired]
public ProjectFileImportUpdatedMessageType type { get; init; }
public required ProjectFileImportUpdatedMessageType type { get; init; }
public FileUpload? upload { get; init; }
}
@@ -39,10 +39,10 @@ public sealed class ProjectFileImportUpdatedMessage : EventArgs
public sealed class ProjectModelsUpdatedMessage : EventArgs
{
[JsonRequired]
public string id { get; init; }
public required string id { get; init; }
[JsonRequired]
public ProjectModelsUpdatedMessageType type { get; init; }
public required ProjectModelsUpdatedMessageType type { get; init; }
public Model? model { get; init; }
}
@@ -50,10 +50,10 @@ public sealed class ProjectModelsUpdatedMessage : EventArgs
public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
{
[JsonRequired]
public string id { get; init; }
public required string id { get; init; }
[JsonRequired]
public ProjectPendingModelsUpdatedMessageType type { get; init; }
public required ProjectPendingModelsUpdatedMessageType type { get; init; }
public FileUpload? model { get; init; }
}
@@ -61,10 +61,10 @@ public sealed class ProjectPendingModelsUpdatedMessage : EventArgs
public sealed class ProjectUpdatedMessage : EventArgs
{
[JsonRequired]
public string id { get; init; }
public required string id { get; init; }
[JsonRequired]
public ProjectUpdatedMessageType type { get; init; }
public required ProjectUpdatedMessageType type { get; init; }
public Project? project { get; init; }
}
@@ -72,13 +72,22 @@ public sealed class ProjectUpdatedMessage : EventArgs
public sealed class ProjectVersionsUpdatedMessage : EventArgs
{
[JsonRequired]
public string id { get; init; }
public required string id { get; init; }
[JsonRequired]
public ProjectVersionsUpdatedMessageType type { get; init; }
public required ProjectVersionsUpdatedMessageType type { get; init; }
[JsonRequired]
public string modelId { get; init; }
public required string modelId { get; init; }
public Version? version { get; init; }
}
public sealed class ProjectModelIngestionUpdatedMessage : EventArgs
{
[JsonRequired]
public required ModelIngestion modelIngestion { get; init; }
[JsonRequired]
public required ProjectModelIngestionUpdatedMessageType type { get; init; }
}
@@ -0,0 +1,316 @@
using GraphQL;
using Speckle.Sdk.Api.GraphQL.Inputs;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Api.GraphQL.Models.Responses;
namespace Speckle.Sdk.Api.GraphQL.Resources;
public sealed class ModelIngestionResource
{
private readonly ISpeckleGraphQLClient _client;
internal ModelIngestionResource(ISpeckleGraphQLClient client)
{
_client = client;
}
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<ModelIngestion> Create(
ModelIngestionCreateInput input,
CancellationToken cancellationToken = default
)
{
//language=graphql
const string QUERY = """
mutation IngestionCreate($input: ModelIngestionCreateInput!) {
data: projectMutations {
data: modelIngestionMutations {
data: create(input: $input) {
id
createdAt
updatedAt
modelId
cancellationRequested
statusData {
... on HasModelIngestionStatus {
status
}
... on HasProgressMessage {
progressMessage
}
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
request,
cancellationToken
)
.ConfigureAwait(false);
return res.data.data.data;
}
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<ModelIngestion> UpdateProgress(
ModelIngestionUpdateInput input,
CancellationToken cancellationToken = default
)
{
//language=graphql
const string QUERY = """
mutation IngestionUpdateProgress(
$input: ModelIngestionUpdateInput!
) {
data: projectMutations {
data: modelIngestionMutations {
data: updateProgress(input: $input) {
id
createdAt
updatedAt
modelId
cancellationRequested
statusData {
... on HasModelIngestionStatus {
status
}
... on HasProgressMessage {
progressMessage
}
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
request,
cancellationToken
)
.ConfigureAwait(false);
return res.data.data.data;
}
/// <summary>
/// Request that the server completes the ingestion by creating a version
/// If successful, the job will be in a terminal "successful" state.
/// </summary>
/// <seealso cref="FailWithError"/>
/// <seealso cref="FailWithCancel"/>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns>The version id</returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<string> Complete(ModelIngestionSuccessInput input, CancellationToken cancellationToken = default)
{
//language=graphql
const string QUERY = """
mutation IngestionComplete($input: ModelIngestionSuccessInput!) {
data: projectMutations {
data: modelIngestionMutations {
data: completeWithVersion(input: $input) {
data:statusData {
... on ModelIngestionSuccessStatus {
data:versionId
}
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
var res = await _client
.ExecuteGraphQLRequest<
RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<RequiredResponse<string>>>>>
>(request, cancellationToken)
.ConfigureAwait(false);
return res.data.data.data.data.data;
}
/// <summary>
/// Fail the job with an error.
/// </summary>
/// <remarks>
/// For requested user cancellation, use <see cref="FailWithCancel"/> instead
/// </remarks>
/// <seealso cref="FailWithCancel"/>
/// <seealso cref="Complete"/>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<ModelIngestion> FailWithError(
ModelIngestionFailedInput input,
CancellationToken cancellationToken = default
)
{
//language=graphql
const string QUERY = """
mutation IngestionFailWithError($input: ModelIngestionFailedInput!) {
data: projectMutations {
data: modelIngestionMutations {
data: failWithError(input: $input) {
id
createdAt
updatedAt
modelId
cancellationRequested
statusData {
... on HasModelIngestionStatus {
status
}
... on HasProgressMessage {
progressMessage
}
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
request,
cancellationToken
)
.ConfigureAwait(false);
return res.data.data.data;
}
/// <summary>
/// Fail the ingestion with a <c>canceled</c> status.
/// 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
/// </summary>
/// <seealso cref="FailWithError"/>
/// <seealso cref="Complete"/>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<ModelIngestion> FailWithCancel(
ModelIngestionCancelledInput input,
CancellationToken cancellationToken = default
)
{
//language=graphql
const string QUERY = """
mutation IngestionFailWithCancel($input: ModelIngestionCancelledInput!) {
data: projectMutations {
data: modelIngestionMutations {
data: failWithCancel(input: $input) {
id
createdAt
updatedAt
modelId
cancellationRequested
statusData {
... on HasModelIngestionStatus {
status
}
... on HasProgressMessage {
progressMessage
}
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
request,
cancellationToken
)
.ConfigureAwait(false);
return res.data.data.data;
}
/// <summary>
/// Request that the <see cref="ModelIngestion"/> is canceled.
/// </summary>
/// <remarks>
/// Note simply calling this mutation does not imediatly cancel, it doesn't even guarantee it will be canceled at all.
/// It's up to the client to observe this cancellation request
/// via <see cref="SubscriptionResource.CreateProjectModelIngestionCancellationRequestedSubscription"/>
/// and report it as canceled via <see cref="ModelIngestionResource.FailWithCancel"/>
/// See "cooperative cancellation pattern"
/// </remarks>
/// <seealso cref="FailWithError"/>
/// <seealso cref="Complete"/>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
public async Task<ModelIngestion> RequestCancellation(
ModelIngestionCancelledInput input,
CancellationToken cancellationToken = default
)
{
//language=graphql
const string QUERY = """
mutation IngestionRequestCancellation($input: ModelIngestionRequestCancellationInput!) {
data: projectMutations {
data: modelIngestionMutations {
data: requestCancellation (input: $input) {
id
createdAt
updatedAt
modelId
cancellationRequested
statusData {
... on HasModelIngestionStatus {
status
}
... on HasProgressMessage {
progressMessage
}
}
}
}
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
var res = await _client
.ExecuteGraphQLRequest<RequiredResponse<RequiredResponse<RequiredResponse<ModelIngestion>>>>(
request,
cancellationToken
)
.ConfigureAwait(false);
return res.data.data.data;
}
}
@@ -1,4 +1,5 @@
using GraphQL;
using Speckle.Sdk.Api.GraphQL.Enums;
using Speckle.Sdk.Api.GraphQL.Inputs;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Api.GraphQL.Models.Responses;
@@ -212,6 +213,52 @@ public sealed class SubscriptionResource : IDisposable
return subscription;
}
/// <summary>Subscribe to a cancellation request being made for a Model Ingestion</summary>
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
public Subscription<ProjectModelIngestionUpdatedMessage> CreateProjectModelIngestionUpdatedSubscription(
ProjectModelIngestionSubscriptionInput input
)
{
//language=graphql
const string QUERY = """
subscription IngestionUpdated(
$input: ProjectModelIngestionSubscriptionInput!
) {
data: projectModelIngestionUpdated(input: $input) {
modelIngestion {
id
createdAt
updatedAt
}
type
}
}
""";
GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
Subscription<ProjectModelIngestionUpdatedMessage> subscription = new(_client, request);
_subscriptions.Add(subscription);
return subscription;
}
/// <summary>Subscribe to a cancellation request being made for a Model Ingestion</summary>
/// <remarks><inheritdoc cref="CreateUserProjectsUpdatedSubscription"/></remarks>
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
public Subscription<ProjectModelIngestionUpdatedMessage> CreateProjectModelIngestionCancellationRequestedSubscription(
string ingestionId,
string projectId
)
{
return CreateProjectModelIngestionUpdatedSubscription(
new ProjectModelIngestionSubscriptionInput(
projectId,
new(ingestionId, null),
ProjectModelIngestionUpdatedMessageType.cancellationRequested
)
);
}
public void Dispose()
{
foreach (var subscription in _subscriptions)
@@ -1,2 +1,2 @@
schema: https://app.speckle.systems/graphql
schema: http://localhost/graphql
documents: '**/*.graphql'
+1 -1
View File
@@ -1,2 +1,2 @@
schema: https://app.speckle.systems/graphql
schema: http://localhost/graphql
documents: '**/*.graphql'
@@ -0,0 +1,8 @@
{
"Type": "AggregateException",
"InnerException": {
"Data": {},
"Message": "NOT_FOUND_ERROR: Model ingestion was not found",
"Type": "SpeckleGraphQLException"
}
}
@@ -0,0 +1,8 @@
{
"Type": "AggregateException",
"InnerException": {
"Data": {},
"Message": "STREAM_NOT_FOUND: Project not found",
"Type": "SpeckleGraphQLStreamNotFoundException"
}
}
@@ -0,0 +1,8 @@
{
"Type": "AggregateException",
"InnerException": {
"Data": {},
"Message": "NOT_FOUND_ERROR: Model ingestion was not found",
"Type": "SpeckleGraphQLException"
}
}
@@ -0,0 +1,73 @@
using System.Reflection;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL.Inputs;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Api.GraphQL.Resources;
using Speckle.Sdk.Host;
using Speckle.Sdk.Models;
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
public sealed class ModelIngestionResourceExceptionalTests : IAsyncLifetime
{
private IClient _testUser;
private ModelIngestionResource Sut => _testUser.Ingestion;
private Project _project;
private Model _model;
public Task DisposeAsync() => Task.CompletedTask;
public async Task InitializeAsync()
{
TypeLoader.Reset();
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
_testUser = await Fixtures.SeedUserWithClient();
_project = await _testUser.Project.Create(new("Test project", "", null));
_model = await _testUser.Model.Create(new("Test Model 1", "", _project.id));
}
[Fact]
public async Task CreateIngestionNonExistentProject()
{
var createInput = new ModelIngestionCreateInput(
_model.id,
"Doesn't exist...",
"Starting processing",
new(".NET test runner", "0.0.0", null, null)
);
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
{
_ = await Sut.Create(createInput);
});
await Verify(ex);
}
[Fact]
public async Task UpdateNonExistentNonExistent()
{
var updateInput = new ModelIngestionUpdateInput("Doesn't exist", _project.id, "Can't be", 0.5);
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
{
_ = await Sut.UpdateProgress(updateInput);
});
await Verify(ex);
}
[Fact]
public async Task CancelNonExistentIngestion()
{
var input = new ModelIngestionCancelledInput(
"Non-existent-ingestion",
_project.id,
cancellationMessage: "This was cancelled for testing purposes"
);
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
{
_ = await Sut.FailWithCancel(input);
});
await Verify(ex);
}
}
@@ -0,0 +1,138 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL.Enums;
using Speckle.Sdk.Api.GraphQL.Inputs;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Api.GraphQL.Resources;
using Speckle.Sdk.Host;
using Speckle.Sdk.Models;
using Speckle.Sdk.Transports;
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
public sealed class ModelIngestionResourceTests : IAsyncLifetime
{
private IClient _testUser;
private ModelIngestionResource Sut => _testUser.Ingestion;
private Project _project;
private Model _model;
private IOperations _operations;
public Task DisposeAsync() => Task.CompletedTask;
public async Task InitializeAsync()
{
TypeLoader.Reset();
TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly());
var serviceProvider = TestServiceSetup.GetServiceProvider();
_operations = serviceProvider.GetRequiredService<IOperations>();
_testUser = await Fixtures.SeedUserWithClient();
_project = await _testUser.Project.Create(new("Test project", "", null));
_model = await _testUser.Model.Create(new("Test Model 1", "", _project.id));
}
[Fact]
public async Task CreateAndError()
{
var createInput = new ModelIngestionCreateInput(
_model.id,
_project.id,
"Starting processing",
new(".NET test runner", "0.0.0", null, null)
);
ModelIngestion ingest = await Sut.Create(createInput);
var errorInput = new ModelIngestionFailedInput(ingest.id, _project.id, "A bad thing happened", "Over hear!");
var res = await Sut.FailWithError(errorInput);
Assert.Equal(ingest.id, res.id);
}
[Fact]
public async Task CreateAndUpdate()
{
var createInput = new ModelIngestionCreateInput(
_model.id,
_project.id,
"Starting processing",
new(".NET test runner", "0.0.0", null, null)
);
ModelIngestion ingest = await Sut.Create(createInput);
await Update(null, "None");
await Update(0.1, "0.1");
await Update(0.5, "Whoa-oh! We're half way there!");
await Update(1, "Finished");
await Update(0.2, "Back to processing again");
async Task Update(double? progress, string message)
{
var updateInput = new ModelIngestionUpdateInput(ingest.id, _project.id, message, progress);
var res = await Sut.UpdateProgress(updateInput);
Assert.Equal(message, res.statusData.progressMessage);
Assert.False(res.cancellationRequested);
Assert.Equal(ModelIngestionStatus.processing, res.statusData.status);
}
}
[Fact]
public async Task CreateAndCancel()
{
var createInput = new ModelIngestionCreateInput(
_model.id,
_project.id,
"Starting processing",
new(".NET test runner", "0.0.0", null, null)
);
ModelIngestion ingest = await Sut.Create(createInput);
var input = new ModelIngestionCancelledInput(
ingest.id,
_project.id,
cancellationMessage: "This was cancelled for testing purposes"
);
var res = await Sut.FailWithCancel(input);
Assert.Equal(ingest.id, res.id);
}
[Fact]
public async Task CreateAndComplete()
{
ModelIngestionCreateInput createInput = new(
_model.id,
_project.id,
"Starting processing",
new(".NET test runner", "0.0.0", null, null)
);
ModelIngestion ingest = await Sut.Create(createInput);
Base myObject = Fixtures.GenerateNestedObject();
var sendResult = await _operations.Send2(
_testUser.ServerUrl,
_project.id,
_testUser.Account.token,
myObject,
new Progress<ProgressArgs>(x =>
{
var updateInput = new ModelIngestionUpdateInput(
ingest.id,
_project.id,
$"{x.Count} / {x.Total}",
x.Total == null ? null : x.Count / x.Total
);
_ = Sut.UpdateProgress(updateInput).Result;
}),
CancellationToken.None,
new(true, true)
);
ModelIngestionSuccessInput finish = new(ingest.id, _project.id, sendResult.RootId);
string versionId = await Sut.Complete(finish);
Version version = await _testUser.Version.Get(versionId, _project.id);
Assert.Equal(version.id, versionId);
Assert.Equal(sendResult.RootId, version.referencedObject);
}
}
@@ -10,7 +10,12 @@ namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
public class SubscriptionResourceTests : IAsyncLifetime
{
private const int WAIT_PERIOD = 300;
#if DEBUG
private const int WAIT_PERIOD = 3000; // WSL is slow AF, so for local runs, we're being extra generous
#else
private const int WAIT_PERIOD = 400; // For CI runs, a much smaller wait time is acceptable
#endif
private const int TIMEOUT = WAIT_PERIOD + WAIT_PERIOD + 400;
private IClient _testUser;
private Project _testProject;
private Model _testModel;
@@ -32,106 +37,122 @@ public class SubscriptionResourceTests : IAsyncLifetime
_testVersion = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.id);
}
[Fact]
[Fact(Timeout = TIMEOUT)]
public async Task UserProjectsUpdated_SubscriptionIsCalled()
{
UserProjectsUpdatedMessage? subscriptionMessage = null;
TaskCompletionSource<UserProjectsUpdatedMessage> tcs = new();
using var sub = Sut.CreateUserProjectsUpdatedSubscription();
sub.Listeners += (_, message) => subscriptionMessage = message;
sub.Listeners += (_, message) => tcs.SetResult(message);
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
var created = await _testUser.Project.Create(new(null, null, null));
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
var subscriptionMessage = await tcs.Task;
subscriptionMessage.Should().NotBeNull();
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(UserProjectsUpdatedMessageType.ADDED);
subscriptionMessage.project.Should().NotBeNull();
}
[Fact]
[Fact(Timeout = TIMEOUT)]
public async Task ProjectModelsUpdated_SubscriptionIsCalled()
{
ProjectModelsUpdatedMessage? subscriptionMessage = null;
TaskCompletionSource<ProjectModelsUpdatedMessage> tcs = new();
using var sub = Sut.CreateProjectModelsUpdatedSubscription(_testProject.id);
sub.Listeners += (_, message) => subscriptionMessage = message;
sub.Listeners += (_, message) => tcs.SetResult(message);
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
CreateModelInput input = new("my model", "myDescription", _testProject.id);
var created = await _testUser.Model.Create(input);
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
var subscriptionMessage = await tcs.Task;
subscriptionMessage.Should().NotBeNull();
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(ProjectModelsUpdatedMessageType.CREATED);
subscriptionMessage.model.Should().NotBeNull();
}
[Fact]
[Fact(Timeout = TIMEOUT)]
public async Task ProjectUpdated_SubscriptionIsCalled()
{
ProjectUpdatedMessage? subscriptionMessage = null;
using var sub = Sut.CreateProjectUpdatedSubscription(_testProject.id);
sub.Listeners += (_, message) => subscriptionMessage = message;
TaskCompletionSource<ProjectUpdatedMessage> tcs = new();
using Subscription<ProjectUpdatedMessage> sub = Sut.CreateProjectUpdatedSubscription(_testProject.id);
sub.Listeners += (_, message) => tcs.SetResult(message);
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
var input = new ProjectUpdateInput(_testProject.id, "This is my new name");
var created = await _testUser.Project.Update(input);
ProjectUpdateInput input = new(_testProject.id, "This is my new name");
Project created = await _testUser.Project.Update(input);
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
ProjectUpdatedMessage subscriptionMessage = await tcs.Task;
subscriptionMessage.Should().NotBeNull();
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(ProjectUpdatedMessageType.UPDATED);
subscriptionMessage.project.Should().NotBeNull();
}
[Fact]
[Fact(Timeout = TIMEOUT)]
public async Task ProjectVersionsUpdated_SubscriptionIsCalled()
{
ProjectVersionsUpdatedMessage? subscriptionMessage = null;
TaskCompletionSource<ProjectVersionsUpdatedMessage> tcs = new();
using var sub = Sut.CreateProjectVersionsUpdatedSubscription(_testProject.id);
sub.Listeners += (_, message) => subscriptionMessage = message;
sub.Listeners += (_, message) => tcs.SetResult(message);
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
var created = await Fixtures.CreateVersion(_testUser, _testProject.id, _testModel.id);
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
var subscriptionMessage = await tcs.Task;
subscriptionMessage.Should().NotBeNull();
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(ProjectVersionsUpdatedMessageType.CREATED);
subscriptionMessage.version.Should().NotBeNull();
}
[Fact(Skip = CommentResourceTests.SERVER_SKIP_MESSAGE)]
[Fact(Skip = CommentResourceTests.SERVER_SKIP_MESSAGE, Timeout = TIMEOUT)]
public async Task ProjectCommentsUpdated_SubscriptionIsCalled()
{
string resourceIdString = $"{_testProject.id},{_testModel.id},{_testVersion}";
ProjectCommentsUpdatedMessage? subscriptionMessage = null;
TaskCompletionSource<ProjectCommentsUpdatedMessage> tcs = new();
using var sub = Sut.CreateProjectCommentsUpdatedSubscription(new(_testProject.id, resourceIdString));
sub.Listeners += (_, message) => subscriptionMessage = message;
sub.Listeners += (_, message) => tcs.SetResult(message);
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
var created = await Fixtures.CreateComment(_testUser, _testProject.id, _testModel.id, _testVersion.id);
await Task.Delay(WAIT_PERIOD); // Give time for subscription to be triggered
var subscriptionMessage = await tcs.Task;
subscriptionMessage.Should().NotBeNull();
subscriptionMessage!.id.Should().Be(created.id);
subscriptionMessage.id.Should().Be(created.id);
subscriptionMessage.type.Should().Be(ProjectCommentsUpdatedMessageType.CREATED);
subscriptionMessage.comment.Should().NotBeNull();
}
[Fact(Timeout = TIMEOUT)]
public async Task ProjectModelIngestionCancellationRequested_SubscriptionIsCalled()
{
ModelIngestion ingestion = await _testUser.Ingestion.Create(
new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
);
TaskCompletionSource<ProjectModelIngestionUpdatedMessage> tcs = new();
using var sub = Sut.CreateProjectModelIngestionCancellationRequestedSubscription(_testProject.id, ingestion.id);
sub.Listeners += (_, message) => tcs.SetResult(message);
await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
await _testUser.Ingestion.RequestCancellation(new(ingestion.id, _testProject.id, "please cancel"));
var subscriptionMessage = await tcs.Task;
subscriptionMessage.Should().NotBeNull();
subscriptionMessage.modelIngestion.id.Should().Be(ingestion.id);
}
}
@@ -1,85 +0,0 @@
using FluentAssertions;
using GraphQL.Client.Http;
using Microsoft.Extensions.DependencyInjection;
using Speckle.Sdk.Api.GraphQL.Models;
using Speckle.Sdk.Credentials;
namespace Speckle.Sdk.Tests.Integration.Credentials;
public class UserServerInfoTests : IAsyncLifetime
{
private Account _acc;
public Task DisposeAsync() => Task.CompletedTask;
public async Task InitializeAsync()
{
_acc = await Fixtures.SeedUser();
}
[Fact]
public async Task IsFrontEnd2True()
{
ServerInfo result = await Fixtures
.ServiceProvider.GetRequiredService<IAccountManager>()
.GetServerInfo(new("https://app.speckle.systems/"));
result.Should().NotBeNull();
result.frontend2.Should().BeTrue();
}
[Fact]
public async Task GetServerInfo_ExpectFail_NoServer()
{
Uri serverUrl = new("http://invalidserver.local");
await FluentActions
.Invoking(async () =>
await Fixtures.ServiceProvider.GetRequiredService<IAccountManager>().GetServerInfo(serverUrl)
)
.Should()
.ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task GetUserInfo()
{
Uri serverUrl = new(_acc.serverInfo.url);
UserInfo result = await Fixtures
.ServiceProvider.GetRequiredService<IAccountManager>()
.GetUserInfo(_acc.token, serverUrl);
result.id.Should().Be(_acc.userInfo.id);
result.name.Should().Be(_acc.userInfo.name);
result.email.Should().Be(_acc.userInfo.email);
result.company.Should().Be(_acc.userInfo.company);
result.avatar.Should().Be(_acc.userInfo.avatar);
}
[Fact]
public async Task GetUserInfo_ExpectFail_NoServer()
{
Uri serverUrl = new("http://invalidserver.local");
await FluentActions
.Invoking(async () =>
await Fixtures.ServiceProvider.GetRequiredService<IAccountManager>().GetUserInfo("", serverUrl)
)
.Should()
.ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task GetUserInfo_ExpectFail_NoUser()
{
Uri serverUrl = new(_acc.serverInfo.url);
await FluentActions
.Invoking(async () =>
await Fixtures
.ServiceProvider.GetRequiredService<IAccountManager>()
.GetUserInfo("Bearer 08913c3c1e7ac65d779d1e1f11b942a44ad9672ca9", serverUrl)
)
.Should()
.ThrowAsync<GraphQLHttpRequestException>();
}
}
@@ -21,7 +21,7 @@ namespace Speckle.Sdk.Tests.Integration;
public static class Fixtures
{
public static readonly ServerInfo Server = new() { url = "http://localhost:3000", name = "Docker Server" };
public static readonly ServerInfo Server = new() { url = "http://localhost", name = "Docker Server" };
public static IServiceProvider ServiceProvider { get; set; }