From 6fcaaffdb01d2b71089f834300dbd8d0ae2b46ee Mon Sep 17 00:00:00 2001 From: Ami Mahloof <130996527+ami-descope@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:17:50 -0500 Subject: [PATCH] Rspec: integration tests. --- .github/workflows/ci.yaml | 22 +- Gemfile | 11 +- Gemfile.lock | 28 +- .../ruby-on-rails-api/descope/Gemfile.lock | 2 +- lib/descope/api/v1/management/role.rb | 4 +- lib/descope/mixins/http.rb | 2 +- .../api/v1/auth/enchantedlink_spec.rb | 81 ++++++ .../lib.descope/api/v1/auth/magiclink_spec.rb | 49 ++++ .../lib.descope/api/v1/auth/otp_spec.rb | 38 +++ .../lib.descope/api/v1/auth/password_spec.rb | 41 +++ .../lib.descope/api/v1/auth/totp_spec.rb | 76 +++++ .../api/v1/management/access_key_spec.rb | 62 +++++ .../api/v1/management/audit_spec.rb | 16 ++ .../api/v1/management/authz_spec.rb | 187 +++++++++++++ .../api/v1/management/flow_spec.rb | 44 +++ .../api/v1/management/permissions_spec.rb | 27 ++ .../api/v1/management/project_spec.rb | 29 ++ .../api/v1/management/roles_spec.rb | 46 +++ .../api/v1/management/user_spec.rb | 262 ++++++++++++++++++ spec/spec_helper.rb | 16 +- spec/support/client_config.rb | 18 ++ spec/support/dummy_class.rb | 1 + spec/support/utils.rb | 2 +- 23 files changed, 1033 insertions(+), 31 deletions(-) create mode 100644 spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/auth/otp_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/auth/password_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/auth/totp_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/management/access_key_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/management/audit_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/management/authz_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/management/flow_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/management/permissions_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/management/project_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/management/roles_spec.rb create mode 100644 spec/integration/lib.descope/api/v1/management/user_spec.rb create mode 100644 spec/support/client_config.rb diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 959927f..aff4b64 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,10 +7,18 @@ on: pull_request: branches: - main + workflow_dispatch: + inputs: + DESCOPE_LOG_LEVEL: + description: "Descope Log Level" + default: "info" + +env: + DESCOPE_LOG_LEVEL: ${{ github.event.inputs.DESCOPE_LOG_LEVEL || 'info' }} jobs: - build: - name: Build Ruby SDK + ci: + name: Descope Ruby SDK CI runs-on: ubuntu-latest steps: - name: Checkout Code @@ -25,12 +33,18 @@ jobs: run: bundle install - name: Run RSpec Test - run: bundle exec rspec + run: bundle exec rspec spec/lib.descope + + - name: Run RSpec Integration Tests + env: + DESCOPE_MANAGEMENT_KEY: ${{ secrets.DESCOPE_MANAGEMENT_KEY }} + DESCOPE_PROJECT_ID: ${{ secrets.DESCOPE_PROJECT_ID }} + run: bundle exec rspec spec/integration # in order to release use conventional commits # $ git commit --allow-empty -m "chore: release 1.0.0" -m "Release-As: 1.0.0" && git push # this will open a new PR with the changelog and bump the version - # Release Please assumes you are using Conventional Commit messages. + # Release Please will assume that you are using Conventional Commit messages. # # The most important prefixes you should have in mind are: # diff --git a/Gemfile b/Gemfile index 908820c..2b0afaf 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec @@ -7,9 +9,12 @@ group :development do end group :test do - gem 'super_diff','0.11.0', require: false gem 'factory_bot', '6.4.6', require: false - gem 'selenium-webdriver', '4.17.0', require: false - gem 'rotp', '6.3.0', require: false + gem 'faker', require: false gem 'rack-test', '2.1.0', require: false + gem 'rotp', '6.3.0', require: false + gem 'rspec', '3.13.0', require: false + gem 'selenium-webdriver', '4.17.0', require: false + gem 'simplecov', '0.22.0', require: false + gem 'super_diff', '0.11.0', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index fdcea99..ae9f741 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,19 +78,19 @@ GEM retryable (3.0.5) rexml (3.2.6) rotp (6.3.0) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.1) + rspec-support (~> 3.13.0) + rspec-support (3.13.0) rubocop (1.60.2) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -144,16 +144,16 @@ DEPENDENCIES concurrent-ruby (~> 1.1) descope! factory_bot (= 6.4.6) - faker (~> 2.0) + faker fuubar (~> 2.0) rack-test (= 2.1.0) rake (~> 13.0) rotp (= 6.3.0) - rspec (~> 3.11) + rspec (= 3.13.0) rubocop (= 1.60.2) rubocop-rails (= 2.23.1) selenium-webdriver (= 4.17.0) - simplecov (~> 0.9) + simplecov (= 0.22.0) super_diff (= 0.11.0) BUNDLED WITH diff --git a/examples/ruby-on-rails-api/descope/Gemfile.lock b/examples/ruby-on-rails-api/descope/Gemfile.lock index 91c3457..a96046e 100644 --- a/examples/ruby-on-rails-api/descope/Gemfile.lock +++ b/examples/ruby-on-rails-api/descope/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: ../../../.. specs: - descope (1.0.3) + descope (1.0.4) addressable (~> 2.8) jwt (~> 2.7) rest-client (~> 2.1) diff --git a/lib/descope/api/v1/management/role.rb b/lib/descope/api/v1/management/role.rb index ea421b4..ab9f5d4 100644 --- a/lib/descope/api/v1/management/role.rb +++ b/lib/descope/api/v1/management/role.rb @@ -36,7 +36,9 @@ def update_role(name: nil, new_name: nil, description: nil, permission_names: ni def delete_role(name: nil, tenant_id: nil) # Delete an existing role. IMPORTANT: This action is irreversible. Use carefully. - post(ROLE_DELETE_PATH, { name:, tenantId: tenant_id }) + request_params = { name: } + request_params[:tenantId] = tenant_id if tenant_id + post(ROLE_DELETE_PATH, request_params) end def load_all_roles diff --git a/lib/descope/mixins/http.rb b/lib/descope/mixins/http.rb index 456296a..d013d0b 100644 --- a/lib/descope/mixins/http.rb +++ b/lib/descope/mixins/http.rb @@ -96,7 +96,7 @@ def request(method, uri, body = {}, extra_headers = {}) raise Descope::Unsupported.new("No response from server", code: 400) unless result && result.respond_to?(:code) - @logger.info "http status code: #{result.code}" + @logger.info("API Request: [#{method}] #{uri} - Response Code: #{result.code}") case result.code when 200...226 then safe_parse_json(result.body) when 400 then raise Descope::BadRequest.new(result.body, code: result.code, headers: result.headers) diff --git a/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb b/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb new file mode 100644 index 0000000..6d1c2ae --- /dev/null +++ b/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +def poll_for_session(descope_client, pending_ref) + max_tries = 15 + i = 0 + done = false + while !done && i < max_tries + begin + i += 1 + @client.logger.info('waiting 4 seconds for session to be created...') + sleep(4) + print '.' + @client.logger.info("Getting session for pending_ref: #{pending_ref}...") + jwt_response = descope_client.enchanted_link_get_session(pending_ref) + done = true + rescue Descope::AuthException, Descope::Unauthorized => e + @client.logger.info("Failed pending session, err: #{e}") + nil + end + + next unless jwt_response + + @client.logger.info("jwt_response: #{jwt_response}") + refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME]['jwt'] + + @client.logger.info("refresh_token: #{refresh_token}") + done = true + return refresh_token + end +end + +def verify_session(descope_client: nil, res: nil, user: nil) + raise StandardError, 'Missing required parameters' if descope_client.nil? || res.nil? || user.nil? + + token = res['link'].match(/.+verify\?t=(.+)/)[1] + @client.logger.info("token: #{token}") + + expect do + descope_client.enchanted_link_verify_token(token) + @client.logger.info('EnchantedLink Token Verified! now getting session information...') + @client.logger.info('Polling for session...') + refresh_token = poll_for_session(descope_client, res['pendingRef']) + my_details = descope_client.me(refresh_token) + expect(my_details['email']).to eq(user['email']) + @client.logger.info('EnchantedLink Token Verified via sign in!') + rescue StandardError => e + raise StandardError, "Verification failed - Could not verify token #{e.message}" + + end.to_not raise_error +end + +describe Descope::Api::V1::Auth::EnchantedLink do + before(:all) do + @client = DescopeClient.new(Configuration.config) + end + + after(:all) do + @client.logger.info('Cleaning up test users...') + all_users = @client.search_all_users + all_users['users'].each do |user| + if user['middleName'] == 'Ruby SDK User' + @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}") + @client.delete_user(user['loginIds'][0]) + end + end + end + + context 'test EnchantedLink for test user' do + it 'should sign in with enchanted link' do + user = build(:user) + test_user = @client.create_test_user(**user)['user'] + @client.logger.info("Should sign in a test user => #{test_user['loginIds'][0]} with enchanted link...") + res = @client.generate_enchanted_link_for_test_user(login_id: test_user['loginIds'][0], uri: 'http://localhost:3000/verify') + @client.logger.info("res: #{res}") + @client.logger.info('Verifying session...') + verify_session(descope_client: @client, res:, user: test_user) + end + end +end diff --git a/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb b/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb new file mode 100644 index 0000000..664b2e3 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Auth::MagicLink do + before(:all) do + @client = DescopeClient.new(Configuration.config) + end + + after(:all) do + @client.logger.info('Cleaning up test users...') + all_users = @client.search_all_users + all_users['users'].each do |user| + if user['middleName'] == 'Ruby SDK User' + @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}") + @client.delete_user(user['loginIds'][0]) + end + end + end + + context 'test Magiclink for test user' do + it 'should sign in with magiclink' do + user = build(:user) + test_user = @client.create_test_user(**user)['user'] + @client.create_test_user(**user) + res = @client.generate_magic_link_for_test_user( + method: Descope::Mixins::Common::DeliveryMethod::EMAIL, + login_id: test_user['loginIds'][0], + uri: 'http://localhost:3000/verify' + ) + @client.logger.info("res: #{res}") + token = res['link'].match(/^http.+verify\?t=(.+)/)[1] + @client.logger.info("token: #{token}") + + expect do + @client.logger.info('Verifying token...') + jwt_response = @client.magiclink_verify_token(token) + @client.logger.info("jwt_response #{jwt_response}") + my_details = @client.me(jwt_response['refreshSessionToken']['jwt']) + @client.logger.info('verifying session...') + expect(my_details['email']).to eq(test_user['email']) + @client.logger.info('Magiclink Token Verified via sign in!') + rescue StandardError => e + raise StandardError, "Verification failed - Could not verify token: #{e.message}" + + end.to_not raise_error + end + end +end diff --git a/spec/integration/lib.descope/api/v1/auth/otp_spec.rb b/spec/integration/lib.descope/api/v1/auth/otp_spec.rb new file mode 100644 index 0000000..d8d9ec0 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/auth/otp_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Auth::OTP do + before(:all) do + @client = DescopeClient.new(Configuration.config) + end + + after(:all) do + @client.logger.info('Cleaning up test users...') + all_users = @client.search_all_users + all_users['users'].each do |user| + if user['middleName'] == 'Ruby SDK User' + @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}") + @client.delete_user(user['loginIds'][0]) + end + end + end + + context 'test otp sign-in with test user' do + it 'should sign in with otp' do + user = build(:user) + test_user = @client.create_test_user(**user)['user'] + @client.create_test_user(**user) + res = @client.generate_otp_for_test_user( + method: Descope::Mixins::Common::DeliveryMethod::EMAIL, + login_id: test_user['loginIds'][0] + ) + @client.logger.info("res: #{res}") + @client.otp_verify_code( + method: Descope::Mixins::Common::DeliveryMethod::EMAIL, + login_id: user[:login_id], + code: res['code'] + ) + end + end +end diff --git a/spec/integration/lib.descope/api/v1/auth/password_spec.rb b/spec/integration/lib.descope/api/v1/auth/password_spec.rb new file mode 100644 index 0000000..3bfbd42 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/auth/password_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rotp' + +describe Descope::Api::V1::Auth::Password do + before(:all) do + @password = SpecUtils.generate_password + @new_password = SpecUtils.generate_password + @user = build(:user) + @client = DescopeClient.new(Configuration.config) + end + + context 'test password methods' do + it 'should get password policy' do + # Get the configured password policy for the project. + res = @client.get_password_policy + @client.logger.info("Password policy: #{res}") + end + + it 'should sign up with password' do + res = @client.password_sign_up(login_id: @user[:login_id], password: @password, user: @user) + expect { res }.not_to raise_error + end + + it 'should sign in with password' do + res = @client.password_sign_in(login_id: @user[:login_id], password: @password) + expect { res }.not_to raise_error + end + + it 'should replace the password' do + res = @client.password_replace(login_id: @user[:login_id], old_password: @password, new_password: @new_password) + expect { res }.not_to raise_error + end + + it 'should login with new password' do + res = @client.password_sign_in(login_id: @user[:login_id], password: @new_password) + expect { res }.not_to raise_error + end + end +end diff --git a/spec/integration/lib.descope/api/v1/auth/totp_spec.rb b/spec/integration/lib.descope/api/v1/auth/totp_spec.rb new file mode 100644 index 0000000..e523a1b --- /dev/null +++ b/spec/integration/lib.descope/api/v1/auth/totp_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rotp' + +describe Descope::Api::V1::Auth::TOTP do + before(:all) do + @client = DescopeClient.new(Configuration.config) + end + + after(:all) do + @client.logger.info('Cleaning up test users...') + all_users = @client.search_all_users + all_users['users'].each do |user| + if user['middleName'] == 'Ruby SDK User' + @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}") + @client.delete_user(user['loginIds'][0]) + end + end + end + + context 'test totp methods' do + it 'should sign up with totp' do + # Initiate a TOTP sign-up process for a new end user. + # Descope will generate a TOTP key (also called a secret or seed) that will be entered into the end user's + # authenticator app so that TOTP codes can be successfully verified. + # The new end user will be registered after the full TOTP sign-up flow has successfully completed. + + user = build(:user) + + @client.logger.info('1. Sign up TOTP') + totp_key = @client.totp_sign_up(login_id: user[:login_id], user:)['key'] + totp = ROTP::TOTP.new(totp_key) + p "Current OTP: #{totp.now}" + + @client.logger.info('2. TOTP sign in') + login_res = @client.totp_sign_in_code(login_id: user[:login_id], code: totp.now) + @client.logger.info("login_res: #{login_res}") + refresh_token = login_res['refreshSessionToken']['jwt'] + + @client.logger.info('3. Verify email') + my_details = @client.me(refresh_token) + expect(my_details['email']).to eq(user[:email]) + end + + it 'should add or update totp key' do + # Add or update TOTP key for existing end user + # Update the email address of an end user, after verifying the authenticity of the end user using OTP. + + user = build(:user) + + @client.logger.info('1. Sign up TOTP') + totp_key = @client.totp_sign_up(login_id: user[:login_id], user:)['key'] + totp = ROTP::TOTP.new(totp_key) + p "Current OTP: #{totp.now}" + + @client.logger.info('2. TOTP sign in') + login_res = @client.totp_sign_in_code(login_id: user[:login_id], code: totp.now) + @client.logger.info("login_res: #{login_res}") + refresh_token = login_res['refreshSessionToken']['jwt'] + + @client.logger.info('3. Add or update TOTP key') + new_key = @client.totp_add_update_key(login_id: user[:login_id], refresh_token:)['key'] + new_totp = ROTP::TOTP.new(new_key) + p "New OTP: #{totp.now}" + + @client.logger.info('4. TOTP sign in with new key') + login_res = @client.totp_sign_in_code(login_id: user[:login_id], code: new_totp.now) + refresh_token = login_res['refreshSessionToken']['jwt'] + + @client.logger.info('5. Verify email') + my_details = @client.me(refresh_token) + expect(my_details['email']).to eq(user[:email]) + end + end +end diff --git a/spec/integration/lib.descope/api/v1/management/access_key_spec.rb b/spec/integration/lib.descope/api/v1/management/access_key_spec.rb new file mode 100644 index 0000000..d3d9ce7 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/management/access_key_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::AccessKey do + before(:all) do + @client = DescopeClient.new(Configuration.config) + end + + context 'perform access key methods like create, delete load' do + before(:all) do + @key_name = 'Ruby SDK Test Key' + + keys = @client.search_all_access_keys['keys'] + keys.each do |key| + if key['name'] == @key_name + @client.delete_access_key(key['id']) + @client.logger.info("deleting test access key #{@key_name}") + end + end + + res = @client.search_all_tenants(names: ['some-new-tenant']) + res['tenants'].each do |tenant| + @client.delete_tenant(tenant['id']) + end + + @client.logger.info('Creating tenant with name: some-new-tenant') + @tenant_id = @client.create_tenant(name: 'some-new-tenant')['id'] + @client.logger.info('creating access key') + @access_key = @client.create_access_key(name: @key_name, key_tenants: [{ tenant_id: @tenant_id }]) + sleep 60 + end + + it 'should create the access key and load it' do + response = @client.load_access_key(@access_key['key']['id']) + expect(response['key']['name']).to eq(@key_name) + end + + it 'should update the access key' do + new_name = 'Ruby SDK Test Key Updated' + @client.logger.info("access key id: #{@access_key['key']['id']}") + response = @client.update_access_key(id: @access_key['key']['id'], name: new_name) + expect(response['key']['name']).to eq(new_name) + end + + it 'should deactivate the access key' do + response = @client.deactivate_access_key(@access_key['key']['id']) + @client.logger.info("deactivate key response: #{response}") + # expect(response['key']['status']).to eq('DEACTIVATED') + end + + it 'should activate the access key' do + response = @client.activate_access_key(@access_key['key']['id']) + @client.logger.info("activate key response: #{response}") + end + + after(:all) do + @client.delete_access_key(@access_key['key']['id']) + @client.delete_tenant(@tenant_id) + end + end +end diff --git a/spec/integration/lib.descope/api/v1/management/audit_spec.rb b/spec/integration/lib.descope/api/v1/management/audit_spec.rb new file mode 100644 index 0000000..9ad465e --- /dev/null +++ b/spec/integration/lib.descope/api/v1/management/audit_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::Audit do + before(:all) do + @client = DescopeClient.new(Configuration.config) + end + + + it 'should search the audit trail for user operations' do + res = @client.audit_search(actions: ['LoginSucceed']) + expect(res).to be_a(Hash) + expect(res['audits']).to be_a(Array) + end +end diff --git a/spec/integration/lib.descope/api/v1/management/authz_spec.rb b/spec/integration/lib.descope/api/v1/management/authz_spec.rb new file mode 100644 index 0000000..2af6eb9 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/management/authz_spec.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::Authz do + before(:all) do + @client = DescopeClient.new(Configuration.config) + puts 'authz schema delete' + end + + context 'authz ops' do + before(:all) do + @client.authz_delete_schema + end + + it 'should create a new schema' do + puts 'Creating the ReBAC schema...' + schema = { + name: '1.0', + namespaces: [ + { + name: 'group', + relationDefinitions: [ + { + name: 'member' + }, + { + name: 'owner' + } + ] + }, + { + name: 'note', + relationDefinitions: [ + { + name: 'owner' + }, + { + name: 'editor', + complexDefinition: { + nType: 'union', + children: [ + { + nType: 'child', + expression: { + neType: 'self' + } + }, + { + nType: 'child', + expression: { + neType: 'targetSet', + targetRelationDefinition: 'owner', + targetRelationDefinitionNamespace: 'note' + } + } + ] + } + }, + { + name: 'viewer', + complexDefinition: { + nType: 'union', + children: [ + { + nType: 'child', + expression: { + neType: 'self' + } + }, + { + nType: 'child', + expression: { + neType: 'targetSet', + targetRelationDefinition: 'editor', + targetRelationDefinitionNamespace: 'note' + } + } + ] + } + } + ] + } + ] + } + @client.authz_save_schema(schema:, upgrade: true) + end + + it 'should create relation definition' do + @client.authz_save_relation_definition( + relation_definition: { + name: 'owner' + }, + namespace: 'group' + ) + @client.authz_save_relation_definition( + relation_definition: { + name: 'member' + }, + namespace: 'group' + ) + @client.authz_save_relation_definition( + relation_definition: { + name: 'owner' + }, + namespace: 'note' + ) + @client.authz_save_relation_definition( + relation_definition: { + name: 'editor', + complexDefinition: { + nType: 'union', + children: [ + { + nType: 'child', + expression: { + neType: 'self' + } + }, + { + nType: 'child', + expression: { + neType: 'targetSet', + targetRelationDefinition: 'owner', + targetRelationDefinitionNamespace: 'note' + } + } + ] + } + }, + namespace: 'note' + ) + @client.authz_save_relation_definition( + relation_definition: { + name: 'viewer', + complexDefinition: { + nType: 'union', + children: [ + { + nType: 'child', + expression: { + neType: 'self' + } + }, + { + nType: 'child', + expression: { + neType: 'targetSet', + targetRelationDefinition: 'editor', + targetRelationDefinitionNamespace: 'note' + } + } + ] + } + }, + namespace: 'note' + ) + end + + it 'should create a relation between a resource and user' do + @client.authz_create_relations( + [ + { + "resource": 'some-doc', + "relationDefinition": 'owner', + "namespace": 'note', + "target": 'user1' + } + ] + ) + + # Check if target has the relevant relation + # The answer should be true because an owner is also a viewer + relations = @client.authz_has_relations?( + [ + { + "resource": 'some-doc', + "relationDefinition": 'viewer', + "namespace": 'note', + "target": 'user1' + } + ] + ) + expect(relations['relationQueries'][0]['hasRelation']).to be_truthy + end + end +end diff --git a/spec/integration/lib.descope/api/v1/management/flow_spec.rb b/spec/integration/lib.descope/api/v1/management/flow_spec.rb new file mode 100644 index 0000000..6654937 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/management/flow_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::Flow do + before(:all) do + @client = DescopeClient.new(Configuration.config) + end + + it 'should return a list of flows' do + flows = @client.list_or_search_flows['flows'] + expect(flows.length).to be > 5 + end + + it 'should search for the sign-up-or-in flow' do + flows = @client.list_or_search_flows(['sign-up-or-in'])['flows'] + expect(flows.length).to eq(1) + expect(flows[0]['name']).to eq('Sign Up or In') + end + + it 'should export the sign-up-or-in flow' do + export = @client.export_flow('sign-up-or-in') + expect(export['flow']['name']).to eq('Sign Up or In') + expect(export['screens'].length).to be > 1 + end + + it 'should import the sign-up-or-in flow' do + flow = @client.export_flow('sign-up-or-in') + imported_flow = @client.import_flow(flow_id: 'sign-up-or-in', flow: flow['flow'], screens: flow['screens']) + expect(imported_flow).not_to be_nil + end + + it 'should export the current project theme' do + theme = @client.export_theme + expect(theme['theme']['cssTemplate']).not_to be_empty + end + + it 'should import the current project theme' do + export_theme = @client.export_theme + export_theme_current_version = export_theme['theme']['version'] + imported_theme = @client.import_theme(export_theme) + expect(imported_theme['theme']['version']).to be(export_theme_current_version + 1) + end +end diff --git a/spec/integration/lib.descope/api/v1/management/permissions_spec.rb b/spec/integration/lib.descope/api/v1/management/permissions_spec.rb new file mode 100644 index 0000000..bd0b777 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/management/permissions_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::Permission do + before(:all) do + @client = DescopeClient.new(Configuration.config) + @client.load_all_permissions['permissions'].each do |perm| + if perm['description'] == 'Ruby SDK' + puts "Deleting permission: #{perm['name']}" + @client.delete_permission(perm['name']) + end + end + end + + it 'should create update and delete a permission' do + @client.create_permission(name: 'test_permission', description: 'Ruby SDK') + all_permissions = @client.load_all_permissions['permissions'] + expect(all_permissions.any? { |perm| perm['name'] == 'test_permission' }).to eq(true) + @client.update_permission(name: 'test_permission', new_name: 'test_permission_2') + all_permissions = @client.load_all_permissions['permissions'] + expect(all_permissions.any? { |perm| perm['name'] == 'test_permission_2' }).to eq(true) + @client.delete_permission('test_permission_2') + all_permissions = @client.load_all_permissions['permissions'] + expect(all_permissions.any? { |perm| perm['name'] == 'test_permission_2' }).to eq(false) + end +end diff --git a/spec/integration/lib.descope/api/v1/management/project_spec.rb b/spec/integration/lib.descope/api/v1/management/project_spec.rb new file mode 100644 index 0000000..7a60391 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/management/project_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::Project do + before(:all) do + @client = DescopeClient.new(Configuration.config) + @export_output = @client.export_project + end + + context 'Test project methods' do + after(:all) do + @client.rename_project('Ruby-SDK-Prod') + end + + it 'should rename a project' do + @client.rename_project('TEST-Ruby-SDK-Prod') + end + + it 'should export a project' do + expect(@export_output).to_not be_empty + expect(@export_output).to be_a(Hash) + end + + it 'should import a project' do + @client.import_project(files: @export_output['files']) + end + end +end diff --git a/spec/integration/lib.descope/api/v1/management/roles_spec.rb b/spec/integration/lib.descope/api/v1/management/roles_spec.rb new file mode 100644 index 0000000..70c2d08 --- /dev/null +++ b/spec/integration/lib.descope/api/v1/management/roles_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::Role do + before(:all) do + @client = DescopeClient.new(Configuration.config) + @client.load_all_permissions['permissions'].each do |perm| + if perm['description'] == 'Ruby SDK' + puts "Deleting permission: #{perm['name']}" + @client.delete_permission(perm['name']) + end + end + + @client.load_all_roles['roles'].each do |role| + if role['description'] == 'Ruby SDK' + puts "Deleting role: #{role['name']}" + @client.delete_role(name: role['name']) + end + end + end + + it 'should create update and delete a role' do + puts 'creating permission for role' + @client.create_permission(name: 'test_permission', description: 'Ruby SDK') + puts 'creating role' + @client.create_role(name: 'Ruby SDK test role', description: 'Ruby SDK', permission_names: ['test_permission']) + puts 'loading all roles' + all_roles = @client.load_all_roles['roles'] + expect(all_roles.any? { |role| role['name'] == 'Ruby SDK test role' }).to eq(true) + expect(all_roles.any? { |role| role['permissionNames'] == ['test_permission'] }).to eq(true) + puts 'updating role' + @client.update_role( + name: 'Ruby SDK test role', + new_name: 'Ruby SDK test role 2', + description: 'Ruby SDK', + permission_names: ['test_permission'] + ) + all_roles = @client.load_all_roles['roles'] + expect(all_roles.any? { |role| role['name'] == 'Ruby SDK test role 2' }).to eq(true) + puts 'deleting permission' + @client.delete_permission('test_permission') + puts 'deleting role' + @client.delete_role(name: 'Ruby SDK test role 2') + end +end diff --git a/spec/integration/lib.descope/api/v1/management/user_spec.rb b/spec/integration/lib.descope/api/v1/management/user_spec.rb new file mode 100644 index 0000000..c64c9ca --- /dev/null +++ b/spec/integration/lib.descope/api/v1/management/user_spec.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Descope::Api::V1::Management::User do + before(:all) do + @password = SpecUtils.generate_password + @new_password = SpecUtils.generate_password + @user = build(:user) + @client = DescopeClient.new(Configuration.config) + include Descope::Mixins::Common::DeliveryMethod + end + + after(:all) do + all_users = @client.search_all_users + all_users['users'].each do |user| + if user['middleName'] == 'Ruby SDK User' + puts "Deleting ruby spec test user #{user['loginIds'][0]}" + @client.delete_user(user['loginIds'][0]) + end + end + end + + it 'should create a user' do + user = build(:user) + created_user = @client.create_user(**user)['user'] + + loaded_user = @client.load_user(created_user['loginIds'][0])['user'] + + expect(loaded_user['loginIds']).to eq(created_user['loginIds']) + expect(loaded_user['email']).to eq(created_user['email']) + expect(loaded_user['phone']).to eq(created_user['phone']) + expect(loaded_user['display_name']).to eq(created_user['display_name']) + expect(loaded_user['given_name']).to eq(created_user['given_name']) + expect(loaded_user['middle_name']).to eq(created_user['middle_name']) + expect(loaded_user['family_name']).to eq(created_user['family_name']) + expect(loaded_user['picture']).to eq(created_user['picture']) + end + + it 'should create batch users' do + users = FactoryBot.build_list(:user, 5) + batch_res = @client.create_batch_users(users) + created_users = batch_res['createdUsers'] + + created_users.each do |user| + expect(user['status']).to eq('invited') + end + + expect(batch_res['failedUsers']).to eq([]) + end + + it 'should update a user' do + user = build(:user) + created_user = @client.create_user(**user)['user'] + updated_first_name = 'new name' + updated_user = @client.update_user(**user, given_name: updated_first_name)['user'] + + expect(updated_user['first_name']).to eq(created_user[updated_first_name]) + end + + it 'should delete a user' do + user = build(:user) + created_user = @client.create_user(**user)['user'] + loaded_user = @client.load_user(created_user['loginIds'][0])['user'] + expect(loaded_user['loginIds']).to eq(created_user['loginIds']) + sleep 10 + + @client.delete_user(created_user['loginIds'][0]) + begin + @client.load_user(created_user['loginIds'][0]) + rescue Descope::NotFound => e + expect(e.message).to match(/"errorCode":"E112102"/) + end + end + + it 'should search all users' do + users = FactoryBot.build_list(:user, 5) + @client.create_batch_users(users) + all_users = @client.search_all_users + sdk_users = all_users['users'].select { |user| user['middleName'] == 'Ruby SDK User' } + expect(sdk_users.length).to be >= 5 + end + + it 'should create a test user' do + @client.delete_all_test_users + sleep 5 + user_args = build(:user) + test_user = @client.create_test_user(**user_args)['user'] + test_users = @client.search_all_users(test_users_only: true)['users'] + expect(test_users.length).to be >= 1 + expect(test_users[0]['loginIds'][0]).to eq(test_user['loginIds'][0]) + end + + it 'should update user status' do + user_args = build(:user) + user = @client.create_user(**user_args)['user'] + loaded_user = @client.load_user(user['loginIds'][0])['user'] + expect(loaded_user['status']).to eq('invited') + @client.activate(user['loginIds'][0]) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + expect(loaded_user['status']).to eq('enabled') + @client.deactivate(user['loginIds'][0]) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + expect(loaded_user['status']).to eq('disabled') + end + + it 'should update user email' do + user_args = build(:user) + user = @client.create_user(**user_args)['user'] + email = Faker::Internet.email + @client.update_email(login_id: user['loginIds'][0], email:) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + puts "loaded_user #{loaded_user}" + expect(loaded_user['email']).to eq(email) + end + + it 'should update user phone' do + user_args = build(:user) + user = @client.create_user(**user_args)['user'] + phone = "+1#{Faker::Number.number(digits: 10)}" + @client.update_phone(login_id: user['loginIds'][0], phone:) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + expect(loaded_user['phone']).to eq(phone) + end + + it 'should update user picture' do + user_args = build(:user) + user = @client.create_user(**user_args)['user'] + picture = Faker::Internet.url + @client.update_picture(login_id: user['loginIds'][0], picture:) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + expect(loaded_user['picture']).to eq(picture) + end + + it 'should update user custom attributes' do + user_args = build(:user) + user = @client.create_user(**user_args, custom_attributes: { newUser: false })['user'] + puts "user## #{user}" + @client.update_custom_attribute(login_id: user['loginIds'][0], attribute_key: 'newUser', attribute_value: true) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + puts "loaded_user #{loaded_user}" + expect(loaded_user['customAttributes']).to eq({ 'newUser' => true }) + end + + it 'should update display name' do + user_args = build(:user) + user = @client.create_user(**user_args)['user'] + name = Faker::Name.name + @client.update_display_name(login_id: user['loginIds'][0], name:) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + expect(loaded_user['name']).to eq(name) + end + + it 'should update user JWT and custom claims' do + user_args = build(:user) + password = SpecUtils.generate_password + custom_claims = { "custom-key1": 'custom-value1', "custom-key2": 'custom-value2' } + user = @client.create_user(**user_args, password:)['user'] + jwt = @client.password_sign_in(login_id: user['loginIds'][0], password:)['refreshSessionToken']['jwt'] + jwt_res = @client.update_jwt(jwt:, custom_claims:) + decoded_jwt = @client.validate_token(jwt_res['jwt']) + + # check if all keys and values from custom_claims are present in decoded_jwt + claims_in_jwt = custom_claims.all? do |k, v| + decoded_jwt[k.to_s] == v + end + + expect(claims_in_jwt).to be true + end + + it 'should expire user password' do + user_args = build(:user) + password = SpecUtils.generate_password + user = @client.create_user(**user_args, password:)['user'] + @client.password_sign_in(login_id: user['loginIds'][0], password:) + begin + @client.expire_password(user['loginIds'][0]) + @client.password_sign_in(login_id: user['loginIds'][0], password:) + rescue Descope::ServerError => e + expect(e.message).to match(/"errorCode":"E062909"/) + end + end + + it 'should set user password' do + user_args = build(:user) + password = SpecUtils.generate_password + user = @client.create_user(**user_args, password:)['user'] + @client.password_sign_in(login_id: user['loginIds'][0], password:) + + begin + new_password = SpecUtils.generate_password + @client.set_password(login_id: user['loginIds'][0], password: new_password) + @client.password_sign_in(login_id: user['loginIds'][0], password:) + rescue Descope::ServerError => e + expect(e.message).to match(/"errorCode":"E062909"/) + end + end + + it 'should update create tenant, add to user, remove from user and delete tenant' do + res = @client.search_all_tenants(names: ['some-new-tenant']) + puts "res #{res}" + res['tenants'].each do |tenant| + puts "Deleting tenant #{tenant['id']}" + @client.delete_tenant(tenant['id']) + end + tenant_id = @client.create_tenant(name: 'some-new-tenant')['id'] + user_args = build(:user) + user = @client.create_user(**user_args)['user'] + @client.user_add_tenant(login_id: user['loginIds'][0], tenant_id:) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + expect(loaded_user['userTenants'][0]['tenantId']).to eq(tenant_id) + @client.user_remove_tenant(login_id: user['loginIds'][0], tenant_id:) + @client.delete_tenant(tenant_id) + end + + it 'should add and remove role from user create and delete role' do + role_name = 'some-new-role' + + # ensure no roles exist with that name + all_roles = @client.load_all_roles + all_roles['roles'].each do |role| + @client.delete_role(name: role['name']) if role['name'] == role_name + end + + @client.create_role(name: role_name) + user_args = build(:user) + user = @client.create_user(**user_args)['user'] + @client.user_add_roles(login_id: user['loginIds'][0], role_names: ['some-new-role']) + loaded_user = @client.load_user(user['loginIds'][0])['user'] + expect(loaded_user['roleNames'][0]).to eq(role_name) + @client.user_remove_roles(login_id: user['loginIds'][0], role_names: [role_name]) + @client.delete_role(name: role_name) + end + + + it 'should logout user of all sessions' do + user_args = build(:user) + password = SpecUtils.generate_password + user = @client.create_user(**user_args, password:)['user'] + session_token = @client.password_sign_in(login_id: user['loginIds'][0], password:)['refreshSessionToken']['jwt'] + @client.logout_user(user['loginIds'][0]) + @client.validate_and_refresh_session(session_token:) + end + + it 'should generate login methods for test user' do + @client.delete_all_test_users + user_args = build(:user) + user = @client.create_test_user(**user_args)['user'] + login_info = @client.generate_otp_for_test_user(method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: user['loginIds'][0]) + expect(login_info['loginId']).to eq(user['loginIds'][0]) + expect(login_info['code']).to_not be_nil + + login_info = @client.generate_enchanted_link_for_test_user(login_id: user['loginIds'][0], uri: 'http://localhost:3001/verify') + expect(login_info['loginId']).to eq(user['loginIds'][0]) + expect(login_info['link']).to start_with('http://localhost:3001/verify?t=') + expect(login_info['pendingRef']).to_not be_nil + + login_info = @client.generate_magic_link_for_test_user(method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: user['loginIds'][0], uri: 'http://localhost:3001/verify') + expect(login_info['loginId']).to eq(user['loginIds'][0]) + expect(login_info['link']).to start_with('http://localhost:3001/verify?t=') + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 57e4bd5..2901d79 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,23 +7,27 @@ require 'descope' require 'super_diff/rspec' require 'factory_bot' +require 'simplecov' +require 'simplecov-html' require 'rspec' - $LOAD_PATH.unshift File.expand_path('..', __FILE__) $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) -Dir['./lib/*.rb'].each { |f| require f } -Dir['./lib/api/**/*.rb'].each { |f| require f } -Dir['./spec/support/**/*.rb'].each { |f| require f } -Dir['./spec/support/*.rb'].each { |f| require f } - include Descope::Api::V1::Management::Common include Descope::Mixins::Common include Descope::Mixins::Common::EndpointsV1 include Descope::Mixins::Common::EndpointsV2 include Descope::Api::V1::Auth +Dir['./lib/*.rb'].each { |f| require f } +Dir['./lib/api/**/*.rb'].each { |f| require f } +Dir['./spec/support/**/*.rb'].each { |f| require f } +Dir['./spec/support/*.rb'].each { |f| require f } + +SimpleCov.start +SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter + RSpec.configure do |config| config.filter_run focus: true config.run_all_when_everything_filtered = true diff --git a/spec/support/client_config.rb b/spec/support/client_config.rb new file mode 100644 index 0000000..3e7dcdb --- /dev/null +++ b/spec/support/client_config.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + + +module Configuration + module_function + + def config + raise 'DESCOPE_MANAGEMENT_KEY is not set' if ENV['DESCOPE_MANAGEMENT_KEY'].nil? + raise 'DESCOPE_PROJECT_ID is not set' if ENV['DESCOPE_PROJECT_ID'].nil? + + { + descope_base_uri: ENV.fetch('DESCOPE_BASE_URI', Descope::Mixins::Common::DEFAULT_BASE_URL), + project_id: ENV.fetch('DESCOPE_PROJECT_ID', nil), + management_key: ENV.fetch('DESCOPE_MANAGEMENT_KEY', nil), + log_level: ENV.fetch('DESCOPE_LOG_LEVEL', 'info') + } + end +end diff --git a/spec/support/dummy_class.rb b/spec/support/dummy_class.rb index 1d42746..3aa02b1 100644 --- a/spec/support/dummy_class.rb +++ b/spec/support/dummy_class.rb @@ -5,6 +5,7 @@ class DummyClass include Descope::Mixins::Headers include Descope::Mixins::Common include Descope::Mixins::Common::EndpointsV1 + include Descope::Mixins::Common::EndpointsV2 include Descope::Api::V1::Management::Common include Descope::Api::V1::Auth diff --git a/spec/support/utils.rb b/spec/support/utils.rb index 7585dc9..4db3aca 100644 --- a/spec/support/utils.rb +++ b/spec/support/utils.rb @@ -16,7 +16,7 @@ def generate_password special_chars.sample # Fill in remaining characters randomly - 4.times { password += [lowercase_characters, uppercase_characters, digits, special_chars].sample.sample } + 5.times { password += [lowercase_characters, uppercase_characters, digits, special_chars].sample.sample } # Randomize the order of characters to make the password less predictable password.split('').shuffle.join