descope 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +51 -12
  3. data/.github/workflows/publish-gem.yaml +6 -26
  4. data/.github/workflows/release-please.yaml +36 -0
  5. data/.gitignore +5 -2
  6. data/.release-please-manifest.json +1 -1
  7. data/.ruby-version +1 -1
  8. data/CHANGELOG.md +21 -0
  9. data/Gemfile +8 -7
  10. data/Gemfile.lock +70 -56
  11. data/README.md +170 -51
  12. data/examples/ruby-on-rails-api/descope/Gemfile +8 -8
  13. data/examples/ruby-on-rails-api/descope/Gemfile.lock +1 -1
  14. data/examples/ruby-on-rails-api/descope/package-lock.json +203 -141
  15. data/examples/ruby-on-rails-api/descope/package.json +1 -1
  16. data/examples/ruby-on-rails-api/descope/yarn.lock +185 -87
  17. data/lib/descope/api/v1/auth/enchantedlink.rb +3 -1
  18. data/lib/descope/api/v1/auth/magiclink.rb +3 -1
  19. data/lib/descope/api/v1/auth/otp.rb +3 -1
  20. data/lib/descope/api/v1/auth/password.rb +6 -2
  21. data/lib/descope/api/v1/auth/totp.rb +3 -1
  22. data/lib/descope/api/v1/auth.rb +47 -12
  23. data/lib/descope/api/v1/management/common.rb +20 -5
  24. data/lib/descope/api/v1/management/sso_application.rb +236 -0
  25. data/lib/descope/api/v1/management/sso_settings.rb +2 -24
  26. data/lib/descope/api/v1/management/user.rb +151 -13
  27. data/lib/descope/api/v1/management.rb +2 -0
  28. data/lib/descope/api/v1/session.rb +37 -4
  29. data/lib/descope/mixins/common.rb +1 -0
  30. data/lib/descope/mixins/http.rb +60 -9
  31. data/lib/descope/mixins/initializer.rb +5 -2
  32. data/lib/descope/mixins/logging.rb +12 -4
  33. data/lib/descope/version.rb +1 -1
  34. data/spec/descope/api/v1/auth_spec.rb +29 -0
  35. data/spec/descope/api/v1/auth_token_extraction_spec.rb +126 -0
  36. data/spec/descope/api/v1/session_refresh_spec.rb +98 -0
  37. data/spec/factories/user.rb +1 -1
  38. data/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb +20 -22
  39. data/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb +6 -2
  40. data/spec/integration/lib.descope/api/v1/auth/otp_spec.rb +6 -2
  41. data/spec/integration/lib.descope/api/v1/auth/session_spec.rb +68 -0
  42. data/spec/integration/lib.descope/api/v1/auth/totp_spec.rb +6 -2
  43. data/spec/integration/lib.descope/api/v1/management/access_key_spec.rb +12 -1
  44. data/spec/integration/lib.descope/api/v1/management/audit_spec.rb +5 -3
  45. data/spec/integration/lib.descope/api/v1/management/authz_spec.rb +28 -5
  46. data/spec/integration/lib.descope/api/v1/management/flow_spec.rb +3 -1
  47. data/spec/integration/lib.descope/api/v1/management/permissions_spec.rb +22 -2
  48. data/spec/integration/lib.descope/api/v1/management/project_spec.rb +18 -2
  49. data/spec/integration/lib.descope/api/v1/management/roles_spec.rb +116 -36
  50. data/spec/integration/lib.descope/api/v1/management/user_spec.rb +74 -8
  51. data/spec/lib.descope/api/v1/auth/enchantedlink_spec.rb +11 -2
  52. data/spec/lib.descope/api/v1/auth/password_spec.rb +10 -1
  53. data/spec/lib.descope/api/v1/auth_spec.rb +167 -5
  54. data/spec/lib.descope/api/v1/cookie_domain_fix_integration_spec.rb +245 -0
  55. data/spec/lib.descope/api/v1/management/sso_application_spec.rb +217 -0
  56. data/spec/lib.descope/api/v1/management/sso_settings_spec.rb +2 -2
  57. data/spec/lib.descope/api/v1/management/user_spec.rb +134 -46
  58. data/spec/lib.descope/api/v1/session_spec.rb +119 -6
  59. data/spec/lib.descope/mixins/http_spec.rb +229 -0
  60. data/spec/support/client_config.rb +0 -1
  61. data/spec/support/utils.rb +21 -0
  62. metadata +14 -8
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require "addressable/uri"
2
+ require 'descope/mixins/common'
3
+ require 'addressable/uri'
3
4
  require 'retryable'
