diff options
author | Tim Zallmann <tzallmann@gitlab.com> | 2018-08-06 17:32:12 +0000 |
---|---|---|
committer | Tim Zallmann <tzallmann@gitlab.com> | 2018-08-06 17:32:12 +0000 |
commit | d737abc537476bf2b500f550b0c733d22f338cf1 (patch) | |
tree | 4b865101f1e4eab3ceedd75973098de9f18ac4b6 /spec | |
parent | 0ffd79319ffc06f9ab5dcfce4f20a4da2f739577 (diff) | |
parent | ee851e58490004f985f914f3cfa715b03cdf4982 (diff) | |
download | gitlab-ce-d737abc537476bf2b500f550b0c733d22f338cf1.tar.gz |
Merge branch 'sh-support-bitbucket-server-import' into 'master'
Add support for Bitbucket Server imports
Closes #25393
See merge request gitlab-org/gitlab-ce!20164
Diffstat (limited to 'spec')
16 files changed, 2319 insertions, 11 deletions
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb new file mode 100644 index 00000000000..5024ef71771 --- /dev/null +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe Import::BitbucketServerController do + let(:user) { create(:user) } + let(:project_key) { 'test-project' } + let(:repo_slug) { 'some-repo' } + let(:client) { instance_double(BitbucketServer::Client) } + + def assign_session_tokens + session[:bitbucket_server_url] = 'http://localhost:7990' + session[:bitbucket_server_username] = 'bitbucket' + session[:bitbucket_server_personal_access_token] = 'some-token' + end + + before do + sign_in(user) + allow(controller).to receive(:bitbucket_server_import_enabled?).and_return(true) + end + + describe 'GET new' do + render_views + + it 'shows the input form' do + get :new + + expect(response.body).to have_text('Bitbucket Server URL') + end + end + + describe 'POST create' do + before do + allow(controller).to receive(:bitbucket_client).and_return(client) + repo = double(name: 'my-project') + allow(client).to receive(:repo).with(project_key, repo_slug).and_return(repo) + assign_session_tokens + end + + set(:project) { create(:project) } + + it 'returns the new project' do + allow(Gitlab::BitbucketServerImport::ProjectCreator) + .to receive(:new).with(project_key, repo_slug, anything, 'my-project', user.namespace, user, anything) + .and_return(double(execute: project)) + + post :create, project: project_key, repository: repo_slug, format: :json + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns an error when an invalid project key is used' do + post :create, project: 'some&project' + + expect(response).to have_gitlab_http_status(422) + end + + it 'returns an error when an invalid repository slug is used' do + post :create, project: 'some-project', repository: 'try*this' + + expect(response).to have_gitlab_http_status(422) + end + + it 'returns an error when the project cannot be found' do + allow(client).to receive(:repo).with(project_key, repo_slug).and_return(nil) + + post :create, project: project_key, repository: repo_slug, format: :json + + expect(response).to have_gitlab_http_status(422) + end + + it 'returns an error when the project cannot be saved' do + allow(Gitlab::BitbucketServerImport::ProjectCreator) + .to receive(:new).with(project_key, repo_slug, anything, 'my-project', user.namespace, user, anything) + .and_return(double(execute: build(:project))) + + post :create, project: project_key, repository: repo_slug, format: :json + + expect(response).to have_gitlab_http_status(422) + end + + it "returns an error when the server can't be contacted" do + expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(BitbucketServer::Client::ServerError) + + post :create, project: project_key, repository: repo_slug, format: :json + + expect(response).to have_gitlab_http_status(422) + end + end + + describe 'POST configure' do + let(:token) { 'token' } + let(:username) { 'bitbucket-user' } + let(:url) { 'http://localhost:7990/bitbucket' } + + it 'clears out existing session' do + post :configure + + expect(session[:bitbucket_server_url]).to be_nil + expect(session[:bitbucket_server_username]).to be_nil + expect(session[:bitbucket_server_personal_access_token]).to be_nil + + expect(response).to have_gitlab_http_status(302) + expect(response).to redirect_to(status_import_bitbucket_server_path) + end + + it 'sets the session variables' do + post :configure, personal_access_token: token, bitbucket_username: username, bitbucket_server_url: url + + expect(session[:bitbucket_server_url]).to eq(url) + expect(session[:bitbucket_server_username]).to eq(username) + expect(session[:bitbucket_server_personal_access_token]).to eq(token) + expect(response).to have_gitlab_http_status(302) + expect(response).to redirect_to(status_import_bitbucket_server_path) + end + end + + describe 'GET status' do + render_views + + before do + allow(controller).to receive(:bitbucket_client).and_return(client) + + @repo = double(slug: 'vim', project_key: 'asd', full_name: 'asd/vim', "valid?" => true, project_name: 'asd', browse_url: 'http://test', name: 'vim') + @invalid_repo = double(slug: 'invalid', project_key: 'foobar', full_name: 'asd/foobar', "valid?" => false, browse_url: 'http://bad-repo') + assign_session_tokens + end + + it 'assigns repository categories' do + created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_source: 'foo/bar', import_status: 'finished') + expect(client).to receive(:repos).and_return([@repo, @invalid_repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([created_project]) + expect(assigns(:repos)).to eq([@repo]) + expect(assigns(:incompatible_repos)).to eq([@invalid_repo]) + end + end + + describe 'GET jobs' do + before do + assign_session_tokens + end + + it 'returns a list of imported projects' do + created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id) + + get :jobs + + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(created_project.id) + expect(json_response.first['import_status']).to eq('none') + end + end +end diff --git a/spec/fixtures/importers/bitbucket_server/activities.json b/spec/fixtures/importers/bitbucket_server/activities.json new file mode 100644 index 00000000000..09adfca9f31 --- /dev/null +++ b/spec/fixtures/importers/bitbucket_server/activities.json @@ -0,0 +1,1121 @@ +{ + "isLastPage": true, + "limit": 25, + "size": 8, + "start": 0, + "values": [ + { + "action": "COMMENTED", + "comment": { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [ + { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [ + { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [], + "createdDate": 1530164016725, + "id": 11, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [ + { + "anchor": { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "createdDate": 1530164016725, + "id": 11, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "text": "Ok", + "type": "COMMENT", + "updatedDate": 1530164016725, + "version": 0 + }, + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "createdDate": 1530164026000, + "id": 1, + "permittedOperations": { + "deletable": true, + "editable": true, + "transitionable": true + }, + "state": "OPEN", + "text": "here's a task" + } + ], + "text": "Ok", + "updatedDate": 1530164016725, + "version": 0 + }, + { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [], + "createdDate": 1530165543990, + "id": 12, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "hi", + "updatedDate": 1530165543990, + "version": 0 + } + ], + "createdDate": 1530164013718, + "id": 10, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "Hello world", + "updatedDate": 1530164013718, + "version": 0 + }, + { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [], + "createdDate": 1530165549932, + "id": 13, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "hello", + "updatedDate": 1530165549932, + "version": 0 + } + ], + "createdDate": 1530161499144, + "id": 9, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "is this a new line?", + "updatedDate": 1530161499144, + "version": 0 + }, + "commentAction": "ADDED", + "commentAnchor": { + "diffType": "EFFECTIVE", + "fileType": "TO", + "fromHash": "c5f4288162e2e6218180779c7f6ac1735bb56eab", + "line": 1, + "lineType": "ADDED", + "orphaned": false, + "path": "CHANGELOG.md", + "toHash": "a4c2164330f2549f67c13f36a93884cf66e976be" + }, + "createdDate": 1530161499144, + "diff": { + "destination": { + "components": [ + "CHANGELOG.md" + ], + "extension": "md", + "name": "CHANGELOG.md", + "parent": "", + "toString": "CHANGELOG.md" + }, + "hunks": [ + { + "destinationLine": 1, + "destinationSpan": 11, + "segments": [ + { + "lines": [ + { + "commentIds": [ + 9 + ], + "destination": 1, + "line": "# Edit 1", + "source": 1, + "truncated": false + }, + { + "destination": 2, + "line": "", + "source": 1, + "truncated": false + } + ], + "truncated": false, + "type": "ADDED" + }, + { + "lines": [ + { + "destination": 3, + "line": "# ChangeLog", + "source": 1, + "truncated": false + }, + { + "destination": 4, + "line": "", + "source": 2, + "truncated": false + }, + { + "destination": 5, + "line": "This log summarizes the changes in each released version of rouge. The versioning scheme", + "source": 3, + "truncated": false + }, + { + "destination": 6, + "line": "we use is semver, although we will often release new lexers in minor versions, as a", + "source": 4, + "truncated": false + }, + { + "destination": 7, + "line": "practical matter.", + "source": 5, + "truncated": false + }, + { + "destination": 8, + "line": "", + "source": 6, + "truncated": false + }, + { + "destination": 9, + "line": "## version TBD: (unreleased)", + "source": 7, + "truncated": false + }, + { + "destination": 10, + "line": "", + "source": 8, + "truncated": false + }, + { + "destination": 11, + "line": "* General", + "source": 9, + "truncated": false + } + ], + "truncated": false, + "type": "CONTEXT" + } + ], + "sourceLine": 1, + "sourceSpan": 9, + "truncated": false + } + ], + "properties": { + "current": true, + "fromHash": "c5f4288162e2e6218180779c7f6ac1735bb56eab", + "toHash": "a4c2164330f2549f67c13f36a93884cf66e976be" + }, + "source": null, + "truncated": false + }, + "id": 19, + "user": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + } + }, + { + "action": "COMMENTED", + "comment": { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [], + "createdDate": 1530053198463, + "id": 7, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "What about this line?", + "updatedDate": 1530053198463, + "version": 0 + }, + "commentAction": "ADDED", + "commentAnchor": { + "diffType": "EFFECTIVE", + "fileType": "FROM", + "fromHash": "c5f4288162e2e6218180779c7f6ac1735bb56eab", + "line": 9, + "lineType": "CONTEXT", + "orphaned": false, + "path": "CHANGELOG.md", + "toHash": "a4c2164330f2549f67c13f36a93884cf66e976be" + }, + "createdDate": 1530053198463, + "diff": { + "destination": { + "components": [ + "CHANGELOG.md" + ], + "extension": "md", + "name": "CHANGELOG.md", + "parent": "", + "toString": "CHANGELOG.md" + }, + "hunks": [ + { + "destinationLine": 1, + "destinationSpan": 12, + "segments": [ + { + "lines": [ + { + "destination": 1, + "line": "# Edit 1", + "source": 1, + "truncated": false + }, + { + "destination": 2, + "line": "", + "source": 1, + "truncated": false + } + ], + "truncated": false, + "type": "ADDED" + }, + { + "lines": [ + { + "destination": 3, + "line": "# ChangeLog", + "source": 1, + "truncated": false + }, + { + "destination": 4, + "line": "", + "source": 2, + "truncated": false + }, + { + "destination": 5, + "line": "This log summarizes the changes in each released version of rouge. The versioning scheme", + "source": 3, + "truncated": false + }, + { + "destination": 6, + "line": "we use is semver, although we will often release new lexers in minor versions, as a", + "source": 4, + "truncated": false + }, + { + "destination": 7, + "line": "practical matter.", + "source": 5, + "truncated": false + }, + { + "destination": 8, + "line": "", + "source": 6, + "truncated": false + }, + { + "destination": 9, + "line": "## version TBD: (unreleased)", + "source": 7, + "truncated": false + }, + { + "destination": 10, + "line": "", + "source": 8, + "truncated": false + }, + { + "commentIds": [ + 7 + ], + "destination": 11, + "line": "* General", + "source": 9, + "truncated": false + }, + { + "destination": 12, + "line": " * Load pastie theme ([#809](https://github.com/jneen/rouge/pull/809) by rramsden)", + "source": 10, + "truncated": false + } + ], + "truncated": false, + "type": "CONTEXT" + } + ], + "sourceLine": 1, + "sourceSpan": 10, + "truncated": false + } + ], + "properties": { + "current": true, + "fromHash": "c5f4288162e2e6218180779c7f6ac1735bb56eab", + "toHash": "a4c2164330f2549f67c13f36a93884cf66e976be" + }, + "source": null, + "truncated": false + }, + "id": 14, + "user": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + } + }, + { + "action": "COMMENTED", + "comment": { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [ + { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [ + { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [], + "createdDate": 1530143330513, + "id": 8, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "How about this?", + "updatedDate": 1530143330513, + "version": 0 + } + ], + "createdDate": 1530053193795, + "id": 6, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "It does.", + "updatedDate": 1530053193795, + "version": 0 + } + ], + "createdDate": 1530053187904, + "id": 5, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "Does this line make sense?", + "updatedDate": 1530053187904, + "version": 0 + }, + "commentAction": "ADDED", + "commentAnchor": { + "diffType": "EFFECTIVE", + "fileType": "FROM", + "fromHash": "c5f4288162e2e6218180779c7f6ac1735bb56eab", + "line": 3, + "lineType": "CONTEXT", + "orphaned": false, + "path": "CHANGELOG.md", + "toHash": "a4c2164330f2549f67c13f36a93884cf66e976be" + }, + "createdDate": 1530053187904, + "diff": { + "destination": { + "components": [ + "CHANGELOG.md" + ], + "extension": "md", + "name": "CHANGELOG.md", + "parent": "", + "toString": "CHANGELOG.md" + }, + "hunks": [ + { + "destinationLine": 1, + "destinationSpan": 12, + "segments": [ + { + "lines": [ + { + "destination": 1, + "line": "# Edit 1", + "source": 1, + "truncated": false + }, + { + "destination": 2, + "line": "", + "source": 1, + "truncated": false + } + ], + "truncated": false, + "type": "ADDED" + }, + { + "lines": [ + { + "destination": 3, + "line": "# ChangeLog", + "source": 1, + "truncated": false + }, + { + "destination": 4, + "line": "", + "source": 2, + "truncated": false + }, + { + "commentIds": [ + 5 + ], + "destination": 5, + "line": "This log summarizes the changes in each released version of rouge. The versioning scheme", + "source": 3, + "truncated": false + }, + { + "destination": 6, + "line": "we use is semver, although we will often release new lexers in minor versions, as a", + "source": 4, + "truncated": false + }, + { + "destination": 7, + "line": "practical matter.", + "source": 5, + "truncated": false + }, + { + "destination": 8, + "line": "", + "source": 6, + "truncated": false + }, + { + "destination": 9, + "line": "## version TBD: (unreleased)", + "source": 7, + "truncated": false + }, + { + "destination": 10, + "line": "", + "source": 8, + "truncated": false + }, + { + "destination": 11, + "line": "* General", + "source": 9, + "truncated": false + }, + { + "destination": 12, + "line": " * Load pastie theme ([#809](https://github.com/jneen/rouge/pull/809) by rramsden)", + "source": 10, + "truncated": false + } + ], + "truncated": false, + "type": "CONTEXT" + } + ], + "sourceLine": 1, + "sourceSpan": 10, + "truncated": false + } + ], + "properties": { + "current": true, + "fromHash": "c5f4288162e2e6218180779c7f6ac1735bb56eab", + "toHash": "a4c2164330f2549f67c13f36a93884cf66e976be" + }, + "source": null, + "truncated": false + }, + "id": 12, + "user": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + } + }, + { + "action": "COMMENTED", + "comment": { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [], + "createdDate": 1529813304164, + "id": 4, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "Hello world", + "updatedDate": 1529813304164, + "version": 0 + }, + "commentAction": "ADDED", + "createdDate": 1529813304164, + "id": 11, + "user": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + } + }, + { + "action": "MERGED", + "commit": { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "authorTimestamp": 1529727872000, + "committer": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "committerTimestamp": 1529727872000, + "displayId": "839fa9a2d43", + "id": "839fa9a2d434eb697815b8fcafaecc51accfdbbc", + "message": "Merge pull request #1 in TEST/rouge from root/CHANGELOGmd-1529725646923 to master\n\n* commit '66fbe6a097803f0acb7342b19563f710657ce5a2':\n CHANGELOG.md edited online with Bitbucket", + "parents": [ + { + "author": { + "emailAddress": "dblessing@users.noreply.github.com", + "name": "Drew Blessing" + }, + "authorTimestamp": 1529604583000, + "committer": { + "emailAddress": "noreply@github.com", + "name": "GitHub" + }, + "committerTimestamp": 1529604583000, + "displayId": "c5f4288162e", + "id": "c5f4288162e2e6218180779c7f6ac1735bb56eab", + "message": "Merge pull request #949 from jneen/dblessing-patch-1\n\nAdd 'obj-c', 'obj_c' as ObjectiveC aliases", + "parents": [ + { + "displayId": "ea7675f741e", + "id": "ea7675f741ee28f3f177ff32a9bde192742ffc59" + }, + { + "displayId": "386b95a977b", + "id": "386b95a977b331e267497aa5206861774656f0c5" + } + ] + }, + { + "author": { + "emailAddress": "test.user@example.com", + "name": "root" + }, + "authorTimestamp": 1529725651000, + "committer": { + "emailAddress": "test.user@example.com", + "name": "root" + }, + "committerTimestamp": 1529725651000, + "displayId": "66fbe6a0978", + "id": "66fbe6a097803f0acb7342b19563f710657ce5a2", + "message": "CHANGELOG.md edited online with Bitbucket", + "parents": [ + { + "displayId": "c5f4288162e", + "id": "c5f4288162e2e6218180779c7f6ac1735bb56eab" + } + ] + } + ] + }, + "createdDate": 1529727872302, + "id": 7, + "user": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + } + }, + { + "action": "COMMENTED", + "comment": { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [ + { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [], + "createdDate": 1529813297478, + "id": 3, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "This is a thread", + "updatedDate": 1529813297478, + "version": 0 + } + ], + "createdDate": 1529725692591, + "id": 2, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "What about this?", + "updatedDate": 1529725692591, + "version": 0 + }, + "commentAction": "ADDED", + "createdDate": 1529725692591, + "id": 6, + "user": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + } + }, + { + "action": "COMMENTED", + "comment": { + "author": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + }, + "comments": [], + "createdDate": 1529725685910, + "id": 1, + "permittedOperations": { + "deletable": true, + "editable": true + }, + "properties": { + "repositoryId": 1 + }, + "tasks": [], + "text": "This is a test.\n\n[analyze.json](attachment:1/1f32f09d97%2Fanalyze.json)\n", + "updatedDate": 1529725685910, + "version": 0 + }, + "commentAction": "ADDED", + "createdDate": 1529725685910, + "id": 5, + "user": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + } + }, + { + "action": "OPENED", + "createdDate": 1529725657542, + "id": 4, + "user": { + "active": true, + "displayName": "root", + "emailAddress": "test.user@example.com", + "id": 1, + "links": { + "self": [ + { + "href": "http://localhost:7990/users/root" + } + ] + }, + "name": "root", + "slug": "root", + "type": "NORMAL" + } + } + ] +} diff --git a/spec/fixtures/importers/bitbucket_server/pull_request.json b/spec/fixtures/importers/bitbucket_server/pull_request.json new file mode 100644 index 00000000000..6c7fcf3b04c --- /dev/null +++ b/spec/fixtures/importers/bitbucket_server/pull_request.json @@ -0,0 +1,146 @@ +{ + "author":{ + "approved":false, + "role":"AUTHOR", + "status":"UNAPPROVED", + "user":{ + "active":true, + "displayName":"root", + "emailAddress":"joe.montana@49ers.com", + "id":1, + "links":{ + "self":[ + { + "href":"http://localhost:7990/users/root" + } + ] + }, + "name":"root", + "slug":"root", + "type":"NORMAL" + } + }, + "closed":true, + "closedDate":1530600648850, + "createdDate":1530600635690, + "description":"Test", + "fromRef":{ + "displayId":"root/CODE_OF_CONDUCTmd-1530600625006", + "id":"refs/heads/root/CODE_OF_CONDUCTmd-1530600625006", + "latestCommit":"074e2b4dddc5b99df1bf9d4a3f66cfc15481fdc8", + "repository":{ + "forkable":true, + "id":1, + "links":{ + "clone":[ + { + "href":"http://root@localhost:7990/scm/test/rouge.git", + "name":"http" + }, + { + "href":"ssh://git@localhost:7999/test/rouge.git", + "name":"ssh" + } + ], + "self":[ + { + "href":"http://localhost:7990/projects/TEST/repos/rouge/browse" + } + ] + }, + "name":"rouge", + "project":{ + "description":"Test", + "id":1, + "key":"TEST", + "links":{ + "self":[ + { + "href":"http://localhost:7990/projects/TEST" + } + ] + }, + "name":"test", + "public":false, + "type":"NORMAL" + }, + "public":false, + "scmId":"git", + "slug":"rouge", + "state":"AVAILABLE", + "statusMessage":"Available" + } + }, + "id":7, + "links":{ + "self":[ + { + "href":"http://localhost:7990/projects/TEST/repos/rouge/pull-requests/7" + } + ] + }, + "locked":false, + "open":false, + "participants":[ + + ], + "properties":{ + "commentCount":1, + "openTaskCount":0, + "resolvedTaskCount":0 + }, + "reviewers":[ + + ], + "state":"MERGED", + "title":"Added a new line", + "toRef":{ + "displayId":"master", + "id":"refs/heads/master", + "latestCommit":"839fa9a2d434eb697815b8fcafaecc51accfdbbc", + "repository":{ + "forkable":true, + "id":1, + "links":{ + "clone":[ + { + "href":"http://root@localhost:7990/scm/test/rouge.git", + "name":"http" + }, + { + "href":"ssh://git@localhost:7999/test/rouge.git", + "name":"ssh" + } + ], + "self":[ + { + "href":"http://localhost:7990/projects/TEST/repos/rouge/browse" + } + ] + }, + "name":"rouge", + "project":{ + "description":"Test", + "id":1, + "key":"TEST", + "links":{ + "self":[ + { + "href":"http://localhost:7990/projects/TEST" + } + ] + }, + "name":"test", + "public":false, + "type":"NORMAL" + }, + "public":false, + "scmId":"git", + "slug":"rouge", + "state":"AVAILABLE", + "statusMessage":"Available" + } + }, + "updatedDate":1530600648850, + "version":2 +} diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb index 343e140f5fb..234690e742b 100644 --- a/spec/helpers/namespaces_helper_spec.rb +++ b/spec/helpers/namespaces_helper_spec.rb @@ -31,6 +31,44 @@ describe NamespacesHelper do expect(options).to include(user.name) end + it 'avoids duplicate groups when extra_group is used' do + allow(helper).to receive(:current_user).and_return(admin) + + options = helper.namespaces_options(user_group.id, display_path: true, extra_group: build(:group, name: admin_group.name)) + + expect(options.scan("data-name=\"#{admin_group.name}\"").count).to eq(1) + expect(options).to include(admin_group.name) + end + + it 'selects existing group' do + allow(helper).to receive(:current_user).and_return(admin) + + options = helper.namespaces_options(:extra_group, display_path: true, extra_group: user_group) + + expect(options).to include("selected=\"selected\" value=\"#{user_group.id}\"") + expect(options).to include(admin_group.name) + end + + it 'selects the new group by default' do + allow(helper).to receive(:current_user).and_return(user) + + options = helper.namespaces_options(:extra_group, display_path: true, extra_group: build(:group, name: 'new-group')) + + expect(options).to include(user_group.name) + expect(options).not_to include(admin_group.name) + expect(options).to include("selected=\"selected\" value=\"-1\"") + end + + it 'falls back to current user selection' do + allow(helper).to receive(:current_user).and_return(user) + + options = helper.namespaces_options(:extra_group, display_path: true, extra_group: build(:group, name: admin_group.name)) + + expect(options).to include(user_group.name) + expect(options).not_to include(admin_group.name) + expect(options).to include("selected=\"selected\" value=\"#{user.namespace.id}\"") + end + it 'returns only groups if groups_only option is true' do allow(helper).to receive(:current_user).and_return(user) diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb new file mode 100644 index 00000000000..f926ae963a4 --- /dev/null +++ b/spec/lib/bitbucket_server/client_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe BitbucketServer::Client do + let(:base_uri) { 'https://test:7990/stash/' } + let(:options) { { base_uri: base_uri, user: 'bitbucket', password: 'mypassword' } } + let(:project) { 'SOME-PROJECT' } + let(:repo_slug) { 'my-repo' } + let(:headers) { { "Content-Type" => "application/json" } } + + subject { described_class.new(options) } + + describe '#pull_requests' do + let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests?state=ALL" } + + it 'requests a collection' do + expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :pull_request) + + subject.pull_requests(project, repo_slug) + end + + it 'throws an exception when connection fails' do + allow(BitbucketServer::Collection).to receive(:new).and_raise(OpenSSL::SSL::SSLError) + + expect { subject.pull_requests(project, repo_slug) }.to raise_error(described_class::ServerError) + end + end + + describe '#activities' do + let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests/1/activities" } + + it 'requests a collection' do + expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :activity) + + subject.activities(project, repo_slug, 1) + end + end + + describe '#repo' do + let(:path) { "/projects/#{project}/repos/#{repo_slug}" } + let(:url) { "#{base_uri}rest/api/1.0/projects/SOME-PROJECT/repos/my-repo" } + + it 'requests a specific repository' do + stub_request(:get, url).to_return(status: 200, headers: headers, body: '{}') + + subject.repo(project, repo_slug) + + expect(WebMock).to have_requested(:get, url) + end + end + + describe '#repos' do + let(:path) { "/repos" } + + it 'requests a collection' do + expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo) + + subject.repos + end + end + + describe '#create_branch' do + let(:branch) { 'test-branch' } + let(:sha) { '12345678' } + let(:url) { "#{base_uri}rest/api/1.0/projects/SOME-PROJECT/repos/my-repo/branches" } + + it 'requests Bitbucket to create a branch' do + stub_request(:post, url).to_return(status: 204, headers: headers, body: '{}') + + subject.create_branch(project, repo_slug, branch, sha) + + expect(WebMock).to have_requested(:post, url) + end + end + + describe '#delete_branch' do + let(:branch) { 'test-branch' } + let(:sha) { '12345678' } + let(:url) { "#{base_uri}rest/branch-utils/1.0/projects/SOME-PROJECT/repos/my-repo/branches" } + + it 'requests Bitbucket to create a branch' do + stub_request(:delete, url).to_return(status: 204, headers: headers, body: '{}') + + subject.delete_branch(project, repo_slug, branch, sha) + + expect(WebMock).to have_requested(:delete, url) + end + end +end diff --git a/spec/lib/bitbucket_server/connection_spec.rb b/spec/lib/bitbucket_server/connection_spec.rb new file mode 100644 index 00000000000..b5da4cb1a49 --- /dev/null +++ b/spec/lib/bitbucket_server/connection_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe BitbucketServer::Connection do + let(:options) { { base_uri: 'https://test:7990', user: 'bitbucket', password: 'mypassword' } } + let(:payload) { { 'test' => 1 } } + let(:headers) { { "Content-Type" => "application/json" } } + let(:url) { 'https://test:7990/rest/api/1.0/test?something=1' } + + subject { described_class.new(options) } + + describe '#get' do + it 'returns JSON body' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: payload.to_json, status: 200, headers: headers) + + expect(subject.get(url, { something: 1 })).to eq(payload) + end + + it 'throws an exception if the response is not 200' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: payload.to_json, status: 500, headers: headers) + + expect { subject.get(url) }.to raise_error(described_class::ConnectionError) + end + + it 'throws an exception if the response is not JSON' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_return(body: 'bad data', status: 200, headers: headers) + + expect { subject.get(url) }.to raise_error(described_class::ConnectionError) + end + end + + describe '#post' do + let(:headers) { { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } } + + it 'returns JSON body' do + WebMock.stub_request(:post, url).with(headers: headers).to_return(body: payload.to_json, status: 200, headers: headers) + + expect(subject.post(url, payload)).to eq(payload) + end + + it 'throws an exception if the response is not 200' do + WebMock.stub_request(:post, url).with(headers: headers).to_return(body: payload.to_json, status: 500, headers: headers) + + expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError) + end + end + + describe '#delete' do + let(:headers) { { 'Accept' => 'application/json', 'Content-Type' => 'application/json' } } + + context 'branch API' do + let(:branch_path) { '/projects/foo/repos/bar/branches' } + let(:branch_url) { 'https://test:7990/rest/branch-utils/1.0/projects/foo/repos/bar/branches' } + let(:path) { } + + it 'returns JSON body' do + WebMock.stub_request(:delete, branch_url).with(headers: headers).to_return(body: payload.to_json, status: 200, headers: headers) + + expect(subject.delete(:branches, branch_path, payload)).to eq(payload) + end + + it 'throws an exception if the response is not 200' do + WebMock.stub_request(:delete, branch_url).with(headers: headers).to_return(body: payload.to_json, status: 500, headers: headers) + + expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError) + end + end + end +end diff --git a/spec/lib/bitbucket_server/page_spec.rb b/spec/lib/bitbucket_server/page_spec.rb new file mode 100644 index 00000000000..cf419a9045b --- /dev/null +++ b/spec/lib/bitbucket_server/page_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe BitbucketServer::Page do + let(:response) { { 'values' => [{ 'description' => 'Test' }], 'isLastPage' => false, 'nextPageStart' => 2 } } + + before do + # Autoloading hack + BitbucketServer::Representation::PullRequest.new({}) + end + + describe '#items' do + it 'returns collection of needed objects' do + page = described_class.new(response, :pull_request) + + expect(page.items.first).to be_a(BitbucketServer::Representation::PullRequest) + expect(page.items.count).to eq(1) + end + end + + describe '#attrs' do + it 'returns attributes' do + page = described_class.new(response, :pull_request) + + expect(page.attrs.keys).to include(:isLastPage, :nextPageStart) + end + end + + describe '#next?' do + it 'returns true' do + page = described_class.new(response, :pull_request) + + expect(page.next?).to be_truthy + end + + it 'returns false' do + response['isLastPage'] = true + response.delete('nextPageStart') + page = described_class.new(response, :pull_request) + + expect(page.next?).to be_falsey + end + end + + describe '#next' do + it 'returns next attribute' do + page = described_class.new(response, :pull_request) + + expect(page.next).to eq(2) + end + end +end diff --git a/spec/lib/bitbucket_server/paginator_spec.rb b/spec/lib/bitbucket_server/paginator_spec.rb new file mode 100644 index 00000000000..2de50eba3c4 --- /dev/null +++ b/spec/lib/bitbucket_server/paginator_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe BitbucketServer::Paginator do + let(:last_page) { double(:page, next?: false, items: ['item_2']) } + let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) } + let(:connection) { instance_double(BitbucketServer::Connection) } + + describe '#items' do + let(:paginator) { described_class.new(connection, 'http://more-data', :pull_request) } + let(:page_attrs) { { 'isLastPage' => false, 'nextPageStart' => 1 } } + + it 'returns items and raises StopIteration in the end' do + allow(paginator).to receive(:fetch_next_page).and_return(first_page) + expect(paginator.items).to match(['item_1']) + + allow(paginator).to receive(:fetch_next_page).and_return(last_page) + expect(paginator.items).to match(['item_2']) + + allow(paginator).to receive(:fetch_next_page).and_return(nil) + expect { paginator.items }.to raise_error(StopIteration) + end + + it 'calls the connection with different offsets' do + expect(connection).to receive(:get).with('http://more-data', start: 0, limit: BitbucketServer::Paginator::PAGE_LENGTH).and_return(page_attrs) + + expect(paginator.items).to eq([]) + + expect(connection).to receive(:get).with('http://more-data', start: 1, limit: BitbucketServer::Paginator::PAGE_LENGTH).and_return({}) + + expect(paginator.items).to eq([]) + + expect { paginator.items }.to raise_error(StopIteration) + end + end +end diff --git a/spec/lib/bitbucket_server/representation/activity_spec.rb b/spec/lib/bitbucket_server/representation/activity_spec.rb new file mode 100644 index 00000000000..15c50e40472 --- /dev/null +++ b/spec/lib/bitbucket_server/representation/activity_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::Activity do + let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] } + let(:inline_comment) { activities.first } + let(:comment) { activities[3] } + let(:merge_event) { activities[4] } + + describe 'regular comment' do + subject { described_class.new(comment) } + + it { expect(subject.comment?).to be_truthy } + it { expect(subject.inline_comment?).to be_falsey } + it { expect(subject.comment).to be_a(BitbucketServer::Representation::Comment) } + it { expect(subject.created_at).to be_a(Time) } + end + + describe 'inline comment' do + subject { described_class.new(inline_comment) } + + it { expect(subject.comment?).to be_truthy } + it { expect(subject.inline_comment?).to be_truthy } + it { expect(subject.comment).to be_a(BitbucketServer::Representation::PullRequestComment) } + it { expect(subject.created_at).to be_a(Time) } + end + + describe 'merge event' do + subject { described_class.new(merge_event) } + + it { expect(subject.comment?).to be_falsey } + it { expect(subject.inline_comment?).to be_falsey } + it { expect(subject.committer_user).to eq('root') } + it { expect(subject.committer_email).to eq('test.user@example.com') } + it { expect(subject.merge_timestamp).to be_a(Time) } + it { expect(subject.created_at).to be_a(Time) } + it { expect(subject.merge_commit).to eq('839fa9a2d434eb697815b8fcafaecc51accfdbbc') } + end +end diff --git a/spec/lib/bitbucket_server/representation/comment_spec.rb b/spec/lib/bitbucket_server/representation/comment_spec.rb new file mode 100644 index 00000000000..53a20a1d80a --- /dev/null +++ b/spec/lib/bitbucket_server/representation/comment_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::Comment do + let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] } + let(:comment) { activities.first } + + subject { described_class.new(comment) } + + describe '#id' do + it { expect(subject.id).to eq(9) } + end + + describe '#author_username' do + it { expect(subject.author_username).to eq('root' ) } + end + + describe '#author_email' do + it { expect(subject.author_email).to eq('test.user@example.com' ) } + end + + describe '#note' do + it { expect(subject.note).to eq('is this a new line?') } + end + + describe '#created_at' do + it { expect(subject.created_at).to be_a(Time) } + end + + describe '#updated_at' do + it { expect(subject.created_at).to be_a(Time) } + end + + describe '#comments' do + it { expect(subject.comments.count).to eq(4) } + it { expect(subject.comments).to all( be_a(described_class) ) } + it { expect(subject.comments.map(&:note)).to match_array(["Hello world", "Ok", "hello", "hi"]) } + + # The thread should look like: + # + # is this a new line? (subject) + # -> Hello world (first) + # -> Ok (third) + # -> Hi (fourth) + # -> hello (second) + it 'comments have the right parent' do + first, second, third, fourth = subject.comments[0..4] + + expect(subject.parent_comment).to be_nil + expect(first.parent_comment).to eq(subject) + expect(second.parent_comment).to eq(subject) + expect(third.parent_comment).to eq(first) + expect(fourth.parent_comment).to eq(first) + end + end +end diff --git a/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb new file mode 100644 index 00000000000..bd7e3597486 --- /dev/null +++ b/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::PullRequestComment do + let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] } + let(:comment) { activities.second } + + subject { described_class.new(comment) } + + describe '#id' do + it { expect(subject.id).to eq(7) } + end + + describe '#from_sha' do + it { expect(subject.from_sha).to eq('c5f4288162e2e6218180779c7f6ac1735bb56eab') } + end + + describe '#to_sha' do + it { expect(subject.to_sha).to eq('a4c2164330f2549f67c13f36a93884cf66e976be') } + end + + describe '#to?' do + it { expect(subject.to?).to be_falsey } + end + + describe '#from?' do + it { expect(subject.from?).to be_truthy } + end + + describe '#added?' do + it { expect(subject.added?).to be_falsey } + end + + describe '#removed?' do + it { expect(subject.removed?).to be_falsey } + end + + describe '#new_pos' do + it { expect(subject.new_pos).to eq(11) } + end + + describe '#old_pos' do + it { expect(subject.old_pos).to eq(9) } + end + + describe '#file_path' do + it { expect(subject.file_path).to eq('CHANGELOG.md') } + end +end diff --git a/spec/lib/bitbucket_server/representation/pull_request_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_spec.rb new file mode 100644 index 00000000000..4b8afdb006b --- /dev/null +++ b/spec/lib/bitbucket_server/representation/pull_request_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::PullRequest do + let(:sample_data) { JSON.parse(fixture_file('importers/bitbucket_server/pull_request.json')) } + + subject { described_class.new(sample_data) } + + describe '#author' do + it { expect(subject.author).to eq('root') } + end + + describe '#author_email' do + it { expect(subject.author_email).to eq('joe.montana@49ers.com') } + end + + describe '#description' do + it { expect(subject.description).to eq('Test') } + end + + describe '#iid' do + it { expect(subject.iid).to eq(7) } + end + + describe '#state' do + it { expect(subject.state).to eq('merged') } + + context 'declined pull requests' do + before do + sample_data['state'] = 'DECLINED' + end + + it 'returns closed' do + expect(subject.state).to eq('closed') + end + end + + context 'open pull requests' do + before do + sample_data['state'] = 'OPEN' + end + + it 'returns open' do + expect(subject.state).to eq('opened') + end + end + end + + describe '#merged?' do + it { expect(subject.merged?).to be_truthy } + end + + describe '#created_at' do + it { expect(subject.created_at.to_i).to eq(sample_data['createdDate'] / 1000) } + end + + describe '#updated_at' do + it { expect(subject.updated_at.to_i).to eq(sample_data['updatedDate'] / 1000) } + end + + describe '#title' do + it { expect(subject.title).to eq('Added a new line') } + end + + describe '#source_branch_name' do + it { expect(subject.source_branch_name).to eq('refs/heads/root/CODE_OF_CONDUCTmd-1530600625006') } + end + + describe '#source_branch_sha' do + it { expect(subject.source_branch_sha).to eq('074e2b4dddc5b99df1bf9d4a3f66cfc15481fdc8') } + end + + describe '#target_branch_name' do + it { expect(subject.target_branch_name).to eq('refs/heads/master') } + end + + describe '#target_branch_sha' do + it { expect(subject.target_branch_sha).to eq('839fa9a2d434eb697815b8fcafaecc51accfdbbc') } + end +end diff --git a/spec/lib/bitbucket_server/representation/repo_spec.rb b/spec/lib/bitbucket_server/representation/repo_spec.rb new file mode 100644 index 00000000000..3ac1030fbb0 --- /dev/null +++ b/spec/lib/bitbucket_server/representation/repo_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe BitbucketServer::Representation::Repo do + let(:sample_data) do + <<~DATA + { + "slug": "rouge", + "id": 1, + "name": "rouge", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "TEST", + "id": 1, + "name": "test", + "description": "Test", + "public": false, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost:7990/projects/TEST" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "http://root@localhost:7990/scm/test/rouge.git", + "name": "http" + }, + { + "href": "ssh://git@localhost:7999/test/rouge.git", + "name": "ssh" + } + ], + "self": [ + { + "href": "http://localhost:7990/projects/TEST/repos/rouge/browse" + } + ] + } + } + DATA + end + + subject { described_class.new(JSON.parse(sample_data)) } + + describe '#project_key' do + it { expect(subject.project_key).to eq('TEST') } + end + + describe '#project_name' do + it { expect(subject.project_name).to eq('test') } + end + + describe '#slug' do + it { expect(subject.slug).to eq('rouge') } + end + + describe '#browse_url' do + it { expect(subject.browse_url).to eq('http://localhost:7990/projects/TEST/repos/rouge/browse') } + end + + describe '#clone_url' do + it { expect(subject.clone_url).to eq('http://root@localhost:7990/scm/test/rouge.git') } + end + + describe '#description' do + it { expect(subject.description).to eq('Test') } + end + + describe '#full_name' do + it { expect(subject.full_name).to eq('test/rouge') } + end +end diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb new file mode 100644 index 00000000000..70423823b89 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb @@ -0,0 +1,291 @@ +require 'spec_helper' + +describe Gitlab::BitbucketServerImport::Importer do + include ImportSpecHelper + + let(:project) { create(:project, :repository, import_url: 'http://my-bitbucket') } + let(:now) { Time.now.utc.change(usec: 0) } + let(:project_key) { 'TEST' } + let(:repo_slug) { 'rouge' } + let(:sample) { RepoHelpers.sample_compare } + + subject { described_class.new(project, recover_missing_commits: true) } + + before do + data = project.create_or_update_import_data( + data: { project_key: project_key, repo_slug: repo_slug }, + credentials: { base_uri: 'http://my-bitbucket', user: 'bitbucket', password: 'test' } + ) + data.save + project.save + end + + describe '#import_repository' do + before do + expect(subject).to receive(:import_pull_requests) + expect(subject).to receive(:delete_temp_branches) + end + + it 'adds a remote' do + expect(project.repository).to receive(:fetch_as_mirror) + .with('http://bitbucket:test@my-bitbucket', + refmap: [:heads, :tags, '+refs/pull-requests/*/to:refs/merge-requests/*/head'], + remote_name: 'bitbucket_server') + + subject.execute + end + end + + describe '#import_pull_requests' do + before do + allow(subject).to receive(:import_repository) + allow(subject).to receive(:delete_temp_branches) + allow(subject).to receive(:restore_branches) + + pull_request = instance_double( + BitbucketServer::Representation::PullRequest, + iid: 10, + source_branch_sha: sample.commits.last, + source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch, + target_branch_sha: sample.commits.first, + target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch, + title: 'This is a title', + description: 'This is a test pull request', + state: 'merged', + author: 'Test Author', + author_email: project.owner.email, + created_at: Time.now, + updated_at: Time.now, + merged?: true) + + allow(subject.client).to receive(:pull_requests).and_return([pull_request]) + + @merge_event = instance_double( + BitbucketServer::Representation::Activity, + comment?: false, + merge_event?: true, + committer_email: project.owner.email, + merge_timestamp: now, + merge_commit: '12345678' + ) + + @pr_note = instance_double( + BitbucketServer::Representation::Comment, + note: 'Hello world', + author_email: 'unknown@gmail.com', + author_username: 'The Flash', + comments: [], + created_at: now, + updated_at: now, + parent_comment: nil) + + @pr_comment = instance_double( + BitbucketServer::Representation::Activity, + comment?: true, + inline_comment?: false, + merge_event?: false, + comment: @pr_note) + end + + it 'imports merge event' do + expect(subject.client).to receive(:activities).and_return([@merge_event]) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.metrics.merged_by).to eq(project.owner) + expect(merge_request.metrics.merged_at).to eq(@merge_event.merge_timestamp) + expect(merge_request.merge_commit_sha).to eq('12345678') + end + + it 'imports comments' do + expect(subject.client).to receive(:activities).and_return([@pr_comment]) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.notes.count).to eq(1) + note = merge_request.notes.first + expect(note.note).to end_with(@pr_note.note) + expect(note.author).to eq(project.owner) + expect(note.created_at).to eq(@pr_note.created_at) + expect(note.updated_at).to eq(@pr_note.created_at) + end + + it 'imports threaded discussions' do + reply = instance_double( + BitbucketServer::Representation::PullRequestComment, + author_email: 'someuser@gitlab.com', + author_username: 'Batman', + note: 'I agree', + created_at: now, + updated_at: now) + + # https://gitlab.com/gitlab-org/gitlab-test/compare/c1acaa58bbcbc3eafe538cb8274ba387047b69f8...5937ac0a7beb003549fc5fd26fc247ad + inline_note = instance_double( + BitbucketServer::Representation::PullRequestComment, + file_type: 'ADDED', + from_sha: sample.commits.first, + to_sha: sample.commits.last, + file_path: '.gitmodules', + old_pos: nil, + new_pos: 4, + note: 'Hello world', + author_email: 'unknown@gmail.com', + author_username: 'Superman', + comments: [reply], + created_at: now, + updated_at: now, + parent_comment: nil) + + allow(reply).to receive(:parent_comment).and_return(inline_note) + + inline_comment = instance_double( + BitbucketServer::Representation::Activity, + comment?: true, + inline_comment?: true, + merge_event?: false, + comment: inline_note) + + expect(subject.client).to receive(:activities).and_return([inline_comment]) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.notes.count).to eq(2) + expect(merge_request.notes.map(&:discussion_id).uniq.count).to eq(1) + + notes = merge_request.notes.order(:id).to_a + start_note = notes.first + expect(start_note.type).to eq('DiffNote') + expect(start_note.note).to end_with(inline_note.note) + expect(start_note.created_at).to eq(inline_note.created_at) + expect(start_note.updated_at).to eq(inline_note.updated_at) + expect(start_note.position.base_sha).to eq(inline_note.from_sha) + expect(start_note.position.start_sha).to eq(inline_note.from_sha) + expect(start_note.position.head_sha).to eq(inline_note.to_sha) + expect(start_note.position.old_line).to be_nil + expect(start_note.position.new_line).to eq(inline_note.new_pos) + + reply_note = notes.last + # Make sure author and reply context is included + expect(reply_note.note).to start_with("*By #{reply.author_username} (#{reply.author_email})*\n\n") + expect(reply_note.note).to end_with("> #{inline_note.note}\n\n#{reply.note}") + expect(reply_note.author).to eq(project.owner) + expect(reply_note.created_at).to eq(reply.created_at) + expect(reply_note.updated_at).to eq(reply.created_at) + expect(reply_note.position.base_sha).to eq(inline_note.from_sha) + expect(reply_note.position.start_sha).to eq(inline_note.from_sha) + expect(reply_note.position.head_sha).to eq(inline_note.to_sha) + expect(reply_note.position.old_line).to be_nil + expect(reply_note.position.new_line).to eq(inline_note.new_pos) + end + + it 'falls back to comments if diff comments fail to validate' do + reply = instance_double( + BitbucketServer::Representation::Comment, + author_email: 'someuser@gitlab.com', + author_username: 'Aquaman', + note: 'I agree', + created_at: now, + updated_at: now) + + # https://gitlab.com/gitlab-org/gitlab-test/compare/c1acaa58bbcbc3eafe538cb8274ba387047b69f8...5937ac0a7beb003549fc5fd26fc247ad + inline_note = instance_double( + BitbucketServer::Representation::PullRequestComment, + file_type: 'REMOVED', + from_sha: sample.commits.first, + to_sha: sample.commits.last, + file_path: '.gitmodules', + old_pos: 8, + new_pos: 9, + note: 'This is a note with an invalid line position.', + author_email: project.owner.email, + author_username: 'Owner', + comments: [reply], + created_at: now, + updated_at: now, + parent_comment: nil) + + inline_comment = instance_double( + BitbucketServer::Representation::Activity, + comment?: true, + inline_comment?: true, + merge_event?: false, + comment: inline_note) + + allow(reply).to receive(:parent_comment).and_return(inline_note) + + expect(subject.client).to receive(:activities).and_return([inline_comment]) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + + merge_request = MergeRequest.first + expect(merge_request.notes.count).to eq(2) + notes = merge_request.notes + + expect(notes.first.note).to start_with('*Comment on .gitmodules') + expect(notes.second.note).to start_with('*Comment on .gitmodules') + end + end + + describe 'inaccessible branches' do + let(:id) { 10 } + let(:temp_branch_from) { "gitlab/import/pull-request/#{id}/from" } + let(:temp_branch_to) { "gitlab/import/pull-request/#{id}/to" } + + before do + pull_request = instance_double( + BitbucketServer::Representation::PullRequest, + iid: id, + source_branch_sha: '12345678', + source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch, + target_branch_sha: '98765432', + target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch, + title: 'This is a title', + description: 'This is a test pull request', + state: 'merged', + author: 'Test Author', + author_email: project.owner.email, + created_at: Time.now, + updated_at: Time.now, + merged?: true) + + expect(subject.client).to receive(:pull_requests).and_return([pull_request]) + expect(subject.client).to receive(:activities).and_return([]) + expect(subject).to receive(:import_repository).twice + end + + it '#restore_branches' do + expect(subject).to receive(:restore_branches).and_call_original + expect(subject).to receive(:delete_temp_branches) + expect(subject.client).to receive(:create_branch) + .with(project_key, repo_slug, + temp_branch_from, + '12345678') + expect(subject.client).to receive(:create_branch) + .with(project_key, repo_slug, + temp_branch_to, + '98765432') + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + end + + it '#delete_temp_branches' do + expect(subject.client).to receive(:create_branch).twice + expect(subject).to receive(:delete_temp_branches).and_call_original + expect(subject.client).to receive(:delete_branch) + .with(project_key, repo_slug, + temp_branch_from, + '12345678') + expect(subject.client).to receive(:delete_branch) + .with(project_key, repo_slug, + temp_branch_to, + '98765432') + expect(project.repository).to receive(:delete_branch).with(temp_branch_from) + expect(project.repository).to receive(:delete_branch).with(temp_branch_to) + + expect { subject.execute }.to change { MergeRequest.count }.by(1) + end + end +end diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index 25827423914..94abf9679c4 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -5,15 +5,16 @@ describe Gitlab::ImportSources do it 'returns a hash' do expected = { - 'GitHub' => 'github', - 'Bitbucket' => 'bitbucket', - 'GitLab.com' => 'gitlab', - 'Google Code' => 'google_code', - 'FogBugz' => 'fogbugz', - 'Repo by URL' => 'git', - 'GitLab export' => 'gitlab_project', - 'Gitea' => 'gitea', - 'Manifest file' => 'manifest' + 'GitHub' => 'github', + 'Bitbucket Cloud' => 'bitbucket', + 'Bitbucket Server' => 'bitbucket_server', + 'GitLab.com' => 'gitlab', + 'Google Code' => 'google_code', + 'FogBugz' => 'fogbugz', + 'Repo by URL' => 'git', + 'GitLab export' => 'gitlab_project', + 'Gitea' => 'gitea', + 'Manifest file' => 'manifest' } expect(described_class.options).to eq(expected) @@ -26,6 +27,7 @@ describe Gitlab::ImportSources do %w( github bitbucket + bitbucket_server gitlab google_code fogbugz @@ -45,6 +47,7 @@ describe Gitlab::ImportSources do %w( github bitbucket + bitbucket_server gitlab google_code fogbugz @@ -60,6 +63,7 @@ describe Gitlab::ImportSources do import_sources = { 'github' => Gitlab::GithubImport::ParallelImporter, 'bitbucket' => Gitlab::BitbucketImport::Importer, + 'bitbucket_server' => Gitlab::BitbucketServerImport::Importer, 'gitlab' => Gitlab::GitlabImport::Importer, 'google_code' => Gitlab::GoogleCodeImport::Importer, 'fogbugz' => Gitlab::FogbugzImport::Importer, @@ -79,7 +83,8 @@ describe Gitlab::ImportSources do describe '.title' do import_sources = { 'github' => 'GitHub', - 'bitbucket' => 'Bitbucket', + 'bitbucket' => 'Bitbucket Cloud', + 'bitbucket_server' => 'Bitbucket Server', 'gitlab' => 'GitLab.com', 'google_code' => 'Google Code', 'fogbugz' => 'FogBugz', @@ -97,7 +102,7 @@ describe Gitlab::ImportSources do end describe 'imports_repository? checker' do - let(:allowed_importers) { %w[github gitlab_project] } + let(:allowed_importers) { %w[github gitlab_project bitbucket_server] } it 'fails if any importer other than the allowed ones implements this method' do current_importers = described_class.values.select { |kind| described_class.importer(kind).try(:imports_repository?) } diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index fd69fe04053..bb3f1501f0e 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -114,6 +114,17 @@ describe Projects::CreateService, '#execute' do end end + context 'import data' do + it 'stores import data and URL' do + import_data = { data: { 'test' => 'some data' } } + project = create_project(user, { name: 'test', import_url: 'http://import-url', import_data: import_data }) + + expect(project.import_data).to be_persisted + expect(project.import_data.data).to eq(import_data[:data]) + expect(project.import_url).to eq('http://import-url') + end + end + context 'builds_enabled global setting' do let(:project) { create_project(user, opts) } |