From c5a7b7483acca9705446d7ae2bfc92a17fc7dd63 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:47:14 -0700 Subject: [PATCH 1/7] feat: go test suite --- .github/workflows/test-go.yml | 78 ++++++++++++ languages/go/bitwarden_client_test.go | 163 ++++++++++++++++++++++++++ languages/go/go.mod | 3 + languages/go/go.sum | 2 + languages/go/setup.sh | 16 +++ languages/go/test.sh | 7 ++ 6 files changed, 269 insertions(+) create mode 100644 .github/workflows/test-go.yml create mode 100644 languages/go/bitwarden_client_test.go create mode 100755 languages/go/setup.sh create mode 100755 languages/go/test.sh diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml new file mode 100644 index 000000000..5babf99a1 --- /dev/null +++ b/.github/workflows/test-go.yml @@ -0,0 +1,78 @@ +name: Test Go SDK + +on: + push: + branches: + - "main" + - "rc" + - "hotfix-rc" + paths: + - "languages/python/**" + - "crates/bitwarden/**" + - "crates/bitwarden-c/**" + - "crates/fake-server/**" + - ".github/workflows/test-go.yml" + pull_request: + types: [opened, synchronize] + paths: + - "languages/go/**" + - "crates/bitwarden/**" + - "crates/bitwarden-c/**" + - "crates/fake-server/**" + - ".github/workflows/test-go.yml" + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + test: + name: Test Go SDK + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: "1.20" + cache: "true" + + - name: Set up Node.js + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version: "18" + cache: "npm" + + - name: Set up Rust + uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable + with: + toolchain: stable + + - name: Cache Rust dependencies + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Setup Go SDK + run: ./scripts/bootstrap.sh setup go + + - name: Run Go SDK tests + run: ./scripts/bootstrap.sh test go diff --git a/languages/go/bitwarden_client_test.go b/languages/go/bitwarden_client_test.go new file mode 100644 index 000000000..9b1544821 --- /dev/null +++ b/languages/go/bitwarden_client_test.go @@ -0,0 +1,163 @@ +package sdk + +import ( + "os" + "testing" + "time" + + "github.com/gofrs/uuid" +) + +func TestBitwardenClient(t *testing.T) { + apiURL := getEnv("API_URL", "http://localhost:3000/api") + identityURL := getEnv("IDENTITY_URL", "http://localhost:3000/identity") + // the following access token is only valid for the fake-server, so it is safe to share + accessToken := getEnv("ACCESS_TOKEN", "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==") + organizationIDStr := getEnv("ORGANIZATION_ID", "ec2c1d46-6a4b-4751-a310-af9601317f2d") + stateFile := getEnv("STATE_FILE", "") + + bitwardenClient, err := NewBitwardenClient(&apiURL, &identityURL) + if err != nil { + t.Errorf("Failed to create Bitwarden client: %v", err) + } + defer bitwardenClient.Close() + + err = bitwardenClient.AccessTokenLogin(accessToken, &stateFile) + if err != nil { + t.Errorf("AccessTokenLogin failed: %v", err) + } + + organizationID, err := uuid.FromString(organizationIDStr) + if err != nil { + t.Errorf("Failed to parse organization ID: %v", err) + } + + // --- generator --- + request := PasswordGeneratorRequest{ + AvoidAmbiguous: true, + Length: 32, + Lowercase: true, + MinLowercase: ptr(int64(2)), + MinNumber: ptr(int64(2)), + MinSpecial: ptr(int64(2)), + MinUppercase: ptr(int64(2)), + Numbers: true, + Special: true, + Uppercase: true, + } + + password, err := bitwardenClient.Generators().GeneratePassword(request) + if err != nil || len(*password) != 32 { + t.Errorf("generate failed: %v", err) + } + + // --- secrets --- + // list; should return a list of secret IDs (without the values) + secretList, err := bitwardenClient.Secrets().List(organizationID.String()) + if err != nil || len(secretList.Data) == 0 { + t.Errorf("secret list failed: %v", err) + t.Errorf("secret list data: %v", secretList.Data) + } + + // get; should return a secret whose key is "btw" (from the fake-server) + secret, err := bitwardenClient.Secrets().Get(secretList.Data[0].ID) + hardCodedSecretKey := "btw" // embedded in the fake-server + if err != nil || secret.Key != hardCodedSecretKey { + t.Errorf("secret get failed: %v", err) + t.Errorf("secret key: %s", secret.Key) + } + + // getByIds; should return secret data for the given IDs + secretIDs := []string{uuid.Must(uuid.NewV4()).String(), uuid.Must(uuid.NewV4()).String()} + hardCodedSecretKey = "FERRIS" // embedded in the fake-server + secrets, err := bitwardenClient.Secrets().GetByIDS(secretIDs) + if err != nil || secrets.Data[0].Key != hardCodedSecretKey { + t.Errorf("secret getByIds failed: %v", err) + t.Errorf("secret key: %s", secrets.Data[0].Key) + } + + // create; should return a secret with the given key, value, and note + newProjectID, _ := uuid.NewV4() // random project ID is fine; the fake-server doesn't validate it + + secret, err = bitwardenClient.Secrets().Create("testKey", "testValue", "testNote", organizationID.String(), []string{newProjectID.String()}) + if err != nil || secret.Key != "testKey" || secret.Value != "testValue" || secret.Note != "testNote" { + t.Errorf("secret create failed: %v", err) + } + + // update; should return a secret with the updated key, value, and note + updatedSecret, err := bitwardenClient.Secrets().Update(secret.ID, "updatedKey", "updatedValue", "updatedNote", organizationID.String(), []string{}) + if err != nil || updatedSecret.Key != "updatedKey" || updatedSecret.Value != "updatedValue" || updatedSecret.Note != "updatedNote" || updatedSecret.ProjectID != nil { + t.Errorf("secret update failed: %v", err) + } + + // delete; should delete the secret and return an empty response + res, err := bitwardenClient.Secrets().Delete([]string{ + uuid.Must(uuid.NewV4()).String(), + }) + if err != nil { + t.Errorf("secret delete failed: %v", err) + t.Errorf("expected nil response, got: %v", res) + } + + // sync; should return new/modified secrets from a given point in time + syncedSecrets, err := bitwardenClient.Secrets().Sync(organizationID.String(), nil) + if err != nil || syncedSecrets.HasChanges == false { + t.Errorf("secret initial sync failed: %v", err) + t.Errorf("secret hasChanges: %v", syncedSecrets.HasChanges) + } + + lastSyncTime := time.Now() + newSyncedSecrets, err := bitwardenClient.Secrets().Sync(organizationID.String(), &lastSyncTime) + if err != nil || newSyncedSecrets.HasChanges == true { + t.Errorf("secret sync with lastSyncTime failed: %v", err) + t.Errorf("secret hasChanges: %v", newSyncedSecrets.HasChanges) + } + + // --- projects --- + // list; should return a list of project IDs + projectList, err := bitwardenClient.Projects().List(organizationID.String()) + if err != nil || len(projectList.Data) == 0 { + t.Errorf("project list failed: %v", err) + t.Errorf("project list data: %v", projectList.Data) + } + + // get; should return a project with the given ID + _, err = bitwardenClient.Projects().Get(projectList.Data[0].ID) + if err != nil { + t.Errorf("project get failed: %v", err) + } + + // create; should return a project with the given name + project, err := bitwardenClient.Projects().Create(organizationID.String(), "testProject") + if err != nil || project.Name != "testProject" { + t.Errorf("project create failed: %v", err) + t.Errorf("expected project name: testProject, got: %s", project.Name) + } + + // update; should return a project with the updated name + project, err = bitwardenClient.Projects().Update(project.ID, organizationID.String(), "updatedProject") + if err != nil || project.Name != "updatedProject" { + t.Errorf("project update failed: %v", err) + t.Errorf("expected project name: updatedProject, got: %s", project.Name) + } + + // delete; should delete the project + projectRes, err := bitwardenClient.Projects().Delete([]string{project.ID}) + if err != nil { + t.Errorf("project delete failed: %v", err) + t.Errorf("expected deleted project ID: %s, got: %v", project.ID, projectRes.Data) + } +} + +// Helper functions +func ptr(i int64) *int64 { + return &i +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + diff --git a/languages/go/go.mod b/languages/go/go.mod index ae9f50111..ba1b5d6e9 100644 --- a/languages/go/go.mod +++ b/languages/go/go.mod @@ -1,3 +1,6 @@ module github.com/bitwarden/sdk-go go 1.21 + +require github.com/gofrs/uuid v4.4.0+incompatible + diff --git a/languages/go/go.sum b/languages/go/go.sum index e69de29bb..c0ad68738 100644 --- a/languages/go/go.sum +++ b/languages/go/go.sum @@ -0,0 +1,2 @@ +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= diff --git a/languages/go/setup.sh b/languages/go/setup.sh new file mode 100755 index 000000000..978c5b67a --- /dev/null +++ b/languages/go/setup.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +GO_ARCH="$(uname -m | sed 's/x86_64/x64/' | sed 's/aarch64/arm64/')" + +mkdir -p "$REPO_ROOT"/languages/go/internal/cinterface/lib/{darwin,linux,windows}-{arm64,x64} + +if [ ! -f ./target/debug/libbitwarden_c.a ]; then + echo "Building libbitwarden_c.a..." + cargo build --quiet -p bitwarden-c +fi + +ln -f "$REPO_ROOT/target/debug/libbitwarden_c.a" "$REPO_ROOT/languages/go/internal/cinterface/lib/$OS-$GO_ARCH/libbitwarden_c.a" || { + echo "Failed to link libbitwarden_c.a" + exit 1 +} diff --git a/languages/go/test.sh b/languages/go/test.sh new file mode 100755 index 000000000..9ada35ae4 --- /dev/null +++ b/languages/go/test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +pushd "$REPO_ROOT"/languages/go > /dev/null || exit 1 + +go test + +popd > /dev/null || exit 1 From 5e5bc1dde3853adce1b2ec839b897d43eb887ed7 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:44:33 -0700 Subject: [PATCH 2/7] fix: go setup requires REPO_ROOT to create dir structure --- scripts/bootstrap.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 1740d4d82..fbb92d4b5 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -REPO_ROOT="$(git rev-parse --show-toplevel)" +# shellcheck disable=SC2155 +export REPO_ROOT="$(git rev-parse --show-toplevel)" TMP_DIR="$(mktemp -d)" # This access token is only used for testing purposes with the fake server From d4e692fa1ca2054153d455163c1c1486e434cbd1 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Mon, 28 Jul 2025 09:58:28 -0700 Subject: [PATCH 3/7] fix: ensure failure exit status if tests fail --- languages/go/setup.sh | 1 + languages/go/test.sh | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/languages/go/setup.sh b/languages/go/setup.sh index 978c5b67a..46a8bf191 100755 --- a/languages/go/setup.sh +++ b/languages/go/setup.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euo pipefail OS="$(uname -s | tr '[:upper:]' '[:lower:]')" GO_ARCH="$(uname -m | sed 's/x86_64/x64/' | sed 's/aarch64/arm64/')" diff --git a/languages/go/test.sh b/languages/go/test.sh index 9ada35ae4..50dada453 100755 --- a/languages/go/test.sh +++ b/languages/go/test.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash +set -euo pipefail pushd "$REPO_ROOT"/languages/go > /dev/null || exit 1 -go test +go test || exit 1 popd > /dev/null || exit 1 From 5a4b9e9a49424d408fb59aaf8aa3c18046724980 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:29:56 -0700 Subject: [PATCH 4/7] ci: rm test step that never worked; there were no tests --- .github/workflows/build-go.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/build-go.yaml b/.github/workflows/build-go.yaml index c15982067..0302331f6 100644 --- a/.github/workflows/build-go.yaml +++ b/.github/workflows/build-go.yaml @@ -46,7 +46,3 @@ jobs: - name: Build working-directory: languages/go run: go build -v ./... - - - name: Test - working-directory: languages/go - run: go test -v ./... From 0dd2b6c59ad48bf12143d3ea12495dd960ae11de Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:36:08 -0700 Subject: [PATCH 5/7] fix: normalize windows uname output --- languages/go/bitwarden_client_test.go | 1 - languages/go/setup.sh | 19 ++++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/languages/go/bitwarden_client_test.go b/languages/go/bitwarden_client_test.go index 9b1544821..e716cad30 100644 --- a/languages/go/bitwarden_client_test.go +++ b/languages/go/bitwarden_client_test.go @@ -160,4 +160,3 @@ func getEnv(key, fallback string) string { } return fallback } - diff --git a/languages/go/setup.sh b/languages/go/setup.sh index 46a8bf191..281db8869 100755 --- a/languages/go/setup.sh +++ b/languages/go/setup.sh @@ -7,11 +7,20 @@ GO_ARCH="$(uname -m | sed 's/x86_64/x64/' | sed 's/aarch64/arm64/')" mkdir -p "$REPO_ROOT"/languages/go/internal/cinterface/lib/{darwin,linux,windows}-{arm64,x64} if [ ! -f ./target/debug/libbitwarden_c.a ]; then - echo "Building libbitwarden_c.a..." + echo "Building bitwarden_c..." cargo build --quiet -p bitwarden-c fi -ln -f "$REPO_ROOT/target/debug/libbitwarden_c.a" "$REPO_ROOT/languages/go/internal/cinterface/lib/$OS-$GO_ARCH/libbitwarden_c.a" || { - echo "Failed to link libbitwarden_c.a" - exit 1 -} +# windows can be either mingw, msys, or cygwin +if [[ "$OS" = *"mingw"* ]] || [[ "$OS" = *"msys"* ]] || [[ "$OS" = *"cygwin"* ]]; then + OS="windows" # normalize to windows + ln -f "$REPO_ROOT/target/debug/bitwarden_c.dll" "$REPO_ROOT/languages/go/internal/cinterface/lib/$OS-$GO_ARCH/bitwarden_c.dll" || { + echo "Failed to symlink bitwarden_c.dll" + exit 1 + } +else + ln -f "$REPO_ROOT/target/debug/libbitwarden_c.a" "$REPO_ROOT/languages/go/internal/cinterface/lib/$OS-$GO_ARCH/libbitwarden_c.a" || { + echo "Failed to symlink libbitwarden_c.a" + exit 1 + } +fi From 3949b776da3d81f5e59d89c9710b1d433dee8a92 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:36:54 -0700 Subject: [PATCH 6/7] ci: comment-out windows tests; linker errors --- .github/workflows/test-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml index 5babf99a1..08ba3dae4 100644 --- a/.github/workflows/test-go.yml +++ b/.github/workflows/test-go.yml @@ -37,7 +37,7 @@ jobs: os: - ubuntu-latest - macos-latest - - windows-latest + # - windows-latest FIXME: https://gist.github.com/tangowithfoxtrot/beb737c1a804533870f10560eaf2f7c3 steps: - name: Checkout repo From 9d15b54e9acca8143cda78ff28683b49fdf3d753 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:48:17 -0700 Subject: [PATCH 7/7] ci: exclude test and scripts from Go release --- .github/workflows/release-go.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-go.yml b/.github/workflows/release-go.yml index 766652f84..cf02f810f 100644 --- a/.github/workflows/release-go.yml +++ b/.github/workflows/release-go.yml @@ -147,6 +147,8 @@ jobs: cp --verbose -rf sdk/languages/go/. sdk-go # Remove the old cinterface lib files rm -rf sdk-go/internal/cinterface/lib/* + # Don't publish test files or scripts + rm -f sdk-go/*_test.go sdk-go/*.sh mkdir -p sdk-go/internal/cinterface/lib/{darwin-{x64,arm64},linux-{x64,arm64},windows-x64} - name: Extract static libs to their respective directories