propel_api 0.1.1

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.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +59 -0
  3. data/LICENSE +21 -0
  4. data/README.md +320 -0
  5. data/Rakefile +36 -0
  6. data/lib/generators/propel_api/USAGE +8 -0
  7. data/lib/generators/propel_api/controller/controller_generator.rb +208 -0
  8. data/lib/generators/propel_api/core/base.rb +19 -0
  9. data/lib/generators/propel_api/core/configuration_methods.rb +187 -0
  10. data/lib/generators/propel_api/core/named_base.rb +457 -0
  11. data/lib/generators/propel_api/core/path_generation_methods.rb +45 -0
  12. data/lib/generators/propel_api/core/relationship_inferrer.rb +117 -0
  13. data/lib/generators/propel_api/install/install_generator.rb +343 -0
  14. data/lib/generators/propel_api/resource/resource_generator.rb +433 -0
  15. data/lib/generators/propel_api/templates/config/propel_api.rb.tt +149 -0
  16. data/lib/generators/propel_api/templates/controllers/api_controller_graphiti.rb +79 -0
  17. data/lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb +76 -0
  18. data/lib/generators/propel_api/templates/controllers/example_controller.rb.tt +96 -0
  19. data/lib/generators/propel_api/templates/scaffold/facet_controller_template.rb.tt +80 -0
  20. data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +141 -0
  21. data/lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt +82 -0
  22. data/lib/generators/propel_api/templates/scaffold/graphiti_model_template.rb.tt +32 -0
  23. data/lib/generators/propel_api/templates/seeds/seeds_template.rb.tt +493 -0
  24. data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +485 -0
  25. data/lib/generators/propel_api/templates/tests/fixtures_template.yml.tt +250 -0
  26. data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +487 -0
  27. data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +252 -0
  28. data/lib/generators/propel_api/unpack/unpack_generator.rb +304 -0
  29. data/lib/propel_api.rb +3 -0
  30. metadata +95 -0
