diff --git a/.github/workflows/language-tests.yml b/.github/workflows/language-tests.yml index 601811147..8965f4ceb 100644 --- a/.github/workflows/language-tests.yml +++ b/.github/workflows/language-tests.yml @@ -51,7 +51,7 @@ jobs: - name: Build schemas run: npm run schemas - - name: zip languages dir + - name: Zip languages dir run: zip -r languages.zip languages - name: Upload schemas @@ -80,7 +80,7 @@ jobs: # - nodejs_wasm # - php - python - # - ruby + - ruby # - swift steps: @@ -102,7 +102,10 @@ jobs: - name: Create run-id id: run-id - run: echo "RUN_ID=$(uuidgen)" >> $GITHUB_ENV + run: | + export RUN_ID=$(uuidgen) + echo "RUN_ID=$RUN_ID" >> $GITHUB_ENV + echo "RUN_ID=$RUN_ID" - name: Get Setup Values uses: bitwarden/sm-action@92d1d6a4f26a89a8191c83ab531a53544578f182 # v2.0.0 @@ -130,6 +133,12 @@ jobs: working-directory: languages/language_tests run: cargo run setup + - name: Install Ruby + if: matrix.language == 'ruby' + uses: ruby/setup-ruby@c04af2bb7258bb6a03df1d3c1865998ac9390972 # v1.194.0 + with: + ruby-version: '3.2' # Not needed with a .ruby-version file + - name: Run language tests run: | cd $GITHUB_WORKSPACE/languages/${{ matrix.language }} diff --git a/languages/ruby/spec/.ruby-version b/languages/ruby/spec/.ruby-version new file mode 100644 index 000000000..5ae69bd5f --- /dev/null +++ b/languages/ruby/spec/.ruby-version @@ -0,0 +1 @@ +3.2.5 diff --git a/languages/ruby/spec/Gemfile b/languages/ruby/spec/Gemfile new file mode 100644 index 000000000..1fdb92530 --- /dev/null +++ b/languages/ruby/spec/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'bitwarden-sdk-secrets', path: '../bitwarden_sdk_secrets' + +gem 'rspec', '~> 3.0' +gem 'rubocop', '~> 1.21' +gem 'pry', '~> 0.14' diff --git a/languages/ruby/spec/e2e_data_manipulation.rb b/languages/ruby/spec/e2e_data_manipulation.rb new file mode 100644 index 000000000..7e2633005 --- /dev/null +++ b/languages/ruby/spec/e2e_data_manipulation.rb @@ -0,0 +1,35 @@ +def env(name) + ENV[name] || raise("Missing ENV['#{name}']") +end + +def with_run_id(str) + run_id = env('RUN_ID') + "#{str}-#{run_id}" +end + +def filter_projects_to_this_run(projects) + run_id = env('RUN_ID') + projects.filter { |p| p['name'].end_with? run_id } +end + +def filter_secrets_to_this_run(secrets) + run_id = env('RUN_ID') + secrets.filter { |p| p['key'].end_with? run_id } +end + +def project_with_run_id(project) + project['name'] = with_run_id project['name'] + project +end + +def secret_with_run_id(secret) + secret['key'] = with_run_id secret['key'] + secret['project_name'] = with_run_id secret['project_name'] + secret +end + +def secret_with_project_id(secret, projects) + project = projects.find { |p| p['name'] == secret['project_name'] } + secret['project_id'] = project['id'] + secret +end diff --git a/languages/ruby/spec/e2e_spec.rb b/languages/ruby/spec/e2e_spec.rb new file mode 100644 index 000000000..752c3f50c --- /dev/null +++ b/languages/ruby/spec/e2e_spec.rb @@ -0,0 +1,209 @@ +require 'bitwarden-sdk-secrets' +require 'rspec/expectations' +require_relative 'e2e_data_manipulation' +require 'pry' + +organization_id = env('ORGANIZATION_ID') +language_tests_path = File.join(File.dirname(__FILE__), '..', 'language_tests') +expected_data_file_name = 'e2e_data.json' +expected_data_file = env("TEST_DATA_FILE") + +RSpec::Matchers.define :equal_secret do |expected| + match do |actual| + # Note: actual secrets are from the sdk, expected are parsed from the test data. + # actual SDK responses have the incorrect casing for organizationId and projectId. + actual['key'] == expected['key'] && actual['value'] == expected['value'] && actual['note'] == expected['note'] && actual['organizationId'] == organization_id && actual['projectId'] == expected['project_id'] + end +end + +RSpec::Matchers.define :equal_secret_identifier do |expected| + match do |actual| + actual['key'] == expected['key'] && actual['organizationId'] == organization_id + end +end + +RSpec::Matchers.define :equal_project do |expected| + match do |actual| + actual['name'] == expected['name'] + end +end + +bitwarden_settings = BitwardenSDKSecrets::BitwardenSettings.new(env('API_URL'), env('IDENTITY_URL')) + +describe 'Ruby Read E2E' do + let(:expected_data) { JSON.parse(File.read(expected_data_file)) } + let(:expected_projects) { expected_data['projects'].map { |p| project_with_run_id p } } + let(:expected_secrets) { expected_data['secrets'].map { |s| secret_with_project_id(secret_with_run_id(s), projects) } } + let(:all_projects) { @client.projects.list(organization_id).filter } + let(:projects) { filter_projects_to_this_run(all_projects) } + let(:all_secrets) { @client.secrets.list(organization_id) } + let(:secrets) { filter_secrets_to_this_run(all_secrets) } + + before(:all) do + # Set up client + @state_file = File.join(language_tests_path, 'state.json') + @client = BitwardenSDKSecrets::BitwardenClient.new(bitwarden_settings) + @client.auth.login_access_token(env('ACCESS_TOKEN'), @state_file) + end + + it 'should successfully list projects' do + expect(projects).to be_an_instance_of(Array) + end + + it 'should successfully list secrets' do + expect(secrets).to be_an_instance_of(Array) + end + + it 'should list the correct projects' do + expected_names = expected_projects.map { |p| p['name'] } + projects.each do |project| + expect(expected_names).to include(project['name']) + end + end + + it 'should list the correct secrets' do + secrets.each do |secret| + expected_secret = expected_secrets.find { |s| s['key'] == secret['key'] } + expect(expected_secret).to_not be_nil + expect(secret).to equal_secret_identifier(expected_secret) + end + end + + it 'should successfully get a project' do + project = projects.first + response = @client.projects.get(project['id']) + expect(response).to equal_project(project) + end + + it 'should successfully get a secret' do + secret = secrets.first + response = @client.secrets.get(secret['id']) + expected_secret = expected_secrets.find { |s| s['key'] == secret['key'] } + expect(response).to equal_secret(expected_secret) + end + + it 'should sync secrets' do + response = @client.secrets.sync(organization_id, nil) + expect(response).to be_an_instance_of(Hash) + expect(response['hasChanges']).to be_truthy + secrets = filter_secrets_to_this_run(response['secrets']) + secrets.each do |secret| + expected_secret = expected_secrets.find { |s| s['key'] == secret['key'] } + expect(secret).to equal_secret(expected_secret) + end + end + + it 'should not have new sync changes' do + response = @client.secrets.sync(organization_id, Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%6NZ')) + expect(response).to be_an_instance_of(Hash) + expect(response['hasChanges']).to be_falsey + expect(response['secrets']).to be_nil + end + + after(:all) do + File.delete(@state_file) if File.exist?(@state_file) + end +end + +describe 'Ruby Secrets Write E2E' do + let(:expected_data) { JSON.parse(File.read(expected_data_file)) } + let(:expected_projects) { expected_data['mutable_projects'].map { |p| project_with_run_id p } } + let(:expected_secrets) { expected_data['mutable_secrets'].map { |s| secret_with_project_id(secret_with_run_id(s), projects) } } + let(:projects) { @client.projects.list(organization_id) } + let(:write_project_name) { with_run_id('for_write_tests') } + let(:write_project) { projects.find { |p| p['name'] == write_project_name } } + let(:secrets) { @client.secrets.list(organization_id) } + + before(:all) do + # Set up client + @state_file = File.join(language_tests_path, 'mutable_state.json') + @client = BitwardenSDKSecrets::BitwardenClient.new(bitwarden_settings) + @client.auth.login_access_token(env('MUTABLE_ACCESS_TOKEN'), @state_file) + end + + it 'should successfully create a secret' do + secret = { + 'key' => with_run_id('create_secret_key'), + 'value' => 'create_secret_value', + 'note' => 'create_secret_note', + 'project_name' => write_project_name, + 'project_id' => write_project['id'] + } + created = @client.secrets.create(organization_id, secret['key'], secret['value'], secret['note'], [secret['project_id']]) + expect(created).to equal_secret(secret) + end + + it 'should delete a secret' do + to_delete = secrets.find { |s| s['key'] == with_run_id('to_delete') } + expect(to_delete).to_not be_nil + deleted = @client.secrets.delete([to_delete['id']]) + expect(deleted).to be_an_instance_of(Array) + expect(deleted.length).to eq(1) + expect(deleted[0]['id']).to eq(to_delete['id']) + end + + it 'should update a secret' do + to_update = secrets.find { |s| s['key'] == with_run_id('to_update') } + expect(to_update).to_not be_nil + updated_secret = { + 'key' => with_run_id('updated_key'), + 'value' => 'updated_value', + 'note' => 'updated_note', + 'project_id' => write_project['id'] + } + updated = @client.secrets.update(organization_id, to_update['id'], updated_secret['key'], updated_secret['value'], updated_secret['note'], [write_project['id']]) + expect(updated).to equal_secret(updated_secret) + end + + after(:all) do + File.delete(@state_file) if File.exist?(@state_file) + end + +end + +describe 'Ruby Projects Write E2E' do + let(:expected_data) { JSON.parse(File.read(expected_data_file)) } + let(:expected_projects) { expected_data['mutable_projects'].map { |p| project_with_run_id p } } + let(:expected_secrets) { expected_data['mutable_secrets'].map { |s| secret_with_project_id(secret_with_run_id(s), projects) } } + let(:projects) { @client.projects.list(organization_id) } + let(:write_project_name) { with_run_id('for_write_tests') } + let(:write_project) { projects.find { |p| p['name'] == write_project_name } } + + before(:all) do + # Set up client + @state_file = File.join(language_tests_path, 'mutable_state.json') + @client = BitwardenSDKSecrets::BitwardenClient.new(bitwarden_settings) + @client.auth.login_access_token(env('MUTABLE_ACCESS_TOKEN'), @state_file) + end + + it 'should successfully create a project' do + to_create = { + 'name' => with_run_id('created_project') + } + created = @client.projects.create(organization_id, to_create['name']) + expect(created).to equal_project(to_create) + end + + it 'should successfully update a project' do + to_update = projects.find { |p| p['name'] == with_run_id('to_update') } + expect(to_update).to_not be_nil + updated_project = { + 'name' => with_run_id('updated_project') + } + updated = @client.projects.update(organization_id, to_update['id'], updated_project['name']) + expect(updated).to equal_project(updated_project) + end + + it 'should delete a project' do + to_delete = projects.find { |p| p['name'] == with_run_id('to_delete') } + expect(to_delete).to_not be_nil + deleted = @client.projects.delete([to_delete['id']]) + expect(deleted).to be_an_instance_of(Array) + expect(deleted.length).to eq(1) + expect(deleted[0]['id']).to eq(to_delete['id']) + end + + after(:all) do + File.delete(@state_file) if File.exist?(@state_file) + end +end diff --git a/languages/ruby/test.sh b/languages/ruby/test.sh new file mode 100644 index 000000000..b59c6eff2 --- /dev/null +++ b/languages/ruby/test.sh @@ -0,0 +1,5 @@ +#!/bin/sh +cargo build --package bitwarden-c +cd spec +bundle install +bundle exec rspec e2e_spec.rb