4
5
  require_relative '../exception'
5
6
 
@@ -7,6 +8,7 @@ module Descope
7
8
  module Mixins
8
9
  # HTTP-related methods
9
10
  module HTTP
11
+ include Descope::Mixins::Common
10
12
  attr_accessor :headers, :base_uri, :timeout, :retry_count
11
13
 
12
14
  DEFAULT_RETRIES = 3
@@ -44,13 +46,61 @@ module Descope
44
46
  }
45
47
  end
46
48
 
47
- def safe_parse_json(body)
49
+ def safe_parse_json(body, cookies: {}, headers: {})
48
50
  @logger.debug "response => #{JSON.parse(body.to_s)}"
49
- JSON.parse(body.to_s)
51
+ res = JSON.parse(body.to_s)
52
+
53
+ # Handle DS and DSR cookies in response.
54
+ # First check RestClient's cookies (works for same-domain cookies)
55
+ extracted_cookies = {}
56
+ if cookies.key?(SESSION_COOKIE_NAME)
57
+ extracted_cookies[SESSION_COOKIE_NAME] = cookies[SESSION_COOKIE_NAME]
58
+ end
59
+ if cookies.key?(REFRESH_SESSION_COOKIE_NAME)
60
+ extracted_cookies[REFRESH_SESSION_COOKIE_NAME] = cookies[REFRESH_SESSION_COOKIE_NAME]
61
+ end
62
+
63
+ # If no cookies found via RestClient, parse Set-Cookie headers directly
64
+ # This handles custom domain cookies that RestClient filters out
65
+ if extracted_cookies.empty? && headers.respond_to?(:[])
66
+ set_cookie_headers = headers[:set_cookie] || headers['set-cookie'] || headers['Set-Cookie'] || []
67
+ set_cookie_headers = [set_cookie_headers] unless set_cookie_headers.is_a?(Array)
68
+
69
+ set_cookie_headers.each do |cookie_header|
70
+ next unless cookie_header.is_a?(String)
71
+
72
+ # Parse DS cookie (session token)
73
+ if cookie_header.include?("#{SESSION_COOKIE_NAME}=")
74
+ cookie_value = parse_cookie_value(cookie_header, SESSION_COOKIE_NAME)
75
+ extracted_cookies[SESSION_COOKIE_NAME] = cookie_value if cookie_value
76
+ end
77
+
78
+ # Parse DSR cookie (refresh token)
79
+ if cookie_header.include?("#{REFRESH_SESSION_COOKIE_NAME}=")
80
+ cookie_value = parse_cookie_value(cookie_header, REFRESH_SESSION_COOKIE_NAME)
81
+ extracted_cookies[REFRESH_SESSION_COOKIE_NAME] = cookie_value if cookie_value
82
+ end
83
+ end
84
+ end
85
+
86
+ # Add extracted cookies to response if any were found
87
+ unless extracted_cookies.empty?
88
+ res['cookies'] = extracted_cookies
89
+ end
90
+
91
+ res
50
92
  rescue JSON::ParserError
51
93
  body
52
94
  end
53
95
 
