diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9484cc..097cae4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: branches: [main] schedule: - cron: "40 9 * * *" + workflow_dispatch: jobs: run_tests: @@ -27,8 +28,64 @@ jobs: ruby-version: 3.4.1 bundler-cache: true - - name: Run tests - run: bundle exec rspec test/integration + - name: Validate Couchbase Configuration + run: | + echo "Validating Couchbase environment variables..." + + if [ -z "$DB_CONN_STR" ]; then + echo "::error::DB_CONN_STR environment variable is not set" + exit 1 + fi + + if [ -z "$DB_USERNAME" ]; then + echo "::error::DB_USERNAME environment variable is not set" + exit 1 + fi + + if [ -z "$DB_PASSWORD" ]; then + echo "::error::DB_PASSWORD secret is not set" + exit 1 + fi + + echo "✓ All Couchbase environment variables are configured" + env: + DB_CONN_STR: ${{ vars.DB_CONN_STR }} + DB_USERNAME: ${{ vars.DB_USERNAME }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + + - name: Test Couchbase Connection + run: | + bundle exec rails runner ' + begin + if defined?(COUCHBASE_CLUSTER) && COUCHBASE_CLUSTER + bucket = COUCHBASE_CLUSTER.bucket("travel-sample") + puts "✓ Successfully connected to Couchbase cluster" + puts "✓ Successfully accessed travel-sample bucket" + else + puts "✗ Couchbase cluster not initialized" + exit 1 + end + rescue => e + puts "✗ Failed to connect to Couchbase: #{e.class.name} - #{e.message}" + exit 1 + end + ' + env: + DB_CONN_STR: ${{ vars.DB_CONN_STR }} + DB_USERNAME: ${{ vars.DB_USERNAME }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + CI: true + + - name: Run integration tests + run: bundle exec rspec spec/requests/api/v1 + + - name: Verify Swagger documentation generates + run: bundle exec rake rswag:specs:swaggerize + env: + DB_CONN_STR: ${{ vars.DB_CONN_STR }} + DB_USERNAME: ${{ vars.DB_USERNAME }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + CI: true - name: Report Status if: always() diff --git a/README.md b/README.md index 71c7c7a..e434086 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,121 @@ DB_PASSWORD= ### Run the integration tests ```sh -bundle exec rspec test/integration +bundle exec rspec spec/requests ``` +## Troubleshooting + +### Integration Tests Failing with "undefined method 'get' for nil" + +This error means Couchbase is not properly initialized. Follow these steps: + +#### 1. Verify Environment Variables + +For local development, ensure `dev.env` file exists in project root: + +```bash +cat dev.env +``` + +Should contain: +``` +DB_CONN_STR="couchbases://cb.hlcup4o4jmjr55yf.cloud.couchbase.com" +DB_USERNAME="your-username" +DB_PASSWORD="your-password" +``` + +#### 2. Verify Couchbase Connection + +Test the connection: +```bash +bundle exec rails runner 'puts COUCHBASE_CLUSTER ? "✓ Connected" : "✗ Not connected"' +``` + +#### 3. Verify travel-sample Bucket + +The application requires the `travel-sample` bucket with: +- **Scope:** `inventory` +- **Collections:** `airline`, `airport`, `route`, `hotel` + +For Couchbase Capella: +1. Log into Capella console +2. Navigate to your cluster +3. Check Buckets > travel-sample exists +4. Verify inventory scope and collections exist + +#### 4. Check Permissions + +The database user needs: +- Read/Write access to `travel-sample` bucket +- Query permissions for N1QL queries +- Search permissions for FTS operations + +#### 5. Verify Network Access (Capella Only) + +For Couchbase Capella: +1. Go to Settings > Allowed IP Addresses +2. Add your IP address or `0.0.0.0/0` for testing +3. Ensure cluster is not paused + +### CI Tests Failing + +Check GitHub repository configuration: + +1. **Secrets** (Settings > Secrets and variables > Actions > Secrets): + - `DB_PASSWORD` - Your Couchbase password + +2. **Variables** (Settings > Secrets and variables > Actions > Variables): + - `DB_CONN_STR` - Your Couchbase connection string + - `DB_USERNAME` - Your Couchbase username + +3. **Capella IP Allowlist**: + - GitHub Actions runners use dynamic IPs + - Temporarily allow `0.0.0.0/0` or use Capella's "Allow All IPs" option + +### Common Errors + +**Error:** `Couchbase::Error::AuthenticationFailure` +- **Solution:** Check username/password in `dev.env` or GitHub Secrets + +**Error:** `Couchbase::Error::BucketNotFound` +- **Solution:** Ensure `travel-sample` bucket is created and loaded + +**Error:** `Couchbase::Error::Timeout` +- **Solution:** Check network connectivity, verify connection string uses correct protocol (`couchbase://` for local, `couchbases://` for Capella) + +**Error:** `Couchbase::Error::ScopeNotFound` or `CollectionNotFound` +- **Solution:** The initializer auto-creates scope/collections, but user needs create permissions + +### Health Check Endpoint + +Check application health: +```bash +curl http://localhost:3000/api/v1/health +``` + +Response shows Couchbase status: +```json +{ + "status": "healthy", + "timestamp": "2025-12-02T10:30:00Z", + "services": { + "couchbase": { + "status": "up", + "message": "Connected to travel-sample bucket" + } + } +} +``` + +### Getting Help + +If issues persist: +1. Check application logs for detailed error messages +2. Verify Ruby version matches `.ruby-version` (3.4.1) +3. Run `bundle install` to ensure all gems are current +4. Check Couchbase SDK compatibility + # Appendix ## Data Model diff --git a/app/controllers/api/v1/airlines_controller.rb b/app/controllers/api/v1/airlines_controller.rb index ab17e47..f449dfe 100644 --- a/app/controllers/api/v1/airlines_controller.rb +++ b/app/controllers/api/v1/airlines_controller.rb @@ -3,7 +3,6 @@ module Api module V1 class AirlinesController < ApplicationController - skip_before_action :verify_authenticity_token, only: %i[create update destroy] before_action :set_airline, only: %i[show update destroy] # GET /api/v1/airlines/{id} diff --git a/app/controllers/api/v1/airports_controller.rb b/app/controllers/api/v1/airports_controller.rb index 7a7d121..e4a6197 100644 --- a/app/controllers/api/v1/airports_controller.rb +++ b/app/controllers/api/v1/airports_controller.rb @@ -3,7 +3,6 @@ module Api module V1 class AirportsController < ApplicationController - skip_before_action :verify_authenticity_token, only: %i[create update destroy] before_action :set_airport, only: %i[show update destroy] # GET /api/v1/airports/{id} diff --git a/app/controllers/api/v1/health_controller.rb b/app/controllers/api/v1/health_controller.rb new file mode 100644 index 0000000..df2065f --- /dev/null +++ b/app/controllers/api/v1/health_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Api + module V1 + class HealthController < ApplicationController + def show + health_status = { + status: 'healthy', + timestamp: Time.current.iso8601, + services: { + couchbase: check_couchbase + } + } + + all_up = health_status[:services].values.all? { |s| s[:status] == 'up' } + status_code = all_up ? :ok : :service_unavailable + + render json: health_status, status: status_code + end + + private + + def check_couchbase + if defined?(COUCHBASE_CLUSTER) && COUCHBASE_CLUSTER + # Perform simple bucket check to verify connection + COUCHBASE_CLUSTER.bucket('travel-sample') + { status: 'up', message: 'Connected to travel-sample bucket' } + else + { status: 'down', message: 'Couchbase not initialized' } + end + rescue StandardError => e + { status: 'down', message: e.message } + end + end + end +end diff --git a/app/controllers/api/v1/hotels_controller.rb b/app/controllers/api/v1/hotels_controller.rb index 7dda9c4..2de1d19 100644 --- a/app/controllers/api/v1/hotels_controller.rb +++ b/app/controllers/api/v1/hotels_controller.rb @@ -3,11 +3,10 @@ module Api module V1 class HotelsController < ApplicationController - skip_before_action :verify_authenticity_token, only: %i[search filter] before_action :validate_query_params, only: [:search] # GET /api/v1/hotels/autocomplete def search - @hotels = HotelSearch.search_name(params[:name]) + @hotels = Hotel.search_name(params[:name]) render json: @hotels, status: :ok rescue StandardError => e render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error @@ -15,22 +14,22 @@ def search # GET /api/v1/hotels/filter def filter - @hotels = HotelSearch.filter(HotelSearch.new( + @hotels = Hotel.filter(Hotel.new( { - "name"=> hotel_search_params[:name], - "title" => hotel_search_params[:title], - "description" => hotel_search_params[:description], - "country" => hotel_search_params[:country], - "city" => hotel_search_params[:city], - "state" => hotel_search_params[:state] + "name"=> hotel_params[:name], + "title" => hotel_params[:title], + "description" => hotel_params[:description], + "country" => hotel_params[:country], + "city" => hotel_params[:city], + "state" => hotel_params[:state] } - ), hotel_search_params[:offset], hotel_search_params[:limit]) + ), hotel_params[:offset], hotel_params[:limit]) render json: @hotels, status: :ok rescue StandardError => e render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error end - def hotel_search_params + def hotel_params params.require(:hotel).permit(:name, :title, :description, :country, :city, :state, :offset, :limit) end diff --git a/app/controllers/api/v1/routes_controller.rb b/app/controllers/api/v1/routes_controller.rb index 272beab..15b3434 100644 --- a/app/controllers/api/v1/routes_controller.rb +++ b/app/controllers/api/v1/routes_controller.rb @@ -3,7 +3,6 @@ module Api module V1 class RoutesController < ApplicationController - skip_before_action :verify_authenticity_token, only: %i[create update destroy] before_action :set_route, only: %i[show update destroy] # GET /api/v1/routes/{id} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d1..b575228 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,12 @@ -class ApplicationController < ActionController::Base +class ApplicationController < ActionController::API + rescue_from CouchbaseConnection::CouchbaseUnavailableError, with: :handle_database_unavailable + + private + + def handle_database_unavailable(exception) + render json: { + error: 'Service Unavailable', + message: 'Database connection is not available. Please check configuration or try again later.' + }, status: :service_unavailable + end end diff --git a/app/models/airline.rb b/app/models/airline.rb index c771cb2..c1da9e5 100644 --- a/app/models/airline.rb +++ b/app/models/airline.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Airline + include CouchbaseConnection + attr_accessor :name, :iata, :icao, :callsign, :country def initialize(attributes) @@ -12,6 +14,7 @@ def initialize(attributes) end def self.find(id) + ensure_couchbase! result = AIRLINE_COLLECTION.get(id) new(result.content) if result.success? rescue Couchbase::Error::DocumentNotFound @@ -19,6 +22,7 @@ def self.find(id) end def self.all(country = nil, limit = 10, offset = 0) + ensure_couchbase! bucket_name = 'travel-sample' scope_name = 'inventory' collection_name = 'airline' @@ -33,6 +37,7 @@ def self.all(country = nil, limit = 10, offset = 0) end def self.to_airport(destination_airport_code, limit = 10, offset = 0) + ensure_couchbase! bucket_name = 'travel-sample' scope_name = 'inventory' route_collection_name = 'route' @@ -58,6 +63,7 @@ def self.to_airport(destination_airport_code, limit = 10, offset = 0) end def self.create(id, attributes) + ensure_couchbase! required_fields = %w[name iata icao callsign country] missing_fields = required_fields - attributes.keys extra_fields = attributes.keys - required_fields @@ -78,6 +84,7 @@ def self.create(id, attributes) end def update(id, attributes) + self.class.ensure_couchbase! required_fields = %w[name iata icao callsign country] missing_fields = required_fields - attributes.keys extra_fields = attributes.keys - required_fields @@ -98,6 +105,7 @@ def update(id, attributes) end def destroy(id) + self.class.ensure_couchbase! AIRLINE_COLLECTION.remove(id) end end diff --git a/app/models/airport.rb b/app/models/airport.rb index e4367e7..6908c3f 100644 --- a/app/models/airport.rb +++ b/app/models/airport.rb @@ -1,4 +1,6 @@ class Airport + include CouchbaseConnection + attr_accessor :airportname, :city, :country, :faa, :icao, :tz, :geo def initialize(attributes) @@ -16,6 +18,7 @@ def initialize(attributes) end def self.find(id) + ensure_couchbase! result = AIRPORT_COLLECTION.get(id) new(result.content) if result.success? rescue Couchbase::Error::DocumentNotFound @@ -23,6 +26,7 @@ def self.find(id) end def self.create(id, attributes) + ensure_couchbase! required_fields = %w[airportname city country faa icao tz geo] missing_fields = required_fields - attributes.keys extra_fields = attributes.keys - (required_fields + ['geo']) @@ -52,6 +56,7 @@ def self.create(id, attributes) end def update(id, attributes) + self.class.ensure_couchbase! required_fields = %w[airportname city country faa icao tz geo] missing_fields = required_fields - attributes.keys extra_fields = attributes.keys - (required_fields + ['geo']) @@ -81,6 +86,7 @@ def update(id, attributes) end def destroy(id) + self.class.ensure_couchbase! AIRPORT_COLLECTION.remove(id) true rescue Couchbase::Error::DocumentNotFound @@ -88,6 +94,7 @@ def destroy(id) end def self.direct_connections(destination_airport_code, limit = 10, offset = 0) + ensure_couchbase! bucket_name = 'travel-sample' scope_name = 'inventory' route_collection_name = 'route' diff --git a/app/models/concerns/couchbase_connection.rb b/app/models/concerns/couchbase_connection.rb new file mode 100644 index 0000000..ad30d1e --- /dev/null +++ b/app/models/concerns/couchbase_connection.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module CouchbaseConnection + extend ActiveSupport::Concern + + class_methods do + def couchbase_available? + defined?(COUCHBASE_CLUSTER) && + !COUCHBASE_CLUSTER.nil? && + defined?(AIRLINE_COLLECTION) && + !AIRLINE_COLLECTION.nil? + end + + def ensure_couchbase! + unless couchbase_available? + raise CouchbaseUnavailableError, + 'Couchbase is not initialized. Check database configuration and ensure Couchbase is running.' + end + end + end + + class CouchbaseUnavailableError < StandardError; end +end diff --git a/app/models/hotel.rb b/app/models/hotel.rb new file mode 100644 index 0000000..a14dcd8 --- /dev/null +++ b/app/models/hotel.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class Hotel + include CouchbaseConnection + + attr_accessor :name, :title, :description, :country, :city, :state + + def initialize(attributes) + @name = attributes['name'] + @title = attributes['title'] + @description = attributes['description'] + @country = attributes['country'] + @city = attributes['city'] + @state = attributes['state'] + end + + def self.search_name(name) + ensure_couchbase! + request = Couchbase::SearchRequest.new( + Couchbase::SearchQuery.match(name) { |obj| obj.field = 'name' } + ) + options = Couchbase::Options::Search.new + options.limit = 50 + options.fields = ['name'] + result = INVENTORY_SCOPE.search(INDEX_NAME, request, options) + result.rows.map do |row| + row.fields['name'] + end + end + + def self.filter(hotel, offset, limit) + ensure_couchbase! + query = Couchbase::SearchQuery.conjuncts + + query.and_also( + Couchbase::SearchQuery.term(hotel.name) { |obj| obj.field = 'name_keyword' } + ) if hotel.name + + query.and_also( + Couchbase::SearchQuery.match(hotel.title) { |obj| obj.field = 'title' } + ) if hotel.title + + query.and_also( + Couchbase::SearchQuery.match(hotel.description) { |obj| obj.field = 'description' } + ) if hotel.description + + query.and_also( + Couchbase::SearchQuery.match(hotel.country) { |obj| obj.field = 'country' } + ) if hotel.country + + query.and_also( + Couchbase::SearchQuery.match(hotel.state) { |obj| obj.field = 'state' } + ) if hotel.state + + query.and_also( + Couchbase::SearchQuery.match(hotel.city) { |obj| obj.field = 'city' } + ) if hotel.city + + request = Couchbase::SearchRequest.new(query) + + options = Couchbase::Options::Search.new + options.skip = 0 + options.limit = 50 + options.skip = offset if offset + options.limit = limit if limit + options.fields = ['*'] + options.sort = ['-_score', 'name_keyword'] + + result = INVENTORY_SCOPE.search(INDEX_NAME, request, options) + result.rows.map do |row| + new(row.fields) + end + end +end diff --git a/app/models/hotel_search.rb b/app/models/hotel_search.rb deleted file mode 100644 index ba2c269..0000000 --- a/app/models/hotel_search.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -class HotelSearch - attr_accessor :name, :title, :description, :country, :city, :state - - def initialize(attributes) - @name = attributes['name'] - @title = attributes['title'] - @description = attributes['description'] - @country = attributes['country'] - @city = attributes['city'] - @state = attributes['state'] - end - - def self.search_name(name) - request = Couchbase::SearchRequest.new( - Couchbase::SearchQuery.match(name) {|obj| obj.field = "name"} - ) - options = Couchbase::Options::Search.new - options.limit = 50 - options.fields = ["name"] - result = INVENTORY_SCOPE.search(INDEX_NAME, request, options) - result.rows.map do |row| - row.fields["name"] - end - end - - def self.filter(hotel_search, offset, limit) - query = Couchbase::SearchQuery.conjuncts() - - query.and_also( - Couchbase::SearchQuery.term(hotel_search.name) {|obj| obj.field = "name_keyword"} - ) if hotel_search.name - - query.and_also( - Couchbase::SearchQuery.match(hotel_search.title) {|obj| obj.field = "title"} - ) if hotel_search.title - - query.and_also( - Couchbase::SearchQuery.match(hotel_search.description) {|obj| obj.field = "description"} - ) if hotel_search.description - - query.and_also( - Couchbase::SearchQuery.match(hotel_search.country) {|obj| obj.field = "country"} - ) if hotel_search.country - - query.and_also( - Couchbase::SearchQuery.match(hotel_search.state) {|obj| obj.field = "state"} - ) if hotel_search.state - - query.and_also( - Couchbase::SearchQuery.match(hotel_search.city) {|obj| obj.field = "city"} - ) if hotel_search.city - - request = Couchbase::SearchRequest.new(query) - - options = Couchbase::Options::Search.new - options.skip = 0 - options.limit = 50 - options.skip = offset if offset - options.limit= limit if limit - options.fields = ["*"] - options.sort = ["-_score", "name_keyword"] - - result = INVENTORY_SCOPE.search(INDEX_NAME, request, options) - result.rows.map do |row| - new(row.fields) - end - end -end - \ No newline at end of file diff --git a/app/models/route.rb b/app/models/route.rb index 393b6a5..2922d20 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -1,4 +1,6 @@ class Route + include CouchbaseConnection + attr_accessor :airline, :airlineid, :sourceairport, :destinationairport, :stops, :equipment, :schedule, :distance def initialize(attributes) @@ -19,6 +21,7 @@ def initialize(attributes) end def self.find(id) + ensure_couchbase! result = ROUTE_COLLECTION.get(id) new(result.content) if result.success? rescue Couchbase::Error::DocumentNotFound @@ -26,6 +29,7 @@ def self.find(id) end def self.create(id, attributes) + ensure_couchbase! required_fields = %w[airline airlineid sourceairport destinationairport stops equipment distance] missing_fields = required_fields - attributes.keys extra_fields = attributes.keys - (required_fields + ['schedule']) @@ -57,6 +61,7 @@ def self.create(id, attributes) end def update(id, attributes) + self.class.ensure_couchbase! required_fields = %w[airline airlineid sourceairport destinationairport stops equipment distance] missing_fields = required_fields - attributes.keys extra_fields = attributes.keys - (required_fields + ['schedule']) @@ -88,6 +93,7 @@ def update(id, attributes) end def destroy(id) + self.class.ensure_couchbase! ROUTE_COLLECTION.remove(id) true rescue Couchbase::Error::DocumentNotFound diff --git a/config/initializers/couchbase.rb b/config/initializers/couchbase.rb index 7e10908..fc48502 100644 --- a/config/initializers/couchbase.rb +++ b/config/initializers/couchbase.rb @@ -6,78 +6,131 @@ DB_CONN_STR = ENV['DB_CONN_STR'] DB_BUCKET_NAME = 'travel-sample' # Hardcoded bucket name -# Check if running in CI environment -if ENV['CI'] - # Use environment variables from GitHub Secrets - options = Couchbase::Cluster::ClusterOptions.new - options.authenticate(DB_USERNAME, DB_PASSWORD) - COUCHBASE_CLUSTER = Couchbase::Cluster.connect(DB_CONN_STR, options) -else - # Load environment variables from dev.env file - require 'dotenv' - Dotenv.load('dev.env') +# Helper method to connect with retry logic for transient failures +def connect_with_retry(max_attempts: 3, delay: 2) + attempts = 0 + begin + attempts += 1 + yield + rescue Couchbase::Error::Timeout, + Errno::ECONNREFUSED, + Errno::EHOSTUNREACH => e + if attempts < max_attempts + warn "Couchbase connection attempt #{attempts}/#{max_attempts} failed: #{e.message}. Retrying in #{delay}s..." + sleep delay + retry + else + raise + end + end +end - # Define default values - DEFAULT_DB_USERNAME = 'Administrator' - DEFAULT_DB_PASSWORD = 'password' - DEFAULT_DB_CONN_STR = 'couchbase://localhost' +# Helper method to set all Couchbase constants to nil +def set_couchbase_constants_to_nil + Object.const_set(:COUCHBASE_CLUSTER, nil) unless defined?(COUCHBASE_CLUSTER) + Object.const_set(:INVENTORY_SCOPE, nil) unless defined?(INVENTORY_SCOPE) + Object.const_set(:INDEX_NAME, nil) unless defined?(INDEX_NAME) + Object.const_set(:AIRLINE_COLLECTION, nil) unless defined?(AIRLINE_COLLECTION) + Object.const_set(:AIRPORT_COLLECTION, nil) unless defined?(AIRPORT_COLLECTION) + Object.const_set(:ROUTE_COLLECTION, nil) unless defined?(ROUTE_COLLECTION) + Object.const_set(:HOTEL_COLLECTION, nil) unless defined?(HOTEL_COLLECTION) +end - # Get environment variables with fallback to default values - DB_USERNAME = ENV.fetch('DB_USERNAME', DEFAULT_DB_USERNAME) - DB_PASSWORD = ENV.fetch('DB_PASSWORD', DEFAULT_DB_PASSWORD) - DB_CONN_STR = ENV.fetch('DB_CONN_STR', DEFAULT_DB_CONN_STR) +begin + # Check if running in CI environment + if ENV['CI'] + # Use environment variables from GitHub Secrets with retry logic + connect_with_retry do + options = Couchbase::Cluster::ClusterOptions.new + options.authenticate(DB_USERNAME, DB_PASSWORD) + COUCHBASE_CLUSTER = Couchbase::Cluster.connect(DB_CONN_STR, options) + end + else + # Load environment variables from dev.env file + require 'dotenv' + Dotenv.load('dev.env') - # Connect to the Couchbase cluster - options = Couchbase::Cluster::ClusterOptions.new - options.authenticate(DB_USERNAME, DB_PASSWORD) - COUCHBASE_CLUSTER = Couchbase::Cluster.connect(DB_CONN_STR, options) -end + # Define default values + DEFAULT_DB_USERNAME = 'Administrator' + DEFAULT_DB_PASSWORD = 'password' + DEFAULT_DB_CONN_STR = 'couchbase://localhost' -# Open the bucket -bucket = COUCHBASE_CLUSTER.bucket(DB_BUCKET_NAME) + # Get environment variables with fallback to default values (using local variables) + db_username = ENV.fetch('DB_USERNAME', DEFAULT_DB_USERNAME) + db_password = ENV.fetch('DB_PASSWORD', DEFAULT_DB_PASSWORD) + db_conn_str = ENV.fetch('DB_CONN_STR', DEFAULT_DB_CONN_STR) -# Open the default collection -default_collection = bucket.default_collection + # Connect to the Couchbase cluster + options = Couchbase::Cluster::ClusterOptions.new + options.authenticate(db_username, db_password) + COUCHBASE_CLUSTER = Couchbase::Cluster.connect(db_conn_str, options) + end -# Create scope and collections if they don't exist -begin - scope = bucket.scope('inventory') -rescue Couchbase::Error::ScopeNotFoundError - bucket.create_scope('inventory') - scope = bucket.scope('inventory') -end + # Open the bucket + bucket = COUCHBASE_CLUSTER.bucket(DB_BUCKET_NAME) -begin - # create hotel search index - index_file_path = 'hotel_search_index.json' - index_content = File.read(index_file_path) - index_data = JSON.parse(index_content) - name = index_data["name"] - index = Couchbase::Management::SearchIndex.new - index.name= index_data["name"] - index.type= index_data["type"] - index.uuid= index_data["uuid"] if index_data.has_key?("uuid") - index.params= index_data["params"] if index_data.has_key?("params") - index.source_name= index_data["sourceName"] if index_data.has_key?("sourceName") - index.source_type= index_data["sourceType"] if index_data.has_key?("sourceType") - index.source_uuid= index_data["sourceUUID"] if index_data.has_key?("sourceUUID") - index.source_params= index_data["sourceParams"] if index_data.has_key?("sourceParams") - index.plan_params= index_data["planParams"] if index_data.has_key?("planParams") - scope.search_indexes.upsert_index(index) -rescue StandardError => err - #puts err.full_message -end + # Open the default collection + default_collection = bucket.default_collection -%w[airline airport route].each do |collection_name| - scope.collection(collection_name) -rescue Couchbase::Error::CollectionNotFoundError - scope.create_collection(collection_name) -end + # Create scope and collections if they don't exist + begin + scope = bucket.scope('inventory') + rescue Couchbase::Error::ScopeNotFoundError + bucket.create_scope('inventory') + scope = bucket.scope('inventory') + end + + begin + # create hotel search index + index_file_path = 'hotel_search_index.json' + index_content = File.read(index_file_path) + index_data = JSON.parse(index_content) + name = index_data["name"] + index = Couchbase::Management::SearchIndex.new + index.name= index_data["name"] + index.type= index_data["type"] + index.uuid= index_data["uuid"] if index_data.has_key?("uuid") + index.params= index_data["params"] if index_data.has_key?("params") + index.source_name= index_data["sourceName"] if index_data.has_key?("sourceName") + index.source_type= index_data["sourceType"] if index_data.has_key?("sourceType") + index.source_uuid= index_data["sourceUUID"] if index_data.has_key?("sourceUUID") + index.source_params= index_data["sourceParams"] if index_data.has_key?("sourceParams") + index.plan_params= index_data["planParams"] if index_data.has_key?("planParams") + scope.search_indexes.upsert_index(index) + rescue StandardError => err + #puts err.full_message + end -# Scope is declared as constant to run FTS queries -INVENTORY_SCOPE = scope -INDEX_NAME = name -AIRLINE_COLLECTION = INVENTORY_SCOPE.collection('airline') -AIRPORT_COLLECTION = INVENTORY_SCOPE.collection('airport') -ROUTE_COLLECTION = INVENTORY_SCOPE.collection('route') -HOTEL_COLLECTION = INVENTORY_SCOPE.collection('hotel') + %w[airline airport route].each do |collection_name| + scope.collection(collection_name) + rescue Couchbase::Error::CollectionNotFoundError + scope.create_collection(collection_name) + end + + # Scope is declared as constant to run FTS queries + INVENTORY_SCOPE = scope + INDEX_NAME = name + AIRLINE_COLLECTION = INVENTORY_SCOPE.collection('airline') + AIRPORT_COLLECTION = INVENTORY_SCOPE.collection('airport') + ROUTE_COLLECTION = INVENTORY_SCOPE.collection('route') + HOTEL_COLLECTION = INVENTORY_SCOPE.collection('hotel') +rescue StandardError => e + error_message = "Couchbase initialization failed: #{e.class.name} - #{e.message}" + + case defined?(Rails) ? Rails.env.to_s : (ENV['RAILS_ENV'] || 'development') + when 'test' + # In test environment, allow boot but log warning + # Tests will fail with CouchbaseUnavailableError from models + warn "\n#{'='*80}\n⚠️ WARNING: #{error_message}\n" + warn "Integration tests require Couchbase. See README.md for setup.\n#{'='*80}\n" + set_couchbase_constants_to_nil + when 'production' + # In production, fail fast - don't start without database + abort("FATAL: #{error_message}\nProduction requires a valid Couchbase connection.") + else + # Development: allow boot for convenience, log warning + warn "\n#{'='*80}\n⚠️ WARNING: #{error_message}\n" + warn "API endpoints will return 503. See README.md for setup.\n#{'='*80}\n" + set_couchbase_constants_to_nil + end +end diff --git a/config/routes.rb b/config/routes.rb index 3cb9d7f..4836351 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,9 @@ namespace :api do namespace :v1 do + # Health check endpoint + get 'health', to: 'health#show' + # Airlines resource routes get 'airlines/list', to: 'airlines#index' get 'airlines/to-airport', to: 'airlines#to_airport' diff --git a/spec/requests/api/v1/airlines_spec.rb b/spec/requests/api/v1/airlines_spec.rb index 004a265..dd84ca1 100644 --- a/spec/requests/api/v1/airlines_spec.rb +++ b/spec/requests/api/v1/airlines_spec.rb @@ -1,171 +1,287 @@ -require 'swagger_helper' - -describe 'Airlines API', type: :request do - path '/api/v1/airlines/{id}' do - get 'Retrieves an airline by ID' do - tags 'Airlines' - produces 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the airline' - - response '200', 'airline found' do - schema type: :object, - properties: { - name: { type: :string }, - iata: { type: :string }, - icao: { type: :string }, - callsign: { type: :string }, - country: { type: :string } - }, - required: %w[name iata icao callsign country] - - let(:id) { 'airline_10' } - run_test! - end +# frozen_string_literal: true - response '404', 'airline not found' do - let(:id) { 'invalid_id' } - run_test! - end +require 'rails_helper' + +RSpec.describe 'Airlines API', type: :request do + describe 'GET /api/v1/airlines/{id}' do + let(:airline_id) { 'airline_10' } + let(:expected_airline) do + { + 'name' => '40-Mile Air', + 'iata' => 'Q5', + 'icao' => 'MLA', + 'callsign' => 'MILE-AIR', + 'country' => 'United States' + } end - post 'Creates an airline' do - tags 'Airlines' - consumes 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the airline' - parameter name: :airline, in: :body, schema: { - type: :object, - properties: { - name: { type: :string }, - iata: { type: :string }, - icao: { type: :string }, - callsign: { type: :string }, - country: { type: :string } - }, - required: %w[name iata icao callsign country] + it 'returns the airline' do + get "/api/v1/airlines/#{airline_id}" + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to eq(expected_airline) + end + end + + describe 'POST /api/v1/airlines/{id}' do + let(:airline_id) { 'airline_post' } + let(:airline_params) do + { + 'name' => '40-Mile Air', + 'iata' => 'Q5', + 'icao' => 'MLA', + 'callsign' => 'MILE-AIR', + 'country' => 'United States' } + end - response '201', 'airline created' do - let(:airline) { { name: 'Foo Airlines', iata: 'FA', icao: 'FOO', callsign: 'FOO', country: 'US' } } - run_test! - end + context 'when the airline is created successfully' do + it 'returns the created airline' do + post "/api/v1/airlines/#{airline_id}", params: { airline: airline_params } - response '400', 'bad request' do - let(:airline) { { name: 'Foo Airlines', iata: 'FA', icao: 'FOO', callsign: 'FOO' } } - run_test! + expect(response).to have_http_status(:created) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(airline_params) + rescue StandardError => e + puts e + ensure + delete "/api/v1/airlines/#{airline_id}" end + end + + context 'when the airline already exists' do + let(:airline_id) { 'airline_137' } + it 'returns a conflict error' do + post "/api/v1/airlines/#{airline_id}", params: { airline: airline_params } - response '409', 'airline already exists' do - let(:airline) { { name: 'Foo Airlines', iata: 'FA', icao: 'FOO', callsign: 'FOO', country: 'US' } } - run_test! + expect(response).to have_http_status(:conflict) + expect(JSON.parse(response.body)).to include({ 'error' => "Airline with ID #{airline_id} already exists" }) end end + end - put 'Updates an airline' do - tags 'Airlines' - consumes 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the airline' - parameter name: :airline, in: :body, schema: { - type: :object, - properties: { - name: { type: :string }, - iata: { type: :string }, - icao: { type: :string }, - callsign: { type: :string }, - country: { type: :string } - } + describe 'PUT /api/v1/airlines/{id}' do + let(:airline_id) { 'airline_put' } + + let(:current_params) do + { + 'name' => '40-Mile Air', + 'iata' => 'U5', + 'icao' => 'UPD', + 'callsign' => 'MILE-AIR', + 'country' => 'United States' } + end + let(:updated_params) do + { + 'name' => '41-Mile Air', + 'iata' => 'U6', + 'icao' => 'UPE', + 'callsign' => 'UPDA-AIR', + 'country' => 'Updated States' + } + end - response '200', 'airline updated' do - let(:id) { 'airline_10' } - let(:airline) { { name: 'Updated Airline' } } - run_test! + context 'when the airline is updated successfully' do + it 'returns the updated airline' do + put "/api/v1/airlines/#{airline_id}", params: { airline: updated_params } + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(updated_params) + rescue StandardError => e + puts e + ensure + puts "Deleting airline with ID #{airline_id}" + delete "/api/v1/airlines/#{airline_id}" end + end + + context 'when the airline is not updated successfully' do + it 'returns a bad request error' do + post "/api/v1/airlines/#{airline_id}", params: { airline: current_params } - response '400', 'bad request' do - let(:id) { 'airline_10' } - let(:airline) { { name: '' } } - run_test! + expect(response).to have_http_status(:created) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(current_params) + + put "/api/v1/airlines/#{airline_id}", params: { airline: { name: '' } } + + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)).to include({ 'error' => 'Invalid request', + 'message' => 'Missing fields: iata, icao, callsign, country' }) + rescue StandardError => e + puts e + ensure + delete "/api/v1/airlines/#{airline_id}" end end + end + + describe 'DELETE /api/v1/airlines/{id}' do + let(:airline_id) { 'airline_delete' } + let(:airline_params) do + { + 'name' => '40-Mile Air', + 'iata' => 'Q5', + 'icao' => 'MLA', + 'callsign' => 'MILE-AIR', + 'country' => 'United States' + } + end + + context 'when the airline is deleted successfully' do + it 'returns a success message' do + post "/api/v1/airlines/#{airline_id}", params: { airline: airline_params } - delete 'Deletes an airline' do - tags 'Airlines' - parameter name: :id, in: :path, type: :string, description: 'ID of the airline' + delete "/api/v1/airlines/#{airline_id}" - response '204', 'airline deleted' do - let(:id) { 'airline_10' } - run_test! + expect(response).to have_http_status(:accepted) + expect(JSON.parse(response.body)).to eq({ 'message' => 'Airline deleted successfully' }) end + end + + context 'when the airline does not exist' do + it 'returns a not found error' do + delete "/api/v1/airlines/#{airline_id}" - response '404', 'airline not found' do - let(:id) { 'invalid_id' } - run_test! + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)).to eq({ 'error' => "Airline with ID #{airline_id} not found" }) end end end - path '/api/v1/airlines/list' do - get 'Retrieves all airlines by country' do - tags 'Airlines' - produces 'application/json' - parameter name: :country, in: :query, type: :string, description: 'Country of the airline' - parameter name: :limit, in: :query, type: :integer, description: 'Maximum number of results to return' - parameter name: :offset, in: :query, type: :integer, description: 'Number of results to skip for pagination' - - response '200', 'airlines found' do - schema type: :array, - items: { - type: :object, - properties: { - name: { type: :string }, - iata: { type: :string }, - icao: { type: :string }, - callsign: { type: :string }, - country: { type: :string } - }, - required: %w[name iata icao callsign country] - } - - let(:country) { 'United States' } - let(:limit) { 10 } - let(:offset) { 0 } - run_test! - end + describe 'GET /api/v1/airlines/list' do + let(:country) { 'United States' } + let(:limit) { '10' } + let(:offset) { '0' } + let(:expected_airlines) do + [ + { 'name' => '40-Mile Air', 'iata' => 'Q5', 'icao' => 'MLA', 'callsign' => 'MILE-AIR', + 'country' => 'United States' }, + { 'name' => 'Texas Wings', 'iata' => 'TQ', 'icao' => 'TXW', 'callsign' => 'TXW', 'country' => 'United States' }, + { 'name' => 'Atifly', 'iata' => 'A1', 'icao' => 'A1F', 'callsign' => 'atifly', 'country' => 'United States' }, + { 'name' => 'Locair', 'iata' => 'ZQ', 'icao' => 'LOC', 'callsign' => 'LOCAIR', 'country' => 'United States' }, + { 'name' => 'SeaPort Airlines', 'iata' => 'K5', 'icao' => 'SQH', 'callsign' => 'SASQUATCH', + 'country' => 'United States' }, + { 'name' => 'Alaska Central Express', 'iata' => 'KO', 'icao' => 'AER', 'callsign' => 'ACE AIR', + 'country' => 'United States' }, + { 'name' => 'AirTran Airways', 'iata' => 'FL', 'icao' => 'TRS', 'callsign' => 'CITRUS', + 'country' => 'United States' }, + { 'name' => 'U.S. Air', 'iata' => '-+', 'icao' => '--+', 'callsign' => nil, 'country' => 'United States' }, + { 'name' => 'PanAm World Airways', 'iata' => 'WQ', 'icao' => 'PQW', 'callsign' => nil, + 'country' => 'United States' }, + { 'name' => 'Bemidji Airlines', 'iata' => 'CH', 'icao' => 'BMJ', 'callsign' => 'BEMIDJI', + 'country' => 'United States' } + ] + end + + it 'returns a list of airlines for a given country' do + get '/api/v1/airlines/list', params: { country:, limit:, offset: } + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to eq(expected_airlines) end end - path '/api/v1/airlines/to-airport' do - get 'Retrieves airlines flying to a destination airport' do - tags 'Airlines' - produces 'application/json' - parameter name: :destinationAirportCode, in: :query, type: :string, - description: 'The ICAO or IATA code of the destination airport' - parameter name: :limit, in: :query, type: :integer, description: 'Maximum number of results to return' - parameter name: :offset, in: :query, type: :integer, description: 'Number of results to skip for pagination' - - response '200', 'airlines found' do - schema type: :array, - items: { - type: :object, - properties: { - name: { type: :string }, - iata: { type: :string }, - icao: { type: :string }, - callsign: { type: :string }, - country: { type: :string } - }, - required: %w[name iata icao callsign country] - } - - let(:destinationAirportCode) { 'LAX' } - let(:limit) { 10 } - let(:offset) { 0 } - run_test! + describe 'GET /api/v1/airlines/to-airport' do + let(:destination_airport_code) { 'JFK' } + let(:limit) { '10' } + let(:offset) { '0' } + let(:expected_airlines) do + [ + { + 'callsign' => 'SPEEDBIRD', + 'country' => 'United Kingdom', + 'iata' => 'BA', + 'icao' => 'BAW', + 'name' => 'British Airways' + }, + { + 'callsign' => 'AIRFRANS', + 'country' => 'France', + 'iata' => 'AF', + 'icao' => 'AFR', + 'name' => 'Air France' + }, + { + 'callsign' => 'DELTA', + 'country' => 'United States', + 'iata' => 'DL', + 'icao' => 'DAL', + 'name' => 'Delta Air Lines' + }, + { + 'callsign' => 'AMERICAN', + 'country' => 'United States', + 'iata' => 'AA', + 'icao' => 'AAL', + 'name' => 'American Airlines' + }, + { + 'callsign' => 'HAWAIIAN', + 'country' => 'United States', + 'iata' => 'HA', + 'icao' => 'HAL', + 'name' => 'Hawaiian Airlines' + }, + { + 'callsign' => 'JETBLUE', + 'country' => 'United States', + 'iata' => 'B6', + 'icao' => 'JBU', + 'name' => 'JetBlue Airways' + }, + { + 'callsign' => 'FLAGSHIP', + 'country' => 'United States', + 'iata' => '9E', + 'icao' => 'FLG', + 'name' => 'Pinnacle Airlines' + }, + { + 'callsign' => 'SUN COUNTRY', + 'country' => 'United States', + 'iata' => 'SY', + 'icao' => 'SCX', + 'name' => 'Sun Country Airlines' + }, + { + 'callsign' => 'UNITED', + 'country' => 'United States', + 'iata' => 'UA', + 'icao' => 'UAL', + 'name' => 'United Airlines' + }, + { + 'callsign' => 'U S AIR', + 'country' => 'United States', + 'iata' => 'US', + 'icao' => 'USA', + 'name' => 'US Airways' + } + ] + end + + context 'when destinationAirportCode is provided' do + it 'returns a list of airlines flying to the destination airport' do + get '/api/v1/airlines/to-airport', + params: { destinationAirportCode: destination_airport_code, limit:, offset: } + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to eq(expected_airlines) end + end + + context 'when destinationAirportCode is not provided' do + it 'returns a bad request error' do + get '/api/v1/airlines/to-airport', params: { limit:, offset: } - response '400', 'bad request' do - let(:destinationAirportCode) { '' } - run_test! + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)).to eq({ 'message' => 'Destination airport code is required' }) end end end diff --git a/spec/requests/api/v1/airports_spec.rb b/spec/requests/api/v1/airports_spec.rb index e25cebc..fe95ca8 100644 --- a/spec/requests/api/v1/airports_spec.rb +++ b/spec/requests/api/v1/airports_spec.rb @@ -1,201 +1,217 @@ -require 'swagger_helper' - -describe 'Airports API', type: :request do - path '/api/v1/airports/{id}' do - get 'Retrieves an airport by ID' do - tags 'Airports' - produces 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the airport' - - response '200', 'airport found' do - schema type: :object, - properties: { - airportname: { type: :string }, - city: { type: :string }, - country: { type: :string }, - faa: { type: :string }, - icao: { type: :string }, - tz: { type: :string }, - geo: { - type: :object, - properties: { - alt: { type: :number }, - lat: { type: :number }, - lon: { type: :number } - } - } - }, - required: %w[airportname city country faa icao tz geo] - - let(:id) { 'airport_1262' } - run_test! +require 'rails_helper' + +RSpec.describe 'Airports API', type: :request do + describe 'GET /api/v1/airports/{id}' do + let(:airport_id) { 'airport_1262' } + + context 'when the airport exists' do + let(:expected_airport) do + { + 'airportname' => 'La Garenne', + 'city' => 'Agen', + 'country' => 'France', + 'faa' => 'AGF', + 'icao' => 'LFBA', + 'tz' => 'Europe/Paris', + 'geo' => { + 'lat' => 44.174721, + 'lon' => 0.590556, + 'alt' => 204 + } + } end - response '404', 'airport not found' do - let(:id) { 'invalid_id' } - run_test! + it 'returns the airport' do + get "/api/v1/airports/#{airport_id}" + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to eq(expected_airport) end end - post 'Creates an airport' do - tags 'Airports' - consumes 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the airport' - parameter name: :airport, in: :body, schema: { - type: :object, - properties: { - airportname: { type: :string }, - city: { type: :string }, - country: { type: :string }, - faa: { type: :string }, - icao: { type: :string }, - tz: { type: :string }, - geo: { - type: :object, - properties: { - alt: { type: :number }, - lat: { type: :number }, - lon: { type: :number } - } - } - }, - required: %w[airportname city country faa icao tz geo] - } + context 'when the airport does not exist' do + it 'returns a not found error' do + get '/api/v1/airports/invalid_id' - response '201', 'airport created' do - let(:airport) do - { - airportname: 'Test Airport', - city: 'Test City', - country: 'Test Country', - faa: '', - icao: 'Test LFAG', - tz: 'Test Europe/Paris', - geo: { - lat: 49.868547, - lon: 3.029578, - alt: 295.0 - } - } - end - run_test! + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)).to eq({ 'message' => 'Airport not found' }) end + end + end - response '400', 'bad request' do - let(:airport) do - { - airportname: 'Test Airport', - city: 'Test City', - country: 'Test Country', - faa: '', - icao: 'Test LFAG', - tz: 'Test Europe/Paris', - geo: { - lat: 49.868547, - lon: 3.029578 - } - } - end - run_test! + describe 'POST /api/v1/airports/{id}' do + let(:airport_id) { 'airport_post' } + let(:airport_params) do + { + 'airportname' => 'Test Airport', + 'city' => 'Test City', + 'country' => 'Test Country', + 'faa' => '', + 'icao' => 'Test LFAG', + 'tz' => 'Test Europe/Paris', + 'geo' => { + 'lat' => 49.868547, + 'lon' => 3.029578, + 'alt' => 295.0 + } + } + end + + context 'when the airport is created successfully' do + it 'returns the created airport' do + post "/api/v1/airports/#{airport_id}", params: { airport: airport_params } + + expect(response).to have_http_status(:created) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(airport_params) + rescue StandardError => e + puts e + ensure + delete "/api/v1/airports/#{airport_id}" end + end - response '409', 'airport already exists' do - let(:airport) do - { - airportname: 'Test Airport', - city: 'Test City', - country: 'Test Country', - faa: '', - icao: 'Test LFAG', - tz: 'Test Europe/Paris', - geo: { - lat: 49.868547, - lon: 3.029578, - alt: 295.0 - } - } - end - run_test! + context 'when the airport already exists' do + let(:airport_id) { 'airport_1262' } + it 'returns a conflict error' do + post "/api/v1/airports/#{airport_id}", params: { airport: airport_params } + + expect(response).to have_http_status(:conflict) + expect(JSON.parse(response.body)).to include({ 'message' => "Airport with ID #{airport_id} already exists" }) end end + end - put 'Updates an airport' do - tags 'Airports' - consumes 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the airport' - parameter name: :airport, in: :body, schema: { - type: :object, - properties: { - airportname: { type: :string }, - city: { type: :string }, - country: { type: :string }, - faa: { type: :string }, - icao: { type: :string }, - tz: { type: :string }, - geo: { - type: :object, - properties: { - alt: { type: :number }, - lat: { type: :number }, - lon: { type: :number } - } - } + describe 'PUT /api/v1/airports/{id}' do + let(:airport_id) { 'airport_put' } + let(:current_params) do + { + 'airportname' => 'Test Airport', + 'city' => 'Test City', + 'country' => 'Test Country', + 'faa' => 'BCD', + 'icao' => 'TEST', + 'tz' => 'Test Europe/Paris', + 'geo' => { + 'lat' => 49.868547, + 'lon' => 3.029578, + 'alt' => 295.0 } } + end + let(:updated_params) do + { + 'airportname' => 'Updated Airport', + 'city' => 'Updated City', + 'country' => 'Updated Country', + 'faa' => 'UPD', + 'icao' => 'UPDT', + 'tz' => 'Updated Europe/Paris', + 'geo' => { + 'lat' => 50.868547, + 'lon' => 4.029578, + 'alt' => 300.0 + } + } + end - response '200', 'airport updated' do - let(:id) { 'airport_1262' } - let(:airport) { { airportname: 'Updated Airport' } } - run_test! + context 'when the airport is updated successfully' do + it 'returns the updated airport' do + put "/api/v1/airports/#{airport_id}", params: { airport: updated_params } + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(updated_params) + rescue StandardError => e + puts e + ensure + puts "Deleting airport with ID #{airport_id}" + delete "/api/v1/airports/#{airport_id}" end + end - response '400', 'bad request' do - let(:id) { 'airport_1262' } - let(:airport) { { airportname: '' } } - run_test! + context 'when the airport is not updated successfully' do + it 'returns a bad request error' do + post "/api/v1/airports/#{airport_id}", params: { airport: current_params } + + expect(response).to have_http_status(:created) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(current_params) + + put "/api/v1/airports/#{airport_id}", params: { airport: { airportname: '' } } + + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)).to include({ 'error' => 'Invalid request', + 'message' => 'Missing fields: city, country, faa, icao, tz, geo' }) + rescue StandardError => e + puts e + ensure + delete "/api/v1/airports/#{airport_id}" end end + end - delete 'Deletes an airport' do - tags 'Airports' - parameter name: :id, in: :path, type: :string, description: 'ID of the airport' + describe 'DELETE /api/v1/airports/{id}' do + let(:airport_id) { 'airport_delete' } + let(:airport_params) do + { + 'airportname' => 'Test Airport', + 'city' => 'Test City', + 'country' => 'Test Country', + 'faa' => 'BCD', + 'icao' => 'TEST', + 'tz' => 'Test Europe/Paris', + 'geo' => { + 'lat' => 49.868547, + 'lon' => 3.029578, + 'alt' => 295.0 + } + } + end + + context 'when the airport is deleted successfully' do + it 'returns a success message' do + post "/api/v1/airports/#{airport_id}", params: { airport: airport_params } + expect(response).to have_http_status(:created) - response '204', 'airport deleted' do - let(:id) { 'airport_1262' } - run_test! + delete "/api/v1/airports/#{airport_id}" + expect(response).to have_http_status(:accepted) + expect(JSON.parse(response.body)).to eq({ 'message' => 'Airport deleted successfully' }) end + end - response '404', 'airport not found' do - let(:id) { 'invalid_id' } - run_test! + context 'when the airport does not exist' do + it 'returns a not found error' do + delete "/api/v1/airports/#{airport_id}" + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)).to eq({ 'message' => 'Airport not found' }) end end end - - path '/api/v1/airports/direct-connections' do - get 'Retrieves all direct connections from a target airport' do - tags 'Airports' - produces 'application/json' - parameter name: :destinationAirportCode, in: :query, type: :string, - description: 'FAA code of the target airport', required: true - parameter name: :limit, in: :query, type: :integer, description: 'Maximum number of results to return' - parameter name: :offset, in: :query, type: :integer, description: 'Number of results to skip for pagination' - - response '200', 'direct connections found' do - schema type: :array, - items: { - type: :string - } - - let(:destinationAirportCode) { 'LAX' } - let(:limit) { 10 } - let(:offset) { 0 } - run_test! + describe 'GET /api/v1/airports/direct-connections' do + let(:destination_airport_code) { 'JFK' } + let(:limit) { 10 } + let(:offset) { 0 } + let(:expected_connections) { %w[DEL LHR EZE ATL CUN MEX LAX SAN SEA SFO] } + + context 'when the destination airport code is provided' do + it 'returns the direct connections' do + get '/api/v1/airports/direct-connections', + params: { destinationAirportCode: destination_airport_code, limit: limit, offset: offset } + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to eq(expected_connections) end + end + + context 'when the destination airport code is not provided' do + it 'returns a bad request error' do + get '/api/v1/airports/direct-connections' - response '400', 'bad request' do - let(:destinationAirportCode) { '' } - run_test! + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)).to eq({ 'message' => 'Destination airport code is required' }) end end end diff --git a/spec/requests/api/v1/routes_spec.rb b/spec/requests/api/v1/routes_spec.rb index 139e130..c28776c 100644 --- a/spec/requests/api/v1/routes_spec.rb +++ b/spec/requests/api/v1/routes_spec.rb @@ -1,188 +1,176 @@ -require 'swagger_helper' - -describe 'Routes API', type: :request do - path '/api/v1/routes/{id}' do - get 'Retrieves a route by ID' do - tags 'Routes' - produces 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the route' - - response '200', 'route found' do - schema type: :object, - properties: { - airline: { type: :string }, - airlineid: { type: :string }, - sourceairport: { type: :string }, - destinationairport: { type: :string }, - stops: { type: :integer }, - equipment: { type: :string }, - schedule: { - type: :array, - items: { - type: :object, - properties: { - day: { type: :integer }, - flight: { type: :string }, - utc: { type: :string } - } - } - }, - distance: { type: :number } - }, - required: %w[airline airlineid sourceairport destinationairport stops equipment schedule - distance] - - let(:id) { 'route_10209' } - run_test! - end +require 'rails_helper' - response '404', 'route not found' do - let(:id) { 'invalid_id' } - run_test! - end +RSpec.describe 'Routes API', type: :request do + describe 'GET /api/v1/routes/{id}' do + let(:route_id) { 'route_10209' } + let(:expected_route) do + { + 'airline' => 'AH', + 'airlineid' => 'airline_794', + 'sourceairport' => 'MRS', + 'destinationairport' => 'TLM', + 'stops' => 0, + 'equipment' => '736', + 'schedule' => [ + { 'day' => 0, 'flight' => 'AH705', + 'utc' => '22:18:00' }, { 'day' => 0, 'flight' => 'AH413', 'utc' => '08:47:00' }, { 'day' => 0, 'flight' => 'AH284', 'utc' => '04:25:00' }, { 'day' => 1, 'flight' => 'AH800', 'utc' => '10:05:00' }, { 'day' => 1, 'flight' => 'AH448', 'utc' => '04:59:00' }, { 'day' => 1, 'flight' => 'AH495', 'utc' => '20:17:00' }, { 'day' => 1, 'flight' => 'AH837', 'utc' => '08:30:00' }, { 'day' => 2, 'flight' => 'AH344', 'utc' => '08:32:00' }, { 'day' => 2, 'flight' => 'AH875', 'utc' => '06:28:00' }, { 'day' => 3, 'flight' => 'AH781', 'utc' => '21:15:00' }, { 'day' => 4, 'flight' => 'AH040', 'utc' => '12:57:00' }, { 'day' => 5, 'flight' => 'AH548', 'utc' => '23:09:00' }, { 'day' => 6, 'flight' => 'AH082', 'utc' => '22:47:00' }, { 'day' => 6, 'flight' => 'AH434', 'utc' => '06:12:00' }, { 'day' => 6, 'flight' => 'AH831', 'utc' => '13:10:00' }, { 'day' => 6, 'flight' => 'AH144', 'utc' => '02:48:00' }, { 'day' => 6, 'flight' => 'AH208', 'utc' => '22:39:00' } + ], + 'distance' => 1097.2184613947677 + } + end + + it 'returns the route' do + get "/api/v1/routes/#{route_id}" + + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(expected_route) end + end - post 'Creates a route' do - tags 'Routes' - consumes 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the route' - parameter name: :route, in: :body, schema: { - type: :object, - properties: { - airline: { type: :string }, - airlineid: { type: :string }, - sourceairport: { type: :string }, - destinationairport: { type: :string }, - stops: { type: :integer }, - equipment: { type: :string }, - schedule: { - type: :array, - items: { - type: :object, - properties: { - day: { type: :integer }, - flight: { type: :string }, - utc: { type: :string } - } - } - }, - distance: { type: :number } - }, - required: %w[airline airlineid sourceairport destinationairport stops equipment schedule - distance] + describe 'POST /api/v1/routes/{id}' do + let(:route_id) { 'route_post' } + let(:route_params) do + { + 'airline' => 'AF', + 'airlineid' => 'airline_137', + 'sourceairport' => 'TLV', + 'destinationairport' => 'MRS', + 'stops' => 0, + 'equipment' => '320', + 'schedule' => [ + { 'day' => 0, 'utc' => '10:13:00', 'flight' => 'AF198' }, + { 'day' => 0, 'utc' => '19:14:00', 'flight' => 'AF547' } + # Add more schedule items as needed + ], + 'distance' => 2881.617376098415 } + end - response '201', 'route created' do - let(:route) do - { - airline: 'AF', - airlineid: 'airline_137', - sourceairport: 'TLV', - destinationairport: 'MRS', - stops: 0, - equipment: '320', - schedule: [ - { day: 0, utc: '10:13:00', flight: 'AF198' }, - { day: 0, utc: '19:14:00', flight: 'AF547' } - ], - distance: 2881.617376098415 - } - end - run_test! - end + context 'when the route is created successfully' do + it 'returns the created route' do + post "/api/v1/routes/#{route_id}", params: { route: route_params } + + expect(response).to have_http_status(:created) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(route_params) - response '400', 'bad request' do - let(:route) do - { - airline: 'AF', - airlineid: 'airline_137', - sourceairport: 'TLV', - destinationairport: 'MRS', - stops: 0, - equipment: '320', - schedule: [ - { day: 0, utc: '10:13:00', flight: 'AF198' }, - { day: 0, utc: '19:14:00' } - ], - distance: 2881.617376098415 - } - end - run_test! + delete "/api/v1/routes/#{route_id}" end + end + + context 'when the route already exists' do + let(:route_id) { 'route_10209' } + it 'returns a conflict error' do + post "/api/v1/routes/#{route_id}", params: { route: route_params } - response '409', 'route already exists' do - let(:route) do - { - airline: 'AF', - airlineid: 'airline_137', - sourceairport: 'TLV', - destinationairport: 'MRS', - stops: 0, - equipment: '320', - schedule: [ - { day: 0, utc: '10:13:00', flight: 'AF198' }, - { day: 0, utc: '19:14:00', flight: 'AF547' } - ], - distance: 2881.617376098415 - } - end - run_test! + expect(response).to have_http_status(:conflict) + expect(JSON.parse(response.body)).to include({ 'error' => 'Route already exists' }) end end + end - put 'Updates a route' do - tags 'Routes' - consumes 'application/json' - parameter name: :id, in: :path, type: :string, description: 'ID of the route' - parameter name: :route, in: :body, schema: { - type: :object, - properties: { - airline: { type: :string }, - airlineid: { type: :string }, - sourceairport: { type: :string }, - destinationairport: { type: :string }, - stops: { type: :integer }, - equipment: { type: :string }, - schedule: { - type: :array, - items: { - type: :object, - properties: { - day: { type: :integer }, - flight: { type: :string }, - utc: { type: :string } - } - } - }, - distance: { type: :number } - } + describe 'PUT /api/v1/routes/{id}' do + let(:route_id) { 'route_put' } + let(:current_params) do + { + 'airline' => 'AF', + 'airlineid' => 'airline_137', + 'sourceairport' => 'TLV', + 'destinationairport' => 'MRS', + 'stops' => 0, + 'equipment' => '320', + 'schedule' => [ + { 'day' => 0, 'utc' => '10:13:00', 'flight' => 'AF198' }, + { 'day' => 0, 'utc' => '19:14:00', 'flight' => 'AF547' } + # Add more schedule items as needed + ], + 'distance' => 3000 + } + end + let(:updated_params) do + { + 'airline' => 'AF', + 'airlineid' => 'airline_137', + 'sourceairport' => 'TLV', + 'destinationairport' => 'CDG', + 'stops' => 1, + 'equipment' => '321', + 'schedule' => [ + { 'day' => 1, 'utc' => '11:13:00', 'flight' => 'AF199' }, + { 'day' => 1, 'utc' => '20:14:00', 'flight' => 'AF548' } + # Add more schedule items as needed + ], + 'distance' => 3500 } + end + + context 'when the route is updated successfully' do + it 'returns the updated route' do + put "/api/v1/routes/#{route_id}", params: { route: updated_params } - response '200', 'route updated' do - let(:id) { 'route_10209' } - let(:route) { { stops: 1 } } - run_test! + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(updated_params) + + delete "/api/v1/routes/#{route_id}" end + end + + context 'when the route is not updated successfully' do + it 'returns a bad request error' do + post "/api/v1/routes/#{route_id}", params: { route: current_params } + + expect(response).to have_http_status(:created) + expect(response.content_type).to eq('application/json; charset=utf-8') + expect(JSON.parse(response.body)).to include(current_params) + + put "/api/v1/routes/#{route_id}", params: { route: { airline: '' } } + + expect(response).to have_http_status(:bad_request) + expect(JSON.parse(response.body)).to include({ 'error' => 'Invalid request' }) - response '400', 'bad request' do - let(:id) { 'route_10209' } - let(:route) { { stops: 'invalid' } } - run_test! + delete "/api/v1/routes/#{route_id}" end end + end + + describe 'DELETE /api/v1/routes/{id}' do + let(:route_id) { 'route_delete' } + let(:route_params) do + { + 'airline' => 'AF', + 'airlineid' => 'airline_137', + 'sourceairport' => 'TLV', + 'destinationairport' => 'MRS', + 'stops' => 0, + 'equipment' => '320', + 'schedule' => [ + { 'day' => 0, 'utc' => '10:13:00', 'flight' => 'AF198' }, + { 'day' => 0, 'utc' => '19:14:00', 'flight' => 'AF547' } + # Add more schedule items as needed + ], + 'distance' => 2881.617376098415 + } + end - delete 'Deletes a route' do - tags 'Routes' - parameter name: :id, in: :path, type: :string, description: 'ID of the route' + context 'when the route is deleted successfully' do + it 'returns a success message' do + post "/api/v1/routes/#{route_id}", params: { route: route_params } - response '204', 'route deleted' do - let(:id) { 'route_10209' } - run_test! + delete "/api/v1/routes/#{route_id}" + + expect(response).to have_http_status(:accepted) + expect(JSON.parse(response.body)).to eq({ 'message' => 'Route deleted successfully' }) end + end + + context 'when the route does not exist' do + it 'returns a not found error' do + delete "/api/v1/routes/#{route_id}" - response '404', 'route not found' do - let(:id) { 'invalid_id' } - run_test! + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)).to eq({ 'message' => 'Route not found' }) end end end diff --git a/spec/requests/swagger/airlines_spec.rb b/spec/requests/swagger/airlines_spec.rb new file mode 100644 index 0000000..35f41db --- /dev/null +++ b/spec/requests/swagger/airlines_spec.rb @@ -0,0 +1,199 @@ +require 'swagger_helper' + +describe 'Airlines API', type: :request do + path '/api/v1/airlines/{id}' do + get 'Retrieves an airline by ID' do + tags 'Airlines' + produces 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the airline' + + response '200', 'airline found' do + schema type: :object, + properties: { + name: { type: :string }, + iata: { type: :string }, + icao: { type: :string }, + callsign: { type: :string }, + country: { type: :string } + }, + required: %w[name iata icao callsign country] + + let(:id) { 'airline_10' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + + response '404', 'airline not found' do + let(:id) { 'invalid_id' } + run_test! + end + end + + post 'Creates an airline' do + tags 'Airlines' + consumes 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the airline' + parameter name: :airline, in: :body, schema: { + type: :object, + properties: { + name: { type: :string }, + iata: { type: :string }, + icao: { type: :string }, + callsign: { type: :string }, + country: { type: :string } + }, + required: %w[name iata icao callsign country] + } + + response '201', 'airline created' do + let(:id) { 'airline_new_123' } + let(:airline) { { name: 'Foo Airlines', iata: 'FA', icao: 'FOO', callsign: 'FOO', country: 'US' } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + + response '400', 'bad request' do + let(:id) { 'airline_bad' } + let(:airline) { { name: 'Foo Airlines', iata: 'FA', icao: 'FOO', callsign: 'FOO' } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + + response '409', 'airline already exists' do + let(:id) { 'airline_137' } + let(:airline) { { name: 'Foo Airlines', iata: 'FA', icao: 'FOO', callsign: 'FOO', country: 'US' } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + end + + put 'Updates an airline' do + tags 'Airlines' + consumes 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the airline' + parameter name: :airline, in: :body, schema: { + type: :object, + properties: { + name: { type: :string }, + iata: { type: :string }, + icao: { type: :string }, + callsign: { type: :string }, + country: { type: :string } + } + } + + response '200', 'airline updated' do + let(:id) { 'airline_10' } + let(:airline) { { name: 'Updated Airline', iata: 'UA', icao: 'UPD', callsign: 'UPDATED', country: 'United States' } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + + response '400', 'bad request' do + let(:id) { 'airline_10' } + let(:airline) { { name: '' } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + end + + delete 'Deletes an airline' do + tags 'Airlines' + parameter name: :id, in: :path, type: :string, description: 'ID of the airline' + + response '204', 'airline deleted' do + let(:id) { 'airline_to_delete' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + + response '404', 'airline not found' do + let(:id) { 'invalid_id' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + end + end + + path '/api/v1/airlines/list' do + get 'Retrieves all airlines by country' do + tags 'Airlines' + produces 'application/json' + parameter name: :country, in: :query, type: :string, description: 'Country of the airline' + parameter name: :limit, in: :query, type: :integer, description: 'Maximum number of results to return' + parameter name: :offset, in: :query, type: :integer, description: 'Number of results to skip for pagination' + + response '200', 'airlines found' do + schema type: :array, + items: { + type: :object, + properties: { + name: { type: :string }, + iata: { type: :string }, + icao: { type: :string }, + callsign: { type: :string }, + country: { type: :string } + }, + required: %w[name iata icao callsign country] + } + + let(:country) { 'United States' } + let(:limit) { 10 } + let(:offset) { 0 } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + end + end + + path '/api/v1/airlines/to-airport' do + get 'Retrieves airlines flying to a destination airport' do + tags 'Airlines' + produces 'application/json' + parameter name: :destinationAirportCode, in: :query, type: :string, + description: 'The ICAO or IATA code of the destination airport' + parameter name: :limit, in: :query, type: :integer, description: 'Maximum number of results to return' + parameter name: :offset, in: :query, type: :integer, description: 'Number of results to skip for pagination' + + response '200', 'airlines found' do + schema type: :array, + items: { + type: :object, + properties: { + name: { type: :string }, + iata: { type: :string }, + icao: { type: :string }, + callsign: { type: :string }, + country: { type: :string } + }, + required: %w[name iata icao callsign country] + } + + let(:destinationAirportCode) { 'LAX' } + let(:limit) { 10 } + let(:offset) { 0 } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + + response '400', 'bad request' do + let(:destinationAirportCode) { '' } + let(:limit) { 10 } + let(:offset) { 0 } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airlines_spec.rb + end + end + end + end +end diff --git a/spec/requests/swagger/airports_spec.rb b/spec/requests/swagger/airports_spec.rb new file mode 100644 index 0000000..cc23dc7 --- /dev/null +++ b/spec/requests/swagger/airports_spec.rb @@ -0,0 +1,229 @@ +require 'swagger_helper' + +describe 'Airports API', type: :request do + path '/api/v1/airports/{id}' do + get 'Retrieves an airport by ID' do + tags 'Airports' + produces 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the airport' + + response '200', 'airport found' do + schema type: :object, + properties: { + airportname: { type: :string }, + city: { type: :string }, + country: { type: :string }, + faa: { type: :string }, + icao: { type: :string }, + tz: { type: :string }, + geo: { + type: :object, + properties: { + alt: { type: :number }, + lat: { type: :number }, + lon: { type: :number } + } + } + }, + required: %w[airportname city country faa icao tz geo] + + let(:id) { 'airport_1262' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + + response '404', 'airport not found' do + let(:id) { 'invalid_id' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + end + + post 'Creates an airport' do + tags 'Airports' + consumes 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the airport' + parameter name: :airport, in: :body, schema: { + type: :object, + properties: { + airportname: { type: :string }, + city: { type: :string }, + country: { type: :string }, + faa: { type: :string }, + icao: { type: :string }, + tz: { type: :string }, + geo: { + type: :object, + properties: { + alt: { type: :number }, + lat: { type: :number }, + lon: { type: :number } + } + } + }, + required: %w[airportname city country faa icao tz geo] + } + + response '201', 'airport created' do + let(:id) { 'airport_new_123' } + let(:airport) do + { + airportname: 'Test Airport', + city: 'Test City', + country: 'Test Country', + faa: '', + icao: 'Test LFAG', + tz: 'Test Europe/Paris', + geo: { + lat: 49.868547, + lon: 3.029578, + alt: 295.0 + } + } + end + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + + response '400', 'bad request' do + let(:id) { 'airport_bad' } + let(:airport) do + { + airportname: 'Test Airport', + city: 'Test City', + country: 'Test Country', + faa: '', + icao: 'Test LFAG', + tz: 'Test Europe/Paris', + geo: { + lat: 49.868547, + lon: 3.029578 + } + } + end + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + + response '409', 'airport already exists' do + let(:id) { 'airport_1262' } + let(:airport) do + { + airportname: 'Test Airport', + city: 'Test City', + country: 'Test Country', + faa: '', + icao: 'Test LFAG', + tz: 'Test Europe/Paris', + geo: { + lat: 49.868547, + lon: 3.029578, + alt: 295.0 + } + } + end + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + end + + put 'Updates an airport' do + tags 'Airports' + consumes 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the airport' + parameter name: :airport, in: :body, schema: { + type: :object, + properties: { + airportname: { type: :string }, + city: { type: :string }, + country: { type: :string }, + faa: { type: :string }, + icao: { type: :string }, + tz: { type: :string }, + geo: { + type: :object, + properties: { + alt: { type: :number }, + lat: { type: :number }, + lon: { type: :number } + } + } + } + } + + response '200', 'airport updated' do + let(:id) { 'airport_1262' } + let(:airport) { { airportname: 'Updated Airport', city: 'Updated City', country: 'Updated Country', faa: 'UPD', icao: 'UPDT', tz: 'America/New_York', geo: { lat: 1.0, lon: 1.0, alt: 100.0 } } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + + response '400', 'bad request' do + let(:id) { 'airport_1262' } + let(:airport) { { airportname: '' } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + end + + delete 'Deletes an airport' do + tags 'Airports' + parameter name: :id, in: :path, type: :string, description: 'ID of the airport' + + response '204', 'airport deleted' do + let(:id) { 'airport_to_delete' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + + response '404', 'airport not found' do + let(:id) { 'invalid_id' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + end + end + + path '/api/v1/airports/direct-connections' do + get 'Retrieves all direct connections from a target airport' do + tags 'Airports' + produces 'application/json' + parameter name: :destinationAirportCode, in: :query, type: :string, + description: 'FAA code of the target airport', required: true + parameter name: :limit, in: :query, type: :integer, description: 'Maximum number of results to return' + parameter name: :offset, in: :query, type: :integer, description: 'Number of results to skip for pagination' + + response '200', 'direct connections found' do + schema type: :array, + items: { + type: :string + } + + let(:destinationAirportCode) { 'LAX' } + let(:limit) { 10 } + let(:offset) { 0 } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + + response '400', 'bad request' do + let(:destinationAirportCode) { '' } + let(:limit) { 10 } + let(:offset) { 0 } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/airports_spec.rb + end + end + end + end +end diff --git a/spec/requests/api/v1/hotels_spec.rb b/spec/requests/swagger/hotels_spec.rb similarity index 93% rename from spec/requests/api/v1/hotels_spec.rb rename to spec/requests/swagger/hotels_spec.rb index e273c6c..9cb52da 100644 --- a/spec/requests/api/v1/hotels_spec.rb +++ b/spec/requests/swagger/hotels_spec.rb @@ -2,7 +2,7 @@ describe 'Hotels API', type: :request do path '/api/v1/hotels/autocomplete' do - get 'Retrieve suggestion for Hotel names' do + get 'Retrieve suggestions for Hotel names' do tags 'Hotels' produces 'application/json' parameter name: :name, in: :query, type: :string, description: 'name of the hotel' @@ -19,7 +19,7 @@ end end - response '200', 'No suggestion' do + response '200', 'List of hotel name suggestions' do schema type: :Array, items: { type: :string @@ -70,7 +70,7 @@ expect(data.length()).to be >(0) end end - response '200', 'only one Hotels found' do + response '200', 'List of hotels matching the filter criteria' do schema type: :Array, items: { type: :object, @@ -90,7 +90,7 @@ expect(data.length()).to eq(1) end end - response '200', 'only one Hotels found' do + response '200', 'List of hotels matching the filter criteria' do schema type: :Array, items: { type: :object, diff --git a/spec/requests/swagger/routes_spec.rb b/spec/requests/swagger/routes_spec.rb new file mode 100644 index 0000000..fa55def --- /dev/null +++ b/spec/requests/swagger/routes_spec.rb @@ -0,0 +1,210 @@ +require 'swagger_helper' + +describe 'Routes API', type: :request do + path '/api/v1/routes/{id}' do + get 'Retrieves a route by ID' do + tags 'Routes' + produces 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the route' + + response '200', 'route found' do + schema type: :object, + properties: { + airline: { type: :string }, + airlineid: { type: :string }, + sourceairport: { type: :string }, + destinationairport: { type: :string }, + stops: { type: :integer }, + equipment: { type: :string }, + schedule: { + type: :array, + items: { + type: :object, + properties: { + day: { type: :integer }, + flight: { type: :string }, + utc: { type: :string } + } + } + }, + distance: { type: :number } + }, + required: %w[airline airlineid sourceairport destinationairport stops equipment schedule + distance] + + let(:id) { 'route_10209' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + + response '404', 'route not found' do + let(:id) { 'invalid_id' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + end + + post 'Creates a route' do + tags 'Routes' + consumes 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the route' + parameter name: :route, in: :body, schema: { + type: :object, + properties: { + airline: { type: :string }, + airlineid: { type: :string }, + sourceairport: { type: :string }, + destinationairport: { type: :string }, + stops: { type: :integer }, + equipment: { type: :string }, + schedule: { + type: :array, + items: { + type: :object, + properties: { + day: { type: :integer }, + flight: { type: :string }, + utc: { type: :string } + } + } + }, + distance: { type: :number } + }, + required: %w[airline airlineid sourceairport destinationairport stops equipment schedule + distance] + } + + response '201', 'route created' do + let(:id) { 'route_new_123' } + let(:route) do + { + airline: 'AF', + airlineid: 'airline_137', + sourceairport: 'TLV', + destinationairport: 'MRS', + stops: 0, + equipment: '320', + schedule: [ + { day: 0, utc: '10:13:00', flight: 'AF198' }, + { day: 0, utc: '19:14:00', flight: 'AF547' } + ], + distance: 2881.617376098415 + } + end + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + + response '400', 'bad request' do + let(:id) { 'route_bad' } + let(:route) do + { + airline: 'AF', + airlineid: 'airline_137', + sourceairport: 'TLV', + destinationairport: 'MRS', + stops: 0, + equipment: '320', + schedule: [ + { day: 0, utc: '10:13:00', flight: 'AF198' }, + { day: 0, utc: '19:14:00' } + ], + distance: 2881.617376098415 + } + end + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + + response '409', 'route already exists' do + let(:id) { 'route_10209' } + let(:route) do + { + airline: 'AF', + airlineid: 'airline_137', + sourceairport: 'TLV', + destinationairport: 'MRS', + stops: 0, + equipment: '320', + schedule: [ + { day: 0, utc: '10:13:00', flight: 'AF198' }, + { day: 0, utc: '19:14:00', flight: 'AF547' } + ], + distance: 2881.617376098415 + } + end + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + end + + put 'Updates a route' do + tags 'Routes' + consumes 'application/json' + parameter name: :id, in: :path, type: :string, description: 'ID of the route' + parameter name: :route, in: :body, schema: { + type: :object, + properties: { + airline: { type: :string }, + airlineid: { type: :string }, + sourceairport: { type: :string }, + destinationairport: { type: :string }, + stops: { type: :integer }, + equipment: { type: :string }, + schedule: { + type: :array, + items: { + type: :object, + properties: { + day: { type: :integer }, + flight: { type: :string }, + utc: { type: :string } + } + } + }, + distance: { type: :number } + } + } + + response '200', 'route updated' do + let(:id) { 'route_10209' } + let(:route) { { airline: 'AF', airlineid: 'airline_137', sourceairport: 'TLV', destinationairport: 'MRS', stops: 1, equipment: '330', schedule: [{ day: 1, utc: '11:00:00', flight: 'AF199' }], distance: 150.0 } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + + response '400', 'bad request' do + let(:id) { 'route_10209' } + let(:route) { { stops: 'invalid' } } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + end + + delete 'Deletes a route' do + tags 'Routes' + parameter name: :id, in: :path, type: :string, description: 'ID of the route' + + response '204', 'route deleted' do + let(:id) { 'route_to_delete' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + + response '404', 'route not found' do + let(:id) { 'invalid_id' } + run_test! do |response| + # Documentation-only - actual testing done in spec/requests/api/v1/routes_spec.rb + end + end + end + end +end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 35ddc71..6bb890b 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -25,12 +25,7 @@ paths: {}, servers: [ { - url: 'http://{defaultHost}', - variables: { - defaultHost: { - default: 'localhost:3000' - } - } + url: '/' } ], components: { diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 427089f..1705240 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -441,6 +441,79 @@ paths: type: string '400': description: bad request + "/api/v1/hotels/autocomplete": + get: + summary: Retrieve suggestions for Hotel names + tags: + - Hotels + parameters: + - name: name + in: query + description: name of the hotel + schema: + type: string + responses: + '200': + description: List of hotel name suggestions + content: + application/json: + schema: + type: Array + items: + type: string + "/api/v1/hotels/filter": + post: + summary: Hotel search filter + tags: + - Hotels + parameters: [] + responses: + '200': + description: List of hotels matching the filter criteria + content: + application/json: + schema: + type: Array + items: + type: object + properties: + name: + type: string + title: + type: string + description: + type: string + country: + type: string + city: + type: string + nullable: true + state: + type: string + nullable: true + required: + - name + - title + - description + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + title: + type: string + description: + type: string + country: + type: string + city: + type: string + state: + type: string + description: hotel filter "/api/v1/routes/{id}": get: summary: Retrieves a route by ID @@ -619,10 +692,7 @@ paths: '404': description: route not found servers: -- url: http://{defaultHost} - variables: - defaultHost: - default: localhost:3000 +- url: "/" components: schemas: Airline: diff --git a/test/integration/airlines_spec.rb b/test/integration/airlines_spec.rb deleted file mode 100644 index dd84ca1..0000000 --- a/test/integration/airlines_spec.rb +++ /dev/null @@ -1,288 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe 'Airlines API', type: :request do - describe 'GET /api/v1/airlines/{id}' do - let(:airline_id) { 'airline_10' } - let(:expected_airline) do - { - 'name' => '40-Mile Air', - 'iata' => 'Q5', - 'icao' => 'MLA', - 'callsign' => 'MILE-AIR', - 'country' => 'United States' - } - end - - it 'returns the airline' do - get "/api/v1/airlines/#{airline_id}" - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to eq(expected_airline) - end - end - - describe 'POST /api/v1/airlines/{id}' do - let(:airline_id) { 'airline_post' } - let(:airline_params) do - { - 'name' => '40-Mile Air', - 'iata' => 'Q5', - 'icao' => 'MLA', - 'callsign' => 'MILE-AIR', - 'country' => 'United States' - } - end - - context 'when the airline is created successfully' do - it 'returns the created airline' do - post "/api/v1/airlines/#{airline_id}", params: { airline: airline_params } - - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(airline_params) - rescue StandardError => e - puts e - ensure - delete "/api/v1/airlines/#{airline_id}" - end - end - - context 'when the airline already exists' do - let(:airline_id) { 'airline_137' } - it 'returns a conflict error' do - post "/api/v1/airlines/#{airline_id}", params: { airline: airline_params } - - expect(response).to have_http_status(:conflict) - expect(JSON.parse(response.body)).to include({ 'error' => "Airline with ID #{airline_id} already exists" }) - end - end - end - - describe 'PUT /api/v1/airlines/{id}' do - let(:airline_id) { 'airline_put' } - - let(:current_params) do - { - 'name' => '40-Mile Air', - 'iata' => 'U5', - 'icao' => 'UPD', - 'callsign' => 'MILE-AIR', - 'country' => 'United States' - } - end - let(:updated_params) do - { - 'name' => '41-Mile Air', - 'iata' => 'U6', - 'icao' => 'UPE', - 'callsign' => 'UPDA-AIR', - 'country' => 'Updated States' - } - end - - context 'when the airline is updated successfully' do - it 'returns the updated airline' do - put "/api/v1/airlines/#{airline_id}", params: { airline: updated_params } - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(updated_params) - rescue StandardError => e - puts e - ensure - puts "Deleting airline with ID #{airline_id}" - delete "/api/v1/airlines/#{airline_id}" - end - end - - context 'when the airline is not updated successfully' do - it 'returns a bad request error' do - post "/api/v1/airlines/#{airline_id}", params: { airline: current_params } - - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(current_params) - - put "/api/v1/airlines/#{airline_id}", params: { airline: { name: '' } } - - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)).to include({ 'error' => 'Invalid request', - 'message' => 'Missing fields: iata, icao, callsign, country' }) - rescue StandardError => e - puts e - ensure - delete "/api/v1/airlines/#{airline_id}" - end - end - end - - describe 'DELETE /api/v1/airlines/{id}' do - let(:airline_id) { 'airline_delete' } - let(:airline_params) do - { - 'name' => '40-Mile Air', - 'iata' => 'Q5', - 'icao' => 'MLA', - 'callsign' => 'MILE-AIR', - 'country' => 'United States' - } - end - - context 'when the airline is deleted successfully' do - it 'returns a success message' do - post "/api/v1/airlines/#{airline_id}", params: { airline: airline_params } - - delete "/api/v1/airlines/#{airline_id}" - - expect(response).to have_http_status(:accepted) - expect(JSON.parse(response.body)).to eq({ 'message' => 'Airline deleted successfully' }) - end - end - - context 'when the airline does not exist' do - it 'returns a not found error' do - delete "/api/v1/airlines/#{airline_id}" - - expect(response).to have_http_status(:not_found) - expect(JSON.parse(response.body)).to eq({ 'error' => "Airline with ID #{airline_id} not found" }) - end - end - end - - describe 'GET /api/v1/airlines/list' do - let(:country) { 'United States' } - let(:limit) { '10' } - let(:offset) { '0' } - let(:expected_airlines) do - [ - { 'name' => '40-Mile Air', 'iata' => 'Q5', 'icao' => 'MLA', 'callsign' => 'MILE-AIR', - 'country' => 'United States' }, - { 'name' => 'Texas Wings', 'iata' => 'TQ', 'icao' => 'TXW', 'callsign' => 'TXW', 'country' => 'United States' }, - { 'name' => 'Atifly', 'iata' => 'A1', 'icao' => 'A1F', 'callsign' => 'atifly', 'country' => 'United States' }, - { 'name' => 'Locair', 'iata' => 'ZQ', 'icao' => 'LOC', 'callsign' => 'LOCAIR', 'country' => 'United States' }, - { 'name' => 'SeaPort Airlines', 'iata' => 'K5', 'icao' => 'SQH', 'callsign' => 'SASQUATCH', - 'country' => 'United States' }, - { 'name' => 'Alaska Central Express', 'iata' => 'KO', 'icao' => 'AER', 'callsign' => 'ACE AIR', - 'country' => 'United States' }, - { 'name' => 'AirTran Airways', 'iata' => 'FL', 'icao' => 'TRS', 'callsign' => 'CITRUS', - 'country' => 'United States' }, - { 'name' => 'U.S. Air', 'iata' => '-+', 'icao' => '--+', 'callsign' => nil, 'country' => 'United States' }, - { 'name' => 'PanAm World Airways', 'iata' => 'WQ', 'icao' => 'PQW', 'callsign' => nil, - 'country' => 'United States' }, - { 'name' => 'Bemidji Airlines', 'iata' => 'CH', 'icao' => 'BMJ', 'callsign' => 'BEMIDJI', - 'country' => 'United States' } - ] - end - - it 'returns a list of airlines for a given country' do - get '/api/v1/airlines/list', params: { country:, limit:, offset: } - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to eq(expected_airlines) - end - end - - describe 'GET /api/v1/airlines/to-airport' do - let(:destination_airport_code) { 'JFK' } - let(:limit) { '10' } - let(:offset) { '0' } - let(:expected_airlines) do - [ - { - 'callsign' => 'SPEEDBIRD', - 'country' => 'United Kingdom', - 'iata' => 'BA', - 'icao' => 'BAW', - 'name' => 'British Airways' - }, - { - 'callsign' => 'AIRFRANS', - 'country' => 'France', - 'iata' => 'AF', - 'icao' => 'AFR', - 'name' => 'Air France' - }, - { - 'callsign' => 'DELTA', - 'country' => 'United States', - 'iata' => 'DL', - 'icao' => 'DAL', - 'name' => 'Delta Air Lines' - }, - { - 'callsign' => 'AMERICAN', - 'country' => 'United States', - 'iata' => 'AA', - 'icao' => 'AAL', - 'name' => 'American Airlines' - }, - { - 'callsign' => 'HAWAIIAN', - 'country' => 'United States', - 'iata' => 'HA', - 'icao' => 'HAL', - 'name' => 'Hawaiian Airlines' - }, - { - 'callsign' => 'JETBLUE', - 'country' => 'United States', - 'iata' => 'B6', - 'icao' => 'JBU', - 'name' => 'JetBlue Airways' - }, - { - 'callsign' => 'FLAGSHIP', - 'country' => 'United States', - 'iata' => '9E', - 'icao' => 'FLG', - 'name' => 'Pinnacle Airlines' - }, - { - 'callsign' => 'SUN COUNTRY', - 'country' => 'United States', - 'iata' => 'SY', - 'icao' => 'SCX', - 'name' => 'Sun Country Airlines' - }, - { - 'callsign' => 'UNITED', - 'country' => 'United States', - 'iata' => 'UA', - 'icao' => 'UAL', - 'name' => 'United Airlines' - }, - { - 'callsign' => 'U S AIR', - 'country' => 'United States', - 'iata' => 'US', - 'icao' => 'USA', - 'name' => 'US Airways' - } - ] - end - - context 'when destinationAirportCode is provided' do - it 'returns a list of airlines flying to the destination airport' do - get '/api/v1/airlines/to-airport', - params: { destinationAirportCode: destination_airport_code, limit:, offset: } - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to eq(expected_airlines) - end - end - - context 'when destinationAirportCode is not provided' do - it 'returns a bad request error' do - get '/api/v1/airlines/to-airport', params: { limit:, offset: } - - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)).to eq({ 'message' => 'Destination airport code is required' }) - end - end - end -end diff --git a/test/integration/airports_spec.rb b/test/integration/airports_spec.rb deleted file mode 100644 index fe95ca8..0000000 --- a/test/integration/airports_spec.rb +++ /dev/null @@ -1,218 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Airports API', type: :request do - describe 'GET /api/v1/airports/{id}' do - let(:airport_id) { 'airport_1262' } - - context 'when the airport exists' do - let(:expected_airport) do - { - 'airportname' => 'La Garenne', - 'city' => 'Agen', - 'country' => 'France', - 'faa' => 'AGF', - 'icao' => 'LFBA', - 'tz' => 'Europe/Paris', - 'geo' => { - 'lat' => 44.174721, - 'lon' => 0.590556, - 'alt' => 204 - } - } - end - - it 'returns the airport' do - get "/api/v1/airports/#{airport_id}" - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to eq(expected_airport) - end - end - - context 'when the airport does not exist' do - it 'returns a not found error' do - get '/api/v1/airports/invalid_id' - - expect(response).to have_http_status(:not_found) - expect(JSON.parse(response.body)).to eq({ 'message' => 'Airport not found' }) - end - end - end - - describe 'POST /api/v1/airports/{id}' do - let(:airport_id) { 'airport_post' } - let(:airport_params) do - { - 'airportname' => 'Test Airport', - 'city' => 'Test City', - 'country' => 'Test Country', - 'faa' => '', - 'icao' => 'Test LFAG', - 'tz' => 'Test Europe/Paris', - 'geo' => { - 'lat' => 49.868547, - 'lon' => 3.029578, - 'alt' => 295.0 - } - } - end - - context 'when the airport is created successfully' do - it 'returns the created airport' do - post "/api/v1/airports/#{airport_id}", params: { airport: airport_params } - - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(airport_params) - rescue StandardError => e - puts e - ensure - delete "/api/v1/airports/#{airport_id}" - end - end - - context 'when the airport already exists' do - let(:airport_id) { 'airport_1262' } - it 'returns a conflict error' do - post "/api/v1/airports/#{airport_id}", params: { airport: airport_params } - - expect(response).to have_http_status(:conflict) - expect(JSON.parse(response.body)).to include({ 'message' => "Airport with ID #{airport_id} already exists" }) - end - end - end - - describe 'PUT /api/v1/airports/{id}' do - let(:airport_id) { 'airport_put' } - let(:current_params) do - { - 'airportname' => 'Test Airport', - 'city' => 'Test City', - 'country' => 'Test Country', - 'faa' => 'BCD', - 'icao' => 'TEST', - 'tz' => 'Test Europe/Paris', - 'geo' => { - 'lat' => 49.868547, - 'lon' => 3.029578, - 'alt' => 295.0 - } - } - end - let(:updated_params) do - { - 'airportname' => 'Updated Airport', - 'city' => 'Updated City', - 'country' => 'Updated Country', - 'faa' => 'UPD', - 'icao' => 'UPDT', - 'tz' => 'Updated Europe/Paris', - 'geo' => { - 'lat' => 50.868547, - 'lon' => 4.029578, - 'alt' => 300.0 - } - } - end - - context 'when the airport is updated successfully' do - it 'returns the updated airport' do - put "/api/v1/airports/#{airport_id}", params: { airport: updated_params } - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(updated_params) - rescue StandardError => e - puts e - ensure - puts "Deleting airport with ID #{airport_id}" - delete "/api/v1/airports/#{airport_id}" - end - end - - context 'when the airport is not updated successfully' do - it 'returns a bad request error' do - post "/api/v1/airports/#{airport_id}", params: { airport: current_params } - - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(current_params) - - put "/api/v1/airports/#{airport_id}", params: { airport: { airportname: '' } } - - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)).to include({ 'error' => 'Invalid request', - 'message' => 'Missing fields: city, country, faa, icao, tz, geo' }) - rescue StandardError => e - puts e - ensure - delete "/api/v1/airports/#{airport_id}" - end - end - end - - describe 'DELETE /api/v1/airports/{id}' do - let(:airport_id) { 'airport_delete' } - let(:airport_params) do - { - 'airportname' => 'Test Airport', - 'city' => 'Test City', - 'country' => 'Test Country', - 'faa' => 'BCD', - 'icao' => 'TEST', - 'tz' => 'Test Europe/Paris', - 'geo' => { - 'lat' => 49.868547, - 'lon' => 3.029578, - 'alt' => 295.0 - } - } - end - - context 'when the airport is deleted successfully' do - it 'returns a success message' do - post "/api/v1/airports/#{airport_id}", params: { airport: airport_params } - expect(response).to have_http_status(:created) - - delete "/api/v1/airports/#{airport_id}" - expect(response).to have_http_status(:accepted) - expect(JSON.parse(response.body)).to eq({ 'message' => 'Airport deleted successfully' }) - end - end - - context 'when the airport does not exist' do - it 'returns a not found error' do - delete "/api/v1/airports/#{airport_id}" - expect(response).to have_http_status(:not_found) - expect(JSON.parse(response.body)).to eq({ 'message' => 'Airport not found' }) - end - end - end - describe 'GET /api/v1/airports/direct-connections' do - let(:destination_airport_code) { 'JFK' } - let(:limit) { 10 } - let(:offset) { 0 } - let(:expected_connections) { %w[DEL LHR EZE ATL CUN MEX LAX SAN SEA SFO] } - - context 'when the destination airport code is provided' do - it 'returns the direct connections' do - get '/api/v1/airports/direct-connections', - params: { destinationAirportCode: destination_airport_code, limit: limit, offset: offset } - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to eq(expected_connections) - end - end - - context 'when the destination airport code is not provided' do - it 'returns a bad request error' do - get '/api/v1/airports/direct-connections' - - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)).to eq({ 'message' => 'Destination airport code is required' }) - end - end - end -end diff --git a/test/integration/routes_spec.rb b/test/integration/routes_spec.rb deleted file mode 100644 index c28776c..0000000 --- a/test/integration/routes_spec.rb +++ /dev/null @@ -1,177 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Routes API', type: :request do - describe 'GET /api/v1/routes/{id}' do - let(:route_id) { 'route_10209' } - let(:expected_route) do - { - 'airline' => 'AH', - 'airlineid' => 'airline_794', - 'sourceairport' => 'MRS', - 'destinationairport' => 'TLM', - 'stops' => 0, - 'equipment' => '736', - 'schedule' => [ - { 'day' => 0, 'flight' => 'AH705', - 'utc' => '22:18:00' }, { 'day' => 0, 'flight' => 'AH413', 'utc' => '08:47:00' }, { 'day' => 0, 'flight' => 'AH284', 'utc' => '04:25:00' }, { 'day' => 1, 'flight' => 'AH800', 'utc' => '10:05:00' }, { 'day' => 1, 'flight' => 'AH448', 'utc' => '04:59:00' }, { 'day' => 1, 'flight' => 'AH495', 'utc' => '20:17:00' }, { 'day' => 1, 'flight' => 'AH837', 'utc' => '08:30:00' }, { 'day' => 2, 'flight' => 'AH344', 'utc' => '08:32:00' }, { 'day' => 2, 'flight' => 'AH875', 'utc' => '06:28:00' }, { 'day' => 3, 'flight' => 'AH781', 'utc' => '21:15:00' }, { 'day' => 4, 'flight' => 'AH040', 'utc' => '12:57:00' }, { 'day' => 5, 'flight' => 'AH548', 'utc' => '23:09:00' }, { 'day' => 6, 'flight' => 'AH082', 'utc' => '22:47:00' }, { 'day' => 6, 'flight' => 'AH434', 'utc' => '06:12:00' }, { 'day' => 6, 'flight' => 'AH831', 'utc' => '13:10:00' }, { 'day' => 6, 'flight' => 'AH144', 'utc' => '02:48:00' }, { 'day' => 6, 'flight' => 'AH208', 'utc' => '22:39:00' } - ], - 'distance' => 1097.2184613947677 - } - end - - it 'returns the route' do - get "/api/v1/routes/#{route_id}" - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(expected_route) - end - end - - describe 'POST /api/v1/routes/{id}' do - let(:route_id) { 'route_post' } - let(:route_params) do - { - 'airline' => 'AF', - 'airlineid' => 'airline_137', - 'sourceairport' => 'TLV', - 'destinationairport' => 'MRS', - 'stops' => 0, - 'equipment' => '320', - 'schedule' => [ - { 'day' => 0, 'utc' => '10:13:00', 'flight' => 'AF198' }, - { 'day' => 0, 'utc' => '19:14:00', 'flight' => 'AF547' } - # Add more schedule items as needed - ], - 'distance' => 2881.617376098415 - } - end - - context 'when the route is created successfully' do - it 'returns the created route' do - post "/api/v1/routes/#{route_id}", params: { route: route_params } - - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(route_params) - - delete "/api/v1/routes/#{route_id}" - end - end - - context 'when the route already exists' do - let(:route_id) { 'route_10209' } - it 'returns a conflict error' do - post "/api/v1/routes/#{route_id}", params: { route: route_params } - - expect(response).to have_http_status(:conflict) - expect(JSON.parse(response.body)).to include({ 'error' => 'Route already exists' }) - end - end - end - - describe 'PUT /api/v1/routes/{id}' do - let(:route_id) { 'route_put' } - let(:current_params) do - { - 'airline' => 'AF', - 'airlineid' => 'airline_137', - 'sourceairport' => 'TLV', - 'destinationairport' => 'MRS', - 'stops' => 0, - 'equipment' => '320', - 'schedule' => [ - { 'day' => 0, 'utc' => '10:13:00', 'flight' => 'AF198' }, - { 'day' => 0, 'utc' => '19:14:00', 'flight' => 'AF547' } - # Add more schedule items as needed - ], - 'distance' => 3000 - } - end - let(:updated_params) do - { - 'airline' => 'AF', - 'airlineid' => 'airline_137', - 'sourceairport' => 'TLV', - 'destinationairport' => 'CDG', - 'stops' => 1, - 'equipment' => '321', - 'schedule' => [ - { 'day' => 1, 'utc' => '11:13:00', 'flight' => 'AF199' }, - { 'day' => 1, 'utc' => '20:14:00', 'flight' => 'AF548' } - # Add more schedule items as needed - ], - 'distance' => 3500 - } - end - - context 'when the route is updated successfully' do - it 'returns the updated route' do - put "/api/v1/routes/#{route_id}", params: { route: updated_params } - - expect(response).to have_http_status(:ok) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(updated_params) - - delete "/api/v1/routes/#{route_id}" - end - end - - context 'when the route is not updated successfully' do - it 'returns a bad request error' do - post "/api/v1/routes/#{route_id}", params: { route: current_params } - - expect(response).to have_http_status(:created) - expect(response.content_type).to eq('application/json; charset=utf-8') - expect(JSON.parse(response.body)).to include(current_params) - - put "/api/v1/routes/#{route_id}", params: { route: { airline: '' } } - - expect(response).to have_http_status(:bad_request) - expect(JSON.parse(response.body)).to include({ 'error' => 'Invalid request' }) - - delete "/api/v1/routes/#{route_id}" - end - end - end - - describe 'DELETE /api/v1/routes/{id}' do - let(:route_id) { 'route_delete' } - let(:route_params) do - { - 'airline' => 'AF', - 'airlineid' => 'airline_137', - 'sourceairport' => 'TLV', - 'destinationairport' => 'MRS', - 'stops' => 0, - 'equipment' => '320', - 'schedule' => [ - { 'day' => 0, 'utc' => '10:13:00', 'flight' => 'AF198' }, - { 'day' => 0, 'utc' => '19:14:00', 'flight' => 'AF547' } - # Add more schedule items as needed - ], - 'distance' => 2881.617376098415 - } - end - - context 'when the route is deleted successfully' do - it 'returns a success message' do - post "/api/v1/routes/#{route_id}", params: { route: route_params } - - delete "/api/v1/routes/#{route_id}" - - expect(response).to have_http_status(:accepted) - expect(JSON.parse(response.body)).to eq({ 'message' => 'Route deleted successfully' }) - end - end - - context 'when the route does not exist' do - it 'returns a not found error' do - delete "/api/v1/routes/#{route_id}" - - expect(response).to have_http_status(:not_found) - expect(JSON.parse(response.body)).to eq({ 'message' => 'Route not found' }) - end - end - end -end