diff --git a/Gemfile b/Gemfile index b1a320395a..c205bc57f8 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,10 @@ end group :development, :test do gem 'rubocop', '1.20' end + +gem "pg", "~> 1.5" + +gem "sinatra", "~> 3.0" +gem "sinatra-contrib", "~> 3.0" +gem "webrick", "~> 1.8" +gem "rack-test", "~> 2.1" diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..d530c7aeb9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,9 +5,18 @@ GEM ast (2.4.2) diff-lcs (1.4.4) docile (1.4.0) + 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) + rack (2.2.7) + rack-protection (3.0.6) + rack + rack-test (2.1.0) + rack (>= 1.3) rainbow (3.0.0) regexp_parser (2.1.1) rexml (3.2.5) @@ -36,6 +45,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 +56,37 @@ 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) unicode-display_width (2.0.0) + webrick (1.8.1) PLATFORMS ruby + x86_64-linux DEPENDENCIES + pg (~> 1.5) + 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..a386ba6a2b 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,51 @@ Chitter Challenge ================= +# 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 +A twitter clone, my first complete web application as part of Makers Academy. -Challenge: -------- +This project has been designed to improve our understanding of HTTP requests and responses, designing routes and structuring web applications. It contains: -As usual please start by forking this repo. + - Users who are able to make posts. + - A homepage displaying all posts and their creator and time + - A sign up form to create a new user and start making posts. + - A user specific page in which you can view only their posts. + - Tagging functionality within posts so users can tag other users (with `@username`) which will contain a link to that user's page + - A login page in which a user can login and only post as them + +This was a challenging and enjoyable project. +The most difficult part was implementing a login feature which I first did manually. I attempted this by creating a GET '/:id' route which goes to a specific user's homepage. Once logged in, the main homepage is redirected to a specific page where only they are able to make posts. I did this by redirecting the GET '/' route to the user specific homepage. However this is not secure, since anybody could manually call upon the '/1', for example, and be logged in as user with ID=1. This was an interesting learning experience in understanding and developing new routes which are connected to each other! But not best practise. -We are going to write a small Twitter clone that will allow the users to post messages to a public stream. +So I researched the sessions functionality in Sinatra which allows us to create a session associated with the user's ID number after they login. Then, in the homepage route, we simplt have to check the session[:user_id] value and return a different HTML page if that value is not nil (ie, a user is logged in). -Features: -------- +Going further, I will look into the Bcrypt password encryption -``` -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 +## Process +I first designed the table schema for users and peeps. +Then created and test drove the model and repository classes for both Users and Peeps. These repository classes allow our application to interact with the database through the PG gem and PostgreSQL. +Then I began designing the routes for our sinatra application and test drove those one by one. -As a Maker -So that I can post messages on Chitter as me -I want to sign up for Chitter +## How to use -HARDER +```shell +# Make the DB: +createdb chitter_site -As a Maker -So that only I can post messages on Chitter as me -I want to log in to Chitter +# Install gems: +bundle install -As a Maker -So that I can avoid others posting messages on Chitter as me -I want to log out of Chitter +# Run the tests: +rspec -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 +# Run server +rackup ``` -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: ------- - -* 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: ------ - -If you have time you can implement the following: - -* In order to start a conversation as a maker I want to reply to a peep from another maker. - -And/Or: - -* Work on the CSS to make it look good. - -Good luck and let the chitter begin! - -Code Review ------------ - -In code review we'll be hoping to see: - -* 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. - -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. - -Notes on test coverage ----------------------- - -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: - -```ruby -require 'simplecov' -require 'simplecov-console' - -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::Console, - # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter -]) -SimpleCov.start -``` +## Built with +- Ruby +- Rspec +- Sinatra +- PostgreSQL -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! +## Still to add +- Bcrypt functionality diff --git a/app.rb b/app.rb new file mode 100644 index 0000000000..510ce8a244 --- /dev/null +++ b/app.rb @@ -0,0 +1,193 @@ +require 'sinatra/base' +require 'sinatra/reloader' +require_relative 'lib/database_connection' +require_relative 'lib/user_repository' +require_relative 'lib/peep_repository' +require_relative 'lib/display' + +DatabaseConnection.connect + +class Application < Sinatra::Base + enable :sessions + + configure :development do + register Sinatra::Reloader + end + + get '/' do + peep_repo = PeepRepository.new + user_repo = UserRepository.new + @display = Display.new + @peeps = peep_repo.all_with_users.reverse + + if session[:user_id] == nil + return erb(:index) + else + @user = user_repo.find(session[:user_id]) + return erb(:user_homepage) + end + end + + post '/peep' do + if invalid_peep_parameters? + status 400 + return '' + else + begin + user_exists(params['username']) + post(new_peep) + rescue RuntimeError + @username = params['username'] + status 400 + return erb(:unknown_username) + end + end + end + + get '/users/new' do + return erb(:new_user) + end + + get '/users/:id' do + repo = UserRepository.new + @user = repo.find_with_peeps(params[:id]) + @display = Display.new + @peeps = @user.peeps.reverse + + return erb(:user_page) + end + + post '/user' do + if invalid_user_parameters? + status 400 + return '' + end + sign_up(new_user) + end + + get '/login' do + return erb(:login) + end + + post '/login' do + begin + user_exists(params['username']) + anyone_logged_in + valid_password(params['username'], params['password']) + login + rescue RuntimeError => e + status 400 + return e.message + end + end + + get '/logout' do + logout + end + + private + + def new_peep + peep = Peep.new + peep.content = params['content'] + peep.time = Time.now.strftime("%k:%M") + peep.user_id = find_id(params['username']) + return peep + end + + def new_user + user = User.new + user.email = params['email'] + user.password = params['password'] + user.name = params['name'] + user.username = params['username'] + return user + end + + def post(peep) + peep_repo = PeepRepository.new + user_repo = UserRepository.new + peep_repo.create(peep) + id = session[:user_id] + + @display = Display.new + @peeps = peep_repo.all_with_users.reverse + if id != nil + @user = user_repo.find(id) + return erb(:user_homepage) + end + return erb(:index) + end + + def sign_up(user) + begin + repo = UserRepository.new + repo.create(user) + return erb(:user_created) + rescue RuntimeError => e + @error_message = e.message + status 400 + return erb(:new_user_error) + end + end + + def login + @username = params['username'] + user_id = find_id(@username) + session[:user_id] = user_id + + return erb(:logged_in) + end + + def logout + session[:user_id] = nil + return erb(:logout) + end + + def invalid_peep_parameters? + if [params[:content], params[:username]] + .any? { |input| input.nil? || input.empty? } + return true + end + return false + end + + def invalid_user_parameters? + if [params[:email], params[:password], + params[:name],params[:username]] + .any? { |input| input.nil? || input.empty? } + return true + end + return false + end + + def find_id(username) + repo = UserRepository.new + users = repo.all + user = users.select do |user| + user.username == username + end[0] + + return user.id + end + + def user_exists(username) + repo = UserRepository.new + usernames = repo.all.map { |user| user.username } + fail 'invalid username, go back!' if !usernames.include?(username) + return true + end + + def anyone_logged_in + fail 'A user is already logged in' if session[:user_id] != nil + return false + end + + def valid_password(username, password) + repo = UserRepository.new + user_id = find_id(username) + user = repo.find(user_id) + fail 'Sorry! Incorrect password, please return' if user.password != password + return true + end +end diff --git a/chitter_tables.sql b/chitter_tables.sql new file mode 100644 index 0000000000..1167ad55ae --- /dev/null +++ b/chitter_tables.sql @@ -0,0 +1,19 @@ +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email text, + password text, + name text, + username text +); + +-- Then the table with the foreign key first. +CREATE TABLE peeps ( + id SERIAL PRIMARY KEY, + content text, + time time, +-- 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 +); \ 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/design_recipes/chitter_model_repo_class_design.md b/design_recipes/chitter_model_repo_class_design.md new file mode 100644 index 0000000000..6eabbb61e6 --- /dev/null +++ b/design_recipes/chitter_model_repo_class_design.md @@ -0,0 +1,465 @@ +# Chitter Model and Repository Classes Design Recipe + +## 1. Design and create the Table +DONE + +## 2. Create Test SQL seeds + +```sql + +-- (file: spec/seeds_users.sql) + +-- Write your SQL seed here. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE users RESTART IDENTITY; + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO users (email, password, name, username) + VALUES ('lou@chitter.com', 'password01', 'Louis', 'lpc'); + +INSERT INTO users (email, password, name, username) + VALUES ('luce@chitter.com', 'password02', 'Lucy', 'leh'); + + +-- (file: spec/seeds_peeps.sql) + +TRUNCATE TABLE users RESTART IDENTITY; +TRUNCATE TABLE peeps RESTART IDENTITY; + +INSERT INTO users (email, password, name, username) + VALUES ('lou@chitter.com', 'password01', 'Louis', 'lpc'); + +INSERT INTO peeps (content, time, user_id) + VALUES ('First post', '12:00', 1); + +INSERT INTO peeps (content, time, user_id) + VALUES ('Second post', '13:00', 1); + +``` + +Run this SQL file on the database to truncate (empty) the table, and insert the seed data. Be mindful of the fact any existing records in the table will be deleted. + +```bash +psql -h 127.0.0.1 chitter_site_test < chitter_tables.sql +psql -h 127.0.0.1 chitter_site_test < spec/seeds_users.sql +``` + +## 3. Define the class names + +Usually, the Model class name will be the capitalised table name (single instead of plural). The same name is then suffixed by `Repository` for the Repository class name. + +```ruby +# Table name: users + +# Model class +# (in lib/user.rb) +class User +end + +# Repository class +# (in lib/user_repository.rb) +class UserRepository +end + + +# Table name: peeps +# Model class +# (in lib/peep.rb) +class Peep +end + +# Repository class +# (in lib/peep_repository.rb) +class PeepRepository +end + +``` + +## 4. Implement the Model class + +Define the attributes of your Model class. You can usually map the table columns to the attributes of the class, including primary and foreign keys. + +```ruby +# Table name: users + +# Model class +# (in lib/user.rb) + +class User + # Replace the attributes by your own columns. + attr_accessor :id, :email, :password, :name, :username +end + + +# Table name: peeps + +# Model class +# (in lib/peep.rb) + +class Peep + # Replace the attributes by your own columns. + attr_accessor :id, :content, :time, :user_id, :tags + + def initialize + @tags = [] + end + + def content=(str) + @content = str + @tags = find_tags(str) + end + + private + + # A method for extracting the tags (`@username`) of other users + # from peep content and returns them inside an array + def find_tags(content) + # returns an array of usernames beginning with `@` + end +end + + +``` + +*You may choose to test-drive this class, but unless it contains any more logic than the example above, it is probably not needed.* + +## 5. Define the Repository Class interface + + +```ruby + +# Table name: users + +# Repository class +# (in lib/user_repository.rb) + +class UserRepository + + # Selecting all records + # No arguments + def all + # Executes the SQL query: + # SELECT id, email, password, name, username FROM users; + + # Returns an array of User objects. + end + + # Gets a single record by its ID + # One argument: the id (number) + def find(id) + # Executes the SQL query: + # SELECT id, email, password, name, username FROM users WHERE id = $1; + + # Returns a single User object. + end + + # Adds a new user to the database + # One argument - a User object + def create(user) + # Executes the SQL: + # INSERT INTO users (email, password, name, username) + # VALUES ($1, $2, $3, $4); + + # Returns nothing - updates the database + end + + def find_with_peeps(id) + # Executes the SQL query: + # SELECT id, content, time, user_id FROM users WHERE id = $1; + + # Returns a single user object containing a .peeps attribute + # with an array of Peep objects + end + + # def update(user) + # end + + # def delete(user) + # end +end + + +# Table name: peeps + +# Repository class +# (in lib/peep_repository.rb) + +class PeepRepository + + # Selecting all records + # No arguments + def all + # Executes the SQL query: + # SELECT id, content, time, user_id FROM peeps; + + # Returns an array of Peep objects. + end + + # Gets a single record by its ID + # One argument: the id (number) + def find(id) + # Executes the SQL query: + # SELECT id, content, time, user_id FROM peeps WHERE id = $1; + + # Returns a single Peep object. + end + + # Adds a new peep to the database + # One argument - a Peep object + def create(peep) + # Executes the SQL: + # INSERT INTO peeps (content, time, user_id) + # VALUES ($1, $2, $3); + + # Returns nothing - updates the database + end + + + # def update(peep) + # end + + # def delete(peep) + # end +end +``` + +## 6. Write Test Examples + +Write Ruby code that defines the expected behaviour of the Repository class, following your design from the table written in step 5. + +These examples will later be encoded as RSpec tests. + +```ruby +# USER REPO: + +# 1 +# Get all users + +repo = UserRepository.new + +users = repo.all + +users.length # => 2 + +users[0].id # => 1 +users[0].email # => 'lou@chitter.com' +users[0].password # => 'password01' +users[0].name # => 'Louis' +users[0].username # => 'lpc' + +users[1].id # => 2 +users[1].email # => 'luce@chitter.com' +users[1].password # => 'password02' +users[1].name # => 'Lucy' +users[1].username # => 'leh' + +# 2 +# Get a single user + +repo = UserRepository.new + +user = repo.find(1) + +user.id # => 1 +user.email # => 'lou@chitter.com' +user.password # => 'password01' +user.name # => 'Louis' +user.username # => 'lpc' + +# 3 +# Add a new user to database + +repo = UserRepository.new +user = User.new +user.email = 'newemail@chitter.com' +user.password = 'password03' +user.name = 'Jon' +user.username = 'doe' + +repo.create(user) + +new_user = repo.all.last + +new_user.id # => 3 +new_user.email # => 'newemail@chitter.com' +new_user.password # => 'password03' +new_user.name # => 'Jon' +new_user.username # => 'doe' + +# 4 +# Throws an error when email already in the database +repo = UserRepository.new +user = User.new +user.email = 'lou@chitter.com' +user.password = 'password03' +user.name = 'Jon' +user.username = 'doe' + +repo.create(user) # => error "email already exists" + +# 5 +# Throws an error when username already in the database +repo = UserRepository.new +user = User.new +user.email = 'jonny@chitter.com' +user.password = 'password03' +user.name = 'Jon' +user.username = 'lpc' + +repo.create(user) # => error "Username already exists" + + +# 6 +# Finds specific user and their peeps + +repo = UserRepository.new + +user = repo.find_with_peeps(1) + +user.id # => 1 +user.email # => 'lou@chitter.com' + +peep = user.peeps.first + +peep['content'] # => 'First post' +peep['time'] # => '12:00:00' + +# Add more examples for each method + + +# PEEP MODEL CLASS: + +# 1 +# Initialises with empty tags array + +peep = Peep.new +peep.tags # => [] + +# 2 +# Tags array empty when content contains no tags +peep = Peep.new +peep.content = 'No tags here' + +peep.tags # => [] + +# 3 +# Extracts the tagged users when content has @ in +peep = Peep.new +peep.content = '@jamie is cool' + +peep.tags # => ['@jamie'] + +# 4 +# Extracts the tagged users when content has @ in +# without duplicating +peep = Peep.new +peep.content = '@jamie is cool but @stephen is cooler than @jamie' + +peep.tags # => ['@jamie', '@stephen'] + + +# PEEP REPO: + +# 1 +# Get all peeps + +repo = PeepRepository.new + +peeps = repo.all + +peeps.length # => 2 + +peeps[0].id # => 1 +peeps[0].content # => 'First post' +peeps[0].time # => '12:00:00' +peeps[0].user_id # => 1 + +peeps[1].id # => 1 +peeps[1].content # => 'Second post' +peeps[1].time # => '13:00:00' +peeps[1].user_id # => 1 + +# 2 +# Get a single peep + +repo = PeepRepository.new + +peep = repo.find(1) + +peep.id # => 1 +peep.content # => 'First post' +peep.time # => '12:00:00' +peep.user_id # => 1 + +# 3 +# Add a new peep to database + +repo = PeepRepository.new +peep = Peep.new +peep.content = 'New post' +peep.time = '15:00' +peep.user_id = 1 + +repo.create(peep) + +new_peep = repo.all.last + +peep.id # => 3 +peep.content # => 'New post' +peep.time # => '15:00:00' +peep.user_id # => 1 + +``` + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +Running the SQL code present in the seed file will empty the table and re-insert the seed data. + +This is so you get a fresh table contents every time you run the test suite. + +```ruby +# EXAMPLE + +# file: spec/user_repository_spec.rb + +def reset_users_table + seed_sql = File.read('spec/seeds_users.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_site_test' }) + connection.exec(seed_sql) +end + +def reset_peeps_table + seed_sql = File.read('spec/seeds_peeps.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_site_test' }) + connection.exec(seed_sql) +end + +RSpec.describe UserRepository do + before(:each) do + reset_users_table + end + + # (your tests will go here). +end + + +RSpec.describe PeepRepository do + before(:each) do + reset_peeps_table + end + + # (your tests will go here). +end +``` + +## 8. Test-drive and implement the Repository class behaviour + +_After each test you write, follow the test-driving process of red, green, refactor to implement the behaviour._ diff --git a/design_recipes/chitter_tables_design.md b/design_recipes/chitter_tables_design.md new file mode 100644 index 0000000000..8e1c58d4d1 --- /dev/null +++ b/design_recipes/chitter_tables_design.md @@ -0,0 +1,129 @@ +## 1. Extract nouns from the user stories or specification + +``` +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 +``` + +``` +Nouns: + +message (peep), time, +``` + +## 2. Infer the Table Name and Columns + +Put the different nouns in this table. Replace the example with your own nouns. + +| Record | Properties | +| --------------------- | ------------------ | +| users | email, password, +| | name, username +| peeps | content, time, + user_id, tags(?) + +1. Name of the first table (always plural): `users` + Column names: `email`, `password`, `name`, `username` + +2. Name of the second table (always plural): `peeps` + Column names: `content`, `time`, `user_id` + + +## 3. Decide the column types. +``` +Table: users +id: SERIAL +email: text +password: text +name: text +username: text + +Table: peeps +id: SERIAL +content: text +time: time +user_id: int + +``` + +## 4. Decide on The Tables Relationship + +Most of the time, you'll be using a **one-to-many** relationship, and will need a **foreign key** on one of the two tables. + +To decide on which one, answer these two questions: + +1. Can one [TABLE ONE] have many [TABLE TWO]? (Yes/No) +2. Can one [TABLE TWO] have many [TABLE ONE]? (Yes/No) + +You'll then be able to say that: + +1. **[A] has many [B]** +2. And on the other side, **[B] belongs to [A]** +3. In that case, the foreign key is in the table [B] + +Replace the relevant bits in this example with your own: + +``` +# EXAMPLE + +1. Can one user have many peeps? YES +2. Can one peep have many users? NO + +-> Therefore, +-> An user HAS MANY peeps +-> An peep BELONGS TO a user + +-> Therefore, the foreign key is on the peeps table. +``` + +*If you can answer YES to the two questions, you'll probably have to implement a Many-to-Many relationship, which is more complex and needs a third table (called a join table).* + +## 4. Write the SQL. + +```sql +-- EXAMPLE +-- file: chitter_tables.sql + +-- Replace the table name, columm names and types. + +-- Create the table without the foreign key first. +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email text, + password text, + name text, + username text +); + +-- Then the table with the foreign key first. +CREATE TABLE peeps ( + id SERIAL PRIMARY KEY, + content text, + time time, +-- 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 +); + +``` + +## 5. Create the tables. + +```bash +psql -h 127.0.0.1 chitter_site < chitter_tables.sql +``` diff --git a/design_recipes/get_new_user_route_design.md b/design_recipes/get_new_user_route_design.md new file mode 100644 index 0000000000..4c3e443755 --- /dev/null +++ b/design_recipes/get_new_user_route_design.md @@ -0,0 +1,88 @@ + +## 1. Design the Route Signature + +You'll need to include: + * the HTTP method: GET + * the path: '/new_user' + * no parameters + +## 2. Design the Response + +`200 OK` + +```html + +Form for creating a new user: + + + Sign up! + + +

