From 971194034ec326e02ee1603b275c88328352ca45 Mon Sep 17 00:00:00 2001 From: kainacrow <33405173+kainacrow@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:21:48 -0400 Subject: [PATCH 1/2] top news feed + favorites list --- Gemfile | 16 ++++- Gemfile.lock | 38 ++++++++++ app/assets/images/star-empty-icon.png | Bin 0 -> 17832 bytes app/assets/images/star-gold-icon.png | Bin 0 -> 2296 bytes app/assets/stylesheets/application.css | 16 +++++ app/controllers/application_controller.rb | 4 ++ app/controllers/flags_controller.rb | 23 ++++++ app/controllers/stories_controller.rb | 14 ++++ app/models/flag.rb | 4 ++ app/models/story.rb | 7 ++ app/models/user.rb | 6 ++ app/services/hacker_news_service.rb | 25 +++++++ app/views/layouts/application.html.erb | 13 ++++ app/views/stories/_story.html.erb | 14 ++++ app/views/stories/flagged_stories.html.erb | 19 +++++ app/views/stories/index.html.erb | 6 ++ config/application.rb | 3 + config/environments/test.rb | 2 +- config/routes.rb | 6 ++ db/migrate/20240704180117_create_stories.rb | 12 ++++ db/migrate/20240704182024_create_flags.rb | 10 +++ db/schema.rb | 21 +++++- spec/controllers/flags_controller_spec.rb | 74 ++++++++++++++++++++ spec/controllers/stories_controller_spec.rb | 36 ++++++++++ spec/factories/flags.rb | 6 ++ spec/factories/stories.rb | 7 ++ spec/factories/users.rb | 7 ++ spec/models/flag_spec.rb | 8 +++ spec/models/story_spec.rb | 16 +++++ spec/rails_helper.rb | 35 ++++++--- spec/services/hacker_news_service_spec.rb | 47 +++++++++++++ spec/spec_helper.rb | 6 +- spec/views/stories/index.html.erb_spec.rb | 5 ++ 33 files changed, 491 insertions(+), 15 deletions(-) create mode 100644 app/assets/images/star-empty-icon.png create mode 100644 app/assets/images/star-gold-icon.png create mode 100644 app/controllers/flags_controller.rb create mode 100644 app/controllers/stories_controller.rb create mode 100644 app/models/flag.rb create mode 100644 app/models/story.rb create mode 100644 app/services/hacker_news_service.rb create mode 100644 app/views/stories/_story.html.erb create mode 100644 app/views/stories/flagged_stories.html.erb create mode 100644 app/views/stories/index.html.erb create mode 100644 db/migrate/20240704180117_create_stories.rb create mode 100644 db/migrate/20240704182024_create_flags.rb create mode 100644 spec/controllers/flags_controller_spec.rb create mode 100644 spec/controllers/stories_controller_spec.rb create mode 100644 spec/factories/flags.rb create mode 100644 spec/factories/stories.rb create mode 100644 spec/factories/users.rb create mode 100644 spec/models/flag_spec.rb create mode 100644 spec/models/story_spec.rb create mode 100644 spec/services/hacker_news_service_spec.rb create mode 100644 spec/views/stories/index.html.erb_spec.rb diff --git a/Gemfile b/Gemfile index 5a8ffc43..bb55027f 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,6 @@ gem 'pg' gem 'pry-rails' gem 'puma' gem 'rails', '~> 7.0.3' -gem 'rspec-rails' gem 'sass-rails' gem 'selenium-webdriver', group: [:development, :test] gem 'spring', group: :development @@ -20,3 +19,18 @@ gem 'turbolinks' gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'uglifier' gem 'web-console', group: :development +gem 'httparty' +gem 'pry-byebug' + +group :test do + gem 'rspec-rails' + gem 'factory_bot_rails' + gem 'faker' + gem 'database_cleaner' + gem 'rails-controller-testing' + gem 'shoulda-matchers', '~> 4.0' +end + +group :test, :development do + gem 'database_cleaner-active_record' +end diff --git a/Gemfile.lock b/Gemfile.lock index 14ec6457..ce9f712a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,6 +69,7 @@ GEM addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) bcrypt (3.1.18) + bigdecimal (3.1.8) bindex (0.8.1) builder (3.2.4) byebug (11.1.3) @@ -92,6 +93,13 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.1.10) crass (1.0.6) + csv (3.3.0) + database_cleaner (2.0.2) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.1.0) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) devise (4.8.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -102,9 +110,20 @@ GEM digest (3.1.0) erubi (1.11.0) execjs (2.8.1) + factory_bot (6.4.6) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) + railties (>= 5.0.0) + faker (3.4.1) + i18n (>= 1.8.11, < 2) ffi (1.15.5) globalid (1.0.0) activesupport (>= 5.0) + httparty (0.22.0) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) i18n (1.12.0) concurrent-ruby (~> 1.0) jbuilder (2.11.5) @@ -124,6 +143,8 @@ GEM mini_mime (1.1.2) mini_portile2 (2.8.0) minitest (5.16.3) + multi_xml (0.7.1) + bigdecimal (~> 3.1) net-imap (0.2.3) digest net-protocol @@ -147,6 +168,9 @@ GEM pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) + pry-byebug (3.10.1) + byebug (~> 11.0) + pry (>= 0.13, < 0.15) pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (5.0.0) @@ -170,6 +194,10 @@ GEM activesupport (= 7.0.4) bundler (>= 1.15.0) railties (= 7.0.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.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -224,6 +252,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + shoulda-matchers (4.5.1) + activesupport (>= 4.2.0) spring (4.1.0) sprockets (4.1.1) concurrent-ruby (~> 1.0) @@ -265,16 +295,24 @@ DEPENDENCIES byebug capybara coffee-rails + database_cleaner + database_cleaner-active_record devise + factory_bot_rails + faker + httparty jbuilder listen pg + pry-byebug pry-rails puma rails (~> 7.0.3) + rails-controller-testing rspec-rails sass-rails selenium-webdriver + shoulda-matchers (~> 4.0) spring turbolinks tzinfo-data diff --git a/app/assets/images/star-empty-icon.png b/app/assets/images/star-empty-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1c7ad3725e1f17a596d1e20d67ced44f7112099f GIT binary patch literal 17832 zcmY*>bzD>5ANMvcunEWj0VQ;FI8s8=QPLqH9STTEgOtD~A_|I>q`*YFQyL})rGSK> zq|zZEDgE4`-{12*fAHFN@44rE&L`fVI+rJUI;vD>n9e{T5Gr*wWqk+)27ZMu!O6f6 zFWsFm;xBJ~RYgc?56dF>L)qR${f@RagdhA4hd?8oAOLX)_{RkPK_Db}D1-$3gc7ge zVgKEQLGUF1eTOa)2d=@-8$lo#h`O?Zp+EFjy7dZcyYHmE5jD{Dz*#BBrwbx}sZGp_ za83h1a_2UMPLVv4_uM&~Gwg4BT@xBAZR8C}A1bPR
4GWsj7%t{qJs7@#ot1beZ}9}VHDByIo#+eGsIsMz*;<*umV
zL3h9WNgU?1oZ<^k$)Rm~+rVqAI(SIYP!p4AjFvDW(PIFK#hAfl7=rDhsxs7HSCp&s
zz13dVisuUJCn{?fx5vlIOWR}ydvR%*9|^g$c&AI_+Uru?uO
<%= notice %>
hU6{v2|x9!CCM|bm4vdukFpAMP;X@B1DH|u)C5b*vgJ`iTVEN$hbt$
zEBPSeE9tCufR>R^^R%2p1Ixw2`|F#s0@jt)VA+fC6d1e^;EQc|hc>l)kia)wHK1uK
zLGuYjx3EOg9+P}~Qw;YnrbJRJVLAP;UuNrujQ>6*dsLp8EHXK9!%5XodoILxOtp!v
zCnjTKMQQ;}jp1u?;R)`XfIT-qHt;Z|&)eq*u`&4x_ESs|lUboH*FB%xGfrIUA6lY}
zbGMl87v_+3!^qgG^0obZyQixXbK*aJxW0k(Kj0F)`0S?Bf_hlsf+k;o%0Zd=qvrED
zTB^C5R~YOx%2b>!82VUimaO=)MAz>}E_CV>VszokJnK*Gg=g_qEVUP-4I&pxujhW7
zJxn@mHuOH}+t;72B!nrwp!S&HYJanF7ZThF0}4@?cg;2DMRq5
+ <% if current_user.flagged_stories.exists?(story['id']) %>
+ <%= form_with url: story_flag_path(story['id']), method: :delete, local: true, class: 'inline-form' do %>
+ <%= image_tag 'star-gold-icon.png', alt: 'Unflag', data: { confirm: 'Are you sure you want to unflag this story?' }, class: 'flag-icon', onclick: 'this.closest("form").submit()' %>
+ <% end %>
+ <% else %>
+ <%= form_with url: story_flag_path(story['id']), method: :post, local: true, class: 'inline-form' do %>
+ <%= image_tag 'star-empty-icon.png', alt: 'Flag', class: 'flag-icon', onclick: 'this.closest("form").submit()' %>
+ <% end %>
+ <% end %>
+ <%= story['title'] %>
+
+Flagged Stories
+
+ <% @flagged_stories.each do |story_id, flags| %>
+
diff --git a/app/views/stories/index.html.erb b/app/views/stories/index.html.erb
new file mode 100644
index 00000000..8905a179
--- /dev/null
+++ b/app/views/stories/index.html.erb
@@ -0,0 +1,6 @@
+
+ <% if current_user.flagged_stories.exists?(story_id) %>
+ <%= form_with url: story_flag_path(Story.find(story_id)), method: :delete, local: true, class: 'inline-form' do |form| %>
+ <%= image_tag 'star-gold-icon.png', alt: 'Unflag', data: { confirm: 'Are you sure you want to unflag this story?' }, class: 'flag-icon', onclick: 'this.closest("form").submit()' %>
+ <% end %>
+ <% else %>
+ <%= form_with url: story_flag_path(Story.find(story_id)), method: :post, local: true, class: 'inline-form' do %>
+ <%= image_tag 'star-empty-icon.png', alt: 'Flag', class: 'flag-icon', onclick: 'this.closest("form").submit()' %>
+ <% end %>
+ <% end %>
+ <%= flags.first.story.title %> - flagged by <%= flags.map(&:user).map(&:name).to_sentence %>
+
+ Top Hacker News Stories
+
+ <% @stories.each do |story| %>
+ <%= render partial: 'story', locals: { story: story } %>
+ <% end %>
+
diff --git a/config/application.rb b/config/application.rb
index dab4cec6..eb28291e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -11,6 +11,9 @@ class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.1
+ config.active_record.legacy_connection_handling = false
+ config.action_controller.urlsafe_csrf_tokens = true
+
# 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.
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 8e5cbde5..7b6dc4c0 100644
--- 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/routes.rb b/config/routes.rb
index c12ef082..929e59d4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,10 @@
Rails.application.routes.draw do
devise_for :users
root to: 'pages#home'
+
+ get 'flagged_stories', to: 'stories#flagged_stories', as: 'flagged_stories'
+
+ resources :stories do
+ resource :flag, only: [:create, :destroy]
+ end
end
diff --git a/db/migrate/20240704180117_create_stories.rb b/db/migrate/20240704180117_create_stories.rb
new file mode 100644
index 00000000..725c5b75
--- /dev/null
+++ b/db/migrate/20240704180117_create_stories.rb
@@ -0,0 +1,12 @@
+class CreateStories < ActiveRecord::Migration[7.0]
+ def change
+ create_table :stories do |t|
+ t.string :title
+ t.string :url
+
+ t.timestamps
+ end
+
+ add_index :stories, [:title, :url], unique: true
+ end
+end
diff --git a/db/migrate/20240704182024_create_flags.rb b/db/migrate/20240704182024_create_flags.rb
new file mode 100644
index 00000000..3e72d59d
--- /dev/null
+++ b/db/migrate/20240704182024_create_flags.rb
@@ -0,0 +1,10 @@
+class CreateFlags < ActiveRecord::Migration[6.1]
+ def change
+ create_table :flags do |t|
+ t.references :user, null: false, foreign_key: true
+ t.references :story, null: false, foreign_key: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index acc34f3b..71ad1a14 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,10 +10,27 @@
#
# 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_07_04_182024) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
+ create_table "flags", force: :cascade do |t|
+ t.bigint "user_id", null: false
+ t.bigint "story_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["story_id"], name: "index_flags_on_story_id"
+ t.index ["user_id"], name: "index_flags_on_user_id"
+ end
+
+ create_table "stories", force: :cascade do |t|
+ t.string "title"
+ t.string "author"
+ t.string "url"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "users", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
@@ -33,4 +50,6 @@
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
+ add_foreign_key "flags", "stories"
+ add_foreign_key "flags", "users"
end
diff --git a/spec/controllers/flags_controller_spec.rb b/spec/controllers/flags_controller_spec.rb
new file mode 100644
index 00000000..6b9a4173
--- /dev/null
+++ b/spec/controllers/flags_controller_spec.rb
@@ -0,0 +1,74 @@
+require 'rails_helper'
+
+RSpec.describe FlagsController, type: :controller do
+ let(:user) { create(:user) }
+ let(:story) { create(:story, title: 'New Story', url: 'www.example.com') }
+
+ before do
+ sign_in user
+ end
+
+ describe 'POST #create' do
+ context 'with a valid story' do
+ it 'does not create a new story if it already exists' do
+ expect(story).to be_persisted
+ expect {
+ post :create, params: { story_id: story.id }
+ }.to change(Story, :count).by(0)
+ end
+
+ it 'creates a new flag record for the story' do
+ expect {
+ post :create, params: { story_id: story.id }
+ }.to change(Flag, :count).by(1)
+ end
+
+ it 'redirects to the stories index page' do
+ post :create, params: { story_id: story.id }
+ expect(response).to redirect_to(stories_path)
+ end
+
+ it 'sets a flash notice on success' do
+ post :create, params: { story_id: story.id }
+ expect(flash[:notice]).to eq('Story flagged successfully.')
+ end
+ end
+
+ context 'with an invalid story' do
+ it 'does not create a new flag record' do
+ expect {
+ post :create, params: { story_id: 999000000000000 }
+ }.to_not change(Flag, :count)
+ end
+
+ it 'redirects to the stories index page with an alert message' do
+ post :create, params: { story_id: 999000000000000 }
+ expect(response).to redirect_to(stories_path)
+ expect(flash[:alert]).to eq('Story not found.')
+ end
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ before do
+ current_user = user
+ current_user.flagged_stories << story
+ end
+
+ it 'removes a flag for the story' do
+ expect {
+ delete :destroy, params: { story_id: story.id }
+ }.to change(Flag, :count).by(-1)
+ end
+
+ it 'redirects to the stories index page' do
+ delete :destroy, params: { story_id: story.id }
+ expect(response).to redirect_to(stories_path)
+ end
+
+ it 'sets a flash notice on success' do
+ delete :destroy, params: { story_id: story.id }
+ expect(flash[:notice]).to eq('Story unflagged successfully.')
+ end
+ end
+end
diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb
new file mode 100644
index 00000000..7b27671c
--- /dev/null
+++ b/spec/controllers/stories_controller_spec.rb
@@ -0,0 +1,36 @@
+require 'rails_helper'
+
+RSpec.describe StoriesController, type: :controller do
+ let(:user) { create(:user) }
+ let(:story) { create(:story) }
+
+ before do
+ sign_in user
+ allow(HackerNewsService).to receive_message_chain(:new, :fetch_top_stories).and_return([story])
+ end
+
+ describe 'before actions' do
+ it 'calls #load_stories' do
+ expect(controller).to receive(:load_stories)
+ get :index
+ end
+
+ it 'calls #flagged_stories' do
+ expect(controller).to receive(:flagged_stories)
+ get :index
+ end
+ end
+
+ describe 'private methods' do
+ it 'correctly sets @stories' do
+ get :index
+ expect(assigns(:stories)).to include(story)
+ end
+
+ it 'correctly sets @flagged_stories' do
+ Flag.create(user: user, story: story)
+ get :index
+ expect(assigns(:flagged_stories)).to eq({ story.id => [Flag.last] })
+ end
+ end
+end
diff --git a/spec/factories/flags.rb b/spec/factories/flags.rb
new file mode 100644
index 00000000..0ddfc123
--- /dev/null
+++ b/spec/factories/flags.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :flag do
+ association :user
+ association :story
+ end
+end
diff --git a/spec/factories/stories.rb b/spec/factories/stories.rb
new file mode 100644
index 00000000..55a34d42
--- /dev/null
+++ b/spec/factories/stories.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :story do
+ sequence(:id) { |n| n }
+ title { 'Example Story' }
+ url { 'https://example.com' }
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
new file mode 100644
index 00000000..a38fda71
--- /dev/null
+++ b/spec/factories/users.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :user do
+ email { 'testuser@example.com' }
+ password { 'password123' }
+ password_confirmation { 'password123' }
+ end
+end
diff --git a/spec/models/flag_spec.rb b/spec/models/flag_spec.rb
new file mode 100644
index 00000000..eb6106a8
--- /dev/null
+++ b/spec/models/flag_spec.rb
@@ -0,0 +1,8 @@
+require 'rails_helper'
+
+RSpec.describe Flag, type: :model do
+ describe 'associations' do
+ it { should belong_to(:user) }
+ it { should belong_to(:story) }
+ end
+end
diff --git a/spec/models/story_spec.rb b/spec/models/story_spec.rb
new file mode 100644
index 00000000..045a6398
--- /dev/null
+++ b/spec/models/story_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+RSpec.describe Story, type: :model do
+ subject { build(:story) }
+
+ describe 'associations' do
+ it { should have_many(:flags).dependent(:destroy) }
+ it { should have_many(:flagged_by_users).through(:flags).source(:user) }
+ end
+
+ describe 'validations' do
+ it { should validate_presence_of(:title) }
+ it { should validate_uniqueness_of(:title).scoped_to(:url) }
+ it { should validate_presence_of(:url) }
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index bbe1ba57..a9c81561 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -1,10 +1,14 @@
# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
-require File.expand_path('../../config/environment', __FILE__)
+require_relative '../config/environment'
# 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 'rails-controller-testing'
+
+Rails::Controller::Testing.install
# Add additional requires below this line. Rails is not loaded until this point!
# Requires supporting ruby files with custom matchers and macros, etc, in
@@ -20,21 +24,29 @@
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
-# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
+# Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f }
# Checks for pending migrations and applies them before tests are run.
-# If you are not using ActiveRecord, you can remove this line.
-ActiveRecord::Migration.maintain_test_schema!
-
+# If you are not using ActiveRecord, you can remove these lines.
+begin
+ ActiveRecord::Migration.maintain_test_schema!
+rescue ActiveRecord::PendingMigrationError => e
+ abort e.to_s.strip
+end
RSpec.configure do |config|
+ config.include FactoryBot::Syntax::Methods
+ config.include Devise::Test::ControllerHelpers, type: :controller
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
- config.fixture_path = "#{::Rails.root}/spec/fixtures"
+ config.fixture_path = Rails.root.join('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
+ # You can uncomment this line to turn off ActiveRecord support entirely.
+ # config.use_active_record = false
+
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
@@ -42,12 +54,12 @@
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
- # RSpec.describe UsersController, :type => :controller do
+ # RSpec.describe UsersController, type: :controller do
# # ...
# end
#
# The different available types are documented in the features, such as in
- # https://relishapp.com/rspec/rspec-rails/docs
+ # https://rspec.info/features/6-0/rspec-rails
config.infer_spec_type_from_file_location!
# Filter lines from Rails gems in backtraces.
@@ -55,3 +67,10 @@
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
end
+
+Shoulda::Matchers.configure do |config|
+ config.integrate do |with|
+ with.test_framework :rspec
+ with.library :rails
+ end
+end
diff --git a/spec/services/hacker_news_service_spec.rb b/spec/services/hacker_news_service_spec.rb
new file mode 100644
index 00000000..25b2913f
--- /dev/null
+++ b/spec/services/hacker_news_service_spec.rb
@@ -0,0 +1,47 @@
+require 'rails_helper'
+
+RSpec.describe HackerNewsService, type: :service do
+ let(:service) { described_class.new }
+
+ before do
+ allow(service).to receive(:fetch_story_ids).and_return([1, 2, 3])
+ allow(service).to receive(:fetch_story).with(1).and_return({ 'id' => 1, 'title' => 'Story 1', 'url' => 'https://example.com/1' })
+ allow(service).to receive(:fetch_story).with(2).and_return({ 'id' => 2, 'title' => 'Story 2', 'url' => 'https://example.com/2' })
+ allow(service).to receive(:fetch_story).with(3).and_return({ 'id' => 3, 'title' => 'Story 3', 'url' => 'https://example.com/3' })
+ end
+
+ describe '#fetch_top_stories' do
+ it 'fetches the top stories from the API' do
+ stories = service.fetch_top_stories
+
+ expect(stories.size).to eq(3)
+ expect(stories.first).to include('id' => 1, 'title' => 'Story 1', 'url' => 'https://example.com/1')
+ expect(stories.second).to include('id' => 2, 'title' => 'Story 2', 'url' => 'https://example.com/2')
+ expect(stories.third).to include('id' => 3, 'title' => 'Story 3', 'url' => 'https://example.com/3')
+ end
+
+ it 'fetches only the number of stories specified by the limit' do
+ stories = service.fetch_top_stories(2)
+
+ expect(stories.size).to eq(2)
+ expect(stories.first['id']).to eq(1)
+ expect(stories.second['id']).to eq(2)
+ end
+ end
+
+ describe '#fetch_story_ids' do
+ it 'fetches the story IDs' do
+ ids = service.send(:fetch_story_ids)
+
+ expect(ids).to eq([1, 2, 3])
+ end
+ end
+
+ describe '#fetch_story' do
+ it 'fetches a single story' do
+ story = service.send(:fetch_story, 1)
+
+ expect(story).to include('id' => 1, 'title' => 'Story 1', 'url' => 'https://example.com/1')
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ce33d66d..327b58ea 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -12,7 +12,7 @@
# the additional setup, and require it from the spec files that actually need
# it.
#
-# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
@@ -61,9 +61,7 @@
# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
- # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
- # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
- # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
+ # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
config.disable_monkey_patching!
# Many RSpec users commonly either run the entire suite or an individual
diff --git a/spec/views/stories/index.html.erb_spec.rb b/spec/views/stories/index.html.erb_spec.rb
new file mode 100644
index 00000000..7227ec6d
--- /dev/null
+++ b/spec/views/stories/index.html.erb_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe "stories/index.html.erb", type: :view do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
From aae6fc6590cd3b6e6ee41206fcf7e02f82308038 Mon Sep 17 00:00:00 2001
From: kainacrow <33405173+kainacrow@users.noreply.github.com>
Date: Mon, 8 Jul 2024 23:50:13 -0400
Subject: [PATCH 2/2] clean up
---
spec/spec_helper.rb | 6 ++++--
spec/views/stories/index.html.erb_spec.rb | 5 -----
2 files changed, 4 insertions(+), 7 deletions(-)
delete mode 100644 spec/views/stories/index.html.erb_spec.rb
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 327b58ea..ce33d66d 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -12,7 +12,7 @@
# the additional setup, and require it from the spec files that actually need
# it.
#
-# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
@@ -61,7 +61,9 @@
# Limits the available syntax to the non-monkey patched syntax that is
# recommended. For more details, see:
- # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
config.disable_monkey_patching!
# Many RSpec users commonly either run the entire suite or an individual
diff --git a/spec/views/stories/index.html.erb_spec.rb b/spec/views/stories/index.html.erb_spec.rb
deleted file mode 100644
index 7227ec6d..00000000
--- a/spec/views/stories/index.html.erb_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe "stories/index.html.erb", type: :view do
- pending "add some examples to (or delete) #{__FILE__}"
-end