Commit cc667781 authored by Nathan Friend's avatar Nathan Friend

Update Releases page to fetch data from GraphQL

This commit update the Releases page to fetch its data from GraphQL
instead of from the REST endpoint. This change is hidden behind the
graphql_releases_page feature flag.
parent d48d68e3
...@@ -30,6 +30,10 @@ export default { ...@@ -30,6 +30,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
projectPath: {
type: String,
required: true,
},
documentationPath: { documentationPath: {
type: String, type: String,
required: true, required: true,
...@@ -62,6 +66,7 @@ export default { ...@@ -62,6 +66,7 @@ export default {
this.fetchReleases({ this.fetchReleases({
page: getParameterByName('page'), page: getParameterByName('page'),
projectId: this.projectId, projectId: this.projectId,
projectPath: this.projectPath,
}); });
}, },
methods: { methods: {
......
...@@ -12,6 +12,11 @@ export default () => { ...@@ -12,6 +12,11 @@ export default () => {
modules: { modules: {
list: listModule, list: listModule,
}, },
featureFlags: {
graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
graphqlReleasesPage: Boolean(gon.features?.graphqlReleasesPage),
graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats),
},
}), }),
render: h => render: h =>
h(ReleaseListApp, { h(ReleaseListApp, {
......
query allReleases($fullPath: ID!) {
project(fullPath: $fullPath) {
releases(first: 20) {
count
nodes {
name
tagName
tagPath
descriptionHtml
releasedAt
upcomingRelease
assets {
count
sources {
nodes {
format
url
}
}
links {
nodes {
id
name
url
directAssetUrl
linkType
external
}
}
}
evidences {
nodes {
filepath
collectedAt
sha
}
}
links {
editUrl
issuesUrl
mergeRequestsUrl
selfUrl
}
commit {
sha
webUrl
title
}
author {
webUrl
avatarUrl
username
}
milestones {
nodes {
id
title
description
webPath
stats {
totalIssuesCount
closedIssuesCount
}
}
}
}
}
}
}
...@@ -7,6 +7,8 @@ import { ...@@ -7,6 +7,8 @@ import {
parseIntPagination, parseIntPagination,
convertObjectPropsToCamelCase, convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { gqClient, convertGraphQLResponse } from '../../../util';
/** /**
* Commits a mutation to update the state while the main endpoint is being requested. * Commits a mutation to update the state while the main endpoint is being requested.
...@@ -21,13 +23,31 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES); ...@@ -21,13 +23,31 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
* *
* @param {String} projectId * @param {String} projectId
*/ */
export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => { export const fetchReleases = ({ dispatch, rootState }, { page = '1', projectId, projectPath }) => {
dispatch('requestReleases'); dispatch('requestReleases');
api if (
.releases(projectId, { page }) rootState.featureFlags.graphqlReleaseData &&
.then(response => dispatch('receiveReleasesSuccess', response)) rootState.featureFlags.graphqlReleasesPage &&
.catch(() => dispatch('receiveReleasesError')); rootState.featureFlags.graphqlMilestoneStats
) {
gqClient
.query({
query: allReleasesQuery,
variables: {
fullPath: projectPath,
},
})
.then(response => {
dispatch('receiveReleasesSuccess', convertGraphQLResponse(response));
})
.catch(() => dispatch('receiveReleasesError'));
} else {
api
.releases(projectId, { page })
.then(response => dispatch('receiveReleasesSuccess', response))
.catch(() => dispatch('receiveReleasesError'));
}
}; };
export const receiveReleasesSuccess = ({ commit }, { data, headers }) => { export const receiveReleasesSuccess = ({ commit }, { data, headers }) => {
......
import { pick } from 'lodash';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { truncateSha } from '~/lib/utils/text_utility';
import { import {
convertObjectPropsToCamelCase, convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase, convertObjectPropsToSnakeCase,
...@@ -39,3 +42,89 @@ export const apiJsonToRelease = json => { ...@@ -39,3 +42,89 @@ export const apiJsonToRelease = json => {
return release; return release;
}; };
export const gqClient = createGqClient({}, { fetchPolicy: fetchPolicies.NO_CACHE });
const convertScalarProperties = graphQLRelease =>
pick(graphQLRelease, [
'name',
'tagName',
'tagPath',
'descriptionHtml',
'releasedAt',
'upcomingRelease',
]);
const convertAssets = graphQLRelease => ({
assets: {
count: graphQLRelease.assets.count,
sources: [...graphQLRelease.assets.sources.nodes],
links: graphQLRelease.assets.links.nodes.map(l => ({
...l,
linkType: l.linkType?.toLowerCase(),
})),
},
});
const convertEvidences = graphQLRelease => ({
evidences: graphQLRelease.evidences.nodes.map(e => e),
});
const convertLinks = graphQLRelease => ({
_links: {
...graphQLRelease.links,
self: graphQLRelease.links?.selfUrl,
},
});
const convertCommit = graphQLRelease => {
if (!graphQLRelease.commit) {
return {};
}
return {
commit: {
shortId: truncateSha(graphQLRelease.commit.sha),
title: graphQLRelease.commit.title,
},
commitPath: graphQLRelease.commit.webUrl,
};
};
const convertAuthor = graphQLRelease => ({ author: graphQLRelease.author });
const convertMilestones = graphQLRelease => ({
milestones: graphQLRelease.milestones.nodes.map(m => ({
...m,
webUrl: m.webPath,
webPath: undefined,
issueStats: {
total: m.stats.totalIssuesCount,
closed: m.stats.closedIssuesCount,
},
stats: undefined,
})),
});
/**
* Converts the response from the GraphQL endpoint into the
* same shape as is returned from the Releases REST API.
*
* This allows the release components to use the response
* from either endpoint interchangeably.
*
* @param response The response received from the GraphQL endpoint
*/
export const convertGraphQLResponse = response => {
const releases = response.data.project.releases.nodes.map(r => ({
...convertScalarProperties(r),
...convertAssets(r),
...convertEvidences(r),
...convertLinks(r),
...convertCommit(r),
...convertAuthor(r),
...convertMilestones(r),
}));
return { data: releases };
};
...@@ -11,6 +11,9 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -11,6 +11,9 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_show_page, project, default_enabled: true) push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
push_frontend_feature_flag(:release_asset_link_editing, project, default_enabled: true) push_frontend_feature_flag(:release_asset_link_editing, project, default_enabled: true)
push_frontend_feature_flag(:release_asset_link_type, project, default_enabled: true) push_frontend_feature_flag(:release_asset_link_type, project, default_enabled: true)
push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true)
push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true)
push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: false)
end end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new before_action :authorize_create_release!, only: :new
......
...@@ -15,6 +15,7 @@ module ReleasesHelper ...@@ -15,6 +15,7 @@ module ReleasesHelper
def data_for_releases_page def data_for_releases_page
{ {
project_id: @project.id, project_id: @project.id,
project_path: @project.full_path,
illustration_path: illustration, illustration_path: illustration,
documentation_path: help_page documentation_path: help_page
}.tap do |data| }.tap do |data|
......
...@@ -13,37 +13,25 @@ RSpec.describe 'User views releases', :js do ...@@ -13,37 +13,25 @@ RSpec.describe 'User views releases', :js do
project.add_guest(guest) project.add_guest(guest)
end end
context('when the user is a maintainer') do shared_examples 'releases page' do
before do context('when the user is a maintainer') do
gitlab_sign_in(maintainer) before do
end gitlab_sign_in(maintainer)
end
it 'sees the release' do
visit project_releases_path(project)
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
expect(page).not_to have_content('Upcoming Release')
end
shared_examples 'asset link tests' do
context 'when there is a link as an asset' do
let!(:release_link) { create(:release_link, release: release, url: url ) }
let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" }
let(:direct_asset_link) { Gitlab::Routing.url_helpers.project_release_url(project, release) << release_link.filepath }
it 'sees the link' do it 'sees the release' do
visit project_releases_path(project) visit project_releases_path(project)
page.within('.js-assets-list') do expect(page).to have_content(release.name)
expect(page).to have_link release_link.name, href: direct_asset_link expect(page).to have_content(release.tag)
expect(page).not_to have_css('[data-testid="external-link-indicator"]') expect(page).not_to have_content('Upcoming Release')
end end
end
context 'when there is a link redirect' do shared_examples 'asset link tests' do
let!(:release_link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: '/binaries/linux-amd64', url: url) } context 'when there is a link as an asset' do
let!(:release_link) { create(:release_link, release: release, url: url ) }
let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" } let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" }
let(:direct_asset_link) { Gitlab::Routing.url_helpers.project_release_url(project, release) << release_link.filepath }
it 'sees the link' do it 'sees the link' do
visit project_releases_path(project) visit project_releases_path(project)
...@@ -53,77 +41,103 @@ RSpec.describe 'User views releases', :js do ...@@ -53,77 +41,103 @@ RSpec.describe 'User views releases', :js do
expect(page).not_to have_css('[data-testid="external-link-indicator"]') expect(page).not_to have_css('[data-testid="external-link-indicator"]')
end end
end end
end
context 'when url points to external resource' do context 'when there is a link redirect' do
let(:url) { 'http://google.com/download' } let!(:release_link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: '/binaries/linux-amd64', url: url) }
let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" }
it 'sees that the link is external resource' do it 'sees the link' do
visit project_releases_path(project) visit project_releases_path(project)
page.within('.js-assets-list') do page.within('.js-assets-list') do
expect(page).to have_css('[data-testid="external-link-indicator"]') expect(page).to have_link release_link.name, href: direct_asset_link
expect(page).not_to have_css('[data-testid="external-link-indicator"]')
end
end
end
context 'when url points to external resource' do
let(:url) { 'http://google.com/download' }
it 'sees that the link is external resource' do
visit project_releases_path(project)
page.within('.js-assets-list') do
expect(page).to have_css('[data-testid="external-link-indicator"]')
end
end end
end end
end end
end end
end
context 'when the release_asset_link_type feature flag is enabled' do context 'when the release_asset_link_type feature flag is enabled' do
before do before do
stub_feature_flags(release_asset_link_type: true) stub_feature_flags(release_asset_link_type: true)
end
it_behaves_like 'asset link tests'
end end
it_behaves_like 'asset link tests' context 'when the release_asset_link_type feature flag is disabled' do
end before do
stub_feature_flags(release_asset_link_type: false)
end
context 'when the release_asset_link_type feature flag is disabled' do it_behaves_like 'asset link tests'
before do
stub_feature_flags(release_asset_link_type: false)
end end
it_behaves_like 'asset link tests' context 'with an upcoming release' do
end let(:tomorrow) { Time.zone.now + 1.day }
let!(:release) { create(:release, project: project, released_at: tomorrow ) }
context 'with an upcoming release' do it 'sees the upcoming tag' do
let(:tomorrow) { Time.zone.now + 1.day } visit project_releases_path(project)
let!(:release) { create(:release, project: project, released_at: tomorrow ) }
it 'sees the upcoming tag' do expect(page).to have_content('Upcoming Release')
visit project_releases_path(project) end
end
context 'with a tag containing a slash' do
it 'sees the release' do
release = create :release, project: project, tag: 'debian/2.4.0-1'
visit project_releases_path(project)
expect(page).to have_content('Upcoming Release') expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
end
end end
end end
context 'with a tag containing a slash' do context('when the user is a guest') do
it 'sees the release' do before do
release = create :release, project: project, tag: 'debian/2.4.0-1' gitlab_sign_in(guest)
end
it 'renders release info except for Git-related data' do
visit project_releases_path(project) visit project_releases_path(project)
expect(page).to have_content(release.name) within('.release-block') do
expect(page).to have_content(release.tag) expect(page).to have_content(release.description)
# The following properties (sometimes) include Git info,
# so they are not rendered for Guest users
expect(page).not_to have_content(release.name)
expect(page).not_to have_content(release.tag)
expect(page).not_to have_content(release.commit.short_id)
end
end end
end end
end end
context('when the user is a guest') do context 'when the graphql_releases_page feature flag is enabled' do
it_behaves_like 'releases page'
end
context 'when the graphql_releases_page feature flag is disabled' do
before do before do
gitlab_sign_in(guest) stub_feature_flags(graphql_releases_page: false)
end end
it 'renders release info except for Git-related data' do it_behaves_like 'releases page'
visit project_releases_path(project)
within('.release-block') do
expect(page).to have_content(release.description)
# The following properties (sometimes) include Git info,
# so they are not rendered for Guest users
expect(page).not_to have_content(release.name)
expect(page).not_to have_content(release.tag)
expect(page).not_to have_content(release.commit.short_id)
end
end
end end
end end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`releases/util.js convertGraphQLResponse matches snapshot 1`] = `
Object {
"data": Array [
Object {
"_links": Object {
"editUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10/edit",
"issuesUrl": null,
"mergeRequestsUrl": null,
"self": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10",
"selfUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10",
},
"assets": Object {
"count": 7,
"links": Array [
Object {
"directAssetUrl": "http://0.0.0.0:3000/root/release-test/-/releases/v5.32/permanent/path/to/runbook",
"external": true,
"id": "gid://gitlab/Releases::Link/69",
"linkType": "other",
"name": "An example link",
"url": "https://example.com/link",
},
Object {
"directAssetUrl": "https://example.com/package",
"external": true,
"id": "gid://gitlab/Releases::Link/68",
"linkType": "package",
"name": "An example package link",
"url": "https://example.com/package",
},
Object {
"directAssetUrl": "https://example.com/image",
"external": true,
"id": "gid://gitlab/Releases::Link/67",
"linkType": "image",
"name": "An example image",
"url": "https://example.com/image",
},
],
"sources": Array [
Object {
"format": "zip",
"url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.zip",
},
Object {
"format": "tar.gz",
"url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.gz",
},
Object {
"format": "tar.bz2",
"url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.bz2",
},
Object {
"format": "tar",
"url": "http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar",
},
],
},
"author": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
"username": "root",
"webUrl": "http://0.0.0.0:3000/root",
},
"commit": Object {
"shortId": "92e7ea2e",
"title": "Testing a change.",
},
"commitPath": "http://0.0.0.0:3000/root/release-test/-/commit/92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7",
"descriptionHtml": "<p data-sourcepos=\\"1:1-1:24\\" dir=\\"auto\\">This is version <strong>1.0</strong>!</p>",
"evidences": Array [
Object {
"collectedAt": "2020-08-21T20:15:19Z",
"filepath": "http://0.0.0.0:3000/root/release-test/-/releases/v5.10/evidences/34.json",
"sha": "22bde8e8b93d870a29ddc339287a1fbb598f45d1396d",
},
],
"milestones": Array [
Object {
"description": "",
"id": "gid://gitlab/Milestone/60",
"issueStats": Object {
"closed": 0,
"total": 0,
},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": "/root/release-test/-/milestones/2",
},
Object {
"description": "Milestone 12.3",
"id": "gid://gitlab/Milestone/59",
"issueStats": Object {
"closed": 1,
"total": 2,
},
"stats": undefined,
"title": "12.3",
"webPath": undefined,
"webUrl": "/root/release-test/-/milestones/1",
},
],
"name": "Release 1.0",
"releasedAt": "2020-08-21T20:15:18Z",
"tagName": "v5.10",
"tagPath": "/root/release-test/-/tags/v5.10",
"upcomingRelease": false,
},
],
}
`;
...@@ -20,6 +20,7 @@ localVue.use(Vuex); ...@@ -20,6 +20,7 @@ localVue.use(Vuex);
describe('Releases App ', () => { describe('Releases App ', () => {
let wrapper; let wrapper;
let fetchReleaseSpy;
const releasesPagination = rge(21).map(index => ({ const releasesPagination = rge(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }), ...convertObjectPropsToCamelCase(release, { deep: true }),
...@@ -28,12 +29,22 @@ describe('Releases App ', () => { ...@@ -28,12 +29,22 @@ describe('Releases App ', () => {
const defaultProps = { const defaultProps = {
projectId: 'gitlab-ce', projectId: 'gitlab-ce',
projectPath: 'gitlab-org/gitlab-ce',
documentationPath: 'help/releases', documentationPath: 'help/releases',
illustrationPath: 'illustration/path', illustrationPath: 'illustration/path',
}; };
const createComponent = (propsData = defaultProps) => { const createComponent = (propsData = defaultProps) => {
const store = createStore({ modules: { list: listModule } }); fetchReleaseSpy = jest.spyOn(listModule.actions, 'fetchReleases');
const store = createStore({
modules: { list: listModule },
featureFlags: {
graphqlReleaseData: true,
graphqlReleasesPage: false,
graphqlMilestoneStats: true,
},
});
wrapper = shallowMount(ReleasesApp, { wrapper = shallowMount(ReleasesApp, {
store, store,
...@@ -46,6 +57,25 @@ describe('Releases App ', () => { ...@@ -46,6 +57,25 @@ describe('Releases App ', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('on startup', () => {
beforeEach(() => {
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
createComponent();
});
it('calls fetchRelease with the page, project ID, and project path', () => {
expect(fetchReleaseSpy).toHaveBeenCalledTimes(1);
expect(fetchReleaseSpy).toHaveBeenCalledWith(expect.anything(), {
page: null,
projectId: defaultProps.projectId,
projectPath: defaultProps.projectPath,
});
});
});
describe('while loading', () => { describe('while loading', () => {
beforeEach(() => { beforeEach(() => {
jest jest
......
...@@ -222,3 +222,131 @@ export const release2 = { ...@@ -222,3 +222,131 @@ export const release2 = {
}; };
export const releases = [release, release2]; export const releases = [release, release2];
export const graphqlReleasesResponse = {
data: {
project: {
releases: {
count: 39,
nodes: [
{
name: 'Release 1.0',
tagName: 'v5.10',
tagPath: '/root/release-test/-/tags/v5.10',
descriptionHtml:
'<p data-sourcepos="1:1-1:24" dir="auto">This is version <strong>1.0</strong>!</p>',
releasedAt: '2020-08-21T20:15:18Z',
upcomingRelease: false,
assets: {
count: 7,
sources: {
nodes: [
{
format: 'zip',
url:
'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.zip',
},
{
format: 'tar.gz',
url:
'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.gz',
},
{
format: 'tar.bz2',
url:
'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar.bz2',
},
{
format: 'tar',
url:
'http://0.0.0.0:3000/root/release-test/-/archive/v5.10/release-test-v5.10.tar',
},
],
},
links: {
nodes: [
{
id: 'gid://gitlab/Releases::Link/69',
name: 'An example link',
url: 'https://example.com/link',
directAssetUrl:
'http://0.0.0.0:3000/root/release-test/-/releases/v5.32/permanent/path/to/runbook',
linkType: 'OTHER',
external: true,
},
{
id: 'gid://gitlab/Releases::Link/68',
name: 'An example package link',
url: 'https://example.com/package',
directAssetUrl: 'https://example.com/package',
linkType: 'PACKAGE',
external: true,
},
{
id: 'gid://gitlab/Releases::Link/67',
name: 'An example image',
url: 'https://example.com/image',
directAssetUrl: 'https://example.com/image',
linkType: 'IMAGE',
external: true,
},
],
},
},
evidences: {
nodes: [
{
filepath:
'http://0.0.0.0:3000/root/release-test/-/releases/v5.10/evidences/34.json',
collectedAt: '2020-08-21T20:15:19Z',
sha: '22bde8e8b93d870a29ddc339287a1fbb598f45d1396d',
},
],
},
links: {
editUrl: 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10/edit',
issuesUrl: null,
mergeRequestsUrl: null,
selfUrl: 'http://0.0.0.0:3000/root/release-test/-/releases/v5.10',
},
commit: {
sha: '92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7',
webUrl:
'http://0.0.0.0:3000/root/release-test/-/commit/92e7ea2ee4496fe0d00ff69830ba0564d3d1e5a7',
title: 'Testing a change.',
},
author: {
webUrl: 'http://0.0.0.0:3000/root',
avatarUrl: '/uploads/-/system/user/avatar/1/avatar.png',
username: 'root',
},
milestones: {
nodes: [
{
id: 'gid://gitlab/Milestone/60',
title: '12.4',
description: '',
webPath: '/root/release-test/-/milestones/2',
stats: {
totalIssuesCount: 0,
closedIssuesCount: 0,
},
},
{
id: 'gid://gitlab/Milestone/59',
title: '12.3',
description: 'Milestone 12.3',
webPath: '/root/release-test/-/milestones/1',
stats: {
totalIssuesCount: 2,
closedIssuesCount: 1,
},
},
],
},
},
],
},
},
},
};
import { cloneDeep } from 'lodash';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { import {
requestReleases, requestReleases,
...@@ -8,18 +9,36 @@ import { ...@@ -8,18 +9,36 @@ import {
import state from '~/releases/stores/modules/list/state'; import state from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types'; import * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api'; import api from '~/api';
import { gqClient, convertGraphQLResponse } from '~/releases/util';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination, releases as originalReleases } from '../../../mock_data'; import {
pageInfoHeadersWithoutPagination,
releases as originalReleases,
graphqlReleasesResponse as originalGraphqlReleasesResponse,
} from '../../../mock_data';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
describe('Releases State actions', () => { describe('Releases State actions', () => {
let mockedState; let mockedState;
let pageInfo; let pageInfo;
let releases; let releases;
let graphqlReleasesResponse;
let projectPath;
beforeEach(() => { beforeEach(() => {
mockedState = state(); mockedState = {
...state(),
featureFlags: {
graphqlReleaseData: true,
graphqlReleasesPage: true,
graphqlMilestoneStats: true,
},
};
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination); pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true }); releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
projectPath = 'root/test-project';
}); });
describe('requestReleases', () => { describe('requestReleases', () => {
...@@ -31,39 +50,17 @@ describe('Releases State actions', () => { ...@@ -31,39 +50,17 @@ describe('Releases State actions', () => {
describe('fetchReleases', () => { describe('fetchReleases', () => {
describe('success', () => { describe('success', () => {
it('dispatches requestReleases and receiveReleasesSuccess', done => { it('dispatches requestReleases and receiveReleasesSuccess', done => {
jest.spyOn(api, 'releases').mockImplementation((id, options) => { jest.spyOn(gqClient, 'query').mockImplementation(({ query, variables }) => {
expect(id).toEqual(1); expect(query).toEqual(allReleasesQuery);
expect(options.page).toEqual('1'); expect(variables).toEqual({
return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); fullPath: projectPath,
}); });
return Promise.resolve(graphqlReleasesResponse);
testAction(
fetchReleases,
{ projectId: 1 },
mockedState,
[],
[
{
type: 'requestReleases',
},
{
payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
type: 'receiveReleasesSuccess',
},
],
done,
);
});
it('dispatches requestReleases and receiveReleasesSuccess on page two', done => {
jest.spyOn(api, 'releases').mockImplementation((_, options) => {
expect(options.page).toEqual('2');
return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
}); });
testAction( testAction(
fetchReleases, fetchReleases,
{ page: '2', projectId: 1 }, { projectPath },
mockedState, mockedState,
[], [],
[ [
...@@ -71,7 +68,7 @@ describe('Releases State actions', () => { ...@@ -71,7 +68,7 @@ describe('Releases State actions', () => {
type: 'requestReleases', type: 'requestReleases',
}, },
{ {
payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, payload: convertGraphQLResponse(graphqlReleasesResponse),
type: 'receiveReleasesSuccess', type: 'receiveReleasesSuccess',
}, },
], ],
...@@ -82,11 +79,11 @@ describe('Releases State actions', () => { ...@@ -82,11 +79,11 @@ describe('Releases State actions', () => {
describe('error', () => { describe('error', () => {
it('dispatches requestReleases and receiveReleasesError', done => { it('dispatches requestReleases and receiveReleasesError', done => {
jest.spyOn(api, 'releases').mockReturnValue(Promise.reject()); jest.spyOn(gqClient, 'query').mockRejectedValue();
testAction( testAction(
fetchReleases, fetchReleases,
{ projectId: null }, { projectPath },
mockedState, mockedState,
[], [],
[ [
...@@ -101,6 +98,85 @@ describe('Releases State actions', () => { ...@@ -101,6 +98,85 @@ describe('Releases State actions', () => {
); );
}); });
}); });
describe('when the graphqlReleaseData feature flag is disabled', () => {
beforeEach(() => {
mockedState.featureFlags.graphqlReleasesPage = false;
});
describe('success', () => {
it('dispatches requestReleases and receiveReleasesSuccess', done => {
jest.spyOn(api, 'releases').mockImplementation((id, options) => {
expect(id).toEqual(1);
expect(options.page).toEqual('1');
return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
});
testAction(
fetchReleases,
{ projectId: 1 },
mockedState,
[],
[
{
type: 'requestReleases',
},
{
payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
type: 'receiveReleasesSuccess',
},
],
done,
);
});
it('dispatches requestReleases and receiveReleasesSuccess on page two', done => {
jest.spyOn(api, 'releases').mockImplementation((_, options) => {
expect(options.page).toEqual('2');
return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
});
testAction(
fetchReleases,
{ page: '2', projectId: 1 },
mockedState,
[],
[
{
type: 'requestReleases',
},
{
payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
type: 'receiveReleasesSuccess',
},
],
done,
);
});
});
describe('error', () => {
it('dispatches requestReleases and receiveReleasesError', done => {
jest.spyOn(api, 'releases').mockReturnValue(Promise.reject());
testAction(
fetchReleases,
{ projectId: null },
mockedState,
[],
[
{
type: 'requestReleases',
},
{
type: 'receiveReleasesError',
},
],
done,
);
});
});
});
}); });
describe('receiveReleasesSuccess', () => { describe('receiveReleasesSuccess', () => {
......
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util'; import { cloneDeep } from 'lodash';
import { releaseToApiJson, apiJsonToRelease, convertGraphQLResponse } from '~/releases/util';
import { graphqlReleasesResponse as originalGraphqlReleasesResponse } from './mock_data';
describe('releases/util.js', () => { describe('releases/util.js', () => {
describe('releaseToApiJson', () => { describe('releaseToApiJson', () => {
...@@ -100,4 +102,55 @@ describe('releases/util.js', () => { ...@@ -100,4 +102,55 @@ describe('releases/util.js', () => {
expect(apiJsonToRelease(json)).toEqual(expectedRelease); expect(apiJsonToRelease(json)).toEqual(expectedRelease);
}); });
}); });
describe('convertGraphQLResponse', () => {
let graphqlReleasesResponse;
let converted;
beforeEach(() => {
graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
converted = convertGraphQLResponse(graphqlReleasesResponse);
});
it('matches snapshot', () => {
expect(converted).toMatchSnapshot();
});
describe('assets', () => {
it("handles asset links that don't have a linkType", () => {
expect(converted.data[0].assets.links[0].linkType).not.toBeUndefined();
delete graphqlReleasesResponse.data.project.releases.nodes[0].assets.links.nodes[0]
.linkType;
converted = convertGraphQLResponse(graphqlReleasesResponse);
expect(converted.data[0].assets.links[0].linkType).toBeUndefined();
});
});
describe('_links', () => {
it("handles releases that don't have any links", () => {
expect(converted.data[0]._links.selfUrl).not.toBeUndefined();
delete graphqlReleasesResponse.data.project.releases.nodes[0].links;
converted = convertGraphQLResponse(graphqlReleasesResponse);
expect(converted.data[0]._links.selfUrl).toBeUndefined();
});
});
describe('commit', () => {
it("handles releases that don't have any commit info", () => {
expect(converted.data[0].commit).not.toBeUndefined();
delete graphqlReleasesResponse.data.project.releases.nodes[0].commit;
converted = convertGraphQLResponse(graphqlReleasesResponse);
expect(converted.data[0].commit).toBeUndefined();
});
});
});
}); });
...@@ -20,7 +20,7 @@ RSpec.describe ReleasesHelper do ...@@ -20,7 +20,7 @@ RSpec.describe ReleasesHelper do
let(:release) { create(:release, project: project) } let(:release) { create(:release, project: project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:can_user_create_release) { false } let(:can_user_create_release) { false }
let(:common_keys) { [:project_id, :illustration_path, :documentation_path] } let(:common_keys) { [:project_id, :project_path, :illustration_path, :documentation_path] }
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
before do before do
......
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