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 {
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