Welcome to Chitter!

+

Fill in your details to sign up:

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +``` + +## 3. Write Examples + +``` +# Request: + +GET /new_user +parameters: none + +# Expected response: + +Response for 200 OK + +erb(:new_user) + +## 4. Encode as Tests Examples + +```ruby + +# file: spec/integration/application_spec.rb + +require "spec_helper" + +describe Application do + include Rack::Test::Methods + + let(:app) { Application.new } + + context "GET /new_user" do + it 'returns 200 OK with the sign up form' do + response = get('/new_user') + + expect(response.status).to eq(200) + + expect(response.body).to include('

Welcome to Chitter!

') + expect(response.body).to include('
') + end + end +end +``` + +## 5. Implement the Route + +Write the route and web server code to implement the route behaviour. \ No newline at end of file diff --git a/design_recipes/get_user_route design.md b/design_recipes/get_user_route design.md new file mode 100644 index 0000000000..290284ed0c --- /dev/null +++ b/design_recipes/get_user_route design.md @@ -0,0 +1,127 @@ +# GET user/:id Route Design Recipe + +_Copy this design recipe template to test-drive a Sinatra route._ + +## 1. Design the Route Signature + +You'll need to include: + * the HTTP method: GET + * the path: '/user/:id' + * any query parameters: id number of a user + +## 2. Design the Response + +The route might return different responses, depending on the result. + +Get user route for a specific user (by its ID) will return `200 OK` if the user exists, but `404 Not Found` if the user is not found in the database. + + +```html + + + + + + +

