Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/build-go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...
2 changes: 2 additions & 0 deletions .github/workflows/release-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions .github/workflows/test-go.yml
Original file line number Diff line number Diff line change
@@ -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 FIXME: https://gist.github.com/tangowithfoxtrot/beb737c1a804533870f10560eaf2f7c3

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
162 changes: 162 additions & 0 deletions languages/go/bitwarden_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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
}
3 changes: 3 additions & 0 deletions languages/go/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
module github.com/bitwarden/sdk-go

go 1.21

require github.com/gofrs/uuid v4.4.0+incompatible

2 changes: 2 additions & 0 deletions languages/go/go.sum
Original file line number Diff line number Diff line change
@@ -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=
26 changes: 26 additions & 0 deletions languages/go/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/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/')"

mkdir -p "$REPO_ROOT"/languages/go/internal/cinterface/lib/{darwin,linux,windows}-{arm64,x64}

if [ ! -f ./target/debug/libbitwarden_c.a ]; then
echo "Building bitwarden_c..."
cargo build --quiet -p bitwarden-c
fi

# 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
8 changes: 8 additions & 0 deletions languages/go/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

pushd "$REPO_ROOT"/languages/go > /dev/null || exit 1

go test || exit 1

popd > /dev/null || exit 1
3 changes: 2 additions & 1 deletion scripts/bootstrap.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading