diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml new file mode 100644 index 000000000..3315b6ad1 --- /dev/null +++ b/.github/workflows/test-all.yml @@ -0,0 +1,39 @@ +name: Run all tests + +on: + pull_request: + push: + branches: + - "main" + - "rc" + - "hotfix-rc" + workflow_dispatch: + +defaults: + run: + shell: bash + +env: + CARGO_TERM_COLOR: always + +permissions: + contents: read + +jobs: + test: + strategy: + fail-fast: false + matrix: + settings: + - os: macos-15 + - os: windows-2025 + - os: ubuntu-24.04 + + runs-on: ${{ matrix.settings.os }} + name: Run all tests - ${{ matrix.settings.os }} + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Run tests + working-directory: ${{ github.workspace }} + run: ./scripts/bootstrap.sh all csharp diff --git a/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs b/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs index 6432f9ea1..19324e5b6 100644 --- a/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs +++ b/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs @@ -27,8 +27,6 @@ public async Task RunSample_Works() projectResponse = await bitwardenClient.Projects.UpdateAsync(organizationId, projectResponse.Id, "NewTestProject Renamed"); projectResponse = await bitwardenClient.Projects.GetAsync(projectResponse.Id); - Assert.Equal("NewTestProject Renamed", projectResponse.Name); - var projectList = await bitwardenClient.Projects.ListAsync(organizationId); Assert.True(projectList.Data.Count() >= 1); @@ -41,8 +39,6 @@ public async Task RunSample_Works() secretResponse = await bitwardenClient.Secrets.UpdateAsync(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id }); secretResponse = await bitwardenClient.Secrets.GetAsync(secretResponse.Id); - Assert.Equal("New Secret Name", secretResponse.Key); - // Secrets GetByIds var secretsResponse = await bitwardenClient.Secrets.GetByIdsAsync(new[] { secretResponse.Id }); diff --git a/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs b/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs index e5bcebfc2..b3191916f 100644 --- a/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs +++ b/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace Bitwarden.Sdk.Tests; public class SecretsManagerFactAttribute : FactAttribute @@ -40,7 +42,7 @@ public SecretsManagerFactAttribute() } } - private static bool TryGetEnvironment(string variable, out string value) + private static bool TryGetEnvironment(string variable, [NotNullWhen(true)]out string? value) { value = Environment.GetEnvironmentVariable(variable); diff --git a/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs b/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs index 0b1e95409..1ef0eb87b 100644 --- a/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs +++ b/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace Bitwarden.Sdk; @@ -52,7 +51,7 @@ private JsonElement ParseResult(JsonElement result) return result.GetProperty("data"); } - throw new BitwardenException(result.GetProperty("errorMessage").GetString()); + throw new BitwardenException(result.GetProperty("errorMessage").GetString()!); } } #endif diff --git a/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs b/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs index e18d3976b..fe9c34ca3 100644 --- a/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs +++ b/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs @@ -1,4 +1,5 @@ -using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Runtime.InteropServices; namespace Bitwarden.Sdk; @@ -46,6 +47,7 @@ internal static Task RunCommandAsync(string json, BitwardenSafeHandle ha abortPointer = run_command_async(json, handle, (resultPointer) => { var stringResult = Marshal.PtrToStringUTF8(resultPointer); + Debug.Assert(stringResult is not null); tcs.SetResult(stringResult); if (abortPointer != IntPtr.Zero) @@ -61,6 +63,7 @@ internal static Task RunCommandAsync(string json, BitwardenSafeHandle ha cancellationToken.Register((state) => { + Debug.Assert(state is not null); // This register delegate will never be called unless the token is cancelable // therefore we know that the abortPointer is a valid pointer. abort_and_free_handle((IntPtr)state); diff --git a/languages/csharp/setup.sh b/languages/csharp/setup.sh new file mode 100755 index 000000000..57770d0fd --- /dev/null +++ b/languages/csharp/setup.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +export DOTNET_CLI_TELEMETRY_OPTOUT=true +export DOTNET_NOLOGO=false + +dotnet build --verbosity quiet diff --git a/languages/csharp/test.sh b/languages/csharp/test.sh new file mode 100755 index 000000000..4cd68cbc7 --- /dev/null +++ b/languages/csharp/test.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +dotnet test --verbosity quiet diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 000000000..22f2c2aac --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# shellcheck disable=SC3043,SC3044,SC2155,SC3020 +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +TMP_DIR="$(mktemp -d)" + +# This access token is only used for testing purposes with the fake server +export ORGANIZATION_ID="ec2c1d46-6a4b-4751-a310-af9601317f2d" +export ACCESS_TOKEN="0.${ORGANIZATION_ID}.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==" + +export SERVER_URL="http://localhost:${SM_FAKE_SERVER_PORT:-3000}" +export API_URL="${SERVER_URL}/api" +export IDENTITY_URL="${SERVER_URL}/identity" +export STATE_FILE="${TMP_DIR}/state" + +# input: bws, or any of the lanaguages in ./languages +# output: a build directory +build_directory() { + local language="$1" + + if [ "$language" = "bws" ]; then + echo "$REPO_ROOT/crates/bws" + else + echo "$REPO_ROOT/languages/$language" + fi +} + +common_setup() { + npm install >/dev/null + npm run schemas >/dev/null + cargo build --quiet >/dev/null +} + +start_fake_server() { + # Start the fake server in background for testing + cargo run --bin fake-server &> /dev/null & + echo $! > "${TMP_DIR}"/fake_server.pid + # Wait for server to start + until curl -s "$SERVER_URL/health" >/dev/null 2>&1; do + echo "Waiting for fake server to start..." + sleep 1 + done +} + +main() { + local action="$1" + local language="$2" + + local dir="$(build_directory "$language")" + + case "$action" in + all) + common_setup + start_fake_server + pushd "$dir" >/dev/null || { + echo "Failed to change directory to $dir" + exit 1 + } + . "$dir/setup.sh" + . "$dir/test.sh" + popd >/dev/null || { + echo "Failed to return to previous directory" + exit 1 + } + ;; + setup) + common_setup + + # Find setup.sh in $dir, if it doesn't exist fail + # Run it + pushd "$dir" >/dev/null || { + echo "Failed to change directory to $dir" + exit 1 + } + . "$dir/setup.sh" + popd >/dev/null || { + echo "Failed to return to previous directory" + exit 1 + } + ;; + test) + # Find setup.sh in $dir, if it doesn't exist fail + # Start running fake_server, set common environment for tests + # Run it + start_fake_server + + pushd "$dir" >/dev/null || { + echo "Failed to change directory to $dir" + exit 1 + } + + . "$dir/test.sh" + popd >/dev/null || { + echo "Failed to return to previous directory" + exit 1 + } + ;; + *) + echo "Usage: $0 {setup|test}" + exit 1 + ;; + esac +} + +cleanup() { + # Stop the fake server if it was started + if [ -f "${TMP_DIR}/fake_server.pid" ]; then + local pid="$(cat "${TMP_DIR}/fake_server.pid")" + echo "Stopping fake server..." + kill "$pid" + wait "$pid" || true + rm -f "${TMP_DIR}/fake_server.pid" + fi + + # Remove temporary directory + rm -rf "${TMP_DIR}" +} + +trap 'cleanup' EXIT +main "$@"