Add end-to-end (system) tests (#25461)
This commit is contained in:
		
							
								
								
									
										97
									
								
								.github/workflows/test-ruby.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										97
									
								
								.github/workflows/test-ruby.yml
									
									
									
									
										vendored
									
									
								
							@@ -153,3 +153,100 @@ jobs:
 | 
			
		||||
        run: './bin/rails db:create db:schema:load db:seed'
 | 
			
		||||
 | 
			
		||||
      - run: bundle exec rake rspec_chunked
 | 
			
		||||
 | 
			
		||||
  test-e2e:
 | 
			
		||||
    name: End to End testing
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    needs:
 | 
			
		||||
      - build
 | 
			
		||||
 | 
			
		||||
    services:
 | 
			
		||||
      postgres:
 | 
			
		||||
        image: postgres:14-alpine
 | 
			
		||||
        env:
 | 
			
		||||
          POSTGRES_PASSWORD: postgres
 | 
			
		||||
          POSTGRES_USER: postgres
 | 
			
		||||
        options: >-
 | 
			
		||||
          --health-cmd pg_isready
 | 
			
		||||
          --health-interval 10s
 | 
			
		||||
          --health-timeout 5s
 | 
			
		||||
          --health-retries 5
 | 
			
		||||
        ports:
 | 
			
		||||
          - 5432:5432
 | 
			
		||||
 | 
			
		||||
      redis:
 | 
			
		||||
        image: redis:7-alpine
 | 
			
		||||
        options: >-
 | 
			
		||||
          --health-cmd "redis-cli ping"
 | 
			
		||||
          --health-interval 10s
 | 
			
		||||
          --health-timeout 5s
 | 
			
		||||
          --health-retries 5
 | 
			
		||||
        ports:
 | 
			
		||||
          - 6379:6379
 | 
			
		||||
 | 
			
		||||
    env:
 | 
			
		||||
      DB_HOST: localhost
 | 
			
		||||
      DB_USER: postgres
 | 
			
		||||
      DB_PASS: postgres
 | 
			
		||||
      DISABLE_SIMPLECOV: true
 | 
			
		||||
      RAILS_ENV: test
 | 
			
		||||
      BUNDLE_WITH: test
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        ruby-version:
 | 
			
		||||
          - '3.0'
 | 
			
		||||
          - '3.1'
 | 
			
		||||
          - '.ruby-version'
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - uses: actions/download-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: './public'
 | 
			
		||||
          name: ${{ github.sha }}
 | 
			
		||||
 | 
			
		||||
      - name: Update package index
 | 
			
		||||
        run: sudo apt-get update
 | 
			
		||||
 | 
			
		||||
      - name: Set up Node.js
 | 
			
		||||
        uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          cache: yarn
 | 
			
		||||
          node-version-file: '.nvmrc'
 | 
			
		||||
 | 
			
		||||
      - name: Install native Ruby dependencies
 | 
			
		||||
        run: sudo apt-get install -y libicu-dev libidn11-dev
 | 
			
		||||
 | 
			
		||||
      - name: Install additional system dependencies
 | 
			
		||||
        run: sudo apt-get install -y ffmpeg imagemagick
 | 
			
		||||
 | 
			
		||||
      - name: Set up bundler cache
 | 
			
		||||
        uses: ruby/setup-ruby@v1
 | 
			
		||||
        with:
 | 
			
		||||
          ruby-version: ${{ matrix.ruby-version}}
 | 
			
		||||
          bundler-cache: true
 | 
			
		||||
 | 
			
		||||
      - run: yarn --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
      - name: Load database schema
 | 
			
		||||
        run: './bin/rails db:create db:schema:load db:seed'
 | 
			
		||||
 | 
			
		||||
      - run: bundle exec rake spec:system
 | 
			
		||||
 | 
			
		||||
      - name: Archive logs
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        if: failure()
 | 
			
		||||
        with:
 | 
			
		||||
          name: e2e-logs-${{ matrix.ruby-version }}
 | 
			
		||||
          path: log/
 | 
			
		||||
 | 
			
		||||
      - name: Archive test screenshots
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        if: failure()
 | 
			
		||||
        with:
 | 
			
		||||
          name: e2e-screenshots
 | 
			
		||||
          path: tmp/screenshots/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Gemfile
									
									
									
									
									
								
							@@ -113,6 +113,10 @@ group :test do
 | 
			
		||||
 | 
			
		||||
  # Browser integration testing
 | 
			
		||||
  gem 'capybara', '~> 3.39'
 | 
			
		||||
  gem 'selenium-webdriver'
 | 
			
		||||
 | 
			
		||||
  # Used to reset the database between system tests
 | 
			
		||||
  gem 'database_cleaner-active_record'
 | 
			
		||||
 | 
			
		||||
  # Used to mock environment variables
 | 
			
		||||
  gem 'climate_control', '~> 0.2'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								Gemfile.lock
									
									
									
									
									
								
							@@ -199,6 +199,10 @@ GEM
 | 
			
		||||
    crass (1.0.6)
 | 
			
		||||
    css_parser (1.14.0)
 | 
			
		||||
      addressable
 | 
			
		||||
    database_cleaner-active_record (2.1.0)
 | 
			
		||||
      activerecord (>= 5.a)
 | 
			
		||||
      database_cleaner-core (~> 2.0.0)
 | 
			
		||||
    database_cleaner-core (2.0.1)
 | 
			
		||||
    date (3.3.3)
 | 
			
		||||
    debug_inspector (1.1.0)
 | 
			
		||||
    devise (4.9.2)
 | 
			
		||||
@@ -656,6 +660,10 @@ GEM
 | 
			
		||||
    scenic (1.7.0)
 | 
			
		||||
      activerecord (>= 4.0.0)
 | 
			
		||||
      railties (>= 4.0.0)
 | 
			
		||||
    selenium-webdriver (4.9.1)
 | 
			
		||||
      rexml (~> 3.2, >= 3.2.5)
 | 
			
		||||
      rubyzip (>= 1.2.2, < 3.0)
 | 
			
		||||
      websocket (~> 1.0)
 | 
			
		||||
    semantic_range (3.0.0)
 | 
			
		||||
    sidekiq (6.5.9)
 | 
			
		||||
      connection_pool (>= 2.2.5, < 3)
 | 
			
		||||
@@ -768,6 +776,7 @@ GEM
 | 
			
		||||
      rack-proxy (>= 0.6.1)
 | 
			
		||||
      railties (>= 5.2)
 | 
			
		||||
      semantic_range (>= 2.3.0)
 | 
			
		||||
    websocket (1.2.9)
 | 
			
		||||
    websocket-driver (0.7.5)
 | 
			
		||||
      websocket-extensions (>= 0.1.0)
 | 
			
		||||
    websocket-extensions (0.1.5)
 | 
			
		||||
@@ -804,6 +813,7 @@ DEPENDENCIES
 | 
			
		||||
  color_diff (~> 0.1)
 | 
			
		||||
  concurrent-ruby
 | 
			
		||||
  connection_pool
 | 
			
		||||
  database_cleaner-active_record
 | 
			
		||||
  devise (~> 4.9)
 | 
			
		||||
  devise-two-factor (~> 4.1)
 | 
			
		||||
  devise_pam_authenticatable2 (~> 9.2)
 | 
			
		||||
@@ -885,6 +895,7 @@ DEPENDENCIES
 | 
			
		||||
  rubyzip (~> 2.3)
 | 
			
		||||
  sanitize (~> 6.0)
 | 
			
		||||
  scenic (~> 1.7)
 | 
			
		||||
  selenium-webdriver
 | 
			
		||||
  sidekiq (~> 6.5)
 | 
			
		||||
  sidekiq-bulk (~> 0.2.0)
 | 
			
		||||
  sidekiq-scheduler (~> 5.0)
 | 
			
		||||
 
 | 
			
		||||
@@ -199,7 +199,7 @@ module Mastodon
 | 
			
		||||
    # We use our own middleware for this
 | 
			
		||||
    config.public_file_server.enabled = false
 | 
			
		||||
 | 
			
		||||
    config.middleware.use PublicFileServerMiddleware if Rails.env.development? || ENV['RAILS_SERVE_STATIC_FILES'] == 'true'
 | 
			
		||||
    config.middleware.use PublicFileServerMiddleware if Rails.env.development? || Rails.env.test? || ENV['RAILS_SERVE_STATIC_FILES'] == 'true'
 | 
			
		||||
    config.middleware.use Rack::Attack
 | 
			
		||||
    config.middleware.use Mastodon::RackMiddleware
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,5 @@ const { merge } = require('webpack-merge');
 | 
			
		||||
const sharedConfig = require('./shared');
 | 
			
		||||
 | 
			
		||||
module.exports = merge(sharedConfig, {
 | 
			
		||||
  mode: 'development',
 | 
			
		||||
  mode: 'production',
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								lib/tasks/spec.rake
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/tasks/spec.rake
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
if Rake::Task.task_defined?('spec:system')
 | 
			
		||||
  namespace :spec do
 | 
			
		||||
    task :enable_system_specs do # rubocop:disable Rails/RakeEnvironment
 | 
			
		||||
      ENV['RUN_SYSTEM_SPECS'] = 'true'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  Rake::Task['spec:system'].enhance ['spec:enable_system_specs']
 | 
			
		||||
end
 | 
			
		||||
@@ -1,6 +1,14 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
ENV['RAILS_ENV'] ||= 'test'
 | 
			
		||||
 | 
			
		||||
# This needs to be defined before Rails is initialized
 | 
			
		||||
RUN_SYSTEM_SPECS = ENV.fetch('RUN_SYSTEM_SPECS', false)
 | 
			
		||||
 | 
			
		||||
if RUN_SYSTEM_SPECS
 | 
			
		||||
  STREAMING_PORT = ENV.fetch('TEST_STREAMING_PORT', '4020')
 | 
			
		||||
  ENV['STREAMING_API_BASE_URL'] = "http://localhost:#{STREAMING_PORT}"
 | 
			
		||||
end
 | 
			
		||||
require File.expand_path('../config/environment', __dir__)
 | 
			
		||||
 | 
			
		||||
abort('The Rails environment is running in production mode!') if Rails.env.production?
 | 
			
		||||
@@ -15,10 +23,14 @@ require 'chewy/rspec'
 | 
			
		||||
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
 | 
			
		||||
 | 
			
		||||
ActiveRecord::Migration.maintain_test_schema!
 | 
			
		||||
WebMock.disable_net_connect!(allow: Chewy.settings[:host])
 | 
			
		||||
WebMock.disable_net_connect!(allow: Chewy.settings[:host], allow_localhost: RUN_SYSTEM_SPECS)
 | 
			
		||||
Sidekiq::Testing.inline!
 | 
			
		||||
Sidekiq.logger = nil
 | 
			
		||||
 | 
			
		||||
# System tests config
 | 
			
		||||
DatabaseCleaner.strategy = [:deletion]
 | 
			
		||||
streaming_server_manager = StreamingServerManager.new
 | 
			
		||||
 | 
			
		||||
Devise::Test::ControllerHelpers.module_eval do
 | 
			
		||||
  alias_method :original_sign_in, :sign_in
 | 
			
		||||
 | 
			
		||||
@@ -56,6 +68,8 @@ module SignedRequestHelpers
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
RSpec.configure do |config|
 | 
			
		||||
  # This is set before running spec:system, see lib/tasks/tests.rake
 | 
			
		||||
  config.filter_run_excluding type: :system unless RUN_SYSTEM_SPECS
 | 
			
		||||
  config.fixture_path = Rails.root.join('spec', 'fixtures')
 | 
			
		||||
  config.use_transactional_fixtures = true
 | 
			
		||||
  config.order = 'random'
 | 
			
		||||
@@ -83,8 +97,7 @@ RSpec.configure do |config|
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  config.before :each, type: :feature do
 | 
			
		||||
    https = ENV['LOCAL_HTTPS'] == 'true'
 | 
			
		||||
    Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}"
 | 
			
		||||
    Capybara.current_driver = :rack_test
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  config.before :each, type: :controller do
 | 
			
		||||
@@ -95,6 +108,35 @@ RSpec.configure do |config|
 | 
			
		||||
    stub_jsonld_contexts!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  config.before :suite do
 | 
			
		||||
    if RUN_SYSTEM_SPECS
 | 
			
		||||
      Webpacker.compile
 | 
			
		||||
      streaming_server_manager.start(port: STREAMING_PORT)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  config.after :suite do
 | 
			
		||||
    streaming_server_manager.stop
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  config.around :each, type: :system do |example|
 | 
			
		||||
    # driven_by :selenium, using: :chrome, screen_size: [1600, 1200]
 | 
			
		||||
    driven_by :selenium, using: :headless_chrome, screen_size: [1600, 1200]
 | 
			
		||||
 | 
			
		||||
    # The streaming server needs access to the database
 | 
			
		||||
    # but with use_transactional_tests every transaction
 | 
			
		||||
    # is rolled-back, so the streaming server never sees the data
 | 
			
		||||
    # So we disable this feature for system tests, and use DatabaseCleaner to clean
 | 
			
		||||
    # the database tables between each test
 | 
			
		||||
    self.use_transactional_tests = false
 | 
			
		||||
 | 
			
		||||
    DatabaseCleaner.cleaning do
 | 
			
		||||
      example.run
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    self.use_transactional_tests = true
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  config.before(:each) do |example|
 | 
			
		||||
    unless example.metadata[:paperclip_processing]
 | 
			
		||||
      allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true) # rubocop:disable RSpec/AnyInstance
 | 
			
		||||
 
 | 
			
		||||
@@ -52,3 +52,80 @@ def expect_push_bulk_to_match(klass, matcher)
 | 
			
		||||
    'args' => matcher,
 | 
			
		||||
  }))
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class StreamingServerManager
 | 
			
		||||
  @running_thread = nil
 | 
			
		||||
 | 
			
		||||
  def initialize
 | 
			
		||||
    at_exit { stop }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def start(port: 4020)
 | 
			
		||||
    return if @running_thread
 | 
			
		||||
 | 
			
		||||
    queue = Queue.new
 | 
			
		||||
 | 
			
		||||
    @queue = queue
 | 
			
		||||
 | 
			
		||||
    @running_thread = Thread.new do
 | 
			
		||||
      Open3.popen2e(
 | 
			
		||||
        {
 | 
			
		||||
          'REDIS_NAMESPACE' => ENV.fetch('REDIS_NAMESPACE'),
 | 
			
		||||
          'DB_NAME' => "#{ENV.fetch('DB_NAME', 'mastodon')}_test#{ENV.fetch('TEST_ENV_NUMBER', '')}",
 | 
			
		||||
          'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'test'),
 | 
			
		||||
          'NODE_ENV' => ENV.fetch('STREAMING_NODE_ENV', 'development'),
 | 
			
		||||
          'PORT' => port.to_s,
 | 
			
		||||
        },
 | 
			
		||||
        'node index.js', # must not call yarn here, otherwise it will fail because yarn does not send signals to its child process
 | 
			
		||||
        chdir: Rails.root.join('streaming')
 | 
			
		||||
      ) do |_stdin, stdout_err, process_thread|
 | 
			
		||||
        status = :starting
 | 
			
		||||
 | 
			
		||||
        # Spawn a thread to listen on streaming server output
 | 
			
		||||
        output_thread = Thread.new do
 | 
			
		||||
          stdout_err.each_line do |line|
 | 
			
		||||
            Rails.logger.info "Streaming server: #{line}"
 | 
			
		||||
 | 
			
		||||
            if status == :starting && line.match('Streaming API now listening on')
 | 
			
		||||
              status = :started
 | 
			
		||||
              @queue.enq 'started'
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        # And another thread to listen on commands from the main thread
 | 
			
		||||
        loop do
 | 
			
		||||
          msg = queue.pop
 | 
			
		||||
 | 
			
		||||
          case msg
 | 
			
		||||
          when 'stop'
 | 
			
		||||
            # we need to properly stop the reading thread
 | 
			
		||||
            output_thread.kill
 | 
			
		||||
 | 
			
		||||
            # Then stop the node process
 | 
			
		||||
            Process.kill('KILL', process_thread.pid)
 | 
			
		||||
 | 
			
		||||
            # And we stop ourselves
 | 
			
		||||
            @running_thread.kill
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    # wait for 10 seconds for the streaming server to start
 | 
			
		||||
    Timeout.timeout(10) do
 | 
			
		||||
      loop do
 | 
			
		||||
        break if @queue.pop == 'started'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def stop
 | 
			
		||||
    return unless @running_thread
 | 
			
		||||
 | 
			
		||||
    @queue.enq 'stop'
 | 
			
		||||
 | 
			
		||||
    # Wait for the thread to end
 | 
			
		||||
    @running_thread.join
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,8 @@ module ProfileStories
 | 
			
		||||
      email: email, password: password, confirmed_at: confirmed_at,
 | 
			
		||||
      account: Fabricate(:account, username: 'bob')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    Web::Setting.where(user: bob).first_or_initialize(user: bob).update!(data: { introductionVersion: 201812160442020 }) if finished_onboarding # rubocop:disable Style/NumericLiterals
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def as_a_logged_in_user
 | 
			
		||||
