Commit 98b72c8a authored by Mark Florian's avatar Mark Florian

Merge branch '208800-step-2-4-mock-server' into 'master'

Setup mock server for IDE integration spec (RUN AS-IF-FOSS)

See merge request gitlab-org/gitlab!37470
parents 5a5cf920 947e747f
......@@ -119,6 +119,15 @@ if (IS_EE) {
});
}
if (!IS_PRODUCTION) {
const fixtureDir = IS_EE ? 'fixtures-ee' : 'fixtures';
Object.assign(alias, {
test_fixtures: path.join(ROOT_PATH, `tmp/tests/frontend/${fixtureDir}`),
test_helpers: path.join(ROOT_PATH, 'spec/frontend_integration/test_helpers'),
});
}
let dll;
if (VENDOR_DLL && !IS_PRODUCTION) {
......
......@@ -40,6 +40,8 @@ module.exports = path => {
'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
'^spec/test_constants$': '<rootDir>/spec/frontend/helpers/test_constants',
'^jest/(.*)$': '<rootDir>/spec/frontend/$1',
'test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
'test_fixtures(/.*)$': '<rootDir>/tmp/tests/frontend/fixtures$1',
};
const collectCoverageFrom = ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'];
......@@ -51,6 +53,7 @@ module.exports = path => {
'^ee_component(/.*)$': rootDirEE,
'^ee_else_ce(/.*)$': rootDirEE,
'^ee_jest/(.*)$': '<rootDir>/ee/spec/frontend/$1',
'test_fixtures(/.*)$': '<rootDir>/tmp/tests/frontend/fixtures-ee$1',
});
collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}'));
......@@ -75,7 +78,7 @@ module.exports = path => {
cacheDirectory: '<rootDir>/tmp/cache/jest',
modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
reporters,
setupFilesAfterEnv: ['<rootDir>/spec/frontend/test_setup.js', 'jest-canvas-mock'],
setupFilesAfterEnv: [`<rootDir>/${path}/test_setup.js`, 'jest-canvas-mock'],
restoreMocks: true,
transform: {
'^.+\\.(gql|graphql)$': 'jest-transform-graphql',
......
......@@ -51,7 +51,7 @@ class CustomEnvironment extends JSDOMEnvironment {
this.global.fetch = () => {};
// Expose the jsdom (created in super class) to the global so that we can call reconfigure({ url: '' }) to properly set `window.location`
this.global.dom = this.dom;
this.global.jsdom = this.dom;
Object.assign(this.global.performance, {
mark: () => null,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
let(:admin) { create(:admin, name: 'root') }
let(:namespace) { create(:namespace, name: 'gitlab-test' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
before(:all) do
clean_frontend_fixtures('api/merge_requests')
end
it 'api/merge_requests/get.json' do
4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") }
get api("/projects/#{project.id}/merge_requests", admin)
expect(response).to be_successful
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
let(:admin) { create(:admin, name: 'root') }
let(:namespace) { create(:namespace, name: 'gitlab-test' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
before(:all) do
clean_frontend_fixtures('api/projects')
end
it 'api/projects/get.json' do
get api("/projects/#{project.id}", admin)
expect(response).to be_successful
end
it 'api/projects/get_empty.json' do
get api("/projects/#{project_empty.id}", admin)
expect(response).to be_successful
end
it 'api/projects/branches/get.json' do
get api("/projects/#{project.id}/repository/branches/#{project.default_branch}", admin)
expect(response).to be_successful
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Projects JSON endpoints (JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin, name: 'root') }
let(:project) { create(:project, :repository) }
before(:all) do
clean_frontend_fixtures('projects_json/')
end
before do
project.add_maintainer(admin)
sign_in(admin)
end
describe Projects::FindFileController, '(JavaScript fixtures)', type: :controller do
it 'projects_json/files.json' do
get :list,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: project.default_branch
},
format: 'json'
expect(response).to be_successful
end
end
describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
it 'projects_json/pipelines_empty.json' do
get :pipelines,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: project.commit(project.default_branch).id,
format: 'json'
}
expect(response).to be_successful
end
end
end
......@@ -8,93 +8,55 @@
*
* See https://gitlab.com/gitlab-org/gitlab/-/issues/208800 for more information.
*/
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { initIde } from '~/ide';
jest.mock('~/api', () => {
return {
project: jest.fn().mockImplementation(() => new Promise(() => {})),
};
});
jest.mock('~/ide/services/gql', () => {
return {
query: jest.fn().mockImplementation(() => new Promise(() => {})),
};
});
import extendStore from '~/ide/stores/extend';
import { TEST_HOST } from 'helpers/test_constants';
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
const TEST_DATASET = {
emptyStateSvgPath: '/test/empty_state.svg',
noChangesStateSvgPath: '/test/no_changes_state.svg',
committedStateSvgPath: '/test/committed_state.svg',
pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg',
promotionSvgPath: '/test/promotion.svg',
ciHelpPagePath: '/test/ci_help_page',
webIDEHelpPagePath: '/test/web_ide_help_page',
clientsidePreviewEnabled: 'true',
renderWhitespaceInCode: 'false',
codesandboxBundlerUrl: 'test/codesandbox_bundler',
};
describe('WebIDE', () => {
useOverclockTimers();
let vm;
let root;
let mock;
let initData;
let location;
beforeEach(() => {
root = document.createElement('div');
initData = {
emptyStateSvgPath: '/test/empty_state.svg',
noChangesStateSvgPath: '/test/no_changes_state.svg',
committedStateSvgPath: '/test/committed_state.svg',
pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg',
promotionSvgPath: '/test/promotion.svg',
ciHelpPagePath: '/test/ci_help_page',
webIDEHelpPagePath: '/test/web_ide_help_page',
clientsidePreviewEnabled: 'true',
renderWhitespaceInCode: 'false',
codesandboxBundlerUrl: 'test/codesandbox_bundler',
};
document.body.appendChild(root);
mock = new MockAdapter(axios);
mock.onAny('*').reply(() => new Promise(() => {}));
location = { pathname: '/-/ide/project/gitlab-test/test', search: '', hash: '' };
Object.defineProperty(window, 'location', {
get() {
return location;
},
global.jsdom.reconfigure({
url: `${TEST_HOST}/-/ide/project/gitlab-test/lorem-ipsum`,
});
});
afterEach(() => {
vm.$destroy();
vm = null;
mock.restore();
root.remove();
});
const createComponent = () => {
const el = document.createElement('div');
Object.assign(el.dataset, initData);
Object.assign(el.dataset, TEST_DATASET);
root.appendChild(el);
vm = initIde(el);
vm = initIde(el, { extendStore });
};
expect.addSnapshotSerializer({
test(value) {
return value instanceof HTMLElement && !value.$_hit;
},
print(element, serialize) {
element.$_hit = true;
element.querySelectorAll('[style]').forEach(el => {
el.$_hit = true;
if (el.style.display === 'none') {
el.textContent = '(jest: contents hidden)';
}
});
return serialize(element)
.replace(/^\s*<!---->$/gm, '')
.replace(/\n\s*\n/gm, '\n');
},
});
it('runs', () => {
createComponent();
return vm.$nextTick().then(() => {
expect(root).toMatchSnapshot();
});
expect(root).toMatchSnapshot();
});
});
import { withValues } from '../utils/obj';
import { getCommit } from '../fixtures';
import { createCommitId } from './commit_id';
// eslint-disable-next-line import/prefer-default-export
export const createNewCommit = ({ id = createCommitId(), message }, orig = getCommit()) => {
return withValues(orig, {
id,
short_id: id.substr(0, 8),
message,
title: message,
web_url: orig.web_url.replace(orig.id, id),
parent_ids: [orig.id],
});
};
const COMMIT_ID_LENGTH = 40;
const DEFAULT_COMMIT_ID = Array(COMMIT_ID_LENGTH)
.fill('0')
.join('');
export const createCommitId = (index = 0) =>
`${index}${DEFAULT_COMMIT_ID}`.substr(0, COMMIT_ID_LENGTH);
export const createCommitIdGenerator = () => {
let prevCommitId = 0;
const next = () => {
prevCommitId += 1;
return createCommitId(prevCommitId);
};
return {
next,
};
};
export * from './commit';
export * from './commit_id';
/* eslint-disable global-require */
import { memoize } from 'lodash';
export const getProject = () => require('test_fixtures/api/projects/get.json');
export const getBranch = () => require('test_fixtures/api/projects/branches/get.json');
export const getMergeRequests = () => require('test_fixtures/api/merge_requests/get.json');
export const getRepositoryFiles = () => require('test_fixtures/projects_json/files.json');
export const getPipelinesEmptyResponse = () =>
require('test_fixtures/projects_json/pipelines_empty.json');
export const getCommit = memoize(() => getBranch().commit);
import { buildSchema, graphql } from 'graphql';
import gitlabSchemaStr from '../../../../doc/api/graphql/reference/gitlab_schema.graphql';
const graphqlSchema = buildSchema(gitlabSchemaStr.loc.source.body);
const graphqlResolvers = {
project({ fullPath }, schema) {
const result = schema.projects.findBy({ path_with_namespace: fullPath });
const userPermission = schema.db.userPermissions[0];
return {
...result.attrs,
userPermissions: {
...userPermission,
},
};
},
};
// eslint-disable-next-line import/prefer-default-export
export const graphqlQuery = (query, variables, schema) =>
graphql(graphqlSchema, query, graphqlResolvers, schema, variables);
import { Server, Model, RestSerializer } from 'miragejs';
import { getProject, getBranch, getMergeRequests, getRepositoryFiles } from 'test_helpers/fixtures';
import setupRoutes from './routes';
export const createMockServerOptions = () => ({
models: {
project: Model,
branch: Model,
mergeRequest: Model,
file: Model,
userPermission: Model,
},
serializers: {
application: RestSerializer.extend({
root: false,
}),
},
seeds(schema) {
schema.db.loadData({
files: getRepositoryFiles().map(path => ({ path })),
projects: [getProject()],
branches: [getBranch()],
mergeRequests: getMergeRequests(),
userPermissions: [
{
createMergeRequestIn: true,
readMergeRequest: true,
pushCode: true,
},
],
});
},
routes() {
this.namespace = '';
this.urlPrefix = '/';
setupRoutes(this);
},
});
export const createMockServer = () => {
const server = new Server(createMockServerOptions());
return server;
};
export default server => {
['get', 'post', 'put', 'delete', 'patch'].forEach(method => {
server[method]('*', () => {
return new Response(404);
});
});
};
import { getPipelinesEmptyResponse } from 'test_helpers/fixtures';
export default server => {
server.get('*/commit/:id/pipelines', () => {
return getPipelinesEmptyResponse();
});
server.get('/api/v4/projects/:id/runners', () => {
return [];
});
};
import { graphqlQuery } from '../graphql';
export default server => {
server.post('/api/graphql', (schema, request) => {
const batches = JSON.parse(request.requestBody);
return Promise.all(
batches.map(({ query, variables }) => graphqlQuery(query, variables, schema)),
);
});
};
/* eslint-disable global-require */
export default server => {
[
require('./graphql'),
require('./projects'),
require('./repository'),
require('./ci'),
require('./404'),
].forEach(({ default: setup }) => {
setup(server);
});
};
import { withKeys } from 'test_helpers/utils/obj';
export default server => {
server.get('/api/v4/projects/:id', (schema, request) => {
const { id } = request.params;
const proj =
schema.projects.findBy({ id }) ?? schema.projects.findBy({ path_with_namespace: id });
return proj.attrs;
});
server.get('/api/v4/projects/:id/merge_requests', (schema, request) => {
const result = schema.mergeRequests.where(
withKeys(request.queryParams, {
source_project_id: 'project_id',
source_branch: 'source_branch',
}),
);
return result.models;
});
};
import { createNewCommit, createCommitIdGenerator } from 'test_helpers/factories';
export default server => {
const commitIdGenerator = createCommitIdGenerator();
server.get('/api/v4/projects/:id/repository/branches', schema => {
return schema.db.branches;
});
server.get('/api/v4/projects/:id/repository/branches/:name', (schema, request) => {
const { name } = request.params;
const branch = schema.branches.findBy({ name });
return branch.attrs;
});
server.get('*/-/files/:id', schema => {
return schema.db.files.map(({ path }) => path);
});
server.post('/api/v4/projects/:id/repository/commits', (schema, request) => {
const { branch: branchName, commit_message: message, actions } = JSON.parse(
request.requestBody,
);
const branch = schema.branches.findBy({ name: branchName });
const commit = {
...createNewCommit({ id: commitIdGenerator.next(), message }, branch.attrs.commit),
__actions: actions,
};
branch.update({ commit });
return commit;
});
};
import { createMockServer } from './index';
if (process.env.NODE_ENV === 'development') {
window.mockServer = createMockServer();
}
import '../../../frontend/test_setup';
import './setup_globals';
import './setup_axios';
import './setup_serializers';
import './setup_mock_server';
import axios from '~/lib/utils/axios_utils';
import adapter from 'axios/lib/adapters/xhr';
// We're removing our default axios adapter because this is handled by our mock server now
axios.defaults.adapter = adapter;
import { setTestTimeout } from 'helpers/timeout';
beforeEach(() => {
window.gon = {
api_version: 'v4',
relative_url_root: '',
};
setTestTimeout(5000);
jest.useRealTimers();
});
afterEach(() => {
jest.useFakeTimers();
});
import { createMockServer } from '../mock_server';
beforeEach(() => {
const server = createMockServer();
server.logging = false;
global.mockServer = server;
});
afterEach(() => {
global.mockServer.shutdown();
global.mockServer = null;
});
import defaultSerializer from '../snapshot_serializer';
expect.addSnapshotSerializer(defaultSerializer);
export default {
test(value) {
return value instanceof HTMLElement && !value.$_hit;
},
print(element, serialize) {
element.$_hit = true;
element.querySelectorAll('[style]').forEach(el => {
el.$_hit = true;
if (el.style.display === 'none') {
el.textContent = '(jest: contents hidden)';
}
});
return serialize(element)
.replace(/^\s*<!---->$/gm, '')
.replace(/\n\s*\n/gm, '\n');
},
};
import { has, mapKeys, pick } from 'lodash';
/**
* This method is used to type-safely set values on the given object
*
* @template T
* @returns {T} A shallow copy of `obj`, with the values from `values`
* @throws {Error} If `values` contains a key that isn't already on `obj`
* @param {T} source
* @param {Object} values
*/
export const withValues = (source, values) =>
Object.entries(values).reduce(
(acc, [key, value]) => {
if (!has(acc, key)) {
throw new Error(
`[mock_server] Cannot write property that does not exist on object '${key}'`,
);
}
return {
...acc,
[key]: value,
};
},
{ ...source },
);
/**
* This method returns a subset of the given object and maps the key names based on the
* given `keys`.
*
* @param {Object} obj The source object.
* @param {Object} map The object which contains the keys to use and mapped key names.
*/
export const withKeys = (obj, map) => mapKeys(pick(obj, Object.keys(map)), (val, key) => map[key]);
import { withKeys, withValues } from './obj';
describe('frontend_integration/test_helpers/utils/obj', () => {
describe('withKeys', () => {
it('picks and maps keys', () => {
expect(withKeys({ a: '123', b: 456, c: 'd' }, { b: 'lorem', c: 'ipsum', z: 'zed ' })).toEqual(
{ lorem: 456, ipsum: 'd' },
);
});
});
describe('withValues', () => {
it('sets values', () => {
expect(withValues({ a: '123', b: 456 }, { b: 789 })).toEqual({ a: '123', b: 789 });
});
it('throws if values has non-existent key', () => {
expect(() => withValues({ a: '123', b: 456 }, { b: 789, bogus: 'throws' })).toThrow(
`[mock_server] Cannot write property that does not exist on object 'bogus'`,
);
});
});
});
/**
* This function replaces the existing `setTimeout` and `setInterval` with wrappers that
* discount the `ms` passed in by `boost`.
*
* For example, if a module has:
*
* ```
* setTimeout(cb, 100);
* ```
*
* But a test has:
*
* ```
* useOverclockTimers(25);
* ```
*
* Then the module's call to `setTimeout` effectively becomes:
*
* ```
* setTimeout(cb, 4);
* ```
*
* It's important to note that the timing for `setTimeout` and order of execution is non-deterministic
* and discounting the `ms` passed could make this very obvious and expose some underlying issues
* with flaky failures.
*
* WARNING: If flaky spec failures show up in a spec that is using this helper, please consider either:
*
* - Refactoring the production code so that it's reactive to state changes, not dependent on timers.
* - Removing the call to this helper from the spec.
*
* @param {Number} boost
*/
// eslint-disable-next-line import/prefer-default-export
export const useOverclockTimers = (boost = 50) => {
if (boost <= 0) {
throw new Error(`[overclock_timers] boost (${boost}) cannot be <= 0`);
}
let origSetTimeout;
let origSetInterval;
const newSetTimeout = (fn, msParam = 0) => {
const ms = msParam > 0 ? Math.floor(msParam / boost) : msParam;
return origSetTimeout(fn, ms);
};
const newSetInterval = (fn, msParam = 0) => {
const ms = msParam > 0 ? Math.floor(msParam / boost) : msParam;
return origSetInterval(fn, ms);
};
beforeEach(() => {
origSetTimeout = global.setTimeout;
origSetInterval = global.setInterval;
global.setTimeout = newSetTimeout;
global.setInterval = newSetInterval;
});
afterEach(() => {
global.setTimeout = origSetTimeout;
global.setInterval = origSetInterval;
});
};
import './test_helpers/setup';
This diff is collapsed.
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