monday_ruby 1.1.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.
- checksums.yaml +4 -4
- data/.env +1 -1
- data/.rubocop.yml +2 -1
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +104 -0
- data/README.md +146 -142
- data/docs/.vitepress/config.mjs +255 -0
- data/docs/.vitepress/theme/index.js +4 -0
- data/docs/.vitepress/theme/style.css +43 -0
- data/docs/README.md +80 -0
- data/docs/explanation/architecture.md +507 -0
- data/docs/explanation/best-practices/errors.md +478 -0
- data/docs/explanation/best-practices/performance.md +1084 -0
- data/docs/explanation/best-practices/rate-limiting.md +630 -0
- data/docs/explanation/best-practices/testing.md +820 -0
- data/docs/explanation/column-values.md +857 -0
- data/docs/explanation/design.md +795 -0
- data/docs/explanation/graphql.md +356 -0
- data/docs/explanation/migration/v1.md +808 -0
- data/docs/explanation/pagination.md +447 -0
- data/docs/guides/advanced/batch.md +1274 -0
- data/docs/guides/advanced/complex-queries.md +1114 -0
- data/docs/guides/advanced/errors.md +818 -0
- data/docs/guides/advanced/pagination.md +934 -0
- data/docs/guides/advanced/rate-limiting.md +981 -0
- data/docs/guides/authentication.md +286 -0
- data/docs/guides/boards/create.md +386 -0
- data/docs/guides/boards/delete.md +405 -0
- data/docs/guides/boards/duplicate.md +511 -0
- data/docs/guides/boards/query.md +530 -0
- data/docs/guides/boards/update.md +453 -0
- data/docs/guides/columns/create.md +452 -0
- data/docs/guides/columns/metadata.md +492 -0
- data/docs/guides/columns/query.md +455 -0
- data/docs/guides/columns/update-multiple.md +459 -0
- data/docs/guides/columns/update-values.md +509 -0
- data/docs/guides/files/add-to-column.md +40 -0
- data/docs/guides/files/add-to-update.md +37 -0
- data/docs/guides/files/clear-column.md +33 -0
- data/docs/guides/first-request.md +285 -0
- data/docs/guides/folders/manage.md +750 -0
- data/docs/guides/groups/items.md +626 -0
- data/docs/guides/groups/manage.md +501 -0
- data/docs/guides/installation.md +169 -0
- data/docs/guides/items/create.md +493 -0
- data/docs/guides/items/delete.md +514 -0
- data/docs/guides/items/query.md +605 -0
- data/docs/guides/items/subitems.md +483 -0
- data/docs/guides/items/update.md +699 -0
- data/docs/guides/updates/manage.md +619 -0
- data/docs/guides/use-cases/dashboard.md +1421 -0
- data/docs/guides/use-cases/import.md +1962 -0
- data/docs/guides/use-cases/task-management.md +1381 -0
- data/docs/guides/workspaces/manage.md +502 -0
- data/docs/index.md +69 -0
- data/docs/package-lock.json +2468 -0
- data/docs/package.json +13 -0
- data/docs/reference/client.md +540 -0
- data/docs/reference/configuration.md +586 -0
- data/docs/reference/errors.md +693 -0
- data/docs/reference/resources/account.md +208 -0
- data/docs/reference/resources/activity-log.md +369 -0
- data/docs/reference/resources/board-view.md +359 -0
- data/docs/reference/resources/board.md +393 -0
- data/docs/reference/resources/column.md +543 -0
- data/docs/reference/resources/file.md +236 -0
- data/docs/reference/resources/folder.md +386 -0
- data/docs/reference/resources/group.md +507 -0
- data/docs/reference/resources/item.md +348 -0
- data/docs/reference/resources/subitem.md +267 -0
- data/docs/reference/resources/update.md +259 -0
- data/docs/reference/resources/workspace.md +213 -0
- data/docs/reference/response.md +560 -0
- data/docs/tutorial/first-integration.md +713 -0
- data/lib/monday/client.rb +24 -0
- data/lib/monday/configuration.rb +5 -0
- data/lib/monday/request.rb +15 -0
- data/lib/monday/resources/base.rb +4 -0
- data/lib/monday/resources/file.rb +56 -0
- data/lib/monday/util.rb +1 -0
- data/lib/monday/version.rb +1 -1
- metadata +87 -4
|
@@ -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.
|