Commit 679e6805 authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into ce-to-ee-2018-03-29

parents 6732f3fd 6aa5e3ff
......@@ -296,7 +296,7 @@ On each database node perform the following:
# Replace XXX.XXX.XXX.XXX/YY with Network Address
postgresql['trust_auth_cidr_addresses'] = %w(XXX.XXX.XXX.XXX/YY)
repmgr['trust_auth_cidr_addresses'] = %w(XXX.XXX.XXX.XXX/YY)
repmgr['trust_auth_cidr_addresses'] = %w(127.0.0.1/32 XXX.XXX.XXX.XXX/YY)
# Replace placeholders:
#
......@@ -537,6 +537,12 @@ Ensure that all migrations ran:
gitlab-rake gitlab:db:configure
```
> **Note**: If you encounter a `rake aborted!` error stating that PGBouncer is failing to connect to
PostgreSQL it may be that your PGBouncer node's IP address is missing from
PostgreSQL's `trust_auth_cidr_addresses` in `gitlab.rb` on your database nodes. See
[PGBouncer error `ERROR: pgbouncer cannot connect to server`](#pgbouncer-error-error-pgbouncer-cannot-connect-to-server)
in the Troubleshooting section before proceeding.
#### Ensure GitLab is running
At this point, your GitLab instance should be up and running. Verify you are
......@@ -966,6 +972,34 @@ For PostgreSQL, it is usually safe to restart the master node by default. Automa
On the consul server nodes, it is important to restart the consul service in a controlled fashion. Read our [consul documentation](consul.md#restarting-the-server-cluster) for instructions on how to restart the service.
#### PGBouncer error `ERROR: pgbouncer cannot connect to server`
You may get this error when running `gitlab-rake gitlab:db:configure` or you
may see the error in the PGBouncer log file.
```
PG::ConnectionBad: ERROR: pgbouncer cannot connect to server
```
The problem may be that your PGBouncer node's IP address is not included in the
`trust_auth_cidr_addresses` setting in `/etc/gitlab/gitlab.rb` on the database nodes.
You can confirm that this is the issue by checking the PostgreSQL log on the master
database node. If you see the following error then `trust_auth_cidr_addresses`
is the problem.
```
2018-03-29_13:59:12.11776 FATAL: no pg_hba.conf entry for host "123.123.123.123", user "pgbouncer", database "gitlabhq_production", SSL off
```
To fix the problem, add the IP address to `/etc/gitlab/gitlab.rb`.
```
postgresql['trust_auth_cidr_addresses'] = %w(123.123.123.123/32 <other_cidrs>)
```
[Reconfigure GitLab] for the changes to take effect.
#### Issues with other components
If you're running into an issue with a component not outlined here, be sure to check the troubleshooting section of their specific documentation page.
......
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
export const setHeadBlobPath = ({ commit }, blobPath) => commit(types.SET_HEAD_BLOB_PATH, blobPath);
export const setBaseBlobPath = ({ commit }, blobPath) => commit(types.SET_BASE_BLOB_PATH, blobPath);
/**
* SAST
*/
export const setSastHeadPath = ({ commit }, path) => commit(types.SET_SAST_HEAD_PATH, path);
export const setSastBasePath = ({ commit }, path) => commit(types.SET_SAST_BASE_PATH, path);
export const requestSastReports = ({ commit }) => commit(types.REQUEST_SAST_REPORTS);
export const receiveSastReports = ({ commit }, response) =>
commit(types.RECEIVE_SAST_REPORTS, response);
export const receiveSastError = ({ commit }, error) =>
commit(types.RECEIVE_SAST_REPORTS_ERROR, error);
export const fetchSastReports = ({ state, dispatch }) => {
const base = state.sast.paths.base;
const head = state.sast.paths.head;
dispatch('requestSastReports');
Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
])
.then(values => {
dispatch('receiveSastReports', {
head: values[0] ? values[0].data : null,
base: values[1] ? values[1].data : null,
});
})
.catch(() => {
dispatch('receiveSastError');
});
};
/**
* SAST CONTAINER
*/
export const setSastContainerHeadPath = ({ commit }, path) =>
commit(types.SET_SAST_CONTAINER_HEAD_PATH, path);
export const setSastContainerBasePath = ({ commit }, path) =>
commit(types.SET_SAST_CONTAINER_BASE_PATH, path);
export const requestSastContainerReports = ({ commit }) =>
commit(types.REQUEST_SAST_CONTAINER_REPORTS);
export const receiveSastContainerReports = ({ commit }, response) =>
commit(types.RECEIVE_SAST_CONTAINER_REPORTS, response);
export const receiveSastContainerError = ({ commit }, error) =>
commit(types.RECEIVE_SAST_CONTAINER_ERROR, error);
export const fetchSastContainerReports = ({ state, dispatch }) => {
const base = state.sastContainer.paths.base;
const head = state.sastContainer.paths.head;
dispatch('requestSastContainerReports');
Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
])
.then(values => {
dispatch('receiveSastContainerReports', {
head: values[0] ? values[0].data : null,
base: values[1] ? values[1].data : null,
});
})
.catch(() => {
dispatch('receiveSastContainerError');
});
};
/**
* DAST
*/
export const setDastHeadPath = ({ commit }, path) => commit(types.SET_DAST_HEAD_PATH, path);
export const setDastBasePath = ({ commit }, path) => commit(types.SET_DAST_BASE_PATH, path);
export const requestDastReports = ({ commit }) => commit(types.REQUEST_DAST_REPORTS);
export const receiveDastReports = ({ commit }, response) =>
commit(types.RECEIVE_DAST_REPORTS, response);
export const receiveDastError = ({ commit }, error) => commit(types.RECEIVE_DAST_ERROR, error);
export const fetchDastReports = ({ state, dispatch }) => {
const base = state.dast.paths.base;
const head = state.dast.paths.head;
dispatch('requestDastReports');
Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
])
.then(values => {
dispatch('receiveDastReports', {
head: values[0] ? values[0].data : null,
base: values[1] ? values[1].data : null,
});
})
.catch(() => {
dispatch('receiveDastError');
});
};
/**
* DEPENDENCY SCANNING
*/
export const setDependencyScanningHeadPath = ({ commit }, path) =>
commit(types.SET_DEPENDENCY_SCANNING_HEAD_PATH, path);
export const setDependencyScanningBasePath = ({ commit }, path) =>
commit(types.SET_DEPENDENCY_SCANNING_BASE_PATH, path);
export const requestDependencyScanningReports = ({ commit }) =>
commit(types.REQUEST_DEPENDENCY_SCANNING_REPORTS);
export const receiveDependencyScanningReports = ({ commit }, response) =>
commit(types.RECEIVE_DEPENDENCY_SCANNING_REPORTS, response);
export const receiveDependencyScanningError = ({ commit }, error) =>
commit(types.RECEIVE_DEPENDENCY_SCANNING_ERROR, error);
export const fetchDependencyScanningReports = ({ state, dispatch }) => {
const base = state.dependencyScanning.paths.base;
const head = state.dependencyScanning.paths.head;
dispatch('requestDependencyScanningReports');
Promise.all([
head ? axios.get(head) : Promise.resolve(),
base ? axios.get(base) : Promise.resolve(),
])
.then(values => {
dispatch('receiveDependencyScanningReports', {
head: values[0] ? values[0].data : null,
base: values[1] ? values[1].data : null,
});
})
.catch(() => {
dispatch('receiveDependencyScanningError');
});
};
import { n__, s__ } from '~/locale';
import { textBuilder, statusIcon } from './utils';
export const groupedSastText = ({ sast }) =>
textBuilder(
'SAST',
sast.paths,
sast.newIssues.length,
sast.resolvedIssues.length,
sast.allIssues.length,
);
export const groupedSastContainerText = ({ sastContainer }) =>
textBuilder(
'Container scanning',
sastContainer.paths,
sastContainer.newIssues.length,
sastContainer.resolvedIssues.length,
);
export const groupedDastText = ({ dast }) =>
textBuilder('DAST', dast.paths, dast.newIssues.length, dast.resolvedIssues.length);
export const groupedDependencyText = ({ dependencyScanning }) =>
textBuilder(
'Dependency scanning',
dependencyScanning.paths,
dependencyScanning.newIssues.length,
dependencyScanning.resolvedIssues.length,
);
export const groupedSummaryText = (state, getters) => {
const { added, fixed } = state.summaryCounts;
// All reports returned error
if (getters.allReportsHaveError) {
return s__('ciReport|Security scanning failed loading any results');
}
// No base is present in any report
if (getters.noBaseInAllReports) {
if (added > 0) {
return n__(
'Security scanning was unable to compare existing and new vulnerabilities. It detected %d vulnerability',
'Security scanning was unable to compare existing and new vulnerabilities. It detected %d vulnerabilities',
added,
);
}
return s__(
'Security scanning was unable to compare existing and new vulnerabilities. It detected no vulnerabilities.',
);
}
const text = [s__('ciReport|Security scanning')];
if (getters.areReportsLoading) {
text.push('(in progress)');
}
if (added > 0 && fixed === 0) {
text.push(n__('detected %d new vulnerability', 'detected %d new vulnerabilities', added));
}
if (added > 0 && fixed > 0) {
text.push(
`${n__('detected %d new vulnerability', 'detected %d new vulnerabilities', added)} ${n__(
'and %d fixed vulnerability',
'and %d fixed vulnerabilities',
fixed,
)}`,
);
}
if (added === 0 && fixed > 0) {
text.push(n__('detected %d fixed vulnerability', 'detected %d fixed vulnerabilities', fixed));
}
if (added === 0 && fixed === 0) {
text.push(s__('detected no vulnerabilities'));
}
return text.join(' ');
};
export const sastStatusIcon = ({ sast }) => statusIcon(sast.hasError, sast.newIssues.length);
export const sastContainerStatusIcon = ({ sastContainer }) =>
statusIcon(sastContainer.hasError, sastContainer.newIssues.length);
export const dastStatusIcon = ({ dast }) => statusIcon(dast.hasError, dast.newIssues.length);
export const dependencyScanningStatusIcon = ({ dependencyScanning }) =>
statusIcon(dependencyScanning.hasError, dependencyScanning.newIssues.length);
export const areReportsLoading = state =>
state.sast.isLoading ||
state.dast.isLoading ||
state.sastContainer.isLoading ||
state.dependencyScanning.isLoading;
export const allReportsHaveError = state =>
state.sast.hasError &&
state.dast.hasError &&
state.sastContainer.hasError &&
state.dependencyScanning.hasError;
export const anyReportHasError = state =>
state.sast.hasError ||
state.dast.hasError ||
state.sastContainer.hasError ||
state.dependencyScanning.hasError;
export const noBaseInAllReports = state =>
!state.sast.paths.base &&
!state.dast.paths.base &&
!state.sastContainer.paths.base &&
!state.dependencyScanning.paths.base;
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
const store = new Vuex.Store({
actions,
getters,
mutations,
state: state(),
});
export default store;
export const SET_HEAD_BLOB_PATH = 'SET_HEAD_BLOB_PATH';
export const SET_BASE_BLOB_PATH = 'SET_BASE_BLOB_PATH';
// SAST
export const SET_SAST_HEAD_PATH = 'SET_SAST_HEAD_PATH';
export const SET_SAST_BASE_PATH = 'SET_SAST_BASE_PATH';
export const REQUEST_SAST_REPORTS = 'REQUEST_SAST_REPORTS';
export const RECEIVE_SAST_REPORTS = 'RECEIVE_SAST_REPORTS';
export const RECEIVE_SAST_REPORTS_ERROR = 'RECEIVE_SAST_REPORTS_ERROR';
// SAST CONTAINER
export const SET_SAST_CONTAINER_HEAD_PATH = 'SET_SAST_CONTAINER_HEAD_PATH';
export const SET_SAST_CONTAINER_BASE_PATH = 'SET_SAST_CONTAINER_BASE_PATH';
export const REQUEST_SAST_CONTAINER_REPORTS = 'REQUEST_SAST_CONTAINER_REPORTS';
export const RECEIVE_SAST_CONTAINER_REPORTS = 'RECEIVE_SAST_CONTAINER_REPORTS';
export const RECEIVE_SAST_CONTAINER_ERROR = 'RECEIVE_SAST_CONTAINER_ERROR';
// DAST
export const SET_DAST_HEAD_PATH = 'SET_DAST_HEAD_PATH';
export const SET_DAST_BASE_PATH = 'SET_DAST_BASE_PATH';
export const REQUEST_DAST_REPORTS = 'REQUEST_DAST_REPORTS';
export const RECEIVE_DAST_REPORTS = 'RECEIVE_DAST_REPORTS';
export const RECEIVE_DAST_ERROR = 'RECEIVE_DAST_ERROR';
// DEPENDENCY_SCANNING
export const SET_DEPENDENCY_SCANNING_HEAD_PATH = 'SET_DEPENDENCY_SCANNING_HEAD_PATH';
export const SET_DEPENDENCY_SCANNING_BASE_PATH = 'SET_DEPENDENCY_SCANNING_BASE_PATH';
export const REQUEST_DEPENDENCY_SCANNING_REPORTS = 'REQUEST_DEPENDENCY_SCANNING_REPORTS';
export const RECEIVE_DEPENDENCY_SCANNING_REPORTS = 'RECEIVE_DEPENDENCY_SCANNING_REPORTS';
export const RECEIVE_DEPENDENCY_SCANNING_ERROR = 'RECEIVE_DEPENDENCY_SCANNING_ERROR';
import * as types from './mutation_types';
import {
parseSastIssues,
filterByKey,
parseSastContainer,
parseDastIssues,
getUnapprovedVulnerabilities,
} from './utils';
export default {
[types.SET_HEAD_BLOB_PATH](state, path) {
Object.assign(state.blobPath, { head: path });
},
[types.SET_BASE_BLOB_PATH](state, path) {
Object.assign(state.blobPath, { base: path });
},
// SAST
[types.SET_SAST_HEAD_PATH](state, path) {
Object.assign(state.sast.paths, { head: path });
},
[types.SET_SAST_BASE_PATH](state, path) {
Object.assign(state.sast.paths, { base: path });
},
[types.REQUEST_SAST_REPORTS](state) {
Object.assign(state.sast, { isLoading: true });
},
/**
* Compares sast results and returns the formatted report
*
* Sast has 3 types of issues: newIssues, resolvedIssues and allIssues.
*
* When we have both base and head:
* - newIssues = head - base
* - resolvedIssues = base - head
* - allIssues = head - newIssues - resolvedIssues
*
* When we only have head
* - newIssues = head
* - resolvedIssues = 0
* - allIssues = 0
*/
[types.RECEIVE_SAST_REPORTS](state, reports) {
if (reports.base && reports.head) {
const filterKey = 'cve';
const parsedHead = parseSastIssues(reports.head, state.blobPath.head);
const parsedBase = parseSastIssues(reports.base, state.blobPath.base);
const newIssues = filterByKey(parsedHead, parsedBase, filterKey);
const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey);
const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey);
Object.assign(state, {
sast: {
...state.sast,
newIssues,
resolvedIssues,
allIssues,
isLoading: false,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
} else if (reports.head && !reports.base) {
const newIssues = parseSastIssues(reports.head, state.blobPath.head);
Object.assign(state.sast, {
newIssues,
isLoading: false,
});
}
},
[types.RECEIVE_SAST_REPORTS_ERROR](state) {
Object.assign(state.sast, {
isLoading: false,
hasError: true,
});
},
// SAST CONTAINER
[types.SET_SAST_CONTAINER_HEAD_PATH](state, path) {
Object.assign(state.sastContainer.paths, { head: path });
},
[types.SET_SAST_CONTAINER_BASE_PATH](state, path) {
Object.assign(state.sastContainer.paths, { base: path });
},
[types.REQUEST_SAST_CONTAINER_REPORTS](state) {
Object.assign(state.sastContainer, { isLoading: true });
},
/**
* For sast container we only render unapproved vulnerabilities.
*/
[types.RECEIVE_SAST_CONTAINER_REPORTS](state, reports) {
if (reports.base && reports.head) {
const headIssues = getUnapprovedVulnerabilities(
parseSastContainer(reports.head.vulnerabilities),
reports.head.unapproved,
);
const baseIssues = getUnapprovedVulnerabilities(
parseSastContainer(reports.base.vulnerabilities),
reports.base.unapproved,
);
const filterKey = 'vulnerability';
const newIssues = filterByKey(headIssues, baseIssues, filterKey);
const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey);
Object.assign(state, {
sastContainer: {
...state.sastContainer,
isLoading: false,
newIssues,
resolvedIssues,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
} else if (reports.head && !reports.base) {
Object.assign(state.sastContainer, {
isLoading: false,
newIssues: getUnapprovedVulnerabilities(
parseSastContainer(reports.head.vulnerabilities),
reports.head.unapproved,
),
});
}
},
[types.RECEIVE_SAST_CONTAINER_ERROR](state) {
Object.assign(state.sastContainer, {
isLoading: false,
hasError: true,
});
},
// DAST
[types.SET_DAST_HEAD_PATH](state, path) {
Object.assign(state.dast.paths, { head: path });
},
[types.SET_DAST_BASE_PATH](state, path) {
Object.assign(state.dast.paths, { base: path });
},
[types.REQUEST_DAST_REPORTS](state) {
Object.assign(state.dast, { isLoading: true });
},
[types.RECEIVE_DAST_REPORTS](state, reports) {
if (reports.head && reports.base) {
const headIssues = parseDastIssues(reports.head.site.alerts);
const baseIssues = parseDastIssues(reports.base.site.alerts);
const filterKey = 'pluginid';
const newIssues = filterByKey(headIssues, baseIssues, filterKey);
const resolvedIssues = filterByKey(baseIssues, headIssues, filterKey);
Object.assign(state, {
dast: {
...state.dast,
isLoading: false,
newIssues,
resolvedIssues,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
} else if (reports.head && !reports.base) {
Object.assign(state.dast, {
isLoading: false,
newIssues: parseDastIssues(reports.head.site.alerts),
});
}
},
[types.RECEIVE_DAST_ERROR](state) {
Object.assign(state.dast, {
isLoading: false,
hasError: true,
});
},
// DEPENDECY SCANNING
[types.SET_DEPENDENCY_SCANNING_HEAD_PATH](state, path) {
Object.assign(state.dependencyScanning.paths, { head: path });
},
[types.SET_DEPENDENCY_SCANNING_BASE_PATH](state, path) {
Object.assign(state.dependencyScanning.paths, { base: path });
},
[types.REQUEST_DEPENDENCY_SCANNING_REPORTS](state) {
Object.assign(state.dependencyScanning, { isLoading: true });
},
/**
* Compares dependency scanning results and returns the formatted report
*
* Dependency report has 3 types of issues, newIssues, resolvedIssues and allIssues.
*
* When we have both base and head:
* - newIssues = head - base
* - resolvedIssues = base - head
* - allIssues = head - newIssues - resolvedIssues
*
* When we only have head
* - newIssues = head
* - resolvedIssues = 0
* - allIssues = 0
*/
[types.RECEIVE_DEPENDENCY_SCANNING_REPORTS](state, reports) {
if (reports.base && reports.head) {
const filterKey = 'cve';
const parsedHead = parseSastIssues(reports.head, state.blobPath.head);
const parsedBase = parseSastIssues(reports.base, state.blobPath.base);
const newIssues = filterByKey(parsedHead, parsedBase, filterKey);
const resolvedIssues = filterByKey(parsedBase, parsedHead, filterKey);
const allIssues = filterByKey(parsedHead, newIssues.concat(resolvedIssues), filterKey);
Object.assign(state, {
dependencyScanning: {
...state.dependencyScanning,
newIssues,
resolvedIssues,
allIssues,
isLoading: false,
},
summaryCounts: {
added: state.summaryCounts.added + newIssues.length,
fixed: state.summaryCounts.fixed + resolvedIssues.length,
},
});
} else {
Object.assign(state.dependencyScanning, {
newIssues: parseSastIssues(reports.head, state.blobPath.head),
isLoading: false,
});
}
},
[types.RECEIVE_DEPENDENCY_SCANNING_ERROR](state) {
Object.assign(state.dependencyScanning, {
isLoading: false,
hasError: true,
});
},
};
export default () => ({
summaryCounts: {
added: 0,
fixed: 0,
},
blobPath: {
head: null,
base: null,
},
sast: {
paths: {
head: null,
base: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
sastContainer: {
paths: {
head: null,
base: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
},
dast: {
paths: {
head: null,
base: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
},
dependencyScanning: {
paths: {
head: null,
base: null,
},
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
allIssues: [],
},
});
import { stripHtml } from '~/lib/utils/text_utility';
import { n__, s__, sprintf } from '~/locale';
/**
* Maps SAST & Dependency scanning issues:
* { tool: String, message: String, url: String , cve: String ,
* file: String , solution: String, priority: String }
* to contain:
* { name: String, path: String, line: String, urlPath: String, priority: String }
* @param {Array} issues
* @param {String} path
*/
export const parseSastIssues = (issues = [], path = '') =>
issues.map(issue => ({
...issue,
name: issue.message,
path: issue.file,
urlPath: issue.line ? `${path}/${issue.file}#L${issue.line}` : `${path}/${issue.file}`,
}),
);
/**
* Parses Sast Container results into a common format to allow to use the same Vue component
* And adds an external link
*
* @param {Array} data
* @returns {Array}
*/
export const parseSastContainer = (data = []) =>
data.map(element => ({
...element,
name: element.vulnerability,
priority: element.severity,
path: element.namespace,
// external link to provide better description
nameLink: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${element.vulnerability}`,
}));
export const parseDastIssues = (issues = []) =>
issues.map(issue => ({
parsedDescription: stripHtml(issue.desc, ' '),
priority: issue.riskdesc,
...issue,
}));
/**
* Compares two arrays by the given key and returns the difference
*
* @param {Array} firstArray
* @param {Array} secondArray
* @param {String} key
* @returns {Array}
*/
export const filterByKey = (firstArray = [], secondArray = [], key = '') =>
firstArray.filter(item => !secondArray.find(el => el[key] === item[key]));
export const getUnapprovedVulnerabilities = (issues = [], unapproved = []) =>
issues.filter(item => unapproved.find(el => el === item.vulnerability));
export const textBuilder = (
type = '',
paths = {},
newIssues = 0,
resolvedIssues = 0,
allIssues = 0,
) => {
// With no issues
if (newIssues === 0 && resolvedIssues === 0 && allIssues === 0) {
return sprintf(s__('ciReport|%{type} detected no security vulnerabilities'), { type });
}
// with no new or fixed but with vulnerabilities
if (newIssues === 0 && resolvedIssues === 0 && allIssues) {
return sprintf(s__('ciReport|%{type} detected no new security vulnerabilities'), { type });
}
// with new issues and only head
if (newIssues > 0 && !paths.base) {
return sprintf(
n__(
'%{type} was unable to compare existing and new vulnerabilities. It detected %d vulnerability',
'%{type} was unable to compare existing and new vulnerabilities. It detected %d vulnerabilities',
newIssues,
),
{ type },
);
}
// with head + base
if (paths.base && paths.head) {
// with only new issues
if (newIssues > 0 && resolvedIssues === 0) {
return sprintf(
n__(
'%{type} detected %d new vulnerability',
'%{type} detected %d new vulnerabilities',
newIssues,
),
{ type },
);
}
// with new and fixed issues
if (newIssues > 0 && resolvedIssues > 0) {
return `${sprintf(
n__(
'%{type} detected %d new vulnerability',
'%{type} detected %d new vulnerabilities',
newIssues,
),
{ type },
)}
${n__('and %d fixed vulnerability', 'and %d fixed vulnerabilities', resolvedIssues)}`;
}
// with only fixed issues
if (newIssues === 0 && resolvedIssues > 0) {
return sprintf(
n__(
'%{type} detected %d fixed vulnerability',
'%{type} detected %d fixed vulnerabilities',
resolvedIssues,
),
{ type },
);
}
}
return '';
};
export const statusIcon = (failed = false, newIssues = 0, neutralIssues = 0) => {
if (failed || newIssues > 0 || neutralIssues > 0) {
return 'warning';
}
return 'success';
};
......@@ -20,7 +20,8 @@ class PgReplicationSlot
# http://bdr-project.org/docs/stable/monitoring-peers.html
def self.slots_retained_bytes
ActiveRecord::Base.connection.execute(<<-SQL.squish)
SELECT slot_name, database, active, pg_xlog_location_diff(pg_current_xlog_insert_location(), restart_lsn)
SELECT slot_name, database,
active, #{Gitlab::Database.pg_wal_lsn_diff}(#{Gitlab::Database.pg_current_wal_insert_lsn}(), restart_lsn)
AS retained_bytes
FROM pg_replication_slots;
SQL
......@@ -30,7 +31,7 @@ class PgReplicationSlot
# returns the max number WAL space (in bytes) being used across the replication slots
def self.max_retained_wal
ActiveRecord::Base.connection.execute(<<-SQL.squish)
SELECT COALESCE(MAX(pg_xlog_location_diff(pg_current_xlog_insert_location(), restart_lsn)), 0)
SELECT COALESCE(MAX(#{Gitlab::Database.pg_wal_lsn_diff}(#{Gitlab::Database.pg_current_wal_insert_lsn}(), restart_lsn)), 0)
FROM pg_replication_slots;
SQL
.first.fetch('coalesce').to_i
......
......@@ -63,7 +63,7 @@ module Gitlab
def data_is_recent_enough?
# It's possible for a replica to not replay WAL data for a while,
# despite being up to date. This can happen when a primary does not
# receive any writes a for a while.
# receive any writes for a while.
#
# To prevent this from happening we check if the lag size (in bytes)
# of the replica is small enough for the replica to be useful. We
......@@ -92,7 +92,10 @@ module Gitlab
# This method will return nil if no lag size could be calculated.
def replication_lag_size
location = connection.quote(primary_write_location)
row = query_and_release("SELECT pg_xlog_location_diff(#{location}, pg_last_xlog_replay_location())::float AS diff")
row = query_and_release(<<-SQL.squish)
SELECT #{Gitlab::Database.pg_wal_lsn_diff}(#{location}, #{Gitlab::Database.pg_last_wal_replay_lsn}())::float
AS diff
SQL
row['diff'].to_i if row.any?
end
......@@ -110,11 +113,14 @@ module Gitlab
def caught_up?(location)
string = connection.quote(location)
# In case the host is a primary pg_last_xlog_replay_location() returns
# In case the host is a primary pg_last_wal_replay_lsn/pg_last_xlog_replay_location() returns
# NULL. The recovery check ensures we treat the host as up-to-date in
# such a case.
query = "SELECT NOT pg_is_in_recovery() OR " \
"pg_xlog_location_diff(pg_last_xlog_replay_location(), #{string}) >= 0 AS result"
query = <<-SQL.squish
SELECT NOT pg_is_in_recovery()
OR #{Gitlab::Database.pg_wal_lsn_diff}(#{Gitlab::Database.pg_last_wal_replay_lsn}(), #{string}) >= 0
AS result
SQL
row = query_and_release(query)
......
......@@ -89,7 +89,7 @@ module Gitlab
def primary_write_location
read_write do |connection|
row = connection
.select_all('SELECT pg_current_xlog_insert_location()::text AS location')
.select_all("SELECT #{Gitlab::Database.pg_current_wal_insert_lsn}()::text AS location")
.first
if row
......
......@@ -76,14 +76,15 @@ module Gitlab
def self.db_replication_lag_seconds
# Obtain the replication lag in seconds
lag =
ActiveRecord::Base.connection.execute('
SELECT CASE
WHEN pg_last_xlog_receive_location() = pg_last_xlog_replay_location()
THEN 0
ELSE
EXTRACT (EPOCH FROM now() - pg_last_xact_replay_timestamp())::INTEGER
END
AS replication_lag')
ActiveRecord::Base.connection.execute(<<-SQL.squish)
SELECT CASE
WHEN #{Gitlab::Database.pg_last_wal_receive_lsn}() = #{Gitlab::Database.pg_last_wal_receive_lsn}()
THEN 0
ELSE
EXTRACT (EPOCH FROM now() - pg_last_xact_replay_timestamp())::INTEGER
END
AS replication_lag
SQL
.first
.fetch('replication_lag')
......
require 'spec_helper'
describe 'Project settings > [EE] Merge Requests', :js do
include GitlabRoutingHelper
feature 'Project settings > Issues', :js do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:project) { create(:project, approvals_before_merge: 1) }
before do
gitlab_sign_in(user)
background do
project.add_master(user)
sign_in(user)
end
context 'when Issues are initially enabled' do
context 'when Pipelines are initially enabled' do
before do
visit edit_project_path(project)
end
scenario 'shows the Issues settings' do
expect(page).to have_content('Customize your issue restrictions')
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][issues_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
expect(page).not_to have_content('Customize your issue restrictions')
end
end
end
context 'when Issues are initially disabled' do
before do
project.project_feature.update_attribute('issues_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project)
end
scenario 'does not show the Issues settings' do
expect(page).not_to have_content('Customize your issue restrictions')
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][issues_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
expect(page).to have_content('Customize your issue restrictions')
end
end
context 'issuable default templates feature not available' do
......
......@@ -7,9 +7,11 @@ describe PgReplicationSlot, :postgresql do
expect(described_class.max_replication_slots).to be >= 0
end
skip = PgReplicationSlot.max_replication_slots <= PgReplicationSlot.count
context 'with enough slots available', skip: (skip ? 'max_replication_slots too small' : nil) do
skip_examples = PgReplicationSlot.max_replication_slots <= PgReplicationSlot.count
context 'with enough slots available' do
before(:all) do
skip('max_replication_slots too small') if skip_examples
@current_slot_count =
ActiveRecord::Base.connection.execute("SELECT COUNT(*) FROM pg_replication_slots;")
.first.fetch('count').to_i
......@@ -21,7 +23,9 @@ describe PgReplicationSlot, :postgresql do
end
after(:all) do
ActiveRecord::Base.connection.execute("SELECT pg_drop_replication_slot('test_slot');")
unless skip_examples
ActiveRecord::Base.connection.execute("SELECT pg_drop_replication_slot('test_slot');")
end
end
it '#slots_count' do
......
require 'spec_helper'
describe Boards::DestroyService, services: true do
describe Boards::DestroyService do
describe '#execute' do
let(:project) { create(:project) }
let(:group) { create(:group) }
let!(:board) { create(:board, group: group) }
subject(:service) { described_class.new(group, double) }
shared_examples 'remove the board' do |parent_name|
let(:parent) { public_send(parent_name) }
let!(:board) { create(:board, parent_name => parent) }
context 'when group have more than one board' do
it 'removes board from group' do
create(:board, group: group)
subject(:service) { described_class.new(parent, double) }
expect { service.execute(board) }.to change(group.boards, :count).by(-1)
context "when #{parent_name} have more than one board" do
it "removes board from #{parent_name}" do
create(:board, parent_name => parent)
expect { service.execute(board) }.to change(parent.boards, :count).by(-1)
end
end
end
context 'when group have one board' do
it 'does not remove board from group' do
expect { service.execute(board) }.not_to change(group.boards, :count)
context "when #{parent_name} have one board" do
it "does not remove board from #{parent_name}" do
expect { service.execute(board) }.not_to change(group.boards, :count)
end
end
end
it_behaves_like 'remove the board', :group
it_behaves_like 'remove the board', :project
end
end
......@@ -46,6 +46,10 @@ module Gitlab
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
def self.postgresql_9_or_less?
postgresql? && version.to_f < 10
end
def self.join_lateral_supported?
postgresql? && version.to_f >= 9.3
end
......@@ -58,6 +62,24 @@ module Gitlab
postgresql? && version.to_f >= 9.6
end
# map some of the function names that changed between PostgreSQL 9 and 10
# https://wiki.postgresql.org/wiki/New_in_postgres_10
def self.pg_wal_lsn_diff
Gitlab::Database.postgresql_9_or_less? ? 'pg_xlog_location_diff' : 'pg_wal_lsn_diff'
end
def self.pg_current_wal_insert_lsn
Gitlab::Database.postgresql_9_or_less? ? 'pg_current_xlog_insert_location' : 'pg_current_wal_insert_lsn'
end
def self.pg_last_wal_receive_lsn
Gitlab::Database.postgresql_9_or_less? ? 'pg_last_xlog_receive_location' : 'pg_last_wal_receive_lsn'
end
def self.pg_last_wal_replay_lsn
Gitlab::Database.postgresql_9_or_less? ? 'pg_last_xlog_replay_location' : 'pg_last_wal_replay_lsn'
end
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
......
require 'spec_helper'
feature 'Project settings > Issues', :js do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
background do
project.add_master(user)
sign_in(user)
end
context 'when Issues are initially enabled' do
context 'when Pipelines are initially enabled' do
before do
visit edit_project_path(project)
end
scenario 'shows the Issues settings' do
expect(page).to have_content('Customize your issue restrictions')
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][issues_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
expect(page).not_to have_content('Customize your issue restrictions')
end
end
end
context 'when Issues are initially disabled' do
before do
project.project_feature.update_attribute('issues_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project)
end
scenario 'does not show the Issues settings' do
expect(page).not_to have_content('Customize your issue restrictions')
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][issues_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
expect(page).to have_content('Customize your issue restrictions')
end
end
end
import state from 'ee/vue_shared/security_reports/store/state';
import mutations from 'ee/vue_shared/security_reports/store/mutations';
import * as types from 'ee/vue_shared/security_reports/store/mutation_types';
import {
sastIssues,
sastIssuesBase,
parsedSastIssuesHead,
parsedSastBaseStore,
dockerReport,
dockerBaseReport,
dockerNewIssues,
dockerOnlyHeadParsed,
dast,
dastBase,
parsedDastNewIssues,
parsedDast,
parsedSastIssuesStore,
} from '../mock_data';
describe('security reports mutations', () => {
let stateCopy;
beforeEach(() => {
stateCopy = state();
});
describe('SET_HEAD_BLOB_PATH', () => {
it('should set head blob path', () => {
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'head_blob_path');
expect(stateCopy.blobPath.head).toEqual('head_blob_path');
});
});
describe('SET_BASE_BLOB_PATH', () => {
it('should set base blob path', () => {
mutations[types.SET_BASE_BLOB_PATH](stateCopy, 'base_blob_path');
expect(stateCopy.blobPath.base).toEqual('base_blob_path');
});
});
describe('SET_SAST_HEAD_PATH', () => {
it('should set sast head path', () => {
mutations[types.SET_SAST_HEAD_PATH](stateCopy, 'sast_head_path');
expect(stateCopy.sast.paths.head).toEqual('sast_head_path');
});
});
describe('SET_SAST_BASE_PATH', () => {
it('sets sast base path', () => {
mutations[types.SET_SAST_BASE_PATH](stateCopy, 'sast_base_path');
expect(stateCopy.sast.paths.base).toEqual('sast_base_path');
});
});
describe('REQUEST_SAST_REPORTS', () => {
it('should set sast loading flag to true', () => {
mutations[types.REQUEST_SAST_REPORTS](stateCopy);
expect(stateCopy.sast.isLoading).toEqual(true);
});
});
describe('RECEIVE_SAST_REPORTS', () => {
describe('with head and base', () => {
it('should set new, fixed and all issues', () => {
mutations[types.SET_BASE_BLOB_PATH](stateCopy, 'path');
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path');
mutations[types.RECEIVE_SAST_REPORTS](stateCopy, {
head: sastIssues,
base: sastIssuesBase,
});
expect(stateCopy.sast.isLoading).toEqual(false);
expect(stateCopy.sast.newIssues).toEqual(parsedSastIssuesHead);
expect(stateCopy.sast.resolvedIssues).toEqual(parsedSastBaseStore);
});
});
describe('with head', () => {
it('should set new issues', () => {
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path');
mutations[types.RECEIVE_SAST_REPORTS](stateCopy, {
head: sastIssues,
});
expect(stateCopy.sast.isLoading).toEqual(false);
expect(stateCopy.sast.newIssues).toEqual(parsedSastIssuesStore);
});
});
});
describe('RECEIVE_SAST_REPORTS_ERROR', () => {
it('should set loading flag to false and error flag to true for sast', () => {
mutations[types.RECEIVE_SAST_REPORTS_ERROR](stateCopy);
expect(stateCopy.sast.isLoading).toEqual(false);
expect(stateCopy.sast.hasError).toEqual(true);
});
});
describe('SET_SAST_CONTAINER_HEAD_PATH', () => {
it('should set sast container head path', () => {
mutations[types.SET_SAST_CONTAINER_HEAD_PATH](stateCopy, 'head_path');
expect(stateCopy.sastContainer.paths.head).toEqual('head_path');
});
});
describe('SET_SAST_CONTAINER_BASE_PATH', () => {
it('should set sast container base path', () => {
mutations[types.SET_SAST_CONTAINER_BASE_PATH](stateCopy, 'base_path');
expect(stateCopy.sastContainer.paths.base).toEqual('base_path');
});
});
describe('REQUEST_SAST_CONTAINER_REPORTS', () => {
it('should set sast container loading flag to true', () => {
mutations[types.REQUEST_SAST_CONTAINER_REPORTS](stateCopy);
expect(stateCopy.sastContainer.isLoading).toEqual(true);
});
});
describe('RECEIVE_SAST_CONTAINER_REPORTS', () => {
describe('with head and base', () => {
it('should set new and resolved issues', () => {
mutations[types.RECEIVE_SAST_CONTAINER_REPORTS](stateCopy, {
head: dockerReport,
base: dockerBaseReport,
});
expect(stateCopy.sastContainer.isLoading).toEqual(false);
expect(stateCopy.sastContainer.newIssues).toEqual(dockerNewIssues);
expect(stateCopy.sastContainer.resolvedIssues).toEqual([]);
});
});
describe('with head', () => {
it('should set new issues', () => {
mutations[types.RECEIVE_SAST_CONTAINER_REPORTS](stateCopy, {
head: dockerReport,
});
expect(stateCopy.sastContainer.isLoading).toEqual(false);
expect(stateCopy.sastContainer.newIssues).toEqual(dockerOnlyHeadParsed);
});
});
});
describe('RECEIVE_SAST_CONTAINER_ERROR', () => {
it('should set sast container loading flag to false and error flag to true', () => {
mutations[types.RECEIVE_SAST_CONTAINER_ERROR](stateCopy);
expect(stateCopy.sastContainer.isLoading).toEqual(false);
expect(stateCopy.sastContainer.hasError).toEqual(true);
});
});
describe('SET_DAST_HEAD_PATH', () => {
it('should set dast head path', () => {
mutations[types.SET_DAST_HEAD_PATH](stateCopy, 'head_path');
expect(stateCopy.dast.paths.head).toEqual('head_path');
});
});
describe('SET_DAST_BASE_PATH', () => {
it('should set dast base path', () => {
mutations[types.SET_DAST_BASE_PATH](stateCopy, 'base_path');
expect(stateCopy.dast.paths.base).toEqual('base_path');
});
});
describe('REQUEST_DAST_REPORTS', () => {
it('should set dast loading flag to true', () => {
mutations[types.REQUEST_DAST_REPORTS](stateCopy);
expect(stateCopy.dast.isLoading).toEqual(true);
});
});
describe('RECEIVE_DAST_REPORTS', () => {
describe('with head and base', () => {
it('sets new and resolved issues with the given data', () => {
mutations[types.RECEIVE_DAST_REPORTS](stateCopy, {
head: dast,
base: dastBase,
});
expect(stateCopy.dast.isLoading).toEqual(false);
expect(stateCopy.dast.newIssues).toEqual(parsedDastNewIssues);
expect(stateCopy.dast.resolvedIssues).toEqual([]);
});
});
describe('with head', () => {
it('sets new issues with the given data', () => {
mutations[types.RECEIVE_DAST_REPORTS](stateCopy, {
head: dast,
});
expect(stateCopy.dast.isLoading).toEqual(false);
expect(stateCopy.dast.newIssues).toEqual(parsedDast);
});
});
});
describe('RECEIVE_DAST_ERROR', () => {
it('should set dast loading flag to false and error flag to true', () => {
mutations[types.RECEIVE_DAST_ERROR](stateCopy);
expect(stateCopy.dast.isLoading).toEqual(false);
expect(stateCopy.dast.hasError).toEqual(true);
});
});
describe('SET_DEPENDENCY_SCANNING_HEAD_PATH', () => {
it('should set dependency scanning head path', () => {
mutations[types.SET_DEPENDENCY_SCANNING_HEAD_PATH](stateCopy, 'head_path');
expect(stateCopy.dependencyScanning.paths.head).toEqual('head_path');
});
});
describe('SET_DEPENDENCY_SCANNING_BASE_PATH', () => {
it('should set dependency scanning base path', () => {
mutations[types.SET_DEPENDENCY_SCANNING_BASE_PATH](stateCopy, 'base_path');
expect(stateCopy.dependencyScanning.paths.base).toEqual('base_path');
});
});
describe('REQUEST_DEPENDENCY_SCANNING_REPORTS', () => {
it('should set dependency scanning loading flag to true', () => {
mutations[types.REQUEST_DEPENDENCY_SCANNING_REPORTS](stateCopy);
expect(stateCopy.dependencyScanning.isLoading).toEqual(true);
});
});
describe('RECEIVE_DEPENDENCY_SCANNING_REPORTS', () => {
describe('with head and base', () => {
it('should set new, fixed and all issues', () => {
mutations[types.SET_BASE_BLOB_PATH](stateCopy, 'path');
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path');
mutations[types.RECEIVE_DEPENDENCY_SCANNING_REPORTS](stateCopy, {
head: sastIssues,
base: sastIssuesBase,
});
expect(stateCopy.dependencyScanning.isLoading).toEqual(false);
expect(stateCopy.dependencyScanning.newIssues).toEqual(parsedSastIssuesHead);
expect(stateCopy.dependencyScanning.resolvedIssues).toEqual(parsedSastBaseStore);
});
});
describe('with head', () => {
it('should set new issues', () => {
mutations[types.SET_HEAD_BLOB_PATH](stateCopy, 'path');
mutations[types.RECEIVE_DEPENDENCY_SCANNING_REPORTS](stateCopy, {
head: sastIssues,
});
expect(stateCopy.dependencyScanning.isLoading).toEqual(false);
expect(stateCopy.dependencyScanning.newIssues).toEqual(parsedSastIssuesStore);
});
});
});
describe('RECEIVE_DEPENDENCY_SCANNING_ERROR', () => {
it('should set dependency scanning loading flag to false and error flag to true', () => {
mutations[types.RECEIVE_DEPENDENCY_SCANNING_ERROR](stateCopy);
expect(stateCopy.dependencyScanning.isLoading).toEqual(false);
expect(stateCopy.dependencyScanning.hasError).toEqual(true);
});
});
});
import {
parseSastIssues,
parseSastContainer,
parseDastIssues,
filterByKey,
getUnapprovedVulnerabilities,
textBuilder,
statusIcon,
} from 'ee/vue_shared/security_reports/store/utils';
import { sastIssues, dockerReport, dast, parsedDast } from '../mock_data';
describe('security reports utils', () => {
describe('parseSastIssues', () => {
it('should parse the received issues', () => {
const security = parseSastIssues(sastIssues, 'path')[0];
expect(security.name).toEqual(sastIssues[0].message);
expect(security.path).toEqual(sastIssues[0].file);
});
});
describe('parseSastContainer', () => {
it('parses sast container issues', () => {
const parsed = parseSastContainer(dockerReport.vulnerabilities)[0];
expect(parsed.name).toEqual(dockerReport.vulnerabilities[0].vulnerability);
expect(parsed.priority).toEqual(dockerReport.vulnerabilities[0].severity);
expect(parsed.path).toEqual(dockerReport.vulnerabilities[0].namespace);
expect(parsed.nameLink).toEqual(
`https://cve.mitre.org/cgi-bin/cvename.cgi?name=${
dockerReport.vulnerabilities[0].vulnerability
}`,
);
});
});
describe('parseDastIssues', () => {
it('parsed dast report', () => {
expect(parseDastIssues(dast.site.alerts)).toEqual(parsedDast);
});
});
describe('filterByKey', () => {
it('filters the array with the provided key', () => {
const array1 = [{ id: '1234' }, { id: 'abg543' }, { id: '214swfA' }];
const array2 = [{ id: '1234' }, { id: 'abg543' }, { id: '453OJKs' }];
expect(filterByKey(array1, array2, 'id')).toEqual([{ id: '214swfA' }]);
});
});
describe('getUnapprovedVulnerabilities', () => {
it('return unapproved vulnerabilities', () => {
const unapproved = getUnapprovedVulnerabilities(
dockerReport.vulnerabilities,
dockerReport.unapproved,
);
expect(unapproved.length).toEqual(dockerReport.unapproved.length);
expect(unapproved[0].vulnerability).toEqual(dockerReport.unapproved[0]);
expect(unapproved[1].vulnerability).toEqual(dockerReport.unapproved[1]);
});
});
describe('textBuilder', () => {
describe('with no issues', () => {
it('should return no vulnerabiltities text', () => {
expect(textBuilder()).toEqual(' detected no security vulnerabilities');
});
});
describe('with only `all` issues', () => {
it('should return no new vulnerabiltities text', () => {
expect(textBuilder('', {}, 0, 0, 1)).toEqual(' detected no new security vulnerabilities');
});
});
describe('with new issues and without base', () => {
it('should return unable to compare text', () => {
expect(textBuilder('', { head: 'foo' }, 1, 0, 0)).toEqual(
' was unable to compare existing and new vulnerabilities. It detected 1 vulnerability',
);
});
});
describe('with base and head', () => {
describe('with only new issues', () => {
it('should return new issues text', () => {
expect(textBuilder('', { head: 'foo', base: 'foo' }, 1, 0, 0)).toEqual(
' detected 1 new vulnerability',
);
});
});
describe('with new and resolved issues', () => {
it('should return new and fixed issues text', () => {
expect(
textBuilder('', { head: 'foo', base: 'foo' }, 1, 1, 0).replace(/\n+\s+/m, ' '),
).toEqual(' detected 1 new vulnerability and 1 fixed vulnerability');
});
});
describe('with only resolved issues', () => {
it('should return fixed issues text', () => {
expect(textBuilder('', { head: 'foo', base: 'foo' }, 0, 1, 0)).toEqual(
' detected 1 fixed vulnerability',
);
});
});
});
});
describe('statusIcon', () => {
describe('with failed report', () => {
it('returns warning', () => {
expect(statusIcon(true)).toEqual('warning');
});
});
describe('with new issues', () => {
it('returns warning', () => {
expect(statusIcon(false, 1)).toEqual('warning');
});
});
describe('with neutral issues', () => {
it('returns warning', () => {
expect(statusIcon(false, 0, 1)).toEqual('warning');
});
});
describe('without new or neutal issues', () => {
it('returns success', () => {
expect(statusIcon()).toEqual('success');
});
});
});
});
......@@ -51,6 +51,28 @@ describe Gitlab::Database do
end
end
describe '.postgresql_9_or_less?' do
it 'returns false when using MySQL' do
allow(described_class).to receive(:postgresql?).and_return(false)
expect(described_class.postgresql_9_or_less?).to eq(false)
end
it 'returns true when using PostgreSQL 9.6' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('9.6')
expect(described_class.postgresql_9_or_less?).to eq(true)
end
it 'returns false when using PostgreSQL 10 or newer' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('10')
expect(described_class.postgresql_9_or_less?).to eq(false)
end
end
describe '.join_lateral_supported?' do
it 'returns false when using MySQL' do
allow(described_class).to receive(:postgresql?).and_return(false)
......@@ -95,6 +117,70 @@ describe Gitlab::Database do
end
end
describe '.pg_wal_lsn_diff' do
it 'returns old name when using PostgreSQL 9.6' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('9.6')
expect(described_class.pg_wal_lsn_diff).to eq('pg_xlog_location_diff')
end
it 'returns new name when using PostgreSQL 10 or newer' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('10')
expect(described_class.pg_wal_lsn_diff).to eq('pg_wal_lsn_diff')
end
end
describe '.pg_current_wal_insert_lsn' do
it 'returns old name when using PostgreSQL 9.6' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('9.6')
expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_xlog_insert_location')
end
it 'returns new name when using PostgreSQL 10 or newer' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('10')
expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_wal_insert_lsn')
end
end
describe '.pg_last_wal_receive_lsn' do
it 'returns old name when using PostgreSQL 9.6' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('9.6')
expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_xlog_receive_location')
end
it 'returns new name when using PostgreSQL 10 or newer' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('10')
expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_wal_receive_lsn')
end
end
describe '.pg_last_wal_replay_lsn' do
it 'returns old name when using PostgreSQL 9.6' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('9.6')
expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_xlog_replay_location')
end
it 'returns new name when using PostgreSQL 10 or newer' do
allow(described_class).to receive(:postgresql?).and_return(true)
allow(described_class).to receive(:version).and_return('10')
expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_wal_replay_lsn')
end
end
describe '.nulls_last_order' do
context 'when using PostgreSQL' do
before do
......
require 'spec_helper'
describe Boards::DestroyService do
describe '#execute' do
let(:project) { create(:project) }
let!(:board) { create(:board, project: project) }
subject(:service) { described_class.new(project, double) }
context 'when project have more than one board' do
it 'removes board from project' do
create(:board, project: project)
expect { service.execute(board) }.to change(project.boards, :count).by(-1)
end
end
context 'when project have one board' do
it 'does not remove board from project' do
expect { service.execute(board) }.not_to change(project.boards, :count)
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