/* * Copyright (C) 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.copybara.git; import static com.google.common.truth.Truth.assertThat; import static com.google.copybara.git.GitRepository.newBareRepo; import static com.google.copybara.testing.git.GitTestUtil.getGitEnv; import static com.google.copybara.testing.git.GitTestUtil.mockResponse; import static com.google.copybara.testing.git.GitTestUtil.mockResponseAndValidateRequest; import static com.google.copybara.testing.git.GitTestUtil.mockResponseWithStatus; import static com.google.copybara.util.CommandRunner.DEFAULT_TIMEOUT; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.matches; import static org.mockito.ArgumentMatchers.startsWith; import com.google.api.client.json.gson.GsonFactory; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.jimfs.Jimfs; import com.google.copybara.exception.ValidationException; import com.google.copybara.feedback.Feedback; import com.google.copybara.testing.DummyChecker; import com.google.copybara.testing.DummyTrigger; import com.google.copybara.testing.OptionsBuilder; import com.google.copybara.testing.SkylarkTestExecutor; import com.google.copybara.testing.git.GitTestUtil; import com.google.copybara.testing.git.GitTestUtil.MockRequestAssertion; import com.google.copybara.testing.git.GitTestUtil.Validator; import com.google.copybara.util.console.testing.TestingConsole; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class GerritEndpointTest { private static final String BASE_URL = "https://user:[email protected]"; private SkylarkTestExecutor skylark; private Path workdir; private DummyTrigger dummyTrigger; private String url; private GitTestUtil gitUtil; @Before public void setup() throws Exception { workdir = Jimfs.newFileSystem().getPath("/"); TestingConsole console = new TestingConsole(); OptionsBuilder options = new OptionsBuilder(); options.setConsole(console).setOutputRootToTmpDir(); dummyTrigger = new DummyTrigger(); options.testingOptions.feedbackTrigger = dummyTrigger; options.testingOptions.checker = new DummyChecker(ImmutableSet.of("badword")); gitUtil = new GitTestUtil(options); Path credentialsFile = Files.createTempFile("credentials", "test"); Files.write(credentialsFile, BASE_URL.getBytes(UTF_8)); GitRepository repo = newBareRepo(Files.createTempDirectory("test_repo"), getGitEnv(), /*verbose=*/true, DEFAULT_TIMEOUT, /*noVerify=*/ false) .init() .withCredentialHelper("store --file=" + credentialsFile); gitUtil.mockRemoteGitRepos(new Validator(), repo); url = BASE_URL + "/foo/bar"; options.general.starlarkMode = "STRICT"; skylark = new SkylarkTestExecutor(options); } private String changeNumberFromRequest(String url) { return url.replaceAll(".*changes/([0-9]{1,10}).*", "$1"); } @Test public void testParsing() throws Exception { skylark.eval( "e", "e = git.gerrit_api(url = 'https://test.googlesource.com/example')"); } @Test public void testParsingWithChecker() throws Exception { skylark.eval( "e", "e = git.gerrit_api(\n" + "url = 'https://test.googlesource.com/example', \n" + "checker = testing.dummy_checker(),\n" + ")\n"); } @Test public void testCheckerIsHonored() throws Exception { String config = "" + "def test_action(ctx):\n" + " ctx.destination.get_change('12_badword_34', include_results = ['LABELS'])\n" + " return ctx.success()\n" + "\n" + "core.feedback(\n" + " name = 'default',\n" + " origin = testing.dummy_trigger(),\n" + " destination = git.gerrit_api(" + " url = 'https://test.googlesource.com/example',\n" + " checker = testing.dummy_checker(),\n" + " ),\n" + " actions = [test_action,],\n" + ")\n" + "\n"; Feedback feedback = (Feedback) skylark.loadConfig(config).getMigration("default"); ValidationException expected = assertThrows( ValidationException.class, () -> feedback.run(workdir, ImmutableList.of("12345"))); assertThat(expected) .hasMessageThat() .contains("Bad word 'badword' found: field 'path'. Location: copy.bara.sky:2:29"); } @Test public void testActions() throws Exception { gitUtil.mockApi( eq("GET"), matches(BASE_URL + "/changes/12345/revisions/sha1/actions"), mockResponse( "" + ")]}'\n" + "{\n" + " \"submit\": {\n" + " \"method\": \"POST\", \n" + " \"label\": \"Submit\", \n" + " \"title\": \"Submit patch set 1 into master\", \n" + " \"enabled\": true\n" + " },\n" + " \"cherrypick\": {\n" + " \"method\": \"POST\", \n" + " \"label\": \"Cherry Pick\", \n" + " \"title\": \"Cherry pick change to a different branch\",\n" + " \"enabled\": false\n" + " }\n" + " }")); runFeedback(ImmutableList.<String>builder() .add("res = ctx.destination.get_actions('12345', 'sha1').get(\"submit\")") .addAll(checkFieldStarLark("res", "label", "'Submit'")) .addAll(checkFieldStarLark("res", "enabled", "True")) .build()); } @Test public void testParsingEmptyUrl() { skylark.evalFails("git.gerrit_api(url = '')", "Invalid empty field 'url'"); } @Test public void testOriginRef() throws Exception { runFeedback(ImmutableList.<String>builder() .add("res = ctx.destination.new_origin_ref('12345')") .addAll(checkFieldStarLark("res", "ref", "'12345'")) .build()); } /** * A test that uses feedback. * * <p>Does not verify all the fields, see {@link #testGetChangeExhaustive()} ()} for that. */ @Test public void testFeedbackGetChange() throws Exception { mockForTest(); Feedback feedback = notifyChangeToOriginFeedback(); feedback.run(workdir, ImmutableList.of("12345")); assertThat(dummyTrigger.messages) .containsAtLeastElementsIn(ImmutableList.of("Change number 12345")); } @Test public void testFeedbackGetChange_malformedJson() throws Exception { gitUtil.mockApi(anyString(), anyString(), mockResponse("foo bar")); Feedback feedback = notifyChangeToOriginFeedback(); ValidationException expected = assertThrows( ValidationException.class, () -> feedback.run(workdir, ImmutableList.of("12345"))); assertThat(expected) .hasMessageThat() .contains("Error while executing the skylark transformation test_action"); Throwable cause = expected.getCause(); assertThat(cause).isInstanceOf(IllegalArgumentException.class); assertThat(dummyTrigger.messages).isEmpty(); } /** * An exhaustive test that evaluates each field of the change object. */ @Test public void testGetChangeExhaustive() throws Exception { gitUtil.mockApi( eq("GET"), startsWith(BASE_URL + "/changes/"), mockResponse( "{\n" + " 'id': 'copybara-project~Ie39b6e2c0c6e5ef8839013360bba38238c6ecfcd',\n" + " 'project': 'copybara-project',\n" + " 'branch': 'master',\n" + " 'topic': 'test_topic',\n" + " 'hashtags': [],\n" + " 'change_id': 'Ie39b6e2c0c6e5ef8839013360bba38238c6ecfcd',\n" + " 'subject': 'JUST A TEST',\n" + " 'status': 'NEW',\n" + " 'created': '2017-12-01 17:33:30.000000000',\n" + " 'updated': '2017-12-02 17:33:30.000000000',\n" + " 'submitted': '2017-12-03 17:33:30.000000000',\n" + " 'submit_type': 'MERGE_IF_NECESSARY',\n" + " 'submittable': true,\n" + " 'insertions': 2,\n" + " 'deletions': 10,\n" + " 'unresolved_comment_count': 0,\n" + " 'has_review_started': true,\n" + " '_number': 1082,\n" + " 'owner': {\n" + " '_account_id': 12345,\n" + " 'name': 'Glorious Copybara',\n" + " 'email': '[email protected]',\n" + " 'secondary_emails': ['[email protected]'],\n" + " 'username': 'glorious.copybara'\n" + " },\n" + " 'labels': {\n" + " 'Code-Review': {\n" + " 'all': [\n" + " {\n" + " 'value': 2,\n" + " 'date': '2017-01-01 12:00:00.000000000',\n" + " 'permitted_voting_range': {\n" + " 'min': 2,\n" + " 'max': 2\n" + " },\n" + " '_account_id': 123456\n" + " },\n" + " {\n" + " 'value': 0,\n" + " '_account_id': 123456\n" + " },\n" + " {\n" + " 'value': 0,\n" + " '_account_id': 123456\n" + " }\n" + " ],\n" + " 'values': {\n" + " '-2': 'Do not submit',\n" + " '-1': 'I would prefer that you didn\\u0027t submit this',\n" + " ' 0': 'No score',\n" + " '+1': 'Looks good to me, but someone else must approve',\n" + " '+2': 'Looks good to me, approved'\n" + " },\n" + " 'default_value': 0\n" + " }\n" + "},\n" + " 'current_revision': 'foo',\n" + " 'revisions': {\n" + " 'foo': {\n" + " 'kind': 'REWORK',\n" + " '_number': 1,\n" + " 'created': '2017-12-07 19:11:59.000000000',\n" + " 'uploader': {\n" + " '_account_id': 12345\n" + " },\n" + " 'ref': 'refs/changes/11/11111/1',\n" + " 'fetch': {\n" + " 'https': {\n" + " 'url': 'https://foo.bar/copybara/test',\n" + " 'ref': 'refs/changes/11/11111/1'\n" + " }\n" + " },\n" + " 'commit': {\n" + " 'parents': [\n" + " {\n" + " 'commit': 'e6b7772add9d2137fd5f879192bd249dfc4d0a00',\n" + " 'subject': 'Parent commit description.'\n" + " }\n" + " ],\n" + " 'author': {\n" + " 'name': 'Glorious Copybara',\n" + " 'email': '[email protected]',\n" + " 'date': '2017-12-01 00:00:00.000000000',\n" + " 'tz': -480\n" + " },\n" + " 'committer': {\n" + " 'name': 'Glorious Copybara',\n" + " 'email': '[email protected]',\n" + " 'date': '2017-12-01 00:00:00.000000000',\n" + " 'tz': -480\n" + " },\n" + " 'subject': 'JUST A TEST',\n" + " 'message': 'JUST A TEST\\n\\nSecond line of description.\n'\n" + " }\n" + " }\n" + " },\n" + " 'messages': [\n" + " {\n" + " 'id': 'e6aa8a323fd948cc9986dd4d8b4c253487bab253',\n" + " 'tag': 'autogenerated:gerrit:newPatchSet',\n" + " 'author': {\n" + " '_account_id': 12345,\n" + " 'name': 'Glorious Copybara',\n" + " 'email': '[email protected]'\n" + " },\n" + " 'real_author': {\n" + " '_account_id': 12345,\n" + " 'name': 'Glorious Copybara',\n" + " 'email': '[email protected]'\n" + " },\n" + " 'date': '2017-12-01 00:00:00.000000000',\n" + " 'message': 'Uploaded patch set 1.',\n" + " '_revision_number': 1\n" + " }\n" + " ]\n" + "}\n")); runFeedback(ImmutableList.<String>builder() .add("res = ctx.destination.get_change('12345', include_results = ['LABELS'])") .addAll(checkFieldStarLark("res", "id", "'copybara-project~Ie39b6e2c0c6e5ef8839013360bba38238c6ecfcd'")) .addAll(checkFieldStarLark("res", "project", "'copybara-project'")) .addAll(checkFieldStarLark("res", "branch", "'master'")) .addAll(checkFieldStarLark("res", "topic", "'test_topic'")) .addAll(checkFieldStarLark("res", "change_id", "'Ie39b6e2c0c6e5ef8839013360bba38238c6ecfcd'")) .addAll(checkFieldStarLark("res", "subject", "'JUST A TEST'")) .addAll(checkFieldStarLark("res", "status", "'NEW'")) .addAll(checkFieldStarLark("res", "created", "'2017-12-01 17:33:30.000000000'")) .addAll(checkFieldStarLark("res", "updated", "'2017-12-02 17:33:30.000000000'")) .addAll(checkFieldStarLark("res", "submitted", "'2017-12-03 17:33:30.000000000'")) .addAll(checkFieldStarLark("res", "submittable", "True")) .addAll(checkFieldStarLark("res", "current_revision", "'foo'")) .addAll(checkFieldStarLark("res", "owner.account_id", "'12345'")) .addAll(checkFieldStarLark("res", "owner.name", "'Glorious Copybara'")) .addAll(checkFieldStarLark("res", "owner.email", "'[email protected]'")) .addAll(checkFieldStarLark("res", "owner.secondary_emails[0]", "'[email protected]'")) .addAll(checkFieldStarLark("res", "owner.username", "'glorious.copybara'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].approved", "None")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].recommended", "None")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].disliked", "None")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].blocking", "False")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].value", "0")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].default_value", "0")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].values['-2']", "'Do not submit'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].values['-1']", "'I would prefer that you didn\\'t submit this'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].values[' 0']", "'No score'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].values['+1']", "'Looks good to me, but someone else must approve'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].values['+2']", "'Looks good to me, approved'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].all[0].value", "2")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].all[0].date", "'2017-01-01 12:00:00.000000000'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].all[0].account_id", "'123456'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].all[1].value", "0")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].all[1].account_id", "'123456'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].all[2].value", "0")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].all[2].account_id", "'123456'")) .addAll(checkFieldStarLark("res", "messages[0].id", "'e6aa8a323fd948cc9986dd4d8b4c253487bab253'")) .addAll(checkFieldStarLark("res", "messages[0].tag", "'autogenerated:gerrit:newPatchSet'")) .addAll(checkFieldStarLark("res", "messages[0].author.account_id", "'12345'")) .addAll(checkFieldStarLark("res", "messages[0].author.name", "'Glorious Copybara'")) .addAll(checkFieldStarLark("res", "messages[0].author.email", "'[email protected]'")) .addAll(checkFieldStarLark("res", "messages[0].date", "'2017-12-01 00:00:00.000000000'")) .addAll(checkFieldStarLark("res", "messages[0].message", "'Uploaded patch set 1.'")) .addAll(checkFieldStarLark("res", "messages[0].revision_number", "1")) .addAll(checkFieldStarLark("res", "revisions['foo'].kind", "'REWORK'")) .addAll(checkFieldStarLark("res", "revisions['foo'].patchset_number", "1")) .addAll(checkFieldStarLark("res", "revisions['foo'].created", "'2017-12-07 19:11:59.000000000'")) .addAll(checkFieldStarLark("res", "revisions['foo'].uploader.account_id", "'12345'")) .addAll(checkFieldStarLark("res", "revisions['foo'].ref", "'refs/changes/11/11111/1'")) .addAll(checkFieldStarLark("res", "revisions['foo'].commit.author.name", "'Glorious Copybara'")) .addAll(checkFieldStarLark("res", "revisions['foo'].commit.author.email", "'[email protected]'")) .addAll(checkFieldStarLark("res", "revisions['foo'].commit.author.date", "'2017-12-01 00:00:00.000000000'")) .addAll(checkFieldStarLark("res", "revisions['foo'].commit.committer.name", "'Glorious Copybara'")) .addAll(checkFieldStarLark("res", "revisions['foo'].commit.committer.email", "'[email protected]'")) .addAll(checkFieldStarLark("res", "revisions['foo'].commit.committer.date", "'2017-12-01 00:00:00.000000000'")) .addAll(checkFieldStarLark("res", "revisions['foo'].commit.subject", "'JUST A TEST'")) .addAll(checkFieldStarLark("res", "revisions['foo'].commit.message", "'JUST A TEST\\n\\nSecond line of description.\\n'")) .addAll(checkFieldStarLark("res", "labels['Code-Review'].value", "0")) .build()); } @Test public void testGetChangePaginationNotSupported() throws IOException { gitUtil.mockApi( eq("GET"), startsWith(BASE_URL + "/changes/"), mockResponse("{" + " id : '12345'," + " _more_changes : true" + "}")); ValidationException expected = assertThrows(ValidationException.class, () -> runFeedback( ImmutableList.of("ctx.destination.get_change('12345', include_results = ['LABELS'])"))); assertThat(expected).hasMessageThat().contains("Pagination is not supported yet."); } @Test public void testPostLabel() throws Exception { mockForTest(); runFeedback(ImmutableList.<String>builder() .add("res = ctx.destination" + ".post_review('12345', 'sha1', git.review_input({'Code-Review': 1}, 'foooo'))") .addAll(checkFieldStarLark("res", "labels", "{'Code-Review': 1}")) .build()); } @Test public void testPostTag() throws Exception { gitUtil.mockApi( eq("POST"), matches(BASE_URL + "/changes/.*/revisions/.*/review"), mockResponse(postLabel())); gitUtil.mockApi(eq("POST"), contains("/changes/12345/revisions/sha1/review"), mockResponseAndValidateRequest( postLabel(), MockRequestAssertion.contains("\"tag\":\"tag:me\""))); runFeedback(ImmutableList.<String>builder() .add("res = ctx.destination" + ".post_review('12345', 'sha1', git.review_input({'Code-Review': 1}, tag='tag:me'))") .build()); } @Test public void deleteVote() throws Exception { AtomicBoolean called = new AtomicBoolean(false); gitUtil.mockApi( eq("POST"), startsWith(BASE_URL + "/changes/12345/reviewers/me/votes/Code-Review/delete"), mockResponseWithStatus("", 204, new MockRequestAssertion("Always true with side-effect", s -> { called.set(true); return true; }))); runFeedback(ImmutableList.<String>builder() .add("ctx.destination" + ".delete_vote('12345', 'me', 'Code-Review')") .build()); assertThat(called.get()).isTrue(); } @Test public void testPostLabel_errorCreatesVe() throws Exception { mockForTest(); gitUtil.mockApi( eq("POST"), matches(BASE_URL + "/changes/.*/revisions/.*/review"), mockResponseWithStatus( "\n\nApplying label \"Verified\": -1 is restricted.", 403)); ValidationException expected = assertThrows(ValidationException.class, () -> runFeedback(ImmutableList.of("ctx.destination.post_review(" + "'12345', 'sha1', git.review_input({'Code-Review': 1}, 'foooo'))"))); assertThat(expected).hasMessageThat().contains("Gerrit returned a permission error"); } @Test public void testListChangesByCommit() throws Exception { mockForTest(); runFeedback(ImmutableList.<String>builder() .add("res = ctx.destination" + ".list_changes_by_commit('7956f527ec8a23ebba9c3ebbcf88787aa3411425')") .addAll(checkFieldStarLark("res[0]", "id", "'copybara-team%2Fcopybara~master~I85dd4ea583ac218d9480eefb12ff2c83ce0bce61'")) .build()); } @Test public void testListChangesByCommit_withIncludeResults() throws Exception { mockForTest(); runFeedback(ImmutableList.<String>builder() .add("res = ctx.destination" + ".list_changes_by_commit('7956f527ec8a23ebba9c3ebbcf88787aa3411425'," + " include_results = ['LABELS', 'MESSAGES'])") .addAll(checkFieldStarLark("res[0]", "id", "'copybara-team%2Fcopybara~master~I16e447bb2bb51952021ec3ea50991d923dcbbf58'")) .build()); } private Feedback notifyChangeToOriginFeedback() throws IOException, ValidationException { return feedback( "" + "def test_action(ctx):\n" + " c = ctx.destination.get_change(ctx.refs[0], include_results = ['LABELS'])\n" + " if c != None and c.id != None:\n" + " ctx.origin.message('Change number ' + str(c.id))\n" + " return ctx.success()\n" + "\n"); } private Feedback feedback(String actionFunction) throws IOException, ValidationException { String config = actionFunction + "\n" + "core.feedback(\n" + " name = 'default',\n" + " origin = testing.dummy_trigger(),\n" + " destination = git.gerrit_api(url = '" + url + "'),\n" + " actions = [test_action,],\n" + ")\n" + "\n"; System.err.println(config); return (Feedback) skylark.loadConfig(config).getMigration("default"); } private void mockForTest() throws IOException { gitUtil.mockApi( eq("GET"), startsWith(BASE_URL + "/changes/?q="), mockResponse( "[{\"id\":" + " \"copybara-team%2Fcopybara~master~I85dd4ea583ac218d9480eefb12ff2c83ce0bce61\"" + "}]")); gitUtil.mockApi( "GET", BASE_URL + "/changes/?q=commit:7956f527ec8a23ebba9c3ebbcf88787aa3411425" + "&o=LABELS&o=MESSAGES", mockResponse( "[{" + "\"id\":" + " \"copybara-team%2Fcopybara~master~I16e447bb2bb51952021ec3ea50991d923dcbbf58\"" + "}]")); gitUtil.mockApi( eq("GET"), matches(BASE_URL + "/changes/[0-9]+.*"), mockResponse("" + "{" + " id : \"12345\"," + " status : \"NEW\"" + "}")); gitUtil.mockApi( eq("POST"), matches(BASE_URL + "/changes/.*/revisions/.*/review"), mockResponse(postLabel())); } private static ImmutableList<String> checkFieldStarLark(String var, String field, String value) { return ImmutableList.of( String.format("if %s.%s != %s:", var, field, value), String.format(" fail('unexpected value for %1$s.%3$s: ' + str(%1$s.%2$s))", var, field, field.replace("'", "\\'"))); } private void runFeedback(ImmutableList<String> funBody) throws Exception { Feedback test = feedback("def test_action(ctx):\n" + funBody.stream().map(s -> " " + s).collect(Collectors.joining("\n")) + "\n return ctx.success()\n"); test.run(workdir, ImmutableList.of("e597746de9c1704e648ddc3ffa0d2096b146d600")); } private String postLabel() { Map<String, Object> result = new LinkedHashMap<>(); result.put("labels", ImmutableMap.of("Code-Review", 1)); try { return GsonFactory.getDefaultInstance().toPrettyString(result); } catch (IOException e) { throw new RuntimeException(e); } } }