diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml
index db7638a9..76d5fe88 100644
--- a/.github/workflows/integration-test.yml
+++ b/.github/workflows/integration-test.yml
@@ -6,10 +6,12 @@ on:
docker-compose-file:
required: true
type: string
- use-github-container-registry:
+ use-internal-image:
default: false
type: boolean
-
+ secrets:
+ CODECOV_TOKEN:
+ required: true
jobs:
integration-test:
env:
@@ -27,7 +29,7 @@ jobs:
cache-dependency-path: "**/packages.lock.json"
- name: 🔐 Login to Github Container Registry
- if: ${{ inputs.use-github-container-registry }}
+ if: ${{ inputs.use-internal-image }}
uses: docker/login-action@v3
with:
registry: "ghcr.io"
@@ -43,11 +45,18 @@ jobs:
- name: 🏗️ Build
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
- - name: 🔨 Integration Tests
+ - name: 🔨 Integration Tests against Public Server
+ if: ${{ !inputs.use-internal-image }}
+ run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Internal)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
+
+ - 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
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
+ continue-on-error: true
with:
+ fail_ci_if_error: true
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 89453c4f..8068e7c2 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -39,7 +39,9 @@ jobs:
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
+ continue-on-error: true
with:
+ fail_ci_if_error: true
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
@@ -47,9 +49,13 @@ jobs:
uses: "./.github/workflows/integration-test.yml"
with:
docker-compose-file: "docker-compose-internal.yml"
- use-github-container-registry: true
-
+ use-internal-image: true
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
integration-test-public:
uses: "./.github/workflows/integration-test.yml"
with:
docker-compose-file: "docker-compose.yml"
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7c87bcff..669d0541 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -48,7 +48,9 @@ jobs:
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
+ continue-on-error: true
with:
+ fail_ci_if_error: true
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 00000000..cd415749
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1 @@
+dotnet 8.0.400
diff --git a/Speckle.Sdk.slnx b/Speckle.Sdk.slnx
index aee97c65..8b77f1f3 100644
--- a/Speckle.Sdk.slnx
+++ b/Speckle.Sdk.slnx
@@ -17,6 +17,7 @@
+
diff --git a/src/Speckle.Sdk/Api/GraphQL/Client.cs b/src/Speckle.Sdk/Api/GraphQL/Client.cs
index 7d6099de..abe96585 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Client.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Client.cs
@@ -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]
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/FileUploadConversionStatus.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/FileUploadConversionStatus.cs
index 327f947e..21f0e49d 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/FileUploadConversionStatus.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/FileUploadConversionStatus.cs
@@ -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)
+///
+/// This enum isn't explicitly defined in the schema, instead its usages are int typed (But represent an enum)
+///
public enum FileUploadConversionStatus
{
Queued = 0,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ModelIngestionStatus.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ModelIngestionStatus.cs
new file mode 100644
index 00000000..85ca92dc
--- /dev/null
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ModelIngestionStatus.cs
@@ -0,0 +1,14 @@
+// ReSharper disable InconsistentNaming
+namespace Speckle.Sdk.Api.GraphQL.Enums;
+
+///
+/// string based enum
+///
+public enum ModelIngestionStatus
+{
+ cancelled,
+ failed,
+ processing,
+ queued,
+ success,
+}
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectCommentsUpdatedMessageType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectCommentsUpdatedMessageType.cs
index 2500425d..8bd5937c 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectCommentsUpdatedMessageType.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectCommentsUpdatedMessageType.cs
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum ProjectCommentsUpdatedMessageType
{
ARCHIVED,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectFileImportUpdatedMessageType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectFileImportUpdatedMessageType.cs
index b1947272..9c53eebd 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectFileImportUpdatedMessageType.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectFileImportUpdatedMessageType.cs
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum ProjectFileImportUpdatedMessageType
{
CREATED,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectModelIngestionUpdatedMessageType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectModelIngestionUpdatedMessageType.cs
new file mode 100644
index 00000000..2ec41934
--- /dev/null
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectModelIngestionUpdatedMessageType.cs
@@ -0,0 +1,13 @@
+// ReSharper disable InconsistentNaming
+namespace Speckle.Sdk.Api.GraphQL.Enums;
+
+///
+/// string based enum
+///
+public enum ProjectModelIngestionUpdatedMessageType
+{
+ cancellationRequested,
+ created,
+ deleted,
+ updated,
+}
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectModelsUpdatedMessageType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectModelsUpdatedMessageType.cs
index b6316fb0..11dc5440 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectModelsUpdatedMessageType.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectModelsUpdatedMessageType.cs
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum ProjectModelsUpdatedMessageType
{
CREATED,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectPendingModelsUpdatedMessageType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectPendingModelsUpdatedMessageType.cs
index 8e24a9c8..d2735ab9 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectPendingModelsUpdatedMessageType.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectPendingModelsUpdatedMessageType.cs
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum ProjectPendingModelsUpdatedMessageType
{
CREATED,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectUpdatedMessageType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectUpdatedMessageType.cs
index a056708c..7e51e635 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectUpdatedMessageType.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectUpdatedMessageType.cs
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum ProjectUpdatedMessageType
{
DELETED,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectVersionsUpdatedMessageType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectVersionsUpdatedMessageType.cs
index 977c93f8..2f83fbd4 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectVersionsUpdatedMessageType.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectVersionsUpdatedMessageType.cs
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum ProjectVersionsUpdatedMessageType
{
CREATED,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectVisibility.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectVisibility.cs
index b0692127..c019dd38 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectVisibility.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ProjectVisibility.cs
@@ -1,5 +1,9 @@
+// ReSharper disable InconsistentNaming
namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum ProjectVisibility
{
Private,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/ResourceType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/ResourceType.cs
index d592e308..ecb8ba29 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/ResourceType.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/ResourceType.cs
@@ -1,5 +1,9 @@
-namespace Speckle.Sdk.Api.GraphQL.Enums;
+// ReSharper disable InconsistentNaming
+namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum ResourceType
{
commit,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Enums/UserProjectsUpdatedMessageType.cs b/src/Speckle.Sdk/Api/GraphQL/Enums/UserProjectsUpdatedMessageType.cs
index 1ad08ce3..4891c110 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Enums/UserProjectsUpdatedMessageType.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Enums/UserProjectsUpdatedMessageType.cs
@@ -1,5 +1,8 @@
namespace Speckle.Sdk.Api.GraphQL.Enums;
+///
+/// string based enum
+///
public enum UserProjectsUpdatedMessageType
{
ADDED,
diff --git a/src/Speckle.Sdk/Api/GraphQL/Inputs/ModelIngestionInputs.cs b/src/Speckle.Sdk/Api/GraphQL/Inputs/ModelIngestionInputs.cs
new file mode 100644
index 00000000..9f243b11
--- /dev/null
+++ b/src/Speckle.Sdk/Api/GraphQL/Inputs/ModelIngestionInputs.cs
@@ -0,0 +1,65 @@
+using Speckle.Newtonsoft.Json;
+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 ModelIngestionStartProcessingInput(
+ string ingestionId,
+ string projectId,
+ string progressMessage,
+ SourceDataInput sourceData
+);
+
+public record ModelIngestionRequeueInput(string ingestionId, string projectId, string progressMessage);
+
+public record ProjectModelIngestionSubscriptionInput(
+ string projectId,
+ ModelIngestionReference ingestionReference,
+ [property: JsonIgnore] ProjectModelIngestionUpdatedMessageType messageType
+)
+{
+ // The Newtonsoft serializer is setup to handle SCREAMING_CASE enums.
+ // But the API requires the enum to look exactly like they are
+ [JsonProperty(nameof(messageType))]
+ public string serializedType => messageType.ToString();
+}
+
+///
+/// @oneOf i.e. server expects either or , but not both.
+///
+///
+///
+public record ModelIngestionReference(string? ingestionId, string? modelId);
diff --git a/src/Speckle.Sdk/Api/GraphQL/Models/ModelIngestion.cs b/src/Speckle.Sdk/Api/GraphQL/Models/ModelIngestion.cs
new file mode 100644
index 00000000..58d0dc67
--- /dev/null
+++ b/src/Speckle.Sdk/Api/GraphQL/Models/ModelIngestion.cs
@@ -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; }
+}
diff --git a/src/Speckle.Sdk/Api/GraphQL/Models/ModelIngestionStatusData.cs b/src/Speckle.Sdk/Api/GraphQL/Models/ModelIngestionStatusData.cs
new file mode 100644
index 00000000..a195bdfc
--- /dev/null
+++ b/src/Speckle.Sdk/Api/GraphQL/Models/ModelIngestionStatusData.cs
@@ -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; }
+}
diff --git a/src/Speckle.Sdk/Api/GraphQL/Models/SubscriptionMessages.cs b/src/Speckle.Sdk/Api/GraphQL/Models/SubscriptionMessages.cs
index 327bac77..0f4ebb07 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Models/SubscriptionMessages.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Models/SubscriptionMessages.cs
@@ -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; }
+}
diff --git a/src/Speckle.Sdk/Api/GraphQL/Resources/ModelIngestionResource.cs b/src/Speckle.Sdk/Api/GraphQL/Resources/ModelIngestionResource.cs
new file mode 100644
index 00000000..bec1c5e9
--- /dev/null
+++ b/src/Speckle.Sdk/Api/GraphQL/Resources/ModelIngestionResource.cs
@@ -0,0 +1,440 @@
+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;
+
+///
+/// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+///
+public sealed class ModelIngestionResource
+{
+ private readonly ISpeckleGraphQLClient _client;
+
+ internal ModelIngestionResource(ISpeckleGraphQLClient client)
+ {
+ _client = client;
+ }
+
+ ///
+ /// Create a new model ingestion
+ ///
+ ///
+ /// The model ingestion created will have a processing state (not queued). This mutation is designed to be used
+ /// by client/connectors that are immediately processing
+ /// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task 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>>>(
+ request,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return res.data.data.data;
+ }
+
+ ///
+ /// For File Import / Cloud integrations only
+ ///
+ ///
+ /// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task StartProcessing(
+ ModelIngestionStartProcessingInput input,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //language=graphql
+ const string QUERY = """
+ mutation IngestionStartProcessing($input: ModelIngestionStartProcessingInput!) {
+ data: projectMutations {
+ data: modelIngestionMutations {
+ data: startProcessing(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>>>(
+ request,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return res.data.data.data;
+ }
+
+ ///
+ /// For File Import / Cloud integrations only
+ ///
+ ///
+ /// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task Requeue(
+ ModelIngestionRequeueInput input,
+ CancellationToken cancellationToken = default
+ )
+ {
+ //language=graphql
+ const string QUERY = """
+ mutation IngestionStartProcessing($input: ModelIngestionRequeueInput!) {
+ data: projectMutations {
+ data: modelIngestionMutations {
+ data: requeue(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>>>(
+ request,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return res.data.data.data;
+ }
+
+ ///
+ /// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task 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>>>(
+ request,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return res.data.data.data;
+ }
+
+ ///
+ /// Request that the server completes the ingestion by creating a version
+ /// If successful, the job will be in a terminal "successful" state.
+ ///
+ ///
+ /// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// The version id
+ ///
+ public async Task 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>>>>
+ >(request, cancellationToken)
+ .ConfigureAwait(false);
+
+ return res.data.data.data.data.data;
+ }
+
+ ///
+ /// Fail the job with an error.
+ ///
+ ///
+ /// For requested user cancellation, use instead
+ /// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task 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>>>(
+ request,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return res.data.data.data;
+ }
+
+ ///
+ /// Fail the ingestion with a canceled status.
+ /// This should only be done if the user has explicitly requested cancellation
+ /// Other forms of cancellation use .
+ /// The ingestion should then enter a terminal "canceled" state.
+ /// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task 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>>>(
+ request,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return res.data.data.data;
+ }
+
+ ///
+ /// Request that the is canceled.
+ ///
+ ///
+ /// 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
+ /// and report it as canceled via
+ /// See "cooperative cancellation pattern"
+ /// Model Ingestion API is available for server versions 3.0.3-alpha.583 and above
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task 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>>>(
+ request,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ return res.data.data.data;
+ }
+}
diff --git a/src/Speckle.Sdk/Api/GraphQL/Resources/SubscriptionResource.cs b/src/Speckle.Sdk/Api/GraphQL/Resources/SubscriptionResource.cs
index f23edae9..9a014cff 100644
--- a/src/Speckle.Sdk/Api/GraphQL/Resources/SubscriptionResource.cs
+++ b/src/Speckle.Sdk/Api/GraphQL/Resources/SubscriptionResource.cs
@@ -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,60 @@ public sealed class SubscriptionResource : IDisposable
return subscription;
}
+ /// Subscribe to a cancellation request being made for a Model Ingestion
+ ///
+ ///
+ public Subscription CreateProjectModelIngestionUpdatedSubscription(
+ ProjectModelIngestionSubscriptionInput input
+ )
+ {
+ //language=graphql
+ const string QUERY = """
+ subscription IngestionUpdated($input: ProjectModelIngestionSubscriptionInput!) {
+ data: projectModelIngestionUpdated(input: $input) {
+ modelIngestion {
+ id
+ createdAt
+ updatedAt
+ modelId
+ cancellationRequested
+ statusData {
+ ... on HasModelIngestionStatus {
+ status
+ }
+ ... on HasProgressMessage {
+ progressMessage
+ }
+ }
+ }
+ type
+ }
+ }
+ """;
+ GraphQLRequest request = new() { Query = QUERY, Variables = new { input } };
+
+ Subscription subscription = new(_client, request);
+ _subscriptions.Add(subscription);
+ return subscription;
+ }
+
+ /// Subscribe to a cancellation request being made for a Model Ingestion
+ ///
+ ///
+ public Subscription CreateProjectModelIngestionCancellationRequestedSubscription(
+ string ingestionId,
+ string projectId
+ )
+ {
+ return CreateProjectModelIngestionUpdatedSubscription(
+ new ProjectModelIngestionSubscriptionInput(
+ projectId,
+ new(ingestionId, null),
+ ProjectModelIngestionUpdatedMessageType.cancellationRequested
+ )
+ );
+ }
+
public void Dispose()
{
foreach (var subscription in _subscriptions)
diff --git a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.CancelNonExistentIngestion.verified.json b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.CancelNonExistentIngestion.verified.json
new file mode 100644
index 00000000..e743803c
--- /dev/null
+++ b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.CancelNonExistentIngestion.verified.json
@@ -0,0 +1,8 @@
+{
+ "Type": "AggregateException",
+ "InnerException": {
+ "Data": {},
+ "Message": "NOT_FOUND_ERROR: Model ingestion not found",
+ "Type": "SpeckleGraphQLException"
+ }
+}
diff --git a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.CreateIngestionNonExistentProject.verified.json b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.CreateIngestionNonExistentProject.verified.json
new file mode 100644
index 00000000..04a2199a
--- /dev/null
+++ b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.CreateIngestionNonExistentProject.verified.json
@@ -0,0 +1,8 @@
+{
+ "Type": "AggregateException",
+ "InnerException": {
+ "Data": {},
+ "Message": "STREAM_NOT_FOUND: Project not found",
+ "Type": "SpeckleGraphQLStreamNotFoundException"
+ }
+}
diff --git a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.UpdateNonExistentNonExistent.verified.json b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.UpdateNonExistentNonExistent.verified.json
new file mode 100644
index 00000000..e743803c
--- /dev/null
+++ b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.UpdateNonExistentNonExistent.verified.json
@@ -0,0 +1,8 @@
+{
+ "Type": "AggregateException",
+ "InnerException": {
+ "Data": {},
+ "Message": "NOT_FOUND_ERROR: Model ingestion not found",
+ "Type": "SpeckleGraphQLException"
+ }
+}
diff --git a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.cs b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.cs
new file mode 100644
index 00000000..0bae1526
--- /dev/null
+++ b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceExceptionalTests.cs
@@ -0,0 +1,74 @@
+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;
+
+[Trait("Server", "Internal")]
+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(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(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(async () =>
+ {
+ _ = await Sut.FailWithCancel(input);
+ });
+ await Verify(ex);
+ }
+}
diff --git a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceTests.cs b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceTests.cs
new file mode 100644
index 00000000..51f52122
--- /dev/null
+++ b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/ModelIngestionResourceTests.cs
@@ -0,0 +1,177 @@
+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;
+
+[Trait("Server", "Internal")]
+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();
+
+ _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(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);
+ }
+
+ [Fact]
+ public async Task TestRequeue()
+ {
+ //Not sure if is desirable that ingestions created by the modelIngestionMutations.create mutation can be re-queued
+ //But the server allows it, so we test it
+ var createInput = new ModelIngestionCreateInput(
+ _model.id,
+ _project.id,
+ "Starting processing",
+ new(".NET test runner", "0.0.0", null, null)
+ );
+ var ingestion = await Sut.Create(createInput);
+ var res = await Sut.Requeue(new(ingestion.id, _project.id, "we'll try and requeue this ingestion"));
+
+ Assert.Equal(ingestion.id, res.id);
+ Assert.Equal(ModelIngestionStatus.queued, res.statusData.status);
+ }
+
+ [Fact]
+ public async Task TestStartProcessing()
+ {
+ //Not sure if is desirable that StartProcessing can be used by ingestions created by the modelIngestionMutations.create mutation
+ //But the server allows it, so we test it
+ var createInput = new ModelIngestionCreateInput(
+ _model.id,
+ _project.id,
+ "Starting processing",
+ new(".NET test runner", "0.0.0", null, null)
+ );
+ var ingestion = await Sut.Create(createInput);
+ var res = await Sut.StartProcessing(
+ new(ingestion.id, _project.id, "", new SourceDataInput("what", "happens", "now", 0))
+ );
+
+ Assert.Equal(ingestion.id, res.id);
+ Assert.Equal(ModelIngestionStatus.processing, res.statusData.status);
+ }
+}
diff --git a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/SubscriptionResourceTests.cs b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/SubscriptionResourceTests.cs
index f56fffa0..63e4f780 100644
--- a/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/SubscriptionResourceTests.cs
+++ b/tests/Speckle.Sdk.Tests.Integration/Api/GraphQL/Resources/SubscriptionResourceTests.cs
@@ -1,4 +1,4 @@
-using FluentAssertions;
+using FluentAssertions;
using Speckle.Sdk.Api;
using Speckle.Sdk.Api.GraphQL.Enums;
using Speckle.Sdk.Api.GraphQL.Inputs;
@@ -11,7 +11,7 @@ namespace Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
public class SubscriptionResourceTests : IAsyncLifetime
{
#if DEBUG
- private const int WAIT_PERIOD = 3000; // WSL is slow AF, so for local runs, we're being extra generous
+ private const int WAIT_PERIOD = 4000; // 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
@@ -80,15 +80,15 @@ public class SubscriptionResourceTests : IAsyncLifetime
public async Task ProjectUpdated_SubscriptionIsCalled()
{
TaskCompletionSource tcs = new();
- using var sub = Sut.CreateProjectUpdatedSubscription(_testProject.id);
+ using Subscription 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);
- var subscriptionMessage = await tcs.Task;
+ ProjectUpdatedMessage subscriptionMessage = await tcs.Task;
subscriptionMessage.Should().NotBeNull();
subscriptionMessage.id.Should().Be(created.id);
@@ -135,4 +135,74 @@ public class SubscriptionResourceTests : IAsyncLifetime
subscriptionMessage.type.Should().Be(ProjectCommentsUpdatedMessageType.CREATED);
subscriptionMessage.comment.Should().NotBeNull();
}
+
+ [Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
+ 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 tcs = new();
+
+ using var sub = Sut.CreateProjectModelIngestionCancellationRequestedSubscription(ingestion.id, _testProject.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.type.Should().Be(ProjectModelIngestionUpdatedMessageType.cancellationRequested);
+ subscriptionMessage.modelIngestion.id.Should().Be(ingestion.id);
+ }
+
+ [Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
+ public async Task ProjectModelIngestionUpdate_UpdateSubscriptionIs()
+ {
+ ModelIngestion ingestion = await _testUser.Ingestion.Create(
+ new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
+ );
+ TaskCompletionSource tcs = new();
+
+ using var sub = Sut.CreateProjectModelIngestionUpdatedSubscription(
+ new(
+ _testProject.id,
+ new ModelIngestionReference(ingestion.id, null),
+ ProjectModelIngestionUpdatedMessageType.updated
+ )
+ );
+ sub.Listeners += (_, message) => tcs.SetResult(message);
+
+ await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
+
+ await _testUser.Ingestion.UpdateProgress(new(ingestion.id, _testProject.id, "Here's an update", 0.314));
+
+ var subscriptionMessage = await tcs.Task;
+
+ subscriptionMessage.Should().NotBeNull();
+ subscriptionMessage.type.Should().Be(ProjectModelIngestionUpdatedMessageType.updated);
+ subscriptionMessage.modelIngestion.id.Should().Be(ingestion.id);
+ }
+
+ [Fact(Timeout = TIMEOUT), Trait("Server", "Internal")]
+ public async Task ProjectModelIngestionUpdate_CancelSubscriptionIsNotCalled()
+ {
+ ModelIngestion ingestion = await _testUser.Ingestion.Create(
+ new(_testModel.id, _testProject.id, "", new(".NET test", "0.0.0", null, null))
+ );
+ TaskCompletionSource tcs = new();
+
+ using var sub = Sut.CreateProjectModelIngestionCancellationRequestedSubscription(ingestion.id, _testProject.id);
+ sub.Listeners += (_, message) => tcs.SetResult(message);
+
+ await Task.Delay(WAIT_PERIOD); // Give time to subscription to be setup
+
+ await _testUser.Ingestion.UpdateProgress(new(ingestion.id, _testProject.id, "this shouldn't cancel", null));
+
+ await Task.Delay(WAIT_PERIOD); // Give time to subscription to maybe fire
+
+ tcs.Task.IsCompleted.Should().BeFalse();
+ }
}