diff --git a/Gemfile b/Gemfile index 211edb2..fc6d138 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ git_source(:github) do |repo_name| end +gem 'pry' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 5.0.1' # Use postgresql as the database for Active Record @@ -30,7 +31,7 @@ gem 'jbuilder', '~> 2.5' # Use Redis adapter to run Action Cable in production # gem 'redis', '~> 3.0' # Use ActiveModel has_secure_password -# gem 'bcrypt', '~> 3.1.7' +gem 'bcrypt', '~> 3.1.7' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development diff --git a/Gemfile.lock b/Gemfile.lock index f778696..8f749b4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -39,8 +39,10 @@ GEM minitest (~> 5.1) tzinfo (~> 1.1) arel (7.1.4) + bcrypt (3.1.11) builder (3.2.2) byebug (9.0.6) + coderay (1.1.1) coffee-rails (4.2.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.2.x) @@ -81,6 +83,10 @@ GEM nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) pg (0.19.0) + pry (0.10.3) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) puma (3.6.2) rack (2.0.1) rack-test (0.6.3) @@ -119,6 +125,7 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + slop (3.6.0) spring (2.0.0) activesupport (>= 4.2) spring-watcher-listen (2.0.1) @@ -154,12 +161,14 @@ PLATFORMS ruby DEPENDENCIES + bcrypt (~> 3.1.7) byebug coffee-rails (~> 4.2) jbuilder (~> 2.5) jquery-rails listen (~> 3.0.5) pg (~> 0.18) + pry puma (~> 3.0) rails (~> 5.0.1) sass-rails (~> 5.0) diff --git a/app/models/concerns/email_format.rb b/app/models/concerns/email_format.rb new file mode 100644 index 0000000..f626c49 --- /dev/null +++ b/app/models/concerns/email_format.rb @@ -0,0 +1,3 @@ +module EmailFormat + EMAIL = /\A[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\z/i +end diff --git a/app/models/concerns/password_format.rb b/app/models/concerns/password_format.rb new file mode 100644 index 0000000..e863baf --- /dev/null +++ b/app/models/concerns/password_format.rb @@ -0,0 +1,6 @@ +module PasswordFormat + EIGHT_OR_MORE_CHARACTERS = /\A.{8,}\z/ + CONTAINS_A_DIGIT = /\d/ + STARTS_WITH_NON_WHITESPACE = /\A\S/ + ENDS_WITH_NON_WHITESPACE = /\S\z/ +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..7aaa78a --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,21 @@ +class User < ApplicationRecord + before_save { email.downcase! } + validates :username, + presence: true, + length: { maximum: 24 }, + uniqueness: { case_sensitive: false } + validates :email, + presence: true, + length: { maximum: 255 }, + format: { with: EmailFormat::EMAIL }, + uniqueness: { case_sensitive: false } + has_secure_password + validates :password, format: { with: PasswordFormat::EIGHT_OR_MORE_CHARACTERS, + message: 'must have 8 or more characters' } + validates :password, format: { with: PasswordFormat::CONTAINS_A_DIGIT, + message: 'must have at least one digit' } + validates :password, format: { with: PasswordFormat::STARTS_WITH_NON_WHITESPACE, + message: 'can not start with whitespace' } + validates :password, format: { with: PasswordFormat::ENDS_WITH_NON_WHITESPACE, + message: 'can not end with whitespace' } +end diff --git a/db/migrate/20170108205331_create_users.rb b/db/migrate/20170108205331_create_users.rb new file mode 100644 index 0000000..d0ee6bc --- /dev/null +++ b/db/migrate/20170108205331_create_users.rb @@ -0,0 +1,14 @@ +class CreateUsers < ActiveRecord::Migration[5.0] + def change + create_table :users do |t| + t.string :username + t.string :email + t.string :first_name + t.string :last_name + t.string :website + t.text :description + + t.timestamps + end + end +end diff --git a/db/migrate/20170110022729_add_password_digest_to_users.rb b/db/migrate/20170110022729_add_password_digest_to_users.rb new file mode 100644 index 0000000..30a8d13 --- /dev/null +++ b/db/migrate/20170110022729_add_password_digest_to_users.rb @@ -0,0 +1,5 @@ +class AddPasswordDigestToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :password_digest, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 2611543..eee5e57 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,21 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 0) do +ActiveRecord::Schema.define(version: 20170110022729) 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" + t.string "email" + t.string "first_name" + t.string "last_name" + t.string "website" + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "password_digest" + end + end diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..e804abe --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,7 @@ +markoates: + username: markoates + email: marks@example.com + first_name: Mark + last_name: Oates + website: www.example.com + description: I like cats! diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..d866b38 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,84 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + test 'creates a new user in the database' do + User.destroy_all + new_user = User.create!(username: 'mr. test', + email: 'test@email.com', + password: 'pass1word', + password_confirmation: 'pass1word' + ) + assert_equal User.count, 1 + end + + test 'username must be present' do + new_user = User.create + assert_includes new_user.errors[:username], "can't be blank" + end + + test 'username can not be longer than 24 characters' do + too_long_username = 'a' * 25 + new_user = User.create(username: too_long_username) + assert_includes new_user.errors[:email], 'is invalid' + end + + test 'username must be unique' do + already_existing_username = users(:markoates).username + new_user = User.create(username: already_existing_username) + assert_includes new_user.errors[:username], 'has already been taken' + end + + test 'email must be present' do + new_user = User.create + assert_includes new_user.errors[:email], "can't be blank" + end + + test 'email must be less that 255 characters' do + too_long_email = 'a' * 256 + new_user = User.create(email: too_long_email) + assert_includes new_user.errors[:email], 'is invalid' + end + + test 'email must be valid' do + new_user = User.create(email: 'an_invalid%^&*email') + assert_includes new_user.errors[:email], 'is invalid' + end + + test 'email must be unique' do + already_existing_email = users(:markoates).email + new_user = User.create(email: already_existing_email) + assert_includes new_user.errors[:email], 'has already been taken' + end + + test 'email is saved in lowercase' do + jumblecase_email = 'JuMbLeCaSe@EmAiL.CoM' + new_user = User.create(username: 'Mrs. Jumble', + email: jumblecase_email, + password: 'pass1word', + password_confirmation: 'pass1word' + ) + new_user.save + new_user.reload + assert_includes new_user.email, 'jumblecase@email.com' + end + + test 'with a password less than 8 characters, is invalid' do + new_user = User.create(password: 'pw2shrt') + assert_includes new_user.errors[:password], 'must have 8 or more characters' + end + + test 'with a password that does not contain at least one digit, is invalid' do + new_user = User.create(password: 'nodigitshere') + assert_includes new_user.errors[:password], 'must have at least one digit' + end + + test 'with a password that starts with whitespace, is invalid' do + new_user = User.create(password: ' startwithspace') + assert_includes new_user.errors[:password], 'can not start with whitespace' + end + + test 'with a password that ends with whitespace, is invalid' do + new_user = User.create(password: 'endswithspace ') + assert_includes new_user.errors[:password], 'can not end with whitespace' + end +end