96
+ def parse_cookie_value(cookie_header, cookie_name)
97
+ # Extract cookie value from Set-Cookie header
98
+ # Format: "cookieName=cookieValue; attribute1=value1; attribute2=value2"
99
+ # Only match valid cookie value characters (RFC 6265: exclude whitespace, semicolon, comma)
100
+ match = cookie_header.match(/#{Regexp.escape(cookie_name)}=([^;]+)/)
101
+ match ? match[1].strip : nil
102
+ end
103
+
54
104
  def encode_uri(uri)
55
105
  encoded_uri = base_uri ? Addressable::URI.parse(uri).normalize : Addressable::URI.escape(uri)
56
106
  @logger.debug "will call #{url(encoded_uri)}"
@@ -94,11 +144,12 @@ module Descope
94
144
  call(method, encode_uri(uri), timeout, @headers, body.to_json)
95
145
  end
96
146
 
97
- raise Descope::Unsupported.new("No response from server", code: 400) unless result && result.respond_to?(:code)
147
+ raise Descope::Unsupported.new('No response from server', code: 400) unless result.respond_to?(:code)
98
148
 
99
149
  @logger.info("API Request: [#{method}] #{uri} - Response Code: #{result.code}")
150
+
100
151
  case result.code
101
- when 200...226 then safe_parse_json(result.body)
152
+ when 200...226 then safe_parse_json(result.body, cookies: result.cookies, headers: result.headers)
102
153
  when 400 then raise Descope::BadRequest.new(result.body, code: result.code, headers: result.headers)
103
154
  when 401 then raise Descope::Unauthorized.new(result.body, code: result.code, headers: result.headers)
104
155
  when 403 then raise Descope::AccessDenied.new(result.body, code: result.code, headers: result.headers)
@@ -113,10 +164,10 @@ module Descope
113
164
 
114
165
  def call(method, url, timeout, headers, body = nil)
115
166
  RestClient::Request.execute(
116
- method:,
117
- url:,
118
- timeout:,
119
- headers:,
167
+ method: method,
168
+ url: url,
169
+ timeout: timeout,
170
+ headers: headers,
120
171
  payload: body
121
172
  )
122
173
  rescue RestClient::Exception => e
@@ -13,10 +13,13 @@ module Descope
13
13
  @base_uri = base_url(options)
14
14
  @headers = client_headers
15
15
  @project_id = options[:project_id] || ENV['DESCOPE_PROJECT_ID'] || ''
16
+ @headers['x-descope-project-id'] = @project_id
16
17
  @public_key = options[:public_key] || ENV['DESCOPE_PUBLIC_KEY']
17
18
  @mlock = Mutex.new
18
- log_level = options[:log_level] || ENV['DESCOPE_LOG_LEVEL'] || 'info'
19
- @logger ||= Descope::Mixins::Logging.logger_for(self.class.name, log_level)
19
+ @logger = options[:logger] || begin
20
+ log_level = options[:log_level] || ENV['DESCOPE_LOG_LEVEL'] || 'info'
21
+ Descope::Mixins::Logging.logger_for(self.class.name, log_level, @project_id)
22
+ end
20
23
 
21
24
  @logger.debug("Initializing Descope API with project_id: #{@project_id} and base_uri: #{@base_uri}")
22
25
 
@@ -7,21 +7,29 @@ module Descope
7
7
 
8
8
  def logger
9
9
  # This is the magical bit that gets mixed into the other modules
10
- @logger ||= Logging.logger_for(self.class.name, 'info')
10
+ @logger ||= Logging.logger_for(self.class.name, 'info', @project_id)
11
11
  end
12
12
 
13
13
  # Use a hash class-ivar to cache a unique Logger per class:
14
14
  @loggers = {}
15
15
 
16
16
  class << self
17
- def logger_for(classname, level)
18
- @loggers[classname] ||= configure_logger_for(classname, level)
17
+ def logger_for(classname, level, project_id = nil)
18
+ key = "#{classname}-#{project_id}"
19
+ @loggers[key] ||= configure_logger_for(classname, level, project_id)
19
20
  end
20
21
 
21
- def configure_logger_for(classname, level = 'info')
22
+ def configure_logger_for(classname, level = 'info', project_id = nil)
22
23
  logger = Logger.new(STDOUT)
23
24
  logger.level = Object.const_get("Logger::#{level.upcase}")
24
25
  logger.progname = classname
26
+
27
+ # Adding Custom Formatter for Project ID
28
+ logger.formatter = proc do |severity, datetime, progname, msg|
29
+ project_info = project_id ? "PRID: #{project_id}" : ""
30
+ "[#{datetime}] #{severity} #{project_info} #{progname}: #{msg}\n"
31
+ end
32
+
25
33
  logger
26
34
  end
27
35
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  # Current version of gem
4
4
  module Descope
5
- VERSION = '1.0.6'
5
+ VERSION = '1.1.0'
6
6
  SDK_VERSION = '1.0.0'
7
7
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Descope::Api::V1::Auth do
6
+ let(:client) { Class.new { include Descope::Api::V1::Auth }.new }
7
+ let(:valid_token) { 'valid_token' }
8
+ let(:refresh_token) { 'refresh_token' }
9
+
10
+ describe 'token extraction with empty strings' do
11
+ it 'handles empty string tokens correctly' do
12
+ response_body = {
13
+ 'sessionJwt' => '',
14
+ 'refreshJwt' => '',
15
+ 'cookies' => {
16
+ 'DS' => valid_token,
17
+ 'DSR' => refresh_token
18
+ }
19
+ }
20
+
21
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
22
+
23
+ result = client.generate_jwt_response(response_body: response_body)
24
+
25
+ expect(result).to have_key('sessionToken')
26
+ expect(result).to have_key('refreshSessionToken')
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Descope::Api::V1::Auth do
6
+ let(:client) { Descope::Client.new(project_id: 'test_project_id', management_key: 'test_key') }
7
+ let(:valid_session_token) { 'valid.session.token' }
8
+ let(:valid_refresh_token) { 'valid.refresh.token' }
9
+
10
+ describe '#generate_auth_info (private method)' do
11
+ context 'session token extraction' do
12
+ it 'extracts session token from sessionJwt field' do
13
+ response_body = {
14
+ 'sessionJwt' => valid_session_token,
15
+ 'refreshJwt' => valid_refresh_token,
16
+ 'cookies' => {}
17
+ }
18
+
19
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
20
+
21
+ result = client.send(:generate_auth_info, response_body, nil, true, nil)
22
+
23
+ expect(result).to have_key('sessionToken')
24
+ end
25
+
26
+ it 'extracts session token from cookies with DS name' do
27
+ response_body = {
28
+ 'sessionJwt' => '',
29
+ 'refreshJwt' => valid_refresh_token,
30
+ 'cookies' => {
31
+ 'DS' => valid_session_token
32
+ }
33
+ }
34
+
35
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
36
+
37
+ result = client.send(:generate_auth_info, response_body, nil, true, nil)
38
+
39
+ expect(result).to have_key('sessionToken')
40
+ end
41
+
42
+ it 'extracts session token from cookies with SESSION_COOKIE_NAME' do
43
+ stub_const('Descope::Api::V1::Auth::SESSION_COOKIE_NAME', 'CustomSession')
44
+
45
+ response_body = {
46
+ 'sessionJwt' => '',
47
+ 'refreshJwt' => valid_refresh_token,
48
+ 'cookies' => {
49
+ 'CustomSession' => valid_session_token
50
+ }
51
+ }
52
+
53
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
54
+
55
+ result = client.send(:generate_auth_info, response_body, nil, true, nil)
56
+
57
+ expect(result).to have_key('sessionToken')
58
+ end
59
+ end
60
+
61
+ context 'refresh token extraction' do
62
+ it 'extracts refresh token from refreshJwt field' do
63
+ response_body = {
64
+ 'sessionJwt' => valid_session_token,
65
+ 'refreshJwt' => valid_refresh_token,
66
+ 'cookies' => {}
67
+ }
68
+
69
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
70
+
71
+ result = client.send(:generate_auth_info, response_body, nil, true, nil)
72
+
73
+ expect(result).to have_key('refreshSessionToken')
74
+ end
75
+
76
+ it 'extracts refresh token from cookies when refreshJwt is empty' do
77
+ stub_const('Descope::Api::V1::Auth::REFRESH_SESSION_COOKIE_NAME', 'DSR')
78
+
79
+ response_body = {
80
+ 'sessionJwt' => valid_session_token,
81
+ 'refreshJwt' => '',
82
+ 'cookies' => {
83
+ 'DSR' => valid_refresh_token
84
+ }
85
+ }
86
+
87
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
88
+
89
+ result = client.send(:generate_auth_info, response_body, nil, true, nil)
90
+
91
+ expect(result).to have_key('refreshSessionToken')
92
+ end
93
+
94
+ it 'falls back to parameter refresh token when cookie has empty string' do
95
+ response_body = {
96
+ 'sessionJwt' => valid_session_token,
97
+ 'refreshJwt' => '',
98
+ 'cookies' => {
99
+ 'DSR' => ''
100
+ }
101
+ }
102
+
103
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
104
+
105
+ result = client.send(:generate_auth_info, response_body, valid_refresh_token, true, nil)
106
+
107
+ expect(result).to have_key('refreshSessionToken')
108
+ expect(client).to have_received(:validate_token).with(valid_refresh_token, nil)
109
+ end
110
+
111
+ it 'raises error when no refresh token is available' do
112
+ response_body = {
113
+ 'sessionJwt' => valid_session_token,
114
+ 'refreshJwt' => '',
115
+ 'cookies' => {}
116
+ }
117
+
118
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
119
+
120
+ expect {
121
+ client.send(:generate_auth_info, response_body, nil, true, nil)
122
+ }.to raise_error(Descope::AuthException, /Could not find refresh token/)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Descope::Api::V1::Session do
6
+ let(:client) { Descope::Client.new(project_id: 'test_project_id', management_key: 'test_key') }
7
+ let(:valid_token) { 'valid.jwt.token' }
8
+ let(:refresh_token) { 'refresh.jwt.token' }
9
+
10
+ describe '#refresh_session' do
11
+ context 'when refresh token is in cookie' do
12
+ it 'uses the cookie refresh token' do
13
+ response_body = {
14
+ 'sessionJwt' => '',
15
+ 'refreshJwt' => '',
16
+ 'cookieData' => {
17
+ 'DSR' => refresh_token
18
+ },
19
+ 'cookies' => {
20
+ 'DS' => valid_token
21
+ }
22
+ }
23
+
24
+ allow(client).to receive(:post).and_return(response_body)
25
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123' })
26
+
27
+ result = client.refresh_session(refresh_token: refresh_token)
28
+
29
+ expect(result).to be_a(Hash)
30
+ expect(client).to have_received(:validate_token).with(refresh_token, nil)
31
+ end
32
+ end
33
+
34
+ context 'when refresh token is in response body' do
35
+ it 'uses the refreshJwt from response body' do
36
+ response_body = {
37
+ 'sessionJwt' => '',
38
+ 'refreshJwt' => refresh_token,
39
+ 'cookieData' => {},
40
+ 'cookies' => {
41
+ 'DS' => valid_token
42
+ }
43
+ }
44
+
45
+ allow(client).to receive(:post).and_return(response_body)
46
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123' })
47
+
48
+ result = client.refresh_session(refresh_token: 'old_refresh_token')
49
+
50
+ expect(result).to be_a(Hash)
51
+ end
52
+ end
53
+
54
+ context 'when refresh token is only in parameter' do
55
+ it 'falls back to the parameter refresh token' do
56
+ response_body = {
57
+ 'sessionJwt' => '',
58
+ 'refreshJwt' => '',
59
+ 'cookieData' => {},
60
+ 'cookies' => {
61
+ 'DS' => valid_token
62
+ }
63
+ }
64
+
65
+ allow(client).to receive(:post).and_return(response_body)
66
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123' })
67
+
68
+ result = client.refresh_session(refresh_token: refresh_token)
69
+
70
+ expect(result).to be_a(Hash)
71
+ expect(client).to have_received(:validate_token).with(refresh_token, nil).at_least(:once)
72
+ end
73
+ end
74
+
75
+ context 'when refresh token values are empty strings' do
76
+ it 'falls back to parameter refresh token when cookie and body have empty strings' do
77
+ response_body = {
78
+ 'sessionJwt' => '',
79
+ 'refreshJwt' => '',
80
+ 'cookieData' => {
81
+ 'DSR' => ''
82
+ },
83
+ 'cookies' => {
84
+ 'DS' => valid_token
85
+ }
86
+ }
87
+
88
+ allow(client).to receive(:post).and_return(response_body)
89
+ allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123' })
90
+
91
+ result = client.refresh_session(refresh_token: refresh_token)
92
+
93
+ expect(result).to be_a(Hash)
94
+ expect(client).to have_received(:validate_token).with(refresh_token, nil).at_least(:once)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -10,7 +10,7 @@ FactoryBot.define do
10
10
  phone { "+1#{Faker::Number.number(digits: 10)}" }
11
11
  name { Faker::Name.name }
12
12
  given_name { Faker::Name.first_name }
13
- middle_name { 'Ruby SDK User' }
13
+ middle_name { "#{SpecUtils.build_prefix}Ruby-SDK-User" }
14
14
  family_name { Faker::Name.last_name }
15
15
  end
16
16
  end
@@ -3,31 +3,25 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  def poll_for_session(descope_client, pending_ref)
6
- max_tries = 15
7
- i = 0
8
- done = false
9
- while !done && i < max_tries
6
+ @client.logger.info('Waiting for session to be created...')
7
+
8
+ SpecUtils.wait_for_condition(max_wait: 60, interval: 3, description: 'enchanted link session') do
10
9
  begin
11
- i += 1
12
- @client.logger.info('waiting 4 seconds for session to be created...')
13
- sleep(4)
14
- print '.'
15
10
  @client.logger.info("Getting session for pending_ref: #{pending_ref}...")
16
11
  jwt_response = descope_client.enchanted_link_get_session(pending_ref)
17
- done = true
12
+
13
+ if jwt_response
14
+ @client.logger.info("jwt_response: #{jwt_response}")
15
+ refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME]['jwt']
16
+ @client.logger.info("refresh_token: #{refresh_token}")
17
+ return refresh_token
18
+ end
19
+
20
+ false
18
21
  rescue Descope::AuthException, Descope::Unauthorized => e
