diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 00000000..bde44de9 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -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@v5 + + - 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 }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 59a80f0a..7712ecf6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,46 +1,52 @@ -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@v5 - - 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: 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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d41bbff7..5829da5b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,9 @@ 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 @@ -20,11 +20,8 @@ jobs: 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 diff --git a/Directory.Build.targets b/Directory.Build.targets index 861d0313..1cb1832c 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,5 @@ - + CS0618;CA1034;CA2201;CA1051;CA1040;CA1724;CA1065; diff --git a/README.md b/README.md index 7f9bfd4a..b13f971f 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/Speckle.Sdk.sln b/Speckle.Sdk.sln index b5b2b15d..9d91eac0 100644 --- a/Speckle.Sdk.sln +++ b/Speckle.Sdk.sln @@ -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}" diff --git a/docker-compose-internal.yml b/docker-compose-internal.yml new file mode 100644 index 00000000..29974bae --- /dev/null +++ b/docker-compose-internal.yml @@ -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: diff --git a/docker-compose.yml b/docker-compose.yml index b05dc03a..31e6006d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/src/Speckle.Sdk/Api/Exceptions.cs b/src/Speckle.Sdk/Api/Exceptions.cs index 0e0de06c..ebae33db 100644 --- a/src/Speckle.Sdk/Api/Exceptions.cs +++ b/src/Speckle.Sdk/Api/Exceptions.cs @@ -23,11 +23,15 @@ public class SpeckleGraphQLException : SpeckleException } /// -/// Represents a "FORBIDDEN" or "UNAUTHORIZED" or "UNAUTHORIZED_ACCESS_ERROR" 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 /// +/// +/// 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. +/// public sealed class SpeckleGraphQLForbiddenException : SpeckleGraphQLException { public SpeckleGraphQLForbiddenException() { } diff --git a/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs b/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs index a2e4b54f..facda30a 100644 --- a/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs +++ b/src/Speckle.Sdk/Api/GraphQL/GraphQLErrorHandler.cs @@ -28,9 +28,8 @@ internal static class GraphQLErrorHandler var ex = code switch { "GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" => new SpeckleGraphQLInvalidQueryException(message), - "FORBIDDEN" or "UNAUTHENTICATED" or "UNAUTHORIZED_ACCESS_ERROR" => 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), diff --git a/src/Speckle.Sdk/Logging/SpecklePathProvider.cs b/src/Speckle.Sdk/Logging/SpecklePathProvider.cs index 01b469ce..d488b40b 100644 --- a/src/Speckle.Sdk/Logging/SpecklePathProvider.cs +++ b/src/Speckle.Sdk/Logging/SpecklePathProvider.cs @@ -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); /// /// Get the installation path. diff --git a/tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj b/tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj index b497eb60..99609e37 100644 --- a/tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj +++ b/tests/Speckle.Sdk.Serialization.Testing/Speckle.Sdk.Serialization.Testing.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true diff --git a/tests/Speckle.Sdk.Testing/MoqTest.cs b/tests/Speckle.Sdk.Testing/MoqTest.cs index 34e8391b..4aab3a39 100644 --- a/tests/Speckle.Sdk.Testing/MoqTest.cs +++ b/tests/Speckle.Sdk.Testing/MoqTest.cs @@ -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); diff --git a/tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj b/tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj index 756d2c60..2390f7ec 100644 --- a/tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj +++ b/tests/Speckle.Sdk.Testing/Speckle.Sdk.Testing.csproj @@ -1,7 +1,7 @@  net8.0 - true + true diff --git a/tests/Speckle.Sdk.Tests.Integration/Fixtures.cs b/tests/Speckle.Sdk.Tests.Integration/Fixtures.cs index 49af3a6a..3fcfd670 100644 --- a/tests/Speckle.Sdk.Tests.Integration/Fixtures.cs +++ b/tests/Speckle.Sdk.Tests.Integration/Fixtures.cs @@ -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 diff --git a/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj b/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj index c1217da2..0931e2e6 100644 --- a/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj +++ b/tests/Speckle.Sdk.Tests.Performance/Speckle.Sdk.Tests.Performance.csproj @@ -4,7 +4,7 @@ net8.0 enable disable - true + true diff --git a/tests/Speckle.Sdk.Tests.Unit/Credentials/AccountManagerTests.cs b/tests/Speckle.Sdk.Tests.Unit/Credentials/AccountManagerTests.cs index 9b23cae1..1e63ae19 100644 --- a/tests/Speckle.Sdk.Tests.Unit/Credentials/AccountManagerTests.cs +++ b/tests/Speckle.Sdk.Tests.Unit/Credentials/AccountManagerTests.cs @@ -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 _mockAccountStorage; private readonly Mock _mockAccountAddLockStorage; +#pragma warning disable CA2213 private readonly AccountManager _accountManager; +#pragma warning restore CA2213 public AccountManagerTests() {