conjure_shield 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2336 @@
1
+ require "fileutils"
2
+
3
+ module ConjureShield
4
+ class TestGenerator
5
+ attr_reader :code, :suggestions
6
+
7
+ def initialize(code, suggestions)
8
+ @code = code
9
+ @suggestions = suggestions
10
+ @codebase_path = Dir.pwd
11
+ @framework = nil
12
+ end
13
+
14
+ def self.generate(code, suggestions)
15
+ new(code, suggestions).generate_all
16
+ end
17
+
18
+ def self.generate_with_path(code, suggestions, codebase_path)
19
+ new(code, suggestions).tap do |g|
20
+ g.instance_variable_set(:@codebase_path, codebase_path)
21
+ g.instance_variable_set(:@framework, nil)
22
+ end.generate_all
23
+ end
24
+
25
+ def self.generate_for_all_frameworks(code, suggestions, codebase_path)
26
+ frameworks = []
27
+ frameworks << :rspec if Dir.exist?(File.join(codebase_path, "spec"))
28
+ if Dir.exist?(File.join(codebase_path, "test"))
29
+ frameworks << :minitest
30
+ else
31
+ FileUtils.mkdir_p(File.join(codebase_path, "test"))
32
+ frameworks << :minitest
33
+ puts "Created test/ directory for Minitest"
34
+ end
35
+
36
+ frameworks.each_with_index do |fw, idx|
37
+ new(code, suggestions).tap do |g|
38
+ g.instance_variable_set(:@codebase_path, codebase_path)
39
+ g.instance_variable_set(:@framework, fw)
40
+ end.generate_all(skip_factories: idx > 0)
41
+ end
42
+ end
43
+
44
+ def generate_all(framework: nil, skip_factories: false)
45
+ @framework = framework if framework
46
+ generate_factories unless skip_factories
47
+
48
+ @suggestions.each do |missing_test|
49
+ inner = missing_test[:suggestions] || []
50
+ context = missing_test.reject { |k, _| k == :suggestions }
51
+
52
+ inner.each do |suggestion|
53
+ merged = context.merge(suggestion)
54
+ @current_type = merged[:type]
55
+ generate_test(merged)
56
+ end
57
+ end
58
+ end
59
+
60
+ def generate_factories
61
+ collect_factory_specs.each do |model_name, info|
62
+ write_factory_file(model_name, factory_content(model_name, info[:columns], info[:associations]))
63
+ end
64
+ end
65
+
66
+ def collect_factory_specs
67
+ specs = {}
68
+ @suggestions.each do |missing_test|
69
+ inner = missing_test[:suggestions] || []
70
+ context = missing_test.reject { |k, _| k == :suggestions }
71
+ inner.each do |suggestion|
72
+ next unless suggestion[:type] == :factories
73
+
74
+ merged = context.merge(suggestion)
75
+ specs[merged[:model]] = {
76
+ columns: merged[:columns] || {},
77
+ associations: merged[:associations] || []
78
+ }
79
+ end
80
+ end
81
+ specs
82
+ end
83
+
84
+ def factory_content(model_name, columns, associations)
85
+ factory_decl = if model_name.include?("::")
86
+ " factory :#{factory_name(model_name)}, class: \"#{model_name}\" do"
87
+ else
88
+ " factory :#{factory_name(model_name)} do"
89
+ end
90
+
91
+ lines = [
92
+ "# frozen_string_literal: true",
93
+ "",
94
+ "FactoryBot.define do",
95
+ factory_decl,
96
+ factory_attribute_lines(columns),
97
+ factory_association_lines(associations),
98
+ " end",
99
+ "end",
100
+ "",
101
+ ]
102
+ lines.flatten.join("\n")
103
+ end
104
+
105
+ EXCLUDED_FACTORY_COLUMNS = %w[
106
+ id created_at updated_at encrypted_password reset_password_token
107
+ reset_password_sent_at remember_created_at
108
+ ].freeze
109
+
110
+ def factory_attribute_lines(columns)
111
+ columns.reject { |name, _| EXCLUDED_FACTORY_COLUMNS.include?(name) }.flat_map do |col_name, col_info|
112
+ line = factory_attribute_line(col_name, col_info[:type])
113
+ line ? " #{line}" : nil
114
+ end.compact
115
+ end
116
+
117
+ def factory_attribute_line(col_name, type)
118
+ case col_name
119
+ when "email", /_email$/
120
+ "sequence(:#{col_name}) { |n| \"user_\#{n}@example.com\" }"
121
+ when /_url$/
122
+ "#{col_name} { \"https://example.com/#{col_name}\" }"
123
+ else
124
+ case type
125
+ when :string then "#{col_name} { \"#{col_name.humanize.downcase}\" }"
126
+ when :text then "#{col_name} { \"sample text\" }"
127
+ when :integer then "#{col_name} { 1 }"
128
+ when :boolean then "#{col_name} { false }"
129
+ when :datetime, :date then "#{col_name} { Time.current }"
130
+ when :decimal, :float then "#{col_name} { 1.0 }"
131
+ else
132
+ nil
133
+ end
134
+ end
135
+ end
136
+
137
+ def factory_association_lines(associations)
138
+ associations.flat_map do |assoc|
139
+ name = assoc[:name]
140
+ target = assoc[:target]
141
+ case assoc[:type]
142
+ when :belongs_to
143
+ " association :#{name}"
144
+ when :has_one, :has_one_through
145
+ [
146
+ " trait :with_#{name} do",
147
+ " association :#{name}",
148
+ " end",
149
+ ]
150
+ when :has_many
151
+ [
152
+ " trait :with_#{name} do",
153
+ " association :#{name}",
154
+ " end",
155
+ ]
156
+ when :has_and_belongs_to_many, :has_many_through
157
+ [
158
+ " trait :with_#{name} do",
159
+ " after(:create) do |obj, evaluator|",
160
+ " obj.#{name} << create(:#{target.underscore})",
161
+ " end",
162
+ " end",
163
+ ]
164
+ else
165
+ nil
166
+ end
167
+ end.compact
168
+ end
169
+
170
+ def write_factory_file(model_name, content)
171
+ base = @codebase_path || Dir.pwd
172
+ base_dir = File.join(base, rspec? ? "spec" : "test", "factories")
173
+ FileUtils.mkdir_p(base_dir)
174
+ path = File.join(base_dir, "#{factory_name(model_name)}.rb")
175
+ if File.exist?(path)
176
+ puts "Skipped existing factory: #{path}"
177
+ return
178
+ end
179
+ File.write(path, content)
180
+ puts "Generated factory: #{path}"
181
+ end
182
+
183
+ private
184
+
185
+ def framework
186
+ @framework ||= detect_framework
187
+ end
188
+
189
+ def rspec?
190
+ framework == :rspec
191
+ end
192
+
193
+ def minitest?
194
+ framework == :minitest
195
+ end
196
+
197
+ def detect_framework
198
+ base = @codebase_path || Dir.pwd
199
+ spec_dir = Dir.exist?(File.join(base, "spec"))
200
+ test_dir = Dir.exist?(File.join(base, "test"))
201
+ return :minitest if test_dir && !spec_dir
202
+ :rspec
203
+ end
204
+
205
+ def spec_helper_require
206
+ base = @codebase_path || Dir.pwd
207
+ if File.exist?(File.join(base, "spec", "rails_helper.rb"))
208
+ 'require "rails_helper"'
209
+ elsif File.exist?(File.join(base, "spec", "spec_helper.rb"))
210
+ 'require "spec_helper"'
211
+ elsif rspec?
212
+ 'require "rails_helper"'
213
+ else
214
+ 'require "test_helper"'
215
+ end
216
+ end
217
+
218
+ def write_lines(lines)
219
+ lines.flatten.join("\n") + "\n"
220
+ end
221
+
222
+ def devise?
223
+ return @_devise if defined?(@_devise)
224
+ base = @codebase_path || Dir.pwd
225
+ gemfile = File.join(base, "Gemfile")
226
+ @_devise = File.exist?(gemfile) && File.read(gemfile).include?("devise")
227
+ end
228
+
229
+ def devise_model_name
230
+ return @_devise_model if defined?(@_devise_model)
231
+ @_devise_model = nil
232
+ models = @code&.select { |f| f[:path].include?("/app/models/") }
233
+ return nil unless models
234
+
235
+ models.each do |model|
236
+ if model[:content].include?("devise ") || model[:content].include?("devise\n")
237
+ @_devise_model = model_name_from_path(model[:path])
238
+ break
239
+ end
240
+ end
241
+ @_devise_model || "User"
242
+ end
243
+
244
+ def model_name_from_path(path)
245
+ relative = path.sub(%r{.*/app/models/}, "").sub(/\.rb\z/, "")
246
+ relative.split("/").map { |part| part.split("_").map(&:capitalize).join }.join("::")
247
+ end
248
+
249
+ def factory_name(model_name)
250
+ model_name.underscore.tr("/", "_")
251
+ end
252
+
253
+ def devise_setup(indent: 2, extra_attrs: {})
254
+ return "" unless devise?
255
+
256
+ model = devise_model_name
257
+ attrs = {email: "test@example.com", password: "password", password_confirmation: "password"}.merge(extra_attrs)
258
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
259
+ pad = " " * indent
260
+
261
+ "#{pad}include Devise::Test::IntegrationHelpers\n" \
262
+ "#{pad}let(:current_user) { #{model}.create!(#{attrs_str}) }\n" \
263
+ "#{pad}before { sign_in current_user }\n\n"
264
+ end
265
+
266
+ def devise_setup_lines(component: " ")
267
+ return [] unless devise?
268
+
269
+ model = devise_model_name
270
+ attrs = {email: "test@example.com", password: "password", password_confirmation: "password"}
271
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
272
+ [
273
+ "#{component}include Devise::Test::IntegrationHelpers",
274
+ "#{component}let(:current_user) { #{model}.create!(#{attrs_str}) }",
275
+ "#{component}before { sign_in current_user }",
276
+ "",
277
+ ]
278
+ end
279
+
280
+ def factory_attributes(model_name, columns: {}, extra_attrs: {})
281
+ suffix = Time.now.to_i.to_s(36) + rand(999).to_s
282
+ excluded = %w[id created_at updated_at encrypted_password reset_password_token reset_password_sent_at remember_created_at]
283
+ hash = columns.reject { |name, _| excluded.include?(name) }.each_with_object({}) do |(col_name, col_info), h|
284
+ value = case col_name
285
+ when "email" then "test#{suffix}@example.com"
286
+ when /_url$/ then "https://example.com/test"
287
+ when /_email$/ then "test#{suffix}@example.com"
288
+ else
289
+ case col_info[:type]
290
+ when :string then "#{col_name.humanize.downcase}#{suffix}"
291
+ when :text then "sample text"
292
+ when :integer then 1
293
+ when :boolean then false
294
+ when :datetime, :date then Time.current
295
+ when :decimal, :float then 1.0
296
+ end
297
+ end
298
+ h[col_name.to_sym] = value if value
299
+ end
300
+ if columns.key?("encrypted_password")
301
+ hash[:email] ||= "test#{suffix}@example.com"
302
+ hash[:password] = "password"
303
+ hash[:password_confirmation] = "password"
304
+ end
305
+ hash.merge!(extra_attrs.symbolize_keys) if extra_attrs.any?
306
+ hash
307
+ end
308
+
309
+ def prepend_devise(content, model: nil)
310
+ setup = devise_setup
311
+ return content if setup.empty?
312
+ setup + content
313
+ end
314
+
315
+ def factory_attrs_str(model_name)
316
+ attrs = factory_attributes(model_name)
317
+ attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
318
+ end
319
+
320
+ def controller_factory_attrs(controller)
321
+ model_name = controller[:model]
322
+ columns = controller[:columns] || {}
323
+ attrs = factory_attributes(model_name, columns: columns)
324
+ attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
325
+ end
326
+
327
+ def ctrl_base(name)
328
+ name.to_s.sub(/Controller$/, "").underscore.singularize.tr("/", "_")
329
+ end
330
+
331
+ def ctrl_route(name)
332
+ ctrl_base(name).pluralize
333
+ end
334
+
335
+ def generate_test(suggestion)
336
+ case suggestion[:type]
337
+ when :validations
338
+ minitest? ? generate_minitest_validations(suggestion) : generate_validations_test(suggestion)
339
+ when :validation_messages
340
+ minitest? ? generate_minitest_validations(suggestion) : generate_validation_messages_test(suggestion)
341
+ when :has_one
342
+ minitest? ? generate_minitest_associations(suggestion) : generate_has_one_test(suggestion)
343
+ when :has_many
344
+ minitest? ? generate_minitest_associations(suggestion) : generate_has_many_test(suggestion)
345
+ when :belongs_to
346
+ minitest? ? generate_minitest_associations(suggestion) : generate_belongs_to_test(suggestion)
347
+ when :association_validations
348
+ minitest? ? generate_minitest_associations(suggestion) : generate_association_validations_test(suggestion)
349
+ when :scopes
350
+ minitest? ? generate_minitest_scopes(suggestion) : generate_scopes_test(suggestion)
351
+ when :scoped_arguments
352
+ minitest? ? generate_minitest_scopes(suggestion) : generate_scoped_arguments_test(suggestion)
353
+ when :before_save
354
+ minitest? ? generate_minitest_callbacks(suggestion) : generate_before_save_test(suggestion)
355
+ when :after_save
356
+ minitest? ? generate_minitest_callbacks(suggestion) : generate_after_save_test(suggestion)
357
+ when :before_destroy
358
+ minitest? ? generate_minitest_callbacks(suggestion) : generate_before_destroy_test(suggestion)
359
+ when :after_destroy
360
+ minitest? ? generate_minitest_callbacks(suggestion) : generate_after_destroy_test(suggestion)
361
+ when :custom_methods
362
+ minitest? ? generate_minitest_custom_methods(suggestion) : generate_custom_methods_test(suggestion)
363
+ when :factories
364
+ minitest? ? generate_minitest_factories(suggestion) : generate_factories_test(suggestion)
365
+ when :serialization
366
+ minitest? ? generate_minitest_serialization(suggestion) : generate_serialization_test(suggestion)
367
+ when :delegation
368
+ minitest? ? generate_minitest_serialization(suggestion) : generate_delegation_test(suggestion)
369
+ when :get_index
370
+ minitest? ? generate_minitest_request_test(suggestion) : generate_get_index_test(suggestion)
371
+ when :post_create
372
+ minitest? ? generate_minitest_request_test(suggestion) : generate_post_create_valid_test(suggestion)
373
+ when :get_show
374
+ minitest? ? generate_minitest_request_test(suggestion) : generate_get_show_test(suggestion)
375
+ when :index_pagination
376
+ minitest? ? generate_minitest_request_test(suggestion) : generate_index_pagination_test(suggestion)
377
+ when :index_sorting
378
+ minitest? ? generate_minitest_request_test(suggestion) : generate_index_sorting_test(suggestion)
379
+ when :show_with_associations
380
+ minitest? ? generate_minitest_request_test(suggestion) : generate_show_with_associations_test(suggestion)
381
+ when :get_new
382
+ minitest? ? generate_minitest_request_test(suggestion) : generate_get_new_test(suggestion)
383
+ when :new_form
384
+ minitest? ? generate_minitest_request_test(suggestion) : generate_new_form_test(suggestion)
385
+ when :get_edit
386
+ minitest? ? generate_minitest_request_test(suggestion) : generate_get_edit_test(suggestion)
387
+ when :edit_form
388
+ minitest? ? generate_minitest_request_test(suggestion) : generate_edit_form_test(suggestion)
389
+ when :post_create_valid
390
+ minitest? ? generate_minitest_request_test(suggestion) : generate_post_create_valid_test(suggestion)
391
+ when :post_create_invalid
392
+ minitest? ? generate_minitest_request_test(suggestion) : generate_post_create_invalid_test(suggestion)
393
+ when :post_create_redirect
394
+ minitest? ? generate_minitest_request_test(suggestion) : generate_post_create_redirect_test(suggestion)
395
+ when :put_patch_update_valid
396
+ minitest? ? generate_minitest_request_test(suggestion) : generate_put_patch_update_valid_test(suggestion)
397
+ when :put_patch_update_invalid
398
+ minitest? ? generate_minitest_request_test(suggestion) : generate_put_patch_update_invalid_test(suggestion)
399
+ when :put_patch_update_redirect
400
+ minitest? ? generate_minitest_request_test(suggestion) : generate_put_patch_update_redirect_test(suggestion)
401
+ when :delete_destroy
402
+ minitest? ? generate_minitest_request_test(suggestion) : generate_delete_destroy_test(suggestion)
403
+ when :delete_destroy_redirect
404
+ minitest? ? generate_minitest_request_test(suggestion) : generate_delete_destroy_redirect_test(suggestion)
405
+ when :strong_parameters_permit
406
+ minitest? ? generate_minitest_request_test(suggestion) : generate_strong_parameters_permit_test(suggestion)
407
+ when :strong_parameters_deny
408
+ minitest? ? generate_minitest_request_test(suggestion) : generate_strong_parameters_deny_test(suggestion)
409
+ when :flash_messages
410
+ minitest? ? generate_minitest_request_test(suggestion) : generate_flash_messages_test(suggestion)
411
+ when :redirects
412
+ minitest? ? generate_minitest_request_test(suggestion) : generate_redirects_test(suggestion)
413
+ when :json_responses
414
+ minitest? ? generate_minitest_request_test(suggestion) : generate_json_responses_test(suggestion)
415
+ when :create_view, :edit_update, :delete, :view_details
416
+ minitest? ? generate_minitest_request_test(suggestion) : generate_integration_test(suggestion)
417
+ when :get_list, :get_single, :put_update
418
+ minitest? ? generate_minitest_request_test(suggestion) : generate_api_test(suggestion)
419
+ when :create_view_feature, :edit_feature, :delete_feature, :view_details_feature
420
+ minitest? ? generate_minitest_request_test(suggestion) : generate_feature_test(suggestion)
421
+ when :stimulus
422
+ minitest? ? generate_minitest_request_test(suggestion) : generate_stimulus_test(suggestion)
423
+ when :cable
424
+ minitest? ? generate_minitest_request_test(suggestion) : generate_cable_test(suggestion)
425
+ end
426
+ end
427
+
428
+ private
429
+
430
+ def generate_stimulus_test(suggestion)
431
+ controller = suggestion[:controller]
432
+ targets = suggestion[:targets] || []
433
+ values = suggestion[:values] || {}
434
+ classes = suggestion[:classes] || []
435
+ actions = suggestion[:actions] || []
436
+
437
+ lines = [
438
+ "# frozen_string_literal: true",
439
+ "",
440
+ spec_helper_require,
441
+ "",
442
+ "RSpec.describe \"##{controller}\", type: :stimulus do",
443
+ " describe \"lifecycle\" do",
444
+ " it \"connects without error\" do",
445
+ " controller = #{controller.camelize}Controller.new",
446
+ " expect { controller.connect() }.not_to raise_error",
447
+ " end",
448
+ "",
449
+ " it \"disconnects without error\" do",
450
+ " controller = #{controller.camelize}Controller.new",
451
+ " expect { controller.disconnect() }.not_to raise_error",
452
+ " end",
453
+ " end",
454
+ "",
455
+ ]
456
+
457
+ if targets.any?
458
+ lines << " describe \"targets\" do"
459
+ targets.each do |target|
460
+ lines << " it \"has a #{target} target\" do"
461
+ lines << " expect(controller).to respond_to(:#{target}_target)"
462
+ lines << " end"
463
+ lines << ""
464
+ end
465
+ lines << " end"
466
+ lines << ""
467
+ end
468
+
469
+ if values.any?
470
+ lines << " describe \"values\" do"
471
+ values.each do |name, type|
472
+ lines << " it \"has #{name} value\" do"
473
+ lines << " expect(controller).to respond_to(:#{name})"
474
+ lines << " end"
475
+ lines << ""
476
+ end
477
+ lines << " end"
478
+ lines << ""
479
+ end
480
+
481
+ if classes.any?
482
+ lines << " describe \"CSS classes\" do"
483
+ classes.each do |cls|
484
+ lines << " it \"has #{cls} CSS class\" do"
485
+ lines << " expect(controller).to respond_to(:#{cls}_class)"
486
+ lines << " end"
487
+ lines << ""
488
+ end
489
+ lines << " end"
490
+ lines << ""
491
+ end
492
+
493
+ if actions.any?
494
+ lines << " describe \"actions\" do"
495
+ actions.each do |action|
496
+ lines << " it \"responds to #{action}\" do"
497
+ lines << " expect(controller).to respond_to(:#{action})"
498
+ lines << " end"
499
+ lines << ""
500
+ end
501
+ lines << " end"
502
+ lines << ""
503
+ end
504
+
505
+ lines << "end"
506
+ lines << ""
507
+
508
+ write_test_file(controller, lines.join("\n"))
509
+ end
510
+
511
+ def generate_cable_test(suggestion)
512
+ channel = suggestion[:channel]
513
+
514
+ test_content = <<~TEST
515
+ # frozen_string_literal: true
516
+
517
+ #{spec_helper_require}
518
+
519
+ RSpec.describe "ApplicationCable::#{channel}", type: :cable do
520
+ let(:connection) { ApplicationCable::Connection.new }
521
+ let(:subscription) { connection.subscriptions["#{channel}"] }
522
+
523
+ describe "connection" do
524
+ it "connects successfully" do
525
+ expect {
526
+ connection.connect
527
+ }.to change { connection.subscriptions }.from({}).to({})
528
+ end
529
+
530
+ it "disconnects successfully" do
531
+ connection.connect
532
+ expect {
533
+ connection.disconnect
534
+ }.to change { connection.subscriptions }.to({})
535
+ end
536
+ end
537
+
538
+ describe "subscriptions" do
539
+ it "subscribes to #{channel}" do
540
+ expect(subscription).to receive(:subscribe)
541
+ connection.subscribe("#{channel}")
542
+ end
543
+
544
+ it "unsubscribes from #{channel}" do
545
+ expect(subscription).to receive(:unsubscribe)
546
+ connection.unsubscribe("#{channel}")
547
+ end
548
+ end
549
+
550
+ describe "broadcast" do
551
+ it "broadcasts to #{channel}" do
552
+ expect(subscription).to receive(:broadcast)
553
+ connection.broadcast_to(subscription, { message: "test" })
554
+ end
555
+ end
556
+
557
+ describe "stream_from" do
558
+ it "streams from #{channel}" do
559
+ expect(subscription).to receive(:stream_from)
560
+ connection.stream_from(subscription, :event)
561
+ end
562
+ end
563
+
564
+ describe "presence" do
565
+ it "handles presence subscriptions" do
566
+ expect(subscription).to receive(:presence)
567
+ connection.presence(subscription)
568
+ end
569
+
570
+ it "handles leave subscriptions" do
571
+ expect(subscription).to receive(:leave)
572
+ connection.leave(subscription)
573
+ end
574
+ end
575
+ end
576
+ TEST
577
+
578
+ write_test_file(channel, test_content)
579
+ end
580
+
581
+ def generate_validations_test(model)
582
+ fields = model[:fields]
583
+ validations = model[:validations]
584
+ model_name = model[:model]
585
+
586
+ inner = validations.flat_map do |validation|
587
+ generate_validation_context(validation) + [""]
588
+ end
589
+ inner.pop
590
+
591
+ lines = [
592
+ "# frozen_string_literal: true",
593
+ "",
594
+ spec_helper_require,
595
+ "",
596
+ "RSpec.describe #{model_name}, type: :model do",
597
+ " describe \"validations\" do",
598
+ inner,
599
+ " end",
600
+ "end",
601
+ "",
602
+ ]
603
+
604
+ write_test_file(model_name, write_lines(lines))
605
+ end
606
+
607
+ def generate_validation_context(validation)
608
+ field = validation[:field]
609
+ validators = validation[:validators] || []
610
+
611
+ matchers = validators.map do |v|
612
+ case v.to_s.downcase
613
+ when "presence"
614
+ " it { is_expected.to validate_presence_of(:#{field}) }"
615
+ when "uniqueness"
616
+ " it { is_expected.to validate_uniqueness_of(:#{field}) }"
617
+ when "length"
618
+ " it { is_expected.to validate_length_of(:#{field}) }"
619
+ when "inclusion"
620
+ " it { is_expected.to validate_inclusion_of(:#{field}).in_array([]) }"
621
+ when "format"
622
+ " it { is_expected.to allow_value(\"value\").for(:#{field}) }"
623
+ when "numericality"
624
+ " it { is_expected.to validate_numericality_of(:#{field}) }"
625
+ else
626
+ " it { is_expected.to validate_presence_of(:#{field}) }"
627
+ end
628
+ end
629
+
630
+ [
631
+ " context \"validates #{field}\" do",
632
+ matchers,
633
+ " end",
634
+ ]
635
+ end
636
+
637
+ def generate_validation_messages_test(model)
638
+ test_content = <<~TEST
639
+ # frozen_string_literal: true
640
+
641
+ #{spec_helper_require}
642
+
643
+ RSpec.describe #{model[:model]}, type: :model do
644
+ describe "validation error messages" do
645
+ it "returns custom error messages" do
646
+ record = build(:#{model[:model].downcase}, invalid_data: "value")
647
+ expect(record.errors.full_messages).to be_present
648
+ end
649
+
650
+ it "returns I18n localized messages" do
651
+ record = build(:#{model[:model].downcase}, invalid_data: "value")
652
+ expect(record.errors.full_messages).to be_present
653
+ end
654
+ end
655
+ end
656
+ TEST
657
+
658
+ write_test_file(model[:model], test_content)
659
+ end
660
+
661
+ def generate_has_one_test(model)
662
+ associations = model[:associations].select { |a| a[:type] == :has_one }
663
+ model_name = model[:model]
664
+
665
+ inner = associations.flat_map do |assoc|
666
+ generate_has_one_context(assoc) + [""]
667
+ end
668
+ inner.pop
669
+
670
+ lines = [
671
+ "# frozen_string_literal: true",
672
+ "",
673
+ spec_helper_require,
674
+ "",
675
+ "RSpec.describe #{model_name}, type: :model do",
676
+ " describe \"associations\" do",
677
+ " context \"has_one associations\" do",
678
+ inner,
679
+ " end",
680
+ " end",
681
+ "end",
682
+ "",
683
+ ]
684
+
685
+ write_test_file(model_name, write_lines(lines))
686
+ end
687
+
688
+ def generate_has_one_context(assoc)
689
+ target = assoc[:target]
690
+
691
+ [
692
+ " it { is_expected.to have_one(:#{target.downcase}) }",
693
+ ]
694
+ end
695
+
696
+ def generate_has_many_test(model)
697
+ associations = model[:associations].select { |a| a[:type] == :has_many }
698
+ model_name = model[:model]
699
+
700
+ inner = associations.flat_map do |assoc|
701
+ generate_has_many_context(assoc) + [""]
702
+ end
703
+ inner.pop
704
+
705
+ lines = [
706
+ "# frozen_string_literal: true",
707
+ "",
708
+ spec_helper_require,
709
+ "",
710
+ "RSpec.describe #{model_name}, type: :model do",
711
+ " describe \"associations\" do",
712
+ " context \"has_many associations\" do",
713
+ inner,
714
+ " end",
715
+ " end",
716
+ "end",
717
+ "",
718
+ ]
719
+
720
+ write_test_file(model_name, write_lines(lines))
721
+ end
722
+
723
+ def generate_has_many_context(assoc)
724
+ target = assoc[:target]
725
+
726
+ [
727
+ " it { is_expected.to have_many(:#{target.downcase.pluralize}) }",
728
+ ]
729
+ end
730
+
731
+ def generate_belongs_to_test(model)
732
+ associations = model[:associations].select { |a| a[:type] == :belongs_to }
733
+ model_name = model[:model]
734
+
735
+ inner = associations.flat_map do |assoc|
736
+ generate_belongs_to_context(assoc) + [""]
737
+ end
738
+ inner.pop
739
+
740
+ lines = [
741
+ "# frozen_string_literal: true",
742
+ "",
743
+ spec_helper_require,
744
+ "",
745
+ "RSpec.describe #{model_name}, type: :model do",
746
+ " describe \"associations\" do",
747
+ " context \"belongs_to associations\" do",
748
+ inner,
749
+ " end",
750
+ " end",
751
+ "end",
752
+ "",
753
+ ]
754
+
755
+ write_test_file(model_name, write_lines(lines))
756
+ end
757
+
758
+ def generate_belongs_to_context(assoc)
759
+ target = assoc[:target]
760
+
761
+ [
762
+ " it { is_expected.to belong_to(:#{target.downcase}) }",
763
+ ]
764
+ end
765
+
766
+ def generate_association_validations_test(model)
767
+ associations = model[:associations]
768
+ model_name = model[:model]
769
+
770
+ lines = [
771
+ "# frozen_string_literal: true",
772
+ "",
773
+ spec_helper_require,
774
+ "",
775
+ "RSpec.describe #{model_name}, type: :model do",
776
+ " describe \"associations\" do",
777
+ " context \"association validations\" do",
778
+ " it \"validates associated #{model_name.downcase.pluralize} are valid\" do",
779
+ " record = #{model_name}.new",
780
+ " record.valid?",
781
+ " expect(record.errors).to be_present",
782
+ " end",
783
+ " end",
784
+ " end",
785
+ "end",
786
+ "",
787
+ ]
788
+
789
+ write_test_file(model_name, write_lines(lines))
790
+ end
791
+
792
+ def generate_scopes_test(model)
793
+ scopes = model[:scopes]
794
+ model_name = model[:model]
795
+ columns = model[:columns] || {}
796
+
797
+ scope_tests = scopes.flat_map do |scope|
798
+ generate_scope_lines(scope, model_name, columns: columns) + [""]
799
+ end
800
+ scope_tests.pop
801
+
802
+ lines = [
803
+ "# frozen_string_literal: true",
804
+ "",
805
+ spec_helper_require,
806
+ "",
807
+ "RSpec.describe #{model_name}, type: :model do",
808
+ " describe \"scopes\" do",
809
+ scope_tests,
810
+ " end",
811
+ "end",
812
+ "",
813
+ ]
814
+
815
+ write_test_file(model_name, write_lines(lines))
816
+ end
817
+
818
+ def generate_scope_lines(scope, model_name, columns: {})
819
+ name = scope[:name]
820
+
821
+ extra = {}
822
+ boolean_cols = columns.select { |_, ci| ci[:type] == :boolean }.keys
823
+ if boolean_cols.include?(name.to_s)
824
+ extra[name.to_sym] = true
825
+ elsif name.to_s == "active" && boolean_cols.include?("deleted")
826
+ extra[:deleted] = false
827
+ elsif name.to_s == "deleted" && boolean_cols.include?("deleted")
828
+ extra[:deleted] = true
829
+ end
830
+ attrs = factory_attributes(model_name, columns: columns, extra_attrs: extra)
831
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
832
+
833
+ [
834
+ " it \"returns #{name} results\" do",
835
+ " record = described_class.create!(#{attrs_str})",
836
+ " expect(described_class.#{name}).to be_present",
837
+ " end",
838
+ ]
839
+ end
840
+
841
+ def generate_scoped_arguments_test(model)
842
+ scopes = model[:scopes]
843
+ model_name = model[:model]
844
+
845
+ inner = scopes.flat_map do |scope|
846
+ generate_scoped_arguments_context(scope, model_name) + [""]
847
+ end
848
+ inner.pop
849
+
850
+ lines = [
851
+ "# frozen_string_literal: true",
852
+ "",
853
+ spec_helper_require,
854
+ "",
855
+ "RSpec.describe #{model_name}, type: :model do",
856
+ " describe \"scopes with arguments\" do",
857
+ inner,
858
+ " end",
859
+ "end",
860
+ "",
861
+ ]
862
+
863
+ write_test_file(model_name, write_lines(lines))
864
+ end
865
+
866
+ def generate_scoped_arguments_context(scope, model_name)
867
+ name = scope[:name]
868
+ args = scope[:args]
869
+
870
+ [
871
+ " it \"accepts #{name} with #{args.join(", ")}\" do",
872
+ " expect(described_class.#{name}(#{args.join(", ")})).to be_present",
873
+ " end",
874
+ ]
875
+ end
876
+
877
+ def generate_before_save_test(model)
878
+ callbacks = model[:callbacks].select { |c| c[:type] == :before_save }
879
+ model_name = model[:model]
880
+ columns = model[:columns] || {}
881
+
882
+ inner = callbacks.flat_map do |cb|
883
+ generate_before_save_context(cb, model_name, columns: columns) + [""]
884
+ end
885
+ inner.pop
886
+
887
+ lines = [
888
+ "# frozen_string_literal: true",
889
+ "",
890
+ spec_helper_require,
891
+ "",
892
+ "RSpec.describe #{model_name}, type: :model do",
893
+ " describe \"callbacks\" do",
894
+ " context \"before_save callbacks\" do",
895
+ inner,
896
+ " end",
897
+ " end",
898
+ "end",
899
+ "",
900
+ ]
901
+
902
+ write_test_file(model_name, write_lines(lines))
903
+ end
904
+
905
+ def generate_before_save_context(callback, model_name, columns: {})
906
+ var = model_name.underscore
907
+ attrs = factory_attributes(model_name, columns: columns)
908
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
909
+ [
910
+ " it \"executes before_save callback\" do",
911
+ " #{var} = described_class.new(#{attrs_str})",
912
+ " expect { #{var}.save(validate: false) }.not_to raise_error",
913
+ " end",
914
+ ]
915
+ end
916
+
917
+ def generate_after_save_test(model)
918
+ callbacks = model[:callbacks].select { |c| c[:type] == :after_save }
919
+ model_name = model[:model]
920
+ columns = model[:columns] || {}
921
+
922
+ inner = callbacks.flat_map do |cb|
923
+ generate_after_save_context(cb, model_name, columns: columns) + [""]
924
+ end
925
+ inner.pop
926
+
927
+ lines = [
928
+ "# frozen_string_literal: true",
929
+ "",
930
+ spec_helper_require,
931
+ "",
932
+ "RSpec.describe #{model_name}, type: :model do",
933
+ " describe \"callbacks\" do",
934
+ " context \"after_save callbacks\" do",
935
+ inner,
936
+ " end",
937
+ " end",
938
+ "end",
939
+ "",
940
+ ]
941
+
942
+ write_test_file(model_name, write_lines(lines))
943
+ end
944
+
945
+ def generate_after_save_context(callback, model_name, columns: {})
946
+ var = model_name.underscore
947
+ attrs = factory_attributes(model_name, columns: columns)
948
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
949
+ [
950
+ " it \"executes after_save callback\" do",
951
+ " #{var} = described_class.new(#{attrs_str})",
952
+ " expect { #{var}.save }.not_to raise_error",
953
+ " end",
954
+ ]
955
+ end
956
+
957
+ def generate_before_destroy_test(model)
958
+ callbacks = model[:callbacks].select { |c| c[:type] == :before_destroy }
959
+ model_name = model[:model]
960
+ columns = model[:columns] || {}
961
+
962
+ inner = callbacks.flat_map do |cb|
963
+ generate_before_destroy_context(cb, model_name, columns: columns) + [""]
964
+ end
965
+ inner.pop
966
+
967
+ lines = [
968
+ "# frozen_string_literal: true",
969
+ "",
970
+ spec_helper_require,
971
+ "",
972
+ "RSpec.describe #{model_name}, type: :model do",
973
+ " describe \"callbacks\" do",
974
+ " context \"before_destroy callbacks\" do",
975
+ inner,
976
+ " end",
977
+ " end",
978
+ "end",
979
+ "",
980
+ ]
981
+
982
+ write_test_file(model_name, write_lines(lines))
983
+ end
984
+
985
+ def generate_before_destroy_context(callback, model_name, columns: {})
986
+ var = model_name.underscore
987
+ attrs = factory_attributes(model_name, columns: columns)
988
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
989
+ [
990
+ " it \"executes before_destroy callback\" do",
991
+ " #{var} = described_class.new(#{attrs_str})",
992
+ " expect { #{var}.destroy }.not_to raise_error",
993
+ " end",
994
+ ]
995
+ end
996
+
997
+ def generate_after_destroy_test(model)
998
+ callbacks = model[:callbacks].select { |c| c[:type] == :after_destroy }
999
+ model_name = model[:model]
1000
+ columns = model[:columns] || {}
1001
+
1002
+ inner = callbacks.flat_map do |cb|
1003
+ generate_after_destroy_context(cb, model_name, columns: columns) + [""]
1004
+ end
1005
+ inner.pop
1006
+
1007
+ lines = [
1008
+ "# frozen_string_literal: true",
1009
+ "",
1010
+ spec_helper_require,
1011
+ "",
1012
+ "RSpec.describe #{model_name}, type: :model do",
1013
+ " describe \"callbacks\" do",
1014
+ " context \"after_destroy callbacks\" do",
1015
+ inner,
1016
+ " end",
1017
+ " end",
1018
+ "end",
1019
+ "",
1020
+ ]
1021
+
1022
+ write_test_file(model_name, write_lines(lines))
1023
+ end
1024
+
1025
+ def generate_after_destroy_context(callback, model_name, columns: {})
1026
+ var = model_name.underscore
1027
+ attrs = factory_attributes(model_name, columns: columns)
1028
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
1029
+ [
1030
+ " it \"executes after_destroy callback\" do",
1031
+ " #{var} = described_class.new(#{attrs_str})",
1032
+ " expect { #{var}.destroy }.not_to raise_error",
1033
+ " end",
1034
+ ]
1035
+ end
1036
+
1037
+ def generate_custom_methods_test(model)
1038
+ methods = model[:custom_methods]
1039
+ model_name = model[:model]
1040
+ columns = model[:columns] || {}
1041
+
1042
+ inner = methods.flat_map do |m|
1043
+ generate_custom_method_test(m, model_name, columns: columns) + [""]
1044
+ end
1045
+ inner.pop
1046
+
1047
+ lines = [
1048
+ "# frozen_string_literal: true",
1049
+ "",
1050
+ spec_helper_require,
1051
+ "",
1052
+ "RSpec.describe #{model_name}, type: :model do",
1053
+ " describe \"custom methods\" do",
1054
+ inner,
1055
+ " end",
1056
+ "end",
1057
+ "",
1058
+ ]
1059
+
1060
+ write_test_file(model_name, write_lines(lines))
1061
+ end
1062
+
1063
+ def generate_custom_method_test(method, model_name, columns: {})
1064
+ name = method.is_a?(Hash) ? method[:name] : method
1065
+ is_class_method = method.is_a?(Hash) && method[:class_method]
1066
+ attrs = factory_attributes(model_name, columns: columns)
1067
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
1068
+
1069
+ receiver = is_class_method ? "described_class" : "described_class.new(#{attrs_str})"
1070
+
1071
+ matcher = case name.to_s
1072
+ when /\?$/ then "be(true).or be(false)"
1073
+ when /^ransackable/ then "be_a(Array)"
1074
+ else "be_present"
1075
+ end
1076
+
1077
+ [
1078
+ " it \"returns #{name} result\" do",
1079
+ " expect(#{receiver}.#{name}).to #{matcher}",
1080
+ " end",
1081
+ ]
1082
+ end
1083
+
1084
+ def generate_factories_test(model)
1085
+ model_name = model[:model]
1086
+
1087
+ inner = [
1088
+ " it \"has a valid factory\" do",
1089
+ " expect(build(:#{factory_name(model_name)})).to be_valid",
1090
+ " end",
1091
+ ]
1092
+
1093
+ lines = [
1094
+ "# frozen_string_literal: true",
1095
+ "",
1096
+ spec_helper_require,
1097
+ "",
1098
+ "RSpec.describe #{model_name} do",
1099
+ inner,
1100
+ "end",
1101
+ "",
1102
+ ]
1103
+
1104
+ write_test_file(model_name, write_lines(lines))
1105
+ end
1106
+
1107
+ def generate_serialization_test(model)
1108
+ test_content = <<~TEST
1109
+ # frozen_string_literal: true
1110
+
1111
+ #{spec_helper_require}
1112
+
1113
+ RSpec.describe #{model[:model]}, type: :model do
1114
+ describe "serialization" do
1115
+ it "serializes to JSON" do
1116
+ expect(build(:#{factory_name(model[:model])}).to_json).to be_present
1117
+ end
1118
+
1119
+ it "serializes to YAML" do
1120
+ expect(build(:#{factory_name(model[:model])}).to_yaml).to be_present
1121
+ end
1122
+ end
1123
+ end
1124
+ TEST
1125
+
1126
+ write_test_file(model[:model], test_content)
1127
+ end
1128
+
1129
+ def generate_delegation_test(model)
1130
+ test_content = <<~TEST
1131
+ # frozen_string_literal: true
1132
+
1133
+ #{spec_helper_require}
1134
+
1135
+ RSpec.describe #{model[:model]}, type: :model do
1136
+ describe "delegation" do
1137
+ it "delegates method to association" do
1138
+ expect(build(:#{factory_name(model[:model])})).to delegate(:method_name).to(:association_name)
1139
+ end
1140
+
1141
+ it "delegates method with prefix" do
1142
+ expect(build(:#{factory_name(model[:model])})).to delegate(:method_name).to(:association_name, prefix: true)
1143
+ end
1144
+ end
1145
+ end
1146
+ TEST
1147
+
1148
+ write_test_file(model[:model], test_content)
1149
+ end
1150
+
1151
+ def generate_get_index_test(controller)
1152
+ route_plural = ctrl_route(controller[:controller])
1153
+ model_name = controller[:model]
1154
+ columns = controller[:columns] || {}
1155
+
1156
+ attrs1 = factory_attributes(model_name, columns: columns)
1157
+ attrs2 = factory_attributes(model_name, columns: columns)
1158
+ attrs1_str = attrs1.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
1159
+ attrs2_str = attrs2.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
1160
+
1161
+ test_lines = [
1162
+ "# frozen_string_literal: true",
1163
+ "",
1164
+ spec_helper_require,
1165
+ "",
1166
+ "RSpec.describe \"#{controller[:controller]}\", type: :request do",
1167
+ ]
1168
+
1169
+ if devise?
1170
+ test_lines << " include Devise::Test::IntegrationHelpers"
1171
+ model = devise_model_name
1172
+ attrs = {email: "test@example.com", password: "password", password_confirmation: "password"}
1173
+ attrs_str = attrs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
1174
+ test_lines << " let(:current_user) { #{model}.create!(#{attrs_str}) }"
1175
+ test_lines << " before { sign_in current_user }"
1176
+ test_lines << ""
1177
+ end
1178
+
1179
+ test_lines += [
1180
+ " describe \"GET index action\" do",
1181
+ " let!(:record1) { #{model_name}.create!(#{attrs1_str}) }",
1182
+ " let!(:record2) { #{model_name}.create!(#{attrs2_str}) }",
1183
+ "",
1184
+ " it \"returns success response\" do",
1185
+ " get #{route_plural}_path",
1186
+ " expect(response).to have_http_status(:ok)",
1187
+ " end",
1188
+ "",
1189
+ " it \"renders index template\" do",
1190
+ " get #{route_plural}_path",
1191
+ " expect(response).to render_template(:index)",
1192
+ " end",
1193
+ "",
1194
+ " it \"passes correct instance variables\" do",
1195
+ " get #{route_plural}_path",
1196
+ " expect(assigns(:#{route_plural})).to be_present",
1197
+ " end",
1198
+ " end",
1199
+ "end",
1200
+ "",
1201
+ ]
1202
+
1203
+ write_test_file(controller[:controller], write_lines(test_lines))
1204
+ end
1205
+
1206
+ def generate_index_pagination_test(controller)
1207
+ test_content = <<~TEST
1208
+ # frozen_string_literal: true
1209
+
1210
+ #{spec_helper_require}
1211
+
1212
+ RSpec.describe "#{controller[:controller]}", type: :request do
1213
+ describe "GET index action" do
1214
+ context "with pagination" do
1215
+ it "returns paginated results" do
1216
+ get #{ctrl_route(controller[:controller])}_path
1217
+ expect(response).to have_http_status(:ok)
1218
+ end
1219
+
1220
+ it "passes page parameter" do
1221
+ get "#{ctrl_route(controller[:controller])}_path?page=2"
1222
+ expect(response).to have_http_status(:ok)
1223
+ end
1224
+ end
1225
+ end
1226
+ end
1227
+ TEST
1228
+
1229
+ write_test_file(controller[:controller], test_content)
1230
+ end
1231
+
1232
+ def generate_index_sorting_test(controller)
1233
+ test_content = <<~TEST
1234
+ # frozen_string_literal: true
1235
+
1236
+ #{spec_helper_require}
1237
+
1238
+ RSpec.describe "#{controller[:controller]}", type: :request do
1239
+ describe "GET index action" do
1240
+ context "with sorting" do
1241
+ it "sorts by default" do
1242
+ get #{ctrl_route(controller[:controller])}_path
1243
+ expect(response).to have_http_status(:ok)
1244
+ end
1245
+
1246
+ it "sorts by custom parameter" do
1247
+ get "#{ctrl_route(controller[:controller])}_path?sort=field"
1248
+ expect(response).to have_http_status(:ok)
1249
+ end
1250
+ end
1251
+ end
1252
+ end
1253
+ TEST
1254
+
1255
+ write_test_file(controller[:controller], test_content)
1256
+ end
1257
+
1258
+ def generate_get_show_test(controller)
1259
+ singular = ctrl_base(controller[:controller])
1260
+ route_param = controller[:route_param]
1261
+ model_name = controller[:model]
1262
+ columns = controller[:columns] || {}
1263
+ attrs_str = factory_attributes(model_name, columns: columns).map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
1264
+
1265
+ show_path_arg = route_param ? "record.#{route_param}" : "record"
1266
+
1267
+ test_lines = [
1268
+ "# frozen_string_literal: true",
1269
+ "",
1270
+ spec_helper_require,
1271
+ "",
1272
+ "RSpec.describe \"#{controller[:controller]}\", type: :request do",
1273
+ ] + devise_setup_lines + [
1274
+ " describe \"GET show action\" do",
1275
+ " let!(:record) { #{model_name}.create!(#{attrs_str}) }",
1276
+ "",
1277
+ " it \"returns success response\" do",
1278
+ " get #{singular}_path(#{show_path_arg})",
1279
+ " expect(response).to have_http_status(:ok)",
1280
+ " end",
1281
+ "",
1282
+ " it \"renders show template\" do",
1283
+ " get #{singular}_path(#{show_path_arg})",
1284
+ " expect(response).to render_template(:show)",
1285
+ " end",
1286
+ "",
1287
+ " it \"passes correct instance variables\" do",
1288
+ " get #{singular}_path(#{show_path_arg})",
1289
+ " expect(assigns(:#{singular})).to be_present",
1290
+ " end",
1291
+ " end",
1292
+ "end",
1293
+ "",
1294
+ ]
1295
+
1296
+ write_test_file(controller[:controller], write_lines(test_lines))
1297
+ end
1298
+
1299
+ def generate_show_with_associations_test(controller)
1300
+ test_content = <<~TEST
1301
+ # frozen_string_literal: true
1302
+
1303
+ #{spec_helper_require}
1304
+
1305
+ RSpec.describe "#{controller[:controller]}", type: :request do
1306
+ describe "GET show action" do
1307
+ context "with associations" do
1308
+ it "includes associated records" do
1309
+ get #{ctrl_base(controller[:controller]).singularize}_path(1)
1310
+ expect(response).to have_http_status(:ok)
1311
+ end
1312
+ end
1313
+ end
1314
+ end
1315
+ TEST
1316
+
1317
+ write_test_file(controller[:controller], test_content)
1318
+ end
1319
+
1320
+ def generate_get_new_test(controller)
1321
+ test_content = <<~TEST
1322
+ # frozen_string_literal: true
1323
+
1324
+ #{spec_helper_require}
1325
+
1326
+ RSpec.describe "#{controller[:controller]}", type: :request do
1327
+ describe "GET new action" do
1328
+ it "returns success response" do
1329
+ get new_#{ctrl_base(controller[:controller])}_path
1330
+ expect(response).to have_http_status(:ok)
1331
+ end
1332
+
1333
+ it "renders new template" do
1334
+ get "new_#{ctrl_base(controller[:controller])}_path"
1335
+ expect(response).to render_template(:new)
1336
+ end
1337
+
1338
+ it "passes correct instance variables" do
1339
+ get "new_#{ctrl_base(controller[:controller])}_path"
1340
+ expect(assigns(:#{ctrl_base(controller[:controller])})).to be_a(#{ctrl_base(controller[:controller]).camelize})
1341
+ end
1342
+ end
1343
+ end
1344
+ TEST
1345
+
1346
+ write_test_file(controller[:controller], test_content)
1347
+ end
1348
+
1349
+ def generate_new_form_test(controller)
1350
+ test_content = <<~TEST
1351
+ # frozen_string_literal: true
1352
+
1353
+ #{spec_helper_require}
1354
+
1355
+ RSpec.describe "#{controller[:controller]}", type: :request do
1356
+ describe "GET new action" do
1357
+ context "form rendering" do
1358
+ it "renders form with all fields" do
1359
+ get "new_#{ctrl_base(controller[:controller])}_path"
1360
+ expect(response).to render_template(:new)
1361
+ end
1362
+
1363
+ it "includes all form fields" do
1364
+ get "new_#{ctrl_base(controller[:controller])}_path"
1365
+ expect(response.body).to include("form")
1366
+ end
1367
+ end
1368
+ end
1369
+ end
1370
+ TEST
1371
+
1372
+ write_test_file(controller[:controller], test_content)
1373
+ end
1374
+
1375
+ def generate_get_edit_test(controller)
1376
+ test_content = <<~TEST
1377
+ # frozen_string_literal: true
1378
+
1379
+ #{spec_helper_require}
1380
+
1381
+ RSpec.describe "#{controller[:controller]}", type: :request do
1382
+ describe "GET edit action" do
1383
+ it "returns success response" do
1384
+ get edit_#{ctrl_base(controller[:controller])}_path(1)
1385
+ expect(response).to have_http_status(:ok)
1386
+ end
1387
+
1388
+ it "renders edit template" do
1389
+ get "edit_#{ctrl_base(controller[:controller])}_path/#{ctrl_base(controller[:controller])}_id"
1390
+ expect(response).to render_template(:edit)
1391
+ end
1392
+
1393
+ it "passes correct instance variables" do
1394
+ get "edit_#{ctrl_base(controller[:controller])}_path/#{ctrl_base(controller[:controller])}_id"
1395
+ expect(assigns(:#{ctrl_base(controller[:controller])})).to be_a(#{ctrl_base(controller[:controller]).camelize})
1396
+ end
1397
+ end
1398
+ end
1399
+ TEST
1400
+
1401
+ write_test_file(controller[:controller], test_content)
1402
+ end
1403
+
1404
+ def generate_edit_form_test(controller)
1405
+ test_content = <<~TEST
1406
+ # frozen_string_literal: true
1407
+
1408
+ #{spec_helper_require}
1409
+
1410
+ RSpec.describe "#{controller[:controller]}", type: :request do
1411
+ describe "GET edit action" do
1412
+ context "form rendering" do
1413
+ it "renders form with pre-filled data" do
1414
+ get "edit_#{ctrl_base(controller[:controller])}_path/#{ctrl_base(controller[:controller])}_id"
1415
+ expect(response).to render_template(:edit)
1416
+ end
1417
+
1418
+ it "includes pre-filled form fields" do
1419
+ get "edit_#{ctrl_base(controller[:controller])}_path/#{ctrl_base(controller[:controller])}_id"
1420
+ expect(response.body).to include("form")
1421
+ end
1422
+ end
1423
+ end
1424
+ end
1425
+ TEST
1426
+
1427
+ write_test_file(controller[:controller], test_content)
1428
+ end
1429
+
1430
+ def generate_post_create_valid_test(controller)
1431
+ test_content = <<~TEST
1432
+ # frozen_string_literal: true
1433
+
1434
+ #{spec_helper_require}
1435
+
1436
+ RSpec.describe "#{controller[:controller]}", type: :request do
1437
+ describe "POST create action" do
1438
+ context "with valid parameters" do
1439
+ it "creates new record" do
1440
+ post #{ctrl_route(controller[:controller])}_path, params: {
1441
+ "#{ctrl_base(controller[:controller])}": {
1442
+ #{generate_create_params(controller)}
1443
+ }
1444
+ }
1445
+ expect(response).to have_http_status(:created)
1446
+ end
1447
+
1448
+ it "redirects to new record" do
1449
+ post #{ctrl_route(controller[:controller])}_path, params: {
1450
+ "#{ctrl_base(controller[:controller])}": {
1451
+ #{generate_create_params(controller)}
1452
+ }
1453
+ }
1454
+ expect(response).to redirect_to(#{ctrl_route(controller[:controller])}_path)
1455
+ end
1456
+
1457
+ it "assigns correct instance variables" do
1458
+ post #{ctrl_route(controller[:controller])}_path, params: {
1459
+ "#{ctrl_base(controller[:controller])}": {
1460
+ #{generate_create_params(controller)}
1461
+ }
1462
+ }
1463
+ expect(assigns(:#{ctrl_base(controller[:controller])})).to be_a(#{ctrl_base(controller[:controller]).camelize})
1464
+ end
1465
+ end
1466
+ end
1467
+ end
1468
+ TEST
1469
+
1470
+ write_test_file(controller[:controller], test_content)
1471
+ end
1472
+
1473
+ def generate_create_params(controller)
1474
+ controller_factory_attrs(controller)
1475
+ end
1476
+
1477
+ def generate_invalid_params(controller)
1478
+ columns = controller[:columns] || {}
1479
+ first = columns.keys.first
1480
+ return "invalid_field: \"\"" unless first
1481
+
1482
+ "#{first}: \"\""
1483
+ end
1484
+
1485
+ def generate_post_create_invalid_test(controller)
1486
+ test_content = <<~TEST
1487
+ # frozen_string_literal: true
1488
+
1489
+ #{spec_helper_require}
1490
+
1491
+ RSpec.describe "#{controller[:controller]}", type: :request do
1492
+ describe "POST create action" do
1493
+ context "with invalid parameters" do
1494
+ it "returns validation errors" do
1495
+ post #{ctrl_route(controller[:controller])}_path, params: {
1496
+ "#{ctrl_base(controller[:controller])}": {
1497
+ #{generate_invalid_params(controller)}
1498
+ }
1499
+ }
1500
+ expect(response).to have_http_status(:unprocessable_entity)
1501
+ end
1502
+
1503
+ it "renders new template with errors" do
1504
+ post #{ctrl_route(controller[:controller])}_path, params: {
1505
+ "#{ctrl_base(controller[:controller])}": {
1506
+ #{generate_invalid_params(controller)}
1507
+ }
1508
+ }
1509
+ expect(response).to render_template(:new)
1510
+ end
1511
+ end
1512
+ end
1513
+ end
1514
+ TEST
1515
+
1516
+ write_test_file(controller[:controller], test_content)
1517
+ end
1518
+
1519
+ def generate_post_create_redirect_test(controller)
1520
+ test_content = <<~TEST
1521
+ # frozen_string_literal: true
1522
+
1523
+ #{spec_helper_require}
1524
+
1525
+ RSpec.describe "#{controller[:controller]}", type: :request do
1526
+ describe "POST create action" do
1527
+ context "with redirect" do
1528
+ it "redirects to new record" do
1529
+ post #{ctrl_route(controller[:controller])}_path, params: {
1530
+ "#{ctrl_base(controller[:controller])}": {
1531
+ #{generate_create_params(controller)}
1532
+ }
1533
+ }
1534
+ expect(response).to redirect_to(#{ctrl_route(controller[:controller])}_path)
1535
+ end
1536
+
1537
+ it "sets flash messages" do
1538
+ post #{ctrl_route(controller[:controller])}_path, params: {
1539
+ "#{ctrl_base(controller[:controller])}": {
1540
+ #{generate_create_params(controller)}
1541
+ }
1542
+ }
1543
+ expect(flash[:notice]).to be_present
1544
+ end
1545
+ end
1546
+ end
1547
+ end
1548
+ TEST
1549
+
1550
+ write_test_file(controller[:controller], test_content)
1551
+ end
1552
+
1553
+ def generate_put_patch_update_valid_test(controller)
1554
+ test_content = <<~TEST
1555
+ # frozen_string_literal: true
1556
+
1557
+ #{spec_helper_require}
1558
+
1559
+ RSpec.describe "#{controller[:controller]}", type: :request do
1560
+ describe "PUT/PATCH update action" do
1561
+ context "with valid parameters" do
1562
+ it "updates the record" do
1563
+ put #{ctrl_base(controller[:controller]).singularize}_path(1), params: {
1564
+ "#{ctrl_base(controller[:controller])}": {
1565
+ #{generate_update_params(controller)}
1566
+ }
1567
+ }
1568
+ expect(response).to have_http_status(:ok)
1569
+ end
1570
+
1571
+ it "redirects to updated record" do
1572
+ put #{ctrl_base(controller[:controller]).singularize}_path(1), params: {
1573
+ "#{ctrl_base(controller[:controller])}": {
1574
+ #{generate_update_params(controller)}
1575
+ }
1576
+ }
1577
+ expect(response).to redirect_to(#{ctrl_route(controller[:controller])}_path)
1578
+ end
1579
+
1580
+ it "updates the record in database" do
1581
+ record = create(:#{factory_name(controller[:model])})
1582
+ new_attrs = attributes_for(:#{factory_name(controller[:model])})
1583
+ put #{ctrl_base(controller[:controller]).singularize}_path(record.id), params: {
1584
+ "#{ctrl_base(controller[:controller])}": new_attrs
1585
+ }
1586
+ record.reload
1587
+ expect(record).to have_attributes(new_attrs)
1588
+ end
1589
+ end
1590
+ end
1591
+ end
1592
+ TEST
1593
+
1594
+ write_test_file(controller[:controller], test_content)
1595
+ end
1596
+
1597
+ def generate_update_params(controller)
1598
+ controller_factory_attrs(controller)
1599
+ end
1600
+
1601
+ def generate_put_patch_update_invalid_test(controller)
1602
+ test_content = <<~TEST
1603
+ # frozen_string_literal: true
1604
+
1605
+ #{spec_helper_require}
1606
+
1607
+ RSpec.describe "#{controller[:controller]}", type: :request do
1608
+ describe "PUT/PATCH update action" do
1609
+ context "with invalid parameters" do
1610
+ it "returns validation errors" do
1611
+ put #{ctrl_base(controller[:controller]).singularize}_path(1), params: {
1612
+ "#{ctrl_base(controller[:controller])}": {
1613
+ #{generate_invalid_params(controller)}
1614
+ }
1615
+ }
1616
+ expect(response).to have_http_status(:unprocessable_entity)
1617
+ end
1618
+
1619
+ it "renders edit template with errors" do
1620
+ put #{ctrl_base(controller[:controller]).singularize}_path(1), params: {
1621
+ "#{ctrl_base(controller[:controller])}": {
1622
+ #{generate_invalid_params(controller)}
1623
+ }
1624
+ }
1625
+ expect(response).to render_template(:edit)
1626
+ end
1627
+ end
1628
+ end
1629
+ end
1630
+ TEST
1631
+
1632
+ write_test_file(controller[:controller], test_content)
1633
+ end
1634
+
1635
+ def generate_put_patch_update_redirect_test(controller)
1636
+ test_content = <<~TEST
1637
+ # frozen_string_literal: true
1638
+
1639
+ #{spec_helper_require}
1640
+
1641
+ RSpec.describe "#{controller[:controller]}", type: :request do
1642
+ describe "PUT/PATCH update action" do
1643
+ context "with redirect" do
1644
+ it "redirects to updated record" do
1645
+ put #{ctrl_base(controller[:controller]).singularize}_path(1), params: {
1646
+ "#{ctrl_base(controller[:controller])}": {
1647
+ #{generate_update_params(controller)}
1648
+ }
1649
+ }
1650
+ expect(response).to redirect_to(#{ctrl_route(controller[:controller])}_path)
1651
+ end
1652
+
1653
+ it "sets flash messages" do
1654
+ put #{ctrl_base(controller[:controller]).singularize}_path(1), params: {
1655
+ "#{ctrl_base(controller[:controller])}": {
1656
+ #{generate_update_params(controller)}
1657
+ }
1658
+ }
1659
+ expect(flash[:notice]).to be_present
1660
+ end
1661
+ end
1662
+ end
1663
+ end
1664
+ TEST
1665
+
1666
+ write_test_file(controller[:controller], test_content)
1667
+ end
1668
+
1669
+ def generate_delete_destroy_test(controller)
1670
+ test_content = <<~TEST
1671
+ # frozen_string_literal: true
1672
+
1673
+ #{spec_helper_require}
1674
+
1675
+ RSpec.describe "#{controller[:controller]}", type: :request do
1676
+ describe "DELETE destroy action" do
1677
+ it "deletes the record" do
1678
+ delete #{ctrl_base(controller[:controller]).singularize}_path(1)
1679
+ expect(response).to have_http_status(:no_content)
1680
+ end
1681
+
1682
+ it "removes the record from database" do
1683
+ record = create(:#{factory_name(controller[:model])})
1684
+ expect {
1685
+ delete #{ctrl_base(controller[:controller]).singularize}_path(record.id)
1686
+ }.to change(#{ctrl_route(controller[:controller])}, :count).by(-1)
1687
+ end
1688
+ end
1689
+ end
1690
+ TEST
1691
+
1692
+ write_test_file(controller[:controller], test_content)
1693
+ end
1694
+
1695
+ def generate_delete_destroy_redirect_test(controller)
1696
+ test_content = <<~TEST
1697
+ # frozen_string_literal: true
1698
+
1699
+ #{spec_helper_require}
1700
+
1701
+ RSpec.describe "#{controller[:controller]}", type: :request do
1702
+ describe "DELETE destroy action" do
1703
+ context "with redirect" do
1704
+ it "redirects after destroy" do
1705
+ delete #{ctrl_base(controller[:controller]).singularize}_path(1)
1706
+ expect(response).to redirect_to(#{ctrl_route(controller[:controller])}_path)
1707
+ end
1708
+
1709
+ it "sets flash messages" do
1710
+ delete #{ctrl_base(controller[:controller]).singularize}_path(1)
1711
+ expect(flash[:notice]).to be_present
1712
+ end
1713
+ end
1714
+ end
1715
+ end
1716
+ TEST
1717
+
1718
+ write_test_file(controller[:controller], test_content)
1719
+ end
1720
+
1721
+ def generate_strong_parameters_permit_test(controller)
1722
+ factory = factory_name(controller[:model])
1723
+ test_content = <<~TEST
1724
+ # frozen_string_literal: true
1725
+
1726
+ #{spec_helper_require}
1727
+
1728
+ RSpec.describe "#{controller[:controller]}", type: :request do
1729
+ describe "strong parameters" do
1730
+ it "persists submitted attributes on create" do
1731
+ expect {
1732
+ post #{ctrl_route(controller[:controller])}_path, params: {
1733
+ "#{ctrl_base(controller[:controller])}": attributes_for(:#{factory})
1734
+ }
1735
+ }.to change(#{ctrl_route(controller[:controller])}, :count).by(1)
1736
+ end
1737
+ end
1738
+ end
1739
+ TEST
1740
+
1741
+ write_test_file(controller[:controller], test_content)
1742
+ end
1743
+
1744
+ def generate_strong_parameters_deny_test(controller)
1745
+ factory = factory_name(controller[:model])
1746
+ test_content = <<~TEST
1747
+ # frozen_string_literal: true
1748
+
1749
+ #{spec_helper_require}
1750
+
1751
+ RSpec.describe "#{controller[:controller]}", type: :request do
1752
+ describe "strong parameters" do
1753
+ it "ignores non-permitted attributes" do
1754
+ post #{ctrl_route(controller[:controller])}_path, params: {
1755
+ "#{ctrl_base(controller[:controller])}": attributes_for(:#{factory}).merge(admin: true)
1756
+ }
1757
+ expect(#{ctrl_route(controller[:controller])}.last.attributes["admin"]).to be_nil.or be_falsy
1758
+ end
1759
+ end
1760
+ end
1761
+ TEST
1762
+
1763
+ write_test_file(controller[:controller], test_content)
1764
+ end
1765
+
1766
+ def generate_flash_messages_test(controller)
1767
+ test_content = <<~TEST
1768
+ # frozen_string_literal: true
1769
+
1770
+ #{spec_helper_require}
1771
+
1772
+ RSpec.describe "#{controller[:controller]}", type: :request do
1773
+ describe "flash messages" do
1774
+ it "sets notice flash" do
1775
+ post #{ctrl_route(controller[:controller])}_path, params: {
1776
+ "#{ctrl_base(controller[:controller])}": {
1777
+ #{generate_create_params(controller)}
1778
+ }
1779
+ }
1780
+ expect(flash[:notice]).to be_present
1781
+ end
1782
+
1783
+ it "sets alert flash" do
1784
+ post #{ctrl_route(controller[:controller])}_path, params: {
1785
+ "#{ctrl_base(controller[:controller])}": {
1786
+ #{generate_invalid_params(controller)}
1787
+ }
1788
+ }
1789
+ expect(flash[:alert]).to be_present
1790
+ end
1791
+ end
1792
+ end
1793
+ TEST
1794
+
1795
+ write_test_file(controller[:controller], test_content)
1796
+ end
1797
+
1798
+ def generate_redirects_test(controller)
1799
+ test_content = <<~TEST
1800
+ # frozen_string_literal: true
1801
+
1802
+ #{spec_helper_require}
1803
+
1804
+ RSpec.describe "#{controller[:controller]}", type: :request do
1805
+ describe "redirects" do
1806
+ it "redirects to new record" do
1807
+ post #{ctrl_route(controller[:controller])}_path, params: {
1808
+ "#{ctrl_base(controller[:controller])}": {
1809
+ #{generate_create_params(controller)}
1810
+ }
1811
+ }
1812
+ expect(response).to redirect_to(#{ctrl_route(controller[:controller])}_path)
1813
+ end
1814
+
1815
+ it "redirects to edit record" do
1816
+ put #{ctrl_base(controller[:controller]).singularize}_path(1), params: {
1817
+ "#{ctrl_base(controller[:controller])}": {
1818
+ #{generate_update_params(controller)}
1819
+ }
1820
+ }
1821
+ expect(response).to redirect_to(#{ctrl_route(controller[:controller])}_path)
1822
+ end
1823
+ end
1824
+ end
1825
+ TEST
1826
+
1827
+ write_test_file(controller[:controller], test_content)
1828
+ end
1829
+
1830
+ def generate_json_responses_test(controller)
1831
+ test_content = <<~TEST
1832
+ # frozen_string_literal: true
1833
+
1834
+ #{spec_helper_require}
1835
+
1836
+ RSpec.describe "#{controller[:controller]}", type: :request do
1837
+ describe "JSON responses" do
1838
+ it "returns JSON for show action" do
1839
+ get #{ctrl_base(controller[:controller]).singularize}_path(1), headers: { "Accept" => "application/json" }
1840
+ expect(response).to have_http_status(:ok)
1841
+ expect(response.parsed_body).to be_a(Hash)
1842
+ end
1843
+
1844
+ it "returns JSON for create action" do
1845
+ post #{ctrl_route(controller[:controller])}_path, params: {
1846
+ "#{ctrl_base(controller[:controller])}": {
1847
+ #{generate_create_params(controller)}
1848
+ }
1849
+ }, headers: { "Accept" => "application/json" }
1850
+ expect(response).to have_http_status(:created)
1851
+ expect(response.parsed_body).to be_a(Hash)
1852
+ end
1853
+ end
1854
+ end
1855
+ TEST
1856
+
1857
+ write_test_file(controller[:controller], test_content)
1858
+ end
1859
+
1860
+ def generate_integration_test(suggestion)
1861
+ controller = suggestion[:controller]
1862
+ model = suggestion[:model]
1863
+
1864
+ test_lines = [
1865
+ "# frozen_string_literal: true",
1866
+ "",
1867
+ spec_helper_require,
1868
+ "",
1869
+ "RSpec.describe \"#{ctrl_base(controller).capitalize}\", type: :request do",
1870
+ ] + devise_setup_lines + [
1871
+ " describe \"user workflow\" do",
1872
+ " context \"create and view\" do",
1873
+ " it \"creates a new #{model} and redirects to show\" do",
1874
+ " post #{ctrl_route(controller)}_path, params: {",
1875
+ " \"#{ctrl_base(controller)}\": {",
1876
+ " id: 1, #{ctrl_base(controller)}: { name: \"Test\" }",
1877
+ " }",
1878
+ " }",
1879
+ " expect(response).to redirect_to(#{ctrl_route(controller)}_path)",
1880
+ " expect(flash[:notice]).to be_present",
1881
+ " end",
1882
+ "",
1883
+ " it \"displays the created #{model} with all attributes\" do",
1884
+ " post #{ctrl_route(controller)}_path, params: {",
1885
+ " \"#{ctrl_base(controller)}\": {",
1886
+ " id: 1, #{ctrl_base(controller)}: { name: \"Test\" }",
1887
+ " }",
1888
+ " }",
1889
+ " follow_redirect!",
1890
+ " expect(response).to have_http_status(:ok)",
1891
+ " expect(page).to have_content(/#{model}/i)",
1892
+ " end",
1893
+ " end",
1894
+ "",
1895
+ " context \"edit and update\" do",
1896
+ " it \"edits the #{model} and saves changes\" do",
1897
+ " put #{ctrl_base(controller).singularize}_path(1), params: {",
1898
+ " \"#{ctrl_base(controller)}\": {",
1899
+ " id: 1, #{ctrl_base(controller)}: { name: \"Updated\" }",
1900
+ " }",
1901
+ " }",
1902
+ " expect(response).to redirect_to(#{ctrl_base(controller)}_path)",
1903
+ " expect(flash[:notice]).to be_present",
1904
+ " end",
1905
+ "",
1906
+ " it \"displays the updated #{model} with new values\" do",
1907
+ " put #{ctrl_base(controller).singularize}_path(1), params: {",
1908
+ " \"#{ctrl_base(controller)}\": {",
1909
+ " id: 1, #{ctrl_base(controller)}: { name: \"Updated\" }",
1910
+ " }",
1911
+ " }",
1912
+ " follow_redirect!",
1913
+ " expect(response).to have_http_status(:ok)",
1914
+ " expect(page).to have_content(/Updated/i)",
1915
+ " end",
1916
+ " end",
1917
+ "",
1918
+ " context \"delete\" do",
1919
+ " it \"deletes the #{model} and redirects to index\" do",
1920
+ " delete #{ctrl_base(controller).singularize}_path(1)",
1921
+ " expect(response).to redirect_to(#{ctrl_base(controller)}_path)",
1922
+ " expect(flash[:notice]).to be_present",
1923
+ " end",
1924
+ " end",
1925
+ " end",
1926
+ "end",
1927
+ "",
1928
+ ]
1929
+
1930
+ write_test_file("#{controller}_integration", write_lines(test_lines))
1931
+ end
1932
+
1933
+ def write_test_file(subject, content)
1934
+ base = @codebase_path || Dir.pwd
1935
+ base_dir = File.join(base, rspec? ? "spec" : "test")
1936
+ FileUtils.mkdir_p(base_dir)
1937
+ ext = rspec? ? "_spec.rb" : "_test.rb"
1938
+ basename = subject.to_s.underscore.tr("/", "_")
1939
+ suffix = @current_type
1940
+ test_path = File.join(base_dir, "#{basename}_#{suffix}#{ext}")
1941
+
1942
+ commented = content.lines.map { |line|
1943
+ if line.strip.empty?
1944
+ "#\n"
1945
+ elsif line.start_with?("#")
1946
+ "##{line}"
1947
+ else
1948
+ "# #{line}"
1949
+ end
1950
+ }.join
1951
+ File.write(test_path, commented)
1952
+ puts "Generated example: #{test_path}"
1953
+ end
1954
+
1955
+ def generate_api_test(suggestion)
1956
+ controller = suggestion[:controller]
1957
+ model = suggestion[:model]
1958
+
1959
+ test_content = <<~TEST
1960
+ # frozen_string_literal: true
1961
+
1962
+ #{spec_helper_require}
1963
+
1964
+ RSpec.describe "#{ctrl_base(controller).capitalize}", type: :api do
1965
+ describe "GET /#{ctrl_route(controller)}" do
1966
+ it "returns 200 and list of #{model.pluralize}" do
1967
+ get #{ctrl_route(controller)}_path
1968
+ expect(response).to have_http_status(:ok)
1969
+ expect(response.parsed_body).to be_a(Array)
1970
+ expect(response.parsed_body).to all(be_a(#{model}))
1971
+ end
1972
+
1973
+ it "returns 401 when not authenticated" do
1974
+ get #{ctrl_route(controller)}_path
1975
+ expect(response).to have_http_status(:unauthorized)
1976
+ end
1977
+ end
1978
+
1979
+ describe "POST /#{ctrl_route(controller)}" do
1980
+ context "with valid payload" do
1981
+ it "creates a new #{model} and returns 201" do
1982
+ post #{ctrl_route(controller)}_path,
1983
+ params: {
1984
+ "#{ctrl_base(controller)}": {
1985
+ id: 1, #{ctrl_base(controller)}: { name: "Test" }
1986
+ }
1987
+ },
1988
+ headers: { "Accept" => "application/json" }
1989
+ expect(response).to have_http_status(:created)
1990
+ expect(response.parsed_body[:id]).to be_present
1991
+ expect(response.parsed_body[:#{ctrl_base(controller)}]).to be_a(Hash)
1992
+ end
1993
+ end
1994
+
1995
+ context "with invalid payload" do
1996
+ it "returns 422 with validation errors" do
1997
+ post #{ctrl_route(controller)}_path,
1998
+ params: {
1999
+ "#{ctrl_base(controller)}": {
2000
+ #{generate_invalid_params(controller)}
2001
+ }
2002
+ },
2003
+ headers: { "Accept" => "application/json" }
2004
+ expect(response).to have_http_status(:unprocessable_entity)
2005
+ expect(response.parsed_body[:errors]).to be_a(Hash)
2006
+ end
2007
+ end
2008
+ end
2009
+
2010
+ describe "GET /#{ctrl_route(controller)}/:id" do
2011
+ it "returns the #{model} with all attributes" do
2012
+ get #{ctrl_base(controller).singularize}_path(1),
2013
+ headers: { "Accept" => "application/json" }
2014
+ expect(response).to have_http_status(:ok)
2015
+ expect(response.parsed_body).to be_a(Hash)
2016
+ expect(response.parsed_body[:id]).to eq(1)
2017
+ end
2018
+
2019
+ it "returns 404 for non-existent #{model}" do
2020
+ get #{ctrl_base(controller).singularize}_path(999),
2021
+ headers: { "Accept" => "application/json" }
2022
+ expect(response).to have_http_status(:not_found)
2023
+ end
2024
+ end
2025
+
2026
+ describe "PUT /#{ctrl_route(controller)}/:id" do
2027
+ it "updates the #{model} and returns 200" do
2028
+ put #{ctrl_base(controller).singularize}_path(1),
2029
+ params: {
2030
+ "#{ctrl_base(controller)}": {
2031
+ id: 1, #{ctrl_base(controller)}: { name: "Updated" }
2032
+ }
2033
+ },
2034
+ headers: { "Accept" => "application/json" }
2035
+ expect(response).to have_http_status(:ok)
2036
+ expect(response.parsed_body[:id]).to eq(1)
2037
+ end
2038
+ end
2039
+
2040
+ describe "DELETE /#{ctrl_route(controller)}/:id" do
2041
+ it "deletes the #{model} and returns 204" do
2042
+ delete #{ctrl_base(controller).singularize}_path(1),
2043
+ headers: { "Accept" => "application/json" }
2044
+ expect(response).to have_http_status(:no_content)
2045
+ end
2046
+ end
2047
+ end
2048
+ TEST
2049
+
2050
+ write_test_file("#{controller}_api", test_content)
2051
+ end
2052
+
2053
+ def generate_feature_test(suggestion)
2054
+ controller = suggestion[:controller]
2055
+ model = suggestion[:model]
2056
+
2057
+ test_content = <<~TEST
2058
+ # frozen_string_literal: true
2059
+
2060
+ #{spec_helper_require}
2061
+
2062
+ RSpec.describe "#{controller}/#{controller}", type: :feature do
2063
+ let(:#{factory_name(model)}) { create(:#{factory_name(model)}) }
2064
+
2065
+ describe "user story: Create and view #{model}" do
2066
+ it "allows users to create a new #{model} and see it on the dashboard" do
2067
+ visit #{ctrl_route(controller)}_path
2068
+ within("form") do
2069
+ fill_in "#{ctrl_base(controller)} Name", with: "Test #{model}"
2070
+ click_button "Create #{model}"
2071
+ end
2072
+ expect(page).to have_content("#{model} was successfully created.")
2073
+ expect(page).to have_content("Test #{model}")
2074
+ end
2075
+ end
2076
+
2077
+ describe "user story: Edit #{model}" do
2078
+ it "allows users to edit an existing #{model}" do
2079
+ visit #{ctrl_route(controller)}_path
2080
+ click_link "Edit #{model}", match: :first
2081
+ within("form") do
2082
+ fill_in "#{ctrl_base(controller)} Name", with: "Updated #{model}"
2083
+ click_button "Update #{model}"
2084
+ end
2085
+ expect(page).to have_content("#{model} was successfully updated.")
2086
+ expect(page).to have_content("Updated #{model}")
2087
+ end
2088
+ end
2089
+
2090
+ describe "user story: Delete #{model}" do
2091
+ it "allows users to delete a #{model}" do
2092
+ visit #{ctrl_route(controller)}_path
2093
+ within(first(%(a[href*="/#{ctrl_route(controller)}/1"]))) do
2094
+ click_link "Delete #{model}"
2095
+ end
2096
+ expect(page).to have_content("#{model} was successfully deleted.")
2097
+ end
2098
+ end
2099
+
2100
+ describe "user story: View #{model} details" do
2101
+ it "displays all #{model} information on the show page" do
2102
+ visit #{ctrl_route(controller)}_path(#{model}.id)
2103
+ expect(page).to have_content("Show #{model}")
2104
+ expect(page).to have_content("#{model} Name")
2105
+ expect(page).to have_content("#{model} Email")
2106
+ end
2107
+ end
2108
+ end
2109
+ TEST
2110
+
2111
+ write_test_file("#{controller}_feature", test_content)
2112
+ end
2113
+
2114
+ # === Minitest generation methods ===
2115
+
2116
+ def generate_minitest_validations(suggestion)
2117
+ model_name = suggestion[:model]
2118
+ fields = suggestion[:fields] || []
2119
+
2120
+ test_content = <<~MINITEST
2121
+ # frozen_string_literal: true
2122
+
2123
+ require "test_helper"
2124
+
2125
+ class #{model_name}Test < ActiveSupport::TestCase
2126
+ #{fields.map { |f| minitest_validation_test(f, model_name) }.join("\n")}
2127
+ end
2128
+ MINITEST
2129
+
2130
+ write_test_file(model_name, test_content)
2131
+ end
2132
+
2133
+ def minitest_validation_test(field, model_name)
2134
+ if field.is_a?(Hash)
2135
+ fname = field[:field]
2136
+ else
2137
+ fname = field
2138
+ end
2139
+
2140
+ <<~MINITEST
2141
+ test "validates #{fname}" do
2142
+ record = #{model_name}.new
2143
+ record.valid?
2144
+ assert_not record.errors[:#{fname}].empty?, "#{fname} should be validated"
2145
+ end
2146
+ MINITEST
2147
+ end
2148
+
2149
+ def generate_minitest_associations(suggestion)
2150
+ model_name = suggestion[:model]
2151
+ assocs = suggestion[:associations] || []
2152
+
2153
+ test_content = <<~MINITEST
2154
+ # frozen_string_literal: true
2155
+
2156
+ require "test_helper"
2157
+
2158
+ class #{model_name}Test < ActiveSupport::TestCase
2159
+ #{assocs.map { |a| minitest_association_test(a, model_name) }.join("\n")}
2160
+ end
2161
+ MINITEST
2162
+
2163
+ write_test_file(model_name, test_content)
2164
+ end
2165
+
2166
+ def minitest_association_test(assoc, model_name)
2167
+ type = assoc[:type]
2168
+ target = assoc[:target]
2169
+ var = model_name.underscore
2170
+
2171
+ <<~MINITEST
2172
+ test "should #{type} #{target}" do
2173
+ #{var} = #{model_name}.new
2174
+ assert_respond_to #{var}, :#{target.underscore}
2175
+ end
2176
+ MINITEST
2177
+ end
2178
+
2179
+ def generate_minitest_scopes(suggestion)
2180
+ model_name = suggestion[:model]
2181
+ scopes = suggestion[:scopes] || []
2182
+
2183
+ test_content = <<~MINITEST
2184
+ # frozen_string_literal: true
2185
+
2186
+ require "test_helper"
2187
+
2188
+ class #{model_name}Test < ActiveSupport::TestCase
2189
+ #{scopes.map { |s| minitest_scope_test(s, model_name) }.join("\n")}
2190
+ end
2191
+ MINITEST
2192
+
2193
+ write_test_file(model_name, test_content)
2194
+ end
2195
+
2196
+ def minitest_scope_test(scope, model_name)
2197
+ name = scope[:name]
2198
+ table = model_name.underscore.pluralize
2199
+
2200
+ <<~MINITEST
2201
+ test "#{name} scope returns results" do
2202
+ assert_respond_to #{model_name}, :#{name}
2203
+ end
2204
+ MINITEST
2205
+ end
2206
+
2207
+ def generate_minitest_callbacks(suggestion)
2208
+ model_name = suggestion[:model]
2209
+ cbs = suggestion[:callbacks] || []
2210
+
2211
+ test_content = <<~MINITEST
2212
+ # frozen_string_literal: true
2213
+
2214
+ require "test_helper"
2215
+
2216
+ class #{model_name}Test < ActiveSupport::TestCase
2217
+ #{cbs.map { |cb| minitest_callback_test(cb, model_name) }.join("\n")}
2218
+ end
2219
+ MINITEST
2220
+
2221
+ write_test_file(model_name, test_content)
2222
+ end
2223
+
2224
+ def minitest_callback_test(callback, model_name)
2225
+ cb_type = callback[:type]
2226
+ var = model_name.underscore
2227
+
2228
+ <<~MINITEST
2229
+ test "executes #{cb_type} callback" do
2230
+ #{var} = #{model_name}.new
2231
+ assert_respond_to #{var}, :valid?
2232
+ end
2233
+ MINITEST
2234
+ end
2235
+
2236
+ def generate_minitest_custom_methods(suggestion)
2237
+ model_name = suggestion[:model]
2238
+ methods = suggestion[:custom_methods] || []
2239
+
2240
+ test_content = <<~MINITEST
2241
+ # frozen_string_literal: true
2242
+
2243
+ require "test_helper"
2244
+
2245
+ class #{model_name}Test < ActiveSupport::TestCase
2246
+ #{methods.map { |m| minitest_custom_method_test(m, model_name) }.join("\n")}
2247
+ end
2248
+ MINITEST
2249
+
2250
+ write_test_file(model_name, test_content)
2251
+ end
2252
+
2253
+ def minitest_custom_method_test(method, model_name)
2254
+ name = method.is_a?(Hash) ? method[:name] : method.to_sym
2255
+ is_class_method = method.is_a?(Hash) && method[:class_method]
2256
+ var = model_name.underscore
2257
+
2258
+ if is_class_method
2259
+ <<~MINITEST
2260
+ test ".#{name} returns a result" do
2261
+ assert_respond_to #{model_name}, :#{name}
2262
+ end
2263
+ MINITEST
2264
+ else
2265
+ <<~MINITEST
2266
+ test "##{name} returns a result" do
2267
+ #{var} = #{model_name}.new
2268
+ assert_respond_to #{var}, :#{name}
2269
+ end
2270
+ MINITEST
2271
+ end
2272
+ end
2273
+
2274
+ def generate_minitest_factories(suggestion)
2275
+ model_name = suggestion[:model]
2276
+ var = model_name.underscore
2277
+
2278
+ test_content = <<~MINITEST
2279
+ # frozen_string_literal: true
2280
+
2281
+ require "test_helper"
2282
+
2283
+ class #{model_name}Test < ActiveSupport::TestCase
2284
+ test "valid factory" do
2285
+ #{var} = #{model_name}.new
2286
+ assert #{var}.valid?
2287
+ end
2288
+ end
2289
+ MINITEST
2290
+
2291
+ write_test_file(model_name, test_content)
2292
+ end
2293
+
2294
+ def generate_minitest_serialization(suggestion)
2295
+ model_name = suggestion[:model]
2296
+ var = model_name.underscore
2297
+
2298
+ test_content = <<~MINITEST
2299
+ # frozen_string_literal: true
2300
+
2301
+ require "test_helper"
2302
+
2303
+ class #{model_name}Test < ActiveSupport::TestCase
2304
+ test "serializes to_json" do
2305
+ #{var} = #{model_name}.new
2306
+ assert_respond_to #{var}, :to_json
2307
+ end
2308
+ end
2309
+ MINITEST
2310
+
2311
+ write_test_file(model_name, test_content)
2312
+ end
2313
+
2314
+ def generate_minitest_request_test(suggestion)
2315
+ controller_name = suggestion[:controller]
2316
+ ctrl = controller_name.to_s.sub(/Controller$/, "").underscore
2317
+ table = ctrl.pluralize
2318
+
2319
+ test_content = <<~MINITEST
2320
+ # frozen_string_literal: true
2321
+
2322
+ require "test_helper"
2323
+
2324
+ class #{controller_name}Test < ActionDispatch::IntegrationTest
2325
+ test "should get index" do
2326
+ get #{table}_url
2327
+ assert_response :success
2328
+ end
2329
+ end
2330
+ MINITEST
2331
+
2332
+ write_test_file("#{ctrl}_controller", test_content)
2333
+ end
2334
+
2335
+ end
2336
+ end