Commit 720a1162 authored by Andrejs Cunskis's avatar Andrejs Cunskis

Merge branch 'qa/webhook_specs_2' into 'master'

Adds E2E tests for WebHooks integration

See merge request gitlab-org/gitlab!80077
parents 815d5d5b ab6c6a51
...@@ -20,6 +20,7 @@ This is a partial list of the [RSpec metadata](https://relishapp.com/rspec/rspec ...@@ -20,6 +20,7 @@ This is a partial list of the [RSpec metadata](https://relishapp.com/rspec/rspec
| `:github` | The test requires a GitHub personal access token. | | `:github` | The test requires a GitHub personal access token. |
| `:group_saml` | The test requires a GitLab instance that has SAML SSO enabled at the group level. Interacts with an external SAML identity provider. Paired with the `:orchestrated` tag. | | `:group_saml` | The test requires a GitLab instance that has SAML SSO enabled at the group level. Interacts with an external SAML identity provider. Paired with the `:orchestrated` tag. |
| `:instance_saml` | The test requires a GitLab instance that has SAML SSO enabled at the instance level. Interacts with an external SAML identity provider. Paired with the `:orchestrated` tag. | | `:instance_saml` | The test requires a GitLab instance that has SAML SSO enabled at the instance level. Interacts with an external SAML identity provider. Paired with the `:orchestrated` tag. |
| `:integrations` | This aims to test the available [integrations](../../../user/project/integrations/overview.md#integrations-listing). The test requires Docker to be installed in the run context. It will provision the containers and can be run against a local instance or using the `gitlab-qa` scenario `Test::Integration::Integrations` |
| `:service_ping_disabled` | The test interacts with the GitLab configuration service ping at the instance level to turn admin setting service ping checkbox on or off. This tag will have the test run only in the `service_ping_disabled` job and must be paired with the `:orchestrated` and `:requires_admin` tags. | | `:service_ping_disabled` | The test interacts with the GitLab configuration service ping at the instance level to turn admin setting service ping checkbox on or off. This tag will have the test run only in the `service_ping_disabled` job and must be paired with the `:orchestrated` and `:requires_admin` tags. |
| `:jira` | The test requires a Jira Server. [GitLab-QA](https://gitlab.com/gitlab-org/gitlab-qa) provisions the Jira Server in a Docker container when the `Test::Integration::Jira` test scenario is run. | `:jira` | The test requires a Jira Server. [GitLab-QA](https://gitlab.com/gitlab-org/gitlab-qa) provisions the Jira Server in a Docker container when the `Test::Integration::Jira` test scenario is run.
| `:kubernetes` | The test includes a GitLab instance that is configured to be run behind an SSH tunnel, allowing a TLS-accessible GitLab. This test also includes provisioning of at least one Kubernetes cluster to test against. _This tag is often be paired with `:orchestrated`._ | | `:kubernetes` | The test includes a GitLab instance that is configured to be run behind an SSH tunnel, allowing a TLS-accessible GitLab. This test also includes provisioning of at least one Kubernetes cluster to test against. _This tag is often be paired with `:orchestrated`._ |
......
# frozen_string_literal: true
module QA
module Resource
class ProjectWebHook < Base
EVENT_TRIGGERS = %i[
issues
job
merge_requests
note
pipeline
push
tag_push
wiki_page
confidential_issues
confidential_note
].freeze
attr_accessor :url, :enable_ssl, :id
attribute :project do
Project.fabricate_via_api! do |resource|
resource.name = 'project-with-webhooks'
end
end
EVENT_TRIGGERS.each do |trigger|
attribute "#{trigger}_events".to_sym do
false
end
end
def initialize
@id = nil
@enable_ssl = false
@url = nil
end
def resource_web_url(resource)
"/project/#{project.name}/~/hooks/##{resource[:id]}/edit"
end
def api_get_path
"/projects/#{project.id}/hooks"
end
def api_post_path
api_get_path
end
def api_post_body
body = {
id: project.id,
url: url,
enable_ssl_verification: enable_ssl
}
EVENT_TRIGGERS.each_with_object(body) do |trigger, memo|
attr = "#{trigger}_events"
memo[attr.to_sym] = send(attr)
memo
end
end
end
end
end
...@@ -295,6 +295,14 @@ module QA ...@@ -295,6 +295,14 @@ module QA
ENV['JIRA_HOSTNAME'] ENV['JIRA_HOSTNAME']
end end
# this is set by the integrations job
# which will allow bidirectional communication
# between the app and the specs container
# should the specs container spin up a server
def qa_hostname
ENV['QA_HOSTNAME']
end
def cache_namespace_name? def cache_namespace_name?
enabled?(ENV['CACHE_NAMESPACE_NAME'], default: true) enabled?(ENV['CACHE_NAMESPACE_NAME'], default: true)
end end
......
# frozen_string_literal: true
module QA
module Service
module DockerRun
class Smocker < Base
def initialize
@image = 'thiht/smocker:0.17.1'
@name = 'smocker-server'
@public_port = '8080'
@admin_port = '8081'
super
@network_cache = network
end
def host_name
return '127.0.0.1' unless QA::Runtime::Env.running_in_ci? || QA::Runtime::Env.qa_hostname
"#{@name}.#{@network_cache}"
end
def base_url
"http://#{host_name}:#{@public_port}"
end
def admin_url
"http://#{host_name}:#{@admin_port}"
end
def wait_for_running
Support::Waiter.wait_until(raise_on_failure: false, reload_page: false) do
running?
end
end
def register!
command = <<~CMD.tr("\n", ' ')
docker run -d --rm
--network #{@network_cache}
--hostname #{host_name}
--name #{@name}
--publish #{@public_port}:8080
--publish #{@admin_port}:8081
#{@image}
CMD
unless QA::Runtime::Env.running_in_ci? || QA::Runtime::Env.qa_hostname
command.gsub!("--network #{@network_cache} ", '')
end
shell command
end
end
end
end
end
# frozen_string_literal: true
module QA
RSpec.describe 'Create' do
describe 'WebHooks integration', :requires_admin, :integrations, :orchestrated do
before(:context) do
toggle_local_requests(true)
end
after(:context) do
Vendor::Smocker::SmockerApi.teardown!
end
let(:session) { SecureRandom.hex(5) }
it 'sends a push event', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348945' do
setup_webhook(push: true) do |webhook, smocker|
Resource::Repository::ProjectPush.fabricate! do |project_push|
project_push.project = webhook.project
end
wait_until do
!smocker.history(session).empty?
end
events = smocker.history(session).map(&:as_hook_event)
aggregate_failures do
expect(events.size).to be(1), "Should have 1 event: \n#{events.map(&:raw).join("\n")}"
expect(events[0].project_name).to eql(webhook.project.name)
expect(events[0].push?).to be(true), "Not push event: \n#{events[0].raw}"
end
end
end
it 'sends a merge request event', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349720' do
setup_webhook(merge_requests: true) do |webhook, smocker|
Resource::MergeRequest.fabricate_via_api! do |merge_request|
merge_request.project = webhook.project
end
wait_until do
!smocker.history(session).empty?
end
events = smocker.history(session).map(&:as_hook_event)
aggregate_failures do
expect(events.size).to be(1), "Should have 1 event: \n#{events.map(&:raw).join("\n")}"
expect(events[0].project_name).to eql(webhook.project.name)
expect(events[0].mr?).to be(true), "Not MR event: \n#{events[0].raw}"
end
end
end
it 'sends a wiki page event', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349722' do
setup_webhook(wiki_page: true) do |webhook, smocker|
Resource::Wiki::ProjectPage.fabricate_via_api! do |page|
page.project = webhook.project
end
wait_until do
!smocker.history(session).empty?
end
events = smocker.history(session).map(&:as_hook_event)
aggregate_failures do
expect(events.size).to be(1), "Should have 1 event: \n#{events.map(&:raw).join("\n")}"
expect(events[0].project_name).to eql(webhook.project.name)
expect(events[0].wiki?).to be(true), "Not wiki event: \n#{events[0].raw}"
end
end
end
it 'sends an issues and note event', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349723' do
setup_webhook(issues: true, note: true) do |webhook, smocker|
issue = Resource::Issue.fabricate_via_api! do |issue_init|
issue_init.project = webhook.project
end
Resource::ProjectIssueNote.fabricate_via_api! do |note|
note.project = issue.project
note.issue = issue
end
wait_until do
smocker.history(session).size > 1
end
events = smocker.history(session).map(&:as_hook_event)
aggregate_failures do
issue_event = events.find(&:issue?)
note_event = events.find(&:note?)
expect(events.size).to be(2), "Should have 2 events: \n#{events.map(&:raw).join("\n")}"
expect(issue_event).not_to be(nil), "Not issue event: \n#{events[0].raw}"
expect(note_event).not_to be(nil), "Not note event: \n#{events[1].raw}"
end
end
end
private
def setup_webhook(**event_args)
Vendor::Smocker::SmockerApi.init(wait: 10) do |smocker|
smocker.register(session: session)
webhook = Resource::ProjectWebHook.fabricate_via_api! do |hook|
hook.url = smocker.url
event_args.each do |event, bool|
hook.send("#{event}_events=", bool)
end
end
yield(webhook, smocker)
smocker.reset
end
end
def toggle_local_requests(on)
Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: on)
end
def wait_until(timeout = 120, &block)
Support::Waiter.wait_until(max_duration: timeout, reload_page: false, raise_on_failure: false, &block)
end
end
end
end
# frozen_string_literal: true
module QA
module Vendor
module Smocker
class EventPayload
def initialize(hook_data)
@hook_data = hook_data
end
def raw
@hook_data
end
def event
raw[:object_kind]&.to_sym
end
def project_name
raw.dig(:project, :name)
end
def mr?
event == :merge_request
end
def issue?
event == :issue
end
def note?
event == :note
end
def push?
event == :push
end
def tag?
event == :tag
end
def wiki?
event == :wiki_page
end
end
end
end
end
# frozen_string_literal: true
require_relative './event_payload'
require 'time'
module QA
module Vendor
module Smocker
class HistoryResponse
attr_reader :payload
def initialize(payload)
@payload = payload
end
# Smocker context including call counter
def context
payload[:context]
end
# Smocker request data
def request
payload[:request]
end
# @return [EventPayload] the request body as a webhook event
def as_hook_event
body = request&.dig(:body)
EventPayload.new body if body
end
# @return [Time] Time request was recieved
def received
date = request&.dig(:date)
Time.parse date if date
end
# Find time elapsed since <target>
#
# @param target [Time] target time
# @return [Integer] seconds elapsed since <target>
def elapsed(target)
(received.to_f - target.to_f).round if received
end
def response
payload[:response]
end
end
end
end
end
# frozen_string_literal: true
module QA
module Vendor
module Smocker
class SmockerApi
include Scenario::Actable
include Support::API
DEFAULT_MOCK = <<~YAML
- request:
method: POST
path: /default
response:
headers:
Content-Type: application/json
body: '{}'
YAML
# @param wait [Integer] seconds to wait for server
# @yieldparam [SmockerApi] the api object ready for interaction
def self.init(**wait_args)
if @container.nil?
@container = Service::DockerRun::Smocker.new
@container.register!
@container.wait_for_running
end
yield new(@container, **wait_args)
end
def self.teardown!
@container&.remove!
end
def initialize(container, **wait_args)
@container = container
wait_for_ready(**wait_args)
end
# @return [String] Base url of mock endpoint
def base_url
@container.base_url
end
# @return [String] Url of admin endpoint
def admin_url
@container.admin_url
end
# @param endpoint [String] path for mock endpoint
# @return [String] url for mock endpoint
def url(endpoint = 'default')
"#{base_url}/#{endpoint}"
end
# Waits for the smocker server to be ready
#
# @param wait [Integer] wait duration for smocker readiness
def wait_for_ready(wait: 10)
Support::Waiter.wait_until(max_duration: wait, reload_page: false, raise_on_failure: true) do
ready?
end
end
# Is smocker server ready for interaction?
#
# @return [Boolean]
def ready?
QA::Runtime::Logger.debug 'Checking Smocker readiness'
get("#{admin_url}/version")
true
# rescuing StandardError because RestClient::ExceptionWithResponse isn't propagating
rescue StandardError => e
QA::Runtime::Logger.debug "Smocker not ready yet \n #{e}"
false
end
# Clears mocks and history
#
# @param force [Boolean] remove locked mocks?
# @return [Boolean] reset was successful?
def reset(force: true)
response = post("#{admin_url}/reset?force=#{force}", {}.to_json)
parse_body(response)['message'] == 'Reset successful'
end
# Fetches an active session id from a name
#
# @param name [String] the name of the session
# @return [String] the unique session id
def get_session_id(name)
sessions = parse_body get("#{admin_url}/sessions/summary")
current = sessions.find do |session|
session[:name] == name
end
current&.dig(:id)
end
# Registers a mock to Smocker
# If a session name is provided, the mock will register to that session
# https://smocker.dev/technical-documentation/mock-definition.html
#
# @param yaml [String] the yaml representing the mock
# @param session [String] the session name for the mock
def register(yaml = DEFAULT_MOCK, session: nil)
query_params = build_params(session: session)
url = "#{admin_url}/mocks?#{query_params}"
headers = { 'Content-Type' => 'application/x-yaml' }
response = post(url, yaml, headers: headers)
parse_body(response)
end
# Fetches call history for a mock
#
# @param session_name [String] the session name for the mock
# @return [Array<HistoryResponse>]
def history(session_name = nil)
query_params = session_name ? build_params(session: get_session_id(session_name)) : ''
response = get("#{admin_url}/history?#{query_params}")
body = parse_body(response)
raise body[:message] unless body.is_a?(Array)
body.map do |entry|
HistoryResponse.new(entry)
end
end
private
def build_params(**args)
args.each_with_object([]) do |(k, v), memo|
memo << "#{k}=#{v}" if v
end.join("&")
end
end
end
end
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