Username

+
Name, email
+

Their peeps:

+
content1
+
time
+ Back to home page + + +``` + +```html + + + + + + +

Sorry!

+
We couldn't find this user. Have a look at the home page
+ + +``` + +## 3. Write Examples + +_Replace these with your own design._ + +``` +# Request: + +GET /users/1 + +# Expected response: + +Response for 200 OK +``` + + + + lpc + + +

lpc

+
Louis, lou@chitter.com
+

Peeps:

+
First post

+
- 12:00:00
+

+
Second post

+
- 13:00:00
+ Back to home page + + + + +``` +# Request: + +GET /users/276278 + +# Expected response: + +Response for 404 Not Found +``` + +## 4. Encode as Tests Examples + +```ruby +# EXAMPLE +# file: spec/integration/application_spec.rb + +require "spec_helper" + +describe Application do + include Rack::Test::Methods + + let(:app) { Application.new } + + context "GET /users/:id" do + it 'returns 200 OK' do + # Assuming the user with id 1 exists. + response = get('/users/1') + + expect(response.status).to eq(200) + expect(response.body).to include('

lpc

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

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

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

') + end + + it 'returns 404 Not Found' do + response = get('/user/276278') + + expect(response.status).to eq(404) + end + end +end +``` + +## 5. Implement the Route + +Write the route and web server code to implement the route behaviour. diff --git a/design_recipes/home_page_route_design.md b/design_recipes/home_page_route_design.md new file mode 100644 index 0000000000..246bb73103 --- /dev/null +++ b/design_recipes/home_page_route_design.md @@ -0,0 +1,104 @@ +# GET '/' Route Design Recipe + +## 1. Design the Route Signature + +You'll need to include: + * the HTTP method: GET + * the path: '/' + * No parameters + +## 2. Design the Response + + +```html + + + + + Chitter home + + +

