panda-core 0.7.1 → 0.7.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80a35426761da785e847321996703aad6eeecb6262b2a781a3d1379bb0d438fd
4
- data.tar.gz: 6eac8f3b86fb4054c6c54083a22bea5d3c6471f4aca43f06e1ab78726c6aa870
3
+ metadata.gz: abf323ca5beed58dcb17cd7f39ed76028413524b20e40cd2de8d9c5d21b97fff
4
+ data.tar.gz: 992d91647d9cd2567b7d818beb2f7a2d173fb16edf8f78f222a576a1e6b9534f
5
5
  SHA512:
6
- metadata.gz: ee2a264cbd6d014680f7773fd56749331de84f30b12c34a6eb87132a3f7c99e8f2fd97c32c6126bea35ecedb3301d9763d4826bbfe44f5f34fa5d2e92cb1ee86
7
- data.tar.gz: 9df32b3390d72b00a4588bf1d0a88666fabdf9a519ed48712736316d66a6581467c1fb48fbacbd59244f7d319533110b9cec37807edc37984ce395f7b31cd5ba
6
+ metadata.gz: 42f8a466d633d31ee8e77650c0afcd9f768ef7990df74b185d3722e614a623f176944a610bee1ca4eee60f33edba812eda96b0b25f5e9466773c1f1920bd1213
7
+ data.tar.gz: a0f9ec858050df85d6a9cac0a04dc179673ad5438d8bd6c9deeb1ee0d52c5a1d0f5c2e0af9784cc717fcd59a987efd79f9ecc55d3d2456ba842d5056acff363b
@@ -29,10 +29,9 @@ Shoulda::Matchers.configure do |config|
29
29
  end
30
30
 
31
31
  # Load all support files from panda-core
32
- panda_core_support_path = Gem.loaded_specs["panda-core"]&.full_gem_path
33
- if panda_core_support_path
34
- Dir[File.join(panda_core_support_path, "spec/support/**/*.rb")].sort.each { |f| require f }
35
- end
32
+ # Files are now in lib/panda/core/testing/support/ to be included in the published gem
33
+ support_path = File.expand_path("../support", __FILE__)
34
+ Dir[File.join(support_path, "**/*.rb")].sort.each { |f| require f }
36
35
 
37
36
  RSpec.configure do |config|
