panda-core 0.7.1 → 0.7.3

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: 2d1c4f83377c0b031d5a68ef550668e6ce9d0e09f9d0865da9ab7a5b7349d981
4
+ data.tar.gz: c82ab418857a1b1961270f417f69ae9e236e1bb36fb3612b7c6e7001ac440cc9
5
5
  SHA512:
6
- metadata.gz: ee2a264cbd6d014680f7773fd56749331de84f30b12c34a6eb87132a3f7c99e8f2fd97c32c6126bea35ecedb3301d9763d4826bbfe44f5f34fa5d2e92cb1ee86
7
- data.tar.gz: 9df32b3390d72b00a4588bf1d0a88666fabdf9a519ed48712736316d66a6581467c1fb48fbacbd59244f7d319533110b9cec37807edc37984ce395f7b31cd5ba
6
+ metadata.gz: 3ab5424862f54e04222d4a8909487e0179e5a73ba1852cce491bd0767cfd26d177d02e720317c610d610275bd895f757cec61d337708a211fd12240db6d3fe47
7
+ data.tar.gz: d24d95c56a5ceac68c0713e51cb029a63b27f88476bf1d93007ae6822a472ea456d919e833c0f26817489dee98af8407a3d6f958b8f2d73846a8b5a064e51dd1
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # AdminController alias configuration
7
+ module AdminControllerConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # Create AdminController alias after controllers are loaded
12
+ # This allows other gems to inherit from Panda::Core::AdminController
13
+ initializer "panda_core.admin_controller_alias", after: :load_config_initializers do
14
+ ActiveSupport.on_load(:action_controller_base) do
15
+ Panda::Core.const_set(:AdminController, Panda::Core::Admin::BaseController) unless Panda::Core.const_defined?(:AdminController)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # Autoload paths configuration
7
+ module AutoloadConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ config.eager_load_namespaces << Panda::Core::Engine
12
+
13
+ # Add engine's app directories to autoload paths
14
+ # Note: Only add the root directories, not nested subdirectories
15
+ # Zeitwerk will automatically discover nested modules from these roots
16
+ config.autoload_paths += Dir[root.join("app", "models")]
17
+ config.autoload_paths += Dir[root.join("app", "controllers")]
18
+ config.autoload_paths += Dir[root.join("app", "builders")]
19
+ config.autoload_paths += Dir[root.join("app", "components")]
20
+ config.autoload_paths += Dir[root.join("app", "services")]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # Generator configuration
7
+ module GeneratorConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ config.generators do |g|
12
+ g.test_framework :rspec
13
+ g.fixture_replacement :factory_bot
14
+ g.factory_bot dir: "spec/factories"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # Importmap configuration
7
+ module ImportmapConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # Add importmap paths from the engine
12
+ initializer "panda_core.importmap", before: "importmap" do |app|
13
+ if app.config.respond_to?(:importmap)
14
+ # Create a new array if frozen
15
+ app.config.importmap.paths = app.config.importmap.paths.dup if app.config.importmap.paths.frozen?
16
+
17
+ # Add our paths
18
+ app.config.importmap.paths << root.join("config/importmap.rb")
19
+
20
+ # Handle cache sweepers similarly
21
+ if app.config.importmap.cache_sweepers.frozen?
22
+ app.config.importmap.cache_sweepers = app.config.importmap.cache_sweepers.dup
23
+ end
24
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # Inflections configuration
7
+ module InflectionsConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # Load inflections early to ensure proper constant resolution
12
+ initializer "panda_core.inflections", before: :load_config_initializers do
13
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
14
+ inflect.acronym "CMS"
15
+ inflect.acronym "SEO"
16
+ inflect.acronym "AI"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # Middleware configuration for static assets
7
+ module MiddlewareConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # Make files in public available to the main app (e.g. /panda-core-assets/panda-logo.png)
12
+ config.middleware.use Rack::Static,
13
+ urls: ["/panda-core-assets"],
14
+ root: Panda::Core::Engine.root.join("public"),
15
+ header_rules: [
16
+ # Disable caching in development for instant CSS updates
17
+ [:all, {"Cache-Control" => Rails.env.development? ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000"}]
18
+ ]
19
+
20
+ # Make JavaScript files available for importmap
21
+ # Serve from app/javascript with proper MIME types
22
+ config.middleware.use Rack::Static,
23
+ urls: ["/panda", "/panda/core"],
24
+ root: Panda::Core::Engine.root.join("app/javascript"),
25
+ header_rules: [
26
+ [:all, {"Cache-Control" => Rails.env.development? ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000",
27
+ "Content-Type" => "text/javascript; charset=utf-8"}]
28
+ ]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # OmniAuth configuration
7
+ module OmniauthConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ initializer "panda_core.omniauth" do |app|
12
+ # Load OAuth provider gems
13
+ require_relative "../oauth_providers"
14
+ Panda::Core::OAuthProviders.setup
15
+
16
+ # Mount OmniAuth at configurable admin path
17
+ app.middleware.use OmniAuth::Builder do
18
+ # Configure OmniAuth to use the configured admin path
19
+ configure do |config|
20
+ config.path_prefix = "#{Panda::Core.config.admin_path}/auth"
21
+ # POST-only for CSRF protection (CVE-2015-9284)
22
+ # All login forms use POST via form_tag method: "post"
23
+ config.allowed_request_methods = [:post]
24
+ end
25
+
26
+ Panda::Core.config.authentication_providers.each do |provider_name, settings|
27
+ # Build provider options, allowing custom path name override
28
+ provider_options = settings[:options] || {}
29
+
30
+ # If path_name is specified, use it to override the default strategy name in URLs
31
+ if settings[:path_name].present?
32
+ provider_options = provider_options.merge(name: settings[:path_name])
33
+ end
34
+
35
+ case provider_name.to_s
36
+ when "microsoft_graph"
37
+ provider :microsoft_graph, settings[:client_id], settings[:client_secret], provider_options
38
+ when "google_oauth2"
39
+ provider :google_oauth2, settings[:client_id], settings[:client_secret], provider_options
40
+ when "github"
41
+ provider :github, settings[:client_id], settings[:client_secret], provider_options
42
+ when "developer"
43
+ provider :developer if Rails.env.development?
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # Phlex configuration
7
+ module PhlexConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # Load Phlex base component after Rails application is initialized
12
+ # This ensures Rails.application.routes is available
13
+ initializer "panda_core.phlex_base", after: :load_config_initializers do
14
+ require "phlex"
15
+ require "phlex-rails"
16
+ require "literal"
17
+ require "tailwind_merge"
18
+
19
+ # Load the base component
20
+ require root.join("app/components/panda/core/base")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ class Engine < ::Rails::Engine
6
+ # Test environment configuration
7
+ module TestConfig
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ # For testing: Don't expose engine migrations since we use "copy to host app" strategy
12
+ # In test environment, migrations should be copied to the host app
13
+ if Rails.env.test?
14
+ config.paths["db/migrate"] = []
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -19,131 +19,36 @@ ensure
19
19
  $stderr = original_stderr
20
20
  end
21
21
 
22
+ # Load engine configuration modules
23
+ require_relative "engine/test_config"
24
+ require_relative "engine/autoload_config"
25
+ require_relative "engine/middleware_config"
26
+ require_relative "engine/importmap_config"
27
+ require_relative "engine/omniauth_config"
28
+ require_relative "engine/phlex_config"
29
+ require_relative "engine/admin_controller_config"
30
+
22
31
  module Panda
23
32
  module Core
24
33
  class Engine < ::Rails::Engine
25
34
  isolate_namespace Panda::Core
26
35
 
27
- # For testing: Don't expose engine migrations since we use "copy to host app" strategy
28
- # In test environment, migrations should be copied to the host app
29
- if Rails.env.test?
30
- config.paths["db/migrate"] = []
31
- end
32
-
33
- config.eager_load_namespaces << Panda::Core::Engine
36
+ # Include shared configuration modules
37
+ include Shared::InflectionsConfig
38
+ include Shared::GeneratorConfig
34
39
 
35
- # Add engine's app directories to autoload paths
36
- # Note: Only add the root directories, not nested subdirectories
37
- # Zeitwerk will automatically discover nested modules from these roots
38
- config.autoload_paths += Dir[root.join("app", "models")]
39
- config.autoload_paths += Dir[root.join("app", "controllers")]
40
- config.autoload_paths += Dir[root.join("app", "builders")]
41
- config.autoload_paths += Dir[root.join("app", "components")]
42
- config.autoload_paths += Dir[root.join("app", "services")]
43
-
44
- # Make files in public available to the main app (e.g. /panda-core-assets/panda-logo.png)
45
- config.middleware.use Rack::Static,
46
- urls: ["/panda-core-assets"],
47
- root: Panda::Core::Engine.root.join("public"),
48
- header_rules: [
49
- # Disable caching in development for instant CSS updates
50
- [:all, {"Cache-Control" => Rails.env.development? ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000"}]
51
- ]
52
-
53
- # Make JavaScript files available for importmap
54
- # Serve from app/javascript with proper MIME types
55
- config.middleware.use Rack::Static,
56
- urls: ["/panda", "/panda/core"],
57
- root: Panda::Core::Engine.root.join("app/javascript"),
58
- header_rules: [
59
- [:all, {"Cache-Control" => Rails.env.development? ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000",
60
- "Content-Type" => "text/javascript; charset=utf-8"}]
61
- ]
62
-
63
- config.generators do |g|
64
- g.test_framework :rspec
65
- g.fixture_replacement :factory_bot
66
- g.factory_bot dir: "spec/factories"
67
- end
40
+ # Include engine-specific configuration modules
41
+ include TestConfig
42
+ include AutoloadConfig
43
+ include MiddlewareConfig
44
+ include ImportmapConfig
45
+ include OmniauthConfig
46
+ include PhlexConfig
47
+ include AdminControllerConfig
68
48
 
69
49
  initializer "panda_core.config" do |app|
70
50
  # Configuration is already initialized with defaults in Configuration class
71
51
  end
72
-
73
- # Add importmap paths from the engine
74
- initializer "panda_core.importmap", before: "importmap" do |app|
75
- if app.config.respond_to?(:importmap)
76
- # Create a new array if frozen
77
- app.config.importmap.paths = app.config.importmap.paths.dup if app.config.importmap.paths.frozen?
78
-
79
- # Add our paths
80
- app.config.importmap.paths << root.join("config/importmap.rb")
81
-
82
- # Handle cache sweepers similarly
83
- if app.config.importmap.cache_sweepers.frozen?
84
- app.config.importmap.cache_sweepers = app.config.importmap.cache_sweepers.dup
85
- end
86
- app.config.importmap.cache_sweepers << root.join("app/javascript")
87
- end
88
- end
89
-
90
- initializer "panda_core.omniauth" do |app|
91
- # Load OAuth provider gems
92
- require_relative "oauth_providers"
93
- Panda::Core::OAuthProviders.setup
94
-
95
- # Mount OmniAuth at configurable admin path
96
- app.middleware.use OmniAuth::Builder do
97
- # Configure OmniAuth to use the configured admin path
98
- configure do |config|
99
- config.path_prefix = "#{Panda::Core.config.admin_path}/auth"
100
- # POST-only for CSRF protection (CVE-2015-9284)
101
- # All login forms use POST via form_tag method: "post"
102
- config.allowed_request_methods = [:post]
103
- end
104
-
105
- Panda::Core.config.authentication_providers.each do |provider_name, settings|
106
- # Build provider options, allowing custom path name override
107
- provider_options = settings[:options] || {}
108
-
109
- # If path_name is specified, use it to override the default strategy name in URLs
110
- if settings[:path_name].present?
111
- provider_options = provider_options.merge(name: settings[:path_name])
112
- end
113
-
114
- case provider_name.to_s
115
- when "microsoft_graph"
116
- provider :microsoft_graph, settings[:client_id], settings[:client_secret], provider_options
117
- when "google_oauth2"
118
- provider :google_oauth2, settings[:client_id], settings[:client_secret], provider_options
119
- when "github"
120
- provider :github, settings[:client_id], settings[:client_secret], provider_options
121
- when "developer"
122
- provider :developer if Rails.env.development?
123
- end
124
- end
125
- end
126
- end
127
-
128
- # Load Phlex base component after Rails application is initialized
129
- # This ensures Rails.application.routes is available
130
- initializer "panda_core.phlex_base", after: :load_config_initializers do
131
- require "phlex"
132
- require "phlex-rails"
133
- require "literal"
134
- require "tailwind_merge"
135
-
136
- # Load the base component
137
- require root.join("app/components/panda/core/base")
138
- end
139
-
140
- # Create AdminController alias after controllers are loaded
141
- # This allows other gems to inherit from Panda::Core::AdminController
142
- initializer "panda_core.admin_controller_alias", after: :load_config_initializers do
143
- ActiveSupport.on_load(:action_controller_base) do
144
- Panda::Core.const_set(:AdminController, Panda::Core::Admin::BaseController) unless Panda::Core.const_defined?(:AdminController)
145
- end
146
- end
147
52
  end
148
53
  end
149
54
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Shared
6
+ # Shared generator configuration for all panda gems
7
+ # This ensures consistent generator behavior across the ecosystem
8
+ module GeneratorConfig
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ config.generators do |g|
13
+ g.orm :active_record, primary_key_type: :uuid
14
+ g.test_framework :rspec, fixture: true
15
+ g.fixture_replacement nil
16
+ g.view_specs false
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Shared
6
+ # Shared inflections configuration for all panda gems
7
+ # Ensures consistent constant naming across the ecosystem
8
+ module InflectionsConfig
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ # Load inflections early to ensure proper constant resolution
13
+ initializer "panda.inflections", before: :load_config_initializers do
14
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
15
+ inflect.acronym "CMS"
16
+ inflect.acronym "SEO"
17
+ inflect.acronym "AI"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -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.3"
6
6
  end
7
7
  end
data/lib/panda/core.rb CHANGED
@@ -15,4 +15,6 @@ require_relative "core/configuration"
15
15
  require_relative "core/asset_loader"
16
16
  require_relative "core/debug"
17
17
  require_relative "core/services/base_service"
18
+ require_relative "core/shared/inflections_config"
19
+ require_relative "core/shared/generator_config"
18
20
  require_relative "core/engine" if defined?(Rails)
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.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Otaina Limited
@@ -461,6 +461,15 @@ files:
461
461
  - lib/panda/core/configuration.rb
462
462
  - lib/panda/core/debug.rb
463
463
  - lib/panda/core/engine.rb
464
+ - lib/panda/core/engine/admin_controller_config.rb
465
+ - lib/panda/core/engine/autoload_config.rb
466
+ - lib/panda/core/engine/generator_config.rb
467
+ - lib/panda/core/engine/importmap_config.rb
468
+ - lib/panda/core/engine/inflections_config.rb
469
+ - lib/panda/core/engine/middleware_config.rb
470
+ - lib/panda/core/engine/omniauth_config.rb
471
+ - lib/panda/core/engine/phlex_config.rb
472
+ - lib/panda/core/engine/test_config.rb
464
473
  - lib/panda/core/media.rb
465
474
  - lib/panda/core/notifications.rb
466
475
  - lib/panda/core/oauth_providers.rb
@@ -468,9 +477,18 @@ files:
468
477
  - lib/panda/core/rake_tasks.rb
469
478
  - lib/panda/core/seo.rb
470
479
  - lib/panda/core/services/base_service.rb
480
+ - lib/panda/core/shared/generator_config.rb
481
+ - lib/panda/core/shared/inflections_config.rb
471
482
  - lib/panda/core/sluggable.rb
472
483
  - lib/panda/core/subscribers/authentication_subscriber.rb
473
484
  - lib/panda/core/testing/rails_helper.rb
485
+ - lib/panda/core/testing/support/authentication_helpers.rb
486
+ - lib/panda/core/testing/support/authentication_test_helpers.rb
487
+ - lib/panda/core/testing/support/generator_spec_helper.rb
488
+ - lib/panda/core/testing/support/html_helpers.rb
489
+ - lib/panda/core/testing/support/omniauth_setup.rb
490
+ - lib/panda/core/testing/support/service_stubs.rb
491
+ - lib/panda/core/testing/support/setup.rb
474
492
  - lib/panda/core/version.rb
475
493
  - lib/tasks/assets.rake
476
494
  - lib/tasks/panda/core/migrations.rake