From 552977c76a4042be2cc37b31d008f5716b751bce Mon Sep 17 00:00:00 2001 From: Dean Lofts Date: Fri, 23 Aug 2024 20:11:58 +1000 Subject: [PATCH 1/2] add model testing --- Gemfile | 1 + Gemfile.lock | 3 ++ app/models/user.rb | 11 +++-- spec/factories/achievements.rb | 11 +++++ spec/factories/links.rb | 10 ++++ spec/factories/users.rb | 10 ++-- spec/models/achievement_spec.rb | 15 ++++++ spec/models/achievement_view_spec.rb | 8 +++- spec/models/daily_metric_spec.rb | 7 ++- spec/models/link_click_spec.rb | 8 +++- spec/models/link_spec.rb | 26 +++++++++++ spec/models/page_view_spec.rb | 7 ++- spec/models/user_spec.rb | 70 ++++++++++++++++++++++++++++ spec/rails_helper.rb | 8 ++++ 14 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 spec/factories/achievements.rb create mode 100644 spec/factories/links.rb create mode 100644 spec/models/achievement_spec.rb create mode 100644 spec/models/link_spec.rb create mode 100644 spec/models/user_spec.rb diff --git a/Gemfile b/Gemfile index 6f6930d..db09839 100644 --- a/Gemfile +++ b/Gemfile @@ -63,6 +63,7 @@ group :development, :test do gem 'debug', platforms: %i[mri windows] gem 'factory_bot_rails' gem 'rspec-rails', '~> 6.0.0' + gem 'shoulda-matchers' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index e14daad..28679b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -295,6 +295,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + shoulda-matchers (6.4.0) + activesupport (>= 5.2.0) sidekiq (7.3.1) concurrent-ruby (< 2) connection_pool (>= 2.3.0) @@ -393,6 +395,7 @@ DEPENDENCIES redis (>= 4.0.1) rspec-rails (~> 6.0.0) selenium-webdriver + shoulda-matchers sidekiq sidekiq-scheduler sprockets-rails diff --git a/app/models/user.rb b/app/models/user.rb index 4972d67..d5ef75e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,4 @@ +# app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable @@ -10,10 +11,10 @@ class User < ApplicationRecord has_many :link_clicks has_many :achievement_views - validates :username, presence: true, uniqueness: true + validates :username, uniqueness: true, allow_blank: true validates :full_name, presence: true + validate :ensure_username_presence - before_validation :set_default_username, on: :create after_save :generate_open_graph_image, unless: -> { Rails.env.test? } after_save :download_and_store_avatar @@ -62,7 +63,9 @@ def download_and_store_avatar private - def set_default_username - self.username ||= email.split('@').first + def ensure_username_presence + if username.blank? + self.username = email.present? ? email.split('@').first : "user#{SecureRandom.hex(4)}" + end end end \ No newline at end of file diff --git a/spec/factories/achievements.rb b/spec/factories/achievements.rb new file mode 100644 index 0000000..c42af3d --- /dev/null +++ b/spec/factories/achievements.rb @@ -0,0 +1,11 @@ +# spec/factories/achievements.rb +FactoryBot.define do + factory :achievement do + title { "My Achievement" } + date { Date.today } + description { "This is a test achievement" } + icon { "fa-trophy" } + url { "http://example.com/achievement" } + association :user + end +end \ No newline at end of file diff --git a/spec/factories/links.rb b/spec/factories/links.rb new file mode 100644 index 0000000..8c2eb9c --- /dev/null +++ b/spec/factories/links.rb @@ -0,0 +1,10 @@ +# spec/factories/links.rb +FactoryBot.define do + factory :link do + title { "MyString" } + url { "http://example.com" } + visible { true } + pinned { false } + association :user + end +end \ No newline at end of file diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 1d8b969..49b66d0 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,9 +1,11 @@ +# spec/factories/users.rb FactoryBot.define do factory :user do - email { "test@example.com" } - password { "password" } + sequence(:email) { |n| "user#{n}@example.com" } + password { "password123" } + password_confirmation { "password123" } + sequence(:username) { |n| "user#{n}" } full_name { "Test User" } - username { "testuser" } - tags { "[]" } + tags { ["tag1", "tag2"] } end end \ No newline at end of file diff --git a/spec/models/achievement_spec.rb b/spec/models/achievement_spec.rb new file mode 100644 index 0000000..c9f9cf6 --- /dev/null +++ b/spec/models/achievement_spec.rb @@ -0,0 +1,15 @@ +# spec/models/achievement_spec.rb +require 'rails_helper' + +RSpec.describe Achievement, type: :model do + describe 'validations' do + it { should validate_presence_of(:title) } + it { should validate_presence_of(:date) } + it { should validate_presence_of(:description) } + end + + describe 'associations' do + it { should belong_to(:user) } + it { should have_many(:achievement_views) } + end +end \ No newline at end of file diff --git a/spec/models/achievement_view_spec.rb b/spec/models/achievement_view_spec.rb index fea44af..0de1893 100644 --- a/spec/models/achievement_view_spec.rb +++ b/spec/models/achievement_view_spec.rb @@ -1,5 +1,9 @@ +# spec/models/achievement_view_spec.rb require 'rails_helper' RSpec.describe AchievementView, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end + describe 'associations' do + it { should belong_to(:achievement) } + it { should belong_to(:user) } + end +end \ No newline at end of file diff --git a/spec/models/daily_metric_spec.rb b/spec/models/daily_metric_spec.rb index 9a9cc7d..58169bc 100644 --- a/spec/models/daily_metric_spec.rb +++ b/spec/models/daily_metric_spec.rb @@ -1,5 +1,8 @@ +# spec/models/daily_metric_spec.rb require 'rails_helper' RSpec.describe DailyMetric, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end + describe 'associations' do + it { should belong_to(:user) } + end +end \ No newline at end of file diff --git a/spec/models/link_click_spec.rb b/spec/models/link_click_spec.rb index 39a7342..8c76257 100644 --- a/spec/models/link_click_spec.rb +++ b/spec/models/link_click_spec.rb @@ -1,5 +1,9 @@ +# spec/models/link_click_spec.rb require 'rails_helper' RSpec.describe LinkClick, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end + describe 'associations' do + it { should belong_to(:link) } + it { should belong_to(:user) } + end +end \ No newline at end of file diff --git a/spec/models/link_spec.rb b/spec/models/link_spec.rb new file mode 100644 index 0000000..a0d9bb3 --- /dev/null +++ b/spec/models/link_spec.rb @@ -0,0 +1,26 @@ +# spec/models/link_spec.rb +require 'rails_helper' + +RSpec.describe Link, type: :model do + describe 'associations' do + it { should belong_to(:user) } + it { should have_many(:link_clicks) } + end + + describe 'scopes' do + let!(:visible_link) { create(:link, visible: true) } + let!(:invisible_link) { create(:link, visible: false) } + let!(:pinned_link) { create(:link, pinned: true) } + let!(:unpinned_link) { create(:link, pinned: false) } + + it 'returns only visible links' do + expect(Link.visible).to include(visible_link) + expect(Link.visible).not_to include(invisible_link) + end + + it 'returns only pinned links' do + expect(Link.pinned).to include(pinned_link) + expect(Link.pinned).not_to include(unpinned_link) + end + end +end \ No newline at end of file diff --git a/spec/models/page_view_spec.rb b/spec/models/page_view_spec.rb index 100ab4a..32928a4 100644 --- a/spec/models/page_view_spec.rb +++ b/spec/models/page_view_spec.rb @@ -1,5 +1,8 @@ +# spec/models/page_view_spec.rb require 'rails_helper' RSpec.describe PageView, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end + describe 'associations' do + it { should belong_to(:user) } + end +end \ No newline at end of file diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 0000000..f5c7832 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,70 @@ +# spec/models/user_spec.rb +require 'rails_helper' + +RSpec.describe User, type: :model do + describe 'validations' do + it { should validate_presence_of(:email) } + it { should validate_uniqueness_of(:username).allow_blank } + it { should validate_presence_of(:full_name) } + end + + describe 'associations' do + it { should have_many(:links).dependent(:destroy) } + it { should have_many(:achievements).dependent(:destroy) } + it { should have_many(:daily_metrics) } + it { should have_many(:page_views) } + it { should have_many(:link_clicks) } + it { should have_many(:achievement_views) } + end + + describe 'username generation' do + it 'sets default username from email if username is blank' do + user = User.new(email: 'test@example.com', password: 'password', full_name: 'Test User') + user.valid? + expect(user.username).to eq('test') + end + + it 'sets a random username when email and username are blank' do + user = User.new(password: 'password', full_name: 'Test User') + user.valid? + expect(user.username).to match(/^user[a-f0-9]{8}$/) + end + + it 'does not change username if it is already set' do + user = User.new(email: 'test@example.com', username: 'customuser', password: 'password', full_name: 'Test User') + user.valid? + expect(user.username).to eq('customuser') + end + end + + describe 'callbacks' do + it 'does not generate open graph image in test environment' do + user = build(:user) + expect(OpenGraphImageGenerator).not_to receive(:new) + user.save + end + + it 'downloads and stores avatar after save' do + user = build(:user, avatar: 'http://example.com/avatar.jpg') + expect(user).to receive(:download_and_store_avatar) + user.save + end + end + + describe '#parsed_tags' do + it 'returns parsed JSON when tags is a valid JSON string' do + user = User.new(tags: '["ruby", "rails"]') + expect(user.parsed_tags).to eq(['ruby', 'rails']) + end + + it 'returns an empty array when tags is an invalid JSON string' do + user = User.new(tags: 'invalid json') + expect(user.parsed_tags).to eq([]) + end + + it 'returns tags as-is when it is already an array' do + user = User.new(tags: ['ruby', 'rails']) + expect(user.parsed_tags).to eq(['ruby', 'rails']) + end + end +end \ No newline at end of file diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d0c73bb..e78c328 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,3 +1,4 @@ +# spec/rails_helper.rb require 'spec_helper' ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment' @@ -32,3 +33,10 @@ Rake::Task['assets:precompile'].invoke end end + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end \ No newline at end of file From c375a669818b893d131fc9e5199607b5efbd52f8 Mon Sep 17 00:00:00 2001 From: Dean Lofts Date: Fri, 23 Aug 2024 20:33:27 +1000 Subject: [PATCH 2/2] add controllers test --- Gemfile | 1 + Gemfile.lock | 5 + .../achievements_controller_spec.rb | 80 ++++++++++++++++ spec/controllers/analytics_controller_spec.rb | 31 +++++++ spec/controllers/links_controller_spec.rb | 91 +++++++++++++++++++ spec/controllers/pages_controller_spec.rb | 11 +++ .../users/registrations_controller_spec.rb | 50 ++++++++++ spec/factories/achievements.rb | 2 +- spec/factories/daily_metrics.rb | 11 +++ spec/rails_helper.rb | 26 +++++- 10 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 spec/controllers/achievements_controller_spec.rb create mode 100644 spec/controllers/analytics_controller_spec.rb create mode 100644 spec/controllers/links_controller_spec.rb create mode 100644 spec/controllers/pages_controller_spec.rb create mode 100644 spec/controllers/users/registrations_controller_spec.rb create mode 100644 spec/factories/daily_metrics.rb diff --git a/Gemfile b/Gemfile index db09839..0394f93 100644 --- a/Gemfile +++ b/Gemfile @@ -81,4 +81,5 @@ group :test do # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] gem 'capybara' gem 'selenium-webdriver' + gem 'rails-controller-testing' end diff --git a/Gemfile.lock b/Gemfile.lock index 28679b2..744477b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -237,6 +237,10 @@ GEM activesupport (= 7.1.3.4) bundler (>= 1.15.0) railties (= 7.1.3.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 @@ -392,6 +396,7 @@ DEPENDENCIES mini_magick puma (>= 5.0) rails (~> 7.1.3, >= 7.1.3.4) + rails-controller-testing redis (>= 4.0.1) rspec-rails (~> 6.0.0) selenium-webdriver diff --git a/spec/controllers/achievements_controller_spec.rb b/spec/controllers/achievements_controller_spec.rb new file mode 100644 index 0000000..d4fa78f --- /dev/null +++ b/spec/controllers/achievements_controller_spec.rb @@ -0,0 +1,80 @@ +# spec/controllers/achievements_controller_spec.rb +require 'rails_helper' + +RSpec.describe AchievementsController, type: :controller do + let(:user) { create(:user) } + let(:achievement) { create(:achievement, user: user) } + + describe "GET #index" do + it "returns a success response" do + get :index + expect(response).to be_successful + end + end + + describe "GET #show" do + context "when achievement doesn't have a URL" do + it "returns a success response" do + get :show, params: { id: achievement.to_param } + expect(response).to be_successful + end + end + + context "when achievement has a URL" do + let(:achievement_with_url) { create(:achievement, user: user, url: 'http://example.com') } + + it "redirects to the achievement URL" do + get :show, params: { id: achievement_with_url.to_param } + expect(response).to redirect_to(achievement_with_url.url) + end + end + + it "creates an AchievementView" do + expect { + get :show, params: { id: achievement.to_param } + }.to change(AchievementView, :count).by(1) + end + end + + describe "GET #new" do + it "returns a success response" do + sign_in user + get :new + expect(response).to be_successful + end + end + + describe "POST #create" do + context "with valid params" do + it "creates a new Achievement" do + sign_in user + expect { + post :create, params: { achievement: attributes_for(:achievement) } + }.to change(Achievement, :count).by(1) + end + end + end + + describe "PUT #update" do + context "with valid params" do + let(:new_attributes) { { title: "New Title" } } + + it "updates the requested achievement" do + sign_in user + put :update, params: { id: achievement.to_param, achievement: new_attributes } + achievement.reload + expect(achievement.title).to eq("New Title") + end + end + end + + describe "DELETE #destroy" do + it "destroys the requested achievement" do + sign_in user + achievement # ensure achievement is created before the expect block + expect { + delete :destroy, params: { id: achievement.to_param } + }.to change(Achievement, :count).by(-1) + end + end +end \ No newline at end of file diff --git a/spec/controllers/analytics_controller_spec.rb b/spec/controllers/analytics_controller_spec.rb new file mode 100644 index 0000000..59204a1 --- /dev/null +++ b/spec/controllers/analytics_controller_spec.rb @@ -0,0 +1,31 @@ +# spec/controllers/analytics_controller_spec.rb +require 'rails_helper' + +RSpec.describe AnalyticsController, type: :controller do + let(:user) { create(:user) } + + before do + sign_in user + create(:daily_metric, user: user, date: Date.today) + end + + describe "GET #index" do + it "returns a success response" do + get :index + expect(response).to be_successful + end + + it "assigns the correct instance variables" do + get :index + expect(assigns(:total_page_views)).to be_a(Integer) + expect(assigns(:total_link_clicks)).to be_a(Integer) + expect(assigns(:total_achievement_views)).to be_a(Integer) + expect(assigns(:unique_visitors)).to be_a(Integer) + expect(assigns(:latest_daily_metric)).to be_a(DailyMetric) + expect(assigns(:link_analytics)).to be_an(Array) + expect(assigns(:achievement_analytics)).to be_an(Array) + expect(assigns(:daily_views)).to be_a(Hash) + expect(assigns(:browser_data)).to be_a(Hash) + end + end +end \ No newline at end of file diff --git a/spec/controllers/links_controller_spec.rb b/spec/controllers/links_controller_spec.rb new file mode 100644 index 0000000..ce4d2e0 --- /dev/null +++ b/spec/controllers/links_controller_spec.rb @@ -0,0 +1,91 @@ +# spec/controllers/links_controller_spec.rb +require 'rails_helper' + +RSpec.describe LinksController, type: :controller do + let(:user) { create(:user) } + let(:link) { create(:link, user: user) } + + describe "GET #index" do + it "returns a success response" do + get :index + expect(response).to be_successful + end + end + + describe "GET #show" do + it "returns a success response" do + get :show, params: { id: link.to_param } + expect(response).to be_successful + end + end + + describe "GET #new" do + it "returns a success response" do + sign_in user + get :new + expect(response).to be_successful + end + end + + describe "POST #create" do + context "with valid params" do + it "creates a new Link" do + sign_in user + expect { + post :create, params: { link: attributes_for(:link) } + }.to change(Link, :count).by(1) + end + end + end + + describe "PUT #update" do + context "with valid params" do + let(:new_attributes) { { title: "New Title" } } + + it "updates the requested link" do + sign_in user + put :update, params: { id: link.to_param, link: new_attributes } + link.reload + expect(link.title).to eq("New Title") + end + end + end + + describe "DELETE #destroy" do + it "destroys the requested link" do + sign_in user + link # ensure link is created before the expect block + expect { + delete :destroy, params: { id: link.to_param } + }.to change(Link, :count).by(-1) + end + end + + describe "GET #user_links" do + it "returns a success response" do + get :user_links, params: { username: user.username } + expect(response).to be_successful + end + + it "assigns the correct instance variables" do + get :user_links, params: { username: user.username } + expect(assigns(:user)).to eq(user) + expect(assigns(:links)).to be_an(ActiveRecord::Relation) + expect(assigns(:pinned_links)).to be_an(ActiveRecord::Relation) + expect(assigns(:achievements)).to be_an(ActiveRecord::Relation) + end + end + + describe "GET #track_click" do + it "creates a LinkClick" do + expect { + get :track_click, params: { id: link.to_param } + }.to change(LinkClick, :count).by(1) + end + + it "redirects to the link url" do + get :track_click, params: { id: link.to_param } + expect(response).to redirect_to(link.url) + 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 0000000..cbc3b9d --- /dev/null +++ b/spec/controllers/pages_controller_spec.rb @@ -0,0 +1,11 @@ +# spec/controllers/pages_controller_spec.rb +require 'rails_helper' + +RSpec.describe PagesController, type: :controller do + describe "GET #home" do + it "returns a success response" do + get :home + expect(response).to be_successful + end + end +end \ No newline at end of file diff --git a/spec/controllers/users/registrations_controller_spec.rb b/spec/controllers/users/registrations_controller_spec.rb new file mode 100644 index 0000000..56ba840 --- /dev/null +++ b/spec/controllers/users/registrations_controller_spec.rb @@ -0,0 +1,50 @@ +# spec/controllers/users/registrations_controller_spec.rb +require 'rails_helper' + +RSpec.describe Users::RegistrationsController, type: :controller do + before do + @request.env["devise.mapping"] = Devise.mappings[:user] + end + + describe "POST #create" do + let(:valid_attributes) { + { email: "test@example.com", password: "password", password_confirmation: "password", + username: "testuser", full_name: "Test User", tags: "tag1,tag2" } + } + + it "creates a new User" do + expect { + post :create, params: { user: valid_attributes } + }.to change(User, :count).by(1) + end + + it "correctly processes tags" do + post :create, params: { user: valid_attributes } + user = User.last + tags = user.tags.is_a?(String) ? JSON.parse(user.tags) : user.tags + expect(tags).to eq(["tag1", "tag2"]) + end + end + + describe "PUT #update" do + let(:user) { create(:user, tags: ["old_tag1", "old_tag2"].to_json) } + + before do + sign_in user + end + + context "with valid params" do + let(:new_attributes) { + { full_name: "New Name", tags: "new_tag1,new_tag2" } + } + + it "updates the requested user" do + put :update, params: { user: new_attributes } + user.reload + expect(user.full_name).to eq("New Name") + tags = user.tags.is_a?(String) ? JSON.parse(user.tags) : user.tags + expect(tags).to eq(["new_tag1", "new_tag2"]) + end + end + end +end \ No newline at end of file diff --git a/spec/factories/achievements.rb b/spec/factories/achievements.rb index c42af3d..7d4d071 100644 --- a/spec/factories/achievements.rb +++ b/spec/factories/achievements.rb @@ -5,7 +5,7 @@ date { Date.today } description { "This is a test achievement" } icon { "fa-trophy" } - url { "http://example.com/achievement" } + url { nil } association :user end end \ No newline at end of file diff --git a/spec/factories/daily_metrics.rb b/spec/factories/daily_metrics.rb new file mode 100644 index 0000000..8f8c037 --- /dev/null +++ b/spec/factories/daily_metrics.rb @@ -0,0 +1,11 @@ +# spec/factories/daily_metrics.rb +FactoryBot.define do + factory :daily_metric do + user + date { Date.today } + page_views { 10 } + link_clicks { 5 } + achievement_views { 3 } + unique_visitors { 2 } + end +end \ No newline at end of file diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index e78c328..92ab311 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -6,6 +6,8 @@ require 'rspec/rails' require 'devise' require 'factory_bot_rails' +require 'capybara/rspec' +require 'rails-controller-testing' Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } @@ -24,6 +26,12 @@ config.include Devise::Test::IntegrationHelpers, type: :request config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view + + [:controller, :view, :request].each do |type| + config.include ::Rails::Controller::Testing::TestProcess, :type => type + config.include ::Rails::Controller::Testing::TemplateAssertions, :type => type + config.include ::Rails::Controller::Testing::Integration, :type => type + end config.include FactoryBot::Syntax::Methods @@ -32,6 +40,14 @@ Rails.application.load_tasks Rake::Task['assets:precompile'].invoke end + + # Clean up uploaded files after each test + config.after(:each) do + FileUtils.rm_rf(Dir["#{Rails.root}/spec/support/uploads"]) + end + + # Add support for time travel in tests + config.include ActiveSupport::Testing::TimeHelpers end Shoulda::Matchers.configure do |config| @@ -39,4 +55,12 @@ with.test_framework :rspec with.library :rails end -end \ No newline at end of file +end + +# Configure Capybara for system tests +Capybara.register_driver :chrome_headless do |app| + options = Selenium::WebDriver::Chrome::Options.new(args: %w[no-sandbox headless disable-gpu]) + Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) +end + +Capybara.javascript_driver = :chrome_headless \ No newline at end of file