lesli_testing 1.4.0 → 1.4.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: 9e25e9ae176117279c4cf416cbc69557158d90aee0a0cda0b59bcc36301171f7
4
- data.tar.gz: 44bac7ca08dabc31650468010baac79760e8781d6f4b400d6d5eeb6dcd3b6dc8
3
+ metadata.gz: 433531e6c20b86725f4e12ec00bc0cb90116c21b84f8a8c74a463f6ab94a2164
4
+ data.tar.gz: f621205f275fef7decb3c1ec25f3af9888d78b8204e1fd5bf002da6bed4aafd6
5
5
  SHA512:
6
- metadata.gz: e608973abe0d050fa4adcc284d4f1ddb3637408142acb241e3a76be931888e77b15e7e67751511eaf818d44f519674a55c4f4b9c66c68656e7dc49185bbfc339
7
- data.tar.gz: ed2da15d9d78d227bf88a0088ce2d9c65eebfbf84d2cb40476783c03657d80f45f69e899e2f1ac7f448999946bf14466bff369be4ba3d7309d9ed937e56150fb
6
+ metadata.gz: 67e13138ca3fd0a466cc9233884e763d3f686435f354ee9deab48890a3a71bb83279587921edaacc74a94757519051f6a7c4723a7a999380a2a744e20dd8f8e2
7
+ data.tar.gz: b7318e4af4ce796f07c3b2b6c458e3eb95c0f3a06a1bd598f397461f0a334d2d4a75e8a4ab1a2f98b99b89da97cd7b8d69c105d8240b00c67b640b2d0f6abc10
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ =begin
4
+
5
+ Lesli
6
+
7
+ Copyright (c) 2026, Lesli Technologies, S. A.
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see http://www.gnu.org/licenses/.
21
+
22
+ Lesli · Ruby on Rails SaaS Development Framework.
23
+
24
+ Made with ♥ by LesliTech
25
+ Building a better future, one line of code at a time.
26
+
27
+ @contact hello@lesli.tech
28
+ @website https://www.lesli.tech
29
+ @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
30
+
31
+ // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
32
+ // ·
33
+ =end
34
+
35
+
36
+ require_relative "version"
37
+ require_relative "environment"
38
+
39
+
40
+ module LesliTesting
41
+ class Error < StandardError; end
42
+
43
+ class << self
44
+
45
+ attr_accessor :app_module;
46
+ attr_accessor :app_name;
47
+ attr_writer :options
48
+
49
+ def app(name, options = {})
50
+ configure(name, options.merge(coverage_profile: "app"))
51
+ end
52
+
53
+ def engine(name, options = {})
54
+ engine_options = options.dup
55
+ engine_target = engine_options.delete(:engine) || name
56
+
57
+ configure(
58
+ name,
59
+ engine_options.merge(
60
+ coverage_profile: "engine",
61
+ engine_target: engine_target
62
+ )
63
+ )
64
+ end
65
+
66
+ def gem(name, options = {})
67
+ configure(name, options.merge(coverage_profile: "gem"))
68
+ end
69
+
70
+ def profile
71
+ @options && @options[:coverage_profile]
72
+ end
73
+
74
+ private
75
+
76
+ def options
77
+ @options ||= options_default
78
+ end
79
+
80
+ def options_default
81
+ {
82
+ coverage_profile: "app",
83
+ coverage_missing_len: 25,
84
+ coverage_min_coverage: 90
85
+ }
86
+ end
87
+
88
+ def configure(name, options)
89
+ self.app_name = name
90
+ self.options = options_default.merge(options)
91
+ configure_coverage()
92
+ configure_reporter()
93
+ configure_rails_integrations()
94
+ end
95
+
96
+ def configure_coverage()
97
+
98
+ return unless LesliTesting::Environment.enabled?("COVERAGE")
99
+ return if defined?(SimpleCov) && SimpleCov.started_in_this_process?
100
+
101
+ require_relative "coverage"
102
+
103
+ # Start Coverage
104
+ LesliTesting::Coverage.start(
105
+ self.app_name,
106
+ profile: self.options[:coverage_profile],
107
+ missing_len: self.options[:coverage_missing_len],
108
+ min_coverage: self.options[:coverage_min_coverage]
109
+ )
110
+ end
111
+
112
+ def configure_reporter
113
+ require "minitest"
114
+ require "termline" unless defined?(Termline)
115
+ require "minitest/lesli_testing_plugin"
116
+
117
+ # Force Minitest to know about Lesli Minitest reporter plugin
118
+ unless Minitest.extensions.include?("lesli_testing")
119
+ Minitest.register_plugin("lesli_testing")
120
+ end
121
+ end
122
+
123
+ def configure_rails_integrations
124
+ return if self.options[:coverage_profile] == "gem"
125
+
126
+ require_relative "rails_integration"
127
+ LesliTesting::RailsIntegration.install!(
128
+ profile: self.options[:coverage_profile],
129
+ project_name: self.app_name,
130
+ engine_target: self.options[:engine_target],
131
+ options: self.options
132
+ )
133
+ end
134
+ end
135
+ end
@@ -35,7 +35,6 @@ Building a better future, one line of code at a time.
35
35
 
