diff --git a/.gitignore b/.gitignore index 48fb168f..3fb9bf3f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,18 @@ !/log/.keep !/tmp/.keep +# Ignore uploaded files in development +/storage/* +!/storage/.keep + +/node_modules +/yarn-error.log + +/public/assets +.byebug_history +/coverage +.DS_Store +.env + # Ignore Byebug command history file. .byebug_history diff --git a/Gemfile b/Gemfile index 42f4bb2c..13a71345 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,10 @@ gem 'coffee-rails', '~> 4.2' # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'therubyracer', platforms: :ruby +gem 'omniauth' +gem 'omniauth-github' +gem 'omniauth-google-oauth2' + # Use jquery as the JavaScript library gem 'jquery-rails' # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks @@ -62,6 +66,7 @@ group :development do # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' + gem 'dotenv-rails' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/Gemfile.lock b/Gemfile.lock index 5b407e7e..9018588e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,11 +70,18 @@ GEM concurrent-ruby (1.0.5) crass (1.0.4) debug_inspector (0.0.3) + dotenv (2.5.0) + dotenv-rails (2.5.0) + dotenv (= 2.5.0) + railties (>= 3.2, < 6.0) erubi (1.7.1) execjs (2.7.0) + faraday (0.15.3) + multipart-post (>= 1.2, < 3) ffi (1.9.25) globalid (0.4.1) activesupport (>= 4.2.0) + hashie (3.5.7) i18n (1.1.0) concurrent-ruby (~> 1.0) jbuilder (2.7.0) @@ -84,6 +91,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + jwt (2.1.0) listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -113,9 +121,30 @@ GEM minitest (~> 5.0) rails (>= 4.1) multi_json (1.13.1) + multi_xml (0.6.0) + multipart-post (2.0.0) nio4r (2.3.1) nokogiri (1.8.4) mini_portile2 (~> 2.3.0) + oauth2 (1.4.1) + faraday (>= 0.8, < 0.16.0) + jwt (>= 1.0, < 3.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + omniauth (1.8.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-github (1.3.0) + omniauth (~> 1.5) + omniauth-oauth2 (>= 1.4.0, < 2.0) + omniauth-google-oauth2 (0.5.3) + jwt (>= 1.5) + omniauth (>= 1.1.1) + omniauth-oauth2 (>= 1.5) + omniauth-oauth2 (1.5.0) + oauth2 (~> 1.1) + omniauth (~> 1.2) pg (0.21.0) popper_js (1.14.3) pry (0.11.3) @@ -207,6 +236,7 @@ DEPENDENCIES bootstrap (~> 4.1.3) byebug coffee-rails (~> 4.2) + dotenv-rails jbuilder (~> 2.5) jquery-rails listen (~> 3.0.5) @@ -214,6 +244,9 @@ DEPENDENCIES minitest-reporters minitest-skip minitest-spec-rails + omniauth + omniauth-github + omniauth-google-oauth2 pg (~> 0.18) pry-rails puma (~> 3.0) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 5f08f884..f841c842 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -85,7 +85,7 @@ a:hover { } .app-header__user-nav-container .nav-item { - margin-left: 2rem; + margin-left: 1rem; } .list-group-item { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c12c7c17..2289b2c0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,13 +1,21 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception - before_action :find_user + before_action :find_user, :require_login def render_404 # DPR: this will actually render a 404 page in production raise ActionController::RoutingError.new('Not Found') end + def require_login + unless find_user + flash[:status] = :failure + flash[:result_text] = "You must be logged in to view this section" + redirect_to root_path + end + end + private def find_user if session[:user_id] diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 5bce99e6..9a9b73da 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,34 +1,38 @@ class SessionsController < ApplicationController - def login_form - end + skip_before_action :require_login, only: [:create] + + def create + auth_hash = request.env['omniauth.auth'] + + user = User.find_by(uid: auth_hash[:uid], provider: auth_hash[:provider]) - def login - username = params[:username] - if username and user = User.find_by(username: username) + if user session[:user_id] = user.id flash[:status] = :success flash[:result_text] = "Successfully logged in as existing user #{user.username}" else - user = User.new(username: username) + user = User.build_from_github(auth_hash) if auth_hash[:provider] == 'github' + user = User.build_from_google(auth_hash) if auth_hash[:provider] == 'google_oauth2' + if user.save session[:user_id] = user.id flash[:status] = :success - flash[:result_text] = "Successfully created new user #{user.username} with ID #{user.id}" + flash[:result_text] = "Successfully created new user #{user.username}" else flash.now[:status] = :failure flash.now[:result_text] = "Could not log in" flash.now[:messages] = user.errors.messages - render "login_form", status: :bad_request - return end end + redirect_to root_path end - def logout + def destroy session[:user_id] = nil flash[:status] = :success flash[:result_text] = "Successfully logged out" + redirect_to root_path end end diff --git a/app/controllers/works_controller.rb b/app/controllers/works_controller.rb index 2020bee4..b6685bb7 100644 --- a/app/controllers/works_controller.rb +++ b/app/controllers/works_controller.rb @@ -2,6 +2,8 @@ class WorksController < ApplicationController # We should always be able to tell what category # of work we're dealing with before_action :category_from_work, except: [:root, :index, :new, :create] + before_action :check_owner, only: [:edit, :update, :destroy] + skip_before_action :require_login, only: [:root] def root @albums = Work.best_albums @@ -50,7 +52,7 @@ def update flash.now[:status] = :failure flash.now[:result_text] = "Could not update #{@media_category.singularize}" flash.now[:messages] = @work.errors.messages - render :edit, status: :not_found + render :edit, status: :bad_request end end @@ -74,6 +76,7 @@ def upvote end else flash[:result_text] = "You must log in to do that" + redirect_to root_path end # Refresh the page to show either the updated vote count @@ -83,7 +86,7 @@ def upvote private def media_params - params.require(:work).permit(:title, :category, :creator, :description, :publication_year) + params.require(:work).permit(:title, :category, :creator, :description, :publication_year, :user_id) end def category_from_work @@ -91,4 +94,12 @@ def category_from_work render_404 unless @work @media_category = @work.category.downcase.pluralize end + + def check_owner + unless @work.user.id == @login_user.id + flash[:status] = :failure + flash[:result_text] = "You can only edit works you added to the site" + redirect_to works_path + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 4cac8fe0..4172bba5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,31 @@ class User < ApplicationRecord - has_many :votes + has_many :votes, dependent: :destroy + has_many :works, dependent: :destroy has_many :ranked_works, through: :votes, source: :work validates :username, uniqueness: true, presence: true + validates :uid, presence: true + validates :provider, presence: true + + def self.build_from_github(auth_hash) + user = User.new + + user.provider = 'github' + user.uid = auth_hash[:uid] + user.username = auth_hash[:info][:nickname] + user.name = auth_hash[:info][:name] + + return user + end + + def self.build_from_google(auth_hash) + user = User.new + + user.provider = 'google_oauth2' + user.uid = auth_hash[:uid] + user.username = auth_hash[:info][:email] + user.name = auth_hash[:info][:name] + + return user + end end diff --git a/app/models/work.rb b/app/models/work.rb index c61da303..04780ea0 100644 --- a/app/models/work.rb +++ b/app/models/work.rb @@ -1,5 +1,6 @@ class Work < ApplicationRecord CATEGORIES = %w(album book movie) + belongs_to :user has_many :votes, dependent: :destroy has_many :ranking_users, through: :votes, source: :user diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e7b07ce4..b6477f0c 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -36,16 +36,19 @@ <% if @login_user %> <% else %> + <% end %> diff --git a/app/views/works/_form.html.erb b/app/views/works/_form.html.erb index 8debadb9..a39c5b9a 100644 --- a/app/views/works/_form.html.erb +++ b/app/views/works/_form.html.erb @@ -25,6 +25,8 @@ <%= f.text_area :description, class: "form-control" %> + <%= f.hidden_field :user_id, value: @login_user.id %> +
<%= f.submit class: "btn btn-primary" %>
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 00000000..252a89b4 --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,7 @@ +Rails.application.config.middleware.use OmniAuth::Builder do + provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], scope: "user:email" +end + +Rails.application.config.middleware.use OmniAuth::Builder do + provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'], { scope: 'userinfo.email, userinfo.profile', redirect_uri: 'http://localhost:3000/auth/google_oauth2/callback' } +end diff --git a/config/routes.rb b/config/routes.rb index a7e8af1d..c994ea09 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,9 +1,14 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html root 'works#root' - get '/login', to: 'sessions#login_form', as: 'login' - post '/login', to: 'sessions#login' - post '/logout', to: 'sessions#logout', as: 'logout' + + # ROUTES PRIOR TO OAUTH USE: + # get '/login', to: 'sessions#login_form', as: 'login' + # post '/login', to: 'sessions#login' + # post '/logout', to: 'sessions#logout', as: 'logout' + + get '/auth/:provider/callback', to: 'sessions#create', as: 'auth_callback' + delete '/logout', to: 'sessions#destroy', as: 'logout' resources :works post '/works/:id/upvote', to: 'works#upvote', as: 'upvote' diff --git a/db/media_seeds.csv b/db/media_seeds.csv index 5f5b252a..104ae273 100644 --- a/db/media_seeds.csv +++ b/db/media_seeds.csv @@ -1,25 +1,25 @@ -category,title,creator,publication_year,description -album,Blue Breaker,Dr. Sarai Langosh,1949,Et et expedita non aut quo. -book,Joe Treat,Blaise Lesch,1968,Voluptatem adipisci qui velit. -album,Kreb-Full-o Been,Ms. Trevion Buckridge,2016,Vero consectetur delectus consequatur id aut accusantium unde excepturi. -album,Wake-up Pie,Timmy Streich I,1919,Voluptatem consequatur qui consectetur nisi officiis culpa. -album,Major Cup,Jayde Bartoletti,1944,Quis recusandae cum est facere consequatur minima magni et. -book,Summer Select,Ms. Gwendolyn Ortiz,1946,Et molestiae eos nam odit aut sed. -album,Holiday Choice,Alexandria Lehner,1940,Excepturi voluptas ut voluptatum. -book,Postmodern Blend,Meredith Brekke,1970,Dolorem fugit accusantium qui. -book,Green Forrester,Raquel Hirthe,1933,Omnis qui quia odio. -album,Winter Mug,Tia Weissnat II,1990,Laboriosam autem iusto quae sed voluptate et. -book,Red Pie,Davon Kub,1961,Id dolorem qui laborum quia. -album,Major Equinox,Queen Satterfield,1997,Fugit perferendis est quam sunt porro vel rerum. -book,Melty Breaker,Montana Dickinson Sr.,1991,Perferendis harum fuga corporis. -book,Winter Pie,Mr. Syble Kuhn,1970,Incidunt molestias deserunt laudantium. -album,Goodbye Utopia,Orion Spencer,1962,Praesentium enim pariatur voluptatem sed quod dolorum. -album,Green Select,Berneice Jenkins,1957,Hic repudiandae molestiae id nulla aliquid maiores necessitatibus. -book,Blacktop Enlightenment,Seamus D'Amore,1928,Ea id cumque et pariatur magni nemo dolorem. -album,Express Extract,Dorothy Jast I,1969,Dolores dolorum aut ea aperiam et voluptatem. -album,Winter Been,Mackenzie Wilkinson,1932,Culpa repudiandae et at sint et amet fugiat et. -book,Heart Mug,Orpha Douglas,2009,Qui voluptas alias quia. -album,Blue Treat,Eliseo Gorczany,1979,Sit est quis veniam saepe. -book,Hello Town,Laury Walter,2005,Est sed ut asperiores sed fugiat. -album,Blacktop Choice,Casey Feil,2008,Temporibus ex maxime labore quam et natus quia ipsum. -book,Huggy Star,Nigel Lesch DVM,1962,Voluptatem ea aspernatur nesciunt ipsa quis error corporis placeat. +category,title,creator,publication_year,description,user_id +album,Blue Breaker,Dr. Sarai Langosh,1949,Et et expedita non aut quo.,1 +book,Joe Treat,Blaise Lesch,1968,Voluptatem adipisci qui velit.,1 +album,Kreb-Full-o Been,Ms. Trevion Buckridge,2016,Vero consectetur delectus consequatur id aut accusantium unde excepturi.,1 +album,Wake-up Pie,Timmy Streich I,1919,Voluptatem consequatur qui consectetur nisi officiis culpa.,1 +album,Major Cup,Jayde Bartoletti,1944,Quis recusandae cum est facere consequatur minima magni et.,1 +book,Summer Select,Ms. Gwendolyn Ortiz,1946,Et molestiae eos nam odit aut sed.,1 +album,Holiday Choice,Alexandria Lehner,1940,Excepturi voluptas ut voluptatum.,1 +book,Postmodern Blend,Meredith Brekke,1970,Dolorem fugit accusantium qui.,1 +book,Green Forrester,Raquel Hirthe,1933,Omnis qui quia odio.,1 +album,Winter Mug,Tia Weissnat II,1990,Laboriosam autem iusto quae sed voluptate et.,1 +book,Red Pie,Davon Kub,1961,Id dolorem qui laborum quia.,1 +album,Major Equinox,Queen Satterfield,1997,Fugit perferendis est quam sunt porro vel rerum.,1 +book,Melty Breaker,Montana Dickinson Sr.,1991,Perferendis harum fuga corporis.,1 +book,Winter Pie,Mr. Syble Kuhn,1970,Incidunt molestias deserunt laudantium.,1 +album,Goodbye Utopia,Orion Spencer,1962,Praesentium enim pariatur voluptatem sed quod dolorum.,1 +album,Green Select,Berneice Jenkins,1957,Hic repudiandae molestiae id nulla aliquid maiores necessitatibus.,1 +book,Blacktop Enlightenment,Seamus D'Amore,1928,Ea id cumque et pariatur magni nemo dolorem.,1 +album,Express Extract,Dorothy Jast I,1969,Dolores dolorum aut ea aperiam et voluptatem.,1 +album,Winter Been,Mackenzie Wilkinson,1932,Culpa repudiandae et at sint et amet fugiat et.,1 +book,Heart Mug,Orpha Douglas,2009,Qui voluptas alias quia.,1 +album,Blue Treat,Eliseo Gorczany,1979,Sit est quis veniam saepe.,1 +book,Hello Town,Laury Walter,2005,Est sed ut asperiores sed fugiat.,1 +album,Blacktop Choice,Casey Feil,2008,Temporibus ex maxime labore quam et natus quia ipsum.,1 +book,Huggy Star,Nigel Lesch DVM,1962,Voluptatem ea aspernatur nesciunt ipsa quis error corporis placeat.,1 diff --git a/db/migrate/20181016211249_add_oauth_to_user.rb b/db/migrate/20181016211249_add_oauth_to_user.rb new file mode 100644 index 00000000..016ee0ef --- /dev/null +++ b/db/migrate/20181016211249_add_oauth_to_user.rb @@ -0,0 +1,7 @@ +class AddOauthToUser < ActiveRecord::Migration[5.2] + def change + add_column :users, :uid, :integer, null: false + add_column :users, :provider, :string, null: false + add_column :users, :name, :string + end +end diff --git a/db/migrate/20181029221607_add_user_reference_to_work.rb b/db/migrate/20181029221607_add_user_reference_to_work.rb new file mode 100644 index 00000000..52b7ba5b --- /dev/null +++ b/db/migrate/20181029221607_add_user_reference_to_work.rb @@ -0,0 +1,5 @@ +class AddUserReferenceToWork < ActiveRecord::Migration[5.2] + def change + add_reference :works, :user, foreign_key: true + end +end diff --git a/db/migrate/20181030044147_change_uid_to_bigint.rb b/db/migrate/20181030044147_change_uid_to_bigint.rb new file mode 100644 index 00000000..60e88f1a --- /dev/null +++ b/db/migrate/20181030044147_change_uid_to_bigint.rb @@ -0,0 +1,5 @@ +class ChangeUidToBigint < ActiveRecord::Migration[5.2] + def change + change_column :users, :uid, :bigint + end +end diff --git a/db/migrate/20181030044631_change_integer_limit_for_uid.rb b/db/migrate/20181030044631_change_integer_limit_for_uid.rb new file mode 100644 index 00000000..1098cfdb --- /dev/null +++ b/db/migrate/20181030044631_change_integer_limit_for_uid.rb @@ -0,0 +1,5 @@ +class ChangeIntegerLimitForUid < ActiveRecord::Migration[5.2] + def change + change_column :users, :uid, :integer, limit: 8 + end +end diff --git a/db/migrate/20181030045718_change_uid_to_string.rb b/db/migrate/20181030045718_change_uid_to_string.rb new file mode 100644 index 00000000..87cac40e --- /dev/null +++ b/db/migrate/20181030045718_change_uid_to_string.rb @@ -0,0 +1,5 @@ +class ChangeUidToString < ActiveRecord::Migration[5.2] + def change + change_column :users, :uid, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 6bc8ba5c..c9fb6d62 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,37 +10,43 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170407164321) do +ActiveRecord::Schema.define(version: 2018_10_30_045718) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" - create_table "users", force: :cascade do |t| - t.string "username" + create_table "users", id: :serial, force: :cascade do |t| + t.string "username" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "uid", null: false + t.string "provider", null: false + t.string "name" end - create_table "votes", force: :cascade do |t| - t.integer "user_id" - t.integer "work_id" + create_table "votes", id: :serial, force: :cascade do |t| + t.integer "user_id" + t.integer "work_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["user_id"], name: "index_votes_on_user_id", using: :btree - t.index ["work_id"], name: "index_votes_on_work_id", using: :btree + t.index ["user_id"], name: "index_votes_on_user_id" + t.index ["work_id"], name: "index_votes_on_work_id" end - create_table "works", force: :cascade do |t| - t.string "title" - t.string "creator" - t.string "description" - t.string "category" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "vote_count", default: 0 - t.integer "publication_year" + create_table "works", id: :serial, force: :cascade do |t| + t.string "title" + t.string "creator" + t.string "description" + t.string "category" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "vote_count", default: 0 + t.integer "publication_year" + t.bigint "user_id" + t.index ["user_id"], name: "index_works_on_user_id" end add_foreign_key "votes", "users" add_foreign_key "votes", "works" + add_foreign_key "works", "users" end diff --git a/db/seeds.rb b/db/seeds.rb index 75cb6480..0385518e 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,6 +1,8 @@ require 'csv' media_file = Rails.root.join('db', 'media_seeds.csv') +User.create(provider: "github", uid: 99999, username: "seed_user", name: "Seed Person") + CSV.foreach(media_file, headers: true, header_converters: :symbol, converters: :all) do |row| data = Hash[row.headers.zip(row.fields)] puts data diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index f641d15c..1016fdac 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -1,5 +1,160 @@ require "test_helper" describe SessionsController do + let (:dan) { users(:dan) } + describe "create with auth_callback" do + it "logs in an existing user and redirects to the root route" do + expect { + perform_login(dan) + }.wont_change 'User.count' + + expect(session[:user_id]).must_equal dan.id + + must_redirect_to root_path + end + + it "logs in an existing google user and redirects to the root route" do + expect { + perform_google_login(users(:google_user)) + }.wont_change 'User.count' + + expect(session[:user_id]).must_equal users(:google_user).id + + must_redirect_to root_path + end + + it "creates an account for a new user and redirects to the root route" do + user = User.new(provider: "github", uid: 400400, username: "test_user", name: "Test Person") + + expect { + perform_login(user) + }.must_change 'User.count', 1 + + new_user = User.find_by(username: user.username) + + expect(new_user).wont_be_nil + expect(session[:user_id]).must_equal new_user.id + + must_redirect_to root_path + + expect(flash[:result_text]).must_equal "Successfully created new user #{new_user.username}" + end + + it "redirects to the login route if given invalid user data" do + user = User.new(provider: "github", uid: "99999", username: nil, name: "Test Person") + + expect { + perform_login(user) + }.wont_change 'User.count' + + invalid_user = User.find_by(username: user.username) + + expect(invalid_user).must_be_nil + expect(session[:user_id]).must_be_nil + + expect(flash[:result_text]).must_equal "Could not log in" + + must_redirect_to root_path + end + end + + describe "destroy" do + it "logs a user out if they are logged in" do + perform_login(dan) + expect(session[:user_id]).must_equal dan.id + + expect { + delete logout_path + }.wont_change 'User.count' + + expect(session[:user_id]).must_be_nil + + must_respond_with :redirect + must_redirect_to root_path + end + + it "redirects to the homepage even if no one is logged in" do + expect { + delete logout_path + }.wont_change 'User.count' + + expect(session[:user_id]).must_be_nil + + must_respond_with :redirect + must_redirect_to root_path + end + end + + # THESE TESTS NO LONGER NEEDED AFTER OAUTH + + # let (:dan) { users(:dan) } + # + # it "login form" do + # get login_path + # + # must_respond_with :success + # end + # + # describe "login action" do + # it "can create a new user" do + # user_hash = { + # username: 'jackie' + # } + # + # expect { + # post login_path, params: user_hash + # }.must_change 'User.count', 1 + # + # must_respond_with :redirect + # must_redirect_to root_path + # + # new_user = User.find_by(username: user_hash[:username]) + # + # expect(new_user).wont_be_nil + # expect(session[:user_id]).must_equal new_user.id + # + # expect(flash[:result_text]).must_equal "Successfully created new user #{new_user.username} with ID #{new_user.id}" + # end + # + # it "should log in an existing user without changing anything" do + # user_hash = { + # username: 'dan' + # } + # + # expect { + # post login_path, params: user_hash + # }.wont_change 'User.count' + # + # must_respond_with :redirect + # must_redirect_to root_path + # + # user = User.find_by(username: user_hash[:username]) + # + # expect(user).wont_be_nil + # expect(session[:user_id]).must_equal user.id + # expect(user.id).must_equal dan.id + # + # expect(flash[:result_text]).must_equal "Successfully logged in as existing user #{dan.username}" + # end + # + # it "should give a bad_request for an invalid username" do + # user_hash = { + # username: nil + # } + # + # expect { + # post login_path, params: user_hash + # }.wont_change 'User.count' + # + # must_respond_with :bad_request + # + # new_user = User.find_by(username: user_hash[:username]) + # + # expect(new_user).must_be_nil + # expect(session[:user_id]).must_be_nil + # + # expect(flash[:result_text]).must_equal "Could not log in" + # end + # end end diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index d2c5cfbb..344ef4ef 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -1,5 +1,73 @@ require 'test_helper' describe UsersController do + let (:dan) { users(:dan) } + describe "logged in users" do + describe "index" do + it "succeeds when there are users" do + perform_login(dan) + get users_path + + must_respond_with :success + end + + it "succeeds when there are no users" do + User.all.each do |user| + user.votes.each do |vote| + vote.destroy + end + user.works.each do |work| + work.destroy + end + user.destroy + end + + expect(User.count).must_equal 0 + + user = User.new(provider: "github", uid: "400400", username: "test_user", name: "Test Person") + + perform_login(user) + get users_path + + must_respond_with :success + end + end + + describe "show" do + it "succeeds for an extant user ID" do + perform_login(dan) + get user_path(users(:kari).id) + + must_respond_with :success + end + + it "renders 404 not_found for a bogus user ID" do + perform_login(dan) + get user_path(-1) + + must_respond_with :not_found + end + end + end + + describe "guest users" do + describe "index" do + it "cannot access user index" do + get users_path + + must_respond_with :redirect + must_redirect_to root_path + end + end + + describe "show" do + it "cannot access user show" do + get user_path(dan.id) + + must_respond_with :redirect + must_redirect_to root_path + end + end + end end diff --git a/test/controllers/works_controller_test.rb b/test/controllers/works_controller_test.rb index 0945ca47..de3f9b4d 100644 --- a/test/controllers/works_controller_test.rb +++ b/test/controllers/works_controller_test.rb @@ -1,116 +1,444 @@ require 'test_helper' describe WorksController do + let (:poodr) { works(:poodr) } + let (:movie) { works(:movie) } + let (:dan) { users(:dan) } + let (:work_hash) { + { + work: { + title: "Return of the King", + creator: "Tolkien", + description: "Lord of the Rings", + publication_year: 1955, + category: "book", + user_id: dan.id + } + } + } + + CATEGORIES = %w(albums books movies) + INVALID_CATEGORIES = ["nope", "42", "", " ", "albumstrailingtext"] + describe "root" do it "succeeds with all media types" do # Precondition: there is at least one media of each category + get root_path + + must_respond_with :success end it "succeeds with one media type absent" do # Precondition: there is at least one media in two of the categories + expect { + perform_login(users(:kari)) + delete work_path(movie.id) + }.must_change 'Work.count', -1 + + get root_path + + must_respond_with :success end it "succeeds with no media" do + Work.all.each do |work| + work.destroy + end + + expect(Work.count).must_equal 0 + get root_path + + must_respond_with :success end end - CATEGORIES = %w(albums books movies) - INVALID_CATEGORIES = ["nope", "42", "", " ", "albumstrailingtext"] + describe "logged in users" do + describe "index" do + it "succeeds when there are works" do + perform_login(dan) + get works_path - describe "index" do - it "succeeds when there are works" do + must_respond_with :success + end - end + it "succeeds when there are no works" do + Work.all.each do |work| + work.destroy + end - it "succeeds when there are no works" do + expect(Work.count).must_equal 0 + perform_login(dan) + get works_path + + must_respond_with :success + end end - end - describe "new" do - it "succeeds" do + describe "new" do + it "succeeds" do + perform_login(dan) + get new_work_path + must_respond_with :success + end end - end - - describe "create" do - it "creates a work with valid data for a real category" do + describe "create" do + it "creates a work with valid data for a real category" do + expect { + perform_login(dan) + post works_path, params: work_hash + }.must_change 'Work.count', 1 + + must_respond_with :redirect + must_redirect_to work_path(Work.last.id) + + expect(Work.last.title).must_equal work_hash[:work][:title] + expect(Work.last.creator).must_equal work_hash[:work][:creator] + expect(Work.last.description).must_equal work_hash[:work][:description] + expect(Work.last.publication_year).must_equal work_hash[:work][:publication_year] + expect(Work.last.category).must_equal work_hash[:work][:category] + expect(Work.last.user_id).must_equal dan.id + + expect(flash[:result_text]).must_equal "Successfully created #{Work.last.category} #{Work.last.id}" + end + + it "renders bad_request and does not update the DB for bogus data" do + work_hash[:work][:title] = nil + + expect { + perform_login(dan) + post works_path, params: work_hash + }.wont_change 'Work.count' + + must_respond_with :bad_request + expect(flash[:result_text]).must_equal "Could not create #{work_hash[:work][:category]}" + end + + it "renders bad_request for bogus categories" do + work_hash[:work][:category] = "spoken word" + + expect { + perform_login(dan) + post works_path, params: work_hash + }.wont_change 'Work.count' + + must_respond_with :bad_request + expect(flash[:result_text]).must_equal "Could not create #{work_hash[:work][:category]}" + end end - it "renders bad_request and does not update the DB for bogus data" do + describe "show" do + it "succeeds for an extant work ID" do + perform_login(dan) + get work_path(poodr.id) - end + must_respond_with :success + end - it "renders 400 bad_request for bogus categories" do + it "renders 404 not_found for a bogus work ID" do + id = -1 + perform_login(dan) + get work_path(id) + + must_respond_with :not_found + end end - end + describe "edit" do + it "succeeds for an extant work ID and a work the user created" do + perform_login(dan) + get edit_work_path(poodr.id) - describe "show" do - it "succeeds for an extant work ID" do + must_respond_with :success + end - end + it "renders 404 not_found for a bogus work ID" do + id = -1 - it "renders 404 not_found for a bogus work ID" do + perform_login(dan) + get work_path(id) - end - end + # Arrange + must_respond_with :not_found + end - describe "edit" do - it "succeeds for an extant work ID" do + it "does not permit access if logged in user is not the user the work belongs to" do + perform_login(dan) + get edit_work_path(movie.id) + # Arrange + must_respond_with :redirect + must_redirect_to works_path + expect(flash[:result_text]).must_equal "You can only edit works you added to the site" + end end - it "renders 404 not_found for a bogus work ID" do + describe "update" do + it "succeeds for valid data and an extant work ID and a work the user created" do + work_hash[:work][:description] = "a new description" - end - end + expect { + perform_login(dan) + patch work_path(poodr.id), params: work_hash + }.wont_change 'Work.count' - describe "update" do - it "succeeds for valid data and an extant work ID" do + work = Work.find_by(id: poodr.id) - end + must_respond_with :redirect + must_redirect_to work_path(work.id) + + expect(work.title).must_equal work_hash[:work][:title] + expect(work.creator).must_equal work_hash[:work][:creator] + expect(work.description).must_equal work_hash[:work][:description] + expect(work.publication_year).must_equal work_hash[:work][:publication_year] + expect(work.category).must_equal work_hash[:work][:category] + + expect(flash[:result_text]).must_equal "Successfully updated #{work.category} #{work.id}" + end + + it "renders bad_request for bogus data" do + work_hash[:work][:title] = nil + + work = Work.find_by(id: poodr.id) - it "renders bad_request for bogus data" do + expect { + perform_login(dan) + patch work_path(work.id), params: work_hash + }.wont_change 'Work.count' + must_respond_with :bad_request + expect(flash[:result_text]).must_equal "Could not update #{work_hash[:work][:category]}" + + # expect no change + expect(work.title).must_equal poodr.title + expect(work.creator).must_equal poodr.creator + expect(work.description).must_equal poodr.description + expect(work.publication_year).must_equal poodr.publication_year + expect(work.category).must_equal poodr.category + end + + it "renders 404 not_found for a bogus work ID" do + work_hash[:work][:description] = "won't work" + + id = -1 + + expect { + perform_login(dan) + patch work_path(id), params: work_hash + }.wont_change 'Work.count' + + must_respond_with :not_found + end + + it "does not update work if logged in user is not the user the work belongs to" do + perform_login(dan) + patch work_path(movie.id), params: work_hash + + must_respond_with :redirect + must_redirect_to works_path + expect(flash[:result_text]).must_equal "You can only edit works you added to the site" + + # expect no change + work = Work.find_by(id: movie.id) + + expect(work.title).must_equal movie.title + expect(work.creator).must_equal movie.creator + expect(work.description).must_equal movie.description + expect(work.publication_year).must_equal movie.publication_year + expect(work.category).must_equal movie.category + end + end + + describe "destroy" do + it "succeeds for an extant work ID" do + expect { + perform_login(dan) + delete work_path(poodr.id) + }.must_change 'Work.count', -1 + + must_respond_with :redirect + expect(flash[:result_text]).must_equal "Successfully destroyed #{poodr.category} #{poodr.id}" + expect(Work.find_by(id: poodr.id)).must_be_nil + end + + it "renders 404 not_found and does not update the DB for a bogus work ID" do + id = -1 + + expect { + perform_login(dan) + delete work_path(id) + }.wont_change 'Work.count' + + must_respond_with :not_found + end + + it "does not delete work if logged in user is not the user the work belongs to" do + expect { + perform_login(dan) + delete work_path(movie.id), params: work_hash + }.wont_change 'Work.count' + + must_respond_with :redirect + must_redirect_to works_path + expect(flash[:result_text]).must_equal "You can only edit works you added to the site" + + # expect no change + expect(Work.find_by(id: movie.id).title).must_equal movie.title + expect(Work.find_by(id: movie.id).creator).must_equal movie.creator + expect(Work.find_by(id: movie.id).description).must_equal movie.description + expect(Work.find_by(id: movie.id).publication_year).must_equal movie.publication_year + expect(Work.find_by(id: movie.id).category).must_equal movie.category + end end - it "renders 404 not_found for a bogus work ID" do + describe "upvote" do + it "redirects to the homepage after the user has logged out" do + perform_login(dan) + + expect(session[:user_id]).must_equal dan.id + + delete logout_path + + expect(session[:user_id]).must_be_nil + + expect{ + post upvote_path(poodr.id) + }.wont_change 'dan.votes.count' + must_respond_with :redirect + must_redirect_to root_path + end + + it "succeeds for a logged-in user and a fresh user-vote pair" do + perform_login(dan) + + user = User.find_by(username: "dan") + + expect{ + post upvote_path(poodr.id) + }.must_change 'user.votes.count', 1 + + must_respond_with :redirect + must_redirect_to work_path(poodr.id) + end + + it "redirects to the work page if the user has already voted for that work" do + perform_login(dan) + + expect{ + post upvote_path(works(:album).id) + }.wont_change 'dan.votes.count' + + must_respond_with :redirect + must_redirect_to work_path(works(:album).id) + end end end - describe "destroy" do - it "succeeds for an extant work ID" do + describe "guest users" do + describe "index" do + it "cannot access index" do + get works_path + must_respond_with :redirect + must_redirect_to root_path + expect(flash[:result_text]).must_equal "You must be logged in to view this section" + end end - it "renders 404 not_found and does not update the DB for a bogus work ID" do + describe "show" do + it "cannot access show" do + get work_path(Work.first.id) + must_respond_with :redirect + must_redirect_to root_path + expect(flash[:result_text]).must_equal "You must be logged in to view this section" + end end - end - describe "upvote" do + describe "new" do + it "cannot access new" do + get new_work_path(poodr.id) + + must_respond_with :redirect + must_redirect_to root_path + expect(flash[:result_text]).must_equal "You must be logged in to view this section" + end + end - it "redirects to the work page if no user is logged in" do + describe "create" do + it "cannot access create" do + expect{ + post works_path, params: work_hash + }.wont_change 'Work.count' + must_respond_with :redirect + must_redirect_to root_path + expect(flash[:result_text]).must_equal "You must be logged in to view this section" + end end - it "redirects to the work page after the user has logged out" do + describe "edit" do + it "cannot access edit" do + get edit_work_path(poodr.id) + must_respond_with :redirect + must_redirect_to root_path + expect(flash[:result_text]).must_equal "You must be logged in to view this section" + end end - it "succeeds for a logged-in user and a fresh user-vote pair" do + describe "update" do + it "cannot access update" do + patch work_path(poodr.id), params: work_hash + + must_respond_with :redirect + must_redirect_to root_path + expect(flash[:result_text]).must_equal "You must be logged in to view this section" + + work = Work.find_by(id: poodr.id) + + expect(work.title).must_equal "Practical Object Oriented Design in Ruby" + expect(work.creator).must_equal "Sandi Metz" + expect(work.description).must_equal "programming!" + expect(work.publication_year).must_equal 2012 + expect(work.category).must_equal "book" + end + end + describe "destroy" do + it "cannot access destroy" do + expect{ + delete work_path(poodr.id) + }.wont_change 'Work.count' + + must_respond_with :redirect + must_redirect_to root_path + expect(flash[:result_text]).must_equal "You must be logged in to view this section" + + expect(Work.find_by(id: poodr.id).title).must_equal "Practical Object Oriented Design in Ruby" + expect(Work.find_by(id: poodr.id).creator).must_equal "Sandi Metz" + expect(Work.find_by(id: poodr.id).description).must_equal "programming!" + expect(Work.find_by(id: poodr.id).publication_year).must_equal 2012 + expect(Work.find_by(id: poodr.id).category).must_equal "book" + end end - it "redirects to the work page if the user has already voted for that work" do + describe "upvote" do + it "redirects to the root path if no user is logged in" do + expect{ + post upvote_path(poodr.id) + }.wont_change 'Vote.count' + must_respond_with :redirect + must_redirect_to root_path + end end end end diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index e2968d78..5db3d074 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -2,6 +2,18 @@ dan: username: dan + name: Dan + uid: "123123" + provider: github kari: username: kari + name: Kari + uid: "456456" + provider: github + +google_user: + username: goog + name: Goog + uid: "789789" + provider: google_oauth2 diff --git a/test/fixtures/works.yml b/test/fixtures/works.yml index a321c1e9..f2a76589 100644 --- a/test/fixtures/works.yml +++ b/test/fixtures/works.yml @@ -6,6 +6,7 @@ album: description: This is an older album publication_year: 1955-11-01 category: album + user: kari another_album: title: New Title @@ -13,6 +14,7 @@ another_album: description: This is a newer album publication_year: 2016-04-08 category: album + user: kari poodr: title: Practical Object Oriented Design in Ruby @@ -20,7 +22,9 @@ poodr: description: programming! publication_year: 2012 category: book + user: dan movie: title: test movie - has only required fields category: movie + user: kari diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 793ce7e6..b2f5ec56 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -10,6 +10,14 @@ end end + it "has a list of works the user added to the site" do + dan = users(:dan) + dan.must_respond_to :works + dan.works.each do |work| + work.must_be_kind_of Work + end + end + it "has a list of ranked works" do dan = users(:dan) dan.must_respond_to :ranked_works @@ -28,17 +36,27 @@ it "requires a unique username" do username = "test username" - user1 = User.new(username: username) + user1 = User.new(username: username, uid: 123, provider: 'github') # This must go through, so we use create! user1.save! - user2 = User.new(username: username) + user2 = User.new(username: username, uid: 123, provider: 'github') result = user2.save result.must_equal false user2.errors.messages.must_include :username end + it "requires a uid" do + user = User.new + user.valid?.must_equal false + user.errors.messages.must_include :uid + end + it "requires a provider" do + user = User.new + user.valid?.must_equal false + user.errors.messages.must_include :uid + end end end diff --git a/test/models/vote_test.rb b/test/models/vote_test.rb index f2615aa1..f336fa86 100644 --- a/test/models/vote_test.rb +++ b/test/models/vote_test.rb @@ -16,8 +16,8 @@ end describe "validations" do - let (:user1) { User.new(username: 'chris') } - let (:user2) { User.new(username: 'chris') } + let (:user1) { User.new(username: 'chris', uid: 123, provider: 'github') } + let (:user2) { User.new(username: 'chris', uid: 123, provider: 'github') } let (:work1) { Work.new(category: 'book', title: 'House of Leaves') } let (:work2) { Work.new(category: 'book', title: 'For Whom the Bell Tolls') } diff --git a/test/models/work_test.rb b/test/models/work_test.rb index d9c00073..78243b13 100644 --- a/test/models/work_test.rb +++ b/test/models/work_test.rb @@ -83,7 +83,7 @@ it "tracks the number of votes" do work = Work.create!(title: "test title", category: "movie") 4.times do |i| - user = User.create!(username: "user#{i}") + user = User.create!(username: "user#{i}", uid: i, provider: 'github') Vote.create!(user: user, work: work) end work.vote_count.must_equal 4 @@ -97,7 +97,7 @@ # Create users to do the voting test_users = [] 20.times do |i| - test_users << User.create!(username: "user#{i}") + test_users << User.create!(username: "user#{i}", uid: i, provider: 'github') end # Create media to vote upon diff --git a/test/test_helper.rb b/test/test_helper.rb index 5b4fb667..0035b69c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,4 +23,48 @@ class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... + + # OK this is what you're adding + def setup + # Once you have enabled test mode, all requests + # to OmniAuth will be short circuited to use the mock authentication hash. + # A request to /auth/provider will redirect immediately to /auth/provider/callback. + OmniAuth.config.test_mode = true + end + + # Test helper method to generate a mock auth hash + # for fixture data + def mock_auth_hash(user) + return { + provider: user.provider, + uid: user.uid, + info: { + name: user.name, + nickname: user.username + } + } + end + + def mock_google_auth_hash(user) + return { + provider: user.provider, + uid: user.uid, + info: { + name: user.name, + email: user.username + } + } + end + + def perform_login(user) + OmniAuth.config.mock_auth[:github] = OmniAuth::AuthHash.new(mock_auth_hash(user)) + + get auth_callback_path(:github) + end + + def perform_google_login(user) + OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(mock_google_auth_hash(user)) + + get auth_callback_path(:google_oauth2) + end end