diff --git a/.gitignore b/.gitignore index d1a1edf06f..dc9aec4925 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ # Local cache of Rubocop remote config .rubocop-* + +.env \ No newline at end of file diff --git a/Gemfile b/Gemfile index b1a320395a..9f67097a88 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,18 @@ end group :development, :test do gem 'rubocop', '1.20' end + +gem "sinatra", "~> 3.0" +gem "sinatra-contrib", "~> 3.0" +gem "webrick", "~> 1.8" +gem "rack-test", "~> 2.1" + +gem "pg", "~> 1.5" + +gem "bcrypt", "~> 3.1" + +gem "dotenv", "~> 2.8" + +gem "sib-api-v3-sdk", "~> 9.1" + +gem "rackup", "~> 1.0" diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..ba0cca760b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,34 @@ GEM remote: https://rubygems.org/ specs: + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) ansi (1.5.0) ast (2.4.2) + bcrypt (3.1.18) diff-lcs (1.4.4) docile (1.4.0) + dotenv (2.8.1) + ethon (0.16.0) + ffi (>= 1.15.0) + ffi (1.15.5) + json (2.6.3) + multi_json (1.15.0) + mustermann (3.0.0) + ruby2_keywords (~> 0.0.1) parallel (1.20.1) parser (3.0.2.0) ast (~> 2.4.1) + pg (1.5.3) + public_suffix (5.0.1) + rack (2.2.7) + rack-protection (3.0.6) + rack + rack-test (2.1.0) + rack (>= 1.3) + rackup (1.0.0) + rack (< 3) + webrick rainbow (3.0.0) regexp_parser (2.1.1) rexml (3.2.5) @@ -36,6 +57,11 @@ GEM rubocop-ast (1.11.0) parser (>= 3.0.1.1) ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + sib-api-v3-sdk (9.1.0) + addressable (~> 2.3, >= 2.3.0) + json (~> 2.1, >= 2.1.0) + typhoeus (~> 1.0, >= 1.0.1) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -46,18 +72,43 @@ GEM terminal-table simplecov-html (0.12.3) simplecov_json_formatter (0.1.3) + sinatra (3.0.6) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.0.6) + tilt (~> 2.0) + sinatra-contrib (3.0.6) + multi_json + mustermann (~> 3.0) + rack-protection (= 3.0.6) + sinatra (= 3.0.6) + tilt (~> 2.0) terminal-table (3.0.1) unicode-display_width (>= 1.1.1, < 3) + tilt (2.1.0) + typhoeus (1.4.0) + ethon (>= 0.9.0) unicode-display_width (2.0.0) + webrick (1.8.1) PLATFORMS ruby + x86_64-linux DEPENDENCIES + bcrypt (~> 3.1) + dotenv (~> 2.8) + pg (~> 1.5) + rack-test (~> 2.1) + rackup (~> 1.0) rspec rubocop (= 1.20) + sib-api-v3-sdk (~> 9.1) simplecov simplecov-console + sinatra (~> 3.0) + sinatra-contrib (~> 3.0) + webrick (~> 1.8) RUBY VERSION ruby 3.0.2p107 diff --git a/README.md b/README.md index 465eda879b..6b5b913d76 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,85 @@ Chitter Challenge ================= +An implementation of the solo project from the Makers Academy Web Applications module. The deployed app can be found at https://chitter-challenge-ct9w.onrender.com/ + +Running the app locally +----------------------- +``` +$ git clone https://github.com/ev-th/chitter-challenge.git +$ bundle install +$ createdb chitter +$ psql -h 127.0.0.1 chitter < chitter.sql +$ createdb chitter_test +$ psql -h 127.0.0.1 chitter_test < chitter.sql +$ rspec +$ rackup +``` + +App features +------------ +* A user can sign up for an account. Username and email address must be unique. +* Users can log in and out using their account. +* Users can post peeps when logged in. +* Peeps are listed on the homepage in reverse chronological order. +* Peeps are displayed with the user's name and username and a timestamp. +* When a user is tagged in a peep, they will receive a notification email. + +Technologies +------------ +* Languages: Ruby, HTML, CSS, SQL +* git and GitHub +* Sinatra +* PostgreSQL +* Bcrypt +* Brevo API +* Render +* Excalidraw +* rack +* RSpec +* rubocop + +Approaching the challenge +------------------------- +This was a big challenge with lots of optional extras, so I wasn't sure what I'd be able to achieve in the time. Therefore, I took the approach to create an MVP with the 'straight up' features first, then iteratively add additional features as time allowed. + +To start, I planned the views, then the necessary routes: +![excalidraw_design](./excalidraw_design.png) + +Then I planned the database tables. The database has two tables: peeps and users. Users has a one to many relationship with peeps. I set this up and wrote the seeds. + +After that, I wrote the database repositories. The fastest option for me was to write the repositories myself as I'm currently unfamiliar with ORMs. This was something I'd hoped to revisit at the end. + +Then I wrote the https routes and sanitised user input for empty inputs and any angle brackets that could be an attempt to inject html by a user. This is a pretty basic attempt as sanitising user input and is another aspect of the project that could be examined further. + +In order to finish the 'straight up' version of the challenge, I added functionality to the database repository to check whether a username or email already exists in the users table to ensure there are no duplications when a user signs up. If users sign up with a duplicated email or username, or input invalid data, they are taken to a failure page with a 400 status code. I also used the bcrypt gem to encrypt user passwords. + +Once the basic features were complete, I moved to the advanced ones. I implemented log in and log out using sessions built into Sinatra. With this, I updated the homepage to adapt to the session. If the user was logged in they could make a new peep or log out. If the user was not logged in they could sign up or log in. Now that the user can log in, I updated the new peep POST route to use session data for user id instead of having the user input their id manually. + +Next, I implemented the email sending feature using Brevo. I copied some boilerplate code from their documentation for this and attempted to unit test it, but it was proving to be very complex and didn't seem to be a good use of time. Therefore this is not covered in the test suite. However when testing it manually, it was working. I put secret data in a .env file and added that to my .gitignore. I also added these to my Render environment variables and checked that the emails were also being sent in the deployed version. + +Finally, I added some CSS styling. Currently there is one style sheet used for all the views. I had some fun here! 🐦 + +What would I do with more time? +------------------------------- +Overall I'm happy with what I achieved, but there are a number of things I would like to have done if I had more time: + +* I missed the final bonus feature to add replies to peeps. +* Following on from the previous point, I would have liked the app to have pages for users, where you can see only their peeps, along with comments on the peeps. This is where the one to many relationship between the database tables would have made sense. +* The failure pages for invalid user inputs are very basic and don't give the user information about why the input was invalid. I would have liked more detail here. +* Bcrypt was severely slowing my tests down so I turned down the cost slightly to make it more workable while in development. I would like to work out how to only turn down costs in tests and leave it higher for deployed code. +* HTML form input sanitisation was very basic and could have been developed further. +* I would implement ActiveRecord as an ORM. +* Overall, I'm happy with my testing and used TDD throughout the majority of the process. However, I struggled to use testing when implementing the email sender, so this is not covered by the test suite. +* I'd like to have spent some more time on styling. + +--- + +The following are the specifications for the project provided by Makers: + +Chitter Challenge Specifications +================================ + * Feel free to use Google, your notes, books, etc. but work on your own * If you refer to the solution of another coach or student, please put a link to that in your README * If you have a partial solution, **still check in a partial solution** diff --git a/app.rb b/app.rb new file mode 100644 index 0000000000..7f2c645170 --- /dev/null +++ b/app.rb @@ -0,0 +1,152 @@ +require 'bcrypt' +require 'sinatra/base' +require 'sinatra/reloader' +require_relative 'lib/peep_repository' +require_relative 'lib/user_repository' +require_relative 'lib/database_connection' +require_relative 'lib/email_sender' + +DatabaseConnection.connect + +class Application < Sinatra::Base + enable :sessions + + configure :development do + register Sinatra::Reloader + also_reload 'lib/user_repository' + also_reload 'lib/peep_repository' + end + + get '/' do + peep_repo = PeepRepository.new + peeps = peep_repo.all + @peeps = peeps.sort_by(&:time_posted).reverse + + user_repo = UserRepository.new + @peeps.each { |peep| peep.user = user_repo.find(peep.user_id) } + @user = session[:user_id].nil? ? nil : user_repo.find(session[:user_id]) + + erb(:index) + end + + get '/login' do + erb(:login) + end + + get '/sign-up' do + erb(:sign_up) + end + + get '/new-peep' do + return redirect('/login') if session[:user_id].nil? + erb(:new_peep) + end + + get '/logout' do + session[:user_id] = nil + return redirect('/') + end + + post '/login' do + repo = UserRepository.new + email = params[:email] + password = params[:password] + + unless input_valid?(email) && input_valid?(password) && repo.email_exists?(email) + status 400 + return erb(:login_failure) + end + + unless repo.correct_password?(email, password) + status 400 + return erb(:login_failure) + end + + @user = repo.find_by_email(email) + session[:user_id] = @user.id + return erb(:login_success) + end + + post '/sign-up' do + unless params.values.all? { |input| input_valid?(input) } + status 400 + return erb(:sign_up_failure) + end + + repo = UserRepository.new + + if repo.email_exists?(params[:email]) || repo.username_exists?(params[:username]) + status 400 + return erb(:sign_up_failure) + end + + user = get_user_from_params(params) + repo.create(user) + return erb(:sign_up_success) + end + + post '/new-peep' do + unless params.values.all? { |input| input_valid?(input) } + status 400 + return erb(:new_peep_failure) + end + + repo = PeepRepository.new + peep = get_peep_from_params(params) + repo.create(peep) + + email_tagged_users(peep) + erb(:new_peep_success) + end + + private + + def get_user_from_params(params) + user = User.new + user.email = params[:email] + user.password = params[:password] + user.name = params[:name] + user.username = params[:username] + user + end + + def get_peep_from_params(params) + peep = Peep.new + peep.content = params[:content] + peep.time_posted = params[:time_posted] || Time.new + peep.user_id = session[:user_id] + peep + end + + def email_tagged_users(peep) + users = get_tagged_users(peep) + return if users.nil? + + users.each do |user| + email_sender = EmailSender.new( + user.email, + 'You have been tagged in a peep!' + ) + email_sender.send_email + end + end + + def get_tagged_users(peep) + words = peep.content.split + tags = words.select { |word| word.start_with?("@") } + usernames = tags.map { |tag| tag[1..] } + + repo = UserRepository.new + + users = [] + usernames.each do |username| + user = repo.find_by_username(username) + users << user unless user.nil? + end + users + end + + def input_valid?(input) + input != '' && !input.match(/[<>]/) + end +end diff --git a/chitter.sql b/chitter.sql new file mode 100644 index 0000000000..77b9dce286 --- /dev/null +++ b/chitter.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS users, peeps; + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email text, + password text, + name text, + username text +); + +CREATE TABLE peeps ( + id SERIAL PRIMARY KEY, + content text, + time_posted timestamp, + user_id integer, + constraint fk_user foreign key(user_id) + references users(id) + on delete cascade +); \ No newline at end of file diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..af14ef717e --- /dev/null +++ b/config.ru @@ -0,0 +1,2 @@ +require './app' +run Application diff --git a/excalidraw_design.png b/excalidraw_design.png new file mode 100644 index 0000000000..e5b6bfb4f0 Binary files /dev/null and b/excalidraw_design.png differ diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 0000000000..34b41dff47 --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,22 @@ +require 'pg' + +class DatabaseConnection + def self.connect + if ENV['DATABASE_URL'] != nil + @connection = PG.connect(ENV['DATABASE_URL']) + return + end + + database_name = ENV['ENV'] == 'test' ? 'chitter_test' : 'chitter' + @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) + end + + def self.exec_params(query, params) + if @connection.nil? + raise 'DatabaseConnection.exec_params: Cannot run a SQL query as the connection to'\ + 'the database was never opened. Did you make sure to call first the method '\ + '`DatabaseConnection.connect` in your app.rb file (or in your tests spec_helper.rb)?' + end + @connection.exec_params(query, params) + end +end diff --git a/lib/email_sender.rb b/lib/email_sender.rb new file mode 100644 index 0000000000..c1adf79612 --- /dev/null +++ b/lib/email_sender.rb @@ -0,0 +1,41 @@ +require 'dotenv' +require 'sib-api-v3-sdk' +Dotenv.load + +class EmailSender + def initialize(recipient, html_message, email_api = SibApiV3Sdk) + @email_api = email_api + @recipient = recipient + @html_message = html_message + + @email_api.configure do |config| + config.api_key['api-key'] = ENV['BREVO_API_KEY'] + config.api_key['partner-key'] = ENV['BREVO_API_KEY'] + end + end + + def send_email + + api_instance = @email_api::TransactionalEmailsApi.new + + send_smtp_email = @email_api::SendSmtpEmail.new + send_smtp_email.html_content = @html_message + send_smtp_email.subject = "You've been tagged!" + + email_to = @email_api::SendSmtpEmailTo.new + email_to.email = @recipient + send_smtp_email.to = [email_to] + + email_from = @email_api::SendSmtpEmailSender.new + email_from.email = ENV['EMAIL_FROM'] + send_smtp_email.sender = email_from + + begin + result = api_instance.send_transac_email(send_smtp_email) + p result + rescue @email_api::ApiError => e + binding.irb + puts "Exception when calling TransactionalEmailsApi->send_transac_email: #{e}" + end + end +end diff --git a/lib/peep.rb b/lib/peep.rb new file mode 100644 index 0000000000..dc11962c5b --- /dev/null +++ b/lib/peep.rb @@ -0,0 +1,8 @@ +class Peep + attr_accessor :id, :content, :time_posted, :user_id, :user + + def formatted_time + return nil if @time_posted.nil? + @time_posted.strftime('%d %b %Y at %H:%M') + end +end diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb new file mode 100644 index 0000000000..a42b3e99bc --- /dev/null +++ b/lib/peep_repository.rb @@ -0,0 +1,40 @@ +require_relative './peep' + +class PeepRepository + def all + sql = 'SELECT * FROM peeps;' + records = DatabaseConnection.exec_params(sql, []) + records.map { |record| create_peep_from_record(record) } + end + + def find(id) + sql = 'SELECT * FROM peeps WHERE id = $1;' + records = DatabaseConnection.exec_params(sql, [id]) + return create_peep_from_record(records.first) + end + + def create(peep) + sql = 'INSERT INTO peeps (content, time_posted, user_id) + VALUES ($1, $2, $3)' + params = [peep.content, peep.time_posted, peep.user_id] + DatabaseConnection.exec_params(sql, params) + end + + private + + def create_peep_from_record(record) + peep = Peep.new + peep.id = record['id'].to_i + peep.content = record['content'] + peep.time_posted = timestamp_to_time_object(record['time_posted']) + peep.user_id = record['user_id'].to_i + return peep + end + + def timestamp_to_time_object(timestamp) + date, time = timestamp.split + year, month, day = date.split("-") + hour, minute, second = time.split(":") + return Time.new(year, month, day, hour, minute, second) + end +end diff --git a/lib/user.rb b/lib/user.rb new file mode 100644 index 0000000000..1f12d2cc2e --- /dev/null +++ b/lib/user.rb @@ -0,0 +1,3 @@ +class User + attr_accessor :id, :email, :password, :name, :username +end diff --git a/lib/user_repository.rb b/lib/user_repository.rb new file mode 100644 index 0000000000..4fb59b7e84 --- /dev/null +++ b/lib/user_repository.rb @@ -0,0 +1,67 @@ +require 'bcrypt' +require_relative './user' + +class UserRepository + def all + sql = 'SELECT * FROM users;' + records = DatabaseConnection.exec_params(sql, []) + records.map { |record| create_user_from_record(record) } + end + + def find(id) + sql = 'SELECT * FROM users WHERE id = $1' + records = DatabaseConnection.exec_params(sql, [id]) + records.ntuples.zero? ? nil : create_user_from_record(records.first) + end + + def find_by_email(email) + sql = 'SELECT * FROM users WHERE email = $1' + records = DatabaseConnection.exec_params(sql, [email]) + records.ntuples.zero? ? nil : create_user_from_record(records.first) + end + + def find_by_username(username) + sql = 'SELECT * FROM users WHERE username = $1' + records = DatabaseConnection.exec_params(sql, [username]) + records.ntuples.zero? ? nil : create_user_from_record(records.first) + end + + def create(user) + encrypted_password = BCrypt::Password.create(user.password, cost: 7) + + sql = 'INSERT INTO users (email, password, name, username) + VALUES ($1, $2, $3, $4)' + params = [user.email, encrypted_password, user.name, user.username] + DatabaseConnection.exec_params(sql, params) + end + + def username_exists?(username) + sql = 'SELECT username FROM users WHERE username = $1' + records = DatabaseConnection.exec_params(sql, [username]) + records.ntuples.positive? + end + + def email_exists?(email) + sql = 'SELECT email FROM users WHERE email = $1' + records = DatabaseConnection.exec_params(sql, [email]) + records.ntuples.positive? + end + + def correct_password?(email, password) + user = find_by_email(email) + stored_password = BCrypt::Password.new(user.password) + stored_password == password + end + + private + + def create_user_from_record(record) + user = User.new + user.id = record['id'].to_i + user.email = record['email'] + user.password = record['password'] + user.name = record['name'] + user.username = record['username'] + return user + end +end diff --git a/public/pigeons.jpeg b/public/pigeons.jpeg new file mode 100644 index 0000000000..bf228bf6dd Binary files /dev/null and b/public/pigeons.jpeg differ diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000000..2cb6b43e6b --- /dev/null +++ b/public/styles.css @@ -0,0 +1,52 @@ +* { + font-family: "Comic Sans MS", "Comic Sans", cursive; + text-align: center; + font-weight: bold; + color: deeppink; +} + +body { + background-image: url(pigeons.jpeg); +} + +h1 { + font-size: 92; + text-shadow: 2px 2px black; +} + +h2 { + font-size: 60; + text-shadow: 2px 2px black; +} + +p { + font-size: 30px; + text-shadow: 1px 1px black; +} + +label { + text-shadow: 1px 1px black; +} + +.peep-container { + width: 45%; + margin: 10px auto 10px auto; + border: 5px solid black; + border-radius: 10px; + background-color: rgba(255,255,255,0.7); + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + +#content { + font-size: 40px; +} + +#time { + font-size: 20px; +} + +#names { + font-size: 20px; +} \ No newline at end of file diff --git a/spec/integration/app_spec.rb b/spec/integration/app_spec.rb new file mode 100644 index 0000000000..7aff94ee9a --- /dev/null +++ b/spec/integration/app_spec.rb @@ -0,0 +1,649 @@ +require 'spec_helper' +require 'rack/test' +require_relative '../../app' + +def log_in_a_user + user = User.new + user.email = 'hello@gmail.com' + user.password = 'new_pass_123!' + user.name = 'My Name' + user.username = 'new_username' + + repo = UserRepository.new + repo.create(user) + + post('/login', email: 'hello@gmail.com', password: 'new_pass_123!') +end + +describe Application do + include Rack::Test::Methods + + let(:app) { Application.new } + + before(:each) do + reset_users_table + reset_peeps_table + end + + describe 'GET /' do + it 'returns 200 OK' do + response = get('/') + expect(response.status).to eq 200 + end + + it 'returns returns html with all peeps' do + response = get('/') + + expect(response.body).to include '