19
- @client.logger.info("Failed pending session, err: #{e}")
20
- nil
22
+ @client.logger.info("Waiting for session, err: #{e}")
23
+ false
21
24
  end
22
-
23
- next unless jwt_response
24
-
25
- @client.logger.info("jwt_response: #{jwt_response}")
26
- refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME]['jwt']
27
-
28
- @client.logger.info("refresh_token: #{refresh_token}")
29
- done = true
30
- return refresh_token
31
25
  end
32
26
  end
33
27
 
@@ -60,9 +54,13 @@ describe Descope::Api::V1::Auth::EnchantedLink do
60
54
  @client.logger.info('Cleaning up test users...')
61
55
  all_users = @client.search_all_users
62
56
  all_users['users'].each do |user|
63
- if user['middleName'] == 'Ruby SDK User'
57
+ if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
64
58
  @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
65
- @client.delete_user(user['loginIds'][0])
59
+ begin
60
+ @client.delete_user(user['loginIds'][0])
61
+ rescue Descope::NotFound => e
62
+ @client.logger.info("User already deleted: #{e.message}")
63
+ end
66
64
  end
67
65
  end
68
66
  end
@@ -11,9 +11,13 @@ describe Descope::Api::V1::Auth::MagicLink do
11
11
  @client.logger.info('Cleaning up test users...')
