diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3c594094 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +docker-compose.yml +Dockerfile +tmp/*.* +log/*.* +data diff --git a/.gitignore b/.gitignore index 82701fed..e15c06b6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ /yarn-error.log .byebug_history + +data/* diff --git a/.rspec b/.rspec index c99d2e73..5be63fcb 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,2 @@ --require spec_helper +--format documentation diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 00000000..955d8a91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM ruby:3.1.2 +RUN apt-get update -qq && apt-get install -y nodejs postgresql-client +WORKDIR /topnews +COPY . /topnews +RUN bundle install + +COPY entrypoint.sh /usr/bin/ +RUN chmod +x /usr/bin/entrypoint.sh +ENTRYPOINT ["entrypoint.sh"] + +EXPOSE 3000 +CMD ["rails", "server", "-b", "0.0.0.0"] diff --git a/Gemfile b/Gemfile old mode 100644 new mode 100755 index 5a8ffc43..08470a49 --- a/Gemfile +++ b/Gemfile @@ -20,3 +20,7 @@ gem 'turbolinks' gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'uglifier' gem 'web-console', group: :development +gem 'shoulda-matchers', '~> 6.0', group: :test +gem 'factory_bot_rails', group: [:development, :test] +gem 'typhoeus' +gem 'rails-controller-testing', group: :test diff --git a/Gemfile.lock b/Gemfile.lock old mode 100644 new mode 100755 index 14ec6457..e8305f4b --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,87 +1,87 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.0.4) - actionpack (= 7.0.4) - activesupport (= 7.0.4) + actioncable (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.4) - actionpack (= 7.0.4) - activejob (= 7.0.4) - activerecord (= 7.0.4) - activestorage (= 7.0.4) - activesupport (= 7.0.4) + actionmailbox (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.4) - actionpack (= 7.0.4) - actionview (= 7.0.4) - activejob (= 7.0.4) - activesupport (= 7.0.4) + actionmailer (7.0.8.4) + actionpack (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.4) - actionview (= 7.0.4) - activesupport (= 7.0.4) - rack (~> 2.0, >= 2.2.0) + actionpack (7.0.8.4) + actionview (= 7.0.8.4) + activesupport (= 7.0.8.4) + rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.4) - actionpack (= 7.0.4) - activerecord (= 7.0.4) - activestorage (= 7.0.4) - activesupport (= 7.0.4) + actiontext (7.0.8.4) + actionpack (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.4) - activesupport (= 7.0.4) + actionview (7.0.8.4) + activesupport (= 7.0.8.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.4) - activesupport (= 7.0.4) + activejob (7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.3.6) - activemodel (7.0.4) - activesupport (= 7.0.4) - activerecord (7.0.4) - activemodel (= 7.0.4) - activesupport (= 7.0.4) - activestorage (7.0.4) - actionpack (= 7.0.4) - activejob (= 7.0.4) - activerecord (= 7.0.4) - activesupport (= 7.0.4) + activemodel (7.0.8.4) + activesupport (= 7.0.8.4) + activerecord (7.0.8.4) + activemodel (= 7.0.8.4) + activesupport (= 7.0.8.4) + activestorage (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activesupport (= 7.0.8.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.4) + activesupport (7.0.8.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - bcrypt (3.1.18) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + base64 (0.2.0) + bcrypt (3.1.20) bindex (0.8.1) - builder (3.2.4) + builder (3.3.0) byebug (11.1.3) - capybara (3.37.1) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - childprocess (4.1.0) coderay (1.1.3) coffee-rails (5.0.0) coffee-script (>= 2.2.0) @@ -90,124 +90,135 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.10) + concurrent-ruby (1.3.4) crass (1.0.6) - devise (4.8.1) + date (3.3.4) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.5.0) - digest (3.1.0) - erubi (1.11.0) - execjs (2.8.1) - ffi (1.15.5) - globalid (1.0.0) - activesupport (>= 5.0) - i18n (1.12.0) + diff-lcs (1.5.1) + erubi (1.13.0) + ethon (0.16.0) + ffi (>= 1.15.0) + execjs (2.9.1) + factory_bot (6.4.6) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) + railties (>= 5.0.0) + ffi (1.17.0) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.5) concurrent-ruby (~> 1.0) - jbuilder (2.11.5) + jbuilder (2.12.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) - listen (3.7.1) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.19.0) + logger (1.6.0) + loofah (2.22.0) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (1.0.2) + net-imap + net-pop + net-smtp + marcel (1.0.4) matrix (0.4.2) - method_source (1.0.0) - mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.16.3) - net-imap (0.2.3) - digest + method_source (1.1.0) + mini_mime (1.1.5) + minitest (5.25.1) + net-imap (0.4.14) + date net-protocol - strscan - net-pop (0.1.1) - digest + net-pop (0.1.2) net-protocol + net-protocol (0.2.2) timeout - net-protocol (0.1.3) - timeout - net-smtp (0.3.1) - digest + net-smtp (0.5.0) net-protocol - timeout - nio4r (2.5.8) - nokogiri (1.13.8) - mini_portile2 (~> 2.8.0) + nio4r (2.7.3) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) - pg (1.4.3) - pry (0.14.1) + pg (1.5.7) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry-rails (0.3.9) - pry (>= 0.10.4) - public_suffix (5.0.0) - puma (5.6.5) + pry-rails (0.3.11) + pry (>= 0.13.0) + public_suffix (6.0.1) + puma (6.4.2) nio4r (~> 2.0) - racc (1.6.0) - rack (2.2.4) - rack-test (2.0.2) + racc (1.8.1) + rack (2.2.9) + rack-test (2.1.0) rack (>= 1.3) - rails (7.0.4) - actioncable (= 7.0.4) - actionmailbox (= 7.0.4) - actionmailer (= 7.0.4) - actionpack (= 7.0.4) - actiontext (= 7.0.4) - actionview (= 7.0.4) - activejob (= 7.0.4) - activemodel (= 7.0.4) - activerecord (= 7.0.4) - activestorage (= 7.0.4) - activesupport (= 7.0.4) + rails (7.0.8.4) + actioncable (= 7.0.8.4) + actionmailbox (= 7.0.8.4) + actionmailer (= 7.0.8.4) + actionpack (= 7.0.8.4) + actiontext (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activemodel (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) bundler (>= 1.15.0) - railties (= 7.0.4) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + railties (= 7.0.8.4) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.4.3) - loofah (~> 2.3) - railties (7.0.4) - actionpack (= 7.0.4) - activesupport (= 7.0.4) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) method_source rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) - rake (13.0.6) + rake (13.2.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - regexp_parser (2.5.0) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) - rexml (3.2.5) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) + regexp_parser (2.9.2) + responders (3.1.1) actionpack (>= 5.2) - activesupport (>= 5.2) railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.11.1) + rexml (3.3.6) + strscan + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.4) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) rubyzip (2.3.2) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) @@ -219,64 +230,73 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.4.0) - childprocess (>= 0.5, < 5.0) + selenium-webdriver (4.23.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - spring (4.1.0) - sprockets (4.1.1) + shoulda-matchers (6.4.0) + activesupport (>= 5.2.0) + spring (4.2.1) + sprockets (4.2.1) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - strscan (3.0.4) - thor (1.2.1) - tilt (2.0.11) - timeout (0.3.0) + strscan (3.1.0) + thor (1.3.1) + tilt (2.4.0) + timeout (0.4.1) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (2.0.5) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.0) + web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - websocket (1.2.9) - websocket-driver (0.7.5) + websocket (1.2.11) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.0) + zeitwerk (2.6.17) PLATFORMS - ruby + x86_64-linux DEPENDENCIES byebug capybara coffee-rails devise + factory_bot_rails jbuilder listen pg pry-rails puma rails (~> 7.0.3) + rails-controller-testing rspec-rails sass-rails selenium-webdriver + shoulda-matchers (~> 6.0) spring turbolinks + typhoeus tzinfo-data uglifier web-console @@ -285,4 +305,4 @@ RUBY VERSION ruby 3.1.2p20 BUNDLED WITH - 2.3.22 + 2.3.7 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/Makefile b/Makefile new file mode 100755 index 00000000..48341681 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +.DEFAULT_GOAL := help +.PHONY: help + +spec_path ?= spec/**/*_spec.rb + +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' + +up: ## Run app and all dependant services + docker-compose up + +down: ## Drop app and all dependant services + docker-compose down + +specs: ## Run specs with optional `spec_opts`, e.g. make specs spec_opts='spec/models/user_spec.rb:15' + docker-compose --profile test run --rm app_test rspec $(spec_opts) + +console: ## rails console + docker-compose run --rm app rails c + +bash: ## app bash shell + docker-compose run --rm app bash + +db_create: ## rails db:create + docker-compose run --rm app rails db:create + +db_migrate: ## rails db:migrate + docker-compose run --rm app rails db:migrate + +db_migrate_test: ## rails db:migrate RAILS_ENV=test + DISABLE_SPRING=true RAILS_ENV=test + docker-compose run --rm app rails db:migrate diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 500f71a1..69306173 --- a/README.md +++ b/README.md @@ -25,3 +25,6 @@ When a team member signs in, they will see recent news stories and be able to st * As an internal tool for a small team, performance optimization is not a requirement. * Be prepared to discuss known performance shortcomings of your solution and potential improvements. * UX design here is of little importance. The design can be minimal or it can have zero design at all. + +# Usage +`make` diff --git a/Rakefile b/Rakefile old mode 100644 new mode 100755 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js old mode 100644 new mode 100755 diff --git a/app/assets/images/.keep b/app/assets/images/.keep old mode 100644 new mode 100755 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js old mode 100644 new mode 100755 diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js old mode 100644 new mode 100755 diff --git a/app/assets/javascripts/channels/.keep b/app/assets/javascripts/channels/.keep old mode 100644 new mode 100755 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css old mode 100644 new mode 100755 diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb old mode 100644 new mode 100755 diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb old mode 100644 new mode 100755 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb old mode 100644 new mode 100755 index 1c07694e..4e5617f5 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,4 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception + before_action :authenticate_user! end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep old mode 100644 new mode 100755 diff --git a/app/controllers/flagged_stories_controller.rb b/app/controllers/flagged_stories_controller.rb new file mode 100644 index 00000000..9035bd66 --- /dev/null +++ b/app/controllers/flagged_stories_controller.rb @@ -0,0 +1,22 @@ +class FlaggedStoriesController < ApplicationController + + def add + raw_url = URI::Parser.new.unescape(params[:url]) + raw_title = URI::Parser.new.unescape(params[:title]) + flagged_story = FlaggedStory.find_or_create_by(url: raw_url, title: raw_title) + flagged_story.users << current_user + flagged_story.save + flash[:notice] = 'Your flag was added to the story' + redirect_to root_path + end + + def remove + flagged_story = FlaggedStory.find(params[:id]) + flagged_story.users.destroy(current_user) + flagged_story.save + flagged_story.destroy if flagged_story.users.count == 0 + flash[:notice] = 'Your flag was removed from the story' + redirect_to root_path + end + +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb old mode 100644 new mode 100755 index ce3bf586..e6fb0482 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,2 +1,10 @@ class PagesController < ApplicationController + def index + @latest_feed = Feed.last || Feed.fetch_and_persist + if @latest_feed.created_at < Time.now - 15.minutes + @latest_feed = Feed.fetch_and_persist + end + + @flagged_stories = FlaggedStory.includes(:users).all.order(created_at: :desc) + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb old mode 100644 new mode 100755 diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb old mode 100644 new mode 100755 diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb old mode 100644 new mode 100755 diff --git a/app/models/application_record.rb b/app/models/application_record.rb old mode 100644 new mode 100755 diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep old mode 100644 new mode 100755 diff --git a/app/models/feed.rb b/app/models/feed.rb new file mode 100644 index 00000000..201ed2c5 --- /dev/null +++ b/app/models/feed.rb @@ -0,0 +1,19 @@ +require 'json' + +class Feed < ApplicationRecord + STORIES_TO_PERSIST = 25 + + class << self + def fetch_and_persist + obj = self.new + obj.stories = [] + feed = JSON.parse(Typhoeus.get("https://hacker-news.firebaseio.com/v0/topstories.json").response_body) + feed.each_index do |i| + obj.stories << JSON.parse(Typhoeus.get("https://hacker-news.firebaseio.com/v0/item/#{feed[i]}.json").response_body) + break if i + 1 == STORIES_TO_PERSIST + end + obj.save + obj + end #fetch_and_persist + end # class methods +end diff --git a/app/models/flagged_story.rb b/app/models/flagged_story.rb new file mode 100755 index 00000000..4f7e58ec --- /dev/null +++ b/app/models/flagged_story.rb @@ -0,0 +1,4 @@ +class FlaggedStory < ApplicationRecord + has_and_belongs_to_many :users, validate: true + validates_presence_of :url, :title +end diff --git a/app/models/user.rb b/app/models/user.rb old mode 100644 new mode 100755 index b2091f9a..e7b84dd5 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,15 @@ class User < ApplicationRecord + has_and_belongs_to_many :flagged_stories, validate: true + # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable + + validates_presence_of :first_name, :last_name, :email + validates_uniqueness_of :email, case_sensitive: false + + def full_name + "#{first_name} #{last_name}" + end end diff --git a/app/views/devise/_sign_out.html.erb b/app/views/devise/_sign_out.html.erb new file mode 100644 index 00000000..fce49008 --- /dev/null +++ b/app/views/devise/_sign_out.html.erb @@ -0,0 +1 @@ +<%= link_to 'Log out', destroy_user_session_path, method: :delete %> \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb old mode 100644 new mode 100755 diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb old mode 100644 new mode 100755 diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb old mode 100644 new mode 100755 diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb deleted file mode 100644 index 8bfd8294..00000000 --- a/app/views/pages/home.html.erb +++ /dev/null @@ -1 +0,0 @@ -