@@ -0,0 +1,487 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
6
+
7
+ def setup
8
+ @organization = organizations(:one)
9
+ @user = users(:one)
10
+ @<%= singular_table_name %> = <%= table_name %>(:one)
11
+ @token = @user.generate_jwt_token
12
+ @auth_headers = { 'Authorization' => "Bearer #{@token}" }
13
+
14
+ # Ensure test <%= singular_table_name %> belongs to test user's organization
15
+ @<%= singular_table_name %>.update!(organization: @organization)
16
+ end
17
+
18
+ # === FULL CRUD WORKFLOW TESTS ===
19
+
20
+ test "complete <%= singular_table_name %> lifecycle workflow" do
21
+ # Step 1: List empty resources
22
+ get <%= api_route_helper %>_url, headers: @auth_headers
23
+ assert_response :success
24
+
25
+ initial_response = JSON.parse(response.body)
26
+ initial_count = initial_response['data'].size
27
+
28
+ # Step 2: Create new <%= singular_table_name %>
29
+ new_<%= singular_table_name %>_params = {
30
+ <% attributes.each_with_index do |attribute, index| -%>
31
+ <% if attribute.type == :references -%>
32
+ <%= attribute.name %>_id: @<%= attribute.name %>.id<%= ',' if index < attributes.length - 1 %>
33
+ <% elsif attribute.type == :string -%>
34
+ <%= attribute.name %>: "Workflow Test <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
35
+ <% elsif attribute.type == :text -%>
36
+ <%= attribute.name %>: "Workflow test <%= attribute.name.humanize %> content"<%= ',' if index < attributes.length - 1 %>
37
+ <% elsif attribute.type == :integer -%>
38
+ <%= attribute.name %>: 100<%= ',' if index < attributes.length - 1 %>
39
+ <% elsif attribute.type == :boolean -%>
40
+ <%= attribute.name %>: true<%= ',' if index < attributes.length - 1 %>
41
+ <% elsif attribute.type == :decimal || attribute.type == :float -%>
42
+ <%= attribute.name %>: 100.50<%= ',' if index < attributes.length - 1 %>
43
+ <% else -%>
44
+ <%= attribute.name %>: "workflow_test"<%= ',' if index < attributes.length - 1 %>
45
+ <% end -%>
46
+ <% end -%>
47
+ }
48
+
49
+ post <%= api_route_helper %>_url,
50
+ params: { data: new_<%= singular_table_name %>_params },
51
+ headers: @auth_headers
52
+
53
+ assert_response :created
54
+ create_response = JSON.parse(response.body)
55
+ created_<%= singular_table_name %> = create_response['data']
56
+ created_id = created_<%= singular_table_name %>['id']
57
+
58
+ # Verify created <%= singular_table_name %> data
59
+ assert_equal @organization.id, created_<%= singular_table_name %>['organization']['id']
60
+ assert_equal @user.id, created_<%= singular_table_name %>['user']['id']
61
+ <% attributes.each do |attribute| -%>
62
+ <% unless attribute.type == :references -%>
63
+ <% if attribute.type == :string -%>
64
+ assert_equal "Workflow Test <%= attribute.name.humanize %>", created_<%= singular_table_name %>['<%= attribute.name %>']
65
+ <% elsif attribute.type == :text -%>
66
+ assert_equal "Workflow test <%= attribute.name.humanize %> content", created_<%= singular_table_name %>['<%= attribute.name %>']
67
+ <% elsif attribute.type == :integer -%>
68
+ assert_equal 100, created_<%= singular_table_name %>['<%= attribute.name %>']
69
+ <% elsif attribute.type == :boolean -%>
70
+ assert_equal true, created_<%= singular_table_name %>['<%= attribute.name %>']
71
+ <% elsif attribute.type == :decimal || attribute.type == :float -%>
72
+ assert_equal 100.5, created_<%= singular_table_name %>['<%= attribute.name %>']
73
+ <% else -%>
74
+ assert_equal "workflow_test", created_<%= singular_table_name %>['<%= attribute.name %>']
75
+ <% end -%>
76
+ <% end -%>
77
+ <% end -%>
78
+
79
+ # Step 3: Verify list now includes new <%= singular_table_name %>
80
+ get <%= api_route_helper %>_url, headers: @auth_headers
81
+ assert_response :success
82
+
83
+ list_response = JSON.parse(response.body)
84
+ assert_equal initial_count + 1, list_response['data'].size
85
+
86
+ <%= table_name %>_ids = list_response['data'].map { |item| item['id'] }
87
+ assert_includes <%= table_name %>_ids, created_id
88
+
89
+ # Step 4: Retrieve specific <%= singular_table_name %>
90
+ get <%= api_route_helper %>_url(created_id), headers: @auth_headers
91
+ assert_response :success
92
+
93
+ show_response = JSON.parse(response.body)
94
+ show_<%= singular_table_name %> = show_response['data']
95
+ assert_equal created_id, show_<%= singular_table_name %>['id']
96
+
97
+ # Step 5: Update the <%= singular_table_name %>
98
+ <% if attributes.any? { |attr| attr.type == :string && attr.name != "organization" && attr.name != "user" } -%>
99
+ <% string_attr = attributes.find { |attr| attr.type == :string && attr.name != "organization" && attr.name != "user" } -%>
100
+ update_params = { <%= string_attr.name %>: "Updated Workflow <%= string_attr.name.humanize %>" }
101
+
102
+ patch <%= api_route_helper %>_url(created_id),
103
+ params: { data: update_params },
104
+ headers: @auth_headers
105
+
106
+ assert_response :success
107
+
108
+ update_response = JSON.parse(response.body)
109
+ updated_<%= singular_table_name %> = update_response['data']
110
+ assert_equal "Updated Workflow <%= string_attr.name.humanize %>", updated_<%= singular_table_name %>['<%= string_attr.name %>']
111
+ <% else -%>
112
+ # Update test - customize based on your model's attributes
113
+ patch <%= api_route_helper %>_url(created_id),
114
+ params: { data: new_<%= singular_table_name %>_params },
115
+ headers: @auth_headers
116
+
117
+ assert_response :success
118
+ <% end -%>
119
+
120
+ # Step 6: Delete the <%= singular_table_name %>
121
+ delete <%= api_route_helper %>_url(created_id), headers: @auth_headers
122
+ assert_response :no_content
123
+
124
+ # Step 7: Verify <%= singular_table_name %> is gone
125
+ get <%= api_route_helper %>_url(created_id), headers: @auth_headers
126
+ assert_response :not_found
127
+
128
+ # Step 8: Verify list count is back to original
129
+ get <%= api_route_helper %>_url, headers: @auth_headers
130
+ assert_response :success
131
+
132
+ final_response = JSON.parse(response.body)
133
+ assert_equal initial_count, final_response['data'].size
134
+ end
135
+
136
+ # === PAGINATION WORKFLOW TESTS ===
137
+
138
+ test "pagination workflow with multiple <%= table_name %>" do
139
+ # Create multiple <%= table_name %> for pagination testing
140
+ <%= table_name %> = []
141
+ 10.times do |i|
142
+ <%= singular_table_name %> = <%= class_name %>.create!(
143
+ <% attributes.each_with_index do |attribute, index| -%>
144
+ <% if attribute.type == :references -%>
145
+ <%= attribute.name %>: @<%= attribute.name %><%= ',' if index < attributes.length - 1 %>
146
+ <% elsif attribute.type == :string -%>
147
+ <%= attribute.name %>: "Pagination Test <%= attribute.name.humanize %> #{i}"<%= ',' if index < attributes.length - 1 %>
148
+ <% elsif attribute.type == :text -%>
149
+ <%= attribute.name %>: "Pagination test <%= attribute.name.humanize %> content #{i}"<%= ',' if index < attributes.length - 1 %>
150
+ <% elsif attribute.type == :integer -%>
151
+ <%= attribute.name %>: i<%= ',' if index < attributes.length - 1 %>
152
+ <% elsif attribute.type == :boolean -%>
153
+ <%= attribute.name %>: i.even?<%= ',' if index < attributes.length - 1 %>
154
+ <% elsif attribute.type == :decimal || attribute.type == :float -%>
155
+ <%= attribute.name %>: i * 10.5<%= ',' if index < attributes.length - 1 %>
156
+ <% else -%>
157
+ <%= attribute.name %>: "test_#{i}"<%= ',' if index < attributes.length - 1 %>
158
+ <% end -%>
159
+ <% end -%>
160
+ )
161
+ <%= table_name %> << <%= singular_table_name %>
162
+ end
163
+
164
+ # Test first page
165
+ get <%= api_route_helper %>_url, params: { page: 1, limit: 5 }, headers: @auth_headers
166
+ assert_response :success
167
+
168
+ page1_response = JSON.parse(response.body)
169
+ assert_equal 5, page1_response['data'].size
170
+
171
+ pagination = page1_response['pagination']
172
+ assert_equal 1, pagination['page']
173
+ assert_equal 5, pagination['items']
174
+ assert pagination['count'] >= 10
175
+ assert pagination['pages'] >= 2
176
+ assert_equal false, pagination['first']
177
+ assert_equal false, pagination['last']
178
+
179
+ # Test second page
180
+ get <%= api_route_helper %>_url, params: { page: 2, limit: 5 }, headers: @auth_headers
181
+ assert_response :success
182
+
183
+ page2_response = JSON.parse(response.body)
184
+ assert_equal 5, page2_response['data'].size
185
+
186
+ pagination2 = page2_response['pagination']
187
+ assert_equal 2, pagination2['page']
188
+ assert_equal 5, pagination2['items']
189
+
190
+ # Verify different records on different pages
191
+ page1_ids = page1_response['data'].map { |item| item['id'] }
192
+ page2_ids = page2_response['data'].map { |item| item['id'] }
193
+ assert_empty (page1_ids & page2_ids), "Pages should contain different records"
194
+ end
195
+
196
+ # === ERROR HANDLING WORKFLOW TESTS ===
197
+
198
+ test "error handling workflow" do
199
+ # Test 1: Invalid authentication
200
+ get <%= api_route_helper %>_url, headers: { 'Authorization' => 'Bearer invalid_token' }
201
+ assert_response :unauthorized
202
+
203
+ error_response = JSON.parse(response.body)
204
+ assert_equal "Invalid token", error_response['error']
205
+
206
+ # Test 2: Missing authentication
207
+ get <%= api_route_helper %>_url
208
+ assert_response :unauthorized
209
+
210
+ error_response = JSON.parse(response.body)
211
+ assert_equal "No token provided", error_response['error']
212
+
213
+ # Test 3: Invalid resource ID
214
+ get <%= api_route_helper %>_url(99999), headers: @auth_headers
215
+ assert_response :not_found
216
+
217
+ # Test 4: Invalid create params
218
+ post <%= api_route_helper %>_url,
219
+ params: { data: { invalid_field: "invalid_value" } },
220
+ headers: @auth_headers
221
+
222
+ assert_response :unprocessable_entity
223
+
224
+ error_response = JSON.parse(response.body)
225
+ assert_includes error_response.keys, 'errors'
226
+
227
+ # Test 5: Malformed JSON
228
+ post <%= api_route_helper %>_url,
229
+ params: "invalid json",
230
+ headers: @auth_headers.merge('Content-Type' => 'application/json')
231
+
232
+ assert_response :bad_request
233
+ end
234
+
235
+ # === MULTI-TENANCY WORKFLOW TESTS ===
236
+
237
+ test "multi-tenancy isolation workflow" do
238
+ # Create another organization and user
239
+ other_organization = Organization.create!(name: "Other Organization")
240
+ other_user = User.create!(
241
+ email_address: "other@example.com",
242
+ username: "otheruser",
243
+ first_name: "Other",
244
+ last_name: "User",
245
+ password: "password123",
246
+ organization: other_organization
247
+ )
248
+ other_token = other_user.generate_jwt_token
249
+ other_headers = { 'Authorization' => "Bearer #{other_token}" }
250
+
251
+ # Create <%= singular_table_name %> in each organization
252
+ org1_<%= singular_table_name %> = <%= class_name %>.create!(
253
+ <% attributes.each_with_index do |attribute, index| -%>
254
+ <% if attribute.name == "organization" && attribute.type == :references -%>
255
+ <%= attribute.name %>: @organization<%= ',' if index < attributes.length - 1 %>
256
+ <% elsif attribute.name == "user" && attribute.type == :references -%>
257
+ <%= attribute.name %>: @user<%= ',' if index < attributes.length - 1 %>
258
+ <% elsif attribute.type == :references -%>
259
+ <%= attribute.name %>: @<%= attribute.name %><%= ',' if index < attributes.length - 1 %>
260
+ <% elsif attribute.type == :string -%>
261
+ <%= attribute.name %>: "Org 1 <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
262
+ <% elsif attribute.type == :text -%>
263
+ <%= attribute.name %>: "Org 1 <%= attribute.name.humanize %> content"<%= ',' if index < attributes.length - 1 %>
264
+ <% elsif attribute.type == :integer -%>
265
+ <%= attribute.name %>: 1<%= ',' if index < attributes.length - 1 %>
266
+ <% elsif attribute.type == :boolean -%>
267
+ <%= attribute.name %>: true<%= ',' if index < attributes.length - 1 %>
268
+ <% elsif attribute.type == :decimal || attribute.type == :float -%>
269
+ <%= attribute.name %>: 11.1<%= ',' if index < attributes.length - 1 %>
270
+ <% else -%>
271
+ <%= attribute.name %>: "org1_value"<%= ',' if index < attributes.length - 1 %>
272
+ <% end -%>
273
+ <% end -%>
274
+ )
275
+
276
+ org2_<%= singular_table_name %> = <%= class_name %>.create!(
277
+ <% attributes.each_with_index do |attribute, index| -%>
278
+ <% if attribute.name == "organization" && attribute.type == :references -%>
279
+ <%= attribute.name %>: other_organization<%= ',' if index < attributes.length - 1 %>
280
+ <% elsif attribute.name == "user" && attribute.type == :references -%>
281
+ <%= attribute.name %>: other_user<%= ',' if index < attributes.length - 1 %>
282
+ <% elsif attribute.type == :references -%>
283
+ <%= attribute.name %>: @<%= attribute.name %><%= ',' if index < attributes.length - 1 %>
284
+ <% elsif attribute.type == :string -%>
285
+ <%= attribute.name %>: "Org 2 <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
286
+ <% elsif attribute.type == :text -%>
287
+ <%= attribute.name %>: "Org 2 <%= attribute.name.humanize %> content"<%= ',' if index < attributes.length - 1 %>
288
+ <% elsif attribute.type == :integer -%>
289
+ <%= attribute.name %>: 2<%= ',' if index < attributes.length - 1 %>
290
+ <% elsif attribute.type == :boolean -%>
291
+ <%= attribute.name %>: false<%= ',' if index < attributes.length - 1 %>
292
+ <% elsif attribute.type == :decimal || attribute.type == :float -%>
293
+ <%= attribute.name %>: 22.2<%= ',' if index < attributes.length - 1 %>
294
+ <% else -%>
295
+ <%= attribute.name %>: "org2_value"<%= ',' if index < attributes.length - 1 %>
296
+ <% end -%>
297
+ <% end -%>
298
+ )
299
+
300
+ # Org 1 user should only see Org 1 <%= table_name %>
301
+ get <%= api_route_helper %>_url, headers: @auth_headers
302
+ assert_response :success
303
+
304
+ org1_response = JSON.parse(response.body)
305
+ org1_ids = org1_response['data'].map { |item| item['id'] }
306
+
307
+ assert_includes org1_ids, org1_<%= singular_table_name %>.id
308
+ assert_not_includes org1_ids, org2_<%= singular_table_name %>.id
309
+
310
+ # Org 2 user should only see Org 2 <%= table_name %>
311
+ get <%= api_route_helper %>_url, headers: other_headers
312
+ assert_response :success
313
+
314
+ org2_response = JSON.parse(response.body)
315
+ org2_ids = org2_response['data'].map { |item| item['id'] }
316
+
317
+ assert_includes org2_ids, org2_<%= singular_table_name %>.id
318
+ assert_not_includes org2_ids, org1_<%= singular_table_name %>.id
319
+
320
+ # Cross-organization access should be denied
321
+ get <%= api_route_helper %>_url(org2_<%= singular_table_name %>.id), headers: @auth_headers
322
+ assert_response :not_found
323
+
324
+ get <%= api_route_helper %>_url(org1_<%= singular_table_name %>.id), headers: other_headers
325
+ assert_response :not_found
326
+ end
327
+
328
+ # === FACET RESPONSE WORKFLOW TESTS ===
329
+
330
+ test "facet response format workflow" do
331
+ # Test index response (short facet)
332
+ get <%= api_route_helper %>_url, headers: @auth_headers
333
+ assert_response :success
334
+
335
+ index_response = JSON.parse(response.body)
336
+
337
+ # Verify pagination structure
338
+ assert_includes index_response.keys, 'data'
339
+ assert_includes index_response.keys, 'pagination'
340
+ assert_kind_of Array, index_response['data']
341
+ assert_kind_of Hash, index_response['pagination']
342
+
343
+ # Verify pagination metadata
344
+ pagination = index_response['pagination']
345
+ %w[page items count pages first last next prev].each do |key|
346
+ assert_includes pagination.keys, key, "Pagination should include #{key}"
347
+ end
348
+
349
+ # Test show response (details facet)
350
+ get <%= api_route_helper %>_url(@<%= singular_table_name %>.id), headers: @auth_headers
351
+ assert_response :success
352
+
353
+ show_response = JSON.parse(response.body)
354
+
355
+ # Verify response structure
356
+ assert_includes show_response.keys, 'data'
357
+ assert_kind_of Hash, show_response['data']
358
+
359
+ <%= singular_table_name %>_data = show_response['data']
360
+
361
+ # Verify core fields are present
362
+ assert_includes <%= singular_table_name %>_data.keys, 'id'
363
+ assert_includes <%= singular_table_name %>_data.keys, 'organization'
364
+ assert_includes <%= singular_table_name %>_data.keys, 'user'
365
+
366
+ # Verify associations are properly included
367
+ assert_kind_of Hash, <%= singular_table_name %>_data['organization']
368
+ assert_kind_of Hash, <%= singular_table_name %>_data['user']
369
+ assert_equal @organization.id, <%= singular_table_name %>_data['organization']['id']
370
+ assert_equal @user.id, <%= singular_table_name %>_data['user']['id']
371
+ end
372
+
373
+ # === CONCURRENT ACCESS WORKFLOW TESTS ===
374
+
375
+ test "concurrent modifications workflow" do
376
+ # Create a <%= singular_table_name %> to modify
377
+ test_<%= singular_table_name %> = <%= class_name %>.create!(
378
+ <% attributes.each_with_index do |attribute, index| -%>
379
+ <% if attribute.type == :references -%>
380
+ <%= attribute.name %>: @<%= attribute.name %><%= ',' if index < attributes.length - 1 %>
381
+ <% elsif attribute.type == :string -%>
382
+ <%= attribute.name %>: "Concurrent Test <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
383
+ <% elsif attribute.type == :text -%>
384
+ <%= attribute.name %>: "Concurrent test <%= attribute.name.humanize %> content"<%= ',' if index < attributes.length - 1 %>
385
+ <% elsif attribute.type == :integer -%>
386
+ <%= attribute.name %>: 50<%= ',' if index < attributes.length - 1 %>
387
+ <% elsif attribute.type == :boolean -%>
388
+ <%= attribute.name %>: true<%= ',' if index < attributes.length - 1 %>
389
+ <% elsif attribute.type == :decimal || attribute.type == :float -%>
390
+ <%= attribute.name %>: 50.5<%= ',' if index < attributes.length - 1 %>
391
+ <% else -%>
392
+ <%= attribute.name %>: "concurrent_test"<%= ',' if index < attributes.length - 1 %>
393
+ <% end -%>
394
+ <% end -%>
395
+ )
396
+
397
+ # Simulate concurrent read
398
+ get <%= api_route_helper %>_url(test_<%= singular_table_name %>.id), headers: @auth_headers
399
+ assert_response :success
400
+
401
+ # Simulate concurrent update
402
+ <% if attributes.any? { |attr| attr.type == :string && attr.name != "organization" && attr.name != "user" } -%>
403
+ <% string_attr = attributes.find { |attr| attr.type == :string && attr.name != "organization" && attr.name != "user" } -%>
404
+ patch <%= api_route_helper %>_url(test_<%= singular_table_name %>.id),
405
+ params: { data: { <%= string_attr.name %>: "Updated Concurrent <%= string_attr.name.humanize %>" } },
406
+ headers: @auth_headers
407
+
408
+ assert_response :success
409
+
410
+ # Verify update was successful
411
+ get <%= api_route_helper %>_url(test_<%= singular_table_name %>.id), headers: @auth_headers
412
+ assert_response :success
413
+
414
+ updated_response = JSON.parse(response.body)
415
+ assert_equal "Updated Concurrent <%= string_attr.name.humanize %>", updated_response['data']['<%= string_attr.name %>']
416
+ <% else -%>
417
+ # Concurrent update test - customize based on your model's attributes
418
+ patch <%= api_route_helper %>_url(test_<%= singular_table_name %>.id),
419
+ params: { data: { organization_id: @organization.id } },
420
+ headers: @auth_headers
421
+
422
+ assert_response :success
423
+ <% end -%>
424
+ end
425
+
426
+ # === BULK OPERATIONS WORKFLOW TESTS ===
427
+
428
+ test "bulk operations workflow" do
429
+ # Create multiple <%= table_name %> for bulk testing
430
+ bulk_<%= table_name %> = []
431
+ 5.times do |i|
432
+ <%= singular_table_name %> = <%= class_name %>.create!(
433
+ <% attributes.each_with_index do |attribute, index| -%>
434
+ <% if attribute.type == :references -%>
435
+ <%= attribute.name %>: @<%= attribute.name %><%= ',' if index < attributes.length - 1 %>
436
+ <% elsif attribute.type == :string -%>
437
+ <%= attribute.name %>: "Bulk Test <%= attribute.name.humanize %> #{i}"<%= ',' if index < attributes.length - 1 %>
438
+ <% elsif attribute.type == :text -%>
439
+ <%= attribute.name %>: "Bulk test <%= attribute.name.humanize %> content #{i}"<%= ',' if index < attributes.length - 1 %>
440
+ <% elsif attribute.type == :integer -%>
441
+ <%= attribute.name %>: i * 10<%= ',' if index < attributes.length - 1 %>
442
+ <% elsif attribute.type == :boolean -%>
443
+ <%= attribute.name %>: i.even?<%= ',' if index < attributes.length - 1 %>
444
+ <% elsif attribute.type == :decimal || attribute.type == :float -%>
445
+ <%= attribute.name %>: i * 10.5<%= ',' if index < attributes.length - 1 %>
446
+ <% else -%>
447
+ <%= attribute.name %>: "bulk_#{i}"<%= ',' if index < attributes.length - 1 %>
448
+ <% end -%>
449
+ <% end -%>
450
+ )
451
+ bulk_<%= table_name %> << <%= singular_table_name %>
452
+ end
453
+
454
+ # Test bulk retrieval
455
+ get <%= api_route_helper %>_url, headers: @auth_headers
456
+ assert_response :success
457
+
458
+ bulk_response = JSON.parse(response.body)
459
+ retrieved_ids = bulk_response['data'].map { |item| item['id'] }
460
+
461
+ # Verify all bulk <%= table_name %> are included
462
+ bulk_<%= table_name %>.each do |<%= singular_table_name %>|
463
+ assert_includes retrieved_ids, <%= singular_table_name %>.id, "Bulk <%= singular_table_name %> should be in results"
464
+ end
465
+
466
+ # Test individual retrieval of bulk items
467
+ bulk_<%= table_name %>.each do |<%= singular_table_name %>|
468
+ get <%= api_route_helper %>_url(<%= singular_table_name %>.id), headers: @auth_headers
469
+ assert_response :success
470
+
471
+ item_response = JSON.parse(response.body)
472
+ assert_equal <%= singular_table_name %>.id, item_response['data']['id']
473
+ end
474
+
475
+ # Test bulk deletion
476
+ bulk_<%= table_name %>.each do |<%= singular_table_name %>|
477
+ delete <%= api_route_helper %>_url(<%= singular_table_name %>.id), headers: @auth_headers
478
+ assert_response :no_content
479
+ end
480
+
481
+ # Verify all items are deleted
482
+ bulk_<%= table_name %>.each do |<%= singular_table_name %>|
483
+ get <%= api_route_helper %>_url(<%= singular_table_name %>.id), headers: @auth_headers
484
+ assert_response :not_found
485
+ end
486
+ end
487
+ end