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 ./... 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 diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml new file mode 100644 index 000000000..08ba3dae4 --- /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 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 diff --git a/languages/go/bitwarden_client_test.go b/languages/go/bitwarden_client_test.go new file mode 100644 index 000000000..e716cad30 --- /dev/null +++ b/languages/go/bitwarden_client_test.go @@ -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 +} 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..281db8869 --- /dev/null +++ b/languages/go/setup.sh @@ -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 diff --git a/languages/go/test.sh b/languages/go/test.sh new file mode 100755 index 000000000..50dada453 --- /dev/null +++ b/languages/go/test.sh @@ -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 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