Welcome to Top News

diff --git a/app/views/pages/index.html.erb b/app/views/pages/index.html.erb new file mode 100755 index 00000000..f95ba7e4 --- /dev/null +++ b/app/views/pages/index.html.erb @@ -0,0 +1,27 @@ +

Welcome to Top News

+<% @latest_feed.stories.each do |story| %> + <%= link_to story['title'], story['url'], target: '_blank' %> + <%= button_to 'FLAG', flag_add_path, params: { + url: URI::Parser.new.escape(story['url']), + title: URI::Parser.new.escape(story['title']) + } %> +
+<% end %> + +<% if @flagged_stories.count > 0 %> +

Flagged stories

+ <% @flagged_stories.each do |story| %> + <%= link_to story['title'], story['url'], target: '_blank' %> +
+ Flagged by: <%= story.users.collect(&:full_name).join(',') %> +
+ <% if story.users.includes(current_user) %> + <%= link_to '[Unflag]', flag_remove_path(story) %> +
+ <% end %> + <% end %> +<% end %> + +
 
+<%= render "devise/sign_out" %> + diff --git a/config.ru b/config.ru old mode 100644 new mode 100755 diff --git a/config/application.rb b/config/application.rb old mode 100644 new mode 100755 index dab4cec6..d20ffe3f --- a/config/application.rb +++ b/config/application.rb @@ -1,6 +1,7 @@ require_relative 'boot' require 'rails/all' +require 'factory_bot_rails' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -14,5 +15,9 @@ class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. + config.generators do |g| + g.test_framework :rspec, fixture: true + g.fixture_replacement :factory_bot, dir: 'spec/factories' + end end end diff --git a/config/boot.rb b/config/boot.rb old mode 100644 new mode 100755 diff --git a/config/cable.yml b/config/cable.yml old mode 100644 new mode 100755 diff --git a/config/database.yml b/config/database.yml old mode 100644 new mode 100755 index 16fc6d17..46ae73d4 --- a/config/database.yml +++ b/config/database.yml @@ -20,6 +20,9 @@ default: &default # For details on connection pooling, see Rails configuration guide # http://guides.rubyonrails.org/configuring.html#database-pooling pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + username: postgres + password: password + host: db development: <<: *default diff --git a/config/entrypoint.sh b/config/entrypoint.sh new file mode 100755 index 00000000..ba3868ff --- /dev/null +++ b/config/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +# Remove a potentially pre-existing server.pid for Rails. +rm -f /topnews/tmp/pids/server.pid + +# Then exec the container's main process (what's set as CMD in the Dockerfile). +exec "$@" \ No newline at end of file diff --git a/config/environment.rb b/config/environment.rb old mode 100644 new mode 100755 diff --git a/config/environments/development.rb b/config/environments/development.rb old mode 100644 new mode 100755 diff --git a/config/environments/production.rb b/config/environments/production.rb old mode 100644 new mode 100755 diff --git a/config/environments/test.rb b/config/environments/test.rb old mode 100644 new mode 100755 index 8e5cbde5..7b6dc4c0 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -5,7 +5,7 @@ # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true + config.cache_classes = false # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb old mode 100644 new mode 100755 diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb old mode 100644 new mode 100755 diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb old mode 100644 new mode 100755 diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb old mode 100644 new mode 100755 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb old mode 100644 new mode 100755 diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb old mode 100644 new mode 100755 diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb old mode 100644 new mode 100755 diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb old mode 100644 new mode 100755 diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb old mode 100644 new mode 100755 diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml old mode 100644 new mode 100755 diff --git a/config/locales/en.yml b/config/locales/en.yml old mode 100644 new mode 100755 diff --git a/config/puma.rb b/config/puma.rb old mode 100644 new mode 100755 diff --git a/config/routes.rb b/config/routes.rb old mode 100644 new mode 100755 index c12ef082..c0cac110 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,7 @@ Rails.application.routes.draw do devise_for :users - root to: 'pages#home' + root to: 'pages#index' + + post 'flagged_stories/add/', to: 'flagged_stories#add', as: 'flag_add' + get 'flagged_stories/remove/:id/', to: 'flagged_stories#remove', as: 'flag_remove' end diff --git a/config/secrets.yml b/config/secrets.yml old mode 100644 new mode 100755 diff --git a/config/spring.rb b/config/spring.rb old mode 100644 new mode 100755 diff --git a/db/migrate/20180228212101_devise_create_users.rb b/db/migrate/20180228212101_devise_create_users.rb old mode 100644 new mode 100755 diff --git a/db/migrate/20240829235602_create_flagged_stories.rb b/db/migrate/20240829235602_create_flagged_stories.rb new file mode 100755 index 00000000..ca4e3f9b --- /dev/null +++ b/db/migrate/20240829235602_create_flagged_stories.rb @@ -0,0 +1,15 @@ +class CreateFlaggedStories < ActiveRecord::Migration[7.0] + def change + create_table :flagged_stories do |t| + t.string :title + t.string :url + + t.timestamps + end + + create_join_table :flagged_stories, :users do |t| + t.index [:flagged_story_id, :user_id], unique: true + end + + end +end diff --git a/db/migrate/20240830163814_create_feeds.rb b/db/migrate/20240830163814_create_feeds.rb new file mode 100644 index 00000000..51bc45db --- /dev/null +++ b/db/migrate/20240830163814_create_feeds.rb @@ -0,0 +1,9 @@ +class CreateFeeds < ActiveRecord::Migration[7.0] + def change + create_table :feeds do |t| + t.jsonb :stories + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb old mode 100644 new mode 100755 index acc34f3b..c17ffd5d --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,29 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2018_02_28_212101) do +ActiveRecord::Schema[7.0].define(version: 2024_08_30_163814) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "feeds", force: :cascade do |t| + t.jsonb "stories" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "flagged_stories", force: :cascade do |t| + t.string "title" + t.string "url" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "flagged_stories_users", id: false, force: :cascade do |t| + t.bigint "flagged_story_id", null: false + t.bigint "user_id", null: false + t.index ["flagged_story_id", "user_id"], name: "index_flagged_stories_users_on_flagged_story_id_and_user_id", unique: true + end + create_table "users", force: :cascade do |t| t.string "first_name" t.string "last_name" diff --git a/db/seeds.rb b/db/seeds.rb old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 00000000..8b6353e7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +services: + app: + build: . + image: topnews_app + volumes: + - .:/topnews + - ruby_gems:/usr/local/bundle + ports: + - 3000:3000 + depends_on: + - db + + db: + image: postgres:16.4 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + restart: always + ports: + - 5432:5432 + volumes: + - ./data/postgres:/var/lib/postgresql/data + + chrome: + image: selenium/standalone-chrome:latest # this version should match that of the selenium-webdriver gem (see Gemfile) + volumes: + - /dev/shm:/dev/shm + profiles: + - test + + app_test: + build: . + image: topnews_app + volumes: + - .:/topnews + - ruby_gems:/usr/local/bundle + ports: + - 3000:3000 + environment: + - HUB_URL=http://chrome:4444/wd/hub + depends_on: + - db + - chrome + profiles: + - test + +volumes: + ruby_gems: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..0721cd7a --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +# Remove a potentially pre-existing server.pid for Rails. +rm -f /myapp/tmp/pids/server.pid + +# Then exec the container's main process (what's set as CMD in the Dockerfile). +exec "$@" \ No newline at end of file diff --git a/lib/assets/.keep b/lib/assets/.keep old mode 100644 new mode 100755 diff --git a/lib/tasks/.keep b/lib/tasks/.keep old mode 100644 new mode 100755 diff --git a/log/.keep b/log/.keep old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 diff --git a/public/404.html b/public/404.html old mode 100644 new mode 100755 diff --git a/public/422.html b/public/422.html old mode 100644 new mode 100755 diff --git a/public/500.html b/public/500.html old mode 100644 new mode 100755 diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png old mode 100644 new mode 100755 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png old mode 100644 new mode 100755 diff --git a/public/favicon.ico b/public/favicon.ico old mode 100644 new mode 100755 diff --git a/public/robots.txt b/public/robots.txt old mode 100644 new mode 100755 diff --git a/spec/controllers/flagged_stories_controller_spec.rb b/spec/controllers/flagged_stories_controller_spec.rb new file mode 100644 index 00000000..664508fd --- /dev/null +++ b/spec/controllers/flagged_stories_controller_spec.rb @@ -0,0 +1,96 @@ +require "rails_helper" +require_relative '../support/devise' + +describe FlaggedStoriesController do + let(:sample_title){ 'Super slick story' } + let(:sample_url){ 'https://example.com/story/1/' } + + login + + after(:each) do + expect(response).to redirect_to(root_path) + end + + describe '#add' do + context 'without existing FlaggedStory' do + + before(:each) do + FlaggedStory.destroy_all + end + + it 'creates a FlaggedStory and associates the user' do + expect do + post :add, params: { + title: sample_title, + url: URI::Parser.new.escape(sample_url) + } + end.to change{ FlaggedStory.count }.by(1) + last_flagged = FlaggedStory.last + expect(last_flagged.url).to eq(sample_url) + expect(last_flagged.users).to include(@user) + end + end + + context 'with existing FlaggedStory' do + before(:each) do + FlaggedStory.destroy_all + story = FlaggedStory.new + story.url = sample_url + story.title = sample_title + story.users << create(:user) + story.save + end + + it 'associates the FlaggedStory with the user' do + expect do + post :add, params: { + title: sample_title, + url: URI::Parser.new.escape(sample_url) + } + end.to change{ FlaggedStory.count }.by(0) + last_flagged = FlaggedStory.last + expect(last_flagged.url).to eq(sample_url) + expect(last_flagged.users).to include(@user) + end + end + end + + describe '#remove' do + context 'with the current_user as the only associated user' do + before(:each) do + FlaggedStory.destroy_all + @story = FlaggedStory.new + @story.url = sample_url + @story.title = sample_title + @story.users << @user + @story.save + end + + it 'deletes the FlaggedStory' do + expect do + get :remove, params: {id: @story.id} + end.to change{ FlaggedStory.count }.by(-1) + end + end + + context 'with the current_user as one of many associated users' do + before(:each) do + FlaggedStory.destroy_all + @story = FlaggedStory.new + @story.url = sample_url + @story.title = sample_title + @story.users << @user + @story.users << create(:user) + @story.save + end + + it 'removes the user from FlaggedStory.users' do + expect do + get :remove, params: {id: @story.id} + end.to change{ FlaggedStory.count }.by(0) + @story.users.reload + expect(@story.users).to_not include(@user) + end + end + end +end \ No newline at end of file diff --git a/spec/controllers/pages_controller_spec.rb b/spec/controllers/pages_controller_spec.rb new file mode 100644 index 00000000..39ef66a7 --- /dev/null +++ b/spec/controllers/pages_controller_spec.rb @@ -0,0 +1,45 @@ +require "rails_helper" +require_relative '../support/devise' + +describe PagesController do + describe '#home' do + login + context 'no Feed objects' do + before(:each) do + Feed.destroy_all + end + + it 'creates a new object and assigns it to @latest_feed' do + expect{ get :index }.to change {Feed.count}.by(1) + expect(response).to render_template(:index) + expect(assigns(:latest_feed)).to be_a(Feed) + end + end + + context 'the latest Feed object created over 15 minutes ago' do + before(:each) do + Feed.destroy_all + @old_feed = Feed.fetch_and_persist + @old_feed.created_at = Time.now - 1.hour + @old_feed.save + end + + it 'creates a new object and assigns it to @latest_feed' do + expect{ get :index }.to change {Feed.count}.by(1) + expect(assigns(:latest_feed)).to_not eq(@old_feed) + end + end + + context 'the latest Feed object created under 15 minutes ago' do + before(:each) do + Feed.destroy_all + @new_feed = Feed.fetch_and_persist + end + + it 'assigns it to @latest_feed' do + expect{ get :index }.to change {Feed.count}.by(0) + expect(assigns(:latest_feed)).to eq(@new_feed) + end + end + end +end \ No newline at end of file diff --git a/spec/factories/feeds.rb b/spec/factories/feeds.rb new file mode 100644 index 00000000..3e137c1c --- /dev/null +++ b/spec/factories/feeds.rb @@ -0,0 +1,29 @@ +FactoryBot.define do + factory :feed do + after(:build) do |feed| + feed.stories = [] + Feed::STORIES_TO_PERSIST.times do |i| + feed.stories << { + by: "user #{i}", + descendants: 22, + id: i, + kids: [ + 41402953, + 41402728, + 41402746, + 41402906, + 41402690, + 41402776, + 41402716, + 41402914 + ], + score: 56, + time: 1725036337, + title: "Story #{i}", + type: "story", + url: "https://example.com/story/#{i}/" + } + end + end + end +end diff --git a/spec/factories/flagged_stories.rb b/spec/factories/flagged_stories.rb new file mode 100755 index 00000000..4020a026 --- /dev/null +++ b/spec/factories/flagged_stories.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :flagged_story do + sequence(:title) {|n| "Story #{n}/" } + sequence(:url) {|n| "https://example.com/story#{n}/" } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100755 index 00000000..744d5fa3 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :user do + first_name { "John" } + last_name { "Doe" } + sequence(:email) {|i| "j.doe#{i}@example.com" } + password { 'password123' } + password_confirmation { password } + end +end \ No newline at end of file diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb new file mode 100755 index 00000000..1aca2271 --- /dev/null +++ b/spec/features/login_spec.rb @@ -0,0 +1,53 @@ +require "rails_helper" + +feature "Feature: User login/out", type: :system do + include Devise::Test::IntegrationHelpers + shared_examples :login_attempt do + scenario 'login attempt' do + visit root_path + + expect(page.current_path).to eq '/users/sign_in' + + fill_in('Email', with: email) + fill_in('Password', with: password) + click_on 'Log in' + + expect(page.current_path).to eq expected_path + end + end + + context 'valid user' do + before :all do + User.destroy_all # Until Database Cleaner or other methodology formalized + @user_attrs = attributes_for(:user) + User.create(@user_attrs) + end + + describe 'CAN authenticate' do + let!(:email) { @user_attrs[:email] } + let!(:password) { @user_attrs[:password] } + let!(:expected_path) { '/' } + include_examples :login_attempt + + end + + scenario 'can logout' do + user = User.first + sign_in user + + visit root_path + click_on 'Log out' + + expect(page.current_path).to eq new_user_session_path + end + end + + context 'invalid user' do + describe 'CANNOT authenticate' do + let!(:email) { 'nonexistent@example.com' } + let!(:password) { 'foo-bar' } + let!(:expected_path) { '/users/sign_in' } + include_examples :login_attempt + end + end +end diff --git a/spec/features/stories_spec.rb b/spec/features/stories_spec.rb new file mode 100644 index 00000000..f471bb56 --- /dev/null +++ b/spec/features/stories_spec.rb @@ -0,0 +1,41 @@ +require "rails_helper" + +feature "Feature: stories", type: :system do + include Devise::Test::IntegrationHelpers + + before(:each) do + FlaggedStory.destroy_all + User.destroy_all + @user = create(:user) + end + + let!(:feed){ create(:feed) } + + scenario 'view latest' do + sign_in @user + + visit root_path + feed.stories.each do |story| + expect(page).to have_selector(:xpath, "//a[@href='#{story['url']}' and text()='#{story['title']}']") + end + end + + scenario 'flag / unflag' do + sign_in @user + + visit root_path + + expect(page).to_not have_content('Flagged stories') + + find_button('FLAG', match: :first).click + + expect(page).to have_content('Your flag was added to the story') + expect(page).to have_content('Flagged stories') + expect(page).to have_content("Flagged by: #{@user.full_name}") + + click_link '[Unflag]' + + expect(page).to have_content('Your flag was removed from the story') + expect(page).to_not have_content('Flagged stories') + end +end \ No newline at end of file diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb new file mode 100644 index 00000000..4a752402 --- /dev/null +++ b/spec/models/feed_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +describe Feed do + context 'Constants' do + it 'STORIES_TO_PERSIST' do + expect(described_class::STORIES_TO_PERSIST).to eq 25 + end + end + + context 'Attributes' do + it { expect(subject).to have_db_column(:stories).of_type(:jsonb) } + it { expect(subject).to have_db_column(:created_at).of_type(:datetime) } + + describe '.stories' do + it 'acts as an Array of Hashes' do + obj = described_class.new + expect(obj.stories).to be_nil + + stories = [] + 2.times do |i| + stories << { + by: "user #{i}", + descendants: 22, + id: i, + kids: [ + 41402953, + 41402728, + 41402746, + 41402906, + 41402690, + 41402776, + 41402716, + 41402914 + ], + score: 56, + time: 1725036337, + title: "Story #{i}", + type: "story", + url: "https://example.com/story/#{i}/" + } + end + + obj.stories = stories + obj.save + expect(obj.stories.count).to eq 2 + expect(obj.stories.first.class).to eq Hash + end + end + end + + context 'Methods' do + context ':: Class' do + describe '::fetch_and_persist' do + it 'creates a new object' do + expect { described_class.fetch_and_persist }.to change { described_class.count }.by(1) + end + + it 'returns an object with the .stories array populated' do + feed = described_class.fetch_and_persist + expect(feed.stories.count).to eq described_class::STORIES_TO_PERSIST + + expect(feed.stories.first['url']).to match /https?:\/\/.*/ + end + end + end + end +end diff --git a/spec/models/flagged_story_spec.rb b/spec/models/flagged_story_spec.rb new file mode 100755 index 00000000..8ba8e5af --- /dev/null +++ b/spec/models/flagged_story_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe FlaggedStory, type: :model do + context 'Attributes' do + it { expect(subject).to have_db_column(:title).of_type(:string) } + it { expect(subject).to have_db_column(:url).of_type(:string) } + it { expect(subject).to have_db_column(:created_at).of_type(:datetime) } + it { expect(subject).to have_db_column(:updated_at).of_type(:datetime) } + end + + context 'Validations' do + it { expect(subject).to validate_presence_of(:title) } + it { expect(subject).to validate_presence_of(:url) } + end + + context 'Associations' do + describe 'HABTM' do + it { should have_and_belong_to_many(:users).validate(true) } + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb old mode 100644 new mode 100755 index b51dc1c3..203f9771 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,17 +1,48 @@ require 'rails_helper' describe User do - context "creating a new user" do - let(:attrs) do - { first_name: :foo, last_name: :bar, email: 'f@b.c', password: 'foobar123' } + context 'Attributes' do + it { expect(subject).to have_db_column(:first_name).of_type(:string) } + it { expect(subject).to have_db_column(:last_name).of_type(:string) } + it { expect(subject).to have_db_column(:email).of_type(:string) } + it { expect(subject).to have_db_column(:encrypted_password).of_type(:string) } + it { expect(subject).to respond_to(:password) } + end + + context 'Validations' do + it { expect(subject).to validate_presence_of(:email) } + it { expect(subject).to validate_uniqueness_of(:email).case_insensitive } + it { expect(subject).to validate_presence_of(:first_name) } + it { expect(subject).to validate_presence_of(:last_name) } + end + + context 'Methods' do + context '# Instance' do + describe '.full_name' do + it 'concatenates :first_name & :last_name' do + user = build(:user, first_name: 'Bob', last_name: 'Jones') + expect(user.full_name).to eq('Bob Jones') + end + end end + end - it "should have first, last, email" do - expect { User.create(attrs) }.to change{ User.count }.by(1) + context 'Associations' do + describe 'HABTM' do + it { should have_and_belong_to_many(:flagged_stories).validate(true) } end + end - it "should require a password" do - expect(User.new(attrs.except(:password))).to be_invalid + context 'Lifecycle' do + describe 'creation' do + let(:attrs) do + { first_name: :foo, last_name: :bar, email: 'f@b.c', password: 'foobar123' } + end + + it "should require a password" do + expect(User.new(attrs.except(:password))).to be_invalid + end end end + end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb old mode 100644 new mode 100755 index bbe1ba57..f6b0287e --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -5,6 +5,10 @@ # Prevent database truncation if the environment is production abort("The Rails environment is running in production mode!") if Rails.env.production? require 'rspec/rails' +require 'support/factory_bot' + +require 'capybara/rspec' +require 'selenium-webdriver' # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in @@ -28,12 +32,12 @@ RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = "#{::Rails.root}/spec/fixtures" + #config.fixture_path = "#{::Rails.root}/spec/fixtures" # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. - config.use_transactional_fixtures = true + #config.use_transactional_fixtures = true # RSpec Rails can automatically mix in different behaviours to your tests # based on their file location, for example enabling you to call `get` and @@ -54,4 +58,40 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include Devise::Test::ControllerHelpers, type: :controller + config.include Warden::Test::Helpers + config.include Devise::Test::IntegrationHelpers, type: :feature +end + +Capybara.register_driver :chrome_headless do |app| + chrome_capabilities = ::Selenium::WebDriver::Chrome::Options.new('goog:chromeOptions' => { 'args': %w[no-sandbox headless disable-gpu window-size=1400,1400] }) + + if ENV['HUB_URL'] + Capybara::Selenium::Driver.new(app, + browser: :remote, + url: ENV['HUB_URL'], + options: chrome_capabilities) + else + Capybara::Selenium::Driver.new(app, + browser: :chrome, + options: chrome_capabilities) + end +end + +RSpec.configure do |config| + config.before(:each, type: :system) do + driven_by :chrome_headless + + Capybara.app_host = "http://#{IPSocket.getaddress(Socket.gethostname)}:3000" + Capybara.server_host = IPSocket.getaddress(Socket.gethostname) + Capybara.server_port = 3000 + end +end + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb old mode 100644 new mode 100755 diff --git a/spec/support/controller_macros.rb b/spec/support/controller_macros.rb new file mode 100644 index 00000000..36dc64f8 --- /dev/null +++ b/spec/support/controller_macros.rb @@ -0,0 +1,10 @@ +module ControllerMacros + def login + before(:each) do + User.destroy_all #TODO: install DatabaseCleaner + @user = FactoryBot.create(:user) + @request.env['devise.mapping'] = Devise.mappings[:user] + sign_in @user + end + end +end \ No newline at end of file diff --git a/spec/support/devise.rb b/spec/support/devise.rb new file mode 100644 index 00000000..9fb910dc --- /dev/null +++ b/spec/support/devise.rb @@ -0,0 +1,6 @@ +require_relative './controller_macros' + +RSpec.configure do |config| + config.include Devise::Test::ControllerHelpers, type: :controller + config.extend ControllerMacros, type: :controller +end \ No newline at end of file diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100755 index 00000000..329748fc --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end \ No newline at end of file diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb old mode 100644 new mode 100755 diff --git a/test/controllers/.keep b/test/controllers/.keep old mode 100644 new mode 100755 diff --git a/test/fixtures/.keep b/test/fixtures/.keep old mode 100644 new mode 100755 diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep old mode 100644 new mode 100755 diff --git a/test/helpers/.keep b/test/helpers/.keep old mode 100644 new mode 100755 diff --git a/test/integration/.keep b/test/integration/.keep old mode 100644 new mode 100755 diff --git a/test/mailers/.keep b/test/mailers/.keep old mode 100644 new mode 100755 diff --git a/test/models/.keep b/test/models/.keep old mode 100644 new mode 100755 diff --git a/test/system/.keep b/test/system/.keep old mode 100644 new mode 100755 diff --git a/test/test_helper.rb b/test/test_helper.rb old mode 100644 new mode 100755 diff --git a/tmp/.keep b/tmp/.keep old mode 100644 new mode 100755 diff --git a/vendor/.keep b/vendor/.keep old mode 100644 new mode 100755