Chitter

+
+ Name (username) says: +
+
+ peep content +
+
+ - 12:45 +
+ + +``` + + +## 3. Write Examples + +_Replace these with your own design._ + +``` +# Request: + +GET / + +# Expected response: + +Response for 200 OK + +``` + + + Chitter home + + +

Chitter

+
+ Name (username) says: +
+
+ peep content +
+
+ - 12:45 +
+ + + +``` + +## 4. Encode as Tests Examples + +```ruby + +# file: spec/integration/app_spec.rb + +require "spec_helper" + +describe Application do + include Rack::Test::Methods + + let(:app) { Application.new } + + + context 'GET /' do + it 'lists all peeps with names' do + response = get('/') + + expect(response.status).to eq 200 + + expect(response.body).to include('

Chitter

') + expect(response.body).to include('Louis (lpc) says:') + expect(response.body).to include('First post') + expect(response.body).to include('- 12:00:00') + expect(response.body).to include('Louis (lpc) says:') + expect(response.body).to include('Second post') + expect(response.body).to include('- 13:00:00') + end + end +end +``` + +## 5. Implement the Route + +Write the route and web server code to implement the route behaviour. diff --git a/design_recipes/post_peep_route_design.md b/design_recipes/post_peep_route_design.md new file mode 100644 index 0000000000..f9d676360e --- /dev/null +++ b/design_recipes/post_peep_route_design.md @@ -0,0 +1,115 @@ + +## 1. Design the Route Signature + +You'll need to include: + * the HTTP method: POST + * the path: '/peep' + * Body parameters: content and username (strings) + +## 2. Design the Response + +`200 OK` as long as the username or email are not taken +```html + + +The home page body - erb(:home) +``` + +```html + + + + + + +

Sorry!

+
This username does not exist...
+
Please sign up first
+
Or return to the home page
+ + +``` + +## 3. Write Examples + +``` +# Request: + +POST /peep +parameters: +content: "This is a new post" +username: "lpc" + +# Expected response: + +Response for 200 OK +body erb(:home) +``` +# Request: + +POST /peep +parameters: +content: "This is a new post" +username: "unknown" + +# Expected response: + +Response for 400 (No such user): + + + + +

Sorry!