12
12
  all_users = @client.search_all_users
13
13
  all_users['users'].each do |user|
14
- if user['middleName'] == 'Ruby SDK User'
14
+ if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
15
15
  @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
16
- @client.delete_user(user['loginIds'][0])
16
+ begin
17
+ @client.delete_user(user['loginIds'][0])
18
+ rescue Descope::NotFound => e
19
+ @client.logger.info("User already deleted: #{e.message}")
20
+ end
17
21
  end
18
22
  end
19
23
  end
@@ -19,9 +19,13 @@ describe Descope::Api::V1::Auth::OTP do
19
19
  @client.logger.info('Cleaning up test users...')
20
20
  all_users = @client.search_all_users
21
21
  all_users['users'].each do |user|
22
- if user['middleName'] == 'Ruby SDK User'
22
+ if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
23
23
  @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
24
- @client.delete_user(user['loginIds'][0])
24
+ begin
25
+ @client.delete_user(user['loginIds'][0])
26
+ rescue Descope::NotFound => e
27
+ @client.logger.info("User already deleted: #{e.message}")
28
+ end
25
29
  end
26
30
  end
27
31
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Descope::Api::V1::Session do
6
+ before(:all) do
7
+ @client = DescopeClient.new(Configuration.config)
8
+
9
+ # Cleanup any existing test users before starting
10
+ @client.logger.info('Cleaning up any existing test users before starting...')
11
+ all_users = @client.search_all_users
12
+ all_users['users'].each do |user|
13
+ if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
14
+ @client.logger.info("Deleting existing ruby spec test user #{user['loginIds'][0]}")
15
+ begin
16
+ @client.delete_user(user['loginIds'][0])
17
+ rescue Descope::NotFound => e
18
+ @client.logger.info("User already deleted: #{e.message}")
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ after(:all) do
25
+ @client.logger.info('Cleaning up test users...')
26
+ all_users = @client.search_all_users
27
+ all_users['users'].each do |user|
28
+ if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
29
+ @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
30
+ begin
31
+ @client.delete_user(user['loginIds'][0])
32
+ rescue Descope::NotFound => e
33
+ @client.logger.info("User already deleted: #{e.message}")
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'test session methods' do
40
+ it 'should refresh session with refresh token' do
41
+ @password = SpecUtils.generate_password
42
+ # Add timestamp to ensure unique login_id across multiple test runs
43
+ user = build(:user, login_id: "session_test_#{Time.now.to_i}_#{SecureRandom.hex(4)}")
44
+
45
+ @client.logger.info("1. Sign up with password for user: #{user[:login_id]}")
46
+ res = @client.password_sign_up(login_id: user[:login_id], password: @password, user:)
47
+ @client.logger.info("sign up with password res: #{res}")
48
+ original_refresh_token = res[REFRESH_SESSION_TOKEN_NAME]['jwt']
49
+
50
+ @client.logger.info('2. Sign in with password')
51
+ login_res = @client.password_sign_in(login_id: user[:login_id], password: @password)
52
+ @client.logger.info("sign_in res: #{login_res}")
53
+
54
+ @client.logger.info('3. Wait briefly to ensure token timestamps differ')
55
+ sleep(2) # Wait 2 seconds to ensure new token will have different 'iat' timestamp
56
+
57
+ @client.logger.info('4. Refresh session')
58
+ refresh_session_res = @client.refresh_session(refresh_token: login_res[REFRESH_SESSION_TOKEN_NAME]['jwt'])
59
+ @client.logger.info("refresh_session_res: #{refresh_session_res}")
60
+
61
+ new_refresh_token = refresh_session_res[REFRESH_SESSION_TOKEN_NAME]['jwt']
62
+ @client.logger.info("new_refresh_token: #{new_refresh_token}")
63
+
64
+ @client.logger.info('5. Check new refresh token is not the same as the original one')
65
+ expect(original_refresh_token).not_to eq(new_refresh_token)
66
+ end
67
+ end
68
+ end
@@ -12,9 +12,13 @@ describe Descope::Api::V1::Auth::TOTP do
12
12
  @client.logger.info('Cleaning up test users...')
