panda-core 0.7.0 → 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: 4aa145ee85e5b0c497754d7d218ceb3cc7c4de6065e3357f85e0676186df37f1
4
- data.tar.gz: 9425c978a3208aca75045e4cf1e36408088829a73c19d10746a91cefc320db6f
3
+ metadata.gz: abf323ca5beed58dcb17cd7f39ed76028413524b20e40cd2de8d9c5d21b97fff
4
+ data.tar.gz: 992d91647d9cd2567b7d818beb2f7a2d173fb16edf8f78f222a576a1e6b9534f
5
5
  SHA512:
6
- metadata.gz: ef76d583b1a36ecef8d13788338a847457bcd36a8efddf9582dcb1a7fc7ee484cb689f5dea6f9628d1fc46e2e7e2f513c84b6531116059496bb5e2e862d82f32
7
- data.tar.gz: 982d52f5927bddf30b2c6ab1d3981e78e542fba07717628d73f4375c8819ebf29a151eaf08f0f89fea48d06830c52315d3ff9a996d19472696c061597d529bd4
6
+ metadata.gz: 42f8a466d633d31ee8e77650c0afcd9f768ef7990df74b185d3722e614a623f176944a610bee1ca4eee60f33edba812eda96b0b25f5e9466773c1f1920bd1213
7
+ data.tar.gz: a0f9ec858050df85d6a9cac0a04dc179673ad5438d8bd6c9deeb1ee0d52c5a1d0f5c2e0af9784cc717fcd59a987efd79f9ecc55d3d2456ba842d5056acff363b
@@ -19,10 +19,14 @@ export default class extends Controller {
19
19
  static targets = ["button", "menu", "icon"]
20
20
 
21
21
  connect() {
22
+ // Ensure menu starts in correct state
22
23
  // Check if this menu should be expanded by default (if a child is active)
23
24
  const hasActiveChild = this.menuTarget.querySelector(".bg-mid")
24
25
  if (hasActiveChild) {
25
26
  this.expand()
27
+ } else {
28
+ // Explicitly ensure the menu is collapsed if no active child
29
+ this.collapse()
26
30
  }
27
31
  }
28
32
 
@@ -42,6 +46,7 @@ export default class extends Controller {
42
46
 
43
47
  expand() {
44
48
  this.menuTarget.classList.remove("hidden")
49
+ this.menuTarget.style.display = ""
45
50
  this.buttonTarget.setAttribute("aria-expanded", "true")
46
51
 
47
52
  if (this.hasIconTarget) {
@@ -51,6 +56,7 @@ export default class extends Controller {
51
56
 
52
57
  collapse() {
53
58
  this.menuTarget.classList.add("hidden")
59
+ this.menuTarget.style.display = "none"
54
60
  this.buttonTarget.setAttribute("aria-expanded", "false")
55
61
 
56
62
  if (this.hasIconTarget) {
@@ -50,6 +50,7 @@
50
50
  </button>
51
51
  <div id="sub-menu-<%= index %>"
52
52
  class="space-y-1 hidden"
53
+ style="display: none;"
53
54
  data-navigation-toggle-target="menu">
54
55
  <% item[:children].each do |child| %>
55
56
  <%
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared RSpec configuration for all Panda gems
4
+ # This file provides common test infrastructure that can be extended by individual gems
5
+ #
6
+ # IMPORTANT: This file should be required AFTER the dummy app is loaded
7
+ # Individual gem rails_helper files should:
8
+ # 1. Set up SimpleCov
9
+ # 2. Require panda/core and the dummy app
10
+ # 3. Require this file
11
+ # 4. Load gem-specific support files
12
+
13
+ # Require common test gems (assumes Rails and RSpec/Rails are already loaded by gem's rails_helper)
14
+ require "database_cleaner/active_record"
15
+ require "shoulda/matchers"
16
+ require "capybara"
17
+ require "capybara/rspec"
18
+ require "puma"
19
+
20
+ # Configure fixtures
21
+ ActiveRecord::FixtureSet.context_class.send :include, ActiveSupport::Testing::TimeHelpers
22
+
23
+ # Shoulda Matchers configuration
24
+ Shoulda::Matchers.configure do |config|
25
+ config.integrate do |with|
26
+ with.test_framework :rspec
27
+ with.library :rails
28
+ end
29
+ end
30
+
31
+ # Load all support files from panda-core
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 }
35
+
36
+ RSpec.configure do |config|
37
+ # Include panda-core route helpers by default
38
+ config.include Panda::Core::Engine.routes.url_helpers if defined?(Panda::Core::Engine)
39
+ config.include Rails.application.routes.url_helpers
40
+
41
+ # Standard RSpec configuration
42
+ config.use_transactional_fixtures = true
43
+ config.infer_spec_type_from_file_location!
44
+ config.filter_rails_from_backtrace!
45
+ config.example_status_persistence_file_path = "spec/examples.txt"
46
+ config.disable_monkey_patching!
47
+
48
+ # Print total example count before running
49
+ config.before(:suite) do
50
+ puts "Total examples to run: #{RSpec.world.example_count}\n"
51
+ end
52
+
53
+ # Use specific formatter for GitHub Actions
54
+ if ENV["GITHUB_ACTIONS"] == "true"
55
+ require "rspec/github"
56
+ config.add_formatter RSpec::Github::Formatter
57
+ end
58
+
59
+ # Controller testing support (if rails-controller-testing is available)
60
+ if defined?(Rails::Controller::Testing)
61
+ config.include Rails::Controller::Testing::TestProcess, type: :controller
62
+ config.include Rails::Controller::Testing::TemplateAssertions, type: :controller
63
+ config.include Rails::Controller::Testing::Integration, type: :controller
64
+ end
65
+
66
+ # Reset column information before suite
67
+ config.before(:suite) do
68
+ if defined?(Panda::Core::User)
69
+ Panda::Core::User.connection.schema_cache.clear!
70
+ Panda::Core::User.reset_column_information
71
+ end
72
+ end
73
+
74
+ # Disable prepared statements for PostgreSQL tests to avoid caching issues
75
+ config.before(:suite) do
76
+ if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
77
+ ActiveRecord::Base.connection.unprepared_statement do
78
+ # This block intentionally left empty
79
+ end
80
+ # Disable prepared statements globally for test environment
81
+ ActiveRecord::Base.establish_connection(
82
+ ActiveRecord::Base.connection_db_config.configuration_hash.merge(prepared_statements: false)
83
+ )
84
+ end
85
+ end
86
+
87
+ # Disable logging during tests
88
+ config.before(:suite) do
89
+ Rails.logger.level = Logger::ERROR
90
+ ActiveRecord::Base.logger = nil if defined?(ActiveRecord)
91
+ ActionController::Base.logger = nil if defined?(ActionController)
92
+ ActionMailer::Base.logger = nil if defined?(ActionMailer)
93
+ end
94
+
95
+ # Suppress Rails command output during generator tests
96
+ config.before(:each, type: :generator) do
97
+ allow(Rails::Command).to receive(:invoke).and_return(true)
98
+ end
99
+
100
+ # Force all examples to run
101
+ config.filter_run_including({})
102
+ config.run_all_when_everything_filtered = true
103
+
104
+ # Allow using focus keywords "f... before a specific test"
105
+ config.filter_run_when_matching :focus
106
+
107
+ # Retry flaky tests automatically
108
+ # This is especially useful for system tests that may have timing issues
109
+ config.around(:each, :flaky) do |example|
110
+ retry_count = example.metadata[:retry] || 3
111
+ retry_count.times do |i|
112
+ example.run
113
+ break unless example.exception
114
+
115
+ if i < retry_count - 1
116
+ puts "\n[RETRY] Test failed, retrying... (attempt #{i + 2}/#{retry_count})"
117
+ puts "[RETRY] Exception: #{example.exception.class.name}: #{example.exception.message[0..100]}"
118
+ example.instance_variable_set(:@exception, nil)
119
+ sleep 1 # Brief pause between retries
120
+ end
121
+ end
122
+ end
123
+
124
+ # Exclude EditorJS tests by default unless specifically requested
125
+ config.filter_run_excluding :editorjs unless ENV["INCLUDE_EDITORJS"] == "true"
126
+
127
+ # Use verbose output if only running one spec file
128
+ config.default_formatter = "doc" if config.files_to_run.one?
129
+
130
+ # Print the 10 slowest examples and example groups at the
131
+ # end of the spec run, to help surface which specs are running
132
+ # particularly slow.
133
+ config.profile_examples = 10
134
+
135
+ # Run specs in random order to surface order dependencies. If you find an
136
+ # order dependency and want to debug it, you can fix the order by providing
137
+ # the seed, which is printed after each run: --seed 1234
138
+ Kernel.srand config.seed
139
+ config.order = :random
140
+
141
+ # Note: Fixture configuration is gem-specific and should be done in each gem's rails_helper.rb
142
+ # This includes config.fixture_paths and config.global_fixtures
143
+
144
+ # Bullet configuration (if available)
145
+ if defined?(Bullet) && Bullet.enable?
146
+ config.before(:each) do
147
+ Bullet.start_request
148
+ end
149
+
150
+ config.after(:each) do
151
+ Bullet.perform_out_of_channel_notifications if Bullet.notification?
152
+ Bullet.end_request
153
+ end
154
+ end
155
+
156
+ # OmniAuth test mode
157
+ OmniAuth.config.test_mode = true if defined?(OmniAuth)
158
+
159
+ # DatabaseCleaner configuration
160
+ config.before(:suite) do
161
+ # Allow DATABASE_URL in CI environment
162
+ if ENV["DATABASE_URL"]
163
+ DatabaseCleaner.allow_remote_database_url = true
164
+ end
165
+
166
+ DatabaseCleaner.clean_with :truncation
167
+
168
+ # Hook for gems to add custom suite setup
169
+ # Gems can define Panda::Testing.before_suite_hook and it will be called here
170
+ if defined?(Panda::Testing) && Panda::Testing.respond_to?(:before_suite_hook)
171
+ Panda::Testing.before_suite_hook
172
+ end
173
+ end
174
+ end
@@ -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.0"
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.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Otaina Limited
@@ -470,6 +470,14 @@ files:
470
470
  - lib/panda/core/services/base_service.rb
471
471
  - lib/panda/core/sluggable.rb
472
472
  - lib/panda/core/subscribers/authentication_subscriber.rb
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
473
481
  - lib/panda/core/version.rb
474
482
  - lib/tasks/assets.rake
475
483
  - lib/tasks/panda/core/migrations.rake