monday_ruby 1.0.0 → 1.2.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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.env +1 -1
  3. data/.rspec +0 -1
  4. data/.rubocop.yml +19 -0
  5. data/.simplecov +1 -0
  6. data/CHANGELOG.md +49 -0
  7. data/CONTRIBUTING.md +165 -0
  8. data/README.md +167 -88
  9. data/docs/.vitepress/config.mjs +255 -0
  10. data/docs/.vitepress/theme/index.js +4 -0
  11. data/docs/.vitepress/theme/style.css +43 -0
  12. data/docs/README.md +80 -0
  13. data/docs/explanation/architecture.md +507 -0
  14. data/docs/explanation/best-practices/errors.md +478 -0
  15. data/docs/explanation/best-practices/performance.md +1084 -0
  16. data/docs/explanation/best-practices/rate-limiting.md +630 -0
  17. data/docs/explanation/best-practices/testing.md +820 -0
  18. data/docs/explanation/column-values.md +857 -0
  19. data/docs/explanation/design.md +795 -0
  20. data/docs/explanation/graphql.md +356 -0
  21. data/docs/explanation/migration/v1.md +808 -0
  22. data/docs/explanation/pagination.md +447 -0
  23. data/docs/guides/advanced/batch.md +1274 -0
  24. data/docs/guides/advanced/complex-queries.md +1114 -0
  25. data/docs/guides/advanced/errors.md +818 -0
  26. data/docs/guides/advanced/pagination.md +934 -0
  27. data/docs/guides/advanced/rate-limiting.md +981 -0
  28. data/docs/guides/authentication.md +286 -0
  29. data/docs/guides/boards/create.md +386 -0
  30. data/docs/guides/boards/delete.md +405 -0
  31. data/docs/guides/boards/duplicate.md +511 -0
  32. data/docs/guides/boards/query.md +530 -0
  33. data/docs/guides/boards/update.md +453 -0
  34. data/docs/guides/columns/create.md +452 -0
  35. data/docs/guides/columns/metadata.md +492 -0
  36. data/docs/guides/columns/query.md +455 -0
  37. data/docs/guides/columns/update-multiple.md +459 -0
  38. data/docs/guides/columns/update-values.md +509 -0
  39. data/docs/guides/files/add-to-column.md +40 -0
  40. data/docs/guides/files/add-to-update.md +37 -0
  41. data/docs/guides/files/clear-column.md +33 -0
  42. data/docs/guides/first-request.md +285 -0
  43. data/docs/guides/folders/manage.md +750 -0
  44. data/docs/guides/groups/items.md +626 -0
  45. data/docs/guides/groups/manage.md +501 -0
  46. data/docs/guides/installation.md +169 -0
  47. data/docs/guides/items/create.md +493 -0
  48. data/docs/guides/items/delete.md +514 -0
  49. data/docs/guides/items/query.md +605 -0
  50. data/docs/guides/items/subitems.md +483 -0
  51. data/docs/guides/items/update.md +699 -0
  52. data/docs/guides/updates/manage.md +619 -0
  53. data/docs/guides/use-cases/dashboard.md +1421 -0
  54. data/docs/guides/use-cases/import.md +1962 -0
  55. data/docs/guides/use-cases/task-management.md +1381 -0
  56. data/docs/guides/workspaces/manage.md +502 -0
  57. data/docs/index.md +69 -0
  58. data/docs/package-lock.json +2468 -0
  59. data/docs/package.json +13 -0
  60. data/docs/reference/client.md +540 -0
  61. data/docs/reference/configuration.md +586 -0
  62. data/docs/reference/errors.md +693 -0
  63. data/docs/reference/resources/account.md +208 -0
  64. data/docs/reference/resources/activity-log.md +369 -0
  65. data/docs/reference/resources/board-view.md +359 -0
  66. data/docs/reference/resources/board.md +393 -0
  67. data/docs/reference/resources/column.md +543 -0
  68. data/docs/reference/resources/file.md +236 -0
  69. data/docs/reference/resources/folder.md +386 -0
  70. data/docs/reference/resources/group.md +507 -0
  71. data/docs/reference/resources/item.md +348 -0
  72. data/docs/reference/resources/subitem.md +267 -0
  73. data/docs/reference/resources/update.md +259 -0
  74. data/docs/reference/resources/workspace.md +213 -0
  75. data/docs/reference/response.md +560 -0
  76. data/docs/tutorial/first-integration.md +713 -0
  77. data/lib/monday/client.rb +41 -2
  78. data/lib/monday/configuration.rb +13 -0
  79. data/lib/monday/deprecation.rb +23 -0
  80. data/lib/monday/error.rb +5 -2
  81. data/lib/monday/request.rb +19 -1
  82. data/lib/monday/resources/base.rb +4 -0
  83. data/lib/monday/resources/board.rb +52 -0
  84. data/lib/monday/resources/column.rb +6 -0
  85. data/lib/monday/resources/file.rb +56 -0
  86. data/lib/monday/resources/folder.rb +55 -0
  87. data/lib/monday/resources/group.rb +66 -0
  88. data/lib/monday/resources/item.rb +62 -0
  89. data/lib/monday/util.rb +33 -1
  90. data/lib/monday/version.rb +1 -1
  91. data/lib/monday_ruby.rb +1 -0
  92. metadata +92 -11
  93. data/monday_ruby.gemspec +0 -39
