| // Copyright (C) 2020 The Android Open Source Project |
| // |
| // 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.gerrit.acceptance.rest; |
| |
| import static com.google.common.net.HttpHeaders.ORIGIN; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.httpd.restapi.RestApiServlet.X_GERRIT_UPDATED_REF; |
| import static com.google.gerrit.httpd.restapi.RestApiServlet.X_GERRIT_UPDATED_REF_ENABLED; |
| import static org.apache.http.HttpStatus.SC_OK; |
| |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.PushOneCommit.Result; |
| import com.google.gerrit.acceptance.RestResponse; |
| import com.google.gerrit.acceptance.config.GerritConfig; |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.client.ChangeStatus; |
| import com.google.gerrit.extensions.restapi.Url; |
| import com.google.gerrit.httpd.restapi.ParameterParser; |
| import com.google.gerrit.httpd.restapi.RestApiServlet; |
| import com.google.inject.Inject; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.regex.Pattern; |
| import org.apache.http.message.BasicHeader; |
| import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; |
| import org.eclipse.jgit.junit.TestRepository; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.junit.Test; |
| |
| public class RestApiServletIT extends AbstractDaemonTest { |
| private static String ANY_REST_API = "/accounts/self/capabilities"; |
| private static BasicHeader ACCEPT_STAR_HEADER = new BasicHeader("Accept", "*/*"); |
| private static BasicHeader X_GERRIT_UPDATED_REF_ENABLED_HEADER = |
| new BasicHeader(X_GERRIT_UPDATED_REF_ENABLED, "true"); |
| private static Pattern ANY_SPACE = Pattern.compile("\\s"); |
| |
| @Inject private ProjectOperations projectOperations; |
| |
| @Test |
| public void restResponseBodyShouldBeCompactWithoutSpaces() throws Exception { |
| RestResponse response = adminRestSession.getWithHeaders(ANY_REST_API, ACCEPT_STAR_HEADER); |
| assertThat(response.getStatusCode()).isEqualTo(SC_OK); |
| |
| assertThat(contentWithoutMagicJson(response)).doesNotContainMatch(ANY_SPACE); |
| } |
| |
| @Test |
| public void restResponseBodyShouldBeCompactWithoutSpacesWhenPPIsZero() throws Exception { |
| assertThat(contentWithoutMagicJson(prettyJsonRestResponse("prettyPrint", 0))) |
| .doesNotContainMatch(ANY_SPACE); |
| } |
| |
| @Test |
| public void restResponseBodyShouldBeCompactWithoutSpacesWhenPrerryPrintIsZero() throws Exception { |
| assertThat(contentWithoutMagicJson(prettyJsonRestResponse("pp", 0))) |
| .doesNotContainMatch(ANY_SPACE); |
| } |
| |
| @Test |
| public void restResponseBodyShouldBePrettyfiedWhenPPIsOne() throws Exception { |
| assertThat(contentWithoutMagicJson(prettyJsonRestResponse("pp", 1))).containsMatch(ANY_SPACE); |
| } |
| |
| @Test |
| public void restResponseBodyShouldBePrettyfiedWhenPrettyPrintIsOne() throws Exception { |
| assertThat(contentWithoutMagicJson(prettyJsonRestResponse("prettyPrint", 1))) |
| .containsMatch(ANY_SPACE); |
| } |
| |
| @Test |
| public void experimentRequestParamIsReserved() throws Exception { |
| assertRestResponseWithParameters(SC_OK, ParameterParser.EXPERIMENT_PARAMETER, "exp1"); |
| } |
| |
| @Test |
| public void xGerritUpdatedRefNotSetByDefault() throws Exception { |
| Result change = createChange(); |
| String origin = adminRestSession.url(); |
| |
| RestResponse response = |
| adminRestSession.putWithHeaders( |
| "/changes/" + change.getChangeId() + "/topic", |
| /* content= */ "A", |
| new BasicHeader(ORIGIN, origin)); |
| response.assertOK(); |
| assertThat(gApi.changes().id(change.getChangeId()).topic()).isEqualTo("A"); |
| |
| // Meta ref updated because of topic update, but updated refs are not set by default. |
| assertThat(response.getHeader(X_GERRIT_UPDATED_REF)).isNull(); |
| } |
| |
| @Test |
| public void xGerritUpdatedRefNotSetWhenUpdatedRefNotEnabled() throws Exception { |
| Result change = createChange(); |
| String origin = adminRestSession.url(); |
| |
| RestResponse response = |
| adminRestSession.putWithHeaders( |
| "/changes/" + change.getChangeId() + "/topic", |
| /* content= */ "A", |
| new BasicHeader(ORIGIN, origin), |
| new BasicHeader(X_GERRIT_UPDATED_REF_ENABLED, "false")); |
| response.assertOK(); |
| assertThat(gApi.changes().id(change.getChangeId()).topic()).isEqualTo("A"); |
| |
| // Meta ref updated because of topic update, but updated refs are not enabled. |
| assertThat(response.getHeader(X_GERRIT_UPDATED_REF)).isNull(); |
| } |
| |
| @Test |
| public void xGerritUpdatedRefNotSetForReadRequests() throws Exception { |
| RestResponse response = |
| adminRestSession.getWithHeaders( |
| ANY_REST_API, ACCEPT_STAR_HEADER, X_GERRIT_UPDATED_REF_ENABLED_HEADER); |
| assertThat(response.getStatusCode()).isEqualTo(SC_OK); |
| assertThat(response.getHeader(X_GERRIT_UPDATED_REF)).isNull(); |
| } |
| |
| @Test |
| public void xGerritUpdatedRefSetForDifferentWriteRequests() throws Exception { |
| Result change = createChange(); |
| String origin = adminRestSession.url(); |
| String project = change.getChange().project().get(); |
| String metaRef = RefNames.changeMetaRef(change.getChange().getId()); |
| |
| ObjectId originalMetaRefSha1 = getMetaRefSha1(change); |
| |
| RestResponse response = |
| adminRestSession.putWithHeaders( |
| "/changes/" + change.getChangeId() + "/topic", |
| /* content= */ "A", |
| new BasicHeader(ORIGIN, origin), |
| X_GERRIT_UPDATED_REF_ENABLED_HEADER); |
| response.assertOK(); |
| assertThat(gApi.changes().id(change.getChangeId()).topic()).isEqualTo("A"); |
| ObjectId firstMetaRefSha1 = getMetaRefSha1(change); |
| |
| // Meta ref updated because of topic update. |
| assertThat(response.getHeader(X_GERRIT_UPDATED_REF)) |
| .isEqualTo( |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project), |
| Url.encode(metaRef), |
| originalMetaRefSha1.getName(), |
| firstMetaRefSha1.getName())); |
| |
| response = |
| adminRestSession.putWithHeaders( |
| "/changes/" + change.getChangeId() + "/topic", |
| /* content= */ "B", |
| new BasicHeader(ORIGIN, origin), |
| X_GERRIT_UPDATED_REF_ENABLED_HEADER); |
| response.assertOK(); |
| assertThat(gApi.changes().id(change.getChangeId()).topic()).isEqualTo("B"); |
| |
| ObjectId secondMetaRefSha1 = getMetaRefSha1(change); |
| |
| // Meta ref updated again because of another topic update. |
| assertThat(response.getHeader(X_GERRIT_UPDATED_REF)) |
| .isEqualTo( |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project), |
| Url.encode(metaRef), |
| firstMetaRefSha1.getName(), |
| secondMetaRefSha1.getName())); |
| |
| // Ensure the meta ref SHA-1 changed for the project~metaRef which means we return different |
| // X-Gerrit-UpdatedRef headers. |
| assertThat(secondMetaRefSha1).isNotEqualTo(firstMetaRefSha1); |
| } |
| |
| @Test |
| public void xGerritUpdatedRefDeleted() throws Exception { |
| Result change = createChange(); |
| String project = change.getChange().project().get(); |
| String metaRef = RefNames.changeMetaRef(change.getChange().getId()); |
| String patchSetRef = RefNames.patchSetRef(change.getPatchSetId()); |
| |
| ObjectId originalMetaRefSha1 = getMetaRefSha1(change); |
| ObjectId originalchangeRefSha1 = change.getCommit().getId(); |
| |
| RestResponse response = |
| adminRestSession.deleteWithHeaders( |
| "/changes/" + change.getChangeId(), X_GERRIT_UPDATED_REF_ENABLED_HEADER); |
| response.assertNoContent(); |
| |
| List<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF); |
| |
| // The change was deleted, so the refs were deleted which means they are ObjectId.zeroId(). |
| assertThat(headers) |
| .containsExactly( |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project), |
| Url.encode(metaRef), |
| originalMetaRefSha1.getName(), |
| ObjectId.zeroId().getName()), |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project), |
| Url.encode(patchSetRef), |
| originalchangeRefSha1.getName(), |
| ObjectId.zeroId().getName())); |
| } |
| |
| @Test |
| public void xGerritUpdatedRefWithProjectNameContainingTilde() throws Exception { |
| Project.NameKey project = createProjectOverAPI("~~pr~oje~ct~~~~", null, true, null); |
| Result change = createChange(cloneProject(project, admin)); |
| String metaRef = RefNames.changeMetaRef(change.getChange().getId()); |
| String patchSetRef = RefNames.patchSetRef(change.getPatchSetId()); |
| |
| ObjectId originalMetaRefSha1 = getMetaRefSha1(change); |
| ObjectId originalchangeRefSha1 = change.getCommit().getId(); |
| |
| RestResponse response = |
| adminRestSession.deleteWithHeaders( |
| "/changes/" + change.getChangeId(), X_GERRIT_UPDATED_REF_ENABLED_HEADER); |
| response.assertNoContent(); |
| |
| List<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF); |
| |
| // The change was deleted, so the refs were deleted which means they are ObjectId.zeroId(). |
| assertThat(headers) |
| .containsExactly( |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project.get()), |
| Url.encode(metaRef), |
| originalMetaRefSha1.getName(), |
| ObjectId.zeroId().getName()), |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project.get()), |
| Url.encode(patchSetRef), |
| originalchangeRefSha1.getName(), |
| ObjectId.zeroId().getName())); |
| |
| // Ensures ~ gets encoded to %7E. |
| assertThat(Url.encode(project.get())).endsWith("%7E%7Epr%7Eoje%7Ect%7E%7E%7E%7E"); |
| } |
| |
| @Test |
| public void xGerritUpdatedRefSetMultipleHeadersForSubmit() throws Exception { |
| Result change1 = createChange(); |
| Result change2 = createChange(); |
| String metaRef1 = RefNames.changeMetaRef(change1.getChange().getId()); |
| String metaRef2 = RefNames.changeMetaRef(change2.getChange().getId()); |
| |
| gApi.changes().id(change1.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(change2.getChangeId()).current().review(ReviewInput.approve()); |
| |
| Project.NameKey project = change1.getChange().project(); |
| |
| try (Repository repository = repoManager.openRepository(project)) { |
| ObjectId originalFirstMetaRefSha1 = getMetaRefSha1(change1); |
| ObjectId originalSecondMetaRefSha1 = getMetaRefSha1(change2); |
| ObjectId originalDestinationBranchSha1 = |
| repository.resolve(change1.getChange().change().getDest().branch()); |
| |
| RestResponse response = |
| adminRestSession.postWithHeaders( |
| "/changes/" + change2.getChangeId() + "/submit", |
| /* content = */ null, |
| X_GERRIT_UPDATED_REF_ENABLED_HEADER); |
| response.assertOK(); |
| |
| ObjectId firstMetaRefSha1 = getMetaRefSha1(change1); |
| ObjectId secondMetaRefSha1 = getMetaRefSha1(change2); |
| |
| List<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF); |
| |
| String branch = change1.getChange().change().getDest().branch(); |
| String branchSha1 = |
| repository |
| .getRefDatabase() |
| .exactRef(change1.getChange().change().getDest().branch()) |
| .getObjectId() |
| .name(); |
| |
| // During submit, all relevant meta refs of the latest patchset are updated + the destination |
| // branch/es. |
| assertThat(headers) |
| .containsExactly( |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project.get()), |
| Url.encode(metaRef1), |
| originalFirstMetaRefSha1.getName(), |
| firstMetaRefSha1.getName()), |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project.get()), |
| Url.encode(metaRef2), |
| originalSecondMetaRefSha1.getName(), |
| secondMetaRefSha1.getName()), |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project.get()), |
| Url.encode(branch), |
| originalDestinationBranchSha1.getName(), |
| branchSha1)); |
| } |
| } |
| |
| @Test |
| @GerritConfig(name = "change.submitWholeTopic", value = "true") |
| public void xGerritUpdatedRefSetMultipleHeadersForSubmitTopic() throws Exception { |
| String secondProject = "secondProject"; |
| projectOperations.newProject().name(secondProject).create(); |
| TestRepository<InMemoryRepository> secondRepo = |
| cloneProject(Project.nameKey("secondProject"), admin); |
| String topic = "topic"; |
| String branch = "refs/heads/master"; |
| Result change1 = createChange(testRepo, branch, "first change", "a.txt", "message", topic); |
| Result change2 = createChange(secondRepo, branch, "second change", "b.txt", "message", topic); |
| |
| String metaRef1 = RefNames.changeMetaRef(change1.getChange().getId()); |
| String metaRef2 = RefNames.changeMetaRef(change2.getChange().getId()); |
| |
| gApi.changes().id(change1.getChangeId()).current().review(ReviewInput.approve()); |
| gApi.changes().id(change2.getChangeId()).current().review(ReviewInput.approve()); |
| |
| Project.NameKey project1 = change1.getChange().project(); |
| Project.NameKey project2 = change2.getChange().project(); |
| |
| try (Repository repository1 = repoManager.openRepository(project1); |
| Repository repository2 = repoManager.openRepository(project2)) { |
| ObjectId originalFirstMetaRefSha1 = getMetaRefSha1(change1); |
| ObjectId originalSecondMetaRefSha1 = getMetaRefSha1(change2); |
| ObjectId originalDestinationBranchSha1Project1 = |
| repository1.resolve(change1.getChange().change().getDest().branch()); |
| ObjectId originalDestinationBranchSha1Project2 = |
| repository2.resolve(change2.getChange().change().getDest().branch()); |
| |
| RestResponse response = |
| adminRestSession.postWithHeaders( |
| "/changes/" + change2.getChangeId() + "/submit", |
| /* content = */ null, |
| X_GERRIT_UPDATED_REF_ENABLED_HEADER); |
| response.assertOK(); |
| assertThat(gApi.changes().id(change1.getChangeId()).get().status) |
| .isEqualTo(ChangeStatus.MERGED); |
| |
| ObjectId firstMetaRefSha1 = getMetaRefSha1(change1); |
| ObjectId secondMetaRefSha1 = getMetaRefSha1(change2); |
| |
| List<String> headers = response.getHeaders(X_GERRIT_UPDATED_REF); |
| |
| String branchSha1Project1 = |
| repository1 |
| .getRefDatabase() |
| .exactRef(change1.getChange().change().getDest().branch()) |
| .getObjectId() |
| .name(); |
| |
| String branchSha1Project2 = |
| repository2 |
| .getRefDatabase() |
| .exactRef(change2.getChange().change().getDest().branch()) |
| .getObjectId() |
| .name(); |
| |
| // During submit, all relevant meta refs of the latest patchset are updated + the destination |
| // branch/es. |
| // TODO(paiking): This doesn't work well for torn submissions: If the changes are in |
| // different projects in the same topic, and we tried to submit those changes together, it's |
| // possible that the first submission only submitted one of the changes, and then the retry |
| // submitted the other change. If that happens, when the user retries, they will not get the |
| // meta ref updates for the change that got submitted on the previous submission attempt. |
| // Ideally, submit should be idempotent and always return all meta refs on all submission |
| // attempts. |
| assertThat(headers) |
| .containsExactly( |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project1.get()), |
| Url.encode(metaRef1), |
| originalFirstMetaRefSha1.getName(), |
| firstMetaRefSha1.getName()), |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project2.get()), |
| Url.encode(metaRef2), |
| originalSecondMetaRefSha1.getName(), |
| secondMetaRefSha1.getName()), |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project1.get()), |
| Url.encode(branch), |
| originalDestinationBranchSha1Project1.getName(), |
| branchSha1Project1), |
| String.format( |
| "%s~%s~%s~%s", |
| Url.encode(project2.get()), |
| Url.encode(branch), |
| originalDestinationBranchSha1Project2.getName(), |
| branchSha1Project2)); |
| } |
| } |
| |
| @Test |
| public void requestsOnRootCollectionDontRequireTrailingSlash() throws Exception { |
| adminRestSession.get("/access").assertOK(); |
| adminRestSession.get("/accounts?q=is:active").assertOK(); |
| adminRestSession.get("/changes?q=status:open").assertOK(); |
| // GET on /config/ is not supported, hence we cannot test GET on /config |
| adminRestSession.get("/groups").assertOK(); |
| adminRestSession.get("/projects").assertOK(); |
| } |
| |
| @Test |
| public void testNumericChangeIdRedirectWithPrefix() throws Exception { |
| int changeNumber = createChange().getChange().getId().get(); |
| |
| String redirectUri = String.format("/c/%s/+/%d/", project.get(), changeNumber); |
| anonymousRestSession.get("/c/" + changeNumber).assertTemporaryRedirect(redirectUri); |
| } |
| |
| @Test |
| public void testCommentLinkWithPrefixRedirects() throws Exception { |
| int changeNumber = createChange().getChange().getId().get(); |
| String commentId = "ff3303fd_8341647b"; |
| |
| String redirectUri = |
| String.format("/c/%s/+/%d/comment/%s", project.get(), changeNumber, commentId); |
| |
| anonymousRestSession |
| .get(String.format("/c/%s/comment/%s", changeNumber, commentId)) |
| .assertTemporaryRedirect(redirectUri); |
| } |
| |
| @Test |
| public void testCommentLinkWithoutPrefixRedirects() throws Exception { |
| int changeNumber = createChange().getChange().getId().get(); |
| String commentId = "ff3303fd_8341647b"; |
| |
| String redirectUri = |
| String.format("/c/%s/+/%d/comment/%s", project.get(), changeNumber, commentId); |
| |
| anonymousRestSession |
| .get(String.format("/%s/comment/%s", changeNumber, commentId)) |
| .assertTemporaryRedirect(redirectUri); |
| } |
| |
| @Test |
| public void testNumericChangeIdRedirectWithoutPrefix() throws Exception { |
| int changeNumber = createChange().getChange().getId().get(); |
| |
| String redirectUri = String.format("/c/%s/+/%d/", project.get(), changeNumber); |
| anonymousRestSession.get("/" + changeNumber).assertTemporaryRedirect(redirectUri); |
| } |
| |
| private ObjectId getMetaRefSha1(Result change) { |
| return change.getChange().notes().getRevision(); |
| } |
| |
| private RestResponse assertRestResponseWithParameters(int status, String k, String v) |
| throws Exception { |
| RestResponse response = |
| adminRestSession.getWithHeaders(ANY_REST_API + "?" + k + "=" + v, ACCEPT_STAR_HEADER); |
| assertThat(response.getStatusCode()).isEqualTo(status); |
| |
| return response; |
| } |
| |
| private RestResponse prettyJsonRestResponse(String ppArgument, int ppValue) throws Exception { |
| RestResponse response = |
| adminRestSession.getWithHeaders( |
| ANY_REST_API + "?" + ppArgument + "=" + ppValue, ACCEPT_STAR_HEADER); |
| assertThat(response.getStatusCode()).isEqualTo(SC_OK); |
| |
| return response; |
| } |
| |
| private String contentWithoutMagicJson(RestResponse response) throws IOException { |
| return response.getEntityContent().substring(RestApiServlet.JSON_MAGIC.length); |
| } |
| } |