diff --git a/Gemfile b/Gemfile index b1a320395a..ad9fe69ef3 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,10 @@ 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.18' diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..6a34d0d0ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,11 +3,21 @@ GEM specs: ansi (1.5.0) ast (2.4.2) + bcrypt (3.1.18) 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.4.6) + 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 +46,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 +57,38 @@ 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) unicode-display_width (2.0.0) + webrick (1.8.1) PLATFORMS ruby + x86_64-linux DEPENDENCIES + bcrypt (~> 3.1.18) + pg (~> 1.4) + 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/app.rb b/app.rb new file mode 100644 index 0000000000..760235ba86 --- /dev/null +++ b/app.rb @@ -0,0 +1,50 @@ +require_relative 'lib/database_connection' +require_relative 'lib/user_repository.rb' +require_relative 'lib/peep_repository.rb' +require 'sinatra/base' +require 'sinatra/reloader' + +DatabaseConnection.connect('chitter') + +class Application < Sinatra::Base + # Sessions are disabled by default, so this line is needed. + enable :sessions + + configure :development do + register Sinatra::Reloader + end + + get '/signup' do + return erb(:signup) + end + + post '/signup' do + new_user = User.new + new_user.username = params['username'] + new_user.email = params['email'] + new_user.password = params['password'] + created_user = UserRepository.new.create(new_user) + session[:user_id] = created_user.id + redirect '/homepage' + end + + get '/homepage' do + repo = PeepRepository.new + @peeps = repo.all + return erb(:homepage) + end + + get '/peep' do + return erb(:peep) + end + + post '/peep' do + new_peep = Peep.new + new_peep.peep = params[:peep] + new_peep.timestamp = Time.now + new_peep.username_id = session[:user_id] + PeepRepository.new.create(new_peep) + redirect '/homepage' + end +end + diff --git a/backup.tar b/backup.tar new file mode 100644 index 0000000000..0e98bf845a Binary files /dev/null and b/backup.tar differ 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/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 0000000000..68b8f27943 --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,35 @@ +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(database_name) + @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 + + def self.setup(dbname) + @connection = PG.connect(dbname: dbname) + @connection.exec("CREATE TABLE IF NOT EXISTS users(id SERIAL PRIMARY KEY, username VARCHAR(60), email VARCHAR(60), password VARCHAR(255));") + end + + def self.clear(dbname) + @connection.exec("TRUNCATE TABLE users;") + end +end \ No newline at end of file diff --git a/lib/peep.rb b/lib/peep.rb new file mode 100644 index 0000000000..cb2f2de3b4 --- /dev/null +++ b/lib/peep.rb @@ -0,0 +1,3 @@ +class Peep + attr_accessor :id, :peep, :date, :username_id, :timestamp +end \ No newline at end of file diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb new file mode 100644 index 0000000000..e18ea68d76 --- /dev/null +++ b/lib/peep_repository.rb @@ -0,0 +1,29 @@ +require_relative 'peep' +require_relative 'database_connection' + +class PeepRepository + + def all + # returns all rows in reverse chronological order + sql = 'SELECT peep, timestamp, username_id FROM peeps;' + result_set = DatabaseConnection.exec_params(sql, []) + + peeps = [] + result_set.each do |row| + new_peep = Peep.new + new_peep.id = row['id'] + new_peep.peep = row['peep'] + new_peep.timestamp = row['timestamp'] + new_peep.username_id = row['username_id'] + peeps << new_peep + end + + return peeps + end + + def create(message) + sql = 'INSERT INTO peeps (peep, username_id, timestamp) VALUES ($1, $2, $3);' + result_set = DatabaseConnection.exec_params(sql, [message.peep, message.username_id, message.timestamp]) + return message + end +end \ No newline at end of file diff --git a/lib/user.rb b/lib/user.rb new file mode 100644 index 0000000000..cbd574bacf --- /dev/null +++ b/lib/user.rb @@ -0,0 +1,3 @@ +class User + attr_accessor :id, :username, :email, :password +end diff --git a/lib/user_repository.rb b/lib/user_repository.rb new file mode 100644 index 0000000000..cbabd5b112 --- /dev/null +++ b/lib/user_repository.rb @@ -0,0 +1,60 @@ +require_relative 'database_connection' +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'] + user.username = row['username'] + user.email = row['email'] + user.password = row['password'] + users << user + end + + return users + end + + def create(user) + encrypted_password = BCrypt::Password.create(user.password) + + sql = ' + INSERT INTO users (username, email, password) + VALUES($1, $2, $3) + RETURNING id; + ' + sql_params = [ + user.username, + user.email, + encrypted_password + ] + result_set = DatabaseConnection.exec_params(sql, sql_params) + + user.id = result_set[0]['id'].to_i + + return user + end + + + def find_by_email(email) + sql = 'SELECT username FROM users WHERE email = $1;' + params = [email] + result = DatabaseConnection.exec_params(sql, params) + + if result.ntuples > 0 + record = result[0] + user = User.new + user.username = record['username'] + user.email = record['email'] + user.password = record['password'] + return user + else + fail 'No such user with given email.' + end + end +end diff --git a/spec/integration/app_spec.rb b/spec/integration/app_spec.rb new file mode 100644 index 0000000000..db082ae7b8 --- /dev/null +++ b/spec/integration/app_spec.rb @@ -0,0 +1,53 @@ +require_relative '../../app' +require "spec_helper" +require "rack/test" + +describe Application do + # This is so we can use rack-test helper methods. + include Rack::Test::Methods + + # We need to declare the `app` value by instantiating the Application + # class so our tests work. + let(:app) { Application.new } + + describe 'GET /signup' do + it 'returns the signup page' do + response = get('/signup') + expect(response.status).to eq 200 + expect(response.body).to include '' + end + end + describe 'POST /signup' do + it 'creates a new user' do + response = post( + '/signup', + username: 'user1', + email: 'user1@gmail.com', + password: '12345678' + ) + expect(response.status).to eq(302) # 302 automatically redirects to another URL + expect(UserRepository.new.find_by_email('user1@gmail.com').username).to eq 'user1' + end + end + describe 'GET /homepage' do + it 'returns all peeps from most recent date' do + response = get('/homepage') + expect(response.status).to eq 200 + expect(response.body).to include '

