From b2d30ad9d7bd41dbfde85a734f7dec593631e4a2 Mon Sep 17 00:00:00 2001 From: Aleksey Evdokimov Date: Fri, 5 Sep 2025 10:11:27 +0300 Subject: [PATCH] Routes API added --- Gemfile | 2 + Gemfile.lock | 14 ++ app/controllers/routes_controller.rb | 19 +++ app/models/permitted_route.rb | 2 +- app/models/segment.rb | 7 +- .../routes/search_segments_service.rb | 53 +++++++ app/services/routes/search_service.rb | 96 +++++++++++++ config/routes.rb | 3 +- ...add_composite_index_on_permitted_routes.rb | 5 + ...4163408_add_composite_index_on_segments.rb | 5 + db/schema.rb | 4 +- spec/factories/permitted_routes.rb | 9 ++ spec/factories/segments.rb | 10 ++ spec/rails_helper.rb | 5 +- .../routes/search_segments_service_spec.rb | 132 +++++++++++++++++ spec/services/routes/search_service_spec.rb | 133 ++++++++++++++++++ spec/support/factory_sequences.rb | 5 + spec/support/shoulda_matchers.rb | 6 + 18 files changed, 504 insertions(+), 6 deletions(-) create mode 100644 app/controllers/routes_controller.rb create mode 100644 app/services/routes/search_segments_service.rb create mode 100644 app/services/routes/search_service.rb create mode 100644 db/migrate/20250904163129_add_composite_index_on_permitted_routes.rb create mode 100644 db/migrate/20250904163408_add_composite_index_on_segments.rb create mode 100644 spec/factories/permitted_routes.rb create mode 100644 spec/factories/segments.rb create mode 100644 spec/services/routes/search_segments_service_spec.rb create mode 100644 spec/services/routes/search_service_spec.rb create mode 100644 spec/support/factory_sequences.rb create mode 100644 spec/support/shoulda_matchers.rb diff --git a/Gemfile b/Gemfile index c2667cf..074c280 100644 --- a/Gemfile +++ b/Gemfile @@ -45,5 +45,7 @@ group :development, :test do gem 'factory_bot_rails' gem 'rspec-rails' + gem 'pry' + gem 'shoulda-matchers' end diff --git a/Gemfile.lock b/Gemfile.lock index fb82a8a..b44c55e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,6 +82,7 @@ GEM brakeman (7.1.0) racc builder (3.3.0) + coderay (1.1.3) concurrent-ruby (1.3.5) connection_pool (2.5.3) crass (1.0.6) @@ -139,6 +140,7 @@ GEM net-pop net-smtp marcel (1.0.4) + method_source (1.1.0) mini_mime (1.1.5) minitest (5.25.5) msgpack (1.8.0) @@ -165,6 +167,8 @@ GEM racc (~> 1.4) nokogiri (1.18.9-arm-linux-musl) racc (~> 1.4) + nokogiri (1.18.9-arm64-darwin) + racc (~> 1.4) nokogiri (1.18.9-x86_64-darwin) racc (~> 1.4) nokogiri (1.18.9-x86_64-linux-gnu) @@ -179,6 +183,7 @@ GEM pg (1.6.1) pg (1.6.1-aarch64-linux) pg (1.6.1-aarch64-linux-musl) + pg (1.6.1-arm64-darwin) pg (1.6.1-x86_64-darwin) pg (1.6.1-x86_64-linux) pg (1.6.1-x86_64-linux-musl) @@ -186,6 +191,9 @@ GEM prettyprint prettyprint (0.2.0) prism (1.4.0) + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) psych (5.2.6) date stringio @@ -285,6 +293,8 @@ GEM rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) securerandom (0.4.1) + shoulda-matchers (6.5.0) + activesupport (>= 5.2.0) solid_cable (3.0.12) actioncable (>= 7.2) activejob (>= 7.2) @@ -312,6 +322,7 @@ GEM thor (1.4.0) thruster (0.1.15) thruster (0.1.15-aarch64-linux) + thruster (0.1.15-arm64-darwin) thruster (0.1.15-x86_64-darwin) thruster (0.1.15-x86_64-linux) timeout (0.4.3) @@ -334,6 +345,7 @@ PLATFORMS aarch64-linux-musl arm-linux-gnu arm-linux-musl + arm64-darwin-24 x86_64-darwin-24 x86_64-linux x86_64-linux-gnu @@ -347,10 +359,12 @@ DEPENDENCIES factory_bot_rails kamal pg (>= 1.1) + pry puma (>= 5.0) rails (~> 8.0.2, >= 8.0.2.1) rspec-rails rubocop-rails-omakase + shoulda-matchers solid_cable solid_cache solid_queue diff --git a/app/controllers/routes_controller.rb b/app/controllers/routes_controller.rb new file mode 100644 index 0000000..928a739 --- /dev/null +++ b/app/controllers/routes_controller.rb @@ -0,0 +1,19 @@ +class RoutesController < ApplicationController + def index + service = Routes::SearchService.new(routes_params) + + if service.perform + render json: service.routes_to_json + else + render json: { errors: service.errors.messages }, status: :unprocessable_entity + end + end + + private + + def routes_params + params.permit(:carrier, :origin_iata, :destination_iata, :departure_from, :departure_to) + end +end + + diff --git a/app/models/permitted_route.rb b/app/models/permitted_route.rb index 08407ea..e097e5f 100644 --- a/app/models/permitted_route.rb +++ b/app/models/permitted_route.rb @@ -14,4 +14,4 @@ class PermittedRoute < ApplicationRecord validates :carrier, :origin_iata, :destination_iata, presence: true -end \ No newline at end of file +end diff --git a/app/models/segment.rb b/app/models/segment.rb index 1a4f078..4c16727 100644 --- a/app/models/segment.rb +++ b/app/models/segment.rb @@ -16,4 +16,9 @@ class Segment < ApplicationRecord validates :airline, :segment_number, presence: true validates :origin_iata, :destination_iata, presence: true, length: { is: 3 } -end \ No newline at end of file + + scope :by_airline, ->(code) { where(airline: code) } + scope :by_path, ->(origin, destination) { where(origin_iata: origin, destination_iata: destination) } + scope :departure_between, ->(from_time, to_time) { where(std: from_time..to_time) } + scope :arrival_before, ->(time) { where('sta < ?', time) } +end diff --git a/app/services/routes/search_segments_service.rb b/app/services/routes/search_segments_service.rb new file mode 100644 index 0000000..06029d6 --- /dev/null +++ b/app/services/routes/search_segments_service.rb @@ -0,0 +1,53 @@ +module Routes + class SearchSegmentsService + MIN_CONNECTION_TIME = 8.hours + MAX_CONNECTION_TIME = 48.hours + + def initialize(carrier:, departure_from:, departure_to:, segment_paths:) + @carrier = carrier + @departure_from = departure_from + @departure_to = departure_to + @segment_paths = segment_paths + end + + def perform + find_segments( + segment_paths: @segment_paths, + segment_departure_from: @departure_from, + segment_departure_to: @departure_to + ) + end + + def segments + @segments ||= [] + end + + private + + def find_segments(segment_paths:, segment_departure_from:, segment_departure_to:, previous_segments: []) + segment_paths.each do |segment_path| + path_segments = Segment.by_airline(@carrier) + .by_path(segment_path[0], segment_path[1]) + .departure_between(segment_departure_from, segment_departure_to) + .arrival_before(@departure_to) + + path_segments.each do |path_segment| + current_segments = previous_segments + [path_segment] + if segment_paths.size == 1 + segments << current_segments + else + next_departure_from = path_segment.sta + MIN_CONNECTION_TIME + next_departure_to = path_segment.sta + MAX_CONNECTION_TIME + + find_segments( + segment_paths: segment_paths[1..-1], + segment_departure_from: next_departure_from, + segment_departure_to: next_departure_to, + previous_segments: current_segments + ) + end + end + end + end + end +end diff --git a/app/services/routes/search_service.rb b/app/services/routes/search_service.rb new file mode 100644 index 0000000..11f5e24 --- /dev/null +++ b/app/services/routes/search_service.rb @@ -0,0 +1,96 @@ +module Routes + class SearchService + include ActiveModel::API + + attr_accessor :carrier, :origin_iata, :destination_iata, :departure_from, :departure_to + + validates :carrier, presence: true + validates :origin_iata, presence: true, length: { is: 3 } + validates :destination_iata, presence: true, length: { is: 3 } + validates :departure_from, presence: true + validates :departure_to, presence: true + + def perform + return false unless valid? + + permitted_route = + PermittedRoute.find_by( + carrier: @carrier, + origin_iata: @origin_iata, + destination_iata: @destination_iata + ) + + return true if permitted_route.blank? + + route_paths(permitted_route).each do |segment_paths| + route_segments_service = + SearchSegmentsService.new( + carrier: carrier, + departure_from: departure_from, + departure_to: departure_to, + segment_paths: segment_paths + ) + + route_segments_service.perform + routes.push(*route_segments_service.segments) + end + + true + end + + def routes_to_json + routes.map do |route_segments| + { + origin_iata: @origin_iata, + destination_iata: @destination_iata, + departure_time: route_segments.first.std.iso8601(3), + arrival_time: route_segments.last.sta.iso8601(3), + segments: + route_segments.map do |segment| + { + carrier: segment.airline, + segment_number: segment.segment_number, + origin_iata: segment.origin_iata, + destination_iata: segment.destination_iata, + std: segment.std.iso8601(3), + sta: segment.sta.iso8601(3) + } + end + } + end + end + + def routes + @routes ||= [] + end + + def departure_from=(value) + @departure_from = Time.zone.parse(value.to_s)&.beginning_of_day + end + + def departure_to=(value) + @departure_to = Time.zone.parse(value.to_s)&.end_of_day + end + + private + + def route_paths(permitted_route) + paths = [] + + if permitted_route.direct + paths << [[permitted_route.origin_iata, permitted_route.destination_iata]] + end + + permitted_route.transfer_iata_codes.each do |code| + stops = code.scan(/.{3}/) + paths.push( + ([permitted_route.origin_iata] + stops + [permitted_route.destination_iata]).each_cons(2).to_a + ) + end + + paths + end + end +end + + diff --git a/config/routes.rb b/config/routes.rb index a125ef0..72c301c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,5 @@ # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check - # Defines the root path route ("/") - # root "posts#index" + resources :routes, only: :index end diff --git a/db/migrate/20250904163129_add_composite_index_on_permitted_routes.rb b/db/migrate/20250904163129_add_composite_index_on_permitted_routes.rb new file mode 100644 index 0000000..239dee5 --- /dev/null +++ b/db/migrate/20250904163129_add_composite_index_on_permitted_routes.rb @@ -0,0 +1,5 @@ +class AddCompositeIndexOnPermittedRoutes < ActiveRecord::Migration[8.0] + def change + add_index :permitted_routes, [:carrier, :origin_iata, :destination_iata] + end +end diff --git a/db/migrate/20250904163408_add_composite_index_on_segments.rb b/db/migrate/20250904163408_add_composite_index_on_segments.rb new file mode 100644 index 0000000..d96b68f --- /dev/null +++ b/db/migrate/20250904163408_add_composite_index_on_segments.rb @@ -0,0 +1,5 @@ +class AddCompositeIndexOnSegments < ActiveRecord::Migration[8.0] + def change + add_index :segments, [:airline, :origin_iata, :destination_iata] + end +end diff --git a/db/schema.rb b/db/schema.rb index 3aa1c2e..2e221bf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_08_28_091639) do +ActiveRecord::Schema[8.0].define(version: 2025_09_04_163408) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -22,6 +22,7 @@ t.text "transfer_iata_codes", default: [], null: false, array: true t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["carrier", "origin_iata", "destination_iata"], name: "idx_on_carrier_origin_iata_destination_iata_0e51fa02f9" end create_table "segments", force: :cascade do |t| @@ -33,5 +34,6 @@ t.datetime "sta" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["airline", "origin_iata", "destination_iata"], name: "index_segments_on_airline_and_origin_iata_and_destination_iata" end end diff --git a/spec/factories/permitted_routes.rb b/spec/factories/permitted_routes.rb new file mode 100644 index 0000000..d871c15 --- /dev/null +++ b/spec/factories/permitted_routes.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :permitted_route do + carrier { "S7" } + origin_iata { "UUS" } + destination_iata { "DME" } + direct { true } + transfer_iata_codes { [] } + end +end diff --git a/spec/factories/segments.rb b/spec/factories/segments.rb new file mode 100644 index 0000000..12194d5 --- /dev/null +++ b/spec/factories/segments.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :segment do + airline { "S" } + segment_number { generate(:segment_number) } + origin_iata { "UUS" } + destination_iata { "DME" } + std { 1.hour.from_now } + sta { 5.hours.from_now } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 79811b8..82f5d18 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -23,7 +23,7 @@ # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # -# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } +Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } # Ensures that the test database schema matches the current schema file. # If there are pending migrations it will invoke `db:test:prepare` to @@ -69,4 +69,7 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + # Include FactoryBot methods + config.include FactoryBot::Syntax::Methods end diff --git a/spec/services/routes/search_segments_service_spec.rb b/spec/services/routes/search_segments_service_spec.rb new file mode 100644 index 0000000..69fbc69 --- /dev/null +++ b/spec/services/routes/search_segments_service_spec.rb @@ -0,0 +1,132 @@ +require 'rails_helper' + +RSpec.describe Routes::SearchSegmentsService do + let(:carrier) { 'S7' } + let(:departure_from) { 1.day.from_now } + let(:departure_to) { 5.days.from_now } + let(:service) { described_class.new(carrier: carrier, departure_from: departure_from, departure_to: departure_to, segment_paths: segment_paths) } + + describe '#perform' do + context 'with single segment path' do + let(:segment_paths) { [['UUS', 'DME']] } + + context 'when segments exist' do + let!(:segment1) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: departure_from + 1.hour, sta: departure_from + 5.hours) } + let!(:segment2) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: departure_from + 2.hours, sta: departure_from + 6.hours) } + let!(:segment3) { create(:segment, airline: 'AE', origin_iata: 'UUS', destination_iata: 'DME', std: departure_from + 1.hour, sta: departure_from + 5.hours) } + + it 'returns segments for the specified carrier and path' do + service.perform + expect(service.segments).to include([segment1], [segment2]) + expect(service.segments).not_to include([segment3]) + end + + context 'when segments are not in departure time range' do + let!(:early_segment) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: departure_from - 1.hour, sta: departure_from + 3.hours) } + let!(:late_segment) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: departure_to + 1.hour, sta: departure_to + 5.hours) } + + it 'returns filtered segments' do + service.perform + expect(service.segments).not_to include([early_segment]) + expect(service.segments).not_to include([late_segment]) + end + end + + context 'when segments are not before departure time' do + let!(:late_arrival_segment) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: departure_from + 1.hour, sta: departure_to + 1.hour) } + + it 'returns filtered segments' do + service.perform + expect(service.segments).not_to include([late_arrival_segment]) + end + end + end + + context 'when no segments exist' do + it 'returns empty array' do + service.perform + expect(service.segments).to eq([]) + end + end + end + + context 'with multiple segment paths' do + let(:segment_paths) { [['UUS', 'DME'], ['DME', 'LED']] } + let(:first_departure) { departure_from + 1.hour } + let(:first_arrival) { departure_from + 5.hours } + let(:second_departure) { first_arrival + 9.hours } + let(:second_arrival) { second_departure + 1.hour } + + context 'when valid connections exist' do + let!(:first_segment) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: first_departure, sta: first_arrival) } + let!(:second_segment) { create(:segment, airline: carrier, origin_iata: 'DME', destination_iata: 'LED', std: second_departure, sta: second_arrival) } + + it 'returns connected segments' do + service.perform + expect(service.segments).to include([first_segment, second_segment]) + end + + context 'with too early connection' do + let!(:too_early_second) { create(:segment, airline: carrier, origin_iata: 'DME', destination_iata: 'LED', std: first_arrival + 1.hour, sta: first_arrival + 2.hours) } + + it 'considers minimum connection time' do + service.perform + expect(service.segments).not_to include([first_segment, too_early_second]) + end + end + + context 'with too late connection' do + let!(:too_late_second) { create(:segment, airline: carrier, origin_iata: 'DME', destination_iata: 'LED', std: first_arrival + 50.hours, sta: first_arrival + 51.hours) } + + it 'considers maximum connection time' do + service.perform + expect(service.segments).not_to include([first_segment, too_late_second]) + end + end + end + + context 'when no valid connections exist' do + let!(:first_segment) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: first_departure, sta: first_arrival) } + let!(:second_segment) { create(:segment, airline: carrier, origin_iata: 'DME', destination_iata: 'LED', std: first_arrival + 1.hour, sta: first_arrival + 2.hours) } + + it 'returns empty array' do + service.perform + expect(service.segments).to eq([]) + end + end + + context 'when 2 segments suit one path' do + let!(:first_segment) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: first_departure, sta: first_arrival) } + let!(:second_segment1) { create(:segment, airline: carrier, origin_iata: 'DME', destination_iata: 'LED', std: second_departure, sta: second_arrival) } + let!(:second_segment2) { create(:segment, airline: carrier, origin_iata: 'DME', destination_iata: 'LED', std: second_departure + 1.hour, sta: second_arrival + 1.hour) } + + it 'returns both valid connections' do + service.perform + expect(service.segments).to include([first_segment, second_segment1]) + expect(service.segments).to include([first_segment, second_segment2]) + end + end + end + + context 'with three segment paths' do + let(:segment_paths) { [['UUS', 'DME'], ['DME', 'LED'], ['LED', 'SVO']] } + let(:first_departure) { departure_from + 1.hour } + let(:first_arrival) { departure_from + 5.hours } + let(:second_departure) { first_arrival + 8.hours } + let(:second_arrival) { second_departure + 1.hour } + let(:third_departure) { second_arrival + 8.hours } + let(:third_arrival) { third_departure + 1.hour } + + context 'when valid connections exist' do + let!(:first_segment) { create(:segment, airline: carrier, origin_iata: 'UUS', destination_iata: 'DME', std: first_departure, sta: first_arrival) } + let!(:second_segment) { create(:segment, airline: carrier, origin_iata: 'DME', destination_iata: 'LED', std: second_departure, sta: second_arrival) } + let!(:third_segment) { create(:segment, airline: carrier, origin_iata: 'LED', destination_iata: 'SVO', std: third_departure, sta: third_arrival) } + + it 'returns all three connected segments' do + service.perform + expect(service.segments).to include([first_segment, second_segment, third_segment]) + end + end + end + end +end diff --git a/spec/services/routes/search_service_spec.rb b/spec/services/routes/search_service_spec.rb new file mode 100644 index 0000000..f46d075 --- /dev/null +++ b/spec/services/routes/search_service_spec.rb @@ -0,0 +1,133 @@ +require 'rails_helper' + +RSpec.describe Routes::SearchService, type: :model do + let(:carrier) { 'S7' } + let(:origin_iata) { 'UUS' } + let(:destination_iata) { 'DME' } + let(:departure_from) { '2024-01-15' } + let(:departure_to) { '2024-01-20' } + let(:service) { described_class.new(carrier: carrier, origin_iata: origin_iata, destination_iata: destination_iata, departure_from: departure_from, departure_to: departure_to) } + + subject(:service) { described_class.new(carrier: carrier, origin_iata: origin_iata, destination_iata: destination_iata, departure_from: departure_from, departure_to: departure_to) } + + describe 'validations' do + it { is_expected.to validate_presence_of(:carrier) } + it { is_expected.to validate_presence_of(:origin_iata) } + it { is_expected.to validate_presence_of(:destination_iata) } + it { is_expected.to validate_presence_of(:departure_from) } + it { is_expected.to validate_presence_of(:departure_to) } + + it { is_expected.to allow_value('UUS').for(:origin_iata) } + it { is_expected.to allow_value('DME').for(:destination_iata) } + it { is_expected.not_to allow_value('UU').for(:origin_iata) } + it { is_expected.not_to allow_value('DM').for(:destination_iata) } + it { is_expected.not_to allow_value('UUUU').for(:origin_iata) } + it { is_expected.not_to allow_value('DMEE').for(:destination_iata) } + end + + describe '#perform' do + context 'when service is invalid' do + let(:carrier) { nil } + + it 'returns false' do + expect(service.perform).to be false + end + end + + context 'when no permitted route exists' do + it 'returns true and does not populate routes' do + expect(service.perform).to be true + expect(service.send(:routes)).to be_empty + end + end + + context 'when permitted route exists' do + context 'with direct route' do + let!(:permitted_route) { create(:permitted_route, carrier: carrier, origin_iata: origin_iata, destination_iata: destination_iata) } + let!(:segment) { create(:segment, airline: carrier, origin_iata: origin_iata, destination_iata: destination_iata, std: service.departure_from + 1.hour, sta: service.departure_from + 5.hours) } + + it 'returns true and populates routes with segments' do + expect(service.perform).to be true + expect(service.send(:routes)).to include([segment]) + end + end + + context 'with transfer route' do + let!(:permitted_route) { create(:permitted_route, carrier: carrier, origin_iata: origin_iata, destination_iata: destination_iata, direct: false, transfer_iata_codes: ["ORD"]) } + let!(:first_segment) { create(:segment, airline: carrier, origin_iata: origin_iata, destination_iata: 'ORD', std: service.departure_from + 1.hour, sta: service.departure_from + 5.hours) } + let!(:second_segment) { create(:segment, airline: carrier, origin_iata: 'ORD', destination_iata: destination_iata, std: service.departure_from + 15.hours, sta: service.departure_from + 16.hours) } + + it 'returns true' do + expect(service.perform).to be true + end + + it 'returns true populates routes with connected segments' do + expect(service.perform).to be true + expect(service.send(:routes)).to include([first_segment, second_segment]) + end + end + + context 'with multiple transfer routes' do + let!(:permitted_route) { create(:permitted_route, carrier: carrier, origin_iata: origin_iata, destination_iata: destination_iata, direct: false, transfer_iata_codes: ["ORDDFW"]) } + let!(:first_segment) { create(:segment, airline: carrier, origin_iata: origin_iata, destination_iata: 'ORD', std: service.departure_from + 1.hour, sta: service.departure_from + 5.hours) } + let!(:second_segment) { create(:segment, airline: carrier, origin_iata: 'ORD', destination_iata: 'DFW', std: service.departure_from + 15.hours, sta: service.departure_from + 17.hours) } + let!(:third_segment) { create(:segment, airline: carrier, origin_iata: 'DFW', destination_iata: destination_iata, std: service.departure_from + 26.hours, sta: service.departure_from + 31.hours) } + + it 'returns true andpopulates routes with all connected segments' do + expect(service.perform).to be true + expect(service.send(:routes)).to include([first_segment, second_segment, third_segment]) + end + end + end + end + + describe '#routes_to_json' do + context 'with single segments' do + let!(:permitted_route) { create(:permitted_route, carrier: carrier, origin_iata: origin_iata, destination_iata: destination_iata) } + let!(:segment1) { create(:segment, airline: carrier, origin_iata: origin_iata, destination_iata: destination_iata, std: service.departure_from + 1.hour, sta: service.departure_from + 5.hours) } + let!(:segment2) { create(:segment, airline: carrier, origin_iata: origin_iata, destination_iata: destination_iata, std: service.departure_from + 14.hours, sta: service.departure_from + 15.hours) } + + it 'returns routes in JSON format' do + service.perform + json_result = service.routes_to_json + + expect(json_result).to be_an(Array) + expect(json_result.length).to eq(2) + + route = json_result.first + expect(route[:origin_iata]).to eq(origin_iata) + expect(route[:destination_iata]).to eq(destination_iata) + expect(route[:departure_time]).to eq(segment1.std.iso8601(3)) + expect(route[:arrival_time]).to eq(segment1.sta.iso8601(3)) + + expect(route[:segments]).to be_an(Array) + expect(route[:segments].length).to eq(1) + + segment_json = route[:segments].first + expect(segment_json[:carrier]).to eq(segment1.airline) + expect(segment_json[:segment_number]).to eq(segment1.segment_number) + expect(segment_json[:origin_iata]).to eq(segment1.origin_iata) + expect(segment_json[:destination_iata]).to eq(segment1.destination_iata) + expect(segment_json[:std]).to eq(segment1.std.iso8601(3)) + expect(segment_json[:sta]).to eq(segment1.sta.iso8601(3)) + end + end + + context 'with multiple segments' do + let!(:permitted_route) { create(:permitted_route, carrier: carrier, origin_iata: origin_iata, destination_iata: destination_iata, transfer_iata_codes: ["ORD"]) } + let!(:segment1) { create(:segment, airline: carrier, origin_iata: origin_iata, destination_iata: 'ORD', std: service.departure_from + 1.hour, sta: service.departure_from + 5.hours) } + let!(:segment2) { create(:segment, airline: carrier, origin_iata: 'ORD', destination_iata: destination_iata, std: service.departure_from + 14.hours, sta: service.departure_from + 16.hours) } + + it 'returns route with multiple segments' do + service.perform + json_result = service.routes_to_json + + route = json_result.first + + expect(route[:segments].length).to eq(2) + expect(route[:departure_time]).to eq(segment1.std.iso8601(3)) + expect(route[:arrival_time]).to eq(segment2.sta.iso8601(3)) + end + end + end +end diff --git a/spec/support/factory_sequences.rb b/spec/support/factory_sequences.rb new file mode 100644 index 0000000..d9b34f3 --- /dev/null +++ b/spec/support/factory_sequences.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + sequence :segment_number do |n| + n.to_s.rjust(4, '0') + end +end diff --git a/spec/support/shoulda_matchers.rb b/spec/support/shoulda_matchers.rb new file mode 100644 index 0000000..7d045f3 --- /dev/null +++ b/spec/support/shoulda_matchers.rb @@ -0,0 +1,6 @@ +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end