38
37
  # Include panda-core route helpers by default
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module AuthenticationHelpers
6
+ # Create test users with fixed IDs for consistent fixture references
7
+ def create_admin_user
8
+ Panda::Core::User.find_or_create_by!(id: "8f481fcb-d9c8-55d7-ba17-5ea5d9ed8b7a") do |user|
9
+ user.email = "admin@test.example.com"
10
+ user.firstname = "Admin"
11
+ user.lastname = "User"
12
+ user.admin = true
13
+ end
14
+ end
15
+
16
+ def create_regular_user
17
+ Panda::Core::User.find_or_create_by!(id: "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d") do |user|
18
+ user.email = "user@test.example.com"
19
+ user.firstname = "Regular"
20
+ user.lastname = "User"
21
+ user.admin = false
22
+ end
23
+ end
24
+
25
+ # For request specs - set session directly
26
+ def sign_in_as(user)
27
+ session[:user_id] = user.id
28
+ Panda::Core::Current.user = user
29
+ end
30
+
31
+ # For system specs - use test session endpoint if available
32
+ def login_as_admin
33
+ admin_user = create_admin_user
34
+ if defined?(Panda::CMS)
35
+ # CMS provides test session endpoint
36
+ post "/admin/test_sessions", params: {user_id: admin_user.id}
37
+ else
38
+ # Fall back to direct session setting
39
+ sign_in_as(admin_user)
40
+ end
41
+ end
42
+
43
+ def login_as_regular_user
44
+ regular_user = create_regular_user
45
+ if defined?(Panda::CMS)
46
+ post "/admin/test_sessions", params: {user_id: regular_user.id}
47
+ else
48
+ sign_in_as(regular_user)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ RSpec.configure do |config|
56
+ # Include authentication helpers for all spec types (model, request, system, component, etc.)
57
+ config.include Panda::Core::AuthenticationHelpers
58
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Comprehensive authentication test helpers for Panda Core and consuming gems
4
+ # This module provides helpers for:
5
+ # - Creating test users with fixed IDs
6
+ # - OAuth mocking and configuration
7
+ # - Test session management
8
+ # - Request and system test authentication
9
+
10
+ module Panda
11
+ module Core
12
+ module AuthenticationTestHelpers
13
+ # ============================================================================
14
+ # USER CREATION HELPERS
15
+ # ============================================================================
16
+
17
+ # Create an admin user with fixed ID for consistent fixture references
18
+ def create_admin_user(attributes = {})
19
+ ensure_columns_loaded
20
+ admin_id = "8f481fcb-d9c8-55d7-ba17-5ea5d9ed8b7a"
21
+ Panda::Core::User.find_or_create_by!(id: admin_id) do |user|
22
+ user.email = attributes[:email] || "admin@example.com"
23
+ user.firstname = attributes[:firstname] || "Admin" if user.respond_to?(:firstname=)
24
+ user.lastname = attributes[:lastname] || "User" if user.respond_to?(:lastname=)
25
+ user.name = attributes[:name] || "Admin User" if user.respond_to?(:name=) && !user.respond_to?(:firstname=)
26
+ user.image_url = attributes[:image_url] || default_image_url if user.respond_to?(:image_url=)
27
+ # Use is_admin for the actual column, but support both for compatibility
28
+ if user.respond_to?(:is_admin=)
29
+ user.is_admin = attributes.fetch(:admin, true)
30
+ elsif user.respond_to?(:admin=)
31
+ user.admin = attributes.fetch(:admin, true)
32
+ end
33
+ # Only set OAuth fields if they exist on the model
34
+ user.uid = attributes[:uid] || "admin_oauth_uid_123" if user.respond_to?(:uid=)
35
+ user.provider = attributes[:provider] || "google_oauth2" if user.respond_to?(:provider=)
36
+ end
37
+ end
38
+
39
+ # Create a regular user with fixed ID for consistent fixture references
40
+ def create_regular_user(attributes = {})
41
+ ensure_columns_loaded
42
+ regular_id = "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d"
43
+ Panda::Core::User.find_or_create_by!(id: regular_id) do |user|
44
+ user.email = attributes[:email] || "user@example.com"
45
+ user.firstname = attributes[:firstname] || "Regular" if user.respond_to?(:firstname=)
46
+ user.lastname = attributes[:lastname] || "User" if user.respond_to?(:lastname=)
47
+ user.name = attributes[:name] || "Regular User" if user.respond_to?(:name=) && !user.respond_to?(:firstname=)
48
+ user.image_url = attributes[:image_url] || default_image_url(dark: true) if user.respond_to?(:image_url=)
49
+ # Use is_admin for the actual column, but support both for compatibility
50
+ if user.respond_to?(:is_admin=)
51
+ user.is_admin = attributes.fetch(:admin, false)
52
+ elsif user.respond_to?(:admin=)
53
+ user.admin = attributes.fetch(:admin, false)
54
+ end
55
+ # Only set OAuth fields if they exist on the model
56
+ user.uid = attributes[:uid] || "user_oauth_uid_456" if user.respond_to?(:uid=)
57
+ user.provider = attributes[:provider] || "google_oauth2" if user.respond_to?(:provider=)
58
+ end
59
+ end
60
+
61
+ # Backwards compatibility with fixture access patterns
62
+ def admin_user
63
+ ensure_columns_loaded
64
+ @admin_user ||= Panda::Core::User.find_by(email: "admin@example.com") || create_admin_user
65
+ end
66
+
67
+ def regular_user
68
+ ensure_columns_loaded
69
+ @regular_user ||= Panda::Core::User.find_by(email: "user@example.com") || create_regular_user
70
+ end
71
+
72
+ # ============================================================================
73
+ # OMNIAUTH HELPERS
74
+ # ============================================================================
75
+
76
+ def clear_omniauth_config
77
+ OmniAuth.config.mock_auth.clear
78
+ Rails.application.env_config.delete("omniauth.auth") if defined?(Rails.application)
79
+ end
80
+
81
+ def mock_oauth_for_user(user, provider: :google_oauth2)
82
+ clear_omniauth_config
83
+ OmniAuth.config.test_mode = true
84
+ OmniAuth.config.mock_auth[provider] = OmniAuth::AuthHash.new({
85
+ provider: provider.to_s,
86
+ uid: (user.respond_to?(:uid) ? user.uid : nil) || user.id,
87
+ info: {
88
+ email: user.email,
89
+ name: user.name,
90
+ image: user.respond_to?(:image_url) ? user.image_url : nil
91
+ },
92
+ credentials: {
93
+ token: "mock_token_#{user.id}",
94
+ expires_at: Time.now + 1.week
95
+ }
96
+ })
97
+ end
98
+
99
+ # ============================================================================
100
+ # REQUEST SPEC HELPERS (Direct Session Manipulation)
101
+ # ============================================================================
102
+
103
+ # For request specs - set session directly (fast, no HTTP requests)
104
+ def sign_in_as(user)
105
+ # This works in request specs where we have direct access to the session
106
+ begin
107
+ if respond_to?(:session)
108
+ session[:user_id] = user.id
109
+ end
110
+ rescue NoMethodError
111
+ # Session method doesn't exist in this context (e.g., system specs)
112
+ end
113
+ Panda::Core::Current.user = user
114
+ user
115
+ end
116
+
117
+ # ============================================================================
118
+ # SYSTEM SPEC HELPERS (HTTP-based Authentication)
119
+ # ============================================================================
120
+
121
+ # For system specs - use test session endpoint (works across processes)
122
+ # This uses the TestSessionsController which is only available in test environment
123
+ #
124
+ # NOTE: Due to Cuprite's redirect handling, we visit the target path directly
125
+ # after setting up the session via the test endpoint. Flash messages won't be
126
+ # available in system tests due to cross-process timing. Use request specs
127
+ # to test flash messages (see authentication_request_spec.rb).
128
+ def login_with_test_endpoint(user, return_to: nil, expect_success: true)
129
+ return_path = return_to || determine_default_redirect_path
130
+
131
+ # Visit the test login endpoint (sets session via Redis)
132
+ # Note: Capybara/Cuprite may not follow the redirect properly, so we
133
+ # manually navigate to the expected destination
134
+ visit "/admin/test_login/#{user.id}?return_to=#{return_path}"
135
+
136
+ # Wait briefly for session to be set
137
+ sleep 0.2
138
+
139
+ # Manually visit the destination since Cuprite doesn't reliably follow redirects
140
+ if expect_success
141
+ visit return_path
142
+ # Wait for page to load
143
+ sleep 0.2
144
+
145
+ # Verify we're on the expected path
146
+ expect(page).to have_current_path(return_path, wait: 2)
147
+ end
148
+ end
149
+
150
+ # Convenience method: Login with Google OAuth provider (using test endpoint)
151
+ def login_with_google(user, expect_success: true)
152
+ login_with_test_endpoint(user, return_to: determine_default_redirect_path, expect_success: expect_success)
153
+ end
154
+
155
+ # Convenience method: Login with GitHub OAuth provider (using test endpoint)
156
+ def login_with_github(user, expect_success: true)
157
+ login_with_test_endpoint(user, return_to: determine_default_redirect_path, expect_success: expect_success)
158
+ end
159
+
160
+ # Convenience method: Login with Microsoft OAuth provider (using test endpoint)
161
+ def login_with_microsoft(user, expect_success: true)
162
+ login_with_test_endpoint(user, return_to: determine_default_redirect_path, expect_success: expect_success)
163
+ end
164
+
165
+ # High-level helper: Login as admin (creates user if needed)
166
+ def login_as_admin(attributes = {})
167
+ user = create_admin_user(attributes)
168
+ if respond_to?(:visit)
169
+ # System spec - use test endpoint
170
+ login_with_test_endpoint(user, expect_success: true)
171
+ else
172
+ # Request spec - use direct session
173
+ sign_in_as(user)
174
+ end
175
+ user
176
+ end
177
+
178
+ # High-level helper: Login as regular user (creates user if needed)
179
+ def login_as_user(attributes = {})
180
+ user = create_regular_user(attributes)
181
+ if respond_to?(:visit)
182
+ # System spec - regular users get redirected to login
183
+ login_with_test_endpoint(user, expect_success: false)
184
+ else
185
+ # Request spec - use direct session
186
+ sign_in_as(user)
187
+ end
188
+ user
189
+ end
190
+
191
+ # Manual OAuth login (slower, but tests actual OAuth flow)
192
+ # Use this when you need to test the OAuth callback handler itself
193
+ def manual_login_with_oauth(user, provider: :google_oauth2)
194
+ mock_oauth_for_user(user, provider: provider)
195
+
196
+ visit admin_login_path
197
+ expect(page).to have_css("#button-sign-in-#{provider}")
198
+ find("#button-sign-in-#{provider}").click
199
+
200
+ Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[provider]
201
+ end
202
+
203
+ # ============================================================================
204
+ # PRIVATE HELPER METHODS
205
+ # ============================================================================
206
+
207
+ private
208
+
209
+ def ensure_columns_loaded
210
+ return if @columns_loaded
211
+ Panda::Core::User.connection.schema_cache.clear!
212
+ Panda::Core::User.reset_column_information
213
+ @columns_loaded = true
214
+ end
215
+
216
+ def default_image_url(dark: false)
217
+ color = dark ? "%23999" : "%23ccc"
218
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='#{color}'/%3E%3C/svg%3E"
219
+ end
220
+
221
+ def determine_default_redirect_path
222
+ # Check if we're in a CMS context
223
+ if defined?(Panda::CMS)
224
+ "/admin/cms"
225
+ else
226
+ "/admin"
227
+ end
228
+ end
229
+
230
+ def admin_login_path
231
+ if defined?(panda_core)
232
+ panda_core.admin_login_path
233
+ else
234
+ "/admin/login"
235
+ end
236
+ end
237
+
238
+ def admin_root_path
239
+ if defined?(panda_core)
240
+ panda_core.admin_root_path
241
+ else
242
+ "/admin"
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ # Configure RSpec to include these helpers in appropriate test types
250
+ RSpec.configure do |config|
251
+ config.include Panda::Core::AuthenticationTestHelpers, type: :request
252
+ config.include Panda::Core::AuthenticationTestHelpers, type: :system
253
+ config.include Panda::Core::AuthenticationTestHelpers, type: :controller
254
+ end
255
+
256
+ # Configure OmniAuth for testing
257
+ OmniAuth.config.test_mode = true
258
+ OmniAuth.config.on_failure = proc { |env|
259
+ OmniAuth::FailureEndpoint.new(env).redirect_to_failure
260
+ }
@@ -0,0 +1,97 @@
1
+ require "rails/generators"
2
+
3
+ module GeneratorSpecHelper
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before(:each) do
8
+ prepare_destination
9
+ @original_stdout = $stdout
10
+ $stdout = File.new(File::NULL, "w")
11
+ end
12
+
13
+ after(:each) do
14
+ FileUtils.rm_rf(destination_root)
15
+ $stdout = @original_stdout
16
+ end
17
+ end
18
+
19
+ def destination_root
20
+ @destination_root ||= File.expand_path("../../tmp/generators", __dir__)
21
+ end
22
+
23
+ def prepare_destination
24
+ FileUtils.rm_rf(destination_root)
25
+ FileUtils.mkdir_p(destination_root)
26
+ end
27
+
28
+ def run_generator(args = [])
29
+ args = Array(args)
30
+ # Use the generator namespace instead of class name
31
+ generator_name = described_class.namespace
32
+ Rails::Generators.invoke(generator_name, args, destination_root: destination_root)
33
+ end
34
+
35
+ def generator
36
+ @generator ||= described_class.new([], destination_root: destination_root)
37
+ end
38
+
39
+ def file_exists?(path)
40
+ File.exist?(File.join(destination_root, path))
41
+ end
42
+
43
+ def read_file(path)
44
+ File.read(File.join(destination_root, path))
45
+ end
46
+
47
+ private
48
+
49
+ def capture(stream)
50
+ stream = stream.to_s
51
+ captured_stream = StringIO.new
52
+
53
+ # Map stream names to their global variables to avoid eval
54
+ streams = {
55
+ "stdout" => $stdout,
56
+ "stderr" => $stderr,
57
+ "stdin" => $stdin
58
+ }
59
+
60
+ original_stream = streams[stream]
61
+ case stream
62
+ when "stdout"
63
+ $stdout = captured_stream
64
+ when "stderr"
65
+ $stderr = captured_stream
66
+ when "stdin"
67
+ $stdin = captured_stream
68
+ else
69
+ raise ArgumentError, "Unsupported stream: #{stream}"
70
+ end
71
+
72
+ yield
73
+ captured_stream.string
74
+ ensure
75
+ case stream
76
+ when "stdout"
77
+ $stdout = original_stream
78
+ when "stderr"
79
+ $stderr = original_stream
80
+ when "stdin"
81
+ $stdin = original_stream
82
+ end
83
+ end
84
+ end
85
+
86
+ RSpec.configure do |config|
87
+ config.include GeneratorSpecHelper, type: :generator
88
+
89
+ # Ensure generator tests have a clean environment
90
+ config.before(:each, type: :generator) do
91
+ prepare_destination
92
+ end
93
+
94
+ config.after(:each, type: :generator) do
95
+ FileUtils.rm_rf(destination_root) if defined?(destination_root)
96
+ end
97
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HtmlHelpers
4
+ def normalize_html(html)
5
+ html.gsub(/\s+/, " ").strip
6
+ end
7
+ end
8
+
9
+ RSpec.configure do |config|
10
+ config.include HtmlHelpers
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure authentication providers for tests
4
+ RSpec.configure do |config|
5
+ config.before(:each, type: :controller) do
6
+ # Set up test authentication providers
7
+ Panda::Core.configure do |core_config|
8
+ core_config.authentication_providers = {
9
+ google_oauth2: {
10
+ client_id: "test_client_id",
11
+ client_secret: "test_client_secret",
12
+ options: {}
13
+ }
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require "base64"
5
+
6
+ # Stub services that make external HTTP requests to prevent them in tests
7
+ RSpec.configure do |config|
8
+ config.before(:each) do
9
+ # Stub URI.open to prevent external HTTP requests when downloading avatars
10
+ # This affects AttachAvatarService which downloads avatars from OAuth providers
11
+ # Tests that specifically need real HTTP requests can override this stub
12
+ allow(URI).to receive(:open).and_wrap_original do |original_method, *args, **kwargs, &block|
13
+ url = args[0]
14
+
15
+ # Only stub avatar downloads from OAuth providers and test URLs
16
+ if /googleusercontent\.com|githubusercontent\.com|graph\.microsoft\.com|example\.com/.match?(url.to_s)
17
+ # Use the actual test fixture file instead of creating a fake file
18
+ # This ensures compatibility with Active Storage
19
+ fixture_path = Panda::Core::Engine.root.join("spec", "fixtures", "files", "test_image.jpg")
20
+ downloaded_file = File.open(fixture_path, "rb")
21
+
22
+ # Add methods that AttachAvatarService expects
23
+ downloaded_file.define_singleton_method(:content_type) { "image/jpeg" }
24
+ downloaded_file.define_singleton_method(:size) { File.size(fixture_path) }
25
+
26
+ # Call the block with our file if a block is given
27
+ if block_given?
28
+ result = block.call(downloaded_file)
29
+ downloaded_file.close unless downloaded_file.closed?
30
+ result
31
+ else
32
+ downloaded_file
33
+ end
34
+ else
35
+ # For other URLs, use the original method
36
+ original_method.call(*args, **kwargs, &block)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ def pause
4
+ $stderr.write "Press enter to continue"
5
+ $stdin.gets
6
+ end
7
+
8
+ def debugit
9
+ # Cuprite-specific debugging method
10
+ page.driver.debug
11
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Panda
4
4
  module Core
5
- VERSION = "0.7.1"
5
+ VERSION = "0.7.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Otaina Limited
@@ -471,6 +471,13 @@ files:
471
471
  - lib/panda/core/sluggable.rb
472
472
  - lib/panda/core/subscribers/authentication_subscriber.rb
473
473
  - lib/panda/core/testing/rails_helper.rb
474
+ - lib/panda/core/testing/support/authentication_helpers.rb
475
+ - lib/panda/core/testing/support/authentication_test_helpers.rb
476
+ - lib/panda/core/testing/support/generator_spec_helper.rb
477
+ - lib/panda/core/testing/support/html_helpers.rb
478
+ - lib/panda/core/testing/support/omniauth_setup.rb
479
+ - lib/panda/core/testing/support/service_stubs.rb
480
+ - lib/panda/core/testing/support/setup.rb
474
481
  - lib/panda/core/version.rb
475
482
  - lib/tasks/assets.rake
476
483
  - lib/tasks/panda/core/migrations.rake