Most Recent Peeps

' + end + end + describe 'GET /peep' do + it 'returns the post peep page' do + response = get('/peep') + expect(response.status).to eq 200 + expect(response.body).to include '' + end + end + describe 'POST /peep' do + it 'creates a new peep' do + post '/peep', { peep: 'This is a new peep' }, { 'rack.session' => { user_id: 1 } } + expect(last_response.status).to eq(302) + expect(PeepRepository.new.all.last.peep).to eq('This is a new peep') + end + end +end \ No newline at end of file diff --git a/spec/peep_repository_test.rb b/spec/peep_repository_test.rb new file mode 100644 index 0000000000..7a0f47e692 --- /dev/null +++ b/spec/peep_repository_test.rb @@ -0,0 +1,39 @@ +require 'peep_repository.rb' +require 'peep.rb' + +def reset_peeps_table + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'peeps_test' }) + connection.exec(seed_sql) +end + +describe PeepRepository do + before(:each) do + reset_peeps_table + end + + let(:repo) {PeepRepository.new} + let(:new_peep) {Peep.new} + + describe '#all' do + it 'returns all peeps in reverse chronological order' do + peeps = repo.all + expect(peeps[0].peep).to eq 'Test peep 1' + expect(peeps[1].peep).to eq 'Test peep 2' + end + end + + describe '#create' do + it 'creates and stores a new peep' do + new_peep.peep = 'Test peep 1' + new_peep.timestamp = '2023-04-10 12:34:56' + new_peep.username_id = '1' + repo.create(new_peep) + + peeps = repo.all + expect(peeps.last.peep).to eq 'Test peep 1' + expect(peeps.last.timestamp).to eq '2023-04-10 12:34:56' + expect(peeps.last.username_id).to eq '1' + end +end +end \ No newline at end of file diff --git a/spec/seeds.sql b/spec/seeds.sql new file mode 100644 index 0000000000..fec90b607d --- /dev/null +++ b/spec/seeds.sql @@ -0,0 +1,12 @@ +TRUNCATE TABLE peeps RESTART IDENTITY CASCADE; +TRUNCATE TABLE users RESTART IDENTITY CASCADE; + +INSERT INTO users (username, email, password) VALUES + ('user1', 'user1@example.com', ''), + ('user2', 'user2@example.com', ''); + +INSERT INTO peeps (peep, username_id, timestamp) VALUES + ('Test peep 1', 1, '2023-04-10 12:34:56'), + ('Test peep 2', 2, '2023-04-11 15:30:00'), + ('Test peep 3', 2, '2023-04-12 20:30:00'); + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d899..2e4beb6fb2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,16 @@ require 'simplecov' require 'simplecov-console' +require 'database_connection' +require 'rack/test' + +# Make sure this connects to your test database +# (its name should end with '_test') +DatabaseConnection.connect('peeps_test') SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter + SimpleCov::Formatter::HTMLFormatter ]) SimpleCov.start @@ -14,4 +20,4 @@ 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 +end \ No newline at end of file diff --git a/spec/user_repository_test.rb b/spec/user_repository_test.rb new file mode 100644 index 0000000000..4f4bd24205 --- /dev/null +++ b/spec/user_repository_test.rb @@ -0,0 +1,56 @@ +require_relative '../lib/user.rb' +require_relative '../lib/user_repository.rb' + +def reset_users_table + seed_sql = File.read('spec/seeds.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'peeps_test' }) + connection.exec(seed_sql) +end + +describe UserRepository do + before(:each) do + reset_users_table + end + + let(:repo) { UserRepository.new } + let(:user) { User.new} + + describe '#all' do + it 'lists all users' do + users = repo.all + expect(users[0].username).to eq 'user1' + expect(users[0].email).to eq 'user1@example.com' + expect(users[1].username).to eq 'user2' + expect(users[1].email).to eq 'user2@example.com' + end + end + + describe '#create' do + it 'adds a new user to the table' do + user.username = 'user3' + user.email = 'user3@example.com' + user.password = '12345678' + + new_user = repo.create(user) + all_users = repo.all + + expect(all_users.last.username).to eq 'user3' + expect(all_users.last.email).to eq 'user3@example.com' + expect(BCrypt::Password.new(all_users.last.password)).to eq '12345678' + end + end + describe '#find_by_email' do + context 'when user exists' do + it 'returns the user associated with that email' do + found_user = repo.find_by_email('user1@example.com') + expect(found_user.username).to eq 'user1' + end + end + context 'when user does not exist' do + it 'fails' do + found_user = repo.find_by_email('nonexistent@example.com') + expect {found_user.username}.to raise_error 'No such user with given email.' + end + end + end +end \ No newline at end of file diff --git a/views/homepage.erb b/views/homepage.erb new file mode 100644 index 0000000000..f14d258e98 --- /dev/null +++ b/views/homepage.erb @@ -0,0 +1,15 @@ +

Welcome to Chitter

+ +

Most Recent Peeps

+ +<% @peeps.each do |peep| %> +
+ <%= Time.parse(peep.timestamp).strftime("%H:%M:%S on %B %d, %Y") %> +

<%= peep.peep %>

+
+
+<% end %> + +
+ Post a Peep +
diff --git a/views/peep.erb b/views/peep.erb new file mode 100644 index 0000000000..9970e0703c --- /dev/null +++ b/views/peep.erb @@ -0,0 +1,12 @@ +
+ + + +
+ +
+ Return to Homepage +
+ + + diff --git a/views/signup.erb b/views/signup.erb new file mode 100644 index 0000000000..93b19e2029 --- /dev/null +++ b/views/signup.erb @@ -0,0 +1,13 @@ +

Sign Up

+ +
+ + + + + + + + + +
\ No newline at end of file