propel_api 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -114,7 +114,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
114
114
  <% elsif attribute.name.to_s.match?(/password/) -%>
115
115
  <%= attribute.name %>: "password123"<%= ',' if index < attributes.length - 1 %>
116
116
  <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
117
- <%= attribute.name %>: "https://workflow.example.com"<%= ',' if index < attributes.length - 1 %>
117
+ <%= attribute.name %>: "https://workflow.com"<%= ',' if index < attributes.length - 1 %>
118
118
  <% elsif attribute.name.to_s.end_with?('_type') && attribute.name.to_s.match?(/_parent_type$/) -%>
119
119
  <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
120
120
  <% else -%>
@@ -205,7 +205,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
205
205
  # Password fields should NOT be returned in API responses for security
206
206
  assert_not_includes created_<%= singular_table_name %>.keys, '<%= attribute.name %>', "Password fields should be filtered from API responses"
207
207
  <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
208
- assert_equal "https://workflow.example.com", created_<%= singular_table_name %>['<%= attribute.name %>']
208
+ assert_equal "https://workflow.com", created_<%= singular_table_name %>['<%= attribute.name %>']
209
209
  <% else -%>
210
210
  assert_equal "Workflow Test <%= attribute.name.humanize %>", created_<%= singular_table_name %>['<%= attribute.name %>']
211
211
  <% end -%>
@@ -408,7 +408,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
408
408
  <% elsif attribute.name.include?('email') -%>
409
409
  <%= attribute.name %>: "pagination#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
410
410
  <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
411
- <%= attribute.name %>: "https://pagination#{i}.example.com"<%= ',' if index < attributes.length - 1 %>
411
+ <%= attribute.name %>: "https://pagination#{i}.com"<%= ',' if index < attributes.length - 1 %>
412
412
  <% elsif attribute.name.to_s.end_with?('_type') -%>
413
413
  <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
414
414
  <% else -%>
@@ -460,6 +460,589 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
460
460
  end
461
461
  <% end -%>
462
462
 
