Skip to content

Commit 6504da8

Browse files
teetanghclaude
andauthored
feat: Implement robust Couchbase error handling and modernize test infrastructure (#8)
* feat: Enhance Couchbase initialization and add hotel API endpoints - Improved Couchbase initialization with error handling for misconfigurations. - Added new API endpoints for hotel autocomplete and filtering in Swagger documentation. - Updated Swagger server URL to a more generic format. * fix: Address valid Gemini Code Assist feedback Fixes 4 valid issues identified in PR #8 code review: 1. HIGH PRIORITY - Fixed constant re-assignment in couchbase.rb - Changed DB_USERNAME, DB_PASSWORD, DB_CONN_STR to local variables - Eliminates Ruby "already initialized constant" warnings - Constants at top of file remain for CI environment - Local variables used only in dev environment 2. Fixed rswag spec grammar (line 5) - Changed "Retrieve suggestion" → "Retrieve suggestions" - Matches plural array return type 3. Fixed misleading response description (line 22) - Changed "No suggestion" → "List of hotel name suggestions" - 200 status should describe success case, not edge case 4. Fixed incorrect response descriptions (lines 73 & 93) - Changed "only one Hotels found" → "List of hotels matching the filter criteria" - Fixed grammar and made description generic for filter endpoint Note: Rejected Gemini's metaprogramming suggestion for search index assignment as it adds unnecessary complexity without benefit. Regenerated swagger.yaml from updated rswag specs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * feat: Add workflow_dispatch trigger for manual CI runs Allows developers to manually trigger the CI workflow from the GitHub Actions UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * refactor: Fix Couchbase integration test failures and improve error handling This commit addresses the failing integration tests and implements a robust error handling strategy for Couchbase connections. ## Problem Fixed - 25 out of 27 integration tests were failing with "NoMethodError: undefined method 'get' for nil" - Couchbase collection constants (AIRLINE_COLLECTION, AIRPORT_COLLECTION, ROUTE_COLLECTION) were nil due to silent initialization failures ## Changes Made ### Core Fixes - Created CouchbaseConnection concern for centralized connection checking - Updated all models (Airline, Airport, Route, Hotel) to use ensure_couchbase! - Changed ApplicationController to inherit from ActionController::API - Improved Couchbase initializer with environment-specific error handling and retry logic for transient failures - Removed skip_before_action :verify_authenticity_token from all API controllers (not needed for ActionController::API) ### Code Organization - Renamed HotelSearch model to Hotel for consistency with other models - Reorganized test files following RSpec conventions: * spec/requests/swagger/ - Swagger/rswag documentation specs * spec/requests/api/v1/ - Integration test specs * Removed test/integration/ directory ### New Features - Added health check endpoint (GET /api/v1/health) for monitoring Couchbase connectivity - Added retry logic for CI to handle transient network failures - Added comprehensive troubleshooting documentation in README.md ### CI/CD Improvements - Updated CI workflow to validate Couchbase configuration before running tests - Added connection test step to verify database availability - Updated test paths to use spec/requests instead of test/integration ## Test Results - All 27 integration tests now pass (0 failures) - Swagger documentation regenerated successfully (64 examples) - Health endpoint verified working ## Files Modified - Models: airline.rb, airport.rb, route.rb, hotel.rb (renamed from hotel_search.rb) - Controllers: airlines, airports, routes, hotels, health (new), application - Config: couchbase.rb initializer, routes.rb, ci.yml - Tests: Reorganized from test/integration/ to spec/requests/api/v1/ - Docs: README.md with troubleshooting guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * refactor: Convert swagger specs to documentation-only Simplified swagger specs to focus on API documentation generation rather than duplicating integration testing. This change improves maintainability and test execution speed. Changes: - Converted all swagger specs (airlines, airports, routes, hotels) to documentation-only mode - Removed database setup/teardown logic from swagger specs - Added inline comments indicating actual testing is in spec/requests/api/v1/ - Updated CI workflow to: - Run only integration tests from spec/requests/api/v1/ - Verify swagger documentation generates without errors - Skip redundant swagger spec testing - Regenerated swagger.yaml with simplified specs Benefits: - Faster swagger documentation generation (64 examples, 0 failures) - Clear separation of concerns: swagger for docs, integration tests for testing - Reduced CI execution time - Eliminated complex test data management in swagger specs - Integration tests in spec/requests/api/v1/ provide actual API validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent f980446 commit 6504da8

28 files changed

+1777
-1323
lines changed

.github/workflows/ci.yml

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
branches: [main]
88
schedule:
99
- cron: "40 9 * * *"
10+
workflow_dispatch:
1011

1112
jobs:
1213
run_tests:
@@ -27,8 +28,64 @@ jobs:
2728
ruby-version: 3.4.1
2829
bundler-cache: true
2930

30-
- name: Run tests
31-
run: bundle exec rspec test/integration
31+
- name: Validate Couchbase Configuration
32+
run: |
33+
echo "Validating Couchbase environment variables..."
34+
35+
if [ -z "$DB_CONN_STR" ]; then
36+
echo "::error::DB_CONN_STR environment variable is not set"
37+
exit 1
38+
fi
39+
40+
if [ -z "$DB_USERNAME" ]; then
41+
echo "::error::DB_USERNAME environment variable is not set"
42+
exit 1
43+
fi
44+
45+
if [ -z "$DB_PASSWORD" ]; then
46+
echo "::error::DB_PASSWORD secret is not set"
47+
exit 1
48+
fi
49+
50+
echo "✓ All Couchbase environment variables are configured"
51+
env:
52+
DB_CONN_STR: ${{ vars.DB_CONN_STR }}
53+
DB_USERNAME: ${{ vars.DB_USERNAME }}
54+
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
55+
56+
- name: Test Couchbase Connection
57+
run: |
58+
bundle exec rails runner '
59+
begin
60+
if defined?(COUCHBASE_CLUSTER) && COUCHBASE_CLUSTER
61+
bucket = COUCHBASE_CLUSTER.bucket("travel-sample")
62+
puts "✓ Successfully connected to Couchbase cluster"
63+
puts "✓ Successfully accessed travel-sample bucket"
64+
else
65+
puts "✗ Couchbase cluster not initialized"
66+
exit 1
67+
end
68+
rescue => e
69+
puts "✗ Failed to connect to Couchbase: #{e.class.name} - #{e.message}"
70+
exit 1
71+
end
72+
'
73+
env:
74+
DB_CONN_STR: ${{ vars.DB_CONN_STR }}
75+
DB_USERNAME: ${{ vars.DB_USERNAME }}
76+
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
77+
CI: true
78+
79+
- name: Run integration tests
80+
run: bundle exec rspec spec/requests/api/v1
81+
82+
- name: Verify Swagger documentation generates
83+
run: bundle exec rake rswag:specs:swaggerize
84+
env:
85+
DB_CONN_STR: ${{ vars.DB_CONN_STR }}
86+
DB_USERNAME: ${{ vars.DB_USERNAME }}
87+
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
88+
CI: true
3289

3390
- name: Report Status
3491
if: always()

README.md

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,121 @@ DB_PASSWORD=<password_for_user>
120120
### Run the integration tests
121121

122122
```sh
123-
bundle exec rspec test/integration
123+
bundle exec rspec spec/requests
124124
```
125125

126+
## Troubleshooting
127+
128+
### Integration Tests Failing with "undefined method 'get' for nil"
129+
130+
This error means Couchbase is not properly initialized. Follow these steps:
131+
132+
#### 1. Verify Environment Variables
133+
134+
For local development, ensure `dev.env` file exists in project root:
135+
136+
```bash
137+
cat dev.env
138+
```
139+
140+
Should contain:
141+
```
142+
DB_CONN_STR="couchbases://cb.hlcup4o4jmjr55yf.cloud.couchbase.com"
143+
DB_USERNAME="your-username"
144+
DB_PASSWORD="your-password"
145+
```
146+
147+
#### 2. Verify Couchbase Connection
148+
149+
Test the connection:
150+
```bash
151+
bundle exec rails runner 'puts COUCHBASE_CLUSTER ? "✓ Connected" : "✗ Not connected"'
152+
```
153+
154+
#### 3. Verify travel-sample Bucket
155+
156+
The application requires the `travel-sample` bucket with:
157+
- **Scope:** `inventory`
158+
- **Collections:** `airline`, `airport`, `route`, `hotel`
159+
160+
For Couchbase Capella:
161+
1. Log into Capella console
162+
2. Navigate to your cluster
163+
3. Check Buckets > travel-sample exists
164+
4. Verify inventory scope and collections exist
165+
166+
#### 4. Check Permissions
167+
168+
The database user needs:
169+
- Read/Write access to `travel-sample` bucket
170+
- Query permissions for N1QL queries
171+
- Search permissions for FTS operations
172+
173+
#### 5. Verify Network Access (Capella Only)
174+
175+
For Couchbase Capella:
176+
1. Go to Settings > Allowed IP Addresses
177+
2. Add your IP address or `0.0.0.0/0` for testing
178+
3. Ensure cluster is not paused
179+
180+
### CI Tests Failing
181+
182+
Check GitHub repository configuration:
183+
184+
1. **Secrets** (Settings > Secrets and variables > Actions > Secrets):
185+
- `DB_PASSWORD` - Your Couchbase password
186+
187+
2. **Variables** (Settings > Secrets and variables > Actions > Variables):
188+
- `DB_CONN_STR` - Your Couchbase connection string
189+
- `DB_USERNAME` - Your Couchbase username
190+
191+
3. **Capella IP Allowlist**:
192+
- GitHub Actions runners use dynamic IPs
193+
- Temporarily allow `0.0.0.0/0` or use Capella's "Allow All IPs" option
194+
195+
### Common Errors
196+
197+
**Error:** `Couchbase::Error::AuthenticationFailure`
198+
- **Solution:** Check username/password in `dev.env` or GitHub Secrets
199+
200+
**Error:** `Couchbase::Error::BucketNotFound`
201+
- **Solution:** Ensure `travel-sample` bucket is created and loaded
202+
203+
**Error:** `Couchbase::Error::Timeout`
204+
- **Solution:** Check network connectivity, verify connection string uses correct protocol (`couchbase://` for local, `couchbases://` for Capella)
205+
206+
**Error:** `Couchbase::Error::ScopeNotFound` or `CollectionNotFound`
207+
- **Solution:** The initializer auto-creates scope/collections, but user needs create permissions
208+
209+
### Health Check Endpoint
210+
211+
Check application health:
212+
```bash
213+
curl http://localhost:3000/api/v1/health
214+
```
215+
216+
Response shows Couchbase status:
217+
```json
218+
{
219+
"status": "healthy",
220+
"timestamp": "2025-12-02T10:30:00Z",
221+
"services": {
222+
"couchbase": {
223+
"status": "up",
224+
"message": "Connected to travel-sample bucket"
225+
}
226+
}
227+
}
228+
```
229+
230+
### Getting Help
231+
232+
If issues persist:
233+
1. Check application logs for detailed error messages
234+
2. Verify Ruby version matches `.ruby-version` (3.4.1)
235+
3. Run `bundle install` to ensure all gems are current
236+
4. Check Couchbase SDK compatibility
237+
126238
# Appendix
127239

128240
## Data Model

app/controllers/api/v1/airlines_controller.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
module Api
44
module V1
55
class AirlinesController < ApplicationController
6-
skip_before_action :verify_authenticity_token, only: %i[create update destroy]
76
before_action :set_airline, only: %i[show update destroy]
87

98
# GET /api/v1/airlines/{id}

app/controllers/api/v1/airports_controller.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
module Api
44
module V1
55
class AirportsController < ApplicationController
6-
skip_before_action :verify_authenticity_token, only: %i[create update destroy]
76
before_action :set_airport, only: %i[show update destroy]
87

98
# GET /api/v1/airports/{id}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module Api
4+
module V1
5+
class HealthController < ApplicationController
6+
def show
7+
health_status = {
8+
status: 'healthy',
9+
timestamp: Time.current.iso8601,
10+
services: {
11+
couchbase: check_couchbase
12+
}
13+
}
14+
15+
all_up = health_status[:services].values.all? { |s| s[:status] == 'up' }
16+
status_code = all_up ? :ok : :service_unavailable
17+
18+
render json: health_status, status: status_code
19+
end
20+
21+
private
22+
23+
def check_couchbase
24+
if defined?(COUCHBASE_CLUSTER) && COUCHBASE_CLUSTER
25+
# Perform simple bucket check to verify connection
26+
COUCHBASE_CLUSTER.bucket('travel-sample')
27+
{ status: 'up', message: 'Connected to travel-sample bucket' }
28+
else
29+
{ status: 'down', message: 'Couchbase not initialized' }
30+
end
31+
rescue StandardError => e
32+
{ status: 'down', message: e.message }
33+
end
34+
end
35+
end
36+
end

app/controllers/api/v1/hotels_controller.rb

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,33 @@
33
module Api
44
module V1
55
class HotelsController < ApplicationController
6-
skip_before_action :verify_authenticity_token, only: %i[search filter]
76
before_action :validate_query_params, only: [:search]
87
# GET /api/v1/hotels/autocomplete
98
def search
10-
@hotels = HotelSearch.search_name(params[:name])
9+
@hotels = Hotel.search_name(params[:name])
1110
render json: @hotels, status: :ok
1211
rescue StandardError => e
1312
render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error
1413
end
1514

1615
# GET /api/v1/hotels/filter
1716
def filter
18-
@hotels = HotelSearch.filter(HotelSearch.new(
17+
@hotels = Hotel.filter(Hotel.new(
1918
{
20-
"name"=> hotel_search_params[:name],
21-
"title" => hotel_search_params[:title],
22-
"description" => hotel_search_params[:description],
23-
"country" => hotel_search_params[:country],
24-
"city" => hotel_search_params[:city],
25-
"state" => hotel_search_params[:state]
19+
"name"=> hotel_params[:name],
20+
"title" => hotel_params[:title],
21+
"description" => hotel_params[:description],
22+
"country" => hotel_params[:country],
23+
"city" => hotel_params[:city],
24+
"state" => hotel_params[:state]
2625
}
27-
), hotel_search_params[:offset], hotel_search_params[:limit])
26+
), hotel_params[:offset], hotel_params[:limit])
2827
render json: @hotels, status: :ok
2928
rescue StandardError => e
3029
render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error
3130
end
3231

33-
def hotel_search_params
32+
def hotel_params
3433
params.require(:hotel).permit(:name, :title, :description, :country, :city, :state, :offset, :limit)
3534
end
3635

app/controllers/api/v1/routes_controller.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
module Api
44
module V1
55
class RoutesController < ApplicationController
6-
skip_before_action :verify_authenticity_token, only: %i[create update destroy]
76
before_action :set_route, only: %i[show update destroy]
87

98
# GET /api/v1/routes/{id}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1-
class ApplicationController < ActionController::Base
1+
class ApplicationController < ActionController::API
2+
rescue_from CouchbaseConnection::CouchbaseUnavailableError, with: :handle_database_unavailable
3+
4+
private
5+
6+
def handle_database_unavailable(exception)
7+
render json: {
8+
error: 'Service Unavailable',
9+
message: 'Database connection is not available. Please check configuration or try again later.'
10+
}, status: :service_unavailable
11+
end
212
end

app/models/airline.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
class Airline
4+
include CouchbaseConnection
5+
46
attr_accessor :name, :iata, :icao, :callsign, :country
57

68
def initialize(attributes)
@@ -12,13 +14,15 @@ def initialize(attributes)
1214
end
1315

1416
def self.find(id)
17+
ensure_couchbase!
1518
result = AIRLINE_COLLECTION.get(id)
1619
new(result.content) if result.success?
1720
rescue Couchbase::Error::DocumentNotFound
1821
nil
1922
end
2023

2124
def self.all(country = nil, limit = 10, offset = 0)
25+
ensure_couchbase!
2226
bucket_name = 'travel-sample'
2327
scope_name = 'inventory'
2428
collection_name = 'airline'
@@ -33,6 +37,7 @@ def self.all(country = nil, limit = 10, offset = 0)
3337
end
3438

3539
def self.to_airport(destination_airport_code, limit = 10, offset = 0)
40+
ensure_couchbase!
3641
bucket_name = 'travel-sample'
3742
scope_name = 'inventory'
3843
route_collection_name = 'route'
@@ -58,6 +63,7 @@ def self.to_airport(destination_airport_code, limit = 10, offset = 0)
5863
end
5964

6065
def self.create(id, attributes)
66+
ensure_couchbase!
6167
required_fields = %w[name iata icao callsign country]
6268
missing_fields = required_fields - attributes.keys
6369
extra_fields = attributes.keys - required_fields
@@ -78,6 +84,7 @@ def self.create(id, attributes)
7884
end
7985

8086
def update(id, attributes)
87+
self.class.ensure_couchbase!
8188
required_fields = %w[name iata icao callsign country]
8289
missing_fields = required_fields - attributes.keys
8390
extra_fields = attributes.keys - required_fields
@@ -98,6 +105,7 @@ def update(id, attributes)
98105
end
99106

100107
def destroy(id)
108+
self.class.ensure_couchbase!
101109
AIRLINE_COLLECTION.remove(id)
102110
end
103111
end

0 commit comments

Comments
 (0)