@@ -0,0 +1,820 @@
1
+ # Testing Best Practices
2
+
3
+ Testing code that integrates with external APIs presents unique challenges. You need to balance test reliability, speed, and realism while managing dependencies on external services. This guide explores testing philosophies and patterns for the monday_ruby gem.
4
+
5
+ ## Testing Philosophy for API Integrations
6
+
7
+ Testing API integrations requires a different mindset than testing pure business logic.
8
+
9
+ ### The Core Tension
10
+
11
+ **Reliability vs. Realism**: Tests should be:
12
+ - **Fast**: Run in seconds, not minutes
13
+ - **Reliable**: Pass consistently, not dependent on network or API state
14
+ - **Isolated**: Not affected by other tests or external changes
15
+ - **Realistic**: Actually test your integration, not just mocks
16
+
17
+ You can't maximize all four simultaneously. The art of testing API integrations is finding the right balance for your needs.
18
+
19
+ ### The Testing Pyramid for API Integrations
20
+
21
+ ```
22
+ /\
23
+ / \ E2E (Real API) - Slow, realistic
24
+ / \
25
+ /------\ Integration (VCR) - Medium speed, recorded reality
26
+ / \
27
+ /----------\ Unit (Mocks) - Fast, isolated
28
+ ```
29
+
30
+ **Unit tests (70%)**: Test business logic with mocked API responses
31
+ **Integration tests (25%)**: Test API interactions with VCR cassettes
32
+ **E2E tests (5%)**: Occasional real API calls to verify cassettes are current
33
+
34
+ This distribution gives you fast feedback (unit tests) while ensuring your integration actually works (VCR/E2E tests).
35
+
36
+ ## Unit vs Integration vs End-to-End Tests
37
+
38
+ ### Unit Tests (Mocked)
39
+
40
+ Test your code's logic without making real API calls:
41
+
42
+ ```ruby
43
+ RSpec.describe BoardSync do
44
+ it 'processes board data correctly' do
45
+ mock_response = {
46
+ 'data' => {
47
+ 'boards' => [
48
+ { 'id' => '123', 'name' => 'Test Board' }
49
+ ]
50
+ }
51
+ }
52
+
53
+ allow(client.board).to receive(:query).and_return(mock_response)
54
+
55
+ sync = BoardSync.new(client)
56
+ result = sync.fetch_boards
57
+
58
+ expect(result.first.name).to eq('Test Board')
59
+ end
60
+ end
61
+ ```
62
+
63
+ **Pros:**
64
+ - Very fast (milliseconds)
65
+ - No network dependencies
66
+ - Full control over responses (easy to test edge cases)
67
+ - No API quota usage
68
+
69
+ **Cons:**
70
+ - Doesn't test actual API integration
71
+ - Mocks can drift from reality
72
+ - Brittle (breaks when implementation changes)
73
+
74
+ **When to use**: Testing business logic, error handling, data transformation
75
+
76
+ ### Integration Tests (VCR)
77
+
78
+ Test with real API responses recorded to cassettes:
79
+
80
+ ```ruby
81
+ RSpec.describe 'Board API', :vcr do
82
+ it 'fetches boards with correct fields' do
83
+ response = client.board.query(
84
+ ids: [12345],
85
+ select: ['id', 'name']
86
+ )
87
+
88
+ expect(response.dig('data', 'boards')).to be_an(Array)
89
+ expect(response.dig('data', 'boards', 0, 'name')).to be_a(String)
90
+ end
91
+ end
92
+ ```
93
+
94
+ **Pros:**
95
+ - Tests actual integration (real request/response structure)
96
+ - Fast (replays from cassettes)
97
+ - Deterministic (same cassette, same result)
98
+ - Safe (no accidental API mutations)
99
+
100
+ **Cons:**
101
+ - Cassettes can become outdated
102
+ - Harder to test error scenarios
103
+ - Requires initial real API call to record
104
+ - Cassettes need maintenance
105
+
106
+ **When to use**: Testing API client methods, query building, response parsing
107
+
108
+ ### End-to-End Tests (Real API)
109
+
110
+ Make actual API calls:
111
+
112
+ ```ruby
113
+ RSpec.describe 'Board API', :e2e do
114
+ it 'creates and fetches a board' do
115
+ board = client.board.create(
116
+ board_name: "Test Board #{Time.now.to_i}"
117
+ )
118
+
119
+ board_id = board.dig('data', 'create_board', 'id')
120
+
121
+ fetched = client.board.query(ids: [board_id])
122
+ expect(fetched.dig('data', 'boards', 0, 'id')).to eq(board_id)
123
+
124
+ # Cleanup
125
+ client.board.delete(board_id: board_id)
126
+ end
127
+ end
128
+ ```
129
+
130
+ **Pros:**
131
+ - Tests real integration (nothing mocked)
132
+ - Catches API changes immediately
133
+ - Validates current API behavior
134
+
135
+ **Cons:**
136
+ - Slow (network latency)
137
+ - Unreliable (network issues, API changes, rate limits)
138
+ - Uses API quota
139
+ - Requires cleanup (can leave test data)
140
+ - Can't run offline
141
+
142
+ **When to use**: Periodic verification, pre-release smoke tests, cassette validation
143
+
144
+ ## Mocking vs VCR vs Real API Calls
145
+
146
+ ### When to Use Each
147
+
148
+ | Scenario | Approach | Reason |
149
+ |----------|----------|--------|
150
+ | Testing error handling logic | Mock | Need to control exact error responses |
151
+ | Testing retry mechanisms | Mock | Need to simulate multiple failure scenarios |
152
+ | Testing request building | VCR | Need realistic request structure |
153
+ | Testing response parsing | VCR | Need realistic response structure |
154
+ | Verifying API hasn't changed | Real API | Need current truth |
155
+ | CI/CD pipeline | VCR | Need speed and reliability |
156
+ | Local development | VCR | Don't waste API quota |
157
+ | Pre-production checks | Real API | Final verification |
158
+
159
+ ### Combining Approaches
160
+
161
+ You don't have to choose just one. Use tags to organize tests:
162
+
163
+ ```ruby
164
+ # spec/spec_helper.rb
165
+ RSpec.configure do |config|
166
+ config.around(:each, :real_api) do |example|
167
+ VCR.turn_off!
168
+ example.run
169
+ VCR.turn_on!
170
+ end
171
+ end
172
+
173
+ # Test with VCR by default
174
+ RSpec.describe 'Board API', :vcr do
175
+ it 'fetches board' do
176
+ # Uses VCR cassette
177
+ end
178
+ end
179
+
180
+ # Specific test uses real API
181
+ RSpec.describe 'Board API', :real_api do
182
+ it 'validates API compatibility' do
183
+ # Makes real API call
184
+ end
185
+ end
186
+ ```
187
+
188
+ Run VCR tests normally, real API tests occasionally:
189
+
190
+ ```bash
191
+ # Normal run (VCR only)
192
+ bundle exec rspec
193
+
194
+ # Full run including real API tests
195
+ bundle exec rspec --tag real_api
196
+ ```
197
+
198
+ ## Testing with VCR Cassettes
199
+
200
+ VCR is the monday_ruby gem's primary testing approach. Understanding how to use it effectively is crucial.
201
+
202
+ ### How VCR Works
203
+
204
+ 1. **First run**: Makes real HTTP request, records to cassette file
205
+ 2. **Subsequent runs**: Replays response from cassette, no HTTP request
206
+ 3. **Cassette matching**: Matches requests by URI, method, and body
207
+
208
+ ### Basic VCR Configuration
209
+
210
+ ```ruby
211
+ # spec/spec_helper.rb
212
+ require 'vcr'
213
+
214
+ VCR.configure do |config|
215
+ config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
216
+ config.hook_into :webmock
217
+ config.configure_rspec_metadata!
218
+
219
+ # Filter sensitive data
220
+ config.filter_sensitive_data('<MONDAY_TOKEN>') do |interaction|
221
+ interaction.request.headers['Authorization']&.first
222
+ end
223
+
224
+ # Match requests by URI, method, and body
225
+ config.default_cassette_options = {
226
+ match_requests_on: [:method, :uri, :body]
227
+ }
228
+ end
229
+ ```
230
+
231
+ ### Recording Cassettes
232
+
233
+ ```ruby
234
+ RSpec.describe 'Board API', :vcr do
235
+ it 'fetches boards' do
236
+ # First run: makes real API call, saves to
237
+ # spec/fixtures/vcr_cassettes/Board_API/fetches_boards.yml
238
+ response = client.board.query(ids: [12345])
239
+ expect(response).to be_a(Monday::Response)
240
+ end
241
+ end
242
+ ```
243
+
244
+ ### Filtering Sensitive Data
245
+
246
+ **Critical**: Don't commit API tokens to cassettes!
247
+
248
+ ```ruby
249
+ VCR.configure do |config|
250
+ # Replace token in request headers
251
+ config.filter_sensitive_data('<MONDAY_TOKEN>') do |interaction|
252
+ interaction.request.headers['Authorization']&.first
253
+ end
254
+
255
+ # Replace token in request body (if present)
256
+ config.filter_sensitive_data('<MONDAY_TOKEN>') do
257
+ ENV['MONDAY_TOKEN']
258
+ end
259
+
260
+ # Replace account IDs
261
+ config.filter_sensitive_data('<ACCOUNT_ID>') do
262
+ ENV['MONDAY_ACCOUNT_ID']
263
+ end
264
+ end
265
+ ```
266
+
267
+ Cassettes will contain `<MONDAY_TOKEN>` instead of actual credentials.
268
+
269
+ ### Updating Cassettes
270
+
271
+ Cassettes become outdated when APIs change. Update them by deleting and re-recording:
272
+
273
+ ```bash
274
+ # Delete all cassettes
275
+ rm -rf spec/fixtures/vcr_cassettes/
276
+
277
+ # Re-record (requires valid API token in .env)
278
+ bundle exec rspec
279
+ ```
280
+
281
+ Or update selectively:
282
+
283
+ ```bash
284
+ # Delete specific cassette
285
+ rm spec/fixtures/vcr_cassettes/Board_API/fetches_boards.yml
286
+
287
+ # Re-record just that test
288
+ bundle exec rspec spec/monday/resources/board_spec.rb:10
289
+ ```
290
+
291
+ ### Cassette Maintenance
292
+
293
+ **When to update cassettes:**
294
+ - API response structure changes
295
+ - Adding new test cases
296
+ - Testing new API features
297
+ - Before major releases (verify API compatibility)
298
+
299
+ **Best practice**: Review cassette changes in PRs. New fields or changed structures may indicate breaking API changes.
300
+
301
+ ## Test Isolation and Idempotency
302
+
303
+ Tests should be independent and repeatable.
304
+
305
+ ### Test Isolation Principles
306
+
307
+ 1. **No shared state**: Each test sets up its own data
308
+ 2. **No order dependencies**: Tests pass in any order
309
+ 3. **Clean up**: Remove test data after execution
310
+
311
+ ### Achieving Isolation with Mocks
312
+
313
+ ```ruby
314
+ RSpec.describe BoardService do
315
+ let(:client) { instance_double(Monday::Client) }
316
+ let(:board_resource) { instance_double(Monday::Resources::Board) }
317
+
318
+ before do
319
+ allow(client).to receive(:board).and_return(board_resource)
320
+ end
321
+
322
+ it 'fetches board' do
323
+ allow(board_resource).to receive(:query).and_return(mock_data)
324
+
325
+ service = BoardService.new(client)
326
+ result = service.fetch
327
+
328
+ expect(result).to eq(expected_result)
329
+ end
330
+
331
+ # Each test is isolated - mocks are fresh
332
+ it 'handles errors' do
333
+ allow(board_resource).to receive(:query).and_raise(Monday::Error)
334
+
335
+ service = BoardService.new(client)
336
+ expect { service.fetch }.to raise_error(Monday::Error)
337
+ end
338
+ end
339
+ ```
340
+
341
+ ### Achieving Isolation with VCR
342
+
343
+ VCR cassettes are read-only, so they're inherently isolated:
344
+
345
+ ```ruby
346
+ RSpec.describe 'Board API', :vcr do
347
+ it 'fetches board 1' do
348
+ response = client.board.query(ids: [12345])
349
+ # Uses cassette: Board_API/fetches_board_1.yml
350
+ end
351
+
352
+ it 'fetches board 2' do
353
+ response = client.board.query(ids: [67890])
354
+ # Uses cassette: Board_API/fetches_board_2.yml
355
+ # Independent of first test
356
+ end
357
+ end
358
+ ```
359
+
360
+ ### Achieving Idempotency with Real API
361
+
362
+ Real API tests are trickier—you need cleanup:
363
+
364
+ ```ruby
365
+ RSpec.describe 'Board lifecycle', :real_api do
366
+ after do
367
+ # Cleanup: delete any boards created during test
368
+ @created_boards&.each do |board_id|
369
+ client.board.delete(board_id: board_id)
370
+ rescue Monday::Error
371
+ # Ignore errors (board may already be deleted)
372
+ end
373
+ end
374
+
375
+ it 'creates and deletes board' do
376
+ @created_boards = []
377
+
378
+ board = client.board.create(board_name: 'Test Board')
379
+ board_id = board.dig('data', 'create_board', 'id')
380
+ @created_boards << board_id
381
+
382
+ # Test operations...
383
+
384
+ client.board.delete(board_id: board_id)
385
+ @created_boards.delete(board_id)
386
+ end
387
+ end
388
+ ```
389
+
390
+ **Better approach**: Use unique identifiers to avoid collisions:
391
+
392
+ ```ruby
393
+ it 'creates board with unique name' do
394
+ board_name = "Test Board #{SecureRandom.uuid}"
395
+ board = client.board.create(board_name: board_name)
396
+ # Even if cleanup fails, won't conflict with other tests
397
+ end
398
+ ```
399
+
400
+ ## Testing Error Scenarios
401
+
402
+ Error handling is critical but often under-tested.
403
+
404
+ ### Mocking Error Responses
405
+
406
+ ```ruby
407
+ RSpec.describe 'error handling' do
408
+ it 'handles authorization errors' do
409
+ allow(client).to receive(:make_request)
410
+ .and_raise(Monday::AuthorizationError.new('Invalid token'))
411
+
412
+ expect { fetch_board }.to raise_error(Monday::AuthorizationError)
413
+ end
414
+
415
+ it 'handles rate limiting' do
416
+ allow(client).to receive(:make_request)
417
+ .and_raise(Monday::ComplexityError.new('Rate limit exceeded'))
418
+
419
+ expect { fetch_board }.to raise_error(Monday::ComplexityError)
420
+ end
421
+
422
+ it 'handles network timeouts' do
423
+ allow(client).to receive(:make_request)
424
+ .and_raise(Timeout::Error)
425
+
426
+ expect { fetch_board }.to raise_error(Timeout::Error)
427
+ end
428
+ end
429
+ ```
430
+
431
+ ### Testing Error Recovery
432
+
433
+ ```ruby
434
+ it 'retries on transient errors' do
435
+ call_count = 0
436
+
437
+ allow(client).to receive(:make_request) do
438
+ call_count += 1
439
+ raise Monday::InternalServerError if call_count < 3
440
+ mock_success_response
441
+ end
442
+
443
+ result = fetch_with_retry
444
+ expect(call_count).to eq(3)
445
+ expect(result).to eq(mock_success_response)
446
+ end
447
+
448
+ it 'gives up after max retries' do
449
+ allow(client).to receive(:make_request)
450
+ .and_raise(Monday::InternalServerError)
451
+
452
+ expect { fetch_with_retry(max_attempts: 3) }
453
+ .to raise_error(Monday::InternalServerError)
454
+ end
455
+ ```
456
+
457
+ ### VCR Error Cassettes
458
+
459
+ You can record error responses too:
460
+
461
+ ```ruby
462
+ # First run: trigger actual error (e.g., invalid ID)
463
+ # VCR records the error response
464
+
465
+ RSpec.describe 'error responses', :vcr do
466
+ it 'handles not found error' do
467
+ expect {
468
+ client.board.query(ids: [999999999])
469
+ }.to raise_error(Monday::ResourceNotFoundError)
470
+ end
471
+ end
472
+ ```
473
+
474
+ The cassette contains the actual error response structure.
475
+
476
+ ## Testing Pagination Logic
477
+
478
+ Pagination is complex and error-prone. Test it thoroughly.
479
+
480
+ ### Mocking Paginated Responses
481
+
482
+ ```ruby
483
+ RSpec.describe 'pagination' do
484
+ it 'fetches all pages' do
485
+ page1 = { 'data' => { 'items' => [{ 'id' => 1 }, { 'id' => 2 }] } }
486
+ page2 = { 'data' => { 'items' => [{ 'id' => 3 }, { 'id' => 4 }] } }
487
+ page3 = { 'data' => { 'items' => [] } } # Empty = end
488
+
489
+ allow(client.item).to receive(:query_by_board)
490
+ .with(hash_including(page: 1)).and_return(page1)
491
+ allow(client.item).to receive(:query_by_board)
492
+ .with(hash_including(page: 2)).and_return(page2)
493
+ allow(client.item).to receive(:query_by_board)
494
+ .with(hash_including(page: 3)).and_return(page3)
495
+
496
+ all_items = fetch_all_items
497
+ expect(all_items.count).to eq(4)
498
+ end
499
+
500
+ it 'handles empty first page' do
501
+ empty = { 'data' => { 'items' => [] } }
502
+
503
+ allow(client.item).to receive(:query_by_board)
504
+ .and_return(empty)
505
+
506
+ all_items = fetch_all_items
507
+ expect(all_items).to be_empty
508
+ end
509
+ end
510
+ ```
511
+
512
+ ### Testing Pagination Edge Cases
513
+
514
+ ```ruby
515
+ it 'stops at max pages to prevent infinite loops' do
516
+ # Mock pagination that never ends
517
+ non_empty = { 'data' => { 'items' => [{ 'id' => 1 }] } }
518
+ allow(client.item).to receive(:query_by_board)
519
+ .and_return(non_empty)
520
+
521
+ # Should stop at max_pages
522
+ all_items = fetch_all_items(max_pages: 10)
523
+ expect(all_items.count).to eq(10)
524
+ end
525
+
526
+ it 'handles errors mid-pagination' do
527
+ page1 = { 'data' => { 'items' => [{ 'id' => 1 }] } }
528
+
529
+ allow(client.item).to receive(:query_by_board)
530
+ .with(hash_including(page: 1)).and_return(page1)
531
+ allow(client.item).to receive(:query_by_board)
532
+ .with(hash_including(page: 2)).and_raise(Monday::Error)
533
+
534
+ expect { fetch_all_items }.to raise_error(Monday::Error)
535
+ end
536
+ ```
537
+
538
+ ## Testing Rate Limiting Behavior
539
+
540
+ Rate limiting is hard to test realistically without actually hitting rate limits.
541
+
542
+ ### Mocking Rate Limit Errors
543
+
544
+ ```ruby
545
+ it 'backs off when rate limited' do
546
+ allow(client).to receive(:make_request)
547
+ .and_raise(Monday::ComplexityError)
548
+ .exactly(3).times
549
+ .then.return(mock_success_response)
550
+
551
+ expect(self).to receive(:sleep).with(2)
552
+ expect(self).to receive(:sleep).with(4)
553
+ expect(self).to receive(:sleep).with(8)
554
+
555
+ result = fetch_with_backoff
556
+ expect(result).to eq(mock_success_response)
557
+ end
558
+ ```
559
+
560
+ ### Testing Rate Limit Tracking
561
+
562
+ ```ruby
563
+ RSpec.describe ComplexityTracker do
564
+ it 'allows requests within budget' do
565
+ tracker = ComplexityTracker.new(budget_per_minute: 1000)
566
+
567
+ expect(tracker.can_request?(cost: 500)).to be true
568
+ tracker.record(cost: 500)
569
+ expect(tracker.can_request?(cost: 400)).to be true
570
+ end
571
+
572
+ it 'blocks requests exceeding budget' do
573
+ tracker = ComplexityTracker.new(budget_per_minute: 1000)
574
+ tracker.record(cost: 900)
575
+
576
+ expect(tracker.can_request?(cost: 200)).to be false
577
+ end
578
+
579
+ it 'resets budget after time window' do
580
+ tracker = ComplexityTracker.new(budget_per_minute: 1000)
581
+ tracker.record(cost: 1000)
582
+
583
+ Timecop.travel(Time.now + 61) do
584
+ expect(tracker.can_request?(cost: 500)).to be true
585
+ end
586
+ end
587
+ end
588
+ ```
589
+
590
+ Use `Timecop` or `travel_to` to test time-based logic without waiting.
591
+
592
+ ## CI/CD Considerations
593
+
594
+ Running tests in CI/CD requires special considerations.
595
+
596
+ ### VCR in CI
597
+
598
+ VCR cassettes make CI simple:
599
+
600
+ ```yaml
601
+ # .github/workflows/test.yml
602
+ name: Tests
603
+ on: [push, pull_request]
604
+
605
+ jobs:
606
+ test:
607
+ runs-on: ubuntu-latest
608
+ steps:
609
+ - uses: actions/checkout@v2
610
+ - uses: ruby/setup-ruby@v1
611
+ with:
612
+ ruby-version: 3.2
613
+ bundler-cache: true
614
+ - run: bundle exec rspec
615
+ # No API token needed - uses VCR cassettes
616
+ ```
617
+
618
+ ### Real API Tests in CI
619
+
620
+ If running real API tests in CI:
621
+
622
+ ```yaml
623
+ jobs:
624
+ test:
625
+ steps:
626
+ - run: bundle exec rspec
627
+ env:
628
+ MONDAY_TOKEN: ${{ secrets.MONDAY_TOKEN }}
629
+ # Only run real API tests in certain conditions
630
+ - run: bundle exec rspec --tag real_api
631
+ if: github.event_name == 'schedule' # Only in nightly runs
632
+ env:
633
+ MONDAY_TOKEN: ${{ secrets.MONDAY_TOKEN }}
634
+ ```
635
+
636
+ ### Cassette Validation in CI
637
+
638
+ Periodically verify cassettes are up-to-date:
639
+
640
+ ```yaml
641
+ # Run weekly to catch API changes
642
+ on:
643
+ schedule:
644
+ - cron: '0 0 * * 0' # Weekly
645
+
646
+ jobs:
647
+ validate-cassettes:
648
+ steps:
649
+ - run: rm -rf spec/fixtures/vcr_cassettes
650
+ - run: bundle exec rspec
651
+ env:
652
+ MONDAY_TOKEN: ${{ secrets.MONDAY_TOKEN }}
653
+ - uses: peter-evans/create-pull-request@v4
654
+ with:
655
+ title: Update VCR cassettes
656
+ body: Automated cassette update from CI
657
+ ```
658
+
659
+ This creates a PR if cassettes change, allowing review before merging.
660
+
661
+ ## Test Data Management
662
+
663
+ Managing test data is crucial for reliable tests.
664
+
665
+ ### Fixture Data
666
+
667
+ Store mock responses as fixtures:
668
+
669
+ ```ruby
670
+ # spec/fixtures/board_response.json
671
+ {
672
+ "data": {
673
+ "boards": [
674
+ {"id": "123", "name": "Test Board"}
675
+ ]
676
+ }
677
+ }
678
+
679
+ # spec/spec_helper.rb
680
+ def load_fixture(name)
681
+ JSON.parse(File.read("spec/fixtures/#{name}.json"))
682
+ end
683
+
684
+ # In tests
685
+ RSpec.describe 'BoardService' do
686
+ it 'processes board data' do
687
+ board_data = load_fixture('board_response')
688
+ allow(client.board).to receive(:query).and_return(board_data)
689
+
690
+ service = BoardService.new(client)
691
+ result = service.process
692
+
693
+ expect(result).to be_a(ProcessedBoard)
694
+ end
695
+ end
696
+ ```
697
+
698
+ ### Factories for Test Data
699
+
700
+ Use factories for creating test objects:
701
+
702
+ ```ruby
703
+ # spec/factories/monday_responses.rb
704
+ FactoryBot.define do
705
+ factory :board_response, class: Hash do
706
+ skip_create # Don't persist
707
+
708
+ sequence(:id) { |n| n.to_s }
709
+ name { "Board #{id}" }
710
+
711
+ initialize_with { attributes.stringify_keys }
712
+ end
713
+ end
714
+
715
+ # In tests
716
+ let(:board) { build(:board_response, name: 'Custom Board') }
717
+ ```
718
+
719
+ ### Real API Test Data
720
+
721
+ For real API tests, use a dedicated test account:
722
+
723
+ ```ruby
724
+ # config/test_data.yml
725
+ test_account:
726
+ board_id: 12345
727
+ item_id: 67890
728
+ user_id: 11111
729
+
730
+ # spec/support/test_data.rb
731
+ def test_board_id
732
+ YAML.load_file('config/test_data.yml')['test_account']['board_id']
733
+ end
734
+
735
+ # In tests
736
+ it 'fetches test board', :real_api do
737
+ response = client.board.query(ids: [test_board_id])
738
+ expect(response).to be_present
739
+ end
740
+ ```
741
+
742
+ ## Security in Tests
743
+
744
+ Tests often handle sensitive data. Protect it.
745
+
746
+ ### Never Commit Tokens
747
+
748
+ ```ruby
749
+ # .gitignore
750
+ .env
751
+ .env.test
752
+ spec/fixtures/vcr_cassettes/* # If cassettes contain secrets
753
+
754
+ # Use environment variables
755
+ RSpec.describe 'API client' do
756
+ let(:client) { Monday::Client.new(token: ENV['MONDAY_TOKEN']) }
757
+ end
758
+ ```
759
+
760
+ ### Filter Sensitive Data in VCR
761
+
762
+ ```ruby
763
+ VCR.configure do |config|
764
+ # Filter headers
765
+ config.filter_sensitive_data('<TOKEN>') do |interaction|
766
+ interaction.request.headers['Authorization']&.first
767
+ end
768
+
769
+ # Filter response data
770
+ config.filter_sensitive_data('<EMAIL>') do |interaction|
771
+ JSON.parse(interaction.response.body).dig('data', 'me', 'email') rescue nil
772
+ end
773
+
774
+ # Filter by pattern
775
+ config.filter_sensitive_data('<API_KEY>') { ENV['MONDAY_TOKEN'] }
776
+ end
777
+ ```
778
+
779
+ ### Secure CI Secrets
780
+
781
+ ```yaml
782
+ # GitHub Actions
783
+ jobs:
784
+ test:
785
+ steps:
786
+ - run: bundle exec rspec
787
+ env:
788
+ MONDAY_TOKEN: ${{ secrets.MONDAY_TOKEN }} # Encrypted secret
789
+ # Never use ${{ secrets.MONDAY_TOKEN }} in commands that echo/log
790
+ ```
791
+
792
+ ### Review Cassettes Before Committing
793
+
794
+ ```bash
795
+ # Check cassettes for sensitive data before committing
796
+ git diff spec/fixtures/vcr_cassettes/
797
+
798
+ # Look for:
799
+ # - Tokens in Authorization headers
800
+ # - Email addresses
801
+ # - Account IDs
802
+ # - User names
803
+ ```
804
+
805
+ If found, update VCR filters and re-record cassettes.
806
+
807
+ ## Key Takeaways
808
+
809
+ 1. **Use the testing pyramid**: Mostly mocks (fast), some VCR (realistic), few real API calls (validation)
810
+ 2. **VCR for integration tests**: monday_ruby's primary testing approach—fast and realistic
811
+ 3. **Test error scenarios**: Don't just test happy paths
812
+ 4. **Maintain test isolation**: Each test should be independent and idempotent
813
+ 5. **Protect sensitive data**: Filter tokens from VCR cassettes, use environment variables
814
+ 6. **Update cassettes regularly**: Verify API compatibility with periodic re-recording
815
+ 7. **Test pagination thoroughly**: Edge cases like empty results and errors mid-pagination
816
+ 8. **Mock time-based logic**: Use Timecop for testing rate limiting and time windows
817
+ 9. **CI/CD-friendly**: VCR cassettes make tests fast and reliable in CI without API tokens
818
+ 10. **Review cassette changes**: API changes may indicate breaking changes
819
+
820
+ Good tests give you confidence to refactor, add features, and upgrade dependencies. Invest time in your test suite—it pays dividends.