36
36
  # Load code coverage tools
37
37
  require "simplecov"
38
- require "simplecov-html"
39
38
  require "simplecov-console"
40
39
  require "simplecov-cobertura"
41
40
  require_relative "simplecov/profiles"
@@ -50,7 +49,6 @@ module LesliTesting
50
49
  # Add coverage formatters
51
50
  SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
52
51
  SimpleCov::Formatter::CoberturaFormatter,
53
- SimpleCov::Formatter::HTMLFormatter,
54
52
  SimpleCov::Formatter::Console
55
53
  ])
56
54
 
@@ -32,36 +32,22 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  module LesliTesting
34
34
  class Engine < ::Rails::Engine
35
- initializer "lesli_testing.load_required_migrations", after: :load_config_initializers do |app|
36
-
37
- # Only run this if we are in the test environment
38
- unless Rails.env.production?
39
-
40
- # 1. Define the dependencies you want to automatically include
41
- # You can add more gems to this list as your ecosystem grows
42
- dependencies = ["Lesli"]
43
-
44
- dependencies.each do |gem_name|
45
- begin
46
- # Find the constant for the gem's Engine
47
- gem_engine = "#{gem_name}::Engine".safe_constantize
35
+ class << self
36
+ attr_reader :engine_target
48
37
 
49
- if gem_engine
50
- migration_path = gem_engine.root.join("db/migrate").to_s
38
+ def configure(engine_target:)
39
+ @engine_target = engine_target
40
+ end
41
+ end
51
42
 
52
- # Append to the main app's migration paths if the directory exists
53
- if Dir.exist?(migration_path)
54
- app.config.paths["db/migrate"] << migration_path
55
- end
56
- end
57
- rescue StandardError => e
58
- puts "LesliTesting: Could not load migrations for #{gem_name}: #{e.message}"
59
- end
60
- end
43
+ initializer "lesli_testing.load_required_migrations", after: :load_config_initializers do |app|
44
+ next unless Rails.env.test?
45
+ next unless LesliTesting::Engine.engine_target
61
46
 
62
- # 2. Clean up duplicates
63
- app.config.paths["db/migrate"].uniq!
64
- end
47
+ LesliTesting::Fixtures.configure_migration_paths(
48
+ app,
49
+ engine_target: LesliTesting::Engine.engine_target
50
+ )
65
51
  end
66
52
  end
67
53
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ =begin
4
+
5
+ Lesli
6
+
7
+ Copyright (c) 2026, Lesli Technologies, S. A.
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see http://www.gnu.org/licenses/.
21
+
22
+ Lesli · Ruby on Rails SaaS Development Framework.
23
+
24
+ Made with ♥ by LesliTech
25
+ Building a better future, one line of code at a time.
26
+
27
+ @contact hello@lesli.tech
28
+ @website https://www.lesli.tech
29
+ @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
30
+
31
+ // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
32
+ // ·
33
+ =end
34
+
35
+
36
+ module LesliTesting
37
+ module Environment
38
+ TRUE_VALUES = %w[true 1 yes].freeze
39
+
40
+ def self.enabled?(name, env: ENV)
41
+ TRUE_VALUES.include?(env[name].to_s.strip.downcase)
42
+ end
43
+ end
44
+ end
@@ -32,40 +32,314 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  module LesliTesting
34
34
  module Fixtures
35
+ LESLI_FIXTURE_CLASSES = {
36
+ lesli_users: "Lesli::User",
37
+ lesli_accounts: "Lesli::Account"
38
+ }.freeze
39
+
35
40
  class << self
