diff --git a/Gemfile b/Gemfile index fc6d138..1873602 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,8 @@ gem 'bcrypt', '~> 3.1.7' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development +gem 'rails-controller-testing' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platform: :mri diff --git a/Gemfile.lock b/Gemfile.lock index 8f749b4..85f1804 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,6 +103,10 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 5.0.1) sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.1) + actionpack (~> 5.x) + actionview (~> 5.x) + activesupport (~> 5.x) rails-dom-testing (2.0.2) activesupport (>= 4.2.0, < 6.0) nokogiri (~> 1.6) @@ -171,6 +175,7 @@ DEPENDENCIES pry puma (~> 3.0) rails (~> 5.0.1) + rails-controller-testing sass-rails (~> 5.0) spring spring-watcher-listen (~> 2.0.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1c07694..82974a2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception + + def record_not_found + raise ActiveRecord::RecordNotFound.new('Not Found') + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..742ba2a --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,24 @@ +class UsersController < ApplicationController + def show + @user = User.find_by(slug: params[:slug]) or record_not_found + end + + def new + @user = User.new + end + + def create + @user = User.new(user_params) + if @user.save + redirect_to user_path(slug: @user.slug) + else + render :new + end + end + + private + + def user_params + params.require(:user).permit(:username, :email, :password, :password_confirmation) + end +end diff --git a/app/models/concerns/password_format.rb b/app/models/concerns/string_format.rb similarity index 67% rename from app/models/concerns/password_format.rb rename to app/models/concerns/string_format.rb index e863baf..4db7f79 100644 --- a/app/models/concerns/password_format.rb +++ b/app/models/concerns/string_format.rb @@ -1,6 +1,7 @@ -module PasswordFormat - EIGHT_OR_MORE_CHARACTERS = /\A.{8,}\z/ - CONTAINS_A_DIGIT = /\d/ +module StringFormat STARTS_WITH_NON_WHITESPACE = /\A\S/ ENDS_WITH_NON_WHITESPACE = /\S\z/ + ONLY_PRINTABLE_CHARACTERS = /\A[[:print:]]*\z/ + EIGHT_OR_MORE_CHARACTERS = /\A.{8,}\z/ + CONTAINS_A_DIGIT = /\d/ end diff --git a/app/models/user.rb b/app/models/user.rb index 7aaa78a..76e5919 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,21 +1,37 @@ class User < ApplicationRecord before_save { email.downcase! } + before_validation :generate_slug validates :username, presence: true, length: { maximum: 24 }, uniqueness: { case_sensitive: false } + validates :username, format: { with: StringFormat::ONLY_PRINTABLE_CHARACTERS, + message: 'can only contain printable characters' } + validates :username, format: { with: StringFormat::STARTS_WITH_NON_WHITESPACE, + message: 'can not start with whitespace' } + validates :username, format: { with: StringFormat::ENDS_WITH_NON_WHITESPACE, + message: 'can not end with whitespace' } validates :email, presence: true, length: { maximum: 255 }, format: { with: EmailFormat::EMAIL }, uniqueness: { case_sensitive: false } + validates :slug, + presence: true, + uniqueness: true has_secure_password - validates :password, format: { with: PasswordFormat::EIGHT_OR_MORE_CHARACTERS, + validates :password, format: { with: StringFormat::EIGHT_OR_MORE_CHARACTERS, message: 'must have 8 or more characters' } - validates :password, format: { with: PasswordFormat::CONTAINS_A_DIGIT, + validates :password, format: { with: StringFormat::CONTAINS_A_DIGIT, message: 'must have at least one digit' } - validates :password, format: { with: PasswordFormat::STARTS_WITH_NON_WHITESPACE, + validates :password, format: { with: StringFormat::STARTS_WITH_NON_WHITESPACE, message: 'can not start with whitespace' } - validates :password, format: { with: PasswordFormat::ENDS_WITH_NON_WHITESPACE, + validates :password, format: { with: StringFormat::ENDS_WITH_NON_WHITESPACE, message: 'can not end with whitespace' } + + private + + def generate_slug + self.slug ||= self.username.parameterize if self.username.present? + end end diff --git a/app/views/shared/_error_messages.html.erb b/app/views/shared/_error_messages.html.erb new file mode 100644 index 0000000..f80053e --- /dev/null +++ b/app/views/shared/_error_messages.html.erb @@ -0,0 +1,12 @@ +<% if @user.errors.any? %> +
+
+ The form contains <%= pluralize(@user.errors.count, "error") %>. +
+ +
+<% end %> diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb new file mode 100644 index 0000000..16ca49d --- /dev/null +++ b/app/views/users/new.html.erb @@ -0,0 +1,21 @@ +<% provide(:title, 'Sign Up') %> +
+

Sign Up

+ <%= form_for(@user, url: signup_path) do |f| %> + <%= render 'shared/error_messages' %> + + <%= f.label :username %>
+ <%= f.text_field :username %>
+ + <%= f.label :email %>
+ <%= f.email_field :email %>
+ + <%= f.label :password %>
+ <%= f.password_field :password %>
+ + <%= f.label :password_confirmation, "Password Again" %>
+ <%= f.password_field :password_confirmation %>
+ + <%= f.submit "Sign me up!" %> + <% end %> +
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb new file mode 100644 index 0000000..6b685c1 --- /dev/null +++ b/app/views/users/show.html.erb @@ -0,0 +1,11 @@ +
+

<%= @user.username %>

+

first_name: <%= @user.first_name %>

+

last_name: <%= @user.last_name %>

+

username: <%= @user.first_name %>

+

id: <%= @user.id %>

+
+

description: <%= @user.description %>

+

website: <%= @user.website %>

+

email: <%= @user.email %>

+
diff --git a/config/routes.rb b/config/routes.rb index bc557ce..51247a1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,7 @@ Rails.application.routes.draw do root 'welcome#index' + + resources :users, param: :slug, only: [:show] + get '/signup', to: 'users#new' + post '/signup', to: 'users#create' end diff --git a/db/migrate/20170111005636_add_slug_to_users.rb b/db/migrate/20170111005636_add_slug_to_users.rb new file mode 100644 index 0000000..4001e43 --- /dev/null +++ b/db/migrate/20170111005636_add_slug_to_users.rb @@ -0,0 +1,5 @@ +class AddSlugToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :slug, :string, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index eee5e57..5572d3c 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.define(version: 20170110022729) do +ActiveRecord::Schema.define(version: 20170111005636) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -25,6 +25,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "password_digest" + t.string "slug" end end diff --git a/db/seeds.rb b/db/seeds.rb index 1beea2a..1a49aa1 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). -# -# Examples: -# -# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) -# Character.create(name: 'Luke', movie: movies.first) +User.create(username: 'Alex', password: 'alex@allegroplanet.com', password: 'pass1word', password_confirmation: 'pass1word') diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb new file mode 100644 index 0000000..b4ec4e1 --- /dev/null +++ b/test/controllers/users_controller_test.rb @@ -0,0 +1,51 @@ +require 'test_helper' + +class UsersControllerTest < ActionDispatch::IntegrationTest + def user + @user ||= User.first + end + + test 'GET #show is successful' do + get user_path(user.slug) + assert_response :success + end + + test 'GET #show renders the "show" template' do + get user_path(user.slug) + assert_template :show + end + + test 'GET #new is successful' do + get signup_path + assert_response :success + end + + test 'GET #new renders the "new" template' do + get signup_path + assert_template :new + end + + test 'POST #create redirects to the user page successfuly' do + valid_new_user_params = { + username: 'Joe Valid', + email: 'valid@email.com', + password: 'valid1pass', + password_confirmation: 'valid1pass' + } + + post signup_path, params: { user: valid_new_user_params } + assert_redirected_to '/users/joe-valid' + end + + test 'POST #create renders the "new" template when invalid user params are passed' do + invalid_new_user_params = { + username: 'Joe InValid', + email: 'invalidemail', + password: 'pass', + password_confirmation: 'pass2' + } + + post signup_path, params: { user: invalid_new_user_params } + assert_template :new + end +end diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index e804abe..b070b45 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,6 +1,7 @@ markoates: username: markoates email: marks@example.com + slug: markoates first_name: Mark last_name: Oates website: www.example.com diff --git a/test/models/user_test.rb b/test/models/user_test.rb index d866b38..4bf8ccf 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -1,84 +1,126 @@ require 'test_helper' class UserTest < ActiveSupport::TestCase + + def user + @user ||= User.new(username: 'mr. test', + email: 'test@email.com', + password: 'pass1word', + password_confirmation: 'pass1word' + ) + end + 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' - ) + user.save 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" + user.username = '' + user.save + assert_includes 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' + username_too_long = 'a' * 25 + user.username = username_too_long + user.save + assert_includes user.errors[:username], 'is too long (maximum is 24 characters)' 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' + user.username = already_existing_username + user.save + assert_includes user.errors[:username], 'has already been taken' + end + + test 'username must contain only printable characters' do + user.username = "\x0A" + user.save + assert_includes user.errors[:username], 'can only contain printable characters' + end + + test 'username can not end in whitespace' do + user.username = 'endsinwhitespace ' + user.save + assert_includes user.errors[:username], 'can not end with whitespace' + end + + test 'username can not start with whitespace' do + user.username = ' startswithwhitespace' + user.save + assert_includes user.errors[:username], 'can not start with whitespace' end test 'email must be present' do - new_user = User.create - assert_includes new_user.errors[:email], "can't be blank" + user.email = '' + user.save + assert_includes 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' + user.email = too_long_email + user.save + assert_includes 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' + user.email = 'an_invalid%^&*email' + user.save + assert_includes 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' + user.email = already_existing_email + user.save + assert_includes 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' + user.email = jumblecase_email + user.save + user.reload + assert_includes 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' + user.password = 'pw2shrt' + user.save + assert_includes 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' + user.password = 'nodigits' + user.save + assert_includes 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' + user.password = ' startwithspace' + user.save + assert_includes 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' + user.password = 'endswithspace ' + user.save + assert_includes user.errors[:password], 'can not end with whitespace' + end + + test 'generates a slug on validation' do + user.save + assert_equal user.slug, 'mr-test' + end + + test 'with a slug that already exists, is invalid' do + username_that_already_exists = users(:markoates).username + user.username = username_that_already_exists + user.save + assert_includes user.errors[:slug], 'has already been taken' end end