diff --git a/.gitignore b/.gitignore index 4a494a75..58456ca4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ # Ignore master key for decrypting credentials and more. /config/master.key + +# Elastic Beanstalk Files +.elasticbeanstalk/* +!.elasticbeanstalk/*.cfg.yml +!.elasticbeanstalk/*.global.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b23f0cb5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: ruby +rvm: +- 2.2 +- jruby + +deploy: + provider: heroku + api_key: + secure: GItCjV2InWRINoetFPfjsKkbGrxkpEzBVudcXAaZb9sQxhG6Sqg3T6TqRyRKheY6aHJMQyjFCtxsRUbGV3p/MZJpFDOTFsor/V08yjixIy4OGOvEhp69f7tlIvJ6lfGaftv9Jd/dhV1ZfbSLXvvVDBbsDJP3OhY9gt34SSu0IfajDvvPK+JfaYr96KlEITFZVFA+dtU5KKbuUE8+wRJF2wRx1ZDXIbaG4V3sLykajm8wqxTqdcb5uDXll4zz8/s5xNaKSufYMcTeUiUpH/dvxFTn82ydT+xEbRL6MoMCWCbtyjVcddeJ/8gfnkM5ka+pNnTDv9u2nM6XMhJibdSqWWgWMkNQeuFpD7rfh3sn70+RTLtHbLqTWG7vDHiMamfGpVret1CDM2/naAesiiwhER8KR1gm1bPNEeSGn+WHKJHFiWfgVehfIeYRaYxaoXiZ6hCYHH0rmvFzSRYMpAUZyEsTt71rAUxi6iScV6K5pKvglnsKIbOUoQoixKcw4auQiwZYSqVh0Q/D1w6wZRi1WIJK7kvAF36A/1NNzJSykCVHZz4xQyXZ4i10PfmLtgSnv+VsNTXq9zXEwoMnmZI6SrpTPHW68+PSy/Vq35gQHp0u4dLRCODUe9V3aMjJgk+niZhIS3SXSmQjHrmaJyp4iuglE96iNUV0B2mOig80tJs= + app: rails-videostore-api + run: + - "sleep 15" + - "rake db:migrate" + - "run rake db:seed" diff --git a/Gemfile.lock b/Gemfile.lock index a0a07df1..ac7226b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,6 +52,8 @@ GEM awesome_print (1.8.0) bootsnap (1.3.2) msgpack (~> 1.0) + bootsnap (1.3.2-java) + msgpack (~> 1.0) builder (3.2.3) byebug (10.0.2) case_transform (0.2) @@ -65,6 +67,9 @@ GEM railties (>= 3.2, < 6.0) erubi (1.7.1) ffi (1.9.25) + ffi (1.9.25-java) + ffi (1.9.25-x64-mingw32) + ffi (1.9.25-x86-mingw32) globalid (0.4.1) activesupport (>= 4.2.0) httparty (0.16.3) @@ -104,18 +109,34 @@ GEM minitest (>= 5.0) ruby-progressbar msgpack (1.2.4) + msgpack (1.2.4-java) + msgpack (1.2.4-x64-mingw32) + msgpack (1.2.4-x86-mingw32) multi_json (1.13.1) multi_xml (0.6.0) nio4r (2.3.1) + nio4r (2.3.1-java) nokogiri (1.8.5) mini_portile2 (~> 2.3.0) + nokogiri (1.8.5-java) + nokogiri (1.8.5-x64-mingw32) + mini_portile2 (~> 2.3.0) + nokogiri (1.8.5-x86-mingw32) + mini_portile2 (~> 2.3.0) pg (1.1.3) + pg (1.1.3-x64-mingw32) + pg (1.1.3-x86-mingw32) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) + pry (0.12.2-java) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + spoon (~> 0.0) pry-rails (0.3.8) pry (>= 0.10.4) puma (3.12.0) + puma (3.12.0-java) rack (2.0.6) rack-cors (1.0.2) rack-test (1.1.0) @@ -150,6 +171,8 @@ GEM ffi (~> 1.0) ruby-progressbar (1.10.0) ruby_dep (1.5.0) + spoon (0.0.6) + ffi spring (2.0.2) activesupport (>= 4.2) spring-watcher-listen (2.0.1) @@ -164,15 +187,24 @@ GEM sprockets (>= 3.0.0) thor (0.20.3) thread_safe (0.3.6) + thread_safe (0.3.6-java) tzinfo (1.2.5) thread_safe (~> 0.1) + tzinfo-data (1.2018.7) + tzinfo (>= 1.0.0) websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) + websocket-driver (0.7.0-java) + websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) will_paginate (3.1.6) PLATFORMS + java ruby + x64-mingw32 + x86-mingw32 + x86-mswin32 DEPENDENCIES active_model_serializers @@ -199,4 +231,4 @@ RUBY VERSION ruby 2.5.1p57 BUNDLED WITH - 1.16.6 + 1.17.2 diff --git a/app/controllers/movies_controller.rb b/app/controllers/movies_controller.rb index 362e2791..9dbfa4cd 100644 --- a/app/controllers/movies_controller.rb +++ b/app/controllers/movies_controller.rb @@ -11,11 +11,48 @@ def index render status: :ok, json: data end + def create + if movie_params[:image_url] != nil + url = movie_params[:image_url] + url = url.slice(31, url.length) + else + url = movie_params[:image_url] + end + movie = Movie.new({external_id: movie_params[:external_id], + overview: movie_params[:overview], + image_url: url, + release_date: movie_params[:release_date], + title: movie_params[:title], + inventory: movie_params[:inventory]}) + if movie.save + render json: { + id: movie.id, + title: movie.title, + overview: movie.overview, + image_url: movie.image_url, + release_date: movie.release_date, + inventory: movie.inventory, + external_id: movie.external_id }, status: :ok + else + if movie.errors.messages[:external_id] == ["has already been taken"] + render json: { + errors: "Hmm... + We think you already have that movie in your film library." + }, status: :bad_request + else + render json: { + errors: movie.errors.messages + }, status: :bad_request + end + end + + end + def show render( status: :ok, json: @movie.as_json( - only: [:title, :overview, :release_date, :inventory], + only: [:title, :overview, :release_date, :inventory, :image_url], methods: [:available_inventory] ) ) @@ -29,4 +66,8 @@ def require_movie render status: :not_found, json: { errors: { title: ["No movie with title #{params["title"]}"] } } end end + + def movie_params + params.permit(:external_id, :image_url, :overview, :release_date, :title, :inventory) + end end diff --git a/app/controllers/rentals_controller.rb b/app/controllers/rentals_controller.rb index 67e77073..ef9c8a0e 100644 --- a/app/controllers/rentals_controller.rb +++ b/app/controllers/rentals_controller.rb @@ -44,6 +44,34 @@ def overdue render status: :ok, json: rentals end + def returned + rentals = Rental.returned.map do |rental| + { + title: rental.movie.title, + customer_id: rental.customer_id, + name: rental.customer.name, + postal_code: rental.customer.postal_code, + checkout_date: rental.checkout_date, + due_date: rental.due_date + } + end + render status: :ok, json: rentals + end + + def out_ok + rentals = Rental.out_ok.map do |rental| + { + title: rental.movie.title, + customer_id: rental.customer_id, + name: rental.customer.name, + postal_code: rental.customer.postal_code, + checkout_date: rental.checkout_date, + due_date: rental.due_date + } + end + render status: :ok, json: rentals + end + private # TODO: make error payloads arrays def require_movie diff --git a/app/models/movie.rb b/app/models/movie.rb index 0016080b..36e42b91 100644 --- a/app/models/movie.rb +++ b/app/models/movie.rb @@ -1,6 +1,8 @@ class Movie < ApplicationRecord has_many :rentals has_many :customers, through: :rentals + validates :title, :external_id, presence: true + validates :external_id, uniqueness: true def available_inventory self.inventory - Rental.where(movie: self, returned: false).length diff --git a/app/models/rental.rb b/app/models/rental.rb index 18654f04..e9147cfb 100644 --- a/app/models/rental.rb +++ b/app/models/rental.rb @@ -15,7 +15,15 @@ def self.first_outstanding(movie, customer) end def self.overdue - self.where(returned: false).where("due_date < ?", Date.today) + self.where(returned: false).where("due_date < ?", Date.today).order(:due_date) + end + + def self.returned + self.where(returned: true).order(:due_date) + end + + def self.out_ok + self.where(returned: false).where("due_date >= ?", Date.today).order(:due_date) end private diff --git a/config/database.yml b/config/database.yml index 50748d61..cedbd30d 100644 --- a/config/database.yml +++ b/config/database.yml @@ -20,8 +20,6 @@ test: database: _test production: - <<: *default - database: _production - username: - password: <%= ENV['_DATABASE_PASSWORD'] %> - + <<: *default + adapter: postgresql + database: db/production.sqlite3 diff --git a/config/routes.rb b/config/routes.rb index f4c99688..49611027 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,12 +3,15 @@ resources :customers, only: [:index] - resources :movies, only: [:index, :show], param: :title + resources :movies, only: [:index, :show, :create], param: :title post "/rentals/:title/check-out", to: "rentals#check_out", as: "check_out" post "/rentals/:title/return", to: "rentals#check_in", as: "check_in" get "/rentals/overdue", to: "rentals#overdue", as: "overdue" + get "/rentals/returned", to: "rentals#returned", as: "returned" + get "/rentals/out-ok", to: "rentals#out_ok", as: "out_ok" + root 'movies#index' end diff --git a/db/schema.rb b/db/schema.rb index ffb28f7e..c7cb7c0a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,40 +10,43 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180618042754) do +ActiveRecord::Schema.define(version: 2018_06_18_042754) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" create_table "customers", force: :cascade do |t| - t.string "name" + t.string "name" t.datetime "registered_at" - t.string "address" - t.string "city" - t.string "state" - t.string "postal_code" - t.string "phone" - t.float "account_credit" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "address" + t.string "city" + t.string "state" + t.string "postal_code" + t.string "phone" + t.float "account_credit" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "movies", force: :cascade do |t| - t.string "title" - t.text "overview" - t.date "release_date" - t.integer "inventory" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "image_url" - t.integer "external_id" + t.string "title" + t.text "overview" + t.date "release_date" + t.integer "inventory" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "image_url" + t.integer "external_id" end create_table "rentals", force: :cascade do |t| - t.integer "customer_id" - t.integer "movie_id" - t.date "checkout_date" - t.date "due_date" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "returned" + t.integer "customer_id" + t.integer "movie_id" + t.date "checkout_date" + t.date "due_date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "returned" t.index ["customer_id"], name: "index_rentals_on_customer_id" t.index ["movie_id"], name: "index_rentals_on_movie_id" end diff --git a/db/seeds.rb b/db/seeds.rb index 6abb1e9f..4c88d3b3 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -9,4 +9,5 @@ ap "#{movie_data['title']} Added to the library!" movies.first.inventory = movie_data['inventory'] movies.first.save unless movies.empty? + sleep(2) end diff --git a/test/controllers/movies_controller_test.rb b/test/controllers/movies_controller_test.rb index 9172cf6e..72ac49bc 100644 --- a/test/controllers/movies_controller_test.rb +++ b/test/controllers/movies_controller_test.rb @@ -1,4 +1,5 @@ require 'test_helper' +require 'pry' class MoviesControllerTest < ActionDispatch::IntegrationTest describe "index" do @@ -42,6 +43,49 @@ class MoviesControllerTest < ActionDispatch::IntegrationTest end end + describe "create" do + let(:movie_data) { + { + title: "Savior Of The Curse", + overview: "The strange woman claims the boy has a DNA strain which very likely offers the key to cure several crippling diseases. Tests will have to be done, but the key to save millions of lives is within the grasp of science through this DNA strain. Unsure what to think of all this and of this strange woman, the boy hesitantly agrees to the proposal, there's something exciting about this whole situation, surely the right choice was made.", + release_date: "2010-11-05", + inventory: 5, + image_url: "https://image.tmdb.org/t/p/w185/kMQPixpwZ9KDTxvjlXJ5HHJtYOG.jpg", + external_id: 999999999 + } + } + + it "creates a new movie given valid data" do + expect { + post movies_path, params: movie_data + }.must_change "Movie.count", 1 + + body = JSON.parse(response.body) + expect(body).must_be_kind_of Hash + expect(body).must_include "id" + + movie = Movie.find(body["id"].to_i) + + expect(movie.title).must_equal movie_data[:title] + must_respond_with :success + end + + it "returns an error for missing needed movie info to create object" do + expect { + post movies_path + }.wont_change "Movie.count" + + body = JSON.parse(response.body) + + expect(body).must_be_kind_of Hash + expect(body).must_include "errors" + expect(body["errors"]).must_include "title" + must_respond_with :bad_request + end + + end + + describe "show" do it "Returns a JSON object" do get movie_url(title: movies(:one).title) diff --git a/test/controllers/rentals_controller_test.rb b/test/controllers/rentals_controller_test.rb index 831b5230..4339eb0c 100644 --- a/test/controllers/rentals_controller_test.rb +++ b/test/controllers/rentals_controller_test.rb @@ -90,7 +90,7 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest @rental.returned.must_equal true end - it "can check out a rental and return it" do + it "can check out a rental and return it" do # Arrange Rental.destroy_all customer = Customer.first @@ -117,11 +117,11 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest expect(rental.returned).must_equal true - + end - + it "requires a valid movie title" do post check_in_path(title: "does not exist"), params: { customer_id: @rental.customer.id @@ -273,4 +273,113 @@ class RentalsControllerTest < ActionDispatch::IntegrationTest end end end + + describe "returned" do + + it "Returns a JSON array" do + get returned_path + must_respond_with :success + @response.headers['Content-Type'].must_include 'json' + + # Attempt to parse + data = JSON.parse @response.body + data.must_be_kind_of Array + end + + it "Returns an empty array if no rentals returned" do + # Make sure there's none returned + Rental.all.each do |r| + r.returned = false + r.save! + end + + get returned_path + must_respond_with :success + + data = JSON.parse @response.body + data.must_be_kind_of Array + data.length.must_equal 0 + end + + it "Returns expected fields" do + # Make sure we get something back + first = Rental.first + first.returned = true + first.save! + Rental.returned.length.must_be :>, 0 + + get returned_path + must_respond_with :success + + data = JSON.parse @response.body + data.must_be_kind_of Array + data.length.must_equal Rental.returned.length + + data.each do |rental| + rental.must_be_kind_of Hash + rental.must_include "title" + rental.must_include "customer_id" + rental.must_include "name" + rental.must_include "postal_code" + rental.must_include "checkout_date" + rental.must_include "due_date" + end + end + end + + describe "out_ok" do + + it "Returns a JSON array" do + get out_ok_path + must_respond_with :success + @response.headers['Content-Type'].must_include 'json' + + # Attempt to parse + data = JSON.parse @response.body + data.must_be_kind_of Array + end + + it "Returns an empty array if no checked-out rentals currently in good standing" do + # Make sure they're all either checked out and overdue, + # or checked out with a due date in the past + Rental.all.each do |r| + r.due_date = Date.today - 30 + r.save! + end + + get out_ok_path + must_respond_with :success + + data = JSON.parse @response.body + data.must_be_kind_of Array + data.length.must_equal 0 + end + + it "Returns expected fields" do + # Make sure we get something back + first = Rental.first + first.due_date = Date.today + 30 + first.returned = false + first.save! + Rental.out_ok.length.must_be :>, 0 + + get out_ok_path + must_respond_with :success + + data = JSON.parse @response.body + data.must_be_kind_of Array + data.length.must_equal Rental.out_ok.length + + data.each do |rental| + rental.must_be_kind_of Hash + rental.must_include "title" + rental.must_include "customer_id" + rental.must_include "name" + rental.must_include "postal_code" + rental.must_include "checkout_date" + rental.must_include "due_date" + end + end + end + end diff --git a/test/models/movie_test.rb b/test/models/movie_test.rb index 70b6a7c6..d0f67823 100644 --- a/test/models/movie_test.rb +++ b/test/models/movie_test.rb @@ -6,12 +6,14 @@ class MovieTest < ActiveSupport::TestCase "title": "Hidden Figures", "overview": "Some text", "release_date": "1960-06-16", - "inventory": 8 + "inventory": 8, + "external_id": 9999 } } before do @movie = Movie.new(movie_data) + @duplicate_movie = Movie.new(movie_data) end describe "Constructor" do @@ -26,6 +28,11 @@ class MovieTest < ActiveSupport::TestCase it "Has customers" do @movie.must_respond_to :customers end + + it "Will not create a movie if external_id is not unique" do + @duplicate_movie.save + expect(@duplicate_movie.errors.messages).present? + end end describe "available_inventory" do diff --git a/test/models/rental_test.rb b/test/models/rental_test.rb index 3a83449c..767eaf8a 100644 --- a/test/models/rental_test.rb +++ b/test/models/rental_test.rb @@ -1,4 +1,5 @@ require 'test_helper' +require 'pry' class RentalTest < ActiveSupport::TestCase let(:rental_data) { @@ -186,4 +187,120 @@ class RentalTest < ActiveSupport::TestCase Rental.overdue.length.must_equal 0 end end + + describe "returned" do + it "returns all returned rentals" do + # Start with a clean slate + Rental.destroy_all + + outstanding = Rental.create!( + movie: movies(:one), + customer: customers(:one), + due_date: Date.today + 30, + returned: false + ) + Rental.new( + movie: movies(:one), + customer: customers(:one), + due_date: Date.today - 10, + returned: true + ).save!(validate: false) + + second = Rental.create!( + movie: movies(:one), + customer: customers(:two), + due_date: Date.today + 10, + returned: true + ) + Rental.returned.length.must_equal 2 + Rental.returned.last.must_equal second + Rental.all.count.must_equal 3 + end + end + + describe "out_ok" do + it "returns all outstanding rentals that aren't due yet" do + # Start with a clean slate + Rental.destroy_all + + first = Rental.create!( + movie: movies(:one), + customer: customers(:one), + due_date: Date.today + 10, + returned: false + ) + second = Rental.create!( + movie: movies(:one), + customer: customers(:two), + due_date: Date.today + 20, + returned: false + ) + returned = Rental.create!( + movie: movies(:two), + customer: customers(:two), + due_date: Date.today + 10, + returned: true + ) + Rental.new( + movie: movies(:two), + customer: customers(:one), + due_date: Date.today - 30, + returned: false + ).save!(validate: false) + Rental.out_ok.length.must_equal 2 + Rental.out_ok.first.must_equal first + Rental.all.count.must_equal 4 + end + + it "considers today's date as not yet overdue" do + # Start with a clean slate + Rental.destroy_all + + Rental.new( + movie: movies(:one), + customer: customers(:one), + due_date: Date.today, + returned: false + ).save!(validate: false) + Rental.out_ok.length.must_equal 1 + Rental.all.count.must_equal 1 + end + end + + describe "all rentals" do + it "all rentals = overdue + returned + out_ok" do + # Start with a clean slate + Rental.destroy_all + + out_ok = Rental.create!( + movie: movies(:one), + customer: customers(:one), + due_date: Date.today + 10, + returned: true + ) + # Overdue rental: + Rental.new( + movie: movies(:one), + customer: customers(:two), + due_date: Date.today - 10, + returned: false + ).save!(validate: false) + returned_due_later = Rental.create!( + movie: movies(:two), + customer: customers(:two), + due_date: Date.today + 10, + returned: true + ) + # Returned rental due in the past: + Rental.new( + movie: movies(:two), + customer: customers(:one), + due_date: Date.today - 10, + returned: true + ).save!(validate: false) + expected_count = (Rental.overdue.length + Rental.returned.length + Rental.out_ok.length) + Rental.all.count.must_equal expected_count + end + end + end