All Peeps

' + + expect(response.body).to include 'content_1' + expect(response.body).to include '1 May 2023 at 17:15' + expect(response.body).to include 'name_1' + expect(response.body).to include 'username_1' + + expect(response.body).to include 'content_3' + expect(response.body).to include '21 Jun 2022 at 00:01' + expect(response.body).to include 'name_3' + expect(response.body).to include 'username_3' + + expect(response.body).to include 'content_4' + expect(response.body).to include '21 Jun 2022 at 22:01' + expect(response.body).to include 'name_4' + expect(response.body).to include 'username_4' + end + + it 'returns html with link to sign up and login when not logged in' do + response = get('/') + expect(response.body).to include 'Sign Up' + expect(response.body).to include 'Log In' + end + + it 'does not return html with link to log in or sign up when logged in' do + log_in_a_user + response = get('/') + expect(response.body).not_to include 'Log In' + expect(response.body).not_to include 'Sign Up' + end + + it 'returns html with link to post a new peep when logged in' do + log_in_a_user + response = get('/') + expect(response.body).to include 'Add new peep' + end + + it 'returns no link to make a new peep when not logged in' do + response = get('/') + expect(response.body).not_to include 'Add new peep' + end + + it 'returns html with link to log out when logged in' do + log_in_a_user + response = get('/') + expect(response.body).to include 'Log Out' + end + + it 'does not return html with link to log out when not logged in' do + response = get('/') + expect(response.body).not_to include 'Log Out' + end + end + + describe 'GET /login' do + it "returns 200 OK" do + response = get('/login') + expect(response.status).to eq 200 + end + + it 'returns html with login form using POST /login route' do + response = get('login') + expect(response.body).to include '
' + expect(response.body).to include '' + expect(response.body).to include '' + expect(response.body).to include '' + end + + it 'returns html with link back to homepage' do + response = get('/login') + expect(response.body).to include 'Back to homepage' + end + end + + describe 'GET /logout' do + it 'logs the user out' do + log_in_a_user + response = get('/logout') + expect(response.status).to eq 302 + end + end + + describe 'GET /sign-up' do + it 'returns 200 OK' do + response = get('/sign-up') + expect(response.status).to eq 200 + end + + it 'returns html with sign up form using POST /sign-up route' do + response = get('/sign-up') + expect(response.body).to include '' + expect(response.body).to include '' + expect(response.body).to include '' + expect(response.body).to include '' + expect(response.body).to include '' + expect(response.body).to include '' + end + + it 'returns html with link back to homepage' do + response = get('/sign-up') + expect(response.body).to include 'Back to homepage' + end + end + + describe 'GET /new-peep' do + context 'when user is logged in' do + it 'returns 200 OK' do + log_in_a_user + response = get('/new-peep') + expect(response.status).to eq 200 + end + + it 'returns html with new peep form using POST /new-peep route' do + log_in_a_user + response = get('/new-peep') + expect(response.body).to include '' + expect(response.body).to include '' + expect(response.body).to include '' + end + + it 'returns html with link back to the homepage' do + log_in_a_user + response = get('/new-peep') + expect(response.body).to include 'Back to homepage' + end + end + + context 'when user is not logged in' do + it 'redirects to the login page' do + response = get('/new-peep') + expect(response.status).to eq 302 + end + end + end + + describe 'POST /login' do + context 'when used with valid params' do + it 'returns 200 OK' do + response = log_in_a_user + expect(response.status).to eq 200 + end + + it 'returns html success message' do + response = log_in_a_user + expect(response.body).to include '