463
+ # === FILTERING AND SORTING TESTS ===
464
+
465
+ <% # Generate filtering tests only if we have filterable attributes -%>
466
+ <% filterable_attributes = attributes.reject { |attr|
467
+ attr.name.match?(/password|token|secret|key|salt|encrypted|digest|hash|auth/i) ||
468
+ attr.name.match?(/_at$/) ||
469
+ attr.name.match?(/_id$/) ||
470
+ attr.type == :references
471
+ } -%>
472
+ <% if filterable_attributes.any? -%>
473
+ test "basic field filtering" do
474
+ <% if class_name == 'Organization' -%>
475
+ # Test against existing fixture data (acme_org)
476
+ # The API only returns the current user's organization due to multi-tenancy
477
+
478
+ # Test exact match filtering against fixture data
479
+ get <%= api_route_helper %>_url,
480
+ params: { name_eq: "Acme Corporation" },
481
+ headers: @auth_headers
482
+ assert_response :success
483
+
484
+ exact_response = JSON.parse(response.body)
485
+ assert_equal 1, exact_response['data'].size, "Should find exactly 1 organization with name = 'Acme Corporation'"
486
+ assert_equal "Acme Corporation", exact_response['data'][0]['name'], "Should return the correct record"
487
+
488
+ # Test contains filtering against fixture data
489
+ get <%= api_route_helper %>_url,
490
+ params: { name_contains: "Acme" },
491
+ headers: @auth_headers
492
+ assert_response :success
493
+
494
+ contains_response = JSON.parse(response.body)
495
+ assert_equal 1, contains_response['data'].size, "Should find exactly 1 organization containing 'Acme'"
496
+ assert_equal "Acme Corporation", contains_response['data'][0]['name']
497
+
498
+ # Test starts_with filtering
499
+ get <%= api_route_helper %>_url,
500
+ params: { name_starts_with: "Acme" },
501
+ headers: @auth_headers
502
+ assert_response :success
503
+
504
+ starts_response = JSON.parse(response.body)
505
+ assert_equal 1, starts_response['data'].size, "Should find exactly 1 organization starting with 'Acme'"
506
+ assert_equal "Acme Corporation", starts_response['data'][0]['name']
507
+
508
+ # Test ends_with filtering
509
+ get <%= api_route_helper %>_url,
510
+ params: { name_ends_with: "Corporation" },
511
+ headers: @auth_headers
512
+ assert_response :success
513
+
514
+ ends_response = JSON.parse(response.body)
515
+ assert_equal 1, ends_response['data'].size, "Should find exactly 1 organization ending with 'Corporation'"
516
+ assert_equal "Acme Corporation", ends_response['data'][0]['name']
517
+
518
+ # Test 'in' filtering
519
+ get <%= api_route_helper %>_url,
520
+ params: { name_in: "Acme Corporation" },
521
+ headers: @auth_headers
522
+ assert_response :success
523
+
524
+ in_response = JSON.parse(response.body)
525
+ assert_equal 1, in_response['data'].size, "Should find exactly 1 organization with name in list"
526
+ returned_names = in_response['data'].map { |item| item['name'] }
527
+ assert_equal ["Acme Corporation"], returned_names, "Should return the correct record"
528
+ <% else -%>
529
+ # Create test data with known values for filtering
530
+ <% filterable_attributes.select { |attr| [:string, :text].include?(attr.type) }.first(1).each do |attr| -%>
531
+ <% if attr.name.match?(/email|mail/i) -%>
532
+ <%= singular_table_name %>_1 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": "green_apple_test@example.com")
533
+ <%= singular_table_name %>_2 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": "black_cherry_test@example.com")
534
+ <%= singular_table_name %>_3 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": "red_grape_test@example.com")
535
+ <% else -%>
536
+ <%= singular_table_name %>_1 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": "Green Apple")
537
+ <%= singular_table_name %>_2 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": "Black Cherry")
538
+ <%= singular_table_name %>_3 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": "Red Grape")
539
+ <% end -%>
540
+
541
+ # Test exact match filtering
542
+ <% if attr.name.match?(/email|mail/i) -%>
543
+ get <%= api_route_helper %>_url,
544
+ params: { <%= attr.name %>_eq: "green_apple_test@example.com" },
545
+ headers: @auth_headers
546
+ assert_response :success
547
+
548
+ exact_response = JSON.parse(response.body)
549
+ assert_equal 1, exact_response['data'].size, "Should find exactly 1 <%= singular_table_name %> with <%= attr.name %> = 'green_apple_test@example.com'"
550
+ assert_equal "green_apple_test@example.com", exact_response['data'][0]['<%= attr.name %>'], "Should return the correct record"
551
+ <% else -%>
552
+ get <%= api_route_helper %>_url,
553
+ params: { <%= attr.name %>_eq: "Green Apple" },
554
+ headers: @auth_headers
555
+ assert_response :success
556
+
557
+ exact_response = JSON.parse(response.body)
558
+ assert_equal 1, exact_response['data'].size, "Should find exactly 1 <%= singular_table_name %> with <%= attr.name %> = 'Green Apple'"
559
+ assert_equal "Green Apple", exact_response['data'][0]['<%= attr.name %>'], "Should return the correct record"
560
+ <% end -%>
561
+
562
+ # Test contains filtering
563
+ <% if attr.name.match?(/email|mail/i) -%>
564
+ get <%= api_route_helper %>_url,
565
+ params: { <%= attr.name %>_contains: "apple" },
566
+ headers: @auth_headers
567
+ assert_response :success
568
+
569
+ contains_response = JSON.parse(response.body)
570
+ assert_equal 1, contains_response['data'].size, "Should find exactly 1 <%= singular_table_name %> containing 'apple'"
571
+ assert_equal "green_apple_test@example.com", contains_response['data'][0]['<%= attr.name %>']
572
+ <% else -%>
573
+ get <%= api_route_helper %>_url,
574
+ params: { <%= attr.name %>_contains: "Apple" },
575
+ headers: @auth_headers
576
+ assert_response :success
577
+
578
+ contains_response = JSON.parse(response.body)
579
+ assert_equal 1, contains_response['data'].size, "Should find exactly 1 <%= singular_table_name %> containing 'Apple'"
580
+ assert_equal "Green Apple", contains_response['data'][0]['<%= attr.name %>']
581
+ <% end -%>
582
+
583
+ # Test starts_with filtering
584
+ <% if attr.name.match?(/email|mail/i) -%>
585
+ get <%= api_route_helper %>_url,
586
+ params: { <%= attr.name %>_starts_with: "black" },
587
+ headers: @auth_headers
588
+ assert_response :success
589
+
590
+ starts_response = JSON.parse(response.body)
591
+ assert_equal 1, starts_response['data'].size, "Should find exactly 1 <%= singular_table_name %> starting with 'black'"
592
+ assert_equal "black_cherry_test@example.com", starts_response['data'][0]['<%= attr.name %>']
593
+ <% else -%>
594
+ get <%= api_route_helper %>_url,
595
+ params: { <%= attr.name %>_starts_with: "Black" },
596
+ headers: @auth_headers
597
+ assert_response :success
598
+
599
+ starts_response = JSON.parse(response.body)
600
+ assert_equal 1, starts_response['data'].size, "Should find exactly 1 <%= singular_table_name %> starting with 'Black'"
601
+ assert_equal "Black Cherry", starts_response['data'][0]['<%= attr.name %>']
602
+ <% end -%>
603
+
604
+ # Test ends_with filtering
605
+ <% if attr.name.match?(/email|mail/i) -%>
606
+ get <%= api_route_helper %>_url,
607
+ params: { <%= attr.name %>_ends_with: "test@example.com" },
608
+ headers: @auth_headers
609
+ assert_response :success
610
+
611
+ ends_response = JSON.parse(response.body)
612
+ assert_equal 3, ends_response['data'].size, "Should find exactly 3 <%= table_name %> ending with 'test@example.com'"
613
+ # All three test records end with test@example.com
614
+ <% else -%>
615
+ get <%= api_route_helper %>_url,
616
+ params: { <%= attr.name %>_ends_with: "Grape" },
617
+ headers: @auth_headers
618
+ assert_response :success
619
+
620
+ ends_response = JSON.parse(response.body)
621
+ assert_equal 1, ends_response['data'].size, "Should find exactly 1 <%= singular_table_name %> ending with 'Grape'"
622
+ assert_equal "Red Grape", ends_response['data'][0]['<%= attr.name %>']
623
+ <% end -%>
624
+
625
+ # Test 'in' filtering
626
+ <% if attr.name.match?(/email|mail/i) -%>
627
+ get <%= api_route_helper %>_url,
628
+ params: { <%= attr.name %>_in: "green_apple_test@example.com,black_cherry_test@example.com" },
629
+ headers: @auth_headers
630
+ assert_response :success
631
+
632
+ in_response = JSON.parse(response.body)
633
+ assert_equal 2, in_response['data'].size, "Should find exactly 2 <%= table_name %> with <%= attr.name %> in list"
634
+ returned_<%= attr.name.pluralize %> = in_response['data'].map { |item| item['<%= attr.name %>'] }.sort
635
+ assert_equal ["black_cherry_test@example.com", "green_apple_test@example.com"], returned_<%= attr.name.pluralize %>, "Should return the correct records"
636
+ <% else -%>
637
+ get <%= api_route_helper %>_url,
638
+ params: { <%= attr.name %>_in: "Green Apple,Black Cherry" },
639
+ headers: @auth_headers
640
+ assert_response :success
641
+
642
+ in_response = JSON.parse(response.body)
643
+ assert_equal 2, in_response['data'].size, "Should find exactly 2 <%= table_name %> with <%= attr.name %> in list"
644
+ returned_<%= attr.name.pluralize %> = in_response['data'].map { |item| item['<%= attr.name %>'] }.sort
645
+ assert_equal ["Black Cherry", "Green Apple"], returned_<%= attr.name.pluralize %>, "Should return the correct records"
646
+ <% end -%>
647
+
648
+ # Clean up test data to avoid interference with other tests
649
+ [<%= singular_table_name %>_1, <%= singular_table_name %>_2, <%= singular_table_name %>_3].each do |record|
650
+ record.destroy if record.persisted?
651
+ end
652
+ <% end -%>
653
+ <% end -%>
654
+ end
655
+ <% end -%>
656
+
657
+ <% # Test numeric filtering if we have numeric attributes -%>
658
+ <% numeric_attributes = filterable_attributes.select { |attr| [:integer, :decimal, :float].include?(attr.type) } -%>
659
+ <% if numeric_attributes.any? -%>
660
+ test "numeric field filtering" do
661
+ <% numeric_attributes.first(1).each do |attr| -%>
662
+ # Create test data with known numeric values that don't overlap with fixtures
663
+ # Fixtures typically have values 1, 2, 3 - we use 100, 200, 300, 400 to avoid conflicts
664
+ unique_id = SecureRandom.hex(4)
665
+ <%= singular_table_name %>_1 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": 100)
666
+ <%= singular_table_name %>_2 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": 200)
667
+ <%= singular_table_name %>_3 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": 300)
668
+ <%= singular_table_name %>_4 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": 400)
669
+
670
+ # Test greater than
671
+ get <%= api_route_helper %>_url,
672
+ params: { <%= attr.name %>_gt: "250" },
673
+ headers: @auth_headers
674
+ assert_response :success
675
+
676
+ gt_response = JSON.parse(response.body)
677
+ # Filter for only our test records to avoid fixture interference
678
+ test_ids = [<%= singular_table_name %>_1.id, <%= singular_table_name %>_2.id, <%= singular_table_name %>_3.id, <%= singular_table_name %>_4.id]
679
+ test_records_gt = gt_response['data'].select { |item| test_ids.include?(item['id']) }
680
+ assert_equal 2, test_records_gt.size, "Should find exactly 2 test <%= table_name %> with <%= attr.name %> > 250"
681
+ gt_values = test_records_gt.map { |item| item['<%= attr.name %>'] }.sort
682
+ <% if attr.type == :decimal || attr.type == :float -%>
683
+ # Rails serializes decimal/float fields as strings in JSON to preserve precision
684
+ assert_equal ["300.0", "400.0"], gt_values, "Should return test records with <%= attr.name %> > 250 (as strings)"
685
+ <% else -%>
686
+ assert_equal [300, 400], gt_values, "Should return test records with <%= attr.name %> > 250"
687
+ <% end -%>
688
+
689
+ # Test less than or equal
690
+ get <%= api_route_helper %>_url,
691
+ params: { <%= attr.name %>_lte: "250" },
692
+ headers: @auth_headers
693
+ assert_response :success
694
+
695
+ lte_response = JSON.parse(response.body)
696
+ # Filter for only our test records to avoid fixture interference
697
+ test_records_lte = lte_response['data'].select { |item| test_ids.include?(item['id']) }
698
+ assert_equal 2, test_records_lte.size, "Should find exactly 2 test <%= table_name %> with <%= attr.name %> <= 250"
699
+ lte_values = test_records_lte.map { |item| item['<%= attr.name %>'] }.sort
700
+ <% if attr.type == :decimal || attr.type == :float -%>
701
+ # Rails serializes decimal/float fields as strings in JSON to preserve precision
702
+ assert_equal ["100.0", "200.0"], lte_values, "Should return test records with <%= attr.name %> <= 250 (as strings)"
703
+ <% else -%>
704
+ assert_equal [100, 200], lte_values, "Should return test records with <%= attr.name %> <= 250"
705
+ <% end -%>
706
+
707
+ # Test range filtering
708
+ get <%= api_route_helper %>_url,
709
+ params: { <%= attr.name %>_range: "150,350" },
710
+ headers: @auth_headers
711
+ assert_response :success
712
+
713
+ range_response = JSON.parse(response.body)
714
+ # Filter for only our test records to avoid fixture interference
715
+ test_records_range = range_response['data'].select { |item| test_ids.include?(item['id']) }
716
+ assert_equal 2, test_records_range.size, "Should find exactly 2 test <%= table_name %> with <%= attr.name %> between 150 and 350"
717
+ range_values = test_records_range.map { |item| item['<%= attr.name %>'] }.sort
718
+ <% if attr.type == :decimal || attr.type == :float -%>
719
+ # Rails serializes decimal/float fields as strings in JSON to preserve precision
720
+ assert_equal ["200.0", "300.0"], range_values, "Should return test records with <%= attr.name %> in range 150-350 (as strings)"
721
+ <% else -%>
722
+ assert_equal [200, 300], range_values, "Should return test records with <%= attr.name %> in range 150-350"
723
+ <% end -%>
724
+
725
+ # Test 'in' filtering for numeric values
726
+ get <%= api_route_helper %>_url,
727
+ params: { <%= attr.name %>_in: "100,300,999" },
728
+ headers: @auth_headers
729
+ assert_response :success
730
+
731
+ in_response = JSON.parse(response.body)
732
+ # Filter for only our test records to avoid fixture interference
733
+ test_records_in = in_response['data'].select { |item| test_ids.include?(item['id']) }
734
+ assert_equal 2, test_records_in.size, "Should find exactly 2 test <%= table_name %> with <%= attr.name %> in [100,300,999]"
735
+ in_values = test_records_in.map { |item| item['<%= attr.name %>'] }.sort
736
+ <% if attr.type == :decimal || attr.type == :float -%>
737
+ # Rails serializes decimal/float fields as strings in JSON to preserve precision
738
+ assert_equal ["100.0", "300.0"], in_values, "Should return test records with <%= attr.name %> in list (as strings)"
739
+ <% else -%>
740
+ assert_equal [100, 300], in_values, "Should return test records with <%= attr.name %> in list"
741
+ <% end -%>
742
+
743
+ # Clean up test data to avoid interference with other tests
744
+ [<%= singular_table_name %>_1, <%= singular_table_name %>_2, <%= singular_table_name %>_3, <%= singular_table_name %>_4].each do |record|
745
+ record.destroy if record.persisted?
746
+ end
747
+ <% end -%>
748
+ end
749
+ <% end -%>
750
+
751
+ <% # Test boolean filtering if we have boolean attributes -%>
752
+ <% boolean_attributes = filterable_attributes.select { |attr| attr.type == :boolean } -%>
753
+ <% if boolean_attributes.any? -%>
754
+ test "boolean field filtering" do
755
+ <% boolean_attributes.first(1).each do |attr| -%>
756
+ # Create test data with known boolean values
757
+ <%= singular_table_name %>_true = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": true)
758
+ <%= singular_table_name %>_false = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": false)
759
+
760
+ # Test filtering for true values
761
+ get <%= api_route_helper %>_url,
762
+ params: { <%= attr.name %>_eq: "true" },
763
+ headers: @auth_headers
764
+ assert_response :success
765
+
766
+ true_response = JSON.parse(response.body)
767
+ # Filter for only our test records to avoid fixture interference
768
+ test_ids = [<%= singular_table_name %>_true.id, <%= singular_table_name %>_false.id]
769
+ test_records_true = true_response['data'].select { |item| test_ids.include?(item['id']) }
770
+ true_count = test_records_true.count { |item| item['<%= attr.name %>'] == true }
771
+ assert_equal 1, true_count, "Should find exactly 1 test <%= singular_table_name %> with <%= attr.name %> = true"
772
+
773
+ # Test filtering for false values
774
+ get <%= api_route_helper %>_url,
775
+ params: { <%= attr.name %>_eq: "false" },
776
+ headers: @auth_headers
777
+ assert_response :success
778
+
779
+ false_response = JSON.parse(response.body)
780
+ # Filter for only our test records to avoid fixture interference
781
+ test_records_false = false_response['data'].select { |item| test_ids.include?(item['id']) }
782
+ false_count = test_records_false.count { |item| item['<%= attr.name %>'] == false }
783
+ assert_equal 1, false_count, "Should find exactly 1 test <%= singular_table_name %> with <%= attr.name %> = false"
784
+
785
+ # Clean up test data to avoid interference with other tests
786
+ [<%= singular_table_name %>_true, <%= singular_table_name %>_false].each do |record|
787
+ record.destroy if record.persisted?
788
+ end
789
+ <% end -%>
790
+ end
791
+ <% end -%>
792
+
793
+ <% # Test datetime filtering if we have datetime attributes -%>
794
+ <% datetime_attributes = filterable_attributes.select { |attr| [:datetime, :date, :time].include?(attr.type) } -%>
795
+ <% if datetime_attributes.any? -%>
796
+ test "datetime field filtering" do
797
+ <% datetime_attributes.first(1).each do |attr| -%>
798
+ # Create test data with known datetime values that don't overlap with fixtures
799
+ # Use dates far in the future to avoid conflicts with fixture data
800
+ base_time = Time.parse("2030-06-15 14:30:00 UTC")
801
+ <%= singular_table_name %>_1 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": base_time - 2.days)
802
+ <%= singular_table_name %>_2 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": base_time - 1.day)
803
+ <%= singular_table_name %>_3 = create_<%= singular_table_name %>_for_filtering("<%= attr.name %>": base_time + 1.day)
804
+
805
+ # Test 'before' filtering
806
+ get <%= api_route_helper %>_url,
807
+ params: { <%= attr.name %>_before: base_time.iso8601 },
808
+ headers: @auth_headers
809
+ assert_response :success
810
+
811
+ before_response = JSON.parse(response.body)
812
+ # Filter for only our test records to avoid fixture interference
813
+ test_ids = [<%= singular_table_name %>_1.id, <%= singular_table_name %>_2.id, <%= singular_table_name %>_3.id]
814
+ test_records_before = before_response['data'].select { |item| test_ids.include?(item['id']) }
815
+ before_count = test_records_before.count { |item| item['<%= attr.name %>'].present? && Time.parse(item['<%= attr.name %>']) < base_time }
816
+ assert_equal 2, before_count, "Should find exactly 2 test <%= table_name %> with <%= attr.name %> before #{base_time}"
817
+
818
+ # Test 'after' filtering
819
+ get <%= api_route_helper %>_url,
820
+ params: { <%= attr.name %>_after: base_time.iso8601 },
821
+ headers: @auth_headers
822
+ assert_response :success
823
+
824
+ after_response = JSON.parse(response.body)
825
+ # Filter for only our test records to avoid fixture interference
826
+ test_records_after = after_response['data'].select { |item| test_ids.include?(item['id']) }
827
+ after_count = test_records_after.count { |item| item['<%= attr.name %>'].present? && Time.parse(item['<%= attr.name %>']) > base_time }
828
+ assert_equal 1, after_count, "Should find exactly 1 test <%= singular_table_name %> with <%= attr.name %> after #{base_time}"
829
+
830
+ # Test 'year' filtering
831
+ get <%= api_route_helper %>_url,
832
+ params: { <%= attr.name %>_year: "2030" },
833
+ headers: @auth_headers
834
+ assert_response :success
835
+
836
+ year_response = JSON.parse(response.body)
837
+ # Filter for only our test records to avoid fixture interference
838
+ test_records_year = year_response['data'].select { |item| test_ids.include?(item['id']) }
839
+ year_count = test_records_year.count { |item| item['<%= attr.name %>'].present? && Time.parse(item['<%= attr.name %>']).year == 2030 }
840
+ assert_equal 3, year_count, "Should find exactly 3 test <%= table_name %> with <%= attr.name %> in year 2030"
841
+
842
+ # Test 'month' filtering
843
+ get <%= api_route_helper %>_url,
844
+ params: { <%= attr.name %>_month: "6" },
845
+ headers: @auth_headers
846
+ assert_response :success
847
+
848
+ month_response = JSON.parse(response.body)
849
+ # Filter for only our test records to avoid fixture interference
850
+ test_records_month = month_response['data'].select { |item| test_ids.include?(item['id']) }
851
+ month_count = test_records_month.count { |item| item['<%= attr.name %>'].present? && Time.parse(item['<%= attr.name %>']).month == 6 }
852
+ assert_equal 3, month_count, "Should find exactly 3 test <%= table_name %> with <%= attr.name %> in month 6"
853
+
854
+ # Clean up test data to avoid interference with other tests
855
+ [<%= singular_table_name %>_1, <%= singular_table_name %>_2, <%= singular_table_name %>_3].each do |record|
856
+ record.destroy if record.persisted?
857
+ end
858
+ <% end -%>
859
+ end
860
+ <% end -%>
861
+
862
+ test "sorting functionality" do
863
+ <% if class_name == 'Organization' -%>
864
+ # Test sorting with existing fixture data
865
+ # Due to multi-tenancy, we only have access to the current user's organization
866
+
867
+ # Test ascending sort
868
+ get <%= api_route_helper %>_url,
869
+ params: { order_by: "name" },
870
+ headers: @auth_headers
871
+ assert_response :success
872
+
873
+ asc_response = JSON.parse(response.body)
874
+ assert asc_response['data'].size >= 1, "Should have at least 1 record for sorting test"
875
+
876
+ # Test that the single organization is returned and properly formatted
877
+ organization_data = asc_response['data'][0]
878
+ assert_equal "Acme Corporation", organization_data['name'], "Should return the correct organization name"
879
+ assert organization_data.key?('id'), "Should include organization ID"
880
+ assert organization_data.key?('website'), "Should include organization website"
881
+
882
+ # Test descending sort
883
+ get <%= api_route_helper %>_url,
884
+ params: { order_by: "-name" },
885
+ headers: @auth_headers
886
+ assert_response :success
887
+
888
+ desc_response = JSON.parse(response.body)
889
+ assert desc_response['data'].size >= 1, "Should have at least 1 record for descending sort test"
890
+
891
+ # Test that the single organization is returned (descending sort of 1 item is the same as ascending)
892
+ organization_data_desc = desc_response['data'][0]
893
+ assert_equal "Acme Corporation", organization_data_desc['name'], "Should return the correct organization name"
894
+ <% elsif filterable_attributes.any? -%>
895
+ <% sortable_attr = filterable_attributes.first -%>
896
+ # Create test data for sorting
897
+ <% if sortable_attr.name.match?(/email|mail/i) -%>
898
+ <%= singular_table_name %>_1 = create_<%= singular_table_name %>_for_filtering("<%= sortable_attr.name %>": "red_grape_test@example.com")
899
+ <%= singular_table_name %>_2 = create_<%= singular_table_name %>_for_filtering("<%= sortable_attr.name %>": "green_apple_test@example.com")
900
+ <%= singular_table_name %>_3 = create_<%= singular_table_name %>_for_filtering("<%= sortable_attr.name %>": "black_cherry_test@example.com")
901
+ <% else -%>
902
+ <%= singular_table_name %>_1 = create_<%= singular_table_name %>_for_filtering("<%= sortable_attr.name %>": <% if [:string, :text].include?(sortable_attr.type) %>"Red Grape"<% elsif [:integer, :decimal, :float].include?(sortable_attr.type) %>30<% elsif sortable_attr.type == :boolean %>true<% else %>"red_grape_value"<% end %>)
903
+ <%= singular_table_name %>_2 = create_<%= singular_table_name %>_for_filtering("<%= sortable_attr.name %>": <% if [:string, :text].include?(sortable_attr.type) %>"Green Apple"<% elsif [:integer, :decimal, :float].include?(sortable_attr.type) %>10<% elsif sortable_attr.type == :boolean %>false<% else %>"green_apple_value"<% end %>)
904
+ <%= singular_table_name %>_3 = create_<%= singular_table_name %>_for_filtering("<%= sortable_attr.name %>": <% if [:string, :text].include?(sortable_attr.type) %>"Black Cherry"<% elsif [:integer, :decimal, :float].include?(sortable_attr.type) %>20<% elsif sortable_attr.type == :boolean %>true<% else %>"black_cherry_value"<% end %>)
905
+ <% end -%>
906
+
907
+ # Test ascending sort
908
+ get <%= api_route_helper %>_url,
909
+ params: { order_by: "<%= sortable_attr.name %>" },
910
+ headers: @auth_headers
911
+ assert_response :success
912
+
913
+ asc_response = JSON.parse(response.body)
914
+ assert asc_response['data'].size >= 3, "Should have at least 3 records for sorting test"
915
+
916
+ # Extract sorted values (only from our test records)
917
+ test_ids = [<%= singular_table_name %>_1.id, <%= singular_table_name %>_2.id, <%= singular_table_name %>_3.id]
918
+ test_records = asc_response['data'].select { |item| test_ids.include?(item['id']) }
919
+ asc_values = test_records.map { |item| item['<%= sortable_attr.name %>'] }
920
+
921
+ <% if sortable_attr.name.match?(/email|mail/i) -%>
922
+ assert_equal ["black_cherry_test@example.com", "green_apple_test@example.com", "red_grape_test@example.com"], asc_values, "Records should be sorted by <%= sortable_attr.name %> in ascending order"
923
+ <% elsif [:string, :text].include?(sortable_attr.type) -%>
924
+ assert_equal ["Black Cherry", "Green Apple", "Red Grape"], asc_values, "Records should be sorted by <%= sortable_attr.name %> in ascending order"
925
+ <% elsif [:integer, :decimal, :float].include?(sortable_attr.type) -%>
926
+ assert_equal [10, 20, 30], asc_values, "Records should be sorted by <%= sortable_attr.name %> in ascending order"
927
+ <% else -%>
928
+ assert_equal asc_values.sort, asc_values, "Records should be sorted by <%= sortable_attr.name %> in ascending order"
929
+ <% end -%>
930
+
931
+ # Test descending sort
932
+ get <%= api_route_helper %>_url,
933
+ params: { order_by: "-<%= sortable_attr.name %>" },
934
+ headers: @auth_headers
935
+ assert_response :success
936
+
937
+ desc_response = JSON.parse(response.body)
938
+ test_records_desc = desc_response['data'].select { |item| test_ids.include?(item['id']) }
939
+ desc_values = test_records_desc.map { |item| item['<%= sortable_attr.name %>'] }
940
+
941
+ <% if sortable_attr.name.match?(/email|mail/i) -%>
942
+ assert_equal ["red_grape_test@example.com", "green_apple_test@example.com", "black_cherry_test@example.com"], desc_values, "Records should be sorted by <%= sortable_attr.name %> in descending order"
943
+ <% elsif [:string, :text].include?(sortable_attr.type) -%>
944
+ assert_equal ["Red Grape", "Green Apple", "Black Cherry"], desc_values, "Records should be sorted by <%= sortable_attr.name %> in descending order"
945
+ <% elsif [:integer, :decimal, :float].include?(sortable_attr.type) -%>
946
+ assert_equal [30, 20, 10], desc_values, "Records should be sorted by <%= sortable_attr.name %> in descending order"
947
+ <% else -%>
948
+ assert_equal asc_values.sort.reverse, desc_values, "Records should be sorted by <%= sortable_attr.name %> in descending order"
949
+ <% end -%>
950
+ <% else -%>
951
+ # No filterable attributes available for sorting test
952
+ get <%= api_route_helper %>_url, headers: @auth_headers
953
+ assert_response :success
954
+
955
+ response_data = JSON.parse(response.body)
956
+ assert response_data['data'].is_a?(Array), "Should return array of records"
957
+ <% end -%>
958
+ end
959
+
960
+ test "filtering security - should reject sensitive field filtering" do
961
+ # Test that sensitive field filtering is rejected/ignored
962
+ get <%= api_route_helper %>_url,
963
+ params: {
964
+ password_eq: "hack_attempt",
965
+ token_contains: "secret",
966
+ encrypted_data_eq: "malicious"
967
+ },
968
+ headers: @auth_headers
969
+ assert_response :success
970
+
971
+ # Should succeed but ignore the sensitive filters
972
+ security_response = JSON.parse(response.body)
973
+ assert security_response['data'].is_a?(Array), "Should return normal results, ignoring sensitive filters"
974
+ end
975
+
976
+ test "filtering error handling" do
977
+ # Test invalid operator
978
+ get <%= api_route_helper %>_url,
979
+ params: { invalid_field_invalid_op: "value" },
980
+ headers: @auth_headers
981
+ assert_response :success # Should succeed but ignore invalid filter
982
+
983
+ # Test malformed range
984
+ <% if filterable_attributes.select { |attr| [:integer, :decimal, :float].include?(attr.type) }.any? -%>
985
+ <% numeric_attr = filterable_attributes.select { |attr| [:integer, :decimal, :float].include?(attr.type) }.first -%>
986
+ get <%= api_route_helper %>_url,
987
+ params: { <%= numeric_attr.name %>_range: "invalid_range" },
988
+ headers: @auth_headers
989
+ assert_response :success # Should succeed but ignore malformed filter
990
+ <% end -%>
991
+
992
+ # Test empty filter values
993
+ <% if filterable_attributes.any? -%>
994
+ <% test_attr = filterable_attributes.first -%>
995
+ get <%= api_route_helper %>_url,
996
+ params: { <%= test_attr.name %>_eq: "" },
997
+ headers: @auth_headers
998
+ assert_response :success
999
+ <% end -%>
1000
+ end
1001
+
1002
+ private
1003
+
1004
+ def create_<%= singular_table_name %>_for_filtering(**attributes)
1005
+ # Use unique identifiers to avoid conflicts with existing test data
1006
+ unique_id = SecureRandom.hex(4)
1007
+ base_attributes = {
1008
+ <% attributes.each_with_index do |attribute, index| -%>
1009
+ <% if attribute.type == :references -%>
1010
+ <%= attribute.name %>: @<%= attribute.name %><%= ',' if index < attributes.length - 1 %>
1011
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
1012
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
1013
+ <% elsif [:string, :text].include?(attribute.type) -%>
1014
+ <% if attribute.name.match?(/email|email_address|mail/i) -%>
1015
+ <%= attribute.name %>: "test_<%= attribute.name %>_#{unique_id}@example.com"<%= ',' if index < attributes.length - 1 %>
1016
+ <% elsif attribute.name.match?(/password/i) -%>
1017
+ <%= attribute.name %>: "password123"<%= ',' if index < attributes.length - 1 %>
1018
+ <% elsif attribute.name.match?(/\A(url|website|web_address|domain|domain_name)\z/i) || attribute.name.match?(/_url$|_link$/i) -%>
1019
+ <%= attribute.name %>: "https://test_<%= attribute.name %>_#{unique_id}.com"<%= ',' if index < attributes.length - 1 %>
1020
+ <% elsif attribute.name.match?(/username/i) -%>
1021
+ <%= attribute.name %>: "test_<%= attribute.name %>_#{unique_id}"<%= ',' if index < attributes.length - 1 %>
1022
+ <% else -%>
1023
+ <%= attribute.name %>: "Test <%= attribute.name.humanize %> #{unique_id}"<%= ',' if index < attributes.length - 1 %>
1024
+ <% end -%>
1025
+ <% elsif attribute.type == :integer -%>
1026
+ <%= attribute.name %>: 1<%= ',' if index < attributes.length - 1 %>
1027
+ <% elsif attribute.type == :boolean -%>
1028
+ <%= attribute.name %>: false<%= ',' if index < attributes.length - 1 %>
1029
+ <% elsif attribute.type == :decimal || attribute.type == :float -%>
1030
+ <%= attribute.name %>: 1.0<%= ',' if index < attributes.length - 1 %>
1031
+ <% elsif [:datetime, :date, :time].include?(attribute.type) -%>
1032
+ <%= attribute.name %>: Time.current<%= ',' if index < attributes.length - 1 %>
1033
+ <% else -%>
1034
+ <%= attribute.name %>: "test_value"<%= ',' if index < attributes.length - 1 %>
1035
+ <% end -%>
1036
+ <% end -%>
1037
+ <% # Add password_confirmation if password field exists but password_confirmation doesn't -%>
1038
+ <% if attributes.any? { |attr| attr.name.match?(/password/i) && !attr.name.match?(/confirmation/i) } && !attributes.any? { |attr| attr.name.match?(/password_confirmation/i) } -%>
1039
+ password_confirmation: "password123"
1040
+ <% end -%>
1041
+ }.merge(attributes)
1042
+
1043
+ <%= class_name %>.create!(base_attributes)
1044
+ end
1045
+
463
1046
  # === ERROR HANDLING WORKFLOW TESTS ===