@@ -42,4 +44,8 @@ module ProfileStories
 | 
			
		||||
  def password
 | 
			
		||||
    @password ||= 'password'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def finished_onboarding
 | 
			
		||||
    @finished_onboarding || false
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								spec/system/new_statuses_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								spec/system/new_statuses_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe 'NewStatuses' do
 | 
			
		||||
  include ProfileStories
 | 
			
		||||
 | 
			
		||||
  subject { page }
 | 
			
		||||
 | 
			
		||||
  let(:email)               { 'test@example.com' }
 | 
			
		||||
  let(:password)            { 'password' }
 | 
			
		||||
  let(:confirmed_at)        { Time.zone.now }
 | 
			
		||||
  let(:finished_onboarding) { true }
 | 
			
		||||
 | 
			
		||||
  before do
 | 
			
		||||
    as_a_logged_in_user
 | 
			
		||||
    visit root_path
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'can be posted' do
 | 
			
		||||
    expect(subject).to have_css('div.app-holder')
 | 
			
		||||
 | 
			
		||||
    status_text = 'This is a new status!'
 | 
			
		||||
 | 
			
		||||
    within('.compose-form') do
 | 
			
		||||
      fill_in "What's on your mind?", with: status_text
 | 
			
		||||
      click_on 'Publish!'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    expect(subject).to have_selector('.status__content__text', text: status_text)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'can be posted again' do
 | 
			
		||||
    expect(subject).to have_css('div.app-holder')
 | 
			
		||||
 | 
			
		||||
    status_text = 'This is a second status!'
 | 
			
		||||
 | 
			
		||||
    within('.compose-form') do
 | 
			
		||||
      fill_in "What's on your mind?", with: status_text
 | 
			
		||||
      click_on 'Publish!'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    expect(subject).to have_selector('.status__content__text', text: status_text)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
		Reference in New Issue
	
	Block a user