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