diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 1a5ca9f8f1..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,24 +0,0 @@ -Tech Test Submission Requirements/Guidelines -====== - -Before submitting your test, please review the requirements/guidelines belows. Note that the requirements are mandatory and if you do not satisfy them we won't review your code (we don't mean to be harsh but this is based on the minimum expectations that our hiring partners require when you submit code for tech tests). - -Requirements ------- - -* We use [Hound CI](https://houndci.com/) to check for violations to our style guide. When you submit your Pull Request, please then check over and correct everything that Hound has sniffed out that is wrong with your code (unless you feel you really can't do anything to fix it). Once you've fixed Hound errors, push your code again and the Pull Request should update automatically. -* Make sure you have written your own README that briefly explains your approach to solving the challenge. -* If your code isn't finished it's not ideal but acceptable as long as you explain in your README where you got to and how you would plan to finish the challenge. -* All code must be written test-first - we're looking for 100% test coverage or as near as possible to that figure. -* Ensure all your tests are passing. - -Desirable -------- - -* Set up [Travis CI](https://travis-ci.org/) on your own repo and add a [status badge](http://docs.travis-ci.com/user/status-images/) to your README showing that all tests are passing - and make sure it passes our own CI when you submit your PR. - -Guidelines -------- - -* Ensure you've understood the specification and built the code according to the challenge guidelines. -* Read through [Code Reviews :pill:](https://github.com/makersacademy/course/blob/main/pills/code_reviews.md) to understand what we're looking for in your code. diff --git a/Gemfile b/Gemfile index b1a320395a..40c8953c30 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,15 @@ end group :development, :test do gem 'rubocop', '1.20' end + +gem "pg", "~> 1.4" + +gem "sinatra", "~> 3.0" + +gem "sinatra-contrib", "~> 3.0" +gem "webrick", "~> 1.8" +gem "rack-test", "~> 2.1" + +gem "bcrypt", "~> 3.1" + +gem "pony", "~> 1.13" diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..ce47dce196 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,11 +3,39 @@ GEM specs: ansi (1.5.0) ast (2.4.2) + bcrypt (3.1.18) + date (3.3.3) diff-lcs (1.4.4) docile (1.4.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + mini_mime (1.1.2) + multi_json (1.15.0) + mustermann (3.0.0) + ruby2_keywords (~> 0.0.1) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol parallel (1.20.1) parser (3.0.2.0) ast (~> 2.4.1) + pg (1.4.6) + pony (1.13.1) + mail (>= 2.0) + rack (2.2.6.4) + rack-protection (3.0.5) + rack + rack-test (2.1.0) + rack (>= 1.3) rainbow (3.0.0) regexp_parser (2.1.1) rexml (3.2.5) @@ -36,6 +64,7 @@ GEM rubocop-ast (1.11.0) parser (>= 3.0.1.1) ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -46,18 +75,39 @@ GEM terminal-table simplecov-html (0.12.3) simplecov_json_formatter (0.1.3) + sinatra (3.0.5) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.0.5) + tilt (~> 2.0) + sinatra-contrib (3.0.5) + multi_json + mustermann (~> 3.0) + rack-protection (= 3.0.5) + sinatra (= 3.0.5) + tilt (~> 2.0) terminal-table (3.0.1) unicode-display_width (>= 1.1.1, < 3) + tilt (2.1.0) + timeout (0.3.2) unicode-display_width (2.0.0) + webrick (1.8.1) PLATFORMS ruby DEPENDENCIES + bcrypt (~> 3.1) + pg (~> 1.4) + pony (~> 1.13) + rack-test (~> 2.1) rspec rubocop (= 1.20) 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..14e747fa26 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,15 @@ Chitter Challenge ================= -* 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** -* You must submit a pull request to this repo with your code by 10am Monday morning +* This project is the culmination of Makers Academy's 'Web Applications' module (in Ruby) +* The application is built with the Sinatra web framework, rendering view files using ERB +* The application uses a PostgresSQL database for the backend +* The database connection is established in `lib/database_connection.rb` using the `pg` gem +* Seed data for tests is included in `spec/seeds.sql` -Challenge: -------- - -As usual please start by forking this repo. - -We are going to write a small Twitter clone that will allow the users to post messages to a public stream. - -Features: +The Brief: ------- +We were tasked with writing a small Twitter clone that will allow the users to post messages to a public stream, and given the following user stories as our brief: ``` STRAIGHT UP @@ -52,72 +47,61 @@ So that I can stay constantly tapped in to the shouty box of Chitter I want to receive an email if I am tagged in a Peep ``` -Technical Approach: ------ - -In the last two weeks, you integrated a database using the `pg` gem and Repository classes. You also implemented small web applications using Sinatra, RSpec, HTML and ERB views to make dynamic webpages. You can continue to use this approach when building Chitter Challenge. - -You can refer to the [guidance on Modelling and Planning a web application](https://github.com/makersacademy/web-applications/blob/main/pills/modelling_and_planning_web_application.md), to help you in planning the different web pages you will need to implement this challenge. If you'd like to deploy your app to Heroku so other people can use it, [you can follow this guidance](https://github.com/makersacademy/web-applications/blob/main/html_challenges/07_deploying.md). - -If you'd like more technical challenge now, try using an [Object Relational Mapper](https://en.wikipedia.org/wiki/Object-relational_mapping) as the database interface, instead of implementing your own Repository classes. - -Some useful resources: -**Ruby Object Mapper** -- [ROM](https://rom-rb.org/) - -**ActiveRecord** -- [ActiveRecord ORM](https://guides.rubyonrails.org/active_record_basics.html) -- [Sinatra & ActiveRecord setup](https://learn.co/lessons/sinatra-activerecord-setup) - -Notes on functionality: ------- +We were given the following notes on functionality: * You don't have to be logged in to see the peeps. * Makers sign up to chitter with their email, password, name and a username (e.g. samm@makersacademy.com, password123, Sam Morgan, sjmog). * The username and email are unique. * Peeps (posts to chitter) have the name of the maker and their user handle. -* Your README should indicate the technologies used, and give instructions on how to install and run the tests. -Bonus: ------ +Getting Started +---------------------- + +`git clone https://github.com/tomcarmichael/chitter-challenge.git` + +Install dependencies: -If you have time you can implement the following: +`bundle install` -* In order to start a conversation as a maker I want to reply to a peep from another maker. +Create the postgreSQL database by executing the [seed SQL file](./chitter.sql). -And/Or: +Ensure that your postgres server is accessable at the IP address 127.0.0.1. -* Work on the CSS to make it look good. +Start the development server: -Good luck and let the chitter begin! +`rackup` -Code Review ------------ +Access the website in your browser at [localhost:9292](http://localhost:9292/). -In code review we'll be hoping to see: +You should see a homepage empty of content, since there are not yet any posted 'cheeps' (see what I did there?) -* All tests passing -* High [Test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) (>95% is good) -* The code is elegant: every class has a clear responsibility, methods are short etc. +Follow the link to register as a user, you will then be prompted to redirect to login. -Reviewers will potentially be using this [code review rubric](docs/review.md). Referring to this rubric in advance may make the challenge somewhat easier. You should be the judge of how much challenge you want at this moment. +After login, you will be redirected to the home page where you can post a new cheep. Cheeps posted by all users will appear here in reverse chronological order. -Notes on test coverage +If another user's username is included in the cheep prefixed with `@`, the application will detect this as a tagged user, and store a record in the database that the given user was tagged in the given cheep. + +Running the tests: ---------------------- -Please ensure you have the following **AT THE TOP** of your spec_helper.rb in order to have test coverage stats generated -on your pull request: +`rspec` -```ruby -require 'simplecov' -require 'simplecov-console' +All tests should pass with a total code coverage of 98.60% -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::Console, - # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter -]) -SimpleCov.start -``` +A TDD process was employed throughout developing this application. I tried to work with the 'testing pyramid' in mind - starting with a user experience based integration test, and then writing and passing additional, more granular unit tests that led to the integration test finally passing. + +Design and Architecture: +---------------------- -You can see your test coverage when you run your tests. If you want this in a graphical form, uncomment the `HTMLFormatter` line and see what happens! +* This application follows a model, view, controller design pattern. +* In the directory [./lib](./lib), each table in the DB has a corresponding Ruby class defined with a singular version of the name of the table. +* Each table has an additional 'TABLENAME_repository' class with methods that allow for CRUD operations on the DB. +* Route handling occurs in the `Application` class defined in [./app.rb](./app.rb). [./config.ru](./config.ru) executes the code inside of `Application` when `rackup` is run at the command line. +* The database tables have the following relationships: `users` has a one-to-many relationship to `peeps` (synonymous with 'cheeps' - a further iteration would be to rename these uniformly), users and peeps have a many-to-many relationsip with respect to tags, so these are stored in the `tags` join table. +* Given the brief, I began the planning process with an intial digram of how the application could be structured: + ![initial diagram](./chitter-initial-diagram.png) +* The [route recipe](./route_recipe.md) and [schema recipe](./schema_recipe) files were created to aid in design. +* The flow of execution for detecting tags in a cheep and storing these in the DB was conceptualised in this diagram: + ![chitter tags diagram](./chitter-tags-diagram.png) +* Passwords are hashed using the [BCrypt](https://rubygems.org/gems/bcrypt/versions/3.1.12) gem. +* User input to cheeps is santized with basic measures to defend against cross-site scripting injection. In a real-world application I would consider instead using a dedicated and maintained library to provide a more robust defence here and would consider whether the use of the `pg` gem, and existing architecture, is enough to guard against a SQL injection attack. diff --git a/app.rb b/app.rb new file mode 100644 index 0000000000..941505e58b --- /dev/null +++ b/app.rb @@ -0,0 +1,118 @@ +require 'sinatra' +require 'sinatra/reloader' +require_relative 'lib/peep_repository' +require_relative 'lib/tag_repository' +require_relative 'lib/user' +require_relative 'lib/user_repository' +require_relative 'lib/database_connection' + +DatabaseConnection.connect + +class Application < Sinatra::Base + enable :sessions + + configure :development do + register Sinatra::Reloader + also_reload "lib/peep_repository" + also_reload "lib/user_repository" + end + + get '/' do + @all_peeps = PeepRespository.new.all_with_author + return erb(:index) + end + + post '/peep' do + return invalid_params_response if invalid_request_parameters? + message = sanitize_user_input(params[:message]) + author_id = session[:user_id] + timestamp = Time.now.strftime "%Y-%m-%d %H:%M:%S" + + peep_repo = PeepRespository.new + + peep_repo.create(message, timestamp, author_id) + peep_id = peep_repo.most_recent_peep_id + tag_repo = TagRepository.new + + tagged_users = tag_repo.check_message_for_tags(message) + tag_repo.add_tags_by_peep(tagged_users, peep_id) if tagged_users + + return redirect ('/') + end + + get '/login' do + redirect_if_logged_in + + return erb(:login) + end + + post '/login_attempt' do + redirect_if_logged_in + + username = params[:username] + password = params[:password] + + # Returns a hash + login = UserRepository.new.login(params[:username], params[:password]) + + if login[:success?] + session[:user_id] = login[:user_id] + session[:username] = login[:username] + return redirect('/') + else + @failure_reason = login[:failure_reason] + status 401 + return erb(:login_denied) + end + end + + get '/register' do + redirect_if_logged_in + + return erb(:register) + end + + post '/submit_register' do + return invalid_params_response if invalid_request_parameters? + + @username = params[:username] + name = params[:name] + email = params[:email] + password = params[:password] + + registration = UserRepository.new.register(@username, name, email, password) + + return erb(:registration_success) if registration[:success?] + + @failure_reason = registration[:failure_reason] + return erb(:registration_failure) + end + + post '/logout' do + session[:user_id] = nil + session[:username] = nil + redirect('/') + end + + def redirect_if_logged_in + return redirect('/') if session[:user_id] + end + + def sanitize_user_input(string) + string.gsub!(/\&/, '&') + string.gsub!(/\/, '>') + string.gsub!(/\"/, '"') + string.gsub!(/\'/, ''') + return string + end + + def invalid_request_parameters? + params.any? { |_key, value| value.nil? || value == "" } ? true : false + end + + def invalid_params_response + status 400 + return "Invalid form parameters entered, please rety and ensure you fill out all fields." + end +end diff --git a/chitter-initial-diagram.png b/chitter-initial-diagram.png new file mode 100644 index 0000000000..52193f08ee Binary files /dev/null and b/chitter-initial-diagram.png differ diff --git a/chitter-tags-diagram.png b/chitter-tags-diagram.png new file mode 100644 index 0000000000..02f4f145be Binary files /dev/null and b/chitter-tags-diagram.png differ diff --git a/chitter.sql b/chitter.sql new file mode 100644 index 0000000000..4c87d97eae --- /dev/null +++ b/chitter.sql @@ -0,0 +1,32 @@ +-- file: chitter_database.sql + +-- Create the table without the foreign key first. +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name text, + username text, + email text, + password text, + UNIQUE (username, email) +); + +-- Then the table with the foreign key first. +CREATE TABLE peeps ( + id SERIAL PRIMARY KEY, + message text, + posted_at timestamp, +-- The foreign key name is always {other_table_singular}_id + user_id int, + constraint fk_user foreign key(user_id) + references users(id) + on delete cascade +); + +-- Create the join table. +CREATE TABLE tags ( + peep_id int, + user_id int, + constraint fk_peep foreign key(peep_id) references peeps(id) on delete cascade, + constraint fk_user foreign key(user_id) references users(id) on delete cascade, + PRIMARY KEY (peep_id, user_id) +); diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..fe38c7e83f --- /dev/null +++ b/config.ru @@ -0,0 +1,2 @@ +require "./app" +run Application diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 0000000000..92de261b43 --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,37 @@ +require 'pg' + +# This class is a thin "wrapper" around the +# PG library. We'll use it in our project to interact +# with the database using SQL. + +class DatabaseConnection + # This method connects to PostgreSQL using the + # PG gem. We connect to 127.0.0.1, and select + # the database name given in argument. + def self.connect + # If the environment variable (set by Render) + # is present, use this to open the connection. + if ENV['DATABASE_URL'] != nil + @connection = PG.connect(ENV['DATABASE_URL']) + return + end + if ENV['ENV'] == 'test' + database_name = 'chitter_test' + else + database_name = 'chitter' + end + @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) + end + + # This method executes an SQL query + # on the database, providing some optional parameters + # (you will learn a bit later about when to provide these parameters). + 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/peep.rb b/lib/peep.rb new file mode 100644 index 0000000000..c3192c7df9 --- /dev/null +++ b/lib/peep.rb @@ -0,0 +1,3 @@ +class Peep + attr_accessor :id, :message, :posted_at, :user_id, :user +end diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb new file mode 100644 index 0000000000..fca8abbf53 --- /dev/null +++ b/lib/peep_repository.rb @@ -0,0 +1,43 @@ +require_relative "./peep" +require_relative "./user" + +class PeepRespository + def all_with_author # Returns array of peeps in reverse chronological order + peeps = [] + + query_all_rev_chronologically.each do |row| + peep = Peep.new + peep.id = row['id'].to_i + peep.message = row['message'] + peep.posted_at = row['posted_at'] + peep.user_id = row['user_id'].to_i + user = User.new + user.name = row['name'] + user.username = row['username'] + peep.user = user + peeps << peep + end + + return peeps + end + + def create(message, timestamp, author_id) + sql = "INSERT INTO peeps (message, posted_at, user_id) VALUES ($1, $2, $3);" + params = [message, timestamp, author_id] + + DatabaseConnection.exec_params(sql, params) + end + + def most_recent_peep_id + query_all_rev_chronologically.first['id'].to_i + end + + def query_all_rev_chronologically + sql = 'SELECT peeps.id, message, posted_at, name, username + FROM peeps JOIN users ON user_id = users.id + ORDER BY posted_at DESC;' + + return DatabaseConnection.exec_params(sql, []) + end + +end diff --git a/lib/tag.rb b/lib/tag.rb new file mode 100644 index 0000000000..b3136c0e39 --- /dev/null +++ b/lib/tag.rb @@ -0,0 +1,4 @@ +class Tag + attr_accessor :peep_id, :user_id + +end diff --git a/lib/tag_repository.rb b/lib/tag_repository.rb new file mode 100644 index 0000000000..736e34d779 --- /dev/null +++ b/lib/tag_repository.rb @@ -0,0 +1,63 @@ +require_relative "./tag" +require_relative "./user_repository" +require "pony" + + +class TagRepository + def all + sql = "SELECT * FROM tags;" + result_set = DatabaseConnection.exec_params(sql, []) + + tags = [] + + result_set.each do |row| + tag = Tag.new + tag.peep_id = row["peep_id"].to_i + tag.user_id = row["user_id"].to_i + tags << tag + end + + return tags + end + + def add(tag) + sql = "INSERT INTO tags (peep_id, user_id) VALUES ($1, $2);" + params = [tag.peep_id, tag.user_id] + + DatabaseConnection.exec_params(sql, params) + + return nil + end + + def check_message_for_tags(message) + # scan returns an array of all strings like '@sometext' + possible_tags = message.scan(/@\w+(?=[^\w]||$)/) + + # returns an array of user_id's for all matching users + tagged_users = UserRepository.new.check_for_matching(possible_tags) + + return nil if tagged_users.empty? + + return tagged_users + end + + def add_tags_by_peep(tagged_users, peep_id) + tagged_users.each do |user| + tag = Tag.new + tag.peep_id = peep_id + tag.user_id = user.id + # Instead of this, INSERT multiple rows at once? + add(tag) + send_email_confirmation(peep_id, user) + end + end + + def send_email_confirmation(peep_id, user) + # use to gem to send email confirmation + Pony.mail( + :to => "#{user.email}", + :html_body => "Hey @#{user.username}, you were just tagged in a peep! Click here to check it out" + ) + end + +end diff --git a/lib/user.rb b/lib/user.rb new file mode 100644 index 0000000000..9b8f2a11fb --- /dev/null +++ b/lib/user.rb @@ -0,0 +1,3 @@ +class User + attr_accessor :id, :name, :username, :email, :password +end diff --git a/lib/user_repository.rb b/lib/user_repository.rb new file mode 100644 index 0000000000..0119d53429 --- /dev/null +++ b/lib/user_repository.rb @@ -0,0 +1,107 @@ +require_relative "./user" +require 'bcrypt' + +class UserRepository + def all + sql = 'SELECT * FROM users;' + result_set = DatabaseConnection.exec_params(sql, []) + + users = [] + + result_set.each do |row| + user = User.new + user.id = row['id'].to_i + user.name = row['name'] + user.username = row['username'] + user.email = row['email'] + user.password = row['password'] + users << user + end + + return users + end + + def find_by_username(username) + sql = 'SELECT * FROM users WHERE username = $1;' + result = DatabaseConnection.exec_params(sql, [username]).first + + return nil unless result + + user = User.new + user.id = result['id'].to_i + user.username = result['username'] + user.name = result['name'] + user.email = result['email'] + user.password = result['password'] + + return user + end + + def find_by_email(email) + sql = 'SELECT * FROM users WHERE email = $1;' + result = DatabaseConnection.exec_params(sql, [email]).first + + return nil unless result + + user = User.new + user.id = result['id'].to_i + user.username = result['username'] + user.name = result['name'] + user.email = result['email'] + user.password = result['password'] + + return user + end + + def login(username, submitted_password) + user = find_by_username(username) + return { success?: false, failure_reason: "invalid username" } if user.nil? + stored_password = BCrypt::Password.new(user.password) + + + return { success?: true, username: user.username, user_id: user.id } if stored_password == submitted_password + + return { success?: false, failure_reason: "incorrect password" } + end + + def register(username, name, email, password) + + return { success?: false, failure_reason: "username is already taken" } if find_by_username(username) + return { success?: false, failure_reason: "email is already taken" } if find_by_email(email) + + sql = 'INSERT INTO users (username, name, email, password) VALUES ($1, $2, $3, $4);' + + hashed_password = BCrypt::Password.create(password) + params = [username, name, email, hashed_password] + + DatabaseConnection.exec_params(sql, params) + + return { success?: true } + end + + def check_for_matching(possible_tags) #takes and array of strings formatted as '@text' + tagged_users = [] + + return tagged_users if possible_tags.empty? + + if possible_tags.length == 1 + # Use array slicing to remove the @ symbol from the search + user = find_by_username(possible_tags.first[1..-1]) + return tagged_users if user.nil? + tagged_users << user + else + # retrieve an array of all User objects + all_users = all + + possible_tags.each do |possible_tag| + all_users.each do |user| + if possible_tag[1..-1] == user.username + tagged_users << user + end + end + end + end + + return tagged_users + end +end diff --git a/public/chitter-low-resolution.png b/public/chitter-low-resolution.png new file mode 100644 index 0000000000..5d245ca869 Binary files /dev/null and b/public/chitter-low-resolution.png differ diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000000..23335d9184 --- /dev/null +++ b/public/style.css @@ -0,0 +1,110 @@ +body { + font-size: 18x; + font-family: "Futura", Helvetica, Arial, sans-serif; + background-color: #f7f7f7; +} + +/* Style the navigation menu */ +.nav_menu_links { + list-style: none; + display: flex; + justify-content: flex-start; + align-items: center; + margin-right: 2rem; +} + +.nav_menu_links li { + margin-left: 1rem; + display: inline; +} + +.nav_menu_links li a { + text-decoration: none; + color: #1da1f2; + font-weight: bold; +} + +.nav_menu_links li a:hover { + text-decoration: underline; +} + +.link-button { + background: none; + border: none; + color: #1da1f2; + font-weight: bold; + cursor: pointer; +} + +.link-button:hover { + text-decoration: underline; +} + +/* Stle the peeps tabls */ + +table { + margin-left: auto; + margin-right: auto; +} + +td { + text-align: center; +} + +/* Style the peep form */ + +.peep_form { + display: flex; + justify-content: center; + align-items: center; + margin-top: 2rem; + margin-bottom: 2rem; + + margin-left: auto; + margin-right: auto; +} + +.peep_form label { + font-weight: bold; + margin-right: 1rem; + +} + +.peep_form input[type="text"] { + flex-grow: 1; + padding: 0.5rem; + border: 1px solid #ccc; + margin-top: 1rem; + padding: 12px 20px; +} + +.peep_form input[type="submit"] { + background-color: #1da1f2; + color: white; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; +} + +.peep_form input[type="submit"]:hover { + background-color: #0c86d1; +} + +/* Style the different parts of each peep */ + +.peep { + padding: 10px; + border: 2px solid black; + border-radius: 4px; +} + +.author { + color: hwb(0 54% 46%); +} + +.message { +} + +.timestamp { + color: hwb(0 54% 46%); +} \ No newline at end of file diff --git a/route_recipe.md b/route_recipe.md new file mode 100644 index 0000000000..2ba0c6e65c --- /dev/null +++ b/route_recipe.md @@ -0,0 +1,141 @@ +# Page: Homepage + +``` +GET / +``` +## If user logged in + +> Response (200 OK) +> +> Form to create new peep (one single message field) +> +>HTML view with all peeps in rev chronological order (w/ author names and handles) + +## Else +> Response (200 OK) +> +> HTML view with all peeps in rev chronological order (w/ author names and handles) + +## After submitting form to create peep: + + +``` +POST /peep +Params: message +``` +1. Santise user input to protect against javascript and SQL injection - swapping /escaping characters where needed +2. Taking User ID from session object, create a new row in peeps table for peep +3. Check if message included “@“ followed by more text && if the text matches any usernames, add that user and that post to the join table, and send an automated email to each of those users +4. Redirect user to GET / where they should see their new peep + +``` +POST /logout +``` + +>If user not logged in to session +> +>302 redirect +> +>Redirect to GET / +> +>ELSE +> +>Update session objec to logged out +> +>return /confirm_logout view + +# Page: Register + +``` +GET /register +``` + +## If user logged in +> +> Response (302 redirect) +> redirect to GET / + +## Else +> Response (200 OK) +> +>Display HTML form to register + +``` +POST/ submit_register +Params: username, name, email, password +``` +>If user logged in to session object already +> +>Return 302 redirect +> +>Redirect to GET / +> +>ELSE +> +>Sanitise user input +> +>Check if valid email +> +>If username or email already taken, or email invalid +> +>Display /deny_register view +> +>Display reason for denial +> +>Else, Implement password hashing +> +>Create user in database, update session object to logged in and return /confirm_register view + +# Page: Login + +``` +GET /login +``` + +>If user logged in to session object already +> +>Return 302 redirect +> +>Redirect to GET / +> +>ELSE +> +>Display view with HTML form to log in +> +>username and password fields + +``` +POST /login_attempt +params: username, password +``` +>If user logged in to session object already +> +>Return 302 redirect +> +>Redirect to GET / +> +>ELSE +> +>Confirm if username exsits +> +>&& That password matches +> +>If either fails, return /login_denied view with reason for failure +> +>Else, update session object to logged in with correct user ID +> +>Redirect to / + +# OPTIONAL EXTRAS + +# Page: All Peeps by User ID +``` +GET /:user_id +``` +>Display all peeps by user_id + +# Page: All peeps a user is tagged in +``` +GET /tagged/:user_id +``` +>Display all peeps that a given user_id was tagged in diff --git a/schema_recipe.md b/schema_recipe.md new file mode 100644 index 0000000000..72b686a0ac --- /dev/null +++ b/schema_recipe.md @@ -0,0 +1,143 @@ +``` +STRAIGHT UP + +As a Maker +So that I can let people know what I am doing +I want to post a message (peep) to chitter + +As a maker +So that I can see what others are saying +I want to see all peeps in reverse chronological order + +As a Maker +So that I can better appreciate the context of a peep +I want to see the time at which it was made + +As a Maker +So that I can post messages on Chitter as me +I want to sign up for Chitter + +HARDER + +As a Maker +So that only I can post messages on Chitter as me +I want to log in to Chitter + +As a Maker +So that I can avoid others posting messages on Chitter as me +I want to log out of Chitter + +ADVANCED + +As a Maker +So that I can stay constantly tapped in to the shouty box of Chitter +I want to receive an email if I am tagged in a Peep +``` + +Nouns: + +`User`, `name`, `password`, `username (unique)`, `email (unique)` + +`Peep`, `peep posted_at`, peeps need to reference name of Maker and username + +`Tagged users` (in a peep) + + +## 2. Infer the Table Name and Columns + + + +| tables | columns | +| --------------------- | ------------------ | +| users | id, name, username (unique), email(unique), password +| peeps | id, message, posted_at, user_id +| tags | maker_id, post_id + +# TODO + +## 3. Decide the column types. + +[Here's a full documentation of PostgreSQL data types](https://www.postgresql.org/docs/current/datatype.html). + + +``` +# EXAMPLE: + +Table: users +id: SERIAL +name: text +username: text (unique) +email: text (unique) +password: text + +Table: peeps +id: SERIAL +message: text +posted_at: timestamp +user_id: int (id from users table) + +Table: tags +peep_id = int (id from peeps table) +user_id = int (id from users table) + +``` + +## 4. Decide on The Tables Relationship + + +``` +-> A user HAS MANY peeps +-> A peep BELONGS TO a user + +-> Therefore, the foreign key is on the peeps table. +``` + +``` +-> A peep HAS MANY tagged users +-> A tagged user HAS MANY peeps they are tagged in + +-> Therefore peeps and user tags have a MANY to MANY relationship +``` + +## 4. Write the SQL. + +```sql +-- file: chitter_database.sql + +-- Create the table without the foreign key first. +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name text, + username text, + email text, + password text, + UNIQUE (username, email); +); + +-- Then the table with the foreign key first. +CREATE TABLE peeps ( + id SERIAL PRIMARY KEY, + message text, + posted_at timestamp, +-- The foreign key name is always {other_table_singular}_id + user_id int, + constraint fk_user foreign key(user_id) + references users(id) + on delete cascade +); + +-- Create the join table. +CREATE TABLE tags ( + peep_id int, + user_id int, + constraint fk_peep foreign key(peep_id) references peeps(id) on delete cascade, + constraint fk_user foreign key(user_id) references users(id) on delete cascade, + PRIMARY KEY (peep_id, user_id) +); + + +``` + +## 5. Create the tables. + +In TablePlus diff --git a/spec/integration/application_spec.rb b/spec/integration/application_spec.rb new file mode 100644 index 0000000000..991cf7e876 --- /dev/null +++ b/spec/integration/application_spec.rb @@ -0,0 +1,306 @@ +require "spec_helper" +require "rack/test" +require_relative "../../app" +require "user_repository" + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect(host: '127.0.0.1', dbname: 'chitter_test') + connection.exec(seed_sql) +end + +describe Application do + before :each do + reset_tables + end + + include Rack::Test::Methods + + let(:app) { Application.new } + let(:session_params) { { 'rack.session' => { username: "tcarmichael", user_id: 1 } } } + let(:freeze_time) { allow(Time).to receive(:now).and_return(Time.new(2099, 9, 1, 10, 5, 0)) } + + describe "homepage" do + it "displays all peeps in reverse chronological order" do + response = get('/') + expect(response.status).to eq(200) + expect(response.body).to match(/Big Brother is watching you @wsmith[\s\S]*@wsmith & @smhanna - this is jam hot, this is jam hot[\s\S]*We shall meet in the place where there is no darkness/) + end + it "displays the peep author's name and username" do + response = get('/') + expect(response.body).to include("@tcarmichael (Tom Carmichael-Mhanna) wrote:") + expect(response.body).to include("@smhanna (Sarwah Mhanna) wrote:") + end + it "displays the peep's timestamp" do + response = get('/') + expect(response.body).to match(/Posted: 2022-12-19 10:23:54[\s\S]*Posted: 1984-06-15 14:33:00/) + end + it "displays a Chitter logo" do + response = get('/') + expect(response.body).to include("Chitter logo') + expect(response.body).to include('') + expect(response.body).to include('') + end + it "doesn't display a link to login" do + response = get('/', {}, session_params) + expect(response.status).to eq(200) + expect(response.body).not_to include('LoginLogout') + end + end + + context "when the user isn't logged in" do + it "displays a link to login" do + response = get('/') + expect(response.status).to eq(200) + expect(response.body).to include('Login') + end + it "displays a link to register" do + response = get('/') + expect(response.status).to eq(200) + expect(response.body).to include('Register') + end + it "doesn't display the form to create a new peep" do + response = get('/') + expect(response.status).to eq (200) + expect(response.body).not_to include('
') + expect(response.body).not_to include('') + end + end + end + ########################### + describe "POST /peep" do + it "Redirects user to homepage and displays the new peep at the top" do + freeze_time + form_params = { message: "Conspiracy uncovered! The sun is flat." } + response = post('/peep', form_params, session_params) + expect(response.status).to eq(302) + follow_redirect! + expect(last_request.path).to eq('/') + response = get('/') + expect(response.body).to match(/@tcarmichael \(Tom Carmichael-Mhanna\) wrote:[\s\S]*Conspiracy uncovered! The sun is flat.[\s\S]*2099-09-01 10:05:00[\s\S]*Big Brother/) + end + it "sanitizes user input against potentially malicious tags" do + js_rick_roll = '' + form_params = { message: js_rick_roll } + response = post('/peep', form_params, session_params) + expect(response).to be_redirect + follow_redirect! + expect(last_response.body).to include('<script>document.location.href="https://www.youtube.com/watch?v=34Ig3X59_qA";</script>') + end + it "detects a tag within peep and records it in the DB" do + session_params = { 'rack.session' => { username: "smhanna", user_id: 2 } } + form_params = { message: "Hello @tcarmichael" } + response = post('/peep', form_params, session_params) + expect(response.status).to eq(302) + response = get('/') + expect(response.body).to include("Hello @tcarmichael") + tags = TagRepository.new.all + expect(tags.length).to eq 4 + expect(tags.last.peep_id).to eq 4 + expect(tags.last.user_id).to eq 1 + end + it "detects 2 tags within peeps and records them in the DB" do + session_params = { 'rack.session' => { username: "smhanna", user_id: 2 } } + form_params = { message: "Hello @tcarmichael check out the photos from @wsmith!" } + response = post('/peep', form_params, session_params) + expect(response.status).to eq(302) + response = get('/') + expect(response.body).to include("Hello @tcarmichael check out the photos from @wsmith!") + tags = TagRepository.new.all + expect(tags.length).to eq 5 + expect(tags.last.peep_id).to eq 4 + expect(tags.last.user_id).to eq 3 + expect(tags[-1].peep_id).to eq 4 + expect(tags[-1].user_id).to eq 3 + end + + context "given {message: ""}" do + it "returns 400 & error message" do + response = post('/submit_register', { message: "" }, session_params) + expect(response.status).to eq(400) + expect(response.body).to include("Invalid form parameters entered, please rety and ensure you fill out all fields.") + end + end + context "given {message: nil}" do + it "returns 400 & error message" do + response = post('/submit_register', { message: nil }, session_params) + expect(response.status).to eq(400) + expect(response.body).to include("Invalid form parameters entered, please rety and ensure you fill out all fields.") + end + end + end + ######## + describe "POST /logout" do + context "When user is logged in" do + it "logs them out & redirects to GET '/'" do + response = post('/logout', {}, session_params) + expect(response.status).to eq(302) + follow_redirect! + expect(last_request.path).to eq('/') + expect(last_request.env['rack.session'][:user_id]).to be_nil + expect(last_request.env['rack.session'][:username]).to be_nil + end + end + end + ######################## + describe "GET /login" do + context "when the user is logged in" do + it "redirects to '/'" do + response = get('/login', {}, session_params) + expect(response.status).to eq(302) + follow_redirect! + expect(last_request.path).to eq('/') + end + end + context "when the user isn't logged in" do + it "displays a login form" do + response = get('/login') + expect(response.status).to eq 200 + 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 + end + end + ################################# + describe "POST /login_attempt" do + context "if the user is already logged in" do + it "redirects to '/'" do + response = post('/login_attempt', { username: 'jimbob', password: 'abracadabra' }, session_params) + expect(response.status).to eq(302) + follow_redirect! + expect(last_request.path).to eq('/') + end + end + context "if the user isn't logged in" do + context "and provides correct credentials" do + it "logs the user in" do + response = post('/login_attempt', { username: 'wsmith', password: 'bigbrother' }) + expect(response.status).to eq(302) + follow_redirect! + expect(last_request.path).to eq('/') + # Check that the session object has been updated with the user's ID + expect(last_request.env['rack.session'][:user_id]).to eq 3 + expect(last_request.env['rack.session'][:username]).to eq 'wsmith' + end + end + context "and provides incorrect username" do + it "returns an error page" do + response = post('/login_attempt', { username: 'jay_dilla', password: 'bigbrother' }) + expect(response.status).to eq 401 + expect(response.body).to include('Login failed: invalid username') + # Check that the session object has not been updated with user ID + expect(last_request.env['rack.session'][:user_id]).to be_nil + end + end + context "and provides incorrect password" do + it "returns an error page" do + response = post('/login_attempt', { username: 'wsmith', password: '1984' }) + expect(response.status).to eq 401 + expect(response.body).to include('Login failed: incorrect password') + # Check that the session object has not been updated with the user's ID + expect(last_request.env['rack.session'][:user_id]).to be_nil + end + end + end + end + ########################### + describe "GET /register" do + context "if the user is logged in" do + it "redirects to'/'" do + response = get('/register', {}, session_params) + expect(response.status).to eq(302) + follow_redirect! + expect(last_request.path).to eq('/') + end + end + + context "if the user isn't logged in" do + it "displays the registration form" do + response = get('/register') + expect(response.status).to eq(200) + 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 + end + end + ################################ + describe "POST /submit_register" do + context "if username doesn't already exist" do + it "registers user" do + response = post('/submit_register', { name: 'Dave Smith', username: 'prophet5', email: "sequential@circuits.com", password: 'polyphony' }) + expect(response.status).to eq(200) + expect(response.body).to include('Congtratulations @prophet5, you successfully signed up for Chitter!') + expect(response.body).to include("Login here to start Chittering.") + user = UserRepository.new.find_by_username('prophet5') + expect(user).not_to be_nil + expect(user.name).to eq('Dave Smith') + expect(user.email).to eq('sequential@circuits.com') + end + context "if username already exists" do + it "doesn't register user" do + response = post('/submit_register', { name: 'name', username: 'tcarmichael', email: "email@email.com", password: 'password' }) + expect(response.status).to eq(200) + expect(response.body).to include('username is already taken') + expect(response.body).to include("Retry registration") + all_users = UserRepository.new.all + expect(all_users.length).to eq 3 + expect(all_users.first.name).to eq "Tom Carmichael-Mhanna" + expect(all_users.last.name).to eq "Winston Smith" + end + end + context "if email already exists" do + it "doesn't register user" do + response = post('/submit_register', { name: 'name', username: 'username', email: "tomcarmichael@hotmail.co.uk", password: 'password' }) + expect(response.status).to eq(200) + expect(response.body).to include('email is already taken') + expect(response.body).to include("Retry registration") + all_users = UserRepository.new.all + expect(all_users.length).to eq 3 + expect(all_users.first.email).to eq "tomcarmichael@hotmail.co.uk" + expect(all_users.last.name).to eq "Winston Smith" + end + end + context "given {name: nil}" do + it "returns 400 & error message" do + response = post('/submit_register', { name: nil, username: 'fake_username', email: "example@email.com", password: 'password' }) + expect(response.status).to eq(400) + expect(response.body).to include("Invalid form parameters entered, please rety and ensure you fill out all fields.") + end + end + context "given {password: nil}" do + it "returns 400 & error message" do + response = post('/submit_register', { name: nil, username: 'fake_username', email: "example@email.com", password: nil }) + expect(response.status).to eq(400) + expect(response.body).to include("Invalid form parameters entered, please rety and ensure you fill out all fields.") + end + end + context "given {username: ""}" do + it "returns 400 & error message" do + response = post('/submit_register', { name: "dave", username: '', email: "example@email.com", password: "test" }) + expect(response.status).to eq(400) + expect(response.body).to include("Invalid form parameters entered, please rety and ensure you fill out all fields.") + end + end + end + end +end diff --git a/spec/peep_repository_spec.rb b/spec/peep_repository_spec.rb new file mode 100644 index 0000000000..4d8f6fe254 --- /dev/null +++ b/spec/peep_repository_spec.rb @@ -0,0 +1,46 @@ +require "peep_repository" + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect(host: '127.0.0.1', dbname: 'chitter_test') + connection.exec(seed_sql) +end + +describe PeepRespository do + before :each do + reset_tables + end + + let(:repo) { PeepRespository.new } + let(:message) { "Hello, world!" } + let(:timestamp) { "2099-10-10 10:10:10" } + let(:author_id) { author_id = 2 } + + it "Returns all peeps in reverse chronological order with their author's name and tag" do + all_peeps = repo.all_with_author + expect(all_peeps.length).to eq 3 + expect(all_peeps.first.message).to eq "Big Brother is watching you @wsmith" + expect(all_peeps.last.message).to eq "We shall meet in the place where there is no darkness" + expect(all_peeps.last.posted_at).to eq "1984-06-15 14:33:00" + expect(all_peeps[1].user.name).to eq "Tom Carmichael-Mhanna" + expect(all_peeps[1].user.username).to eq "tcarmichael" + end + + it "Creates a new peep" do + repo.create(message, timestamp, author_id) + all_peeps = repo.all_with_author + expect(all_peeps.length).to eq 4 + expect(all_peeps.last.message).to eq "We shall meet in the place where there is no darkness" + expect(all_peeps.first.message).to eq "Hello, world!" + expect(all_peeps.first.posted_at).to eq "2099-10-10 10:10:10" + expect(all_peeps.first.user.name).to eq "Sarwah Mhanna" + expect(all_peeps.first.user.username).to eq "smhanna" + end + + it "returns the ID of the most recent peep" do + expect(repo.most_recent_peep_id).to eq 2 + repo.create(message, timestamp, author_id) + expect(repo.most_recent_peep_id).to eq 4 + end + +end diff --git a/spec/seeds.sql b/spec/seeds.sql new file mode 100644 index 0000000000..8768770c49 --- /dev/null +++ b/spec/seeds.sql @@ -0,0 +1,19 @@ +TRUNCATE TABLE users, peeps, tags RESTART IDENTITY; + +INSERT INTO "public"."users" ("name", "username", "email", "password") VALUES +('Tom Carmichael-Mhanna', 'tcarmichael', 'tomcarmichael@hotmail.co.uk', +'$2a$12$Uq6.MjwYOnRrReg8MER7k.tbst9C8endU7NsqbfPOcJrBv/Vjlaii'), --' ' +('Sarwah Mhanna', 'smhanna', 'sarwah_mhanna@hotmail.com', +'$2a$12$EJbWANsMc/.jLPCm5rccYedi0PqM0qM4TP3UuJyi8I/oin2ctrQtm'), --'password456' +('Winston Smith', 'wsmith', 'orwell.george@aol.com', +'$2a$12$OAMZTYU7QDGVpgQIIYGdteqshFWf9LOuwWVw8RB.4NdX3yeHy.CPC'); --bigbrother + +INSERT INTO "public"."peeps" ("message", "posted_at", "user_id") VALUES +('@wsmith & @smhanna - this is jam hot, this is jam hot', '2022-12-19 10:23:54', 1), +('Big Brother is watching you @wsmith', '2023-03-27 22:05:37', 2), +('We shall meet in the place where there is no darkness', '1984-06-15 14:33:00', 3); + +INSERT INTO "public"."tags" ("peep_id", "user_id") VALUES +(1, 2), +(1, 3), +(2, 3); diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d899..69d9d7c341 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,10 @@ require 'simplecov' require 'simplecov-console' +require 'database_connection' + +ENV['ENV'] = 'test' + +DatabaseConnection.connect SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, diff --git a/spec/tag_repository_spec.rb b/spec/tag_repository_spec.rb new file mode 100644 index 0000000000..39a20215e9 --- /dev/null +++ b/spec/tag_repository_spec.rb @@ -0,0 +1,106 @@ +require "tag_repository" +require "user" + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect(host: '127.0.0.1', dbname: 'chitter_test') + connection.exec(seed_sql) +end + +describe TagRepository do + before :each do + reset_tables + end + + let(:repo) { TagRepository.new } + let(:user_4) { + user = User.new + user.id = 4 + user.name = "Alan Turing" + user.username = "universal_turing_machine" + user.email = "alan@bletchley_park.mod" + user.password = "engima" + user + } + + let(:user_5) { + user = User.new + user.id = 5 + user.name = "Grace Hopper" + user.username = "ghopper" + user.email = "debugger@mail.com" + user.password = "COBOL" + user + } + + it "returns all tags" do + all_tags = repo.all + expect(all_tags.length).to eq 3 + expect(all_tags.last.user_id).to eq 3 + expect(all_tags.last.peep_id).to eq 2 + expect(all_tags[1].user_id).to eq 3 + end + + it "adds a tag to the DB" do + tag = Tag.new + tag.peep_id = 3 + tag.user_id = 1 + repo.add(tag) + all_tags = repo.all + expect(all_tags.length).to eq 4 + expect(all_tags.last.user_id).to eq 1 + expect(all_tags.last.peep_id).to eq 3 + expect(all_tags[1].user_id).to eq 3 + end + + it "finds a single tag within a message and returns the user ID" do + message = "Hello @tcarmichael" + result = repo.check_message_for_tags(message) + expect(result).to be_an_instance_of(Array) + expect(result.length).to eq(1) + expect(result.first).to be_an_instance_of(User) + expect(result.first.email).to eq("tomcarmichael@hotmail.co.uk") + expect(result.first.id).to eq(1) + end + + it "finds two tags within a message and returns the user IDs" do + message = "Hello @tcarmichael check out the photos from @wsmith!" + result = repo.check_message_for_tags(message) + expect(result).to be_an_instance_of(Array) + expect(result.length).to eq(2) + expect(result.first.email).to eq("tomcarmichael@hotmail.co.uk") + expect(result.first.id).to eq(1) + expect(result.last.email).to eq("orwell.george@aol.com") + expect(result.last.id).to eq(3) + end + it "returns nil if we check a message for tags which has none" do + message = "Hello world" + result = repo.check_message_for_tags(message) + expect(result).to eq nil + end + it "returns nil if we check a message for tags which dont match a suer" do + message = "Hello @world!" + result = repo.check_message_for_tags(message) + expect(result).to eq nil + end + context "when adding the tags for a peep to the DB" do + it "adds the only tag specified" do + UserRepository.new.register(user_4.username, user_4.name, user_4.email, user_4.password) + repo.add_tags_by_peep([user_4], 1) + all_tags = repo.all + expect(all_tags.length).to eq 4 + expect(all_tags.last.user_id).to eq 4 + expect(all_tags.last.peep_id).to eq 1 + end + it "adds multiple tags" do + UserRepository.new.register(user_4.username, user_4.name, user_4.email, user_4.password) + UserRepository.new.register(user_5.username, user_5.name, user_5.email, user_5.password) + repo.add_tags_by_peep([user_4, user_5], 1) + all_tags = repo.all + expect(all_tags.length).to eq 5 + expect(all_tags.last.peep_id).to eq 1 + expect(all_tags[-2].user_id).to eq 4 + expect(all_tags.last.user_id).to eq 5 + end + end +end diff --git a/spec/user_repository_spec.rb b/spec/user_repository_spec.rb new file mode 100644 index 0000000000..70a6b0e934 --- /dev/null +++ b/spec/user_repository_spec.rb @@ -0,0 +1,138 @@ +require "user_repository" + +def reset_tables + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect(host: '127.0.0.1', dbname: 'chitter_test') + connection.exec(seed_sql) +end + +describe UserRepository do + before :each do + reset_tables + end + + let(:repo) { UserRepository.new } + + context "when searching by username" do + it "returns the user" do + user = repo.find_by_username('tcarmichael') + expect(user).to be_truthy + expect(user.name).to eq "Tom Carmichael-Mhanna" + expect(user.email).to eq "tomcarmichael@hotmail.co.uk" + end + it "returns nil if user not found" do + user = repo.find_by_username('jay_dilla') + expect(user).not_to be_truthy + end + end + + context "when searching by email" do + it "returns the user" do + user = repo.find_by_email('tomcarmichael@hotmail.co.uk') + expect(user).to be_truthy + expect(user.name).to eq "Tom Carmichael-Mhanna" + expect(user.username).to eq "tcarmichael" + end + end + + let(:register_user) { + repo = UserRepository.new + username = "jsmith" + name = "John Smith" + email = "john@smith.com" + password = "JS123" + repo.register(username, name, email, password) + } + + context "when creating a new user" do + it "adds the user to the DB and indicates success" do + expect(register_user).to eq({ success?: true }) + expect(repo.all.length).to eq 4 + expect(repo.all.last.name).to eq "John Smith" + expect(repo.all.last.email).to eq "john@smith.com" + end + it "stores their password using the BCrypt hashing algorithm" do + register_user + added_user = repo.all.last + stored_password = BCrypt::Password.new(added_user.password) + expect(stored_password).to eq "JS123" + end + + context "if the username already exists" do + it "indicates failure reason & doesn't add the user to the DB" do + registration = UserRepository.new.register("tcarmichael", "name", "email@email.com", "password") + expect(registration).to eq({ success?: false, failure_reason: "username is already taken" }) + expect(repo.all.length).to eq 3 + expect(repo.all.last.name).to eq "Winston Smith" + end + end + + context "if the email already exists" do + it "indicates failure reason & doesn't add the user to the DB" do + registration = UserRepository.new.register("username", "name", "tomcarmichael@hotmail.co.uk", "password") + expect(registration).to eq({ success?: false, failure_reason: "email is already taken" }) + expect(repo.all.length).to eq 3 + expect(repo.all.last.name).to eq "Winston Smith" + end + end + end + + context "when attempting to sign in w incorrect username" do + it "indicates failure" do + expect(repo.login('jazzy_jeff', 'fresh')).to eq({ success?: false, failure_reason: "invalid username" }) + end + end + + context "when attempting to sign in w incorrect password" do + it "indicates failure" do + register_user + expect(repo.login('jsmith', 'js123')).to eq({ success?: false, failure_reason: "incorrect password" }) + end + end + + context "when attempting to sign in w correct credentials" do + it "indicates success & returns username and ID" do + register_user + login_attempt = repo.login('jsmith', "JS123") + expect(login_attempt).to eq({ success?: true, username: "jsmith", user_id: 4 }) + end + it "succeeds for a different user account" do + login_attempt = repo.login('wsmith', "bigbrother") + expect(login_attempt).to eq({ success?: true, username: "wsmith", user_id: 3 }) + end + end + + context "when comparing possible tags with usernames" do + it "returns an empty array if not given any tags" do + possible_tags = [] + result = repo.check_for_matching(possible_tags) + expect(result).to eq([]) + end + it "returns an empty array if the 1 given tag matches no users" do + possible_tags = ['@dave'] + result = repo.check_for_matching(possible_tags) + expect(result).to eq([]) + end + it "returns an empty array if the given tags match no users" do + possible_tags = ['@dave', '@clive', '@derek'] + result = repo.check_for_matching(possible_tags) + expect(result).to eq([]) + end + it "returns the 1 matching user ID" do + possible_tags = ['@tcarmichael', '@dave'] + result = repo.check_for_matching(possible_tags) + expect(result.length).to eq(1) + expect(result.first).to be_an_instance_of(User) + expect(result.first.email).to eq("tomcarmichael@hotmail.co.uk") + expect(result.first.id).to eq(1) + end + it "returns the 2 matching user IDs" do + possible_tags = ['@tcarmichael', '@wsmith'] + result = repo.check_for_matching(possible_tags) + expect(result.length).to eq(2) + expect(result.first.id).to eq 1 + expect(result.last.id).to eq 3 + expect(result.last.email).to eq "orwell.george@aol.com" + end + end +end diff --git a/views/index.erb b/views/index.erb new file mode 100644 index 0000000000..b53e37e1cb --- /dev/null +++ b/views/index.erb @@ -0,0 +1,45 @@ + + + + + +
+ Chitter logo + +
+ + <% if session[:user_id] %> +
+
+
+ + +
+
+ <% end %> + + + <% @all_peeps.each do |peep| %> + + + + <% end %> +
+

@<%= peep.user.username%> (<%= peep.user.name%>) wrote:

+

<%= peep.message %>

+

Posted: <%= peep.posted_at %>

+
+ + diff --git a/views/login.erb b/views/login.erb new file mode 100644 index 0000000000..e5c45531f3 --- /dev/null +++ b/views/login.erb @@ -0,0 +1,15 @@ + + + + + +

Login to Chitter

+
+ + + + + +
+ + diff --git a/views/login_denied.erb b/views/login_denied.erb new file mode 100644 index 0000000000..642b1c6dda --- /dev/null +++ b/views/login_denied.erb @@ -0,0 +1,11 @@ + + + + + +

Login failed: <%= @failure_reason %>

+

+ Retry login +

+ + diff --git a/views/register.erb b/views/register.erb new file mode 100644 index 0000000000..242c9aba3d --- /dev/null +++ b/views/register.erb @@ -0,0 +1,19 @@ + + + + + +

Register for Chitter

+
+ + + + + + + + + +
+ + diff --git a/views/registration_failure.erb b/views/registration_failure.erb new file mode 100644 index 0000000000..4005c9b57f --- /dev/null +++ b/views/registration_failure.erb @@ -0,0 +1,15 @@ + + + + + + +

Registration denied: @<%= @failure_reason %>

+

+ Retry registration +

+ + + + + diff --git a/views/registration_success.erb b/views/registration_success.erb new file mode 100644 index 0000000000..987fb666b2 --- /dev/null +++ b/views/registration_success.erb @@ -0,0 +1,15 @@ + + + + + + +

Congtratulations @<%= @username %>, you successfully signed up for Chitter!

+

+ Login here to start Chittering. +

+ + + + +