36
- def load_fixtures_for_lesli()
37
-
38
- if defined?(Lesli)
39
- ActiveSupport.on_load(:active_support_test_case) do
40
- # En Rails 8, usamos ActiveRecord::TestFixtures si está disponible
41
- include ActiveRecord::TestFixtures if defined?(ActiveRecord::TestFixtures)
42
-
43
- lesli_fixtures = Lesli::Engine.root.join("test", "fixtures").to_s
44
- lesli_files = Lesli::Engine.root.join("test", "fixtures", "files").to_s
45
-
46
- # Rails 8 usa fixture_paths (plural) como el estándar
47
- # Pero para evitar el NoMethodError, lo asignamos de forma segura:
48
- if respond_to?(:fixture_paths=)
49
- # Combinamos las rutas existentes con la nueva del Engine
50
- self.fixture_paths |= [ lesli_fixtures ]
51
- else
52
- # Fallback por si la carga de ActiveRecord se retrasa
53
- class_attribute :fixture_paths, default: [ lesli_fixtures ]
54
- end
55
-
56
- # Los archivos adjuntos (Active Storage, etc)
57
- self.file_fixture_path = lesli_files
58
-
59
- # IMPORTANT: attach fixture sets to namespaced models BEFORE loading fixtures
60
- self.set_fixture_class(
61
- lesli_users: "Lesli::User",
62
- lesli_accounts: "Lesli::Account"
63
- )
41
+ def install!(profile: "app", engine_name: nil, options: {})
42
+ @configuration = build_configuration(profile, engine_name, options)
43
+ return if @installed
44
+
45
+ ActiveSupport.on_load(
46
+ :active_support_test_case,
47
+ run_once: true,
48
+ yield: true
49
+ ) do |test_case|
50
+ configure_test_case(test_case, load_sets: true)
51
+ end
52
+
53
+ ActiveSupport.on_load(
54
+ :action_dispatch_integration_test,
55
+ run_once: true,
56
+ yield: true
57
+ ) do |test_case|
58
+ configure_test_case(test_case, load_sets: false)
59
+ end
60
+
61
+ ActiveSupport.on_load(
62
+ :active_record,
63
+ run_once: true,
64
+ yield: true
65
+ ) do
66
+ configure_migration_paths
67
+ end
68
+
69
+ @installed = true
70
+ end
71
+
72
+ def load_fixtures_for_engine(engine_module, test_case: nil)
73
+ test_case ||= ActiveSupport::TestCase if defined?(ActiveSupport::TestCase)
74
+ raise ArgumentError, "A Rails test case is required" unless test_case
75
+
76
+ configure_test_case(
77
+ test_case,
78
+ engine_module: engine_module,
79
+ load_sets: true
80
+ )
81
+ end
82
+
83
+ def configure_migration_paths(application = nil, engine_target: nil)
84
+ configuration = @configuration
85
+ return [] unless configuration
86
+ return [] unless configuration[:profile] == "engine"
87
+ return [] unless configuration[:migrations][:enabled]
88
+ return [] unless test_environment?
89
+
90
+ application ||= Rails.application if defined?(Rails.application)
91
+ paths = migration_paths(application, engine_target: engine_target)
92
+
93
+ add_application_migration_paths(application, paths)
94
+ add_active_record_migration_paths(paths)
95
+
96
+ paths
97
+ end
98
+
99
+ private
100
+
101
+ def test_environment?
102
+ return false unless defined?(Rails)
103
+ return false unless Rails.respond_to?(:env)
104
+
105
+ Rails.env.respond_to?(:test?) && Rails.env.test?
106
+ end
107
+
108
+ def build_configuration(profile, engine_name, options)
109
+ fixture_options = options.fetch(:fixtures, {})
110
+ migration_options = options.fetch(:migrations, {})
111
+ default_shared = default_shared_engines(profile, engine_name)
112
+
113
+ {
114
+ profile: profile,
115
+ fixtures: build_fixture_configuration(
116
+ fixture_options,
117
+ profile,
118
+ engine_name,
119
+ default_shared
120
+ ),
121
+ migrations: build_migration_configuration(
122
+ migration_options,
123
+ engine_name,
124
+ default_shared
125
+ )
126
+ }
127
+ end
128
+
129
+ def build_fixture_configuration(options, profile, engine_name, default_shared)
130
+ return { enabled: false } if options == false
131
+
132
+ default_owner = engine_name if profile == "engine"
133
+ owner = options.fetch(:owner, default_owner)
134
+ shared = (default_shared + Array(options[:shared])).uniq
135
+ default_classes = if includes_lesli_fixtures?(owner, shared)
136
+ LESLI_FIXTURE_CLASSES
137
+ else
138
+ {}
139
+ end
140
+
141
+ {
142
+ enabled: true,
143
+ owner: owner,
144
+ path: options[:path],
145
+ file_path: options[:file_path],
146
+ shared: shared,
147
+ shared_paths: Array(options[:shared_paths]),
148
+ sets: options.fetch(:sets, profile == "engine" ? :all : nil),
149
+ classes: default_classes.merge(options.fetch(:classes, {}))
150
+ }
151
+ end
152
+
153
+ def build_migration_configuration(options, engine_name, default_shared)
154
+ return { enabled: false } if options == false
155
+
156
+ {
157
+ enabled: true,
158
+ owner: options.fetch(:owner, engine_name),
159
+ owner_explicit: options.key?(:owner),
160
+ include_dummy: options.fetch(:include_dummy, true),
161
+ include_owner: options.fetch(:include_owner, true),
162
+ shared: (default_shared + Array(options[:shared])).uniq,
163
+ paths: Array(options[:paths])
164
+ }
165
+ end
166
+
167
+ def default_shared_engines(profile, engine_name)
168
+ return ["Lesli"] if profile == "app"
169
+ return [] unless profile == "engine"
170
+ return [] if ["Lesli", "Lesli::Engine"].include?(engine_name.to_s)
171
+
172
+ ["Lesli"]
173
+ end
174
+
175
+ def includes_lesli_fixtures?(owner, shared)
176
+ [owner, *shared].any? do |reference|
177
+ ["Lesli", "Lesli::Engine"].include?(reference.to_s)
178
+ end
179
+ end
180
+
181
+ def configure_test_case(test_case, engine_module: nil, load_sets:)
182
+ configuration = @configuration
183
+ return [] unless configuration
184
+
185
+ fixture_configuration = configuration[:fixtures]
186
+ return [] unless fixture_configuration[:enabled]
187
+
188
+ owner_engine = engine_module || resolve_engine(fixture_configuration[:owner])
189
+ configured_paths = fixture_paths(owner_engine, fixture_configuration)
190
+
191
+ include_active_record_fixtures(test_case)
192
+ assign_fixture_paths(test_case, configured_paths, configuration[:profile])
193
+ assign_file_fixture_path(test_case, owner_engine, fixture_configuration)
194
+ configure_fixture_sets(test_case, fixture_configuration) if load_sets
195
+
196
+ configured_paths
197
+ end
198
+
199
+ def include_active_record_fixtures(test_case)
200
+ return unless defined?(ActiveRecord::TestFixtures)
201
+ return if test_case < ActiveRecord::TestFixtures
202
+
203
+ test_case.include(ActiveRecord::TestFixtures)
204
+ end
205
+
206
+ def fixture_paths(owner_engine, configuration)
207
+ paths = []
208
+ paths << expand_path(configuration[:path]) if configuration[:path]
209
+ paths << engine_path(owner_engine, "test", "fixtures") unless configuration[:path]
210
+
211
+ configuration[:shared].each do |shared_engine|
212
+ paths << engine_path(resolve_engine(shared_engine), "test", "fixtures")
213
+ end
214
+
215
+ paths.concat(configuration[:shared_paths].map { |path| expand_path(path) })
216
+ paths.compact.uniq
217
+ end
218
+
219
+ def assign_fixture_paths(test_case, configured_paths, profile)
220
+ if test_case.respond_to?(:fixture_paths=)
221
+ current_paths = if test_case.respond_to?(:fixture_paths)
222
+ Array(test_case.fixture_paths)
223
+ else
224
+ []
225
+ end
226
+
227
+ paths = if profile == "engine"
228
+ configured_paths + current_paths
229
+ else
230
+ current_paths + configured_paths
231
+ end
232
+ test_case.fixture_paths = paths.compact.uniq
233
+ elsif test_case.respond_to?(:class_attribute)
234
+ test_case.class_attribute :fixture_paths, default: configured_paths
235
+ end
236
+ end
237
+
238
+ def assign_file_fixture_path(test_case, owner_engine, configuration)
239
+ return unless test_case.respond_to?(:file_fixture_path=)
240
+
241
+ file_path = configuration[:file_path]
242
+ file_path ||= engine_path(owner_engine, "test", "fixtures", "files") if owner_engine
243
+ test_case.file_fixture_path = expand_path(file_path) if file_path
244
+ end
245
+
246
+ def configure_fixture_sets(test_case, configuration)
247
+ if test_case.respond_to?(:set_fixture_class) && configuration[:classes].any?
248
+ test_case.set_fixture_class(configuration[:classes])
249
+ end
250
+
251
+ sets = configuration[:sets]
252
+ return unless sets
253
+ return unless test_case.respond_to?(:fixtures)
254
+
255
+ sets == :all ? test_case.fixtures(:all) : test_case.fixtures(*Array(sets))
256
+ end
257
+
258
+ def migration_paths(application, engine_target: nil)
259
+ configuration = @configuration[:migrations]
260
+ paths = []
261
+
262
+ if configuration[:include_dummy] && application&.respond_to?(:root)
263
+ paths << File.join(application.root.to_s, "db", "migrate")
264
+ end
265
+
266
+ if configuration[:include_owner]
267
+ owner = if configuration[:owner_explicit]
268
+ configuration[:owner]
269
+ else
270
+ engine_target || configuration[:owner]
64
271
  end
