Commit bf386c7a authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch 'nfriend-use-graphql-for-releases-page' into 'master'

Step 1/4: Begin using GraphQL on the Releases page

See merge request gitlab-org/gitlab!33095
parents 59e704c8 cc667781
......@@ -30,6 +30,10 @@ export default {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
documentationPath: {
type: String,
required: true,
......@@ -62,6 +66,7 @@ export default {
this.fetchReleases({
page: getParameterByName('page'),
projectId: this.projectId,
projectPath: this.projectPath,
});
},
methods: {
......
......@@ -12,6 +12,11 @@ export default () => {
modules: {
list: listModule,
},
featureFlags: {
graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
graphqlReleasesPage: Boolean(gon.features?.graphqlReleasesPage),
graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats),
},
}),
render: h =>
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 {
parseIntPagination,
convertObjectPropsToCamelCase,
} 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.
......@@ -21,13 +23,31 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES);
*
* @param {String} projectId
*/
export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => {
export const fetchReleases = ({ dispatch, rootState }, { page = '1', projectId, projectPath }) => {
dispatch('requestReleases');
api
.releases(projectId, { page })
.then(response => dispatch('receiveReleasesSuccess', response))
.catch(() => dispatch('receiveReleasesError'));
if (
rootState.featureFlags.graphqlReleaseData &&
rootState.featureFlags.graphqlReleasesPage &&
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 }) => {
......
import { pick } from 'lodash';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { truncateSha } from '~/lib/utils/text_utility';
import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
......@@ -39,3 +42,89 @@ export const apiJsonToRelease = json => {
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
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_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
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new
......
......@@ -15,6 +15,7 @@ module ReleasesHelper
def data_for_releases_page
{
project_id: @project.id,
project_path: @project.full_path,
illustration_path: illustration,
documentation_path: help_page
}.tap do |data|
......
......@@ -13,37 +13,25 @@ RSpec.describe 'User views releases', :js do
project.add_guest(guest)
end
context('when the user is a maintainer') do
before do
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 }
shared_examples 'releases page' do
context('when the user is a maintainer') do
before do
gitlab_sign_in(maintainer)
end
it 'sees the link' do
visit project_releases_path(project)
it 'sees the release' do
visit project_releases_path(project)
page.within('.js-assets-list') do
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
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
expect(page).not_to have_content('Upcoming Release')
end
context 'when there is a link redirect' do
let!(:release_link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: '/binaries/linux-amd64', url: url) }
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
visit project_releases_path(project)
......@@ -53,77 +41,103 @@ RSpec.describe 'User views releases', :js do
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' }
context 'when there is a link redirect' do
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
visit project_releases_path(project)
it 'sees the link' do
visit project_releases_path(project)
page.within('.js-assets-list') do
expect(page).to have_css('[data-testid="external-link-indicator"]')
page.within('.js-assets-list') do
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
context 'when the release_asset_link_type feature flag is enabled' do
before do
stub_feature_flags(release_asset_link_type: true)
context 'when the release_asset_link_type feature flag is enabled' do
before do
stub_feature_flags(release_asset_link_type: true)
end
it_behaves_like 'asset link tests'
end
it_behaves_like 'asset link tests'
end
context 'when the release_asset_link_type feature flag is disabled' do
before do
stub_feature_flags(release_asset_link_type: false)
end
context 'when the release_asset_link_type feature flag is disabled' do
before do
stub_feature_flags(release_asset_link_type: false)
it_behaves_like 'asset link tests'
end
it_behaves_like 'asset link tests'
end
context 'with an upcoming release' do
let(:tomorrow) { Time.zone.now + 1.day }
let!(:release) { create(:release, project: project, released_at: tomorrow ) }
context 'with an upcoming release' do
let(:tomorrow) { Time.zone.now + 1.day }
let!(:release) { create(:release, project: project, released_at: tomorrow ) }
it 'sees the upcoming tag' do
visit project_releases_path(project)
it 'sees the upcoming tag' do
visit project_releases_path(project)
expect(page).to have_content('Upcoming Release')
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
context 'with a tag containing a slash' do
it 'sees the release' do
release = create :release, project: project, tag: 'debian/2.4.0-1'
context('when the user is a guest') do
before do
gitlab_sign_in(guest)
end
it 'renders release info except for Git-related data' do
visit project_releases_path(project)
expect(page).to have_content(release.name)
expect(page).to have_content(release.tag)
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
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
gitlab_sign_in(guest)
stub_feature_flags(graphql_releases_page: false)
end
it 'renders release info except for Git-related data' do
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
it_behaves_like 'releases page'
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);
describe('Releases App ', () => {
let wrapper;
let fetchReleaseSpy;
const releasesPagination = rge(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }),
......@@ -28,12 +29,22 @@ describe('Releases App ', () => {
const defaultProps = {
projectId: 'gitlab-ce',
projectPath: 'gitlab-org/gitlab-ce',
documentationPath: 'help/releases',
illustrationPath: 'illustration/path',
};
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, {
store,
......@@ -46,6 +57,25 @@ describe('Releases App ', () => {
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', () => {
beforeEach(() => {
jest
......
......@@ -222,3 +222,131 @@ export const 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 {
requestReleases,
......@@ -8,18 +9,36 @@ import {
import state from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api';
import { gqClient, convertGraphQLResponse } from '~/releases/util';
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', () => {
let mockedState;
let pageInfo;
let releases;
let graphqlReleasesResponse;
let projectPath;
beforeEach(() => {
mockedState = state();
mockedState = {
...state(),
featureFlags: {
graphqlReleaseData: true,
graphqlReleasesPage: true,
graphqlMilestoneStats: true,
},
};
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
graphqlReleasesResponse = cloneDeep(originalGraphqlReleasesResponse);
projectPath = 'root/test-project';
});
describe('requestReleases', () => {
......@@ -31,39 +50,17 @@ describe('Releases State actions', () => {
describe('fetchReleases', () => {
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 });
jest.spyOn(gqClient, 'query').mockImplementation(({ query, variables }) => {
expect(query).toEqual(allReleasesQuery);
expect(variables).toEqual({
fullPath: projectPath,
});
return Promise.resolve(graphqlReleasesResponse);
});
testAction(
fetchReleases,
{ page: '2', projectId: 1 },
{ projectPath },
mockedState,
[],
[
......@@ -71,7 +68,7 @@ describe('Releases State actions', () => {
type: 'requestReleases',
},
{
payload: { data: releases, headers: pageInfoHeadersWithoutPagination },
payload: convertGraphQLResponse(graphqlReleasesResponse),
type: 'receiveReleasesSuccess',
},
],
......@@ -82,11 +79,11 @@ describe('Releases State actions', () => {
describe('error', () => {
it('dispatches requestReleases and receiveReleasesError', done => {
jest.spyOn(api, 'releases').mockReturnValue(Promise.reject());
jest.spyOn(gqClient, 'query').mockRejectedValue();
testAction(
fetchReleases,
{ projectId: null },
{ projectPath },
mockedState,
[],
[
......@@ -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', () => {
......
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('releaseToApiJson', () => {
......@@ -100,4 +102,55 @@ describe('releases/util.js', () => {
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
let(:release) { create(:release, project: project) }
let(:user) { create(:user) }
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
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