diff --git a/Gemfile b/Gemfile index f6e0857..cc953d7 100644 --- a/Gemfile +++ b/Gemfile @@ -43,6 +43,7 @@ group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem # gem 'bundler-audit' gem 'capybara' #used to write tests + gem 'cloudinary' gem "debug", platforms: %i[ mri mingw x64_mingw ] gem 'factory_bot_rails' #used to make fake data gem 'faker' #used to make fake data diff --git a/Gemfile.lock b/Gemfile.lock index 93b7b12..c28bc73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,6 +69,7 @@ GEM addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) + aws_cf_signer (0.1.3) base64 (0.1.1) bcrypt (3.1.19) bootsnap (1.16.0) @@ -83,6 +84,9 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + cloudinary (1.27.0) + aws_cf_signer + rest-client (>= 2.0.0) coderay (1.1.3) concurrent-ruby (1.2.2) crass (1.0.6) @@ -92,6 +96,8 @@ GEM reline (>= 0.3.1) diff-lcs (1.5.0) docile (1.4.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) erubi (1.12.0) factory_bot (6.2.1) activesupport (>= 5.0.0) @@ -102,6 +108,9 @@ GEM i18n (>= 1.8.11, < 2) globalid (1.2.1) activesupport (>= 6.1) + http-accept (1.7.0) + http-cookie (1.0.5) + domain_name (~> 0.5) i18n (1.14.1) concurrent-ruby (~> 1.0) io-console (0.6.0) @@ -123,6 +132,9 @@ GEM marcel (1.0.2) matrix (0.4.2) method_source (1.0.0) + mime-types (3.5.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2023.1003) mini_mime (1.1.5) minitest (5.20.0) msgpack (1.7.2) @@ -135,6 +147,7 @@ GEM timeout net-smtp (0.4.0) net-protocol + netrc (0.11.0) nio4r (2.5.9) nokogiri (1.15.4-x86_64-darwin) racc (~> 1.4) @@ -194,6 +207,11 @@ GEM regexp_parser (2.8.2) reline (0.3.9) io-console (~> 0.5) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) rexml (3.2.6) rspec-core (3.12.2) rspec-support (~> 3.12.0) @@ -244,6 +262,9 @@ GEM timeout (0.4.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.2) unicode-display_width (2.5.0) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) @@ -260,6 +281,7 @@ DEPENDENCIES bcrypt (~> 3.1.7) bootsnap capybara + cloudinary debug factory_bot_rails faker diff --git a/app/controllers/api/v1/sessions_controller.rb b/app/controllers/api/v1/sessions_controller.rb index a5ff7ac..4eecb75 100644 --- a/app/controllers/api/v1/sessions_controller.rb +++ b/app/controllers/api/v1/sessions_controller.rb @@ -25,7 +25,7 @@ def destroy end private - + def authorize render json: { error: 'Not authorized' }, status: :unauthorized unless session[:user_id] end diff --git a/app/controllers/api/v1/users/loans_controller.rb b/app/controllers/api/v1/users/loans_controller.rb index 4d7002c..8e0f508 100644 --- a/app/controllers/api/v1/users/loans_controller.rb +++ b/app/controllers/api/v1/users/loans_controller.rb @@ -1,14 +1,14 @@ class Api::V1::Users::LoansController < ApplicationController - def create - puzzle = Puzzle.find(params[:puzzle_id]) - borrower = User.find(params[:borrower_id]) - - loan = Loan.new(owner: puzzle.user, puzzle:, borrower:) + before_action :find_users_and_puzzle, only: [:create] + before_action :check_puzzle_status, only: [:create] + def create + loan = Loan.new(owner_id: @owner.id, puzzle_id: @puzzle.id, borrower_id: @borrower.id) + if loan.save - render json: LoanSerializer.new(loan), status: 201 + render json: LoanSerializer.new(loan), status: :created #201 else - render json: { error: "Unable to create loan" }, status: 422 + render json: { error: "Unable to create loan" }, status: :unprocessable_entity #422 end end @@ -28,4 +28,16 @@ def update render json: { error: "Unable to update loan status" }, status: 422 end end + + private + + def find_users_and_puzzle + @owner = User.find(params[:user_id]) + @borrower = User.find(params[:borrower_id]) + @puzzle = Puzzle.find(params[:puzzle_id]) + end + + def check_puzzle_status + render json: { error: "Puzzle is not available for loan." }, status: :unprocessable_entity if @puzzle.status != "Available" + end end diff --git a/app/controllers/api/v1/users/puzzles_controller.rb b/app/controllers/api/v1/users/puzzles_controller.rb index 1af4f5c..d2e60ac 100644 --- a/app/controllers/api/v1/users/puzzles_controller.rb +++ b/app/controllers/api/v1/users/puzzles_controller.rb @@ -1,17 +1,21 @@ class Api::V1::Users::PuzzlesController < ApplicationController + before_action :find_user + def index - user = User.find(params[:user_id]) - render json: PuzzleSerializer.new(user.puzzles) + render json: PuzzleSerializer.new(@user.puzzles) end def show - user = User.find(params[:user_id]) - puzzle = user.puzzles.find(params[:puzzle_id]) + puzzle = @user.puzzles.find(params[:puzzle_id]) render json: PuzzleSerializer.new(puzzle) end + def create + puzzle = @user.puzzles.new(puzzle_params) + render json: PuzzleSerializer.new(puzzle), status: 201 if puzzle.save + end + def update - User.find(params[:user_id]) puzzle = Puzzle.find(params[:puzzle_id]) puzzle.update(puzzle_params) @@ -23,4 +27,8 @@ def update def puzzle_params params.permit(:status, :title, :description, :total_pieces, :notes, :puzzle_image_url) # did not include user_id end + + def find_user + @user = User.find(params[:user_id]) + end end diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 5035057..43ee3b7 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -IL4N8M95vrB/esDvIFD5VSPSd4WeSCFyWl8gIS0raJnDgaPfEf0e9t0mWKB5xeI01GrMczu0RACUHhYJezUimYqdsaKxURxQN+7EnvyLHF5OdAa+j4GFE+B/UoTk5hbozyd1zT4eD/D0zytev1cfzlaeI2ZNSS4uBK8oE4rX+G3c7rMY8Gz0wt3S62C6X1zzJMONP21qDmmdFWi5/75KUbNXwYNp4hxCjuV20Y/T512QkyT/DXA6oe47kKYZb8bxcAfax1kf7MAWFssUbjHfmQ5yyHS9/DgUMTiiQrrvoHK9D4JDoLP5WdBjkYvQ90Ei/kJFLk0mkxSXaGZqI8brZP3gjckgBT758XPPafa/ar9YELzLmFbjK+D058Sejpd+DrHYgq+I58pWuReCn1pgrnjR4kuUXVDbFfHp--1S5k6pYYWALfcPgC--Ks7JFZU4UIoPdcpUkX/BHg== \ No newline at end of file +RsfZcGeY28P15m432K6wNTLzYLVZr3nvtd0zYo+mCW7/fmEFz1IKeQgeTvd9NEmPnIRnrDQJ0IGGC6rSLwSCeKbFUBJMsGsbbXLCUw8pbgL0Pm4dS5NPVAEaAlLkzai+DzYbvKA1qglMMi1wYSJlsN+Wh66uj6bMwbkrOey/sEYuNL0HDOay+uH45IUKPRh69HgBFQiFimXRxKxeygb5oU06m3fkqKNkJfKkYz89XIu6OxeToFO5DY/Qx0qohtn+RH2Sd+uet0XHXHos00EP8FAGH+f0/sDr3K1GxaYF5JH0u/M2faaHZkrvebZNsqR4P3QFz/zZWe79XPdFba9Y5hFsykBF3C0m8tfdhWe9ppyRtGXWoW0FmBgxFF+vG2FBi3WicbqmQyDFnOy+fuvk4l9bD9cv8hK9HjMeKazw3ISJoy7EvcjXOmaKWjFMWz3b9QYj8trekH20Z09LlTmWS7bIgSa45Ql+mGOmYqLW82tMI5fy70tXE6mEqYU=--2nrBAjQmfyodtPiG--I0EEp060JLqs5zabzyvndA== \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index cf633a9..abd0ec8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,6 +17,7 @@ get '/users/:user_id/puzzles', to: 'users/puzzles#index' get '/users/:user_id/puzzles/:puzzle_id', to: 'users/puzzles#show' + post '/users/:user_id/puzzles', to: 'users/puzzles#create' patch '/users/:user_id/puzzles/:puzzle_id', to: 'users/puzzles#update' post '/users/:user_id/loans', to: 'users/loans#create' diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 977f6a1..31598a1 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -37,6 +37,8 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods + config.fixture_path = "#{::Rails.root}/spec/fixtures" + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" diff --git a/spec/requests/api/v1/sessions_request_spec.rb b/spec/requests/api/v1/sessions_request_spec.rb index c8da9aa..a5977b4 100644 --- a/spec/requests/api/v1/sessions_request_spec.rb +++ b/spec/requests/api/v1/sessions_request_spec.rb @@ -9,8 +9,8 @@ password: "PuzzleQueen1", password_confirmation: "PuzzleQueen1", email: "dpuzzler@myemail.com", - zip_code: 12_345, - phone_number: 5_550_009_999 + zip_code: 12345, + phone_number: 5500009999 ) login_data = { @@ -22,13 +22,38 @@ post '/api/v1/login', headers:, params: JSON.generate(login_data) expect(response).to have_http_status(201) - - parsed_data = JSON.parse(response.body, symbolize_names: true) + expect(session[:user_id]).to eq(user.id) end end - # context 'when NOT successful' do - # end + context 'when NOT successful' do + it 'cannot log in a user with an incorrect password' do + user = User.create( + full_name: "Diana Puzzler", + password: "PuzzleQueen1", + password_confirmation: "PuzzleQueen1", + email: "dpuzzler@myemail.com", + zip_code: 12_345, + phone_number: 5_550_009_999 + ) + + login_data = { + email: "dpuzzler@myemail.com", + password: "Queen_of_Puzzles" + } + + headers = { 'CONTENT_TYPE' => 'application/json' } + post '/api/v1/login', headers:, params: JSON.generate(login_data) + + expect(response).to have_http_status(401) + + parsed_error_data = JSON.parse(response.body, symbolize_names: true) + + expect(parsed_error_data).to be_a(Hash) + expect(parsed_error_data.keys).to eq([:error]) + expect(parsed_error_data[:error]).to eq("Invalid email or password") + end + end end describe '#destroy' do @@ -41,18 +66,33 @@ post '/api/v1/login', headers:, params: JSON.generate(login_data) expect(response).to have_http_status(201) + expect(session[:user_id]).to eq(user.id) - token = JSON.parse(response.body)['token'] - - delete '/api/v1/logout', headers: { 'Authorization' => "Bearer #{token}" } + delete '/api/v1/logout' expect(response).to have_http_status(204) - expect(session[:user_id]).to be_nil end end - # context 'when NOT successful' do - # end + context 'when NOT successful' do + #Unsure how to test this: + + # it 'cannot delete a user session of another user' do + # user = create(:user) + # login_data = { email: user.email, password: user.password } + + # headers = { 'CONTENT_TYPE' => 'application/json' } + # post '/api/v1/login', headers:, params: JSON.generate(login_data) + + # expect(response).to have_http_status(201) + # expect(session[:user_id]).to eq(user.id) + + # delete '/api/v1/logout' + + # expect(response).to have_http_status(???) + # expect(session[:user_id]).to eq(user.id) + # end + end end end diff --git a/spec/requests/api/v1/users/loans_request_spec.rb b/spec/requests/api/v1/users/loans_request_spec.rb index 487384a..41a272e 100644 --- a/spec/requests/api/v1/users/loans_request_spec.rb +++ b/spec/requests/api/v1/users/loans_request_spec.rb @@ -6,11 +6,16 @@ let(:user_2) { create(:user, id: 2) } let(:puzzle_1) { create(:puzzle, user: user_1) } let(:loan_1) { create(:loan, owner: user_1, borrower: user_2) } + context "when successful" do it 'creates a new loan' do + user_1 = create(:user, id: 1) + user_2 = create(:user, id: 2) + puzzle_1 = create(:puzzle, user: user_1) + post "/api/v1/users/#{user_1.id}/loans", params: { puzzle_id: puzzle_1.id, - borrower_id: loan_1.borrower.id + borrower_id: user_2.id } expect(response).to have_http_status(201) @@ -24,15 +29,20 @@ expect(parsed_data[:data][:attributes]).to be_a(Hash) expect(parsed_data[:data][:attributes].keys).to eq([:owner_id, :borrower_id, :puzzle_id, :status]) - expect(parsed_data[:data][:attributes][:owner_id]).to eq(loan_1.owner_id) - expect(parsed_data[:data][:attributes][:borrower_id]).to eq(loan_1.borrower_id) - expect(parsed_data[:data][:attributes][:puzzle_id]).to eq(loan_1.puzzle_id) - expect(parsed_data[:data][:attributes][:status]).to eq(loan_1.status) + expect(parsed_data[:data][:attributes][:owner_id]).to eq(user_1.id) + expect(parsed_data[:data][:attributes][:borrower_id]).to eq(user_2.id) + expect(parsed_data[:data][:attributes][:puzzle_id]).to eq(puzzle_1.id) + expect(parsed_data[:data][:attributes][:status]).to eq("Pending") end end context "when NOT successful" do - it 'creates an error message' do + it 'returns an error message if trying to make the same loan twice' do + user_1 = create(:user, id: 1) + user_2 = create(:user, id: 2) + puzzle_1 = create(:puzzle, user: user_1) + + create(:loan, owner: user_1, borrower: user_2, puzzle: puzzle_1) allow(Loan).to receive(:new).and_return(double(save: false)) post "/api/v1/users/#{user_1.id}/loans", params: { @@ -48,6 +58,44 @@ expect(parsed_error_data.keys).to eq([:error]) expect(parsed_error_data[:error]).to eq("Unable to create loan") end + + it 'returns an error message if trying to make a loan when a Puzzle status is Pending' do + user_1 = create(:user, id: 1) + user_2 = create(:user, id: 2) + puzzle_1 = create(:puzzle, user: user_1, status: 1) #Puzzle status 1 = "Pending" + + post "/api/v1/users/#{user_1.id}/loans", params: { + puzzle_id: puzzle_1.id, + borrower_id: user_2.id + } + + expect(response).to have_http_status(422) + + parsed_error_data = JSON.parse(response.body, symbolize_names: true) + + expect(parsed_error_data).to be_a(Hash) + expect(parsed_error_data.keys).to eq([:error]) + expect(parsed_error_data[:error]).to eq("Puzzle is not available for loan.") + end + + it 'returns an error message if trying to make a loan when a Puzzle status is Not Available' do + user_1 = create(:user, id: 1) + user_2 = create(:user, id: 2) + puzzle_1 = create(:puzzle, user: user_1, status: 2) #Puzzle status 2 = "Not Available" + + post "/api/v1/users/#{user_1.id}/loans", params: { + puzzle_id: puzzle_1.id, + borrower_id: user_2.id + } + + expect(response).to have_http_status(422) + + parsed_error_data = JSON.parse(response.body, symbolize_names: true) + + expect(parsed_error_data).to be_a(Hash) + expect(parsed_error_data.keys).to eq([:error]) + expect(parsed_error_data[:error]).to eq("Puzzle is not available for loan.") + end end end diff --git a/spec/requests/api/v1/users/puzzles_request_spec.rb b/spec/requests/api/v1/users/puzzles_request_spec.rb index 266cb89..c95474b 100644 --- a/spec/requests/api/v1/users/puzzles_request_spec.rb +++ b/spec/requests/api/v1/users/puzzles_request_spec.rb @@ -150,8 +150,7 @@ title: "Winter Scene", description: "Log Cabin and Bear", total_pieces: 100, - notes: "This puzzle wasn't too difficult. It's fun to do with the whole family!", - puzzle_image_url: "/aws/s3/bucket/for_you.com" + notes: "This puzzle wasn't too difficult. It's fun to do with the whole family!" } headers = { 'CONTENT_TYPE' => 'application/json' } @@ -174,14 +173,13 @@ expect(parsed_data[:data][:attributes][:description]).to eq("Log Cabin and Bear") expect(parsed_data[:data][:attributes][:total_pieces]).to eq(100) expect(parsed_data[:data][:attributes][:notes]).to eq("This puzzle wasn't too difficult. It's fun to do with the whole family!") - expect(parsed_data[:data][:attributes][:puzzle_image_url]).to eq("/aws/s3/bucket/for_you.com") + expect(parsed_data[:data][:attributes][:puzzle_image_url]).to eq(@puzzle_2.puzzle_image_url) expect(parsed_data[:data][:attributes][:status]).to_not eq(@puzzle_2.status) # puzzle default enum status 0 = "Available" expect(parsed_data[:data][:attributes][:title]).to_not eq(@puzzle_2.title) expect(parsed_data[:data][:attributes][:description]).to_not eq(@puzzle_2.description) expect(parsed_data[:data][:attributes][:total_pieces]).to_not eq(@puzzle_2.total_pieces) expect(parsed_data[:data][:attributes][:notes]).to_not eq(@puzzle_2.notes) - expect(parsed_data[:data][:attributes][:puzzle_image_url]).to_not eq(@puzzle_2.puzzle_image_url) end end @@ -241,4 +239,47 @@ # REFACTOR: We could add a test to limit the integers allowed for for total_pieces. Ex: [260, 500, 1000, 1500, 2000, 3000] only? end end + + describe '#create' do + before(:each) do + @user_1 = create(:user, id: 1) + end + + context "when successful" do + it "can create a new puzzle object" do + new_puzzle_info = { + title: "Winter Scene", + description: "Log Cabin and Bear", + total_pieces: 1000, + notes: "This puzzle wasn't too difficult. It's fun to do with the whole family!", + puzzle_image_url: "http://res.cloudinary.com/dwcorjdyo/image/upload/pretend_image.jpg" + } + + headers = { 'CONTENT_TYPE' => 'application/json' } + post "/api/v1/users/#{@user_1.id}/puzzles", headers:, params: JSON.generate(new_puzzle_info) + + expect(response).to have_http_status(201) + + parsed_data = JSON.parse(response.body, symbolize_names: true) + + expect(parsed_data).to be_a(Hash) + expect(parsed_data.keys).to eq([:data]) + expect(parsed_data[:data]).to be_a(Hash) + expect(parsed_data[:data].keys).to eq([:id, :type, :attributes]) + + expect(parsed_data[:data][:attributes]).to be_a(Hash) + expect(parsed_data[:data][:attributes].keys).to eq([:user_id, :status, :title, :description, :total_pieces, :notes, :puzzle_image_url]) + expect(parsed_data[:data][:attributes][:user_id]).to eq(@user_1.id) + expect(parsed_data[:data][:attributes][:status]).to eq("Available") # enums digit is transformed into string + expect(parsed_data[:data][:attributes][:title]).to eq("Winter Scene") + expect(parsed_data[:data][:attributes][:description]).to eq("Log Cabin and Bear") + expect(parsed_data[:data][:attributes][:total_pieces]).to eq(1000) + expect(parsed_data[:data][:attributes][:notes]).to eq("This puzzle wasn't too difficult. It's fun to do with the whole family!") + expect(parsed_data[:data][:attributes][:puzzle_image_url]).to eq("http://res.cloudinary.com/dwcorjdyo/image/upload/pretend_image.jpg") + end + end + + # context "when NOT successful" do + # end + end end