Success!

' + end + + it 'returns html with link back to the homepage' do + response = log_in_a_user + expect(response.body).to include '

Back to homepage

' + end + + it 'logs the user in' do + response = log_in_a_user + expect(response.body).to include '

Hello My Name! You are logged in as new_username

' + end + end + + context 'when used with invalid params' do + context 'when email is blank' do + it 'returns 400 Bad Request and failure page' do + response = post('/login', email: '', password: 'new_pass_123!') + expect(response.status).to eq 400 + expect(response.body).to include 'Back to homepage' + expect(response.body).to include '

One or more of your inputs was invalid

' + expect(response.body).to include 'Try again' + end + end + + context 'when email does not exist in the database' do + it 'returns 400 Bad Request and failure page' do + response = post('/login', email: 'fake_email@email.com', password: 'new_pass_123!') + expect(response.status).to eq 400 + expect(response.body).to include 'Back to homepage' + expect(response.body).to include '

One or more of your inputs was invalid

' + expect(response.body).to include 'Try again' + end + end + + context 'when password is incorrect' do + it 'returns 400 Bad Request and failure page' do + user = User.new + user.email = 'hello@gmail.com' + user.password = 'new_pass_123!' + user.name = 'My Name' + user.username = 'new_username' + + repo = UserRepository.new + repo.create(user) + + response = post('/login', email: 'hello@gmail.com', password: 'bad_pass') + expect(response.status).to eq 400 + expect(response.body).to include 'Back to homepage' + expect(response.body).to include '