13
13
  all_users = @client.search_all_users
14
14
  all_users['users'].each do |user|
15
- if user['middleName'] == 'Ruby SDK User'
15
+ if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
16
16
  @client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
17
- @client.delete_user(user['loginIds'][0])
17
+ begin
18
+ @client.delete_user(user['loginIds'][0])
19
+ rescue Descope::NotFound => e
20
+ @client.logger.info("User already deleted: #{e.message}")
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -4,6 +4,8 @@ require 'spec_helper'
4
4
 
5
5
  describe Descope::Api::V1::Management::AccessKey do
6
6
  before(:all) do
7
+ raise 'DESCOPE_MANAGEMENT_KEY is not set' if ENV['DESCOPE_MANAGEMENT_KEY'].nil?
8
+
7
9
  @client = DescopeClient.new(Configuration.config)
8
10
  end
9
11
 
@@ -28,7 +30,16 @@ describe Descope::Api::V1::Management::AccessKey do
28
30
  @tenant_id = @client.create_tenant(name: 'some-new-tenant')['id']
29
31
  @client.logger.info('creating access key')
30
32
  @access_key = @client.create_access_key(name: @key_name, key_tenants: [{ tenant_id: @tenant_id }])
31
- sleep 60
33
+ @client.logger.info("waiting for access key #{@access_key['key']['id']} to be active")
34
+ SpecUtils.wait_for_condition(max_wait: 65, interval: 3, description: 'access key to be active') do
35
+ begin
36
+ key = @client.load_access_key(@access_key['key']['id'])
37
+ key['key']['status'] == 'active'
38
+ rescue StandardError => e
39
+ @client.logger.info("Waiting for key activation: #{e.message}")
40
+ false
41
+ end
42
+ end
32
43
  end
33
44
 
34
45
  it 'should create the access key and load it' do