272
+ paths << engine_path(resolve_engine(owner), "db", "migrate")
273
+ end
274
+
275
+ configuration[:shared].each do |shared_engine|
276
+ paths << engine_path(resolve_engine(shared_engine), "db", "migrate")
65
277
  end
278
+
279
+ paths.concat(configuration[:paths].map { |path| expand_path(path) })
280
+ paths.compact.select { |path| Dir.exist?(path) }.uniq
281
+ end
282
+
283
+ def add_application_migration_paths(application, paths)
284
+ return unless application&.respond_to?(:config)
285
+
286
+ rails_paths = application.config.paths["db/migrate"]
287
+ return unless rails_paths
288
+
289
+ registered_paths = rails_paths.expanded.map { |path| expand_path(path) }
290
+ paths.each do |path|
291
+ expanded_path = expand_path(path)
292
+ next if registered_paths.include?(expanded_path)
293
+
294
+ rails_paths << path
295
+ registered_paths << expanded_path
296
+ end
297
+ end
298
+
299
+ def add_active_record_migration_paths(paths)
300
+ return unless defined?(ActiveRecord::Migrator)
301
+ return unless ActiveRecord::Migrator.respond_to?(:migrations_paths=)
302
+
303
+ current_paths = if ActiveRecord::Migrator.respond_to?(:migrations_paths)
304
+ Array(ActiveRecord::Migrator.migrations_paths)
305
+ else
306
+ []
307
+ end
308
+ registered_paths = current_paths.map { |path| expand_path(path) }
309
+ missing_paths = paths.reject do |path|
310
+ registered_paths.include?(expand_path(path))
311
+ end
312
+
313
+ ActiveRecord::Migrator.migrations_paths = current_paths + missing_paths
314
+ end
315
+
316
+ def resolve_engine(reference)
317
+ return reference if reference.respond_to?(:root)
318
+ return if reference.nil?
319
+
320
+ constant = constantize(reference.to_s)
321
+ return constant if constant&.respond_to?(:root)
322
+ return unless constant&.const_defined?(:Engine)
323
+
324
+ constant.const_get(:Engine)
325
+ end
326
+
327
+ def constantize(name)
328
+ name.split("::").reject(&:empty?).inject(Object) do |namespace, constant_name|
329
+ namespace.const_get(constant_name)
330
+ end
331
+ rescue NameError
332
+ nil
333
+ end
334
+
335
+ def engine_path(engine_module, *parts)
336
+ return unless engine_module&.respond_to?(:root)
337
+
338
+ File.join(engine_module.root.to_s, *parts)
66
339
  end
