Compare commits

...

30 Commits

Author SHA1 Message Date
Dogukan Karatas 80d1df8eca Merge pull request #424 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
dev -> main for release
2025-12-04 12:01:06 +01:00
Dogukan Karatas b5796245aa Merge pull request #422 from specklesystems/dogukan/solidx-class
.NET Build and Publish / build (push) Has been cancelled
feat (objects): introducing SolidX class
2025-12-04 09:20:34 +01:00
Dogukan Karatas 639c774f80 sat added 2025-12-04 09:07:30 +01:00
Dogukan Karatas 3bb5d1e73a SolidX class added
.NET Build and Publish / build (push) Has been cancelled
2025-11-26 12:09:24 +01:00
Jedd Morgan e01360ad03 mark version received (#419) 2025-11-24 19:53:09 +00:00
dependabot[bot] 2494b160e8 chore(deps): bump actions/checkout from 5 to 6 (#421)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 19:46:31 +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
Jedd Morgan 0aacc3fe89 Merge pull request #417 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
main -> dev for 3.9.0 release
2025-11-05 11:03:28 +00:00
Björn Steinhagen 39f5257f85 Merge pull request #409 from specklesystems/bjorn/cnx-2722-grasshopper-root-collection-props
feat(grasshopper): add model-wide properties to send/receive
2025-11-05 12:53:21 +02:00
Björn Steinhagen 2ea6348689 fix: again :/ 2025-11-05 12:24:06 +02:00
Björn Steinhagen 42c26e38bf fix: unnecessary using directive 2025-11-05 12:22:27 +02:00
Björn Steinhagen cb31fd1a08 chore: namespace change 2025-11-05 12:18:40 +02:00
Björn 67236abafe refactor: rootCollection to use IProperties 2025-10-29 14:39:06 +02:00
Björn 59a4f8f864 Merge remote-tracking branch 'origin/dev' into bjorn/cnx-2722-grasshopper-root-collection-props 2025-10-29 14:19:33 +02:00
Jedd Morgan 0e98e1cccd refactor(ci): Refactor CI to run integration tests as separate workflow (#413)
* Refactor CI to run integration tests as separate workflow

* Tool restore

* correct cache path

* conditionally use container registry

* use sln because net8

* fix typo

* Correct trait filter

* Correct mistake again

* fix again

* fml

* clarify names

* hopefully we're properly filtering test categories now

* maybe this?

* What does this do?

* revert is test project changes

* IsTestProject fix

* Correct test setup for automate

* maybe fix unit tests

* docker-compose-file alighment

* remove debug

* Ok tests should now pass
2025-10-29 10:00:51 +00:00
Claire Kuang 79c6f02544 Merge branch 'dev' into bjorn/cnx-2722-grasshopper-root-collection-props 2025-10-29 09:45:31 +00:00
Jedd Morgan 07713b41e1 Fix(gql)!: Treat UNAUTHORIZED_ACCESS_ERROR as an SpeckleGraphQLForbiddenException (#411)
* Respect UNAUTHORIZED_ACCESS_ERROR

* Correct test setup for automate

* dumb dumb typo
2025-10-28 15:48:37 +00:00
Claire Kuang 3c0b9e8b1c Merge pull request #414 from specklesystems/claire/add-camera-class
feat(objects): add Camera class
2025-10-28 13:42:39 +00:00
Björn b67eb8d8af refactor: model properties to properties 2025-10-27 13:03:52 +02:00
Björn 63f06c8541 chore: format 2025-10-25 15:41:00 +02:00
Björn 3f49bb05d1 chore: cleanup 2025-10-25 15:20:24 +02:00
Björn 9a879fd1ac fix: parameterless constructor 2025-10-25 13:35:58 +02:00
Björn c3230d5d91 refactor: naming conflict 2025-10-25 12:09:34 +02:00
Björn f1a64590d7 chore(models): adds RootCollection 2025-10-25 11:59:39 +02:00
Jedd Morgan 6568781275 Merge pull request #405 from specklesystems/dev
Dev -> Main
2025-10-15 11:07:32 +01:00
Jedd Morgan 6740659af4 dev -> main for release (#404)
* Expose options for sending and receiving (#394)

* chore(docs): Update doc comments (#398)

* path provider

* tweaks

* Update RenderMaterial.cs (#399)

* removes the extra serializer (#402)

* feat(sdk): align SpecklePathProvider with connector repo (#400)

* path provider

* tweaks

* Align with duplicated class

* skip some slow tests (#403)

---------

Co-authored-by: Adam Hathcock <adamhathcock@users.noreply.github.com>
Co-authored-by: Dogukan Karatas <61163577+dogukankaratas@users.noreply.github.com>
2025-10-15 10:45:26 +01:00
Adam Hathcock 701013ad46 Merge pull request #393 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
dev to main for release (DONUT squash)
2025-09-24 10:22:05 +01:00
Adam Hathcock fdc0842b03 Merge pull request #388 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Main to dev (no squash)
2025-09-12 12:04:04 +01:00
Jedd Morgan 23d5dd44bc Merge pull request #382 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
Dev -> Main for release
2025-09-08 10:54:56 +01:00
30 changed files with 392 additions and 228 deletions
+53
View File
@@ -0,0 +1,53 @@
name: Integration Test
on:
workflow_call:
inputs:
docker-compose-file:
required: true
type: string
use-github-container-registry:
default: false
type: boolean
jobs:
integration-test:
env:
Solution: "Speckle.Sdk.sln"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.x.x
cache: true
cache-dependency-path: "**/packages.lock.json"
- name: 🔐 Login to Github Container Registry
if: ${{ inputs.use-github-container-registry }}
uses: docker/login-action@v3
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ github.token }}
- name: ⚙️ Spin up Server
run: docker compose -f ${{ inputs.docker-compose-file }} up --wait
- name: 📦 Restore
run: dotnet restore ${{ env.Solution }} --locked-mode
- name: 🏗️ Build
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
- name: 🔨 Integration Tests
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
with:
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
+46 -37
View File
@@ -1,46 +1,55 @@
name: .NET CI Build
name: PR Test
on:
pull_request:
jobs:
build:
env:
Solution: "Speckle.Sdk.sln"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Checkout
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.x.x
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
- id: set-version
name: Set version to output
run: |
SEMVER="3.0.99.${{ github.run_number }}"
FILE_VERSION=$(echo "$SEMVER" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
echo $SEMVER
echo $FILE_VERSION
- name: 🔫 Build All
run: ./build.sh
env:
SEMVER: ${{ steps.set-version.outputs.SEMVER }}
FILE_VERSION: ${{ steps.set-version.outputs.FILE_VERSION }}
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
with:
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.x.x
cache: true
cache-dependency-path: "**/packages.lock.json"
- name: 📦 Tool Restore
run: dotnet tool restore
- name: 📄 Format
run: dotnet csharpier check .
- name: 📦 Restore
run: dotnet restore ${{ env.Solution }} --locked-mode
- name: 🏗️ Build
run: dotnet build ${{ env.Solution }} --configuration Release --no-restore -warnaserror
- 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
with:
files: tests/**/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
integration-test-internal:
uses: "./.github/workflows/integration-test.yml"
with:
docker-compose-file: "docker-compose-internal.yml"
use-github-container-registry: true
integration-test-public:
uses: "./.github/workflows/integration-test.yml"
with:
docker-compose-file: "docker-compose.yml"
+5 -8
View File
@@ -8,23 +8,20 @@ jobs:
build:
runs-on: ubuntu-latest
environment:
name: 'nuget.org'
name: "nuget.org"
permissions:
id-token: write # enable GitHub OIDC token issuance for this job
id-token: write # enable GitHub OIDC token issuance for this job
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.x.x
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
cache: true
cache-dependency-path: "**/packages.lock.json"
- id: set-version
name: Set version to output
+1 -1
View File
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
<PropertyGroup Condition="'$(IsTestProject)' == 'true' or '$(TestProjectAnalyserRules)' == 'true' ">
<NoWarn>
<!-- Things we need to test -->
CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;CA1065;
+4 -2
View File
@@ -65,10 +65,12 @@ Docs are a bit patchy [https://docs.speckle.systems/developers/looking-for-devel
### Tests
There are several test projects. It is a requirement that all tests pass for PRs to be merged.
The Integration test projects require a local server to be running.
You must have docker installed. Then you can run `docker compose up` from the root of the repo to start the required containers.
You must have docker installed. Then you can run `docker compose up --wait` from the root of the repo to start the required containers.
In CI, they will be run against both the public and private versions of the server.
It is important that we remain compatible with both server versions.
## Contributing
Before embarking on submitting a patch, please make sure you read:
+2
View File
@@ -27,6 +27,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{DA2AED
CodeMetricsConfig.txt = CodeMetricsConfig.txt
Directory.Build.Targets = Directory.Build.Targets
.config\dotnet-tools.json = .config\dotnet-tools.json
docker-compose-internal.yml = docker-compose-internal.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{58D37DA9-F948-48CA-9A73-F5BBBD533DBF}"
@@ -41,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}"
+118
View File
@@ -0,0 +1,118 @@
name: "speckle-server"
services:
####
# Speckle Server dependencies
#######
postgres:
image: "postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf"
restart: always
environment:
POSTGRES_DB: speckle
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- ./.volumes/postgres-data:/var/lib/postgresql/data/
healthcheck:
# the -U user has to match the POSTGRES_USER value
test: ["CMD-SHELL", "pg_isready -U speckle"]
interval: 5s
timeout: 5s
retries: 30
redis:
image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
restart: always
volumes:
- ./.volumes/redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 5s
timeout: 5s
retries: 30
minio:
image: "minio/minio:RELEASE.2023-10-25T06-33-25Z"
command: server /data --console-address ":9001"
restart: always
volumes:
- ./.volumes/minio-data:/data
ports:
- '127.0.0.1:9000:9000'
- '127.0.0.1:9001:9001'
healthcheck:
test:
[
"CMD-SHELL",
"curl -s -o /dev/null http://127.0.0.1:9000/minio/index.html",
]
interval: 5s
timeout: 30s
retries: 30
start_period: 10s
speckle-server:
image: ghcr.io/specklesystems/speckle-server:latest
restart: always
healthcheck:
test:
- CMD
- /nodejs/bin/node
- -e
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(Number(res.statusCode != 200 || body.toLowerCase().includes('error')));}); }).end(); } catch { process.exit(1); }"
interval: 10s
timeout: 10s
retries: 3
start_period: 90s
ports:
- "0.0.0.0:3000:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
environment:
# TODO: Change this to the URL of the speckle server, as accessed from the network
CANONICAL_URL: "http://127.0.0.1:8080"
SPECKLE_AUTOMATE_URL: "http://127.0.0.1:3030"
FRONTEND_ORIGIN: "http://127.0.0.1:8081"
# TODO: Change thvolumes:
REDIS_URL: "redis://redis"
S3_ENDPOINT: "http://minio:9000"
S3_PUBLIC_ENDPOINT: "http://127.0.0.1:9000"
S3_ACCESS_KEY: "minioadmin"
S3_SECRET_KEY: "minioadmin"
S3_BUCKET: "speckle-server"
S3_CREATE_BUCKET: "true"
FILE_SIZE_LIMIT_MB: 100
MAX_PROJECT_MODELS_PER_PAGE: 500
# TODO: Change this to a unique secret for this server
SESSION_SECRET: "TODO:ReplaceWithLongString"
STRATEGY_LOCAL: "true"
POSTGRES_URL: "postgres"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
LOG_PRETTY: "true"
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
networks:
default:
name: speckle-server
volumes:
postgres-data:
redis-data:
minio-data:
+8 -10
View File
@@ -12,7 +12,7 @@ services:
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- postgres-data:/var/lib/postgresql/data/
- ./.volumes/postgres-data:/var/lib/postgresql/data/
healthcheck:
# the -U user has to match the POSTGRES_USER value
test: ["CMD-SHELL", "pg_isready -U speckle"]
@@ -24,7 +24,7 @@ services:
image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
restart: always
volumes:
- redis-data:/data
- ./.volumes/redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 5s
@@ -36,7 +36,7 @@ services:
command: server /data --console-address ":9001"
restart: always
volumes:
- minio-data:/data
- ./.volumes/minio-data:/data
ports:
- '127.0.0.1:9000:9000'
- '127.0.0.1:9001:9001'
@@ -55,7 +55,7 @@ services:
image: speckle/speckle-server:latest
restart: always
healthcheck:
test:
test:
- CMD
- /nodejs/bin/node
- -e
@@ -81,9 +81,9 @@ services:
# TODO: Change thvolumes:
REDIS_URL: "redis://redis"
S3_ENDPOINT: "http://minio:9000"
S3_PUBLIC_ENDPOINT: 'http://127.0.0.1:9000'
S3_PUBLIC_ENDPOINT: "http://127.0.0.1:9000"
S3_ACCESS_KEY: "minioadmin"
S3_SECRET_KEY: "minioadmin"
S3_BUCKET: "speckle-server"
@@ -96,19 +96,17 @@ services:
SESSION_SECRET: "TODO:ReplaceWithLongString"
STRATEGY_LOCAL: "true"
DEBUG: "speckle:*"
POSTGRES_URL: "postgres"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
LOG_PRETTY: "true"
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
networks:
default:
@@ -63,7 +63,7 @@ internal sealed class AutomationContext(IOperations operations) : IAutomationCon
);
}
Base? rootObject = await operations
Base rootObject = await operations
.Receive2(
SpeckleClient.ServerUrl,
AutomationRunData.ProjectId,
@@ -74,6 +74,10 @@ internal sealed class AutomationContext(IOperations operations) : IAutomationCon
)
.ConfigureAwait(false);
await SpeckleClient
.Version.Received(new(version.id, AutomationRunData.ProjectId, "automate_function"), cancellationToken)
.ConfigureAwait(false);
Console.WriteLine($"It took {Elapsed.TotalSeconds} seconds to receive the speckle version {versionId}");
return rootObject;
}
+6
View File
@@ -0,0 +1,6 @@
using Speckle.Sdk.Models;
namespace Speckle.Objects.Geometry;
[SpeckleType("Objects.Geometry.SolidX")]
public class SolidX : RawEncodedObject;
+2 -9
View File
@@ -2,6 +2,7 @@ using Speckle.Objects.Geometry;
using Speckle.Objects.Other;
using Speckle.Objects.Primitive;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Data;
namespace Speckle.Objects;
@@ -110,15 +111,7 @@ public interface IDisplayValue<out T> : ISpeckleObject
#region Data objects
/// <summary>
/// Specifies properties on objects to be used for data-based workflows
/// </summary>
public interface IProperties : ISpeckleObject
{
Dictionary<string, object?> properties { get; }
}
public interface IDataObject : IProperties, IDisplayValue<IReadOnlyList<Base>>
public interface IDataObject : IProperties, IDisplayValue<IReadOnlyList<Base>>, ISpeckleObject
{
/// <summary>
/// The name of the object, primarily used to decorate the object for consumption in frontend and other apps
+2
View File
@@ -20,4 +20,6 @@ public class RawEncoding : Base // note: at this stage, since we're using this f
public static class RawEncodingFormats
{
public const string RHINO_3DM = "3dm";
public const string ACAD_DWG = "dwg";
public const string ACAD_SAT = "sat";
}
+6 -1
View File
@@ -23,10 +23,15 @@ public class SpeckleGraphQLException : SpeckleException
}
/// <summary>
/// Represents a "FORBIDDEN" or "UNAUTHORIZED" GraphQL error as an exception.
/// Represents a "FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" GraphQL error as an exception.
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#unauthenticated
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#forbidden
/// https://github.com/specklesystems/speckle-server/blob/v2.23.18/packages/server/modules/shared/errors/index.ts#L34
/// </summary>
/// <remarks>
/// Server is a bit inconsistent with these error codes, hence there's 4 different codes that mean "auth no work"
/// Apollo no longer considers "FORBIDDEN" or "UNAUTHENTICATED" as built in error codes, so everything is custom anyway.
/// </remarks>
public sealed class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
{
public SpeckleGraphQLForbiddenException() { }
@@ -28,7 +28,8 @@ internal static class GraphQLErrorHandler
var ex = code switch
{
"GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" => new SpeckleGraphQLInvalidQueryException(message),
"FORBIDDEN" or "UNAUTHENTICATED" => new SpeckleGraphQLForbiddenException(message),
"FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" =>
new SpeckleGraphQLForbiddenException(message),
"STREAM_NOT_FOUND" => new SpeckleGraphQLStreamNotFoundException(message),
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
@@ -15,8 +15,8 @@ public static class SpecklePathProvider
private const string ACCOUNTS_FOLDER_NAME = "Accounts";
private static string UserDataPathEnvVar => "SPECKLE_USERDATA_PATH";
private static string? Path => Environment.GetEnvironmentVariable(UserDataPathEnvVar);
public const string USER_DATA_PATH_ENV_VAR = "SPECKLE_USERDATA_PATH";
private static string? Path => Environment.GetEnvironmentVariable(USER_DATA_PATH_ENV_VAR);
/// <summary>
/// Get the installation path.
@@ -0,0 +1,21 @@
using Speckle.Sdk.Models.Data;
namespace Speckle.Sdk.Models.Collections;
/// <summary>
/// Root collection that represents the top-level commit object.
/// Extends Collection to include model-wide properties that apply to the entire model.
/// </summary>
[SpeckleType("Speckle.Core.Models.Collections.RootCollection")]
public class RootCollection : Collection, IProperties
{
public RootCollection() { }
public RootCollection(string name)
: base(name) { }
/// <summary>
/// Model-wide properties that apply to the entire model.
/// </summary>
public Dictionary<string, object?> properties { get; set; } = new();
}
@@ -0,0 +1,10 @@
namespace Speckle.Sdk.Models.Data;
/// <summary>
/// Specifies properties on objects to be used for data-based workflows.
/// Can be applied to both objects and collections.
/// </summary>
public interface IProperties
{
Dictionary<string, object?> properties { get; }
}
@@ -3,6 +3,13 @@
<TargetFramework>net8.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="altcover" />
<PackageReference Include="AwesomeAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.assert" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Speckle.Sdk.Testing\Speckle.Sdk.Testing.csproj" />
<ProjectReference Include="..\..\src\Speckle.Automate.Sdk\Speckle.Automate.Sdk.csproj" />
@@ -25,7 +25,9 @@ public sealed class AutomationContextTest : IAsyncLifetime
public async Task InitializeAsync()
{
var serviceProvider = TestServiceSetup.GetServiceProvider();
var serviceCollection = new ServiceCollection();
serviceCollection.AddAutomateSdk();
var serviceProvider = serviceCollection.BuildServiceProvider();
_account = await Fixtures.SeedUser().ConfigureAwait(false);
_client = serviceProvider.GetRequiredService<IClientFactory>().Create(_account);
_runner = serviceProvider.GetRequiredService<IAutomationRunner>();
@@ -42,7 +44,7 @@ public sealed class AutomationContextTest : IAsyncLifetime
private async Task<AutomationRunData> AutomationRunData(Base testObject)
{
Project project = await _client.Project.Create(new("Automate function e2e test", null, ProjectVisibility.Public));
const string BRANCH_NAME = "main";
const string BRANCH_NAME = "Trigger";
var model = await _client.Model.Create(new(BRANCH_NAME, null, project.id));
string modelId = model.id;
@@ -2,6 +2,28 @@
"version": 2,
"dependencies": {
"net8.0": {
"altcover": {
"type": "Direct",
"requested": "[9.0.1, )",
"resolved": "9.0.1",
"contentHash": "aadciFNDT5bnylaYUkKal+s5hF7yU/lmZxImQWAlk1438iPqK1Uf79H5ylELpyLIU49HL5ql+tnWBihp3WVLCA=="
},
"AwesomeAssertions": {
"type": "Direct",
"requested": "[8.1.0, )",
"resolved": "8.1.0",
"contentHash": "IfNC4cpXPi9tclWvuNO9lfkuIxJsUTLTS1NXto55jDrAUQJYl0zLI9ByISrfkbBE2Xtg+IWaAXQ6jnUx3anDuw=="
},
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
"requested": "[17.13.0, )",
"resolved": "17.13.0",
"contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==",
"dependencies": {
"Microsoft.CodeCoverage": "17.13.0",
"Microsoft.TestPlatform.TestHost": "17.13.0"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
@@ -24,6 +46,18 @@
"resolved": "0.9.6",
"contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w=="
},
"xunit.assert": {
"type": "Direct",
"requested": "[2.9.3, )",
"resolved": "2.9.3",
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
},
"xunit.runner.visualstudio": {
"type": "Direct",
"requested": "[3.0.2, )",
"resolved": "3.0.2",
"contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow=="
},
"Argon": {
"type": "Transitive",
"resolved": "0.28.0",
@@ -364,18 +398,6 @@
"xunit.runner.visualstudio": "[3.0.2, )"
}
},
"altcover": {
"type": "CentralTransitive",
"requested": "[9.0.1, )",
"resolved": "9.0.1",
"contentHash": "aadciFNDT5bnylaYUkKal+s5hF7yU/lmZxImQWAlk1438iPqK1Uf79H5ylELpyLIU49HL5ql+tnWBihp3WVLCA=="
},
"AwesomeAssertions": {
"type": "CentralTransitive",
"requested": "[8.1.0, )",
"resolved": "8.1.0",
"contentHash": "IfNC4cpXPi9tclWvuNO9lfkuIxJsUTLTS1NXto55jDrAUQJYl0zLI9ByISrfkbBE2Xtg+IWaAXQ6jnUx3anDuw=="
},
"GraphQL.Client": {
"type": "CentralTransitive",
"requested": "[6.0.0, )",
@@ -424,16 +446,6 @@
"Microsoft.Extensions.Options": "2.2.0"
}
},
"Microsoft.NET.Test.Sdk": {
"type": "CentralTransitive",
"requested": "[17.13.0, )",
"resolved": "17.13.0",
"contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==",
"dependencies": {
"Microsoft.CodeCoverage": "17.13.0",
"Microsoft.TestPlatform.TestHost": "17.13.0"
}
},
"Moq": {
"type": "CentralTransitive",
"requested": "[4.20.72, )",
@@ -515,18 +527,6 @@
"xunit.assert": "2.9.3",
"xunit.core": "[2.9.3]"
}
},
"xunit.assert": {
"type": "CentralTransitive",
"requested": "[2.9.3, )",
"resolved": "2.9.3",
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
},
"xunit.runner.visualstudio": {
"type": "CentralTransitive",
"requested": "[3.0.2, )",
"resolved": "3.0.2",
"contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow=="
}
}
}
@@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TestProjectAnalyserRules>true</TestProjectAnalyserRules>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Speckle.Sdk\Speckle.Sdk.csproj" />
+13 -1
View File
@@ -8,7 +8,19 @@ public abstract class MoqTest : IDisposable
{
protected MoqTest() => Repository = new(MockBehavior.Strict);
public void Dispose() => Repository.VerifyAll();
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)
{
Repository.VerifyAll();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected MockRepository Repository { get; private set; } = new(MockBehavior.Strict);
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsTestProject>true</IsTestProject>
<TestProjectAnalyserRules>true</TestProjectAnalyserRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
@@ -93,7 +93,7 @@ public class ProjectResourceExceptionalTests : IAsyncLifetime
new(_testProject.id, "My new name", ProjectVisibility.Public, "NonExistentWorkspace")
)
);
ex.InnerExceptions.Single().Should().BeOfType<SpeckleGraphQLException>();
ex.InnerExceptions.Single().Should().BeOfType<SpeckleGraphQLForbiddenException>();
}
[Theory]
@@ -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,105 +37,101 @@ 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;
TaskCompletionSource<ProjectUpdatedMessage> tcs = new();
using var sub = Sut.CreateProjectUpdatedSubscription(_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 input = new ProjectUpdateInput(_testProject.id, "This is my new name");
var created = await _testUser.Project.Update(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(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();
}
@@ -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>();
}
}
@@ -15,6 +15,8 @@ using Speckle.Sdk.Tests.Integration.API.GraphQL.Resources;
using Speckle.Sdk.Transports;
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
[assembly: AssemblyTrait("Category", "Integration")]
namespace Speckle.Sdk.Tests.Integration;
public static class Fixtures
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<IsTestProject>true</IsTestProject>
<TestProjectAnalyserRules>true</TestProjectAnalyserRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
@@ -11,6 +11,7 @@ public class GraphQLErrorHandlerTests
{
yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "FORBIDDEN" } }];
yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHENTICATED" } }];
yield return [typeof(SpeckleGraphQLForbiddenException), new Map { { "code", "UNAUTHORIZED_ACCESS_ERROR" } }];
yield return [typeof(SpeckleGraphQLInternalErrorException), new Map { { "code", "INTERNAL_SERVER_ERROR" } }];
yield return [typeof(SpeckleGraphQLStreamNotFoundException), new Map { { "code", "STREAM_NOT_FOUND" } }];
yield return [typeof(SpeckleGraphQLBadInputException), new Map { { "code", "BAD_USER_INPUT" } }];
@@ -9,7 +9,7 @@ using Speckle.Sdk.Testing;
namespace Speckle.Sdk.Tests.Unit.Credentials;
public class AccountManagerTests : MoqTest
public sealed class AccountManagerTests : MoqTest
{
private class TestAccountFactory : IAccountFactory
{
@@ -36,7 +36,9 @@ public class AccountManagerTests : MoqTest
private readonly Mock<ISqLiteJsonCacheManager> _mockAccountStorage;
private readonly Mock<ISqLiteJsonCacheManager> _mockAccountAddLockStorage;
#pragma warning disable CA2213
private readonly AccountManager _accountManager;
#pragma warning restore CA2213
public AccountManagerTests()
{