+
This username does not exist...
+
Please sign up first
+
Or return to the home page
+ + + +``` + +## 4. Encode as Tests Examples + +```ruby +# EXAMPLE +# file: spec/integration/application_spec.rb + +require "spec_helper" + +describe Application do + include Rack::Test::Methods + + let(:app) { Application.new } + + context "POST /peep" do + it 'returns 200 OK with valid username' do + # Assuming the username already exisis + response = post( + '/peep', + content: "New peep posted", + username: "lpc" + ) + + expect(response.status).to eq(200) + expect(response.body).to include(
Louis (lpc) says:
) + expect(response.body).to include(
New peep posted
) + end + + it 'returns 400 with invalid username' do + response = post( + '/peep', + content: "New peep posted", + username: "unknown" + ) + + expect(response.status).to eq 400 + expect(response.body).to include('
This username does not exist...
') + expect(response.body).to include('
Please sign up first
') + end + end +end +``` + +## 5. Implement the Route + +Write the route and web server code to implement the route behaviour. \ No newline at end of file diff --git a/design_recipes/post_user_route_design.md b/design_recipes/post_user_route_design.md new file mode 100644 index 0000000000..1670edc3f5 --- /dev/null +++ b/design_recipes/post_user_route_design.md @@ -0,0 +1,177 @@ + +## 1. Design the Route Signature + +You'll need to include: + * the HTTP method: POST + * the path: '/user' + * Body parameters: + email, password, name, username (all strings) + +## 2. Design the Response + +`200 OK` + +```html + + + + +

Sign up complete!

+
Now you're all ready to start sharing fascinating peeps
+
Return to the home page to make your first peep
+ + + + + + + +

Uh oh!

+
Sorry, this email already exists
+
Please try signing up again with a unique email
+
Or return to the home page
+ + + + + + + + +

Uh oh!

+
Sorry, this username already exists
+
Please try signing up again with a unique username
+
Or return to the home page
+ + + +``` + +## 3. Write Examples + +# Request: + +POST /user +parameters: +email: email@example.com, +password: password +name: Jimmy +username: jm123 + +# Expected response: + +Response for 200 OK +```html +erb(:user_created): + + + +

Sign up complete!

+
Now you're all ready to start sharing fascinating peeps
+
Return to the home page to make your first peep
+ + +``` + +# Request +POST /user +parameters: +email: lou@chitter.com, +password: password +name: Jimmy +username: jm123 + + +# Expected response: + +Response status 500, as email taken: +body + + + +

Sorry!

+
Your email is already taken
+
Please try signing up again with a unique email
+
Or return to the home page
+ + + +# Request +POST /user +parameters: +email: email@example.com, +password: password +name: Jimmy +username: lpc + +# Expected response: + +Response status 500, as username taken: +body + + + +

Sorry!

+
Your username is already taken
+
Please try signing up again with a unique username
+
Or return to the home page
+ + + + +## 4. Encode as Tests Examples + +```ruby + +# file: spec/integration/application_spec.rb + +require "spec_helper" + +describe Application do + include Rack::Test::Methods + + let(:app) { Application.new } + + context "POST /user" do + it 'returns 200 OK with valid email and username' do + response = post( + '/user', + email: 'email@example.com', + password: 'password', + name: 'Jimmy', + username: 'jm123') + + expect(response.status).to eq(200) + expect(response.body).to include('

Sign up complete!

') + end + + it 'returns 500 when email taken' do + response = post( + '/user', + email: 'lou@chitter.com', + password: 'password', + name: 'Jimmy', + username: 'jm123') + + expect(response.status).to eq(500) + expect(response.body).to include('
Your email is already taken
') + end + + it 'returns 500 when username taken' do + response = post( + '/user', + email: 'lou@chitter.com', + password: 'password', + name: 'Jimmy', + username: 'jm123') + + expect(response.status).to eq(500) + expect(response.body).to include('
Your username is already taken
') + end + end +end +``` + +## 5. Implement the Route + +Write the route and web server code to implement the route behaviour. \ No newline at end of file diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 0000000000..709ada30ed --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,36 @@ +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 ENV['DATABASE_URL'] != nil + @connection = PG.connect(ENV['DATABASE_URL']) + return + end + + if ENV['ENV'] == 'test' + database_name = 'chitter_site_test' + else + database_name = 'chitter_site' + 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/display.rb b/lib/display.rb new file mode 100644 index 0000000000..583615e392 --- /dev/null +++ b/lib/display.rb @@ -0,0 +1,52 @@ +require_relative 'peep_repository' +require_relative 'user_repository' + +class Display + def peep(peep) + content = content_with_links(peep) + str = " + #{peep.name} (#{link(peep.username)}) says:
+ #{content}
+ - #{peep.time} +
+ " + end + + def user(user) + str = " +

#{user.username}

+
#{user.name}, #{user.email}
+
+ " + end + + private + + # returns an html link to the user's page + def link(username) + repo = UserRepository.new + user = repo.all.select { |user| user.username == username }[0] + id = user.id + str = "#{username}" + end + + def tag_to_link(tag) + return tag unless username_exists?(tag[1..]) + username = tag[1..] + return link(username) + end + + def content_with_links(peep) + content = peep.content + peep.tags.each do |tag| + content.gsub!(tag, tag_to_link(tag)) + end + return content + end + + def username_exists?(username) + repo = UserRepository.new + usernames = repo.all.map { |user| user.username } + return usernames.include?(username) + end +end diff --git a/lib/peep.rb b/lib/peep.rb new file mode 100644 index 0000000000..5f4043c6b2 --- /dev/null +++ b/lib/peep.rb @@ -0,0 +1,18 @@ +class Peep + attr_accessor :id, :content, :time, :user_id, :tags, + :username, :name + + def content=(str) + @content = str + @tags = find_tags(str) + end + + private + + def find_tags(content) + content_arr = content.split + content_arr.select do |string| + string[0] == '@' && string != '@' + end.uniq + end +end diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb new file mode 100644 index 0000000000..5afdfe1333 --- /dev/null +++ b/lib/peep_repository.rb @@ -0,0 +1,65 @@ +require_relative 'peep' +require_relative 'database_connection' + +class PeepRepository + def all + sql = 'SELECT id, content, time, user_id FROM peeps;' + result_set = DatabaseConnection.exec_params(sql, []) + peeps = [] + result_set.each do |record| + peeps << record_to_peep_object(record) + end + + return peeps + end + + def find(id) + sql = 'SELECT id, content, time, user_id FROM peeps + WHERE id = $1;' + result_set = DatabaseConnection.exec_params(sql, [id]) + + return record_to_peep_object(result_set[0]) + end + + def create(peep) + sql = 'INSERT INTO peeps (content, time, user_id) + VALUES ($1, $2, $3);' + params = [ + peep.content, + peep.time, + peep.user_id + ] + DatabaseConnection.exec_params(sql, params) + return nil + end + + def all_with_users + sql = 'SELECT peeps.id, peeps.content, + peeps.time, peeps.user_id, + users.name, users.username + FROM peeps JOIN users + ON peeps.user_id = users.id;' + + result_set = DatabaseConnection.exec_params(sql, []) + peeps = [] + + result_set.each do |record| + peep = record_to_peep_object(record) + peep.name = record['name'] + peep.username = record['username'] + peeps << peep + end + return peeps + end + + private + + def record_to_peep_object(record) + peep = Peep.new + peep.id = record['id'].to_i + peep.content = record['content'] + peep.time = record['time'] + peep.user_id = record['user_id'].to_i + return peep + end +end diff --git a/lib/user.rb b/lib/user.rb new file mode 100644 index 0000000000..012a6e26af --- /dev/null +++ b/lib/user.rb @@ -0,0 +1,6 @@ +class User + attr_accessor :id, :email, :password, :name, :username, :peeps + def initialize + @peeps = [] + end +end diff --git a/lib/user_repository.rb b/lib/user_repository.rb new file mode 100644 index 0000000000..85f7aa0e08 --- /dev/null +++ b/lib/user_repository.rb @@ -0,0 +1,83 @@ +require_relative 'user' +require_relative 'peep' +require_relative 'database_connection' + +class UserRepository + def all + sql = 'SELECT * FROM users;' + result_set = DatabaseConnection.exec_params(sql, []) + users = [] + result_set.each do |record| + users << record_to_user_object(record) + end + + return users + end + + def find(id) + sql = 'SELECT * FROM users WHERE id = $1;' + result_set = DatabaseConnection.exec_params(sql, [id]) + user = record_to_user_object(result_set[0]) + + return user + end + + def create(user) + fail "email already exists" if email_taken?(user.email) + fail "username already exists" if username_taken?(user.username) + + sql = 'INSERT INTO users (email, password, name, username) + VALUES ($1, $2, $3, $4);' + params = [user.email, user.password, + user.name, user.username] + + DatabaseConnection.exec_params(sql, params) + return nil + end + + def find_with_peeps(id) + sql = 'SELECT users.id, users.email, + users.password, users.name, + users.username, + peeps.id AS peep_id, + peeps.content, peeps.time + FROM users JOIN peeps + ON peeps.user_id = users.id + WHERE users.id = $1;' + result_set = DatabaseConnection.exec_params(sql, [id]) + + user = record_to_user_object(result_set[0]) + + result_set.each do |record| + peep = Peep.new + peep.id = record['peep_id'].to_i + peep.content = record['content'] + peep.time = record['time'] + user.peeps << peep + end + + return user + end + + private + + def record_to_user_object(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 + + def email_taken?(email) + used_emails = all.map { |user| user.email } + return used_emails.include?(email) + end + + def username_taken?(username) + used_usernames = all.map { |user| user.username } + return used_usernames.include?(username) + end +end diff --git a/spec/formatter_spec.rb b/spec/formatter_spec.rb new file mode 100644 index 0000000000..7090f5cb8e --- /dev/null +++ b/spec/formatter_spec.rb @@ -0,0 +1,48 @@ +require 'display' + +RSpec.describe Display do + before(:each) do + reset_tables + end + + it 'formats a peep into a nice post' do + peep = double( + :peep, + content: 'First post', + time: '12:00:00', + user_id: 1, + name: 'Louis', + username: 'lpc', + tags: [] + ) + display = Display.new + result = display.peep(peep) + + expected_result = " + Louis (lpc) says:
+ First post
+ - 12:00:00 +
+ " + + expect(result).to eq expected_result + end + + it 'formats a user into a display of their details' do + user = double( + :user, + email: 'lou@chitter.com', + name: 'Louis', + username: 'lpc' + ) + display = Display.new + result = display.user(user) + expected_result = " +

lpc

+
Louis, lou@chitter.com
+
+ " + + expect(result).to eq(expected_result) + end +end diff --git a/spec/integration/app_spec.rb b/spec/integration/app_spec.rb new file mode 100644 index 0000000000..c1b897c5d9 --- /dev/null +++ b/spec/integration/app_spec.rb @@ -0,0 +1,213 @@ +require 'spec_helper' +require 'rack/test' +require_relative '../../app' + +RSpec.describe Application do + before(:each) do + reset_tables + end + + include Rack::Test::Methods + + let(:app) { Application.new } + + context 'GET /' do + it 'lists all peeps with names' do + response = get('/') + + expect(response.status).to eq 200 + expect(response.body).to include('

Chitter

') + expect(response.body).to include('Sign up now!') + expect(response.body).to include("Louis (lpc) says:") + expect(response.body).to include('First post') + expect(response.body).to include('- 12:00:00') + expect(response.body).to include("Louis (lpc) says:") + expect(response.body).to include('Second post') + expect(response.body).to include('- 13:00:00') + end + + it 'returns 200 OK when user logged in' do + session = { :user_id => 1 } + response = get('/', {}, 'rack.session' => session) + + expect(response.status).to eq(200) + expect(response.body).to include('

Welcome lpc!

') + expect(response.body).to include('Logout here!') + expect(response.body).to include('

Make a new peep!

') + expect(response.body).to include('First post') + # And has a form for making a peep + expect(response.body).to include('') + expect(response.body).to include('') + end + end + + context "POST /peep" do + it 'returns 200 OK with valid username' do + response = post( + '/peep', + content: 'New peep posted', + username: 'lpc' + ) + + expect(response.status).to eq(200) + expect(response.body).to include('New peep posted') + + response = get('/') + expect(response.body).to include('New peep posted') + end + + it 'returns 400 with invalid username' do + response = post( + '/peep', + content: "New peep posted", + username: "unknown" + ) + + expect(response.status).to eq 400 + expect(response.body).to include('
This user (unknown) does not exist...
') + expect(response.body).to include('
Please sign up first
') + end + + it 'returns 400 and no response when parameters not valid' do + response = post( + '/peep', + fake_content: "New peep posted", + bad_param: "unknown" + ) + + expect(response.status).to eq 400 + expect(response.body).to eq '' + end + end + + context "GET /users/new" do + it 'returns 200 OK with the sign up form' do + response = get('/users/new') + + expect(response.status).to eq(200) + expect(response.body).to include('

Welcome to Chitter!

') + expect(response.body).to include('') + end + end + + context "POST /user" do + it 'returns 200 OK with valid email and username' do + response = post( + '/user', + email: 'email@example.com', + password: 'password', + name: 'Jimmy', + username: 'jm123') + + expect(response.status).to eq(200) + expect(response.body).to include('

Sign up complete!

') + end + + it 'returns 400 when email taken' do + response = post( + '/user', + email: 'lou@chitter.com', + password: 'password', + name: 'Jimmy', + username: 'jm123') + + expect(response.status).to eq(400) + expect(response.body).to include('
Sorry, this email already exists
') + end + + it 'returns 400 when username taken' do + response = post( + '/user', + email: 'example@email.com', + password: 'password', + name: 'Jimmy', + username: 'lpc') + + expect(response.status).to eq(400) + expect(response.body).to include('
Sorry, this username already exists
') + end + + it 'returns 400 and no response when parameters not valid' do + response = post( + '/user', + not_email: 'example@email.com', + random_param: 1234, + no_name: 'Jimmy', + or_username: 'lpc') + + expect(response.status).to eq 400 + expect(response.body).to eq '' + end + end + + context "GET /users/:id" do + it 'returns 200 OK' do + response = get('/users/1') + + expect(response.status).to eq(200) + expect(response.body).to include('

lpc

') + expect(response.body).to include('
First post
') + expect(response.body).to include('
Second post
') + expect(response.body).to include('
- 13:00:00
') + end + + it 'returns 404 Not Found' do + response = get('/user/276278') + + expect(response.status).to eq(404) + end + end + + context 'GET /login' do + it 'returns a form for logging in' do + response = get('/login') + + expect(response.status).to eq(200) + expect(response.body).to include('

Login to chitter!

') + expect(response.body).to include('') + expect(response.body).to include('') + end + end + + context 'POST /login' do + it 'logs in a user with correct username and password' do + response = post('/login', username: 'lpc', password: 'password01') + + expect(response.status).to eq(200) + expect(response.body).to include('

Now logged in as lpc

') + end + + it 'points out when username is not in the DB' do + response = post('/login', username: 'lou', password: 'password01') + + expect(response.status).to eq(400) + expect(response.body).to eq('invalid username, go back!') + end + + it 'points out when a user is already logged in' do + response = post('/login', username: 'lpc', password: 'password01') + response = post('/login', username: 'lpc', password: 'password01') + + expect(response.status).to eq(400) + expect(response.body).to eq('A user is already logged in') + end + + it 'points out password not matching' do + response = post('/login', username: 'lpc', password: 'password05') + + expect(response.status).to eq 400 + expect(response.body).to eq ('Sorry! Incorrect password, please return') + end + end + + context 'GET /logout' do + it '200 OK and logs the user out' do + response = post('/login', username: 'lou', password: 'password01') + response = get('/logout') + + expect(response.status).to eq(200) + expect(response.body).to include('

You have logged out!

') + expect(response.body).to include('
Return to the home page
') + end + end +end diff --git a/spec/peep_repository_spec.rb b/spec/peep_repository_spec.rb new file mode 100644 index 0000000000..68e314ee19 --- /dev/null +++ b/spec/peep_repository_spec.rb @@ -0,0 +1,69 @@ +require 'peep_repository' +require 'peep' + +RSpec.describe PeepRepository do + before(:each) do + reset_tables + end + + it 'returns an array of all peeps' do + repo = PeepRepository.new + peeps = repo.all + + expect(peeps.length).to eq 2 + + expect(peeps[0].id).to eq 1 + expect(peeps[0].content).to eq 'First post' + expect(peeps[0].time).to eq '12:00:00' + expect(peeps[0].user_id).to eq 1 + + expect(peeps[1].id).to eq 2 + expect(peeps[1].content).to eq 'Second post' + expect(peeps[1].time).to eq '13:00:00' + expect(peeps[1].user_id).to eq 1 + end + + it 'finds a single peep' do + repo = PeepRepository.new + + peep = repo.find(1) + + expect(peep.id).to eq 1 + expect(peep.content).to eq 'First post' + expect(peep.time).to eq '12:00:00' + expect(peep.user_id).to eq 1 + end + + it 'adds a new peep to the database' do + repo = PeepRepository.new + peep = Peep.new + peep.content = 'New post' + peep.time = '15:00' + peep.user_id = 1 + + repo.create(peep) + + new_peep = repo.all.last + + expect(new_peep.id).to eq 3 + expect(new_peep.content).to eq 'New post' + expect(new_peep.time).to eq '15:00:00' + expect(new_peep.user_id).to eq 1 + end + + it "finds all peeps with user's details" do + repo = PeepRepository.new + peeps = repo.all_with_users + + expect(peeps.length).to eq 2 + expect(peeps[0].id).to eq 1 + expect(peeps[0].content).to eq 'First post' + expect(peeps[0].time).to eq '12:00:00' + expect(peeps[0].user_id).to eq 1 + + expect(peeps[0].name).to eq 'Louis' + expect(peeps[0].username).to eq 'lpc' + expect(peeps[1].name).to eq 'Louis' + expect(peeps[1].username).to eq 'lpc' + end +end diff --git a/spec/peep_spec.rb b/spec/peep_spec.rb new file mode 100644 index 0000000000..dd58b82227 --- /dev/null +++ b/spec/peep_spec.rb @@ -0,0 +1,29 @@ +require 'peep' + +RSpec.describe Peep do + it 'tags is nil until content defined' do + peep = Peep.new + expect(peep.tags).to eq nil + end + + it 'returns empty tags array when content contains no @' do + peep = Peep.new + peep.content = 'No tags here' + + expect(peep.tags).to eq [] + end + + it 'returns tagged usernames' do + peep = Peep.new + peep.content = '@jamie is cool' + + expect(peep.tags).to eq ['@jamie'] + end + + it 'returns tagged usernames without duplicates' do + peep = Peep.new + peep.content = '@jamie is cool but @stephen is cooler @ than @jamie' + + expect(peep.tags).to eq ['@jamie', '@stephen'] + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d899..b958eab019 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,11 @@ require 'simplecov' require 'simplecov-console' +require 'database_connection' + +# Make sure this connects to your test database +# (its name should end with '_test') + +ENV['ENV'] = 'test' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, @@ -9,9 +15,19 @@ SimpleCov.start RSpec.configure do |config| + config.before(:suite) do + DatabaseConnection.connect + end + config.after(:suite) do puts puts "\e[33mHave you considered running rubocop? It will help you improve your code!\e[0m" puts "\e[33mTry it now! Just run: rubocop\e[0m" end end + +def reset_tables + seed_sql = File.read('spec/test_seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_site_test' }) + connection.exec(seed_sql) +end diff --git a/spec/test_seeds.sql b/spec/test_seeds.sql new file mode 100644 index 0000000000..6d60846fc5 --- /dev/null +++ b/spec/test_seeds.sql @@ -0,0 +1,13 @@ +TRUNCATE TABLE users, peeps RESTART IDENTITY; + +INSERT INTO users (email, password, name, username) + VALUES ('lou@chitter.com', 'password01', 'Louis', 'lpc'); + +INSERT INTO users (email, password, name, username) + VALUES ('luce@chitter.com', 'password02', 'Lucy', 'leh'); + +INSERT INTO peeps (content, time, user_id) + VALUES ('First post', '12:00', 1); + +INSERT INTO peeps (content, time, user_id) + VALUES ('Second post', '13:00', 1); \ No newline at end of file diff --git a/spec/user_repository_spec.rb b/spec/user_repository_spec.rb new file mode 100644 index 0000000000..06387b7235 --- /dev/null +++ b/spec/user_repository_spec.rb @@ -0,0 +1,92 @@ +require 'user_repository' + +RSpec.describe UserRepository do + before(:each) do + reset_tables + end + + it 'returns an array of all users' do + repo = UserRepository.new + users = repo.all + + expect(users.length).to eq 2 + + expect(users[0].id).to eq 1 + expect(users[0].email).to eq 'lou@chitter.com' + expect(users[0].password).to eq 'password01' + expect(users[0].name).to eq 'Louis' + expect(users[0].username).to eq 'lpc' + + expect(users[1].id).to eq 2 + expect(users[1].email).to eq 'luce@chitter.com' + expect(users[1].password).to eq 'password02' + expect(users[1].name).to eq 'Lucy' + expect(users[1].username).to eq 'leh' + end + + it 'finds a single user' do + repo = UserRepository.new + + user = repo.find(1) + + expect(user.id).to eq 1 + expect(user.email).to eq 'lou@chitter.com' + expect(user.password).to eq 'password01' + expect(user.name).to eq 'Louis' + expect(user.username).to eq 'lpc' + end + + context 'when creating new user' do + it 'adds a new user to database' do + repo = UserRepository.new + user = User.new + user.email = 'newemail@chitter.com' + user.password = 'password03' + user.name = 'Jon' + user.username = 'doe' + + repo.create(user) + + new_user = repo.all.last + + expect(new_user.id).to eq 3 + expect(new_user.email).to eq 'newemail@chitter.com' + expect(new_user.password).to eq 'password03' + expect(new_user.name).to eq 'Jon' + expect(new_user.username).to eq 'doe' + end + + it 'throws an error when email already contained in database' do + repo = UserRepository.new + user = User.new + user.email = 'lou@chitter.com' + user.password = 'password03' + user.name = 'Jon' + user.username = 'doe' + + expect { repo.create(user) }.to raise_error "email already exists" + end + it 'throws an error when email already contained in database' do + repo = UserRepository.new + user = User.new + user.email = 'jonny@chitter.com' + user.password = 'password03' + user.name = 'Jon' + user.username = 'lpc' + + expect { repo.create(user) }.to raise_error "username already exists" + end + end + + it 'finds a single user and their peeps' do + repo = UserRepository.new + + user = repo.find_with_peeps(1) + expect(user.id).to eq 1 + expect(user.email).to eq 'lou@chitter.com' + + peep = user.peeps.first + expect(peep.content).to eq 'First post' + expect(peep.time).to eq '12:00:00' + end +end diff --git a/views/index.erb b/views/index.erb new file mode 100644 index 0000000000..8277c72377 --- /dev/null +++ b/views/index.erb @@ -0,0 +1,18 @@ + + + Chitter home + + +

Chitter

+

If you'd like to add to this enthralling world of chitter, then login or sign up below!

+ Sign up now! + Login here! +

All peeps:

+ <% @peeps.each do |peep| %> +
+ <%= @display.peep(peep) %> +

--------

+
+ <% end %> + + \ No newline at end of file diff --git a/views/logged_in.erb b/views/logged_in.erb new file mode 100644 index 0000000000..ab121b113f --- /dev/null +++ b/views/logged_in.erb @@ -0,0 +1,2 @@ +

Now logged in as <%= @username %>

+Go back to homepage! \ No newline at end of file diff --git a/views/login.erb b/views/login.erb new file mode 100644 index 0000000000..ad0e36047f --- /dev/null +++ b/views/login.erb @@ -0,0 +1,17 @@ + + + Login + + +

Chitter

+

Login to chitter!

+ + + + + + + +
Back to homepage + + \ No newline at end of file diff --git a/views/logout.erb b/views/logout.erb new file mode 100644 index 0000000000..ddd4b6a8ec --- /dev/null +++ b/views/logout.erb @@ -0,0 +1,2 @@ +

You have logged out!

+
Return to the home page
\ No newline at end of file diff --git a/views/new_user.erb b/views/new_user.erb new file mode 100644 index 0000000000..9752de78fc --- /dev/null +++ b/views/new_user.erb @@ -0,0 +1,20 @@ + + + Sign up! + + +

Welcome to Chitter!

+

Fill in your details to sign up:

+
+ + + + + + + + + +
+ + \ No newline at end of file diff --git a/views/new_user_error.erb b/views/new_user_error.erb new file mode 100644 index 0000000000..c6d39ec981 --- /dev/null +++ b/views/new_user_error.erb @@ -0,0 +1,9 @@ + + + +

Uh oh!

+
Sorry, this <%= @error_message%>
+
Please try signing up again with a unique username
+
Or return to the home page
+ + \ No newline at end of file diff --git a/views/unknown_username.erb b/views/unknown_username.erb new file mode 100644 index 0000000000..bac6b9bccc --- /dev/null +++ b/views/unknown_username.erb @@ -0,0 +1,9 @@ + + + +

Sorry!

+
This user (<%= @username %>) does not exist...
+
Please sign up first
+
Or return to the home page
+ + \ No newline at end of file diff --git a/views/user_created.erb b/views/user_created.erb new file mode 100644 index 0000000000..27a8e8e90b --- /dev/null +++ b/views/user_created.erb @@ -0,0 +1,9 @@ + + + +

Sign up complete!

+
Now you're all ready to start sharing fascinating peeps
+
Now login here to make your first peep
+
Or return to the home page
+ + \ No newline at end of file diff --git a/views/user_homepage.erb b/views/user_homepage.erb new file mode 100644 index 0000000000..936d67812d --- /dev/null +++ b/views/user_homepage.erb @@ -0,0 +1,29 @@ + + + Chitter home + + +

Chitter

+

Welcome <%= @user.username %>!

+ Logout here! +

Make a new peep!

+
+
+ + +
+
+ + readonly> +
+ +
+

All peeps:

+ <% @peeps.each do |peep| %> +
+ <%= @display.peep(peep) %> +

--------

+
+ <% end %> + + \ No newline at end of file diff --git a/views/user_page.erb b/views/user_page.erb new file mode 100644 index 0000000000..9e95688100 --- /dev/null +++ b/views/user_page.erb @@ -0,0 +1,16 @@ + + + <%= @user.username %> + + +

Chitter

+ Back to home page + <%= @display.user(@user) %> +

Peeps:

+ <% @peeps.each do |peep| %> +
<%= peep.content %>
+
- <%= peep.time %>
+

--------

+ <% end %> + + \ No newline at end of file