diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..865a5ded8 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,56 @@ +version: 2.1 + +orbs: + ruby: circleci/ruby@1.1.0 + node: circleci/node@2 + +jobs: + build: + docker: + - image: cimg/ruby:2.6.6-node + steps: + - checkout + - ruby/install-deps + # Store bundle cache + - node/install-packages: + pkg-manager: yarn + cache-key: "yarn.lock" + test: + parallelism: 3 + docker: + - image: cimg/ruby:2.6.6-node + - image: circleci/postgres:9.5-alpine + environment: + POSTGRES_USER: circleci-ruby + POSTGRES_DB: sample_app_test + POSTGRES_PASSWORD: "" + environment: + BUNDLE_JOBS: "3" + BUNDLE_RETRY: "3" + PGHOST: 127.0.0.1 + DATABASE_USER: circleci-ruby + DATABASE_PASSWORD: "" + RAILS_ENV: test + steps: + - checkout + - ruby/install-deps + - node/install-packages: + pkg-manager: yarn + cache-key: "yarn.lock" + - run: + name: Wait for DB + command: dockerize -wait tcp://localhost:5432 -timeout 1m + - run: + name: Database setup + command: bundle exec rails db:schema:load --trace + # Run rspec in parallel + - ruby/rspec-test + +workflows: + version: 2 + build_and_test: + jobs: + - build + - test: + requires: + - build diff --git a/.gitignore b/.gitignore index 55c9f130c..28b1c394d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-debug.log* # Ignore db test files. db/test.* + +# Ignore application configuration +/config/application.yml diff --git a/.rubocop.yml b/.rubocop.yml index 3770f39c7..2598f8e2e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,3 +17,15 @@ RSpec/MultipleExpectations: Metrics/BlockLength: Enabled: false + +Bundler/OrderedGems: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/SymbolArray: + Enabled: false + +Layout/ExtraSpacing: + Enabled: false diff --git a/Gemfile b/Gemfile index 9adf1695e..2352e005e 100644 --- a/Gemfile +++ b/Gemfile @@ -18,10 +18,11 @@ gem 'jbuilder', '2.10.0' gem 'bootsnap', '1.4.6', require: false gem 'rubocop', '1.5.2', require: false gem 'rubocop-rspec', '2.0.1', require: false +gem 'pg', '0.21.0' +gem "figaro", '1.2.0' group :development, :test do - gem 'sqlite3', '1.4.2' - gem 'byebug', '11.1.3', platforms: [:mri, :mingw, :x64_mingw] + gem 'byebug', '11.1.3', platforms: [:mri, :mingw, :x64_mingw] gem 'pry-rails' gem 'rspec-rails', '~> 4.0.1' gem 'factory_bot_rails', '~> 6.1.0' @@ -46,10 +47,11 @@ group :test do gem 'cucumber-rails', '2.2.0', require: false gem 'database_cleaner-active_record', '1.8.0' gem 'launchy', '2.5.0' + gem 'shoulda-matchers', '4.4.1' + gem 'rspec_junit_formatter', '0.4.1' end group :production do - gem 'pg', '1.2.3' gem 'aws-sdk-s3', '1.46.0', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index 2bd2fea1a..6e04e9232 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -155,6 +155,8 @@ GEM faker (2.11.0) i18n (>= 1.6, < 2) ffi (1.13.1) + figaro (1.2.0) + thor (>= 0.14.0, < 2) formatador (0.2.5) globalid (0.4.2) activesupport (>= 4.2.0) @@ -219,7 +221,7 @@ GEM parallel (1.20.1) parser (2.7.2.0) ast (~> 2.4.1) - pg (1.2.3) + pg (0.21.0) protobuf-cucumber (3.10.8) activesupport (>= 3.2) middleware @@ -292,6 +294,8 @@ GEM rspec-mocks (~> 3.9) rspec-support (~> 3.9) rspec-support (3.10.0) + rspec_junit_formatter (0.4.1) + rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.5.2) parallel (~> 1.10) parser (>= 2.7.1.5) @@ -324,6 +328,8 @@ GEM childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) shellany (0.0.1) + shoulda-matchers (4.4.1) + activesupport (>= 4.2.0) spring (2.1.1) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) @@ -335,7 +341,6 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sqlite3 (1.4.2) sys-uname (1.2.2) ffi (~> 1.1) thor (1.0.1) @@ -384,6 +389,7 @@ DEPENDENCIES database_cleaner-active_record (= 1.8.0) factory_bot_rails (~> 6.1.0) faker (= 2.11.0) + figaro (= 1.2.0) guard (= 2.16.2) guard-minitest (= 2.4.6) image_processing (= 1.9.3) @@ -393,19 +399,20 @@ DEPENDENCIES mini_magick (= 4.9.5) minitest (= 5.11.3) minitest-reporters (= 1.3.8) - pg (= 1.2.3) + pg (= 0.21.0) pry-rails puma (= 5.0.4) rails (= 6.0.3.4) rails-controller-testing (= 1.0.4) rspec-rails (~> 4.0.1) + rspec_junit_formatter (= 0.4.1) rubocop (= 1.5.2) rubocop-rspec (= 2.0.1) sass-rails (= 6.0.0) selenium-webdriver (= 3.142.7) + shoulda-matchers (= 4.4.1) spring (= 2.1.1) spring-watcher-listen (= 2.0.1) - sqlite3 (= 1.4.2) turbolinks (= 5.2.1) web-console (= 4.0.2) webdrivers (= 4.3.0) diff --git a/app/models/user.rb b/app/models/user.rb index f59977d81..5568e7e05 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,18 +1,23 @@ class User < ApplicationRecord has_many :microposts, dependent: :destroy - has_many :active_relationships, class_name: "Relationship", + has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", - dependent: :destroy - has_many :passive_relationships, class_name: "Relationship", + dependent: :destroy + + has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", - dependent: :destroy + dependent: :destroy + has_many :following, through: :active_relationships, source: :followed has_many :followers, through: :passive_relationships, source: :follower attr_accessor :remember_token, :activation_token, :reset_token + before_save :downcase_email before_create :create_activation_digest validates :name, presence: true, length: { maximum: 50 } - VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i + + VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze + validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: true @@ -20,14 +25,18 @@ class User < ApplicationRecord validates :password, presence: true, length: { minimum: 6 }, allow_nil: true # Returns the hash digest of the given string. - def User.digest(string) - cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : - BCrypt::Engine.cost + def self.digest(string) + cost = if ActiveModel::SecurePassword.min_cost + BCrypt::Engine::MIN_COST + else + BCrypt::Engine.cost + end + BCrypt::Password.create(string, cost: cost) end # Returns a random token. - def User.new_token + def self.new_token SecureRandom.urlsafe_base64 end @@ -48,6 +57,7 @@ def session_token def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? + BCrypt::Password.new(digest).is_password?(token) end @@ -109,14 +119,14 @@ def following?(other_user) private - # Converts email to all lower-case. - def downcase_email - self.email = email.downcase - end + # Converts email to all lower-case. + def downcase_email + self.email = email.downcase + end - # Creates and assigns the activation token and digest. - def create_activation_digest - self.activation_token = User.new_token - self.activation_digest = User.digest(activation_token) - end + # Creates and assigns the activation token and digest. + def create_activation_digest + self.activation_token = User.new_token + self.activation_digest = User.digest(activation_token) + end end diff --git a/config/application.sample.yml b/config/application.sample.yml new file mode 100644 index 000000000..2c48531b7 --- /dev/null +++ b/config/application.sample.yml @@ -0,0 +1,7 @@ +development: + DATABASE_USER: '' + DATABASE_PASSWORD: '' + +test: + DATABASE_USER: '' + DATABASE_PASSWORD: '' diff --git a/config/database.yml b/config/database.yml index bad265644..9466b55cd 100644 --- a/config/database.yml +++ b/config/database.yml @@ -5,27 +5,28 @@ # gem 'sqlite3' # default: &default - adapter: sqlite3 + adapter: postgresql + encoding: unicode pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default - database: db/development.sqlite3 + database: sample_app_development + username: <%= ENV.fetch("DATABASE_USER") %> + password: <%= ENV.fetch("DATABASE_PASSWORD") %> # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default - database: db/test.sqlite3 + database: sample_app_test + username: <%= ENV.fetch("DATABASE_USER") %> + password: <%= ENV.fetch("DATABASE_PASSWORD") %> production: - adapter: postgresql - encoding: unicode - # For details on connection pooling, see Rails configuration guide - # https://guides.rubyonrails.org/configuring.html#database-pooling - pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + <<: *default database: sample_app_production username: sample_app - password: <%= ENV['SAMPLE_APP_DATABASE_PASSWORD'] %> + password: <%= ENV['DATABASE_PASSWORD'] %> diff --git a/db/schema.rb b/db/schema.rb index 71eac3e5b..7b58f01d5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,11 +12,14 @@ ActiveRecord::Schema.define(version: 2019_08_27_030205) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false - t.integer "record_id", null: false - t.integer "blob_id", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false t.datetime "created_at", null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true @@ -35,7 +38,7 @@ create_table "microposts", force: :cascade do |t| t.text "content" - t.integer "user_id", null: false + t.bigint "user_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["user_id", "created_at"], name: "index_microposts_on_user_id_and_created_at" diff --git a/features/home_page.feature b/features/home_page.feature new file mode 100644 index 000000000..78efb2196 --- /dev/null +++ b/features/home_page.feature @@ -0,0 +1,15 @@ +Feature: Users + Background: + Given I am logged in as an activated user + + @javascript_driver + Scenario: User can create micropost + When I go to home page + Then I should see "Compose new micropost" textare + When I create a new micropost + Then I should see the new micropost from "Micropost Feed" + + Scenario: User can destroy owned microposts + + + Scenario: User can see microposts from followed users diff --git a/features/support/env.rb b/features/support/env.rb index 003fb1319..dc94db95e 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -58,3 +58,6 @@ # See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature Cucumber::Rails::Database.javascript_strategy = :truncation +Capybara.javascript_driver = :selenium_chrome + + diff --git a/features/users.feature b/features/users.feature index 3d4cdacb4..48eef3a51 100644 --- a/features/users.feature +++ b/features/users.feature @@ -5,4 +5,3 @@ Feature: Users Scenario: Users List When I go to the list of users Then I should see "Users" - And I should see "All users" diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb new file mode 100644 index 000000000..92ca269ff --- /dev/null +++ b/spec/models/application_record_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' +RSpec.describe ApplicationRecord, type: :model do + it '#self' do + expect(ApplicationRecord.abstract_class).to eq true + end + +end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5d5b6d00d..dcd1199f8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -5,15 +5,17 @@ expect(create(:user)).to be_valid end - context 'when name is not present' do - let(:user) { described_class.new(email: Faker::Internet.email, password: described_class.digest('password')) } - - it 'is inivalid' do - expect(user.valid?).to be false - end + describe 'associations' do + it { is_expected.to have_many(:microposts).dependent(:destroy) } + it { is_expected.to have_many(:active_relationships).with_foreign_key('follower_id').class_name('Relationship').dependent(:destroy) } + it { is_expected.to have_many(:passive_relationships).with_foreign_key('followed_id').class_name('Relationship').dependent(:destroy) } + it { is_expected.to have_many(:following).through(:active_relationships).source(:followed) } + it { is_expected.to have_many(:followers).through(:passive_relationships).source(:follower) } + end - it 'is failed to save' do - expect(user.save).to be false - end + describe 'validations' do + it { is_expected.to validate_presence_of(:name)} + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to validate_presence_of(:password) } end end diff --git a/spec/support/shoulda_matcher.rb b/spec/support/shoulda_matcher.rb new file mode 100644 index 000000000..7d045f359 --- /dev/null +++ b/spec/support/shoulda_matcher.rb @@ -0,0 +1,6 @@ +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end