patch_retention 0.1.5 → 0.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.development +5 -0
- data/.env.example +5 -0
- data/.env.test.example +5 -0
- data/.rubocop.yml +33 -21
- data/.ruby-version +1 -0
- data/.tool-versions +1 -1
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +248 -0
- data/DEPENDENCY_UPDATE_REPORT.md +89 -0
- data/Gemfile +0 -12
- data/Gemfile.lock +44 -12
- data/README.md +189 -3
- data/Rakefile +1 -1
- data/docs/decisions/001-api-implementation.md +66 -0
- data/docs/patterns/testing-patterns.md +152 -0
- data/docs/references/github-actions.md +100 -0
- data/docs/references/troubleshooting.md +122 -0
- data/docs/wip/session-2025-05-26.md +66 -0
- data/lib/patch_retention/configuration.rb +1 -1
- data/lib/patch_retention/contacts.rb +1 -1
- data/lib/patch_retention/events/create.rb +1 -1
- data/lib/patch_retention/events.rb +1 -1
- data/lib/patch_retention/memberships.rb +59 -0
- data/lib/patch_retention/products.rb +62 -0
- data/lib/patch_retention/util.rb +2 -2
- data/lib/patch_retention/version.rb +1 -1
- data/lib/patch_retention.rb +4 -4
- data/patch_retention.gemspec +56 -0
- data/script/setup_test_env +44 -0
- metadata +219 -8
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# Patch Retention API Wrapper
|
2
2
|
|
3
|
+
[](https://github.com/[USERNAME]/patch_retention/actions/workflows/ci.yml)
|
4
|
+
[](https://badge.fury.io/rb/patch_retention)
|
5
|
+
|
3
6
|
## Installation
|
4
7
|
|
5
8
|
Install the gem and add to the application's Gemfile by executing:
|
@@ -18,7 +21,30 @@ $ gem install patch_retention
|
|
18
21
|
|
19
22
|
### Configuration
|
20
23
|
|
21
|
-
|
24
|
+
#### Using Environment Files (Recommended for Development)
|
25
|
+
|
26
|
+
This gem uses dotenv for managing environment variables in development and test environments. Copy the example file and add your credentials:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
# For development
|
30
|
+
cp .env.example .env.development.local
|
31
|
+
# Edit .env.development.local with your credentials
|
32
|
+
|
33
|
+
# For running tests
|
34
|
+
cp .env.test.example .env.test
|
35
|
+
# Edit .env.test with your test credentials
|
36
|
+
```
|
37
|
+
|
38
|
+
The following files are used:
|
39
|
+
- `.env.test` - Test environment (gitignored - copy from `.env.test.example`)
|
40
|
+
- `.env.test.example` - Test environment template (committed)
|
41
|
+
- `.env.development` - Development defaults (committed)
|
42
|
+
- `.env.development.local` - Your local development credentials (gitignored)
|
43
|
+
- `.env.example` - Example configuration (committed)
|
44
|
+
|
45
|
+
#### Using Environment Variables
|
46
|
+
|
47
|
+
For production or CI environments, you can set the following environment variables:
|
22
48
|
|
23
49
|
```bash
|
24
50
|
PATCH_RETENTION_API_URL='your_custom_api_url' # Optional, by default is set to: https://api.patchretention.com/v2
|
@@ -38,9 +64,52 @@ PatchRetention.configure do |config|
|
|
38
64
|
end
|
39
65
|
```
|
40
66
|
|
67
|
+
### Using a Custom Configuration per Call
|
68
|
+
|
69
|
+
While you can configure the gem globally, there might be scenarios where you need to use a different set of credentials or API endpoint for a specific API call. All API call methods (e.g., `PatchRetention::Products.create`, `PatchRetention::Memberships.create`, `PatchRetention::Events.create`, `PatchRetention::Contacts.find_or_create_by`, etc.) accept an optional `config:` parameter.
|
70
|
+
|
71
|
+
This parameter expects an instance of `PatchRetention::Configuration`.
|
72
|
+
|
73
|
+
**Example: Creating a call with a custom `PatchRetention::Configuration` Object:**
|
74
|
+
|
75
|
+
If you want to use different configuration values for a specific call, you can create a new `PatchRetention::Configuration` object, populate it, and pass it to the desired method.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
# Create a custom configuration instance
|
79
|
+
custom_config = PatchRetention::Configuration.new
|
80
|
+
custom_config.client_id = "your_other_client_id"
|
81
|
+
custom_config.client_secret = "your_other_client_secret"
|
82
|
+
custom_config.api_url = "https://specific-instance.patchretention.com/v2"
|
83
|
+
# custom_config.proxy_url = "http://your.other.proxy.com:8080" # Optional
|
84
|
+
|
85
|
+
# Use this custom configuration for an API call
|
86
|
+
begin
|
87
|
+
product_details = {
|
88
|
+
name: "Special Product",
|
89
|
+
price: 1500,
|
90
|
+
status: "PUBLISHED"
|
91
|
+
}
|
92
|
+
new_product = PatchRetention::Products.create(**product_details, config: custom_config)
|
93
|
+
puts "Created product with custom config: #{new_product}"
|
94
|
+
|
95
|
+
membership_payload = {
|
96
|
+
contact_id: "ct_somecontact",
|
97
|
+
product_id: new_product["id"], # Assuming new_product is a hash with an "id" key
|
98
|
+
start_at: Time.now.xmlschema
|
99
|
+
}
|
100
|
+
new_membership = PatchRetention::Memberships.create(**membership_payload, config: custom_config)
|
101
|
+
puts "Created membership with custom config: #{new_membership}"
|
102
|
+
|
103
|
+
rescue PatchRetention::Error => e
|
104
|
+
puts "Error with custom config: #{e.message}"
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
This approach allows you to maintain multiple configurations if needed, for example, when interacting with different Patch Retention accounts or environments within the same application. If you need to use a highly customized Faraday connection (e.g., with special middleware), you would need to ensure the `PatchRetention.connection` method can accommodate this through the `Configuration` object, potentially by enhancing the `Configuration` class or the `PatchRetention.connection` method itself to interpret more advanced settings from the `Configuration` object.
|
109
|
+
|
41
110
|
### Usage
|
42
111
|
|
43
|
-
First, you need to configure the gem with your API credentials.
|
112
|
+
First, you need to configure the gem with your API credentials globally (if not providing custom configurations per call).
|
44
113
|
|
45
114
|
Then, you can use the gem's methods to interact with the API.
|
46
115
|
|
@@ -148,9 +217,126 @@ event = PatchRetention::Events.create(
|
|
148
217
|
)
|
149
218
|
```
|
150
219
|
|
220
|
+
**Products**
|
221
|
+
|
222
|
+
To create a product:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
product = PatchRetention::Products.create(
|
226
|
+
name: "Test Product",
|
227
|
+
price: 999, # Price in Cents
|
228
|
+
status: "PUBLISHED", # or "UNPUBLISHED"
|
229
|
+
description: "This is a product for testing purposes.", # Optional
|
230
|
+
membership: true, # Optional, boolean
|
231
|
+
tags: ["test", "new_product"], # Optional, array of strings
|
232
|
+
external_id: "SKU12345", # Optional
|
233
|
+
external_data: { "custom_key": "custom_value" } # Optional, hash
|
234
|
+
)
|
235
|
+
# => {"id"=>"prod_xxxxxxxxxxxxxx", "name"=>"Test Product", ...}
|
236
|
+
```
|
237
|
+
|
238
|
+
To update a product:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
product = PatchRetention::Products.update(
|
242
|
+
product_id: "prod_xxxxxxxxxxxxxx",
|
243
|
+
name: "Updated Product Name",
|
244
|
+
price: 1299,
|
245
|
+
status: "UNPUBLISHED"
|
246
|
+
)
|
247
|
+
```
|
248
|
+
|
249
|
+
To find a product:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
product = PatchRetention::Products.find(product_id: "prod_xxxxxxxxxxxxxx")
|
253
|
+
```
|
254
|
+
|
255
|
+
**Memberships**
|
256
|
+
|
257
|
+
To create a membership:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
require 'time'
|
261
|
+
|
262
|
+
membership = PatchRetention::Memberships.create(
|
263
|
+
contact_id: "68273ecd9xxxxxxxxxxxx", # Required, ID of the contact
|
264
|
+
product_id: "65de5xxxxxxxxxxxxx", # Required, ID of the product (e.g., from product creation)
|
265
|
+
start_at: Time.now.xmlschema, # Optional, ISO8601 timestamp
|
266
|
+
end_at: (Time.now + 30*24*60*60).xmlschema, # Optional, ISO8601 timestamp (e.g., 30 days from now)
|
267
|
+
external_id: "MEM123", # Optional
|
268
|
+
external_data: { "notes": "Trial membership" }, # Optional, hash
|
269
|
+
tags: ["trial", "api_created"] # Optional, array of strings
|
270
|
+
)
|
271
|
+
# => {"id"=>"mem_xxxxxxxxxxxxxx", "contact_id"=>"ct_xxxxxxxxxxxxxx", ...}
|
272
|
+
```
|
273
|
+
|
274
|
+
To update a membership:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
membership = PatchRetention::Memberships.update(
|
278
|
+
membership_id: "mem_xxxxxxxxxxxxxx",
|
279
|
+
end_at: (Time.now + 60*24*60*60).xmlschema, # Extend to 60 days
|
280
|
+
tags: ["extended", "premium"]
|
281
|
+
)
|
282
|
+
```
|
283
|
+
|
284
|
+
To find a membership:
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
membership = PatchRetention::Memberships.find(membership_id: "mem_xxxxxxxxxxxxxx")
|
288
|
+
```
|
289
|
+
|
151
290
|
## Development
|
152
291
|
|
153
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
292
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
293
|
+
|
294
|
+
### Setting up Test Environment
|
295
|
+
|
296
|
+
To run tests, you'll need to set up test credentials. The test credentials should NOT be committed to the repository.
|
297
|
+
|
298
|
+
1. **Using environment variables (recommended):**
|
299
|
+
```bash
|
300
|
+
export PATCH_RETENTION_TEST_CLIENT_ID=your_test_client_id
|
301
|
+
export PATCH_RETENTION_TEST_CLIENT_SECRET=your_test_client_secret
|
302
|
+
```
|
303
|
+
|
304
|
+
2. **Using the setup script:**
|
305
|
+
```bash
|
306
|
+
# This will create .env.test from your environment variables
|
307
|
+
./script/setup_test_env
|
308
|
+
```
|
309
|
+
|
310
|
+
3. **Manual setup:**
|
311
|
+
Create a `.env.test` file with:
|
312
|
+
```
|
313
|
+
PATCH_RETENTION_CLIENT_ID=your_test_client_id
|
314
|
+
PATCH_RETENTION_CLIENT_SECRET=your_test_client_secret
|
315
|
+
PATCH_RETENTION_API_URL=https://api.patchretention.com/v2
|
316
|
+
```
|
317
|
+
|
318
|
+
**Note:** The `.env.test` file is gitignored and should never be committed.
|
319
|
+
|
320
|
+
### Running Tests
|
321
|
+
|
322
|
+
Once your test environment is set up:
|
323
|
+
```bash
|
324
|
+
bundle exec rake spec
|
325
|
+
```
|
326
|
+
|
327
|
+
#### VCR Recording Modes
|
328
|
+
|
329
|
+
By default, VCR runs in `:once` mode, which only records HTTP interactions once. To re-record cassettes:
|
330
|
+
|
331
|
+
```bash
|
332
|
+
# Re-record all cassettes
|
333
|
+
VCR_RECORD_MODE=new_episodes bundle exec rake spec
|
334
|
+
|
335
|
+
# Never make real HTTP calls (useful for CI)
|
336
|
+
VCR_RECORD_MODE=none bundle exec rake spec
|
337
|
+
```
|
338
|
+
|
339
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
154
340
|
|
155
341
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
156
342
|
|
data/Rakefile
CHANGED
@@ -0,0 +1,66 @@
|
|
1
|
+
# API Implementation Decisions
|
2
|
+
|
3
|
+
## Date: 2025-05-26
|
4
|
+
|
5
|
+
### Context
|
6
|
+
During the implementation of Products and Memberships endpoints for the Patch Retention API v2, several important decisions were made based on API behavior discoveries.
|
7
|
+
|
8
|
+
### Key Decisions
|
9
|
+
|
10
|
+
#### 1. ID Format Handling
|
11
|
+
- **Decision**: Use flexible ID validation instead of expecting specific prefixes
|
12
|
+
- **Rationale**: The API returns IDs in MongoDB format (e.g., `6833ef45d8ebcbe4774efb98`) rather than prefixed formats like `prod_xxx` or `mem_xxx`
|
13
|
+
- **Implementation**: Changed tests from `expect(result["id"]).to match(/^prod_/)` to `expect(result["id"]).to be_a(String)`
|
14
|
+
|
15
|
+
#### 2. Contact ID Field Naming
|
16
|
+
- **Decision**: Handle both `_id` and `id` fields for contacts
|
17
|
+
- **Rationale**: Contacts API returns `_id` as the primary identifier, while other endpoints use `id`
|
18
|
+
- **Implementation**: Use `contact["_id"] || contact["id"]` pattern throughout tests
|
19
|
+
|
20
|
+
#### 3. External Data Handling
|
21
|
+
- **Decision**: Don't expect `external_data` to be returned in responses
|
22
|
+
- **Rationale**: The API accepts `external_data` but doesn't always return it in the response
|
23
|
+
- **Implementation**: Removed assertions for `external_data` in test expectations
|
24
|
+
|
25
|
+
#### 4. Tag Capitalization
|
26
|
+
- **Decision**: Expect tags to be capitalized by the API
|
27
|
+
- **Rationale**: The API automatically capitalizes tags (e.g., "test" becomes "Test")
|
28
|
+
- **Implementation**: Updated test data to use capitalized tags
|
29
|
+
|
30
|
+
#### 5. Membership Status Updates
|
31
|
+
- **Decision**: Remove `cancel` and `expire` convenience methods
|
32
|
+
- **Rationale**: Based on Excel documentation and API testing, only create and update operations are supported
|
33
|
+
- **Implementation**: Removed these methods and updated documentation accordingly
|
34
|
+
|
35
|
+
### API Response Patterns Discovered
|
36
|
+
|
37
|
+
1. **Products Response Structure**:
|
38
|
+
```json
|
39
|
+
{
|
40
|
+
"account_id": 654173,
|
41
|
+
"name": "Test Widget",
|
42
|
+
"price": 799,
|
43
|
+
"status": "PUBLISHED",
|
44
|
+
"_id": "6833ef45...",
|
45
|
+
"id": "6833ef45...",
|
46
|
+
"created_at": "2025-05-26T04:34:13.503Z",
|
47
|
+
"updated_at": "2025-05-26T04:34:13.503Z"
|
48
|
+
}
|
49
|
+
```
|
50
|
+
|
51
|
+
2. **Memberships Response Structure**:
|
52
|
+
```json
|
53
|
+
{
|
54
|
+
"account_id": 654173,
|
55
|
+
"contact_id": "6833ef45...",
|
56
|
+
"product_id": "6833ef45...",
|
57
|
+
"status": "PENDING",
|
58
|
+
"_id": "6833ef45...",
|
59
|
+
"id": "6833ef45..."
|
60
|
+
}
|
61
|
+
```
|
62
|
+
|
63
|
+
### Future Considerations
|
64
|
+
- Monitor if API adds support for prefixed IDs
|
65
|
+
- Watch for changes in external_data handling
|
66
|
+
- Consider adding response object wrappers to normalize field names
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# Testing Patterns for Patch Retention Gem
|
2
|
+
|
3
|
+
## VCR Configuration
|
4
|
+
|
5
|
+
### Setup
|
6
|
+
```ruby
|
7
|
+
VCR.configure do |config|
|
8
|
+
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
|
9
|
+
config.hook_into :webmock
|
10
|
+
config.configure_rspec_metadata!
|
11
|
+
config.default_cassette_options = {
|
12
|
+
record: :new_episodes,
|
13
|
+
match_requests_on: [:method, :uri, :body]
|
14
|
+
}
|
15
|
+
|
16
|
+
# Filter sensitive data
|
17
|
+
config.filter_sensitive_data('<CLIENT_ID>') { '654173' }
|
18
|
+
config.filter_sensitive_data('<CLIENT_SECRET>') { 'secret_...' }
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
### Usage in Specs
|
23
|
+
Tag specs with `:vcr` to automatically record/replay HTTP interactions:
|
24
|
+
```ruby
|
25
|
+
describe ".create", :vcr do
|
26
|
+
# test implementation
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
## Debugging Failed Specs
|
31
|
+
|
32
|
+
### Using Debug Output
|
33
|
+
When specs fail, add debug output to understand API responses:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
it "creates a product successfully" do
|
37
|
+
result = create_product
|
38
|
+
puts "DEBUG: Product creation result: #{result.inspect}"
|
39
|
+
|
40
|
+
# assertions...
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
### Common Debugging Patterns
|
45
|
+
|
46
|
+
1. **Inspect Full Response**:
|
47
|
+
```ruby
|
48
|
+
puts "DEBUG: API Response: #{result.inspect}"
|
49
|
+
```
|
50
|
+
|
51
|
+
2. **Check Specific Fields**:
|
52
|
+
```ruby
|
53
|
+
puts "DEBUG: Contact ID used: #{contact['_id'] || contact['id']}"
|
54
|
+
puts "DEBUG: Product ID used: #{product['id']}"
|
55
|
+
```
|
56
|
+
|
57
|
+
3. **Conditional Debugging**:
|
58
|
+
```ruby
|
59
|
+
if result["error"]
|
60
|
+
puts "DEBUG: Error response: #{result.inspect}"
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
## Test Data Patterns
|
65
|
+
|
66
|
+
### Creating Test Objects
|
67
|
+
Always create fresh test data for each test to avoid dependencies:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
let(:contact) do
|
71
|
+
PatchRetention::Contacts.find_or_create_by(
|
72
|
+
contact_params: {
|
73
|
+
first_name: "Test",
|
74
|
+
last_name: "User",
|
75
|
+
email: "test.user@example.com",
|
76
|
+
phone: "1234567890"
|
77
|
+
},
|
78
|
+
query_params: {
|
79
|
+
email: "test.user@example.com"
|
80
|
+
}
|
81
|
+
)
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
### Handling API Quirks
|
86
|
+
|
87
|
+
1. **Tag Capitalization**:
|
88
|
+
```ruby
|
89
|
+
let(:tags) { ["Test", "Widget", "Api_created"] } # API capitalizes tags
|
90
|
+
```
|
91
|
+
|
92
|
+
2. **Optional Field Validation**:
|
93
|
+
```ruby
|
94
|
+
# Only check if field exists in response
|
95
|
+
if result["tags"]
|
96
|
+
expect(result["tags"]).to eq(expected_tags)
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
3. **Flexible ID Validation**:
|
101
|
+
```ruby
|
102
|
+
expect(result["id"]).to be_a(String)
|
103
|
+
expect(result["id"].length).to be > 0
|
104
|
+
```
|
105
|
+
|
106
|
+
## Custom Configuration Testing
|
107
|
+
|
108
|
+
Always test that methods accept custom configuration:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
context "with custom configuration" do
|
112
|
+
let(:custom_config) do
|
113
|
+
PatchRetention::Configuration.new.tap do |config|
|
114
|
+
config.client_id = '654173'
|
115
|
+
config.client_secret = 'secret_...'
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it "uses the custom configuration" do
|
120
|
+
result = described_class.create(
|
121
|
+
# params...
|
122
|
+
config: custom_config
|
123
|
+
)
|
124
|
+
|
125
|
+
expect(result).to be_a(Hash)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
## Error Testing Patterns
|
131
|
+
|
132
|
+
### Required Parameter Validation
|
133
|
+
```ruby
|
134
|
+
context "missing required fields" do
|
135
|
+
it "raises an error when name is missing" do
|
136
|
+
expect {
|
137
|
+
described_class.create(price: 999, status: "PUBLISHED")
|
138
|
+
}.to raise_error(ArgumentError, /missing keyword: :?name/)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
### API Error Handling
|
144
|
+
```ruby
|
145
|
+
context "with invalid data" do
|
146
|
+
it "raises PatchRetention::Error" do
|
147
|
+
expect {
|
148
|
+
described_class.update(membership_id: "invalid_id")
|
149
|
+
}.to raise_error(PatchRetention::Error, /Failed to update membership/)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# GitHub Actions CI/CD Setup
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
This gem uses GitHub Actions for continuous integration and deployment. The workflows ensure code quality, test coverage, and security.
|
5
|
+
|
6
|
+
## Workflows
|
7
|
+
|
8
|
+
### 1. CI Workflow (`.github/workflows/ci.yml`)
|
9
|
+
**Triggers**: Push to main/master, Pull requests
|
10
|
+
|
11
|
+
**Jobs**:
|
12
|
+
- **Test Matrix**: Runs tests on Ruby 3.1, 3.2, and 3.3
|
13
|
+
- **Minimum Version Test**: Ensures compatibility with Ruby 3.1
|
14
|
+
- **Security Audit**: Checks for known vulnerabilities using bundler-audit
|
15
|
+
|
16
|
+
**Steps**:
|
17
|
+
1. Run RSpec tests
|
18
|
+
2. Run RuboCop for code style
|
19
|
+
3. Build the gem
|
20
|
+
4. Security audit
|
21
|
+
|
22
|
+
### 2. PR Test Workflow (`.github/workflows/pr-test.yml`)
|
23
|
+
**Triggers**: Pull request events
|
24
|
+
|
25
|
+
**Features**:
|
26
|
+
- Runs tests with VCR in playback-only mode
|
27
|
+
- Checks for sensitive data in VCR cassettes
|
28
|
+
- Uses dummy credentials for testing
|
29
|
+
|
30
|
+
**Security Check**:
|
31
|
+
```bash
|
32
|
+
grep -r "654173\|secret_A654173" spec/fixtures/vcr_cassettes/
|
33
|
+
```
|
34
|
+
|
35
|
+
### 3. Release Workflow (`.github/workflows/release.yml`)
|
36
|
+
**Triggers**: Push of version tags (v*)
|
37
|
+
|
38
|
+
**Process**:
|
39
|
+
1. Run tests and linting
|
40
|
+
2. Build gem
|
41
|
+
3. Create GitHub release
|
42
|
+
4. Upload gem artifact
|
43
|
+
5. (Optional) Publish to RubyGems
|
44
|
+
|
45
|
+
## Local Testing
|
46
|
+
|
47
|
+
Run the test script to validate CI locally:
|
48
|
+
```bash
|
49
|
+
./.github/test-workflow.sh
|
50
|
+
```
|
51
|
+
|
52
|
+
This script runs:
|
53
|
+
- All tests
|
54
|
+
- RuboCop
|
55
|
+
- Gem build
|
56
|
+
- Sensitive data check
|
57
|
+
- Security audit
|
58
|
+
|
59
|
+
## Maintaining CI
|
60
|
+
|
61
|
+
### Adding Ruby Versions
|
62
|
+
Edit `.github/workflows/ci.yml`:
|
63
|
+
```yaml
|
64
|
+
strategy:
|
65
|
+
matrix:
|
66
|
+
ruby-version: ['3.1', '3.2', '3.3']
|
67
|
+
```
|
68
|
+
|
69
|
+
### Updating Dependencies
|
70
|
+
The workflows use `bundler-cache: true` which automatically:
|
71
|
+
- Runs `bundle install`
|
72
|
+
- Caches dependencies
|
73
|
+
- Restores cache on subsequent runs
|
74
|
+
|
75
|
+
### Security Considerations
|
76
|
+
1. **Never commit real API credentials**
|
77
|
+
2. **VCR cassettes are checked for sensitive data**
|
78
|
+
3. **Use environment variables for secrets**
|
79
|
+
4. **Regular security audits via bundler-audit**
|
80
|
+
|
81
|
+
### Troubleshooting
|
82
|
+
|
83
|
+
**Tests fail in CI but pass locally**:
|
84
|
+
- Check Ruby version differences
|
85
|
+
- Ensure VCR cassettes are committed
|
86
|
+
- Verify environment variables
|
87
|
+
|
88
|
+
**RuboCop failures**:
|
89
|
+
- Run `bundle exec rubocop -A` locally
|
90
|
+
- Commit the fixes
|
91
|
+
|
92
|
+
**Security audit failures**:
|
93
|
+
- Update affected gems: `bundle update [gem_name]`
|
94
|
+
- Check advisory details in output
|
95
|
+
|
96
|
+
## Badge
|
97
|
+
Add to README:
|
98
|
+
```markdown
|
99
|
+
[](https://github.com/[USERNAME]/patch_retention/actions/workflows/ci.yml)
|
100
|
+
```
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Troubleshooting Guide
|
2
|
+
|
3
|
+
## Common Issues and Solutions
|
4
|
+
|
5
|
+
### 1. Contact ID Not Found
|
6
|
+
**Problem**: Membership creation fails with "Invalid contact_id specified"
|
7
|
+
|
8
|
+
**Cause**: Contacts API returns `_id` field, not `id`
|
9
|
+
|
10
|
+
**Solution**:
|
11
|
+
```ruby
|
12
|
+
contact_id = contact["_id"] || contact["id"]
|
13
|
+
```
|
14
|
+
|
15
|
+
### 2. Tags Don't Match Expected Values
|
16
|
+
**Problem**: Test assertions fail because tags don't match
|
17
|
+
|
18
|
+
**Cause**: API automatically capitalizes all tags
|
19
|
+
|
20
|
+
**Solution**:
|
21
|
+
```ruby
|
22
|
+
# Use capitalized tags in tests
|
23
|
+
let(:tags) { ["Test", "Widget", "Premium"] }
|
24
|
+
```
|
25
|
+
|
26
|
+
### 3. External Data Not Returned
|
27
|
+
**Problem**: `external_data` is nil in API response
|
28
|
+
|
29
|
+
**Cause**: API accepts but doesn't always return external_data
|
30
|
+
|
31
|
+
**Solution**:
|
32
|
+
```ruby
|
33
|
+
# Don't assert on external_data in responses
|
34
|
+
# Or make it conditional:
|
35
|
+
expect(result["external_data"]).to eq(data) if result["external_data"]
|
36
|
+
```
|
37
|
+
|
38
|
+
### 4. JSON Parse Error on Update
|
39
|
+
**Problem**: `JSON::ParserError: unexpected token at 'Not Found'`
|
40
|
+
|
41
|
+
**Cause**: Invalid ID format or non-existent resource
|
42
|
+
|
43
|
+
**Solution**:
|
44
|
+
```ruby
|
45
|
+
# Ensure you're using the correct ID from creation
|
46
|
+
membership_id = membership["id"] # not "_id" for memberships
|
47
|
+
```
|
48
|
+
|
49
|
+
### 5. VCR Cassette Mismatch
|
50
|
+
**Problem**: Tests fail with VCR errors about request not matching
|
51
|
+
|
52
|
+
**Cause**: Request body or headers changed
|
53
|
+
|
54
|
+
**Solution**:
|
55
|
+
```bash
|
56
|
+
# Delete the cassette and re-record
|
57
|
+
rm spec/fixtures/vcr_cassettes/[cassette_name].yml
|
58
|
+
bundle exec rspec [spec_file]
|
59
|
+
```
|
60
|
+
|
61
|
+
### 6. Debugging API Responses
|
62
|
+
|
63
|
+
**Add debug output to specs**:
|
64
|
+
```ruby
|
65
|
+
it "creates a resource" do
|
66
|
+
result = create_resource
|
67
|
+
puts "DEBUG: API Response: #{result.inspect}"
|
68
|
+
|
69
|
+
# This helps identify:
|
70
|
+
# - Actual field names returned
|
71
|
+
# - Data transformations by API
|
72
|
+
# - Error messages
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
### 7. Phone Number Formatting
|
77
|
+
**Problem**: Phone numbers have unexpected format
|
78
|
+
|
79
|
+
**Cause**: API may prepend country code (e.g., "1234567890" becomes "11234567890")
|
80
|
+
|
81
|
+
**Solution**: Be flexible with phone number assertions or normalize before comparison
|
82
|
+
|
83
|
+
### 8. Timestamp Precision
|
84
|
+
**Problem**: Time comparisons fail by small amounts
|
85
|
+
|
86
|
+
**Solution**:
|
87
|
+
```ruby
|
88
|
+
expect(Time.parse(result["start_at"])).to be_within(60).of(Time.parse(expected_time))
|
89
|
+
```
|
90
|
+
|
91
|
+
### 9. Status Field Values
|
92
|
+
**Note**: Memberships have a `status` field that defaults to "PENDING"
|
93
|
+
|
94
|
+
### 10. Missing Methods After Updates
|
95
|
+
**Problem**: NoMethodError for cancel/expire methods
|
96
|
+
|
97
|
+
**Cause**: These methods were removed as they're not supported by the API
|
98
|
+
|
99
|
+
**Solution**: Use the `update` method with status parameter if needed
|
100
|
+
|
101
|
+
## Debug Checklist
|
102
|
+
|
103
|
+
When a test fails:
|
104
|
+
1. ✓ Add debug output to see actual API response
|
105
|
+
2. ✓ Check if field names match (id vs _id)
|
106
|
+
3. ✓ Verify data transformations (capitalization, formatting)
|
107
|
+
4. ✓ Ensure test data is valid for the API
|
108
|
+
5. ✓ Check VCR cassettes are up to date
|
109
|
+
6. ✓ Verify API credentials are correct
|
110
|
+
|
111
|
+
## Useful Debug Commands
|
112
|
+
|
113
|
+
```bash
|
114
|
+
# Run specific test with output
|
115
|
+
bundle exec rspec spec/path/to/spec.rb:line_number --format documentation
|
116
|
+
|
117
|
+
# Clear VCR cassettes
|
118
|
+
rm -rf spec/fixtures/vcr_cassettes/
|
119
|
+
|
120
|
+
# Run tests without VCR (live API calls)
|
121
|
+
VCR=off bundle exec rspec
|
122
|
+
```
|