One or more of your inputs was invalid

' + expect(response.body).to include 'Try again' + end + end + end + end + + describe 'POST /sign-up' do + context 'when used with valid params' do + it 'returns 200 OK' do + response = post( + '/sign-up', + email: 'new@gmail.com', + password: 'new_password', + name: 'New Name', + username: 'new_username' + ) + expect(response.status).to eq 200 + end + + it 'returns html with success message' do + response = post( + '/sign-up', + email: 'new@gmail.com', + password: 'new_password', + name: 'New Name', + username: 'new_username' + ) + expect(response.body).to include '

Success!

' + expect(response.body).to include '

Thanks for signing up!

' + end + + it 'returns html with link back to the homepage' do + response = post('/sign-up') + expect(response.body).to include 'Back to homepage' + end + + it 'adds a new user to the database' do + response = post( + '/sign-up', + email: 'new@gmail.com', + password: 'new_password', + name: 'New Name', + username: 'new_username' + ) + + repo = UserRepository.new + user = repo.find(5) + + expect(user.email).to eq 'new@gmail.com' + expect(user.name).to eq 'New Name' + expect(user.username).to eq 'new_username' + + stored_password = BCrypt::Password.new(user.password) + expect(stored_password).to eq 'new_password' + end + end + + context 'when used with invalid params' do + context 'when email already exists' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'email_1', + password: 'new_password', + name: 'New Name', + username: 'new_username' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when username already exists' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'new_email', + password: 'new_password', + name: 'New Name', + username: 'username_1' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when email is empty' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: '', + password: 'new_password', + name: 'New Name', + username: 'new_username' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when password is empty' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'new_email', + password: '', + name: 'New Name', + username: 'new_username' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when name is empty' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'new_email', + password: 'new_password', + name: '', + username: 'new_username' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when username is empty' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'new_email', + password: 'new_password', + name: 'New Name', + username: '' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when email has invalid characters' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'new_email<', + password: 'new_password', + name: 'New Name', + username: 'username' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when password has invalid characters' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'new_email', + password: 'new_password>', + name: 'New Name', + username: 'username' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when name has invalid characters' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'new_email', + password: 'new_password', + name: 'New Name', + username: 'username' + ) + expect(response.status).to eq 400 + expect(response.body).to include '

Error!

' + expect(response.body).to include ( + '

One or more of your inputs was invalid

' + ) + expect(response.body).to include 'Back to homepage' + expect(response.body).to include 'Try again' + end + end + + context 'when username has invalid characters' do + it 'returns 400 Bad Request and html failure page' do + response = post( + '/sign-up', + email: 'new_email', + password: 'new_password', + name: 'New Name', + username: '