464
1047
 
465
1048
  test "error handling workflow" do
@@ -544,7 +1127,40 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
544
1127
  unique_id = "#{Time.current.to_i}_#{SecureRandom.hex(6)}"
545
1128
  test_params = { email_address: "tenancy_test_#{unique_id}@example.com", username: "tenancy_test_#{unique_id}", password: "password123" }
546
1129
  <% else -%>
547
- test_params = { title: "Test <%= class_name %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> }
1130
+ test_params = { <%
1131
+ # Use appropriate field name based on model type
1132
+ case class_name
1133
+ when 'Agency', 'Organization'
1134
+ field_name = 'name'
1135
+ field_value = "Test #{class_name}"
1136
+ when 'Agent'
1137
+ field_name = 'title'
1138
+ field_value = "Test #{class_name}"
1139
+ else
1140
+ # Default to title for other models, but check if name field exists
1141
+ if attributes.any? { |attr| attr.name == 'name' && attr.type == :string }
1142
+ field_name = 'name'
1143
+ field_value = "Test #{class_name}"
1144
+ elsif attributes.any? { |attr| attr.name == 'title' && attr.type == :string }
1145
+ field_name = 'title'
1146
+ field_value = "Test #{class_name}"
1147
+ else
1148
+ # Fallback to first string attribute if no name/title found
1149
+ string_attr = attributes.find { |attr| attr.type == :string && !['organization', 'agency', 'user', 'agent'].include?(attr.name) }
1150
+ if string_attr
1151
+ field_name = string_attr.name
1152
+ field_value = if string_attr.name.match?(/email/)
1153
+ "test@example.com"
1154
+ else
1155
+ "Test #{string_attr.name.humanize}"
1156
+ end
1157
+ else
1158
+ field_name = 'name' # Final fallback
1159
+ field_value = "Test #{class_name}"
1160
+ end
1161
+ end
1162
+ end
1163
+ %><%= field_name %>: "<%= field_value %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> }
548
1164
  <% end -%>
549
1165
  <% end -%>
550
1166
 
@@ -613,7 +1229,24 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
613
1229
  headers: @auth_headers
614
1230
  <% else -%>
615
1231
  post <%= api_route_helper %>_url,
616
- params: { data: { organization_id: @organization.id, agency_id: 99999, title: "Test <%= class_name %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> } },
1232
+ params: { data: { organization_id: @organization.id, agency_id: 99999, <%=
1233
+ # Use appropriate field name for security tests
1234
+ case class_name
1235
+ when 'Agency', 'Organization'
1236
+ 'name'
1237
+ when 'Agent'
1238
+ 'title'
1239
+ else
1240
+ # Check which field the model actually has
1241
+ if attributes.any? { |attr| attr.name == 'name' && attr.type == :string }
1242
+ 'name'
1243
+ elsif attributes.any? { |attr| attr.name == 'title' && attr.type == :string }
1244
+ 'title'
1245
+ else
1246
+ 'name' # Fallback
1247
+ end
1248
+ end
1249
+ %>: "Test <%= class_name %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> } },
617
1250
  headers: @auth_headers
618
1251
  <% end -%>
619
1252
 
@@ -632,7 +1265,24 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
632
1265
  headers: @auth_headers
633
1266
  <% else -%>
634
1267
  post <%= api_route_helper %>_url,
635
- params: { data: { organization_id: 99999, title: "Test <%= class_name %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> } },
1268
+ params: { data: { organization_id: 99999, <%=
1269
+ # Use appropriate field name for security tests
1270
+ case class_name
1271
+ when 'Agency', 'Organization'
1272
+ 'name'
1273
+ when 'Agent'
1274
+ 'title'
1275
+ else
1276
+ # Check which field the model actually has
1277
+ if attributes.any? { |attr| attr.name == 'name' && attr.type == :string }
1278
+ 'name'
1279
+ elsif attributes.any? { |attr| attr.name == 'title' && attr.type == :string }
1280
+ 'title'
1281
+ else
1282
+ 'name' # Fallback
1283
+ end
1284
+ end
1285
+ %>: "Test <%= class_name %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> } },
636
1286
  headers: @auth_headers
637
1287
  <% end -%>
638
1288
 
@@ -708,7 +1358,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
708
1358
  <% elsif attribute.name.include?('password') -%>
709
1359
  <%= attribute.name %>: "org1_secure_password"<%= ',' if index < attributes.length - 1 %>
710
1360
  <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
711
- <%= attribute.name %>: "https://org1.example.com"<%= ',' if index < attributes.length - 1 %>
1361
+ <%= attribute.name %>: "https://org1.com"<%= ',' if index < attributes.length - 1 %>
712
1362
  <% elsif attribute.name.to_s.end_with?('_type') && attribute.name.to_s.match?(/_parent_type$/) -%>
713
1363
  <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
714
1364
  <% else -%>
@@ -775,7 +1425,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
775
1425
  <% elsif attribute.name.include?('password') -%>
776
1426
  <%= attribute.name %>: "org2_secure_password"<%= ',' if index < attributes.length - 1 %>
777
1427
  <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
778
- <%= attribute.name %>: "https://org2.example.com"<%= ',' if index < attributes.length - 1 %>
1428
+ <%= attribute.name %>: "https://org2.com"<%= ',' if index < attributes.length - 1 %>
779
1429
  <% elsif attribute.name.to_s.end_with?('_type') && attribute.name.to_s.match?(/_parent_type$/) -%>
780
1430
  <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
781
1431
  <% else -%>
@@ -952,7 +1602,7 @@ if attributes.any? { |attr| attr.type == :references && attr.name == 'user' } &&
952
1602
  <% elsif attribute.name.include?('email') -%>
953
1603
  <%= attribute.name %>: "concurrent@example.com"<%= ',' if index < attributes.length - 1 %>
954
1604
  <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
955
- <%= attribute.name %>: "https://concurrent.example.com"<%= ',' if index < attributes.length - 1 %>
1605
+ <%= attribute.name %>: "https://concurrent.com"<%= ',' if index < attributes.length - 1 %>
956
1606
  <% elsif attribute.name.to_s.end_with?('_type') && attribute.name.to_s.match?(/_parent_type$/) -%>
957
1607
  <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
958
1608
  <% else -%>
@@ -1088,7 +1738,7 @@ if attributes.any? { |attr| attr.type == :references && attr.name == 'user' } &&
1088
1738
  <% elsif attribute.name.include?('email') -%>
1089
1739
  <%= attribute.name %>: "bulk#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
1090
1740
  <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
1091
- <%= attribute.name %>: "https://bulk#{i}.example.com"<%= ',' if index < attributes.length - 1 %>
1741
+ <%= attribute.name %>: "https://bulk#{i}.com"<%= ',' if index < attributes.length - 1 %>
1092
1742
  <% elsif attribute.name.to_s.end_with?('_type') && attribute.name.to_s.match?(/_parent_type$/) -%>
1093
1743
  <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
1094
1744
  <% else -%>