Commit 0240caa0 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'feature/gb/gitlab-qa-integration-tests' into 'master'

Add GitLab QA integrations tests to GitLab CE / EE

Closes gitlab-qa#30

See merge request !9370
parents a9aff2f7 0731365b
--color
--format documentation
--require spec_helper
FROM ruby:2.3
LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \
apt-get update && apt-get install -y --force-yes \
libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \
apt-get clean
WORKDIR /home/qa
COPY ./ ./
RUN bundle install
ENTRYPOINT ["bin/test"]
source 'https://rubygems.org'
gem 'capybara', '~> 2.12.1'
gem 'capybara-screenshot', '~> 1.0.14'
gem 'capybara-webkit', '~> 1.12.0'
gem 'rake', '~> 12.0.0'
gem 'rspec', '~> 3.5'
## Integration tests for GitLab
This directory contains integration tests for GitLab.
It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
## What GitLab QA is?
GitLab QA is an integration tests suite for GitLab.
These are black-box and entirely click-driven integration tests you can run
against any existing instance.
## How does it work?
1. When we release a new version of GitLab, we build a Docker images for it.
1. Along with GitLab Docker Images we also build and publish GitLab QA images.
1. GitLab QA project uses these images to execute integration tests.
#!/usr/bin/env ruby
require_relative '../qa'
QA::Scenario
.const_get(ARGV.shift)
.perform(*ARGV)
#!/bin/bash
xvfb-run bundle exec bin/qa $@
$: << File.expand_path(File.dirname(__FILE__))
module QA
##
# GitLab QA runtime classes, mostly singletons.
#
module Runtime
autoload :Release, 'qa/runtime/release'
autoload :User, 'qa/runtime/user'
autoload :Namespace, 'qa/runtime/namespace'
end
##
# GitLab QA Scenarios
#
module Scenario
##
# Support files
#
autoload :Actable, 'qa/scenario/actable'
autoload :Template, 'qa/scenario/template'
##
# Test scenario entrypoints.
#
module Test
autoload :Instance, 'qa/scenario/test/instance'
end
##
# GitLab instance scenarios.
#
module Gitlab
module Project
autoload :Create, 'qa/scenario/gitlab/project/create'
end
end
end
##
# Classes describing structure of GitLab, pages, menus etc.
#
# Needed to execute click-driven-only black-box tests.
#
module Page
autoload :Base, 'qa/page/base'
module Main
autoload :Entry, 'qa/page/main/entry'
autoload :Menu, 'qa/page/main/menu'
autoload :Groups, 'qa/page/main/groups'
autoload :Projects, 'qa/page/main/projects'
end
module Project
autoload :New, 'qa/page/project/new'
autoload :Show, 'qa/page/project/show'
end
module Admin
autoload :Menu, 'qa/page/admin/menu'
end
end
##
# Classes describing operations on Git repositories.
#
module Git
autoload :Repository, 'qa/git/repository'
end
##
# Classes that make it possible to execute features tests.
#
module Specs
autoload :Config, 'qa/specs/config'
autoload :Runner, 'qa/specs/runner'
end
end
QA::Runtime::Release.extend_autoloads!
module QA
module CE
module Strategy
extend self
def extend_autoloads!
# noop
end
def perform_before_hooks
# noop
end
end
end
end
require 'uri'
module QA
module Git
class Repository
include Scenario::Actable
def self.perform(*args)
Dir.mktmpdir do |dir|
Dir.chdir(dir) { super }
end
end
def location=(address)
@location = address
@uri = URI(address)
end
def username=(name)
@username = name
@uri.user = name
end
def password=(pass)
@password = pass
@uri.password = pass
end
def use_default_credentials
self.username = Runtime::User.name
self.password = Runtime::User.password
end
def clone(opts = '')
`git clone #{opts} #{@uri.to_s} ./`
end
def shallow_clone
clone('--depth 1')
end
def configure_identity(name, email)
`git config user.name #{name}`
`git config user.email #{email}`
end
def commit_file(name, contents, message)
add_file(name, contents)
commit(message)
end
def add_file(name, contents)
File.write(name, contents)
`git add #{name}`
end
def commit(message)
`git commit -m "#{message}"`
end
def push_changes(branch = 'master')
`git push #{@uri.to_s} #{branch}`
end
def commits
`git log --oneline`.split("\n")
end
end
end
end
module QA
module Page
module Admin
class Menu < Page::Base
def go_to_license
within_middle_menu { click_link 'License' }
end
private
def within_middle_menu
page.within('.nav-control') do
yield
end
end
end
end
end
end
module QA
module Page
class Base
include Capybara::DSL
include Scenario::Actable
def refresh
visit current_path
end
end
end
end
module QA
module Page
module Main
class Entry < Page::Base
def initialize
visit('/')
# This resolves cold boot problems with login page
find('.application', wait: 120)
end
def sign_in_using_credentials
if page.has_content?('Change your password')
fill_in :user_password, with: Runtime::User.password
fill_in :user_password_confirmation, with: Runtime::User.password
click_button 'Change your password'
end
fill_in :user_login, with: Runtime::User.name
fill_in :user_password, with: Runtime::User.password
click_button 'Sign in'
end
end
end
end
end
module QA
module Page
module Main
class Groups < Page::Base
def prepare_test_namespace
return if page.has_content?(Runtime::Namespace.name)
click_on 'New Group'
fill_in 'group_path', with: Runtime::Namespace.name
fill_in 'group_description',
with: "QA test run at #{Runtime::Namespace.time}"
choose 'Private'
click_button 'Create group'
end
end
end
end
end
module QA
module Page
module Main
class Menu < Page::Base
def go_to_groups
within_global_menu { click_link 'Groups' }
end
def go_to_projects
within_global_menu { click_link 'Projects' }
end
def go_to_admin_area
within_user_menu { click_link 'Admin Area' }
end
def sign_out
within_user_menu do
find('.header-user-dropdown-toggle').click
click_link('Sign out')
end
end
def has_personal_area?
page.has_selector?('.header-user-dropdown-toggle')
end
private
def within_global_menu
find('.global-dropdown-toggle').click
page.within('.global-dropdown-menu') do
yield
end
end
def within_user_menu
page.within('.navbar-nav') do
yield
end
end
end
end
end
end
module QA
module Page
module Main
class Projects < Page::Base
def go_to_new_project
##
# There are 'New Project' and 'New project' buttons on the projects
# page, so we can't use `click_on`.
#
button = find('a', text: /^new project$/i)
button.click
end
end
end
end
end
module QA
module Page
module Project
class New < Page::Base
def choose_test_namespace
find('#s2id_project_namespace_id').click
find('.select2-result-label', text: Runtime::Namespace.name).click
end
def choose_name(name)
fill_in 'project_path', with: name
end
def add_description(description)
fill_in 'project_description', with: description
end
def create_new_project
click_on 'Create project'
end
end
end
end
end
module QA
module Page
module Project
class Show < Page::Base
def choose_repository_clone_http
find('#clone-dropdown').click
page.within('#clone-dropdown') do
find('span', text: 'HTTP').click
end
end
def repository_location
find('#project_clone').value
end
def wait_for_push
sleep 5
end
end
end
end
end
module QA
module Runtime
module Namespace
extend self
def time
@time ||= Time.now
end
def name
'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S')
end
end
end
end
module QA
module Runtime
##
# Class that is responsible for plugging CE/EE extensions in, depending on
# existence of EE module.
#
# We need that to reduce the probability of conflicts when merging
# CE to EE.
#
class Release
def initialize
require "qa/#{version.downcase}/strategy"
end
def version
@version ||= File.directory?("#{__dir__}/../ee") ? :EE : :CE
end
def strategy
QA.const_get("QA::#{version}::Strategy")
end
def self.method_missing(name, *args)
self.new.strategy.public_send(name, *args)
end
end
end
end
module QA
module Runtime
module User
extend self
def name
ENV['GITLAB_USERNAME'] || 'root'
end
def password
ENV['GITLAB_PASSWORD'] || 'test1234'
end
end
end
end
module QA
module Scenario
module Actable
def act(*args, &block)
instance_exec(*args, &block)
end
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def perform
yield new if block_given?
end
def act(*args, &block)
new.act(*args, &block)
end
end
end
end
end
require 'securerandom'
module QA
module Scenario
module Gitlab
module Project
class Create < Scenario::Template
attr_writer :description
def name=(name)
@name = "#{name}-#{SecureRandom.hex(8)}"
end
def perform
Page::Main::Menu.act { go_to_groups }
Page::Main::Groups.act { prepare_test_namespace }
Page::Main::Menu.act { go_to_projects }
Page::Main::Projects.act { go_to_new_project }
Page::Project::New.perform do |page|
page.choose_test_namespace
page.choose_name(@name)
page.add_description(@description)
page.create_new_project
end
end
end
end
end
end
end
module QA
module Scenario
class Template
def self.perform(*args)
new.tap do |scenario|
yield scenario if block_given?
return scenario.perform(*args)
end
end
def perform(*_args)
raise NotImplementedError
end
end
end
end
module QA
module Scenario
module Test
##
# Run test suite against any GitLab instance,
# including staging and on-premises installation.
#
class Instance < Scenario::Template
def perform(address, *files)
Specs::Config.perform do |specs|
specs.address = address
end
##
# Perform before hooks, which are different for CE and EE
#
Runtime::Release.perform_before_hooks
Specs::Runner.perform do |specs|
specs.rspec('--tty', files.any? ? files : 'qa/specs/features')
end
end
end
end
end
end
require 'rspec/core'
require 'capybara/rspec'
require 'capybara-webkit'
require 'capybara-screenshot/rspec'
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/LineLength
module QA
module Specs
class Config < Scenario::Template
attr_writer :address
def initialize
@address = ENV['GITLAB_URL']
end
def perform
raise 'Please configure GitLab address!' unless @address
configure_rspec!
configure_capybara!
configure_webkit!
end
def configure_rspec!
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
# This option will default to `true` in RSpec 4. It makes the `description`
# and `failure_message` of custom matchers include text for helper methods
# defined using `chain`.
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
# Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended, and will default to
# `true` in RSpec 4.
mocks.verify_partial_doubles = true
end
# Run specs in random order to surface order dependencies.
config.order = :random
Kernel.srand config.seed
config.before(:all) do
page.current_window.resize_to(1200, 1800)
end
config.formatter = :documentation
config.color = true
end
end
def configure_capybara!
Capybara.configure do |config|
config.app_host = @address
config.default_driver = :webkit
config.javascript_driver = :webkit
config.default_max_wait_time = 4
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = 'tmp'
end
end
def configure_webkit!
Capybara::Webkit.configure do |config|
config.allow_url(@address)
config.block_unknown_urls
end
rescue RuntimeError # rubocop:disable Lint/HandleExceptions
# TODO, Webkit is already configured, this make this
# configuration step idempotent, should be improved.
end
end
end
end
module QA
feature 'standard root login' do
scenario 'user logs in using credentials' do
Page::Main::Entry.act { sign_in_using_credentials }
# TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly.
#
Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
end
end
end
end
module QA
feature 'create a new project' do
scenario 'user creates a new project' do
Page::Main::Entry.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |project|
project.name = 'awesome-project'
project.description = 'create awesome project test'
end
expect(page).to have_content(
/Project \S?awesome-project\S+ was successfully created/
)
expect(page).to have_content('create awesome project test')
expect(page).to have_content('The repository for this project is empty')
end
end
end
module QA
feature 'clone code from the repository' do
context 'with regular account over http' do
given(:location) do
Page::Project::Show.act do
choose_repository_clone_http
repository_location
end
end
before do
Page::Main::Entry.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project-with-code'
scenario.description = 'project for git clone tests'
end
Git::Repository.perform do |repository|
repository.location = location
repository.use_default_credentials
repository.act do
clone
configure_identity('GitLab QA', 'root@gitlab.com')
commit_file('test.rb', 'class Test; end', 'Add Test class')
commit_file('README.md', '# Test', 'Add Readme')
push_changes
end
end
end
scenario 'user performs a deep clone' do
Git::Repository.perform do |repository|
repository.location = location
repository.use_default_credentials
repository.act { clone }
expect(repository.commits.size).to eq 2
end
end
scenario 'user performs a shallow clone' do
Git::Repository.perform do |repository|
repository.location = location
repository.use_default_credentials
repository.act { shallow_clone }
expect(repository.commits.size).to eq 1
expect(repository.commits.first).to include 'Add Readme'
end
end
end
end
end
module QA
feature 'push code to repository' do
context 'with regular account over http' do
scenario 'user pushes code to the repository' do
Page::Main::Entry.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project_with_code'
scenario.description = 'project with repository'
end
Git::Repository.perform do |repository|
repository.location = Page::Project::Show.act do
choose_repository_clone_http
repository_location
end
repository.use_default_credentials
repository.act do
clone
configure_identity('GitLab QA', 'root@gitlab.com')
add_file('README.md', '# This is test project')
commit('Add README.md')
push_changes
end
end
Page::Project::Show.act do
wait_for_push
refresh
end
expect(page).to have_content('README.md')
expect(page).to have_content('This is test project')
end
end
end
end
require 'rspec/core'
module QA
module Specs
class Runner
include Scenario::Actable
def rspec(*args)
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
end
end
end
end
end
describe QA::Runtime::Release do
context 'when release version has extension strategy' do
let(:strategy) { spy('strategy') }
before do
stub_const('QA::CE::Strategy', strategy)
stub_const('QA::EE::Strategy', strategy)
end
describe '#version' do
it 'return either CE or EE version' do
expect(subject.version).to eq(:CE).or eq(:EE)
end
end
describe '#strategy' do
it 'return the strategy constant' do
expect(subject.strategy).to eq strategy
end
end
describe 'delegated class methods' do
it 'delegates all calls to strategy class' do
described_class.some_method(1, 2)
expect(strategy).to have_received(:some_method)
.with(1, 2)
end
end
end
context 'when release version does not have extension strategy' do
before do
allow_any_instance_of(described_class)
.to receive(:version).and_return('something')
end
describe '#strategy' do
it 'raises error' do
expect { subject.strategy }.to raise_error(LoadError)
end
end
describe 'delegated class methods' do
it 'raises error' do
expect { described_class.some_method(2, 3) }.to raise_error(LoadError)
end
end
end
end
describe QA::Scenario::Actable do
subject do
Class.new do
include QA::Scenario::Actable
attr_accessor :something
def do_something(arg = nil)
"some#{arg}"
end
end
end
describe '.act' do
it 'provides means to run steps' do
result = subject.act { do_something }
expect(result).to eq 'some'
end
it 'supports passing variables' do
result = subject.act('thing') do |variable|
do_something(variable)
end
expect(result).to eq 'something'
end
it 'returns value from the last method' do
result = subject.act { 'test' }
expect(result).to eq 'test'
end
end
describe '.perform' do
it 'makes it possible to pass binding' do
variable = 'something'
result = subject.perform do |object|
object.something = variable
end
expect(result).to eq 'something'
end
end
end
require_relative '../qa'
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.disable_monkey_patching!
config.expose_dsl_globally = true
config.warnings = true
config.profile_examples = 10
config.order = :random
Kernel.srand config.seed
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment