diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ed613820 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI +on: [pull_request] +jobs: + build: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:12 + ports: + - 5432:5432 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - uses: actions/checkout@v1 + + - name: Setup Ruby and install gems + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Install dependent libraries + run: sudo apt-get install libpq-dev + + - name: Bundle install + run: | + gem install bundler + bundle install --jobs 4 --retry 3 + + - name: Install daisyui + run: npm i daisyui + + - name: Compile assets + run: bundle exec rake assets:precompile + + - name: Setup Database + run: | + cp config/database.yml.github-actions config/database.yml + bundle exec rake db:create + bundle exec rake db:schema:load + env: + RAILS_ENV: test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + + - name: Run RSpec + run: COVERAGE=true bundle exec rspec --require rails_helper + env: + RAILS_ENV: test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + + - name: Simplecov Report + uses: aki77/simplecov-report-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Gemfile b/Gemfile index 3a25ecc2..0a9d2f56 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'bootsnap', require: false gem 'devise', '4.8.1' gem 'pg', '~> 1.1' gem 'puma', '~> 5.0' +gem 'pundit', '~> 2.2' gem 'rails', '7.0.4' gem 'sprockets-rails' gem 'stimulus-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 2cfc61a5..2a781ffa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -130,6 +130,8 @@ GEM pg (1.4.4) puma (5.6.5) nio4r (~> 2.0) + pundit (2.3.0) + activesupport (>= 3.0.0) racc (1.6.0) rack (2.2.4) rack-test (2.0.2) @@ -253,6 +255,7 @@ DEPENDENCIES foreman pg (~> 1.1) puma (~> 5.0) + pundit (~> 2.2) rails (= 7.0.4) rspec-rails (~> 5.1.0) rubocop-rails diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb new file mode 100644 index 00000000..e000cba5 --- /dev/null +++ b/app/policies/application_policy.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class ApplicationPolicy + attr_reader :user, :record + + def initialize(user, record) + @user = user + @record = record + end + + def index? + false + end + + def show? + false + end + + def create? + false + end + + def new? + create? + end + + def update? + false + end + + def edit? + update? + end + + def destroy? + false + end + + class Scope + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + raise NotImplementedError, "You must define #resolve in #{self.class}" + end + + private + + attr_reader :user, :scope + end +end diff --git a/config/environments/production.rb b/config/environments/production.rb index 72b3d08f..27cdc256 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -46,7 +46,7 @@ Rails.application.configure do # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true + config.force_ssl = true # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). @@ -68,6 +68,20 @@ Rails.application.configure do # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false + config.action_mailer.delivery_method = :smtp + config.action_mailer.smtp_settings = { + address: ENV['SMTP_SERVER'], + port: ENV['SMTP_PORT'], + domain: 'example.com', + user_name: ENV['SMTP_USERNAME'], + password: ENV['SMTP_PASSWORD'], + authentication: 'plain', + enable_starttls_auto: true, + open_timeout: 5, + read_timeout: 5 + } + config.action_mailer.default_url_options = { host: 'example.comg' } + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index cfcec2f5..262a13f5 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -263,7 +263,9 @@ Devise.setup do |config| # should add them to the navigational formats lists. # # The "*/*" below is required to match Internet Explorer requests. - # config.navigational_formats = ['*/*', :html] + # turbo_stream is a custom format for Turbo Streams (https://turbo.hotwired.dev/handbook/streams) + # https://stackoverflow.com/questions/36646226/undefined-method-user-url-for-devise-sessionscontrollercreate/71297012#71297012 + config.navigational_formats = ['*/*', :html, :turbo_stream] # The default HTTP method used to sign out a resource. Default is :delete. config.sign_out_via = :delete diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index e1b8eaf6..661615a4 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,26 +1,12 @@ -# This file is copied to spec/ when you run 'rails generate rspec:install' require 'spec_helper' ENV['RAILS_ENV'] ||= 'test' 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? + +abort('The Rails environment is running in production mode!') if Rails.env.production? require 'rspec/rails' # Add additional requires below this line. Rails is not loaded until this point! -# Requires supporting ruby files with custom matchers and macros, etc, in -# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are -# run as spec files by default. This means that files in spec/support that end -# in _spec.rb will both be required and run as specs, causing the specs to be -# run twice. It is recommended that you do not name files matching this glob to -# end with _spec.rb. You can configure this pattern with the --pattern -# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -# -# The following line is provided for convenience purposes. It has the downside -# of increasing the boot-up time by auto-requiring all files in the support -# directory. Alternatively, in the individual `*_spec.rb` files, manually -# require only the support files necessary. -# -# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } +Dir[Rails.root.join('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 these lines. @@ -30,34 +16,18 @@ rescue ActiveRecord::PendingMigrationError => e puts e.to_s.strip exit 1 end + RSpec.configure do |config| - # 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`. - # - # 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 - # # ... - # end - # - # The different available types are documented in the features, such as in - # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! - - # Filter lines from Rails gems in backtraces. config.filter_rails_from_backtrace! - # arbitrary gems may also be filtered via: - # config.filter_gems_from_backtrace("gem name") + + config.include FactoryBot::Syntax::Methods + config.include Devise::Test::IntegrationHelpers, type: :request + + config.before do + ActiveJob::Base.queue_adapter = :test + end end Shoulda::Matchers.configure do |config| diff --git a/spec/support/devise.rb b/spec/support/devise.rb new file mode 100644 index 00000000..b5aa99b4 --- /dev/null +++ b/spec/support/devise.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# https://makandracards.com/makandra/37161-rspec-devise-how-to-sign-in-users-in-request-specs + +module DeviseRequestSpecHelpers + include Warden::Test::Helpers + + def sign_in(resource_or_scope, resource = nil) + resource ||= resource_or_scope + scope = Devise::Mapping.find_scope!(resource_or_scope) + login_as(resource, scope: scope) + end + + def sign_out(resource_or_scope) + scope = Devise::Mapping.find_scope!(resource_or_scope) + logout(scope) + end + +end + +RSpec.configure do |config| + config.include DeviseRequestSpecHelpers, type: :request +end diff --git a/spec/support/pundit_matchers.rb b/spec/support/pundit_matchers.rb new file mode 100644 index 00000000..474def74 --- /dev/null +++ b/spec/support/pundit_matchers.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Custom RSpec matchers for Pundit policies + +RSpec::Matchers.define :permit do |action| + match do |policy| + policy.public_send("#{action}?") + end + + failure_message do |policy| + "#{policy.class} does not permit #{action} on #{policy.record} for #{policy.user.inspect}." + end + + failure_message_when_negated do |policy| + "#{policy.class} does not forbid #{action} on #{policy.record} for #{policy.user.inspect}." + end +end + +RSpec::Matchers.define :forbid do |action| + match do |policy| + policy.public_send("#{action}?") + end + + failure_message do |policy| + "#{policy.class} does not forbid #{action} on #{policy.record} for #{policy.user.inspect}." + end + + failure_message_when_negated do |policy| + "#{policy.class} does not permit #{action} on #{policy.record} for #{policy.user.inspect}." + end +end