Commit ab93e29b authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Natalia Tepluhina

Simplify DAST profiles GraphQL queries

This simplifies DAST site and scanner profiles GraphQL queries by
removing unnecessary edge connections. We can fetch nodes directly.
parent 2fa14ee0
......@@ -52,11 +52,11 @@ const createProfilesApolloOptions = (name, field, { fetchQuery, fetchError }) =>
};
},
update(data) {
const edges = data?.project?.[name]?.edges ?? [];
if (edges.length === 1) {
this[field] = edges[0].node.id;
const nodes = data?.project?.[name]?.nodes ?? [];
if (nodes.length === 1) {
this[field] = nodes[0].id;
}
return edges.map(({ node }) => node);
return nodes;
},
error(e) {
Sentry.captureException(e);
......
......@@ -19,11 +19,11 @@ const createProfilesApolloOptions = (name, field, { fetchQuery, fetchError }) =>
};
},
update(data) {
const edges = data?.project?.[name]?.edges ?? [];
if (edges.length === 1) {
this[field] = edges[0].node.id;
const nodes = data?.project?.[name]?.nodes ?? [];
if (nodes.length === 1) {
this[field] = nodes[0].id;
}
return edges.map(({ node }) => node);
return nodes;
},
error(e) {
Sentry.captureException(e);
......
......@@ -100,11 +100,9 @@ export default {
result({ data, error }) {
if (!error) {
const { project } = data;
const profileEdges = project?.[profileType]?.edges ?? [];
const profiles = profileEdges.map(({ node }) => node);
const pageInfo = project?.[profileType].pageInfo;
this.profileTypes[profileType].profiles = profiles;
this.profileTypes[profileType].profiles = project?.[profileType]?.nodes;
this.profileTypes[profileType].pageInfo = pageInfo;
}
},
......
......@@ -11,10 +11,10 @@ import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graph
*/
export const appendToPreviousResult = (profileType) => (previousResult, { fetchMoreResult }) => {
const newResult = { ...fetchMoreResult };
const previousEdges = previousResult.project[profileType].edges;
const newEdges = newResult.project[profileType].edges;
const previousNodes = previousResult.project[profileType].nodes;
const newNodes = newResult.project[profileType].nodes;
newResult.project[profileType].edges = [...previousEdges, ...newEdges];
newResult.project[profileType].nodes = [...previousNodes, ...newNodes];
return newResult;
};
......@@ -31,11 +31,9 @@ export const removeProfile = ({ profileId, profileType, store, queryBody }) => {
const sourceData = store.readQuery(queryBody);
const data = produce(sourceData, (draftState) => {
draftState.project[profileType].edges = draftState.project[profileType].edges.filter(
({ node }) => {
return node.id !== profileId;
},
);
draftState.project[profileType].nodes = draftState.project[profileType].nodes.filter((node) => {
return node.id !== profileId;
});
});
store.writeQuery({ ...queryBody, data });
......@@ -67,8 +65,8 @@ export const updateSiteProfilesStatuses = ({ fullPath, normalizedTargetUrl, stat
const sourceData = store.readQuery(queryBody);
const profilesWithNormalizedTargetUrl = sourceData.project.siteProfiles.edges.flatMap(
({ node }) => (node.normalizedTargetUrl === normalizedTargetUrl ? node : []),
const profilesWithNormalizedTargetUrl = sourceData.project.siteProfiles.nodes.map((node) =>
node.normalizedTargetUrl === normalizedTargetUrl ? node : [],
);
profilesWithNormalizedTargetUrl.forEach(({ id }) => {
......
......@@ -18,19 +18,17 @@ query DastScannerProfiles(
pageInfo {
...PageInfo
}
edges {
cursor
node {
id
profileName
spiderTimeout
targetTimeout
scanType
useAjaxSpider
showDebugMessages
editPath
referencedInSecurityPolicies
}
nodes {
__typename
id
profileName
spiderTimeout
targetTimeout
scanType
useAjaxSpider
showDebugMessages
editPath
referencedInSecurityPolicies
}
}
}
......
......@@ -8,27 +8,25 @@ query DastSiteProfiles($fullPath: ID!, $after: String, $before: String, $first:
pageInfo {
...PageInfo
}
edges {
cursor
node {
id
profileName
normalizedTargetUrl
targetUrl
targetType
editPath
validationStatus
referencedInSecurityPolicies
auth {
enabled
url
usernameField
passwordField
username
}
excludedUrls
requestHeaders
nodes {
__typename
id
profileName
normalizedTargetUrl
targetUrl
targetType
editPath
validationStatus
referencedInSecurityPolicies
auth {
enabled
url
usernameField
passwordField
username
}
excludedUrls
requestHeaders
}
}
}
......
......@@ -22,7 +22,7 @@ RSpec.describe 'DAST profiles (GraphQL fixtures)' do
})
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :siteProfiles, :edges)).to have_attributes(size: dast_site_profiles.length)
expect(graphql_data_at(:project, :siteProfiles, :nodes)).to have_attributes(size: dast_site_profiles.length)
end
end
......@@ -38,7 +38,7 @@ RSpec.describe 'DAST profiles (GraphQL fixtures)' do
})
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :scannerProfiles, :edges)).to have_attributes(size: dast_scanner_profiles.length)
expect(graphql_data_at(:project, :scannerProfiles, :nodes)).to have_attributes(size: dast_scanner_profiles.length)
end
end
......
......@@ -3,6 +3,8 @@ import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import { merge } from 'lodash';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import siteProfilesFixtures from 'test_fixtures/graphql/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql.basic.json';
import scannerProfilesFixtures from 'test_fixtures/graphql/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql.basic.json';
import OnDemandScansForm from 'ee/on_demand_scans_form/components/on_demand_scans_form.vue';
import ScannerProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/scanner_profile_selector.vue';
import SiteProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/site_profile_selector.vue';
......@@ -15,7 +17,6 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createApolloProvider from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { redirectTo } from '~/lib/utils/url_utility';
import RefSelector from '~/ref/components/ref_selector.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
......@@ -26,7 +27,7 @@ import {
nonValidatedSiteProfile,
validatedSiteProfile,
} from 'ee_jest/security_configuration/dast_profiles/mocks/mock_data';
import * as responses from '../mocks/apollo_mocks';
import { itSelectsOnlyAvailableProfile } from './shared_assertions';
const dastSiteValidationDocsPath = '/application_security/dast/index#dast-site-validation';
const projectPath = 'group/project';
......@@ -125,10 +126,11 @@ describe('OnDemandScansForm', () => {
localVue.use(VueApollo);
requestHandlers = {
dastScannerProfiles: jest.fn().mockResolvedValue(responses.dastScannerProfiles()),
dastSiteProfiles: jest.fn().mockResolvedValue(responses.dastSiteProfiles()),
dastScannerProfiles: jest.fn().mockResolvedValue(scannerProfilesFixtures),
dastSiteProfiles: jest.fn().mockResolvedValue(siteProfilesFixtures),
...handlers,
};
return createApolloProvider([
[dastScannerProfilesQuery, requestHandlers.dastScannerProfiles],
[dastSiteProfilesQuery, requestHandlers.dastSiteProfiles],
......@@ -189,6 +191,7 @@ describe('OnDemandScansForm', () => {
},
),
);
return wrapper;
};
const createComponent = createComponentFactory(mount);
const createShallowComponent = createComponentFactory();
......@@ -205,6 +208,8 @@ describe('OnDemandScansForm', () => {
localStorage.clear();
});
itSelectsOnlyAvailableProfile(createShallowComponent);
describe('when creating a new scan', () => {
it('renders properly', () => {
createComponent();
......@@ -559,29 +564,6 @@ describe('OnDemandScansForm', () => {
},
);
describe.each`
profileType | query | selector | profiles
${'scanner'} | ${'dastScannerProfiles'} | ${ScannerProfileSelector} | ${scannerProfiles}
${'site'} | ${'dastSiteProfiles'} | ${SiteProfileSelector} | ${siteProfiles}
`('when there is a single $profileType profile', ({ query, selector, profiles }) => {
const [profile] = profiles;
beforeEach(async () => {
createShallowComponent(
{},
{
[query]: jest.fn().mockResolvedValue(responses[query]([profile])),
},
);
await waitForPromises();
});
it('automatically selects the only available profile', () => {
expect(wrapper.findComponent(selector).attributes('value')).toBe(profile.id);
});
});
describe('scanner profile summary', () => {
const [{ id }] = scannerProfiles;
......
......@@ -3,6 +3,8 @@ import { createLocalVue } from '@vue/test-utils';
import { merge } from 'lodash';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import siteProfilesFixtures from 'test_fixtures/graphql/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql.basic.json';
import scannerProfilesFixtures from 'test_fixtures/graphql/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql.basic.json';
import DastProfilesSelector from 'ee/on_demand_scans_form/components/profile_selector/dast_profiles_selector.vue';
import ScannerProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/scanner_profile_selector.vue';
import SiteProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/site_profile_selector.vue';
......@@ -11,7 +13,6 @@ import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graph
import createApolloProvider from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
siteProfiles,
......@@ -19,7 +20,7 @@ import {
nonValidatedSiteProfile,
validatedSiteProfile,
} from 'ee_jest/security_configuration/dast_profiles/mocks/mock_data';
import * as responses from '../../mocks/apollo_mocks';
import { itSelectsOnlyAvailableProfile } from '../shared_assertions';
const URL_HOST = 'https://localhost/';
......@@ -43,8 +44,8 @@ describe('EE - DAST Profiles Selector', () => {
const createMockApolloProvider = (handlers) => {
localVue.use(VueApollo);
requestHandlers = {
dastScannerProfiles: jest.fn().mockResolvedValue(responses.dastScannerProfiles()),
dastSiteProfiles: jest.fn().mockResolvedValue(responses.dastSiteProfiles()),
dastScannerProfiles: jest.fn().mockResolvedValue(scannerProfilesFixtures),
dastSiteProfiles: jest.fn().mockResolvedValue(siteProfilesFixtures),
...handlers,
};
return createApolloProvider([
......@@ -98,6 +99,7 @@ describe('EE - DAST Profiles Selector', () => {
},
),
);
return wrapper;
};
const createComponent = createComponentFactory();
......@@ -106,6 +108,8 @@ describe('EE - DAST Profiles Selector', () => {
wrapper.destroy();
});
itSelectsOnlyAvailableProfile(createComponent);
describe('loading state', () => {
it.each`
scannerProfilesLoading | siteProfilesLoading | isLoading
......@@ -161,29 +165,6 @@ describe('EE - DAST Profiles Selector', () => {
},
);
describe.each`
profileType | query | selector | profiles
${'scanner'} | ${'dastScannerProfiles'} | ${ScannerProfileSelector} | ${scannerProfiles}
${'site'} | ${'dastSiteProfiles'} | ${SiteProfileSelector} | ${siteProfiles}
`('when there is a single $profileType profile', ({ query, selector, profiles }) => {
const [profile] = profiles;
beforeEach(async () => {
createComponent(
{},
{
[query]: jest.fn().mockResolvedValue(responses[query]([profile])),
},
);
await waitForPromises();
});
it('automatically selects the only available profile', () => {
expect(wrapper.findComponent(selector).attributes('value')).toBe(profile.id);
});
});
describe('populate profiles from query params', () => {
const [siteProfile] = siteProfiles;
const [scannerProfile] = scannerProfiles;
......
/* eslint-disable jest/no-export */
import { cloneDeep, set } from 'lodash';
import siteProfilesFixtures from 'test_fixtures/graphql/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql.basic.json';
import scannerProfilesFixtures from 'test_fixtures/graphql/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql.basic.json';
import waitForPromises from 'helpers/wait_for_promises';
import ScannerProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/scanner_profile_selector.vue';
import SiteProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/site_profile_selector.vue';
const [firstSiteProfile] = siteProfilesFixtures.data.project.siteProfiles.nodes;
const [firstScannerProfile] = scannerProfilesFixtures.data.project.scannerProfiles.nodes;
const siteProfilesReponseWithSingleProfile = set(
cloneDeep(siteProfilesFixtures),
'data.project.siteProfiles.nodes',
[firstSiteProfile],
);
const scannerProfilesReponseWithSingleProfile = set(
cloneDeep(scannerProfilesFixtures),
'data.project.scannerProfiles.nodes',
[firstScannerProfile],
);
export const itSelectsOnlyAvailableProfile = (componentFactory) => {
let wrapper;
describe.each`
profileType | query | selector | response | expectedId
${'site'} | ${'dastSiteProfiles'} | ${SiteProfileSelector} | ${siteProfilesReponseWithSingleProfile} | ${firstSiteProfile.id}
${'scanner'} | ${'dastScannerProfiles'} | ${ScannerProfileSelector} | ${scannerProfilesReponseWithSingleProfile} | ${firstScannerProfile.id}
`('when there is a single $profileType profile', ({ query, selector, response, expectedId }) => {
beforeEach(async () => {
wrapper = componentFactory(
{},
{
[query]: jest.fn().mockResolvedValue(response),
},
);
await waitForPromises();
});
it('automatically selects the only available profile', () => {
expect(wrapper.findComponent(selector).attributes('value')).toBe(expectedId);
});
});
};
import {
siteProfiles,
scannerProfiles,
} from 'ee_jest/security_configuration/dast_profiles/mocks/mock_data';
const defaults = {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
};
export const dastScannerProfiles = (profiles = scannerProfiles) => ({
data: {
project: {
id: '1',
scannerProfiles: {
...defaults,
edges: profiles.map((profile) => ({
cursor: '',
node: profile,
})),
},
},
},
});
export const dastSiteProfiles = (profiles = siteProfiles) => ({
data: {
project: {
id: '1',
siteProfiles: {
...defaults,
edges: profiles.map((profile) => ({
cursor: '',
node: profile,
})),
},
},
},
});
......@@ -12,10 +12,10 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => {
it.each(['siteProfiles', 'scannerProfiles'])(
'appends new results to previous',
(profileType) => {
const previousResult = { project: { [profileType]: { edges: ['foo'] } } };
const fetchMoreResult = { project: { [profileType]: { edges: ['bar'] } } };
const previousResult = { project: { [profileType]: { nodes: ['foo'] } } };
const fetchMoreResult = { project: { [profileType]: { nodes: ['bar'] } } };
const expected = { project: { [profileType]: { edges: ['foo', 'bar'] } } };
const expected = { project: { [profileType]: { nodes: ['foo', 'bar'] } } };
const result = appendToPreviousResult(profileType)(previousResult, { fetchMoreResult });
expect(result).toEqual(expected);
......@@ -32,7 +32,7 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => {
const mockData = {
project: {
[profileType]: {
edges: [{ node: mockProfiles[0] }, { node: mockProfiles[1] }],
nodes: [mockProfiles[0], mockProfiles[1]],
},
},
};
......@@ -53,7 +53,7 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => {
data: {
project: {
[profileType]: {
edges: [{ node: mockProfiles[1] }],
nodes: [mockProfiles[1]],
},
},
},
......@@ -91,7 +91,7 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => {
const mockData = {
project: {
siteProfiles: {
edges: [{ node: siteProfile }],
nodes: [siteProfile],
},
},
};
......
......@@ -4,9 +4,7 @@ import policySiteProfilesFixtures from 'test_fixtures/graphql/security_configura
import policyScannerProfilesFixtures from 'test_fixtures/graphql/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql.from_policies.json';
import dastFailedSiteValidationsFixtures from 'test_fixtures/graphql/security_configuration/dast_profiles/graphql/dast_failed_site_validations.query.graphql.json';
export const siteProfiles = siteProfilesFixture.data.project.siteProfiles.edges.map(
({ node }) => node,
);
export const siteProfiles = siteProfilesFixture.data.project.siteProfiles.nodes;
export const nonValidatedSiteProfile = siteProfiles.find(
({ validationStatus }) => validationStatus === 'NONE',
......@@ -15,17 +13,12 @@ export const validatedSiteProfile = siteProfiles.find(
({ validationStatus }) => validationStatus === 'PASSED_VALIDATION',
);
export const policySiteProfiles = policySiteProfilesFixtures.data.project.siteProfiles.edges.map(
({ node }) => node,
);
export const policySiteProfiles = policySiteProfilesFixtures.data.project.siteProfiles.nodes;
export const policyScannerProfiles = policyScannerProfilesFixtures.data.project.scannerProfiles.edges.map(
({ node }) => node,
);
export const policyScannerProfiles =
policyScannerProfilesFixtures.data.project.scannerProfiles.nodes;
export const scannerProfiles = scannerProfilesFixtures.data.project.scannerProfiles.edges.map(
({ node }) => node,
);
export const scannerProfiles = scannerProfilesFixtures.data.project.scannerProfiles.nodes;
export const failedSiteValidations =
dastFailedSiteValidationsFixtures.data.project.validations.nodes;
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