diff options
author | Manoj MJ <mmj@gitlab.com> | 2019-09-09 03:38:42 +0000 |
---|---|---|
committer | Ash McKenzie <amckenzie@gitlab.com> | 2019-09-09 03:38:42 +0000 |
commit | b041321a355b507cd9329e80935e960c2b9114eb (patch) | |
tree | 5013fa7c76955750a869b5a071a1d8d8cecd9686 /spec | |
parent | e3763f9cb60e7f1ddf8c40ddc4bf05747e944f9b (diff) | |
download | gitlab-ce-b041321a355b507cd9329e80935e960c2b9114eb.tar.gz |
Application Statistics API
This change implements Application
Statistics API
Diffstat (limited to 'spec')
-rw-r--r-- | spec/features/admin/dashboard_spec.rb | 2 | ||||
-rw-r--r-- | spec/fixtures/api/schemas/statistics.json | 29 | ||||
-rw-r--r-- | spec/frontend/admin/statistics_panel/components/app_spec.js | 73 | ||||
-rw-r--r-- | spec/frontend/admin/statistics_panel/mock_data.js | 15 | ||||
-rw-r--r-- | spec/frontend/admin/statistics_panel/store/actions_spec.js | 115 | ||||
-rw-r--r-- | spec/frontend/admin/statistics_panel/store/getters_spec.js | 48 | ||||
-rw-r--r-- | spec/frontend/admin/statistics_panel/store/mutations_spec.js | 41 | ||||
-rw-r--r-- | spec/requests/api/statistics_spec.rb | 91 |
8 files changed, 413 insertions, 1 deletions
diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb index e204e0a515d..6cb345c5066 100644 --- a/spec/features/admin/dashboard_spec.rb +++ b/spec/features/admin/dashboard_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'admin visits dashboard' do +describe 'admin visits dashboard', :js do include ProjectForksHelper before do diff --git a/spec/fixtures/api/schemas/statistics.json b/spec/fixtures/api/schemas/statistics.json new file mode 100644 index 00000000000..ef2f39aad9d --- /dev/null +++ b/spec/fixtures/api/schemas/statistics.json @@ -0,0 +1,29 @@ +{ + "type": "object", + "required" : [ + "forks", + "issues", + "merge_requests", + "notes", + "snippets", + "ssh_keys", + "milestones", + "users", + "projects", + "groups", + "active_users" + ], + "properties" : { + "forks": { "type": "string" }, + "issues'": { "type": "string" }, + "merge_requests'": { "type": "string" }, + "notes'": { "type": "string" }, + "snippets'": { "type": "string" }, + "ssh_keys'": { "type": "string" }, + "milestones'": { "type": "string" }, + "users'": { "type": "string" }, + "projects'": { "type": "string" }, + "groups'": { "type": "string" }, + "active_users'": { "type": "string" } + } +} diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js new file mode 100644 index 00000000000..25b1d432e2d --- /dev/null +++ b/spec/frontend/admin/statistics_panel/components/app_spec.js @@ -0,0 +1,73 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import StatisticsPanelApp from '~/admin/statistics_panel/components/app.vue'; +import statisticsLabels from '~/admin/statistics_panel/constants'; +import createStore from '~/admin/statistics_panel/store'; +import { GlLoadingIcon } from '@gitlab/ui'; +import mockStatistics from '../mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Admin statistics app', () => { + let wrapper; + let store; + let axiosMock; + + const createComponent = () => { + wrapper = shallowMount(StatisticsPanelApp, { + localVue, + store, + sync: false, + }); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + axiosMock.onGet(/api\/(.*)\/application\/statistics/).reply(200); + store = createStore(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findStats = idx => wrapper.findAll('.js-stats').at(idx); + + describe('template', () => { + describe('when app is loading', () => { + it('renders a loading indicator', () => { + store.dispatch('requestStatistics'); + createComponent(); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + }); + + describe('when app has finished loading', () => { + const statistics = convertObjectPropsToCamelCase(mockStatistics, { deep: true }); + + it.each` + statistic | count | index + ${'forks'} | ${12} | ${0} + ${'issues'} | ${180} | ${1} + ${'mergeRequests'} | ${31} | ${2} + ${'notes'} | ${986} | ${3} + ${'snippets'} | ${50} | ${4} + ${'sshKeys'} | ${10} | ${5} + ${'milestones'} | ${40} | ${6} + ${'activeUsers'} | ${50} | ${7} + `('renders the count for the $statistic statistic', ({ statistic, count, index }) => { + const label = statisticsLabels[statistic]; + store.dispatch('receiveStatisticsSuccess', statistics); + createComponent(); + + expect(findStats(index).text()).toContain(label); + expect(findStats(index).text()).toContain(count); + }); + }); + }); +}); diff --git a/spec/frontend/admin/statistics_panel/mock_data.js b/spec/frontend/admin/statistics_panel/mock_data.js new file mode 100644 index 00000000000..6d861059dfd --- /dev/null +++ b/spec/frontend/admin/statistics_panel/mock_data.js @@ -0,0 +1,15 @@ +const mockStatistics = { + forks: 12, + issues: 180, + merge_requests: 31, + notes: 986, + snippets: 50, + ssh_keys: 10, + milestones: 40, + users: 50, + projects: 29, + groups: 9, + active_users: 50, +}; + +export default mockStatistics; diff --git a/spec/frontend/admin/statistics_panel/store/actions_spec.js b/spec/frontend/admin/statistics_panel/store/actions_spec.js new file mode 100644 index 00000000000..9b18b1aebda --- /dev/null +++ b/spec/frontend/admin/statistics_panel/store/actions_spec.js @@ -0,0 +1,115 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import * as actions from '~/admin/statistics_panel/store/actions'; +import * as types from '~/admin/statistics_panel/store/mutation_types'; +import getInitialState from '~/admin/statistics_panel/store/state'; +import mockStatistics from '../mock_data'; + +describe('Admin statistics panel actions', () => { + let mock; + let state; + + beforeEach(() => { + state = getInitialState(); + mock = new MockAdapter(axios); + }); + + describe('fetchStatistics', () => { + describe('success', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(200, mockStatistics); + }); + + it('dispatches success with received data', done => + testAction( + actions.fetchStatistics, + null, + state, + [], + [ + { type: 'requestStatistics' }, + { + type: 'receiveStatisticsSuccess', + payload: expect.objectContaining( + convertObjectPropsToCamelCase(mockStatistics, { deep: true }), + ), + }, + ], + done, + )); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(500); + }); + + it('dispatches error', done => + testAction( + actions.fetchStatistics, + null, + state, + [], + [ + { + type: 'requestStatistics', + }, + { + type: 'receiveStatisticsError', + payload: new Error('Request failed with status code 500'), + }, + ], + done, + )); + }); + }); + + describe('requestStatistic', () => { + it('should commit the request mutation', done => + testAction( + actions.requestStatistics, + null, + state, + [{ type: types.REQUEST_STATISTICS }], + [], + done, + )); + }); + + describe('receiveStatisticsSuccess', () => { + it('should commit received data', done => + testAction( + actions.receiveStatisticsSuccess, + mockStatistics, + state, + [ + { + type: types.RECEIVE_STATISTICS_SUCCESS, + payload: mockStatistics, + }, + ], + [], + done, + )); + }); + + describe('receiveStatisticsError', () => { + it('should commit error', done => { + testAction( + actions.receiveStatisticsError, + 500, + state, + [ + { + type: types.RECEIVE_STATISTICS_ERROR, + payload: 500, + }, + ], + [], + done, + ); + }); + }); +}); diff --git a/spec/frontend/admin/statistics_panel/store/getters_spec.js b/spec/frontend/admin/statistics_panel/store/getters_spec.js new file mode 100644 index 00000000000..152d82531ed --- /dev/null +++ b/spec/frontend/admin/statistics_panel/store/getters_spec.js @@ -0,0 +1,48 @@ +import createState from '~/admin/statistics_panel/store/state'; +import * as getters from '~/admin/statistics_panel/store/getters'; + +describe('Admin statistics panel getters', () => { + let state; + + beforeEach(() => { + state = createState(); + }); + + describe('getStatistics', () => { + describe('when statistics data exists', () => { + it('returns an array of statistics objects with key, label and value', () => { + state.statistics = { forks: 10, issues: 20 }; + + const statisticsLabels = { + forks: 'Forks', + issues: 'Issues', + }; + + const statisticsData = [ + { key: 'forks', label: 'Forks', value: 10 }, + { key: 'issues', label: 'Issues', value: 20 }, + ]; + + expect(getters.getStatistics(state)(statisticsLabels)).toEqual(statisticsData); + }); + }); + + describe('when no statistics data exists', () => { + it('returns an array of statistics objects with key, label and sets value to null', () => { + state.statistics = null; + + const statisticsLabels = { + forks: 'Forks', + issues: 'Issues', + }; + + const statisticsData = [ + { key: 'forks', label: 'Forks', value: null }, + { key: 'issues', label: 'Issues', value: null }, + ]; + + expect(getters.getStatistics(state)(statisticsLabels)).toEqual(statisticsData); + }); + }); + }); +}); diff --git a/spec/frontend/admin/statistics_panel/store/mutations_spec.js b/spec/frontend/admin/statistics_panel/store/mutations_spec.js new file mode 100644 index 00000000000..179f38d2bc5 --- /dev/null +++ b/spec/frontend/admin/statistics_panel/store/mutations_spec.js @@ -0,0 +1,41 @@ +import mutations from '~/admin/statistics_panel/store/mutations'; +import * as types from '~/admin/statistics_panel/store/mutation_types'; +import getInitialState from '~/admin/statistics_panel/store/state'; +import mockStatistics from '../mock_data'; + +describe('Admin statistics panel mutations', () => { + let state; + + beforeEach(() => { + state = getInitialState(); + }); + + describe(`${types.REQUEST_STATISTICS}`, () => { + it('sets isLoading to true', () => { + mutations[types.REQUEST_STATISTICS](state); + + expect(state.isLoading).toBe(true); + }); + }); + + describe(`${types.RECEIVE_STATISTICS_SUCCESS}`, () => { + it('updates the store with the with statistics', () => { + mutations[types.RECEIVE_STATISTICS_SUCCESS](state, mockStatistics); + + expect(state.isLoading).toBe(false); + expect(state.error).toBe(null); + expect(state.statistics).toEqual(mockStatistics); + }); + }); + + describe(`${types.RECEIVE_STATISTICS_ERROR}`, () => { + it('sets error and clears data', () => { + const error = 500; + mutations[types.RECEIVE_STATISTICS_ERROR](state, error); + + expect(state.isLoading).toBe(false); + expect(state.error).toBe(error); + expect(state.statistics).toEqual(null); + }); + }); +}); diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb new file mode 100644 index 00000000000..91fc4d4c123 --- /dev/null +++ b/spec/requests/api/statistics_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Statistics, 'Statistics' do + include ProjectForksHelper + TABLES_TO_ANALYZE = %w[ + projects + users + namespaces + issues + merge_requests + notes + snippets + fork_networks + fork_network_members + keys + milestones + ].freeze + + let(:path) { "/application/statistics" } + + describe "GET /application/statistics" do + context 'when no user' do + it "returns authentication error" do + get api(path, nil) + + expect(response).to have_gitlab_http_status(401) + end + end + + context "when not an admin" do + let(:user) { create(:user) } + + it "returns forbidden error" do + get api(path, user) + + expect(response).to have_gitlab_http_status(403) + end + end + + context 'when authenticated as admin' do + let(:admin) { create(:admin) } + + it 'matches the response schema' do + get api(path, admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('statistics') + end + + it 'gives the right statistics' do + projects = create_list(:project, 4, namespace: create(:namespace, owner: admin)) + issues = create_list(:issue, 2, project: projects.first, updated_by: admin) + + create_list(:snippet, 2, :public, author: admin) + create_list(:note, 2, author: admin, project: projects.first, noteable: issues.first) + create_list(:milestone, 3, project: projects.first) + create(:key, user: admin) + create(:merge_request, source_project: projects.first) + fork_project(projects.first, admin) + + # Make sure the reltuples have been updated + # to get a correct count on postgresql + TABLES_TO_ANALYZE.each do |table| + ActiveRecord::Base.connection.execute("ANALYZE #{table}") + end + + get api(path, admin) + + expected_statistics = { + issues: 2, + merge_requests: 1, + notes: 2, + snippets: 2, + forks: 1, + ssh_keys: 1, + milestones: 3, + users: 1, + projects: 5, + groups: 1, + active_users: 1 + } + + expected_statistics.each do |entity, count| + expect(json_response[entity.to_s]).to eq(count.to_s) + end + end + end + end +end |