Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
532eca0362
commit
1b6de87b5c
|
@ -33,6 +33,7 @@ import {
|
||||||
TH_CREATED_AT_TEST_ID,
|
TH_CREATED_AT_TEST_ID,
|
||||||
TH_INCIDENT_SLA_TEST_ID,
|
TH_INCIDENT_SLA_TEST_ID,
|
||||||
TH_SEVERITY_TEST_ID,
|
TH_SEVERITY_TEST_ID,
|
||||||
|
TH_ESCALATION_STATUS_TEST_ID,
|
||||||
TH_PUBLISHED_TEST_ID,
|
TH_PUBLISHED_TEST_ID,
|
||||||
INCIDENT_DETAILS_PATH,
|
INCIDENT_DETAILS_PATH,
|
||||||
trackIncidentCreateNewOptions,
|
trackIncidentCreateNewOptions,
|
||||||
|
@ -67,8 +68,11 @@ export default {
|
||||||
{
|
{
|
||||||
key: 'escalationStatus',
|
key: 'escalationStatus',
|
||||||
label: s__('IncidentManagement|Status'),
|
label: s__('IncidentManagement|Status'),
|
||||||
thClass: `${thClass} gl-w-eighth gl-pointer-events-none`,
|
thClass: `${thClass} gl-w-eighth`,
|
||||||
tdClass,
|
tdClass: `${tdClass} sortable-cell`,
|
||||||
|
actualSortKey: 'ESCALATION_STATUS',
|
||||||
|
sortable: true,
|
||||||
|
thAttr: TH_ESCALATION_STATUS_TEST_ID,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
|
|
|
@ -47,6 +47,7 @@ export const ESCALATION_STATUSES = {
|
||||||
export const DEFAULT_PAGE_SIZE = 20;
|
export const DEFAULT_PAGE_SIZE = 20;
|
||||||
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
|
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
|
||||||
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
|
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
|
||||||
|
export const TH_ESCALATION_STATUS_TEST_ID = { 'data-testid': 'incident-management-status-sort' };
|
||||||
export const TH_INCIDENT_SLA_TEST_ID = { 'data-testid': 'incident-management-sla' };
|
export const TH_INCIDENT_SLA_TEST_ID = { 'data-testid': 'incident-management-sla' };
|
||||||
export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' };
|
export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' };
|
||||||
export const INCIDENT_DETAILS_PATH = 'incident';
|
export const INCIDENT_DETAILS_PATH = 'incident';
|
||||||
|
|
|
@ -61,7 +61,7 @@ class IssueEntity < IssuableEntity
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :locked_discussion_docs_path, if: -> (issue) { issue.discussion_locked? } do |issue|
|
expose :locked_discussion_docs_path, if: -> (issue) { issue.discussion_locked? } do |issue|
|
||||||
help_page_path('user/discussions/index.md', anchor: 'lock-discussions')
|
help_page_path('user/discussions/index.md', anchor: 'prevent-comments-by-locking-an-issue')
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :is_project_archived do |issue|
|
expose :is_project_archived do |issue|
|
||||||
|
|
|
@ -43,7 +43,7 @@ class MergeRequestNoteableEntity < IssuableEntity
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :locked_discussion_docs_path, if: -> (merge_request) { merge_request.discussion_locked? } do |merge_request|
|
expose :locked_discussion_docs_path, if: -> (merge_request) { merge_request.discussion_locked? } do |merge_request|
|
||||||
help_page_path('user/discussions/index.md', anchor: 'lock-discussions')
|
help_page_path('user/discussions/index.md', anchor: 'prevent-comments-by-locking-an-issue')
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :is_project_archived do |merge_request|
|
expose :is_project_archived do |merge_request|
|
||||||
|
|
|
@ -345,8 +345,8 @@ By default, the API fuzzer uses the Postman file to resolve Postman variable val
|
||||||
is set in a GitLab CI/CD variable `FUZZAPI_POSTMAN_COLLECTION_VARIABLES`, then the JSON
|
is set in a GitLab CI/CD variable `FUZZAPI_POSTMAN_COLLECTION_VARIABLES`, then the JSON
|
||||||
file takes precedence to get Postman variable values.
|
file takes precedence to get Postman variable values.
|
||||||
|
|
||||||
Although Postman can export environment variables into a JSON file, the format is not compatible
|
WARNING:
|
||||||
with the JSON expected by `FUZZAPI_POSTMAN_COLLECTION_VARIABLES`.
|
Although Postman can export environment variables into a JSON file, the format is not compatible with the JSON expected by `FUZZAPI_POSTMAN_COLLECTION_VARIABLES`.
|
||||||
|
|
||||||
Here is an example of using `FUZZAPI_POSTMAN_COLLECTION_VARIABLES`:
|
Here is an example of using `FUZZAPI_POSTMAN_COLLECTION_VARIABLES`:
|
||||||
|
|
||||||
|
|
|
@ -391,8 +391,8 @@ By default, the DAST API scanner uses the Postman file to resolve Postman variab
|
||||||
is set in a GitLab CI environment variable `DAST_API_POSTMAN_COLLECTION_VARIABLES`, then the JSON
|
is set in a GitLab CI environment variable `DAST_API_POSTMAN_COLLECTION_VARIABLES`, then the JSON
|
||||||
file takes precedence to get Postman variable values.
|
file takes precedence to get Postman variable values.
|
||||||
|
|
||||||
Although Postman can export environment variables into a JSON file, the format is not compatible
|
WARNING:
|
||||||
with the JSON expected by `DAST_API_POSTMAN_COLLECTION_VARIABLES`.
|
Although Postman can export environment variables into a JSON file, the format is not compatible with the JSON expected by `DAST_API_POSTMAN_COLLECTION_VARIABLES`.
|
||||||
|
|
||||||
Here is an example of using `DAST_API_POSTMAN_COLLECTION_VARIABLES`:
|
Here is an example of using `DAST_API_POSTMAN_COLLECTION_VARIABLES`:
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
# Harbor container registry integration **(FREE)**
|
# Harbor container registry integration **(FREE)**
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80999) in GitLab 14.9.
|
||||||
|
|
||||||
Use Harbor as the container registry for your GitLab project.
|
Use Harbor as the container registry for your GitLab project.
|
||||||
|
|
||||||
[Harbor](https://goharbor.io/) is an open source registry that can help you manage artifacts across cloud native compute platforms, like Kubernetes and Docker.
|
[Harbor](https://goharbor.io/) is an open source registry that can help you manage artifacts across cloud native compute platforms, like Kubernetes and Docker.
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello world</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -194,6 +194,8 @@ module QA
|
||||||
def initialize(instance, page_class)
|
def initialize(instance, page_class)
|
||||||
@session_address = Runtime::Address.new(instance, page_class)
|
@session_address = Runtime::Address.new(instance, page_class)
|
||||||
@page_class = page_class
|
@page_class = page_class
|
||||||
|
|
||||||
|
Session.enable_interception if Runtime::Env.can_intercept?
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
|
@ -255,6 +257,27 @@ module QA
|
||||||
@network_conditions_configured = false
|
@network_conditions_configured = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.enable_interception
|
||||||
|
script = File.read("#{__dir__}/script_extensions/interceptor.js")
|
||||||
|
command = {
|
||||||
|
cmd: 'Page.addScriptToEvaluateOnNewDocument',
|
||||||
|
params: {
|
||||||
|
source: script
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@interceptor_script_params = Capybara.current_session.driver.browser.send(:bridge).send_command(command)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.disable_interception
|
||||||
|
return unless @interceptor_script_params
|
||||||
|
|
||||||
|
command = {
|
||||||
|
cmd: 'Page.removeScriptToEvaluateOnNewDocument',
|
||||||
|
params: @interceptor_script_params
|
||||||
|
}
|
||||||
|
Capybara.current_session.driver.browser.send(:bridge).send_command(command)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def simulate_slow_connection
|
def simulate_slow_connection
|
||||||
|
|
|
@ -37,6 +37,14 @@ module QA
|
||||||
ENV['QA_PRAEFECT_REPOSITORY_STORAGE']
|
ENV['QA_PRAEFECT_REPOSITORY_STORAGE']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def interception_enabled?
|
||||||
|
enabled?(ENV['INTERCEPT_REQUESTS'], default: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_intercept?
|
||||||
|
browser == :chrome && interception_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
def ci_job_url
|
def ci_job_url
|
||||||
ENV['CI_JOB_URL']
|
ENV['CI_JOB_URL']
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
(() => {
|
||||||
|
const CACHE_NAME = 'INTERCEPTOR_CACHE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and parses JSON from the sessionStorage cache
|
||||||
|
* @returns {(Object)}
|
||||||
|
*/
|
||||||
|
const getCache = () => {
|
||||||
|
return JSON.parse(sessionStorage.getItem(CACHE_NAME));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits an object to the sessionStorage cache
|
||||||
|
* @param {Object} data
|
||||||
|
*/
|
||||||
|
const saveCache = (data) => {
|
||||||
|
sessionStorage.setItem(CACHE_NAME, JSON.stringify(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the cache is available
|
||||||
|
* and if the current context has access to it
|
||||||
|
* @returns {boolean} can we access the cache?
|
||||||
|
*/
|
||||||
|
const checkCache = () => {
|
||||||
|
try {
|
||||||
|
getCache();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(`Couldn't access cache: ${error.toString()}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback cacheCommitCallback
|
||||||
|
* @param {object} cache
|
||||||
|
* @return {object} mutated cache
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the cache is available, takes a callback function that is called
|
||||||
|
* with an object returned from getCache,
|
||||||
|
* and saves whatever is returned from the callback function
|
||||||
|
* to the cache
|
||||||
|
* @param {cacheCommitCallback} cb
|
||||||
|
*/
|
||||||
|
const commitToCache = (cb) => {
|
||||||
|
if (checkCache()) {
|
||||||
|
const cache = cb(getCache());
|
||||||
|
saveCache(cache);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Interceptor = {
|
||||||
|
saveCache,
|
||||||
|
commitToCache,
|
||||||
|
getCache,
|
||||||
|
checkCache,
|
||||||
|
activeFetchRequests: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pureFetch = window.fetch;
|
||||||
|
const pureXHROpen = window.XMLHttpRequest.prototype.open;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replacement for XMLHttpRequest.prototype.open
|
||||||
|
* listens for complete xhr events
|
||||||
|
* if the xhr response has a status code higher than 400
|
||||||
|
* then commit request/response metadata to the cache
|
||||||
|
* @param method intercepted HTTP method (GET|POST|etc..)
|
||||||
|
* @param url intercepted HTTP url
|
||||||
|
* @param args intercepted XHR arguments (credentials, headers, options
|
||||||
|
* @return {Promise} the result of the original XMLHttpRequest.prototype.open implementation
|
||||||
|
*/
|
||||||
|
function interceptXhr(method, url, ...args) {
|
||||||
|
this.addEventListener(
|
||||||
|
'readystatechange',
|
||||||
|
() => {
|
||||||
|
const self = this;
|
||||||
|
if (this.readyState === XMLHttpRequest.DONE) {
|
||||||
|
if (this.status >= 400 || this.status === 0) {
|
||||||
|
commitToCache((cache) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
cache.errors ||= [];
|
||||||
|
cache.errors.push({
|
||||||
|
status: self.status === 0 ? -1 : self.status,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers: { 'x-request-id': self.getResponseHeader('x-request-id') },
|
||||||
|
});
|
||||||
|
return cache;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
return pureXHROpen.apply(this, [method, url, ...args]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replacement for fetch implementation
|
||||||
|
* tracks active requests, and commits metadata to the cache
|
||||||
|
* if the response is not ok or was cancelled.
|
||||||
|
* Additionally tracks activeFetchRequests on the Interceptor object
|
||||||
|
* @param url target HTTP url
|
||||||
|
* @param opts fetch options, including request method, body, etc
|
||||||
|
* @param args additional fetch arguments
|
||||||
|
* @returns {Promise<"success"|"error">} the result of the original fetch call
|
||||||
|
*/
|
||||||
|
async function interceptedFetch(url, opts, ...args) {
|
||||||
|
const method = opts && opts.method ? opts.method : 'GET';
|
||||||
|
window.Interceptor.activeFetchRequests += 1;
|
||||||
|
try {
|
||||||
|
const response = await pureFetch(url, opts, ...args);
|
||||||
|
window.Interceptor.activeFetchRequests += -1;
|
||||||
|
const clone = response.clone();
|
||||||
|
|
||||||
|
if (!clone.ok) {
|
||||||
|
commitToCache((cache) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
cache.errors ||= [];
|
||||||
|
cache.errors.push({
|
||||||
|
status: clone.status,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers: { 'x-request-id': clone.headers.get('x-request-id') },
|
||||||
|
});
|
||||||
|
return cache;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
commitToCache((cache) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
cache.errors ||= [];
|
||||||
|
cache.errors.push({
|
||||||
|
status: -1,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
return cache;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.Interceptor.activeFetchRequests += -1;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkCache()) {
|
||||||
|
saveCache({});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.fetch = interceptedFetch;
|
||||||
|
window.XMLHttpRequest.prototype.open = interceptXhr;
|
||||||
|
})();
|
|
@ -61,6 +61,39 @@ module QA
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Log request errors triggered from async api calls from the browser
|
||||||
|
#
|
||||||
|
# If any errors are found in the session, log them
|
||||||
|
# using QA::Runtime::Logger
|
||||||
|
# @param [Capybara::Session] page
|
||||||
|
def log_request_errors(page)
|
||||||
|
return if QA::Runtime::Browser.blank_page?
|
||||||
|
|
||||||
|
url = page.driver.browser.current_url
|
||||||
|
QA::Runtime::Logger.debug "Fetching API error cache for #{url}"
|
||||||
|
|
||||||
|
cache = page.execute_script <<~JS
|
||||||
|
return !(typeof(Interceptor)==="undefined") ? Interceptor.getCache() : null;
|
||||||
|
JS
|
||||||
|
|
||||||
|
return unless cache&.dig('errors')
|
||||||
|
|
||||||
|
grouped_errors = group_errors(cache['errors'])
|
||||||
|
|
||||||
|
errors = grouped_errors.map do |error_metadata, request_id_string|
|
||||||
|
"#{error_metadata} -- #{request_id_string}"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless errors.nil? || errors.empty?
|
||||||
|
QA::Runtime::Logger.error "Interceptor Api Errors\n#{errors.join("\n")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# clear the cache after logging the errors
|
||||||
|
page.execute_script <<~JS
|
||||||
|
Interceptor && Interceptor.saveCache({});
|
||||||
|
JS
|
||||||
|
end
|
||||||
|
|
||||||
def error_report_for(logs)
|
def error_report_for(logs)
|
||||||
logs
|
logs
|
||||||
.map(&:message)
|
.map(&:message)
|
||||||
|
@ -70,6 +103,16 @@ module QA
|
||||||
def logs(page)
|
def logs(page)
|
||||||
page.driver.browser.manage.logs.get(:browser)
|
page.driver.browser.manage.logs.get(:browser)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def group_errors(errors)
|
||||||
|
errors.each_with_object({}) do |error, memo|
|
||||||
|
url = error['url']&.split('?')&.first || 'Unknown url'
|
||||||
|
key = "[#{error['status']}] #{error['method']} #{url}"
|
||||||
|
memo[key] = "Correlation Id: #{error.dig('headers', 'x-request-id') || 'Correlation Id not found'}"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,12 +16,16 @@ module QA
|
||||||
Waiter.wait_until(log: false) do
|
Waiter.wait_until(log: false) do
|
||||||
finished_all_ajax_requests? && (!skip_finished_loading_check ? finished_loading?(wait: 1) : true)
|
finished_all_ajax_requests? && (!skip_finished_loading_check ? finished_loading?(wait: 1) : true)
|
||||||
end
|
end
|
||||||
|
QA::Support::PageErrorChecker.log_request_errors(Capybara.page) if QA::Runtime::Env.can_intercept?
|
||||||
rescue Repeater::WaitExceededError
|
rescue Repeater::WaitExceededError
|
||||||
raise $!, 'Page did not fully load. This could be due to an unending async request or loading icon.'
|
raise $!, 'Page did not fully load. This could be due to an unending async request or loading icon.'
|
||||||
end
|
end
|
||||||
|
|
||||||
def finished_all_ajax_requests?
|
def finished_all_ajax_requests?
|
||||||
Capybara.page.evaluate_script('window.pendingRequests || window.pendingRailsUJSRequests || 0').zero? # rubocop:disable Style/NumericPredicate
|
requests = %w[window.pendingRequests window.pendingRailsUJSRequests 0]
|
||||||
|
requests.unshift('(window.Interceptor && window.Interceptor.activeFetchRequests)') if Runtime::Env.can_intercept?
|
||||||
|
script = requests.join(' || ')
|
||||||
|
Capybara.page.evaluate_script(script).zero? # rubocop:disable Style/NumericPredicate
|
||||||
end
|
end
|
||||||
|
|
||||||
def finished_loading?(wait: DEFAULT_MAX_WAIT_TIME)
|
def finished_loading?(wait: DEFAULT_MAX_WAIT_TIME)
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe 'Interceptor' do
|
||||||
|
let(:browser) { Capybara.current_session }
|
||||||
|
# need a real host for the js runtime
|
||||||
|
let(:url) { "file://#{__dir__}/../../../qa/fixtures/script_extensions/test.html" }
|
||||||
|
|
||||||
|
before(:context) do
|
||||||
|
skip 'Only can test for chrome' unless QA::Runtime::Env.can_intercept?
|
||||||
|
|
||||||
|
QA::Runtime::Browser::Session.enable_interception
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:context) do
|
||||||
|
QA::Runtime::Browser::Session.disable_interception
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
browser.visit url
|
||||||
|
|
||||||
|
clear_cache
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
browser.visit 'about:blank'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Interceptor' do
|
||||||
|
context 'caching' do
|
||||||
|
it 'checks the cache' do
|
||||||
|
expect(check_cache).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if the cache cannot be accessed' do
|
||||||
|
browser.visit 'about:blank'
|
||||||
|
|
||||||
|
expect(check_cache).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets and sets the cache data' do
|
||||||
|
commit_to_cache({ foo: 'bar' })
|
||||||
|
|
||||||
|
expect(get_cache['data']).to eql({ 'foo' => 'bar' })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when intercepting' do
|
||||||
|
let(:resource_url) { 'chrome://chrome-urls' }
|
||||||
|
|
||||||
|
it 'intercepts fetch errors' do
|
||||||
|
trigger_fetch(resource_url, 'GET')
|
||||||
|
|
||||||
|
errors = get_cache['errors']
|
||||||
|
|
||||||
|
expect(errors.size).to be(1)
|
||||||
|
expect(errors[0]['status']).to be(-1)
|
||||||
|
expect(errors[0]['method']).to eql('GET')
|
||||||
|
expect(errors[0]['url']).to eql(resource_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'intercepts xhr' do
|
||||||
|
trigger_xhr(resource_url, 'POST')
|
||||||
|
|
||||||
|
errors = get_cache['errors']
|
||||||
|
|
||||||
|
expect(errors.size).to be(1)
|
||||||
|
expect(errors[0]['status']).to be(-1)
|
||||||
|
expect(errors[0]['method']).to eql('POST')
|
||||||
|
expect(errors[0]['url']).to eql(resource_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_cache
|
||||||
|
browser.execute_script <<~JS
|
||||||
|
Interceptor.saveCache({})
|
||||||
|
JS
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_cache
|
||||||
|
browser.execute_script <<~JS
|
||||||
|
return Interceptor.checkCache()
|
||||||
|
JS
|
||||||
|
end
|
||||||
|
|
||||||
|
def trigger_fetch(url, method)
|
||||||
|
browser.execute_script <<~JS
|
||||||
|
(() => {
|
||||||
|
fetch('#{url}', { method: '#{method}' })
|
||||||
|
})()
|
||||||
|
JS
|
||||||
|
end
|
||||||
|
|
||||||
|
def trigger_xhr(url, method)
|
||||||
|
browser.execute_script <<~JS
|
||||||
|
(() => {
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('#{method}', '#{url}')
|
||||||
|
xhr.send()
|
||||||
|
})()
|
||||||
|
JS
|
||||||
|
end
|
||||||
|
|
||||||
|
def commit_to_cache(payload)
|
||||||
|
browser.execute_script <<~JS
|
||||||
|
Interceptor.commitToCache((cache) => {
|
||||||
|
cache.data = JSON.parse('#{payload.to_json}');
|
||||||
|
return cache
|
||||||
|
})
|
||||||
|
JS
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_cache
|
||||||
|
browser.execute_script <<~JS
|
||||||
|
return Interceptor.getCache()
|
||||||
|
JS
|
||||||
|
end
|
||||||
|
end
|
|
@ -238,6 +238,88 @@ RSpec.describe QA::Support::PageErrorChecker do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '::log_request_errors' do
|
||||||
|
let(:page_url) { 'https://baz.foo' }
|
||||||
|
let(:browser) { double('browser', current_url: page_url) }
|
||||||
|
let(:driver) { double('driver', browser: browser) }
|
||||||
|
let(:session) { double('session', driver: driver) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Capybara).to receive(:current_session).and_return(session)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'logs from the error cache' do
|
||||||
|
error = {
|
||||||
|
'url' => 'https://foo.bar',
|
||||||
|
'status' => 500,
|
||||||
|
'method' => 'GET',
|
||||||
|
'headers' => { 'x-request-id' => '12345' }
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(page).to receive(:driver).and_return(driver)
|
||||||
|
expect(page).to receive(:execute_script).and_return({ 'errors' => [error] })
|
||||||
|
expect(page).to receive(:execute_script)
|
||||||
|
|
||||||
|
expect(QA::Runtime::Logger).to receive(:debug).with("Fetching API error cache for #{page_url}")
|
||||||
|
expect(QA::Runtime::Logger).to receive(:error).with(<<~ERROR.chomp)
|
||||||
|
Interceptor Api Errors
|
||||||
|
[500] GET https://foo.bar -- Correlation Id: 12345
|
||||||
|
ERROR
|
||||||
|
|
||||||
|
QA::Support::PageErrorChecker.log_request_errors(page)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes duplicates' do
|
||||||
|
error = {
|
||||||
|
'url' => 'https://foo.bar',
|
||||||
|
'status' => 500,
|
||||||
|
'method' => 'GET',
|
||||||
|
'headers' => { 'x-request-id' => '12345' }
|
||||||
|
}
|
||||||
|
expect(page).to receive(:driver).and_return(driver)
|
||||||
|
expect(page).to receive(:execute_script).and_return({ 'errors' => [error, error, error] })
|
||||||
|
expect(page).to receive(:execute_script)
|
||||||
|
|
||||||
|
expect(QA::Runtime::Logger).to receive(:debug).with("Fetching API error cache for #{page_url}")
|
||||||
|
expect(QA::Runtime::Logger).to receive(:error).with(<<~ERROR.chomp).exactly(1).time
|
||||||
|
Interceptor Api Errors
|
||||||
|
[500] GET https://foo.bar -- Correlation Id: 12345
|
||||||
|
ERROR
|
||||||
|
|
||||||
|
QA::Support::PageErrorChecker.log_request_errors(page)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'chops the url query string' do
|
||||||
|
error = {
|
||||||
|
'url' => 'https://foo.bar?query={ sensitive-data: 12345 }',
|
||||||
|
'status' => 500,
|
||||||
|
'method' => 'GET',
|
||||||
|
'headers' => { 'x-request-id' => '12345' }
|
||||||
|
}
|
||||||
|
expect(page).to receive(:driver).and_return(driver)
|
||||||
|
expect(page).to receive(:execute_script).and_return({ 'errors' => [error] })
|
||||||
|
expect(page).to receive(:execute_script)
|
||||||
|
|
||||||
|
expect(QA::Runtime::Logger).to receive(:debug).with("Fetching API error cache for #{page_url}")
|
||||||
|
expect(QA::Runtime::Logger).to receive(:error).with(<<~ERROR.chomp)
|
||||||
|
Interceptor Api Errors
|
||||||
|
[500] GET https://foo.bar -- Correlation Id: 12345
|
||||||
|
ERROR
|
||||||
|
|
||||||
|
QA::Support::PageErrorChecker.log_request_errors(page)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns if cache is nil' do
|
||||||
|
expect(page).to receive(:driver).and_return(driver)
|
||||||
|
expect(page).to receive(:execute_script).and_return(nil)
|
||||||
|
|
||||||
|
expect(QA::Runtime::Logger).to receive(:debug).with("Fetching API error cache for #{page_url}")
|
||||||
|
expect(QA::Runtime::Logger).not_to receive(:error)
|
||||||
|
|
||||||
|
QA::Support::PageErrorChecker.log_request_errors(page)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.logs' do
|
describe '.logs' do
|
||||||
before do
|
before do
|
||||||
logs_class = Class.new do
|
logs_class = Class.new do
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
I18N,
|
I18N,
|
||||||
TH_CREATED_AT_TEST_ID,
|
TH_CREATED_AT_TEST_ID,
|
||||||
TH_SEVERITY_TEST_ID,
|
TH_SEVERITY_TEST_ID,
|
||||||
|
TH_ESCALATION_STATUS_TEST_ID,
|
||||||
TH_PUBLISHED_TEST_ID,
|
TH_PUBLISHED_TEST_ID,
|
||||||
TH_INCIDENT_SLA_TEST_ID,
|
TH_INCIDENT_SLA_TEST_ID,
|
||||||
trackIncidentCreateNewOptions,
|
trackIncidentCreateNewOptions,
|
||||||
|
@ -297,6 +298,7 @@ describe('Incidents List', () => {
|
||||||
description | selector | initialSort | firstSort | nextSort
|
description | selector | initialSort | firstSort | nextSort
|
||||||
${'creation date'} | ${TH_CREATED_AT_TEST_ID} | ${descSort} | ${ascSort} | ${descSort}
|
${'creation date'} | ${TH_CREATED_AT_TEST_ID} | ${descSort} | ${ascSort} | ${descSort}
|
||||||
${'severity'} | ${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
|
${'severity'} | ${TH_SEVERITY_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
|
||||||
|
${'status'} | ${TH_ESCALATION_STATUS_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
|
||||||
${'publish date'} | ${TH_PUBLISHED_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
|
${'publish date'} | ${TH_PUBLISHED_TEST_ID} | ${noneSort} | ${descSort} | ${ascSort}
|
||||||
${'due date'} | ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${ascSort} | ${descSort}
|
${'due date'} | ${TH_INCIDENT_SLA_TEST_ID} | ${noneSort} | ${ascSort} | ${descSort}
|
||||||
`(
|
`(
|
||||||
|
|
Loading…
Reference in New Issue