67
340
 
68
- def load_fixtures_for_engine(engine_module)
341
+ def expand_path(path)
342
+ File.expand_path(path.to_s)
69
343
  end
70
344
  end
71
345
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  Lesli
4
4
 
5
- Copyright (c) 2025, Lesli Technologies, S. A.
5
+ Copyright (c) 2026, Lesli Technologies, S. A.
6
6
 
7
7
  This program is free software: you can redistribute it and/or modify
8
8
  it under the terms of the GNU General Public License as published by
@@ -30,17 +30,24 @@ Building a better future, one line of code at a time.
30
30
  // ·
31
31
  =end
32
32
 
33
- module ResponseIntegrationHelper
33
+ require "json"
34
34
 
35
- # return the body of a request response parsed as JSON
36
- def response_json
37
- JSON.parse(@response.body.blank? ? "{}" : @response.body)
38
- end
35
+ module LesliTesting
36
+ module ResponseIntegrationHelper
37
+
38
+ # return the body of a request response parsed as JSON
39
+ def response_json(http_response = response)
40
+ JSON.parse(http_response.body.blank? ? "{}" : http_response.body)
41
+ end
39
42
 
43
+ def assert_successful_json_response(http_response = response)
44
+ assert_predicate(http_response, :successful?)
45
+ assert_equal("application/json", http_response.media_type)
46
+ end
40
47
 
41
- # test a standard successful response for lesli 3
42
- def expect_response_with_successful
43
- assert_response(:success)
44
- assert_equal("application/json; charset=utf-8", @response.content_type)
48
+ # test a standard successful response for lesli 3
49
+ def expect_response_with_successful(http_response = response)
50
+ assert_successful_json_response(http_response)
51
+ end
45
52
  end
46
53
  end