diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index e5f36c84987..6680834a8d1 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,148 +1,175 @@ -/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */ +import $ from 'jquery'; -var Api = { - groupsPath: "/api/:version/groups.json", - groupPath: "/api/:version/groups/:id.json", - namespacesPath: "/api/:version/namespaces.json", - groupProjectsPath: "/api/:version/groups/:id/projects.json", - projectsPath: "/api/:version/projects.json?simple=true", - labelsPath: "/:namespace_path/:project_path/labels", - licensePath: "/api/:version/templates/licenses/:key", - gitignorePath: "/api/:version/templates/gitignores/:key", - gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", - dockerfilePath: "/api/:version/templates/dockerfiles/:key", - issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", - group: function(group_id, callback) { - var url = Api.buildUrl(Api.groupPath) - .replace(':id', group_id); +const Api = { + groupsPath: '/api/:version/groups.json', + groupPath: '/api/:version/groups/:id.json', + namespacesPath: '/api/:version/namespaces.json', + groupProjectsPath: '/api/:version/groups/:id/projects.json', + projectsPath: '/api/:version/projects.json?simple=true', + labelsPath: '/:namespace_path/:project_path/labels', + licensePath: '/api/:version/templates/licenses/:key', + gitignorePath: '/api/:version/templates/gitignores/:key', + gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', + dockerfilePath: '/api/:version/templates/dockerfiles/:key', + issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', + usersPath: '/api/:version/users.json', + + group(groupId, callback) { + const url = Api.buildUrl(Api.groupPath) + .replace(':id', groupId); return $.ajax({ - url: url, - dataType: "json" - }).done(function(group) { - return callback(group); - }); + url, + dataType: 'json', + }) + .done(group => callback(group)); }, + // Return groups list. Filtered by query - groups: function(query, options, callback) { - var url = Api.buildUrl(Api.groupsPath); + groups(query, options, callback) { + const url = Api.buildUrl(Api.groupsPath); return $.ajax({ - url: url, - data: $.extend({ - search: query, - per_page: 20 - }, options), - dataType: "json" - }).done(function(groups) { - return callback(groups); - }); - }, - // Return namespaces list. Filtered by query - namespaces: function(query, callback) { - var url = Api.buildUrl(Api.namespacesPath); - return $.ajax({ - url: url, - data: { - search: query, - per_page: 20 - }, - dataType: "json" - }).done(function(namespaces) { - return callback(namespaces); - }); - }, - // Return projects list. Filtered by query - projects: function(query, options, callback) { - var url = Api.buildUrl(Api.projectsPath); - return $.ajax({ - url: url, - data: $.extend({ + url, + data: Object.assign({ search: query, per_page: 20, - membership: true }, options), - dataType: "json" - }).done(function(projects) { - return callback(projects); - }); + dataType: 'json', + }) + .done(groups => callback(groups)); }, - newLabel: function(namespace_path, project_path, data, callback) { - var url = Api.buildUrl(Api.labelsPath) - .replace(':namespace_path', namespace_path) - .replace(':project_path', project_path); + + // Return namespaces list. Filtered by query + namespaces(query, callback) { + const url = Api.buildUrl(Api.namespacesPath); return $.ajax({ - url: url, - type: "POST", - data: { 'label': data }, - dataType: "json" - }).done(function(label) { - return callback(label); - }).error(function(message) { - return callback(message.responseJSON); - }); - }, - // Return group projects list. Filtered by query - groupProjects: function(group_id, query, callback) { - var url = Api.buildUrl(Api.groupProjectsPath) - .replace(':id', group_id); - return $.ajax({ - url: url, + url, data: { search: query, - per_page: 20 + per_page: 20, }, - dataType: "json" - }).done(function(projects) { - return callback(projects); - }); + dataType: 'json', + }).done(namespaces => callback(namespaces)); }, + + // Return projects list. Filtered by query + projects(query, options, callback) { + const url = Api.buildUrl(Api.projectsPath); + return $.ajax({ + url, + data: Object.assign({ + search: query, + per_page: 20, + membership: true, + }, options), + dataType: 'json', + }) + .done(projects => callback(projects)); + }, + + newLabel(namespacePath, projectPath, data, callback) { + const url = Api.buildUrl(Api.labelsPath) + .replace(':namespace_path', namespacePath) + .replace(':project_path', projectPath); + return $.ajax({ + url, + type: 'POST', + data: { label: data }, + dataType: 'json', + }) + .done(label => callback(label)) + .error(message => callback(message.responseJSON)); + }, + + // Return group projects list. Filtered by query + groupProjects(groupId, query, callback) { + const url = Api.buildUrl(Api.groupProjectsPath) + .replace(':id', groupId); + return $.ajax({ + url, + data: { + search: query, + per_page: 20, + }, + dataType: 'json', + }) + .done(projects => callback(projects)); + }, + // Return text for a specific license - licenseText: function(key, data, callback) { - var url = Api.buildUrl(Api.licensePath) + licenseText(key, data, callback) { + const url = Api.buildUrl(Api.licensePath) .replace(':key', key); return $.ajax({ - url: url, - data: data - }).done(function(license) { - return callback(license); - }); + url, + data, + }) + .done(license => callback(license)); }, - gitignoreText: function(key, callback) { - var url = Api.buildUrl(Api.gitignorePath) + + gitignoreText(key, callback) { + const url = Api.buildUrl(Api.gitignorePath) .replace(':key', key); - return $.get(url, function(gitignore) { - return callback(gitignore); - }); + return $.get(url, gitignore => callback(gitignore)); }, - gitlabCiYml: function(key, callback) { - var url = Api.buildUrl(Api.gitlabCiYmlPath) + + gitlabCiYml(key, callback) { + const url = Api.buildUrl(Api.gitlabCiYmlPath) .replace(':key', key); - return $.get(url, function(file) { - return callback(file); - }); + return $.get(url, file => callback(file)); }, - dockerfileYml: function(key, callback) { - var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); + + dockerfileYml(key, callback) { + const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); $.get(url, callback); }, - issueTemplate: function(namespacePath, projectPath, key, type, callback) { - var url = Api.buildUrl(Api.issuableTemplatePath) + + issueTemplate(namespacePath, projectPath, key, type, callback) { + const url = Api.buildUrl(Api.issuableTemplatePath) .replace(':key', key) .replace(':type', type) .replace(':project_path', projectPath) .replace(':namespace_path', namespacePath); $.ajax({ - url: url, - dataType: 'json' - }).done(function(file) { - callback(null, file); - }).error(callback); + url, + dataType: 'json', + }) + .done(file => callback(null, file)) + .error(callback); }, - buildUrl: function(url) { + + users(query, options) { + const url = Api.buildUrl(this.usersPath); + return Api.wrapAjaxCall({ + url, + data: Object.assign({ + search: query, + per_page: 20, + }, options), + dataType: 'json', + }); + }, + + buildUrl(url) { + let urlRoot = ''; if (gon.relative_url_root != null) { - url = gon.relative_url_root + url; + urlRoot = gon.relative_url_root; } - return url.replace(':version', gon.api_version); - } + return urlRoot + url.replace(':version', gon.api_version); + }, + + wrapAjaxCall(options) { + return new Promise((resolve, reject) => { + // jQuery 2 is not Promises/A+ compatible (missing catch) + $.ajax(options) // eslint-disable-line promise/catch-or-return + .then(data => resolve(data), + (jqXHR, textStatus, errorThrown) => { + const error = new Error(`${options.url}: ${errorThrown}`); + error.textStatus = textStatus; + reject(error); + }, + ); + }); + }, }; -window.Api = Api; +export default Api; diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js index ab5b3751c4e..5ae30990aea 100644 --- a/app/assets/javascripts/blob/file_template_selector.js +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -1,5 +1,3 @@ -/* global Api */ - export default class FileTemplateSelector { constructor(mediator) { this.mediator = mediator; @@ -65,4 +63,3 @@ export default class FileTemplateSelector { this.reportSelection(opts); } } - diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js index f2f81af137b..9c41e429c8d 100644 --- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -1,4 +1,4 @@ -/* global Api */ +import Api from '../../api'; import FileTemplateSelector from '../file_template_selector'; diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 3cb7b960aaa..45fb614fe00 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -1,4 +1,4 @@ -/* global Api */ +import Api from '../../api'; import FileTemplateSelector from '../file_template_selector'; diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js index 7efda8e7f50..a894953cc86 100644 --- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -1,4 +1,4 @@ -/* global Api */ +import Api from '../../api'; import FileTemplateSelector from '../file_template_selector'; diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index 1d757332f6c..b7c4da0f62e 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -1,4 +1,4 @@ -/* global Api */ +import Api from '../../api'; import FileTemplateSelector from '../file_template_selector'; diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 121d64db789..907b468e576 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ -/* global Api */ +import Api from './api'; class CreateLabelDropdown { constructor ($el, namespacePath, projectPath) { diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index acfa4bd4c6b..b5975295329 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -3,7 +3,7 @@ prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, promise/catch-or-return */ -/* global Api */ +import Api from './api'; var slice = [].slice; diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 426d7f3288e..5da2db063a4 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */ -/* global Api */ +import Api from './api'; (function() { window.NamespaceSelect = (function() { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 3c1c1e7dceb..0ff0a3b6cc4 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ -/* global Api */ +import Api from './api'; (function() { this.ProjectSelect = (function() { diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 39e4006ac4e..05caf177aec 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */ /* global Flash */ -/* global Api */ +import Api from './api'; (function() { this.Search = (function() { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index e62f429f1ae..9dd14488f22 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -1,5 +1,5 @@ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ -/* global Api */ +import Api from '../api'; import TemplateSelector from '../blob/template_selector'; diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js new file mode 100644 index 00000000000..867322ce8ae --- /dev/null +++ b/spec/javascripts/api_spec.js @@ -0,0 +1,281 @@ +import Api from '~/api'; + +describe('Api', () => { + const dummyApiVersion = 'v3000'; + const dummyUrlRoot = 'http://host.invalid'; + const dummyGon = { + api_version: dummyApiVersion, + relative_url_root: dummyUrlRoot, + }; + const dummyResponse = 'hello from outer space!'; + const sendDummyResponse = () => { + const deferred = $.Deferred(); + deferred.resolve(dummyResponse); + return deferred.promise(); + }; + let originalGon; + + beforeEach(() => { + originalGon = window.gon; + window.gon = dummyGon; + }); + + afterEach(() => { + window.gon = originalGon; + }); + + describe('buildUrl', () => { + it('adds URL root and fills in API version', () => { + const input = '/api/:version/foo/bar'; + const expectedOutput = `${dummyUrlRoot}/api/${dummyApiVersion}/foo/bar`; + + const builtUrl = Api.buildUrl(input); + + expect(builtUrl).toEqual(expectedOutput); + }); + }); + + describe('group', () => { + it('fetches a group', (done) => { + const groupId = '123456'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}.json`; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + return sendDummyResponse(); + }); + + Api.group(groupId, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('groups', () => { + it('fetches groups', (done) => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`; + const expectedData = Object.assign({ + search: query, + per_page: 20, + }, options); + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.groups(query, options, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('namespaces', () => { + it('fetches namespaces', (done) => { + const query = 'dummy query'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`; + const expectedData = { + search: query, + per_page: 20, + }; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.namespaces(query, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('projects', () => { + it('fetches projects', (done) => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; + const expectedData = Object.assign({ + search: query, + per_page: 20, + membership: true, + }, options); + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.projects(query, options, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('newLabel', () => { + it('creates a new label', (done) => { + const namespace = 'some namespace'; + const project = 'some project'; + const labelData = { some: 'data' }; + const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/labels`; + const expectedData = { + label: labelData, + }; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.type).toEqual('POST'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.newLabel(namespace, project, labelData, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('groupProjects', () => { + it('fetches group projects', (done) => { + const groupId = '123456'; + const query = 'dummy query'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; + const expectedData = { + search: query, + per_page: 20, + }; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.groupProjects(groupId, query, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('licenseText', () => { + it('fetches a license text', (done) => { + const licenseKey = "driver's license"; + const data = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.data).toEqual(data); + return sendDummyResponse(); + }); + + Api.licenseText(licenseKey, data, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('gitignoreText', () => { + it('fetches a gitignore text', (done) => { + const gitignoreKey = 'ignore git'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`; + spyOn(jQuery, 'get').and.callFake((url, callback) => { + expect(url).toEqual(expectedUrl); + callback(dummyResponse); + }); + + Api.gitignoreText(gitignoreKey, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('gitlabCiYml', () => { + it('fetches a .gitlab-ci.yml', (done) => { + const gitlabCiYmlKey = 'Y CI ML'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`; + spyOn(jQuery, 'get').and.callFake((url, callback) => { + expect(url).toEqual(expectedUrl); + callback(dummyResponse); + }); + + Api.gitlabCiYml(gitlabCiYmlKey, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('dockerfileYml', () => { + it('fetches a Dockerfile', (done) => { + const dockerfileYmlKey = 'a giant whale'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`; + spyOn(jQuery, 'get').and.callFake((url, callback) => { + expect(url).toEqual(expectedUrl); + callback(dummyResponse); + }); + + Api.dockerfileYml(dockerfileYmlKey, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('issueTemplate', () => { + it('fetches an issue template', (done) => { + const namespace = 'some namespace'; + const project = 'some project'; + const templateKey = 'template key'; + const templateType = 'template type'; + const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${templateKey}`; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + return sendDummyResponse(); + }); + + Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => { + expect(error).toBe(null); + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('users', () => { + it('fetches users', (done) => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`; + const expectedData = Object.assign({ + search: query, + per_page: 20, + }, options); + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.users(query, options) + .then((response) => { + expect(response).toBe(dummyResponse); + }) + .then(done) + .catch(done.fail); + }); + }); +});