/* * SonarQube * Copyright (C) 2009-2017 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarqube.ws.client; import com.google.common.base.Joiner; import com.google.protobuf.Parser; import java.util.ArrayList; import java.util.List; import java.util.Objects; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assertions; import org.assertj.core.data.MapEntry; import org.junit.rules.ExternalResource; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; /** * Convenient rule to test a subclass of {@link BaseService}. * * <p> * Declaration sample: * <pre> * {@literal @}Rule * public ServiceTester<PermissionsService> serviceTester = new ServiceTester<>(new PermissionsService(mock(WsConnector.class))); * * private PermissionsService underTest = serviceTester.getInstanceUnderTest(); * </pre> * </p> * * <p> * Method {@link #getInstanceUnderTest()} will return an instance of the class under test which will be instrumented * and will allow recording internal calls to {@link BaseService#call(BaseRequest, Parser)} and * {@link BaseService#call(WsRequest)}. * </p> * <p> * Argument of calls to these method will be logged and can be accessed through {@link #getGetCalls()}, {@link #getPostCalls()} * and {@link #getRawCalls()} depending on whether they are made respectively with {@link GetRequest}, {@link PostRequest} * or other subclass of {@link BaseRequest}. * </p> * <p> * For convenience, when one is testing a single Ws call, on case use {@link #getGetRequest()} (and its associated * {@link #getGetParser()}) or {@link #getPostRequest()} (and its associated {@link #getPostParser()}). Those three * method will make the appropriate assertions assuming that only a single GET (or POST) request has been made. * </p> * <p> * Last but not least, to easily verify the content of a {@link GetRequest} (or a {@link PostRequest}), one can use * methods {@link #assertThat(GetRequest)} (or {@link #assertThat(PostRequest)}) to write assertions on a * {@link GetRequest} (or {@link PostRequest}) returned by methods of this Rule. * </p> * * <p> * Assertion usage sample: * <pre> * PostRequest postRequest = serviceTester.getPostRequest(); * serviceTester.assertThat(postRequest) * .hasPath("add_group") * .hasParam(PARAM_PERMISSION, PERMISSION_VALUE) * .hasParam(PARAM_PROJECT_ID, PROJECT_ID_VALUE) * .hasParam(PARAM_PROJECT_KEY, PROJECT_KEY_VALUE) * .hasParam(PARAM_GROUP_ID, GROUP_ID_VALUE) * .hasParam(PARAM_GROUP_NAME, GROUP_NAME_VALUE) * .andNoOtherParam(); * </pre> * </p> * */ public class ServiceTester<T extends BaseService> extends ExternalResource { private static final Joiner COMMA_JOINER = Joiner.on(","); private final T underTest; private final List<GetCall> getCalls = new ArrayList<>(); private final List<PostCall> postCalls = new ArrayList<>(); private final List<RawCall> rawCalls = new ArrayList<>(); /** * @param underTestInstance an instance of the type to test. Use {@link #getInstanceUnderTest()} to retrieve the * instrumented instance to use in your test. */ public ServiceTester(T underTestInstance) { this.underTest = spy(underTestInstance); } @Override protected void before() throws Throwable { Answer<Object> answer = new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object[] arguments = invocation.getArguments(); Object request = arguments[0]; Parser<?> parser = arguments.length == 2 ? (Parser<?>) arguments[1] : null; if (request instanceof PostRequest) { postCalls.add(new PostCall((PostRequest) request, parser)); } else if (request instanceof GetRequest) { getCalls.add(new GetCall((GetRequest) request, parser)); } else { rawCalls.add(new RawCall((WsRequest) request)); } return null; } }; doAnswer(answer).when(this.underTest).call(any(GetRequest.class), any(Parser.class)); doAnswer(answer).when(this.underTest).call(any(WsRequest.class)); } @Override protected void after() { this.getCalls.clear(); } public T getInstanceUnderTest() { return underTest; } public List<GetCall> getGetCalls() { return getCalls; } @CheckForNull public GetRequest getGetRequest() { assertSingleGetCall(); return getCalls.iterator().next().getRequest(); } public RequestAssert<GetRequest> assertThat(GetRequest getRequest) { return new RequestAssert<>(getRequest); } public RequestAssert<PostRequest> assertThat(PostRequest postRequest) { return new RequestAssert<>(postRequest); } @CheckForNull public Parser<?> getGetParser() { assertSingleGetCall(); return getCalls.iterator().next().getParser(); } public List<PostCall> getPostCalls() { return postCalls; } public PostRequest getPostRequest() { assertSinglePostCall(); return postCalls.iterator().next().getRequest(); } @CheckForNull public Parser<?> getPostParser() { assertSinglePostCall(); return postCalls.iterator().next().getParser(); } private void assertSingleGetCall() { Assertions.assertThat(getCalls).hasSize(1); Assertions.assertThat(postCalls).isEmpty(); Assertions.assertThat(rawCalls).isEmpty(); } private void assertSinglePostCall() { Assertions.assertThat(postCalls).hasSize(1); Assertions.assertThat(getRawCalls()).isEmpty(); Assertions.assertThat(rawCalls).isEmpty(); } public List<RawCall> getRawCalls() { return rawCalls; } @Immutable public static final class GetCall extends CallWithParser<GetRequest> { public GetCall(GetRequest getRequest, @Nullable Parser<?> parser) { super(getRequest, parser); } } @Immutable public static final class PostCall extends CallWithParser<PostRequest> { public PostCall(PostRequest postRequest, @Nullable Parser<?> parser) { super(postRequest, parser); } } @Immutable public static final class RawCall { private final WsRequest wsRequest; public RawCall(WsRequest wsRequest) { this.wsRequest = wsRequest; } public WsRequest getWsRequest() { return wsRequest; } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } RawCall rawCalls = (RawCall) o; return Objects.equals(wsRequest, rawCalls.wsRequest); } @Override public int hashCode() { return Objects.hash(wsRequest); } } public static abstract class CallWithParser<T extends BaseRequest<T>> { private final T request; private final Parser<?> parser; public CallWithParser(T request, @Nullable Parser<?> parser) { this.request = request; this.parser = parser; } public T getRequest() { return request; } public Parser<?> getParser() { return parser; } @Override public boolean equals(@Nullable Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } CallWithParser getCall = (CallWithParser) o; return Objects.equals(request, getCall.request) && Objects.equals(parser, getCall.request); } @Override public int hashCode() { return Objects.hash(request, parser); } } public final class RequestAssert<T extends BaseRequest<T>> extends AbstractAssert<RequestAssert<T>, BaseRequest<T>> { private final List<MapEntry<String, String>> assertedParams = new ArrayList<>(); protected RequestAssert(T actual) { super(actual, RequestAssert.class); } public RequestAssert hasPath(String path) { isNotNull(); String expectedPath = underTest.controller + "/" + path; if (!Objects.equals(actual.getPath(), expectedPath)) { failWithMessage("Expected path to be <%s> but was <%s>", expectedPath, actual.getPath()); } return this; } public RequestAssert hasParam(String key, String value) { isNotNull(); MapEntry<String, String> entry = MapEntry.entry(key, value); Assertions.assertThat(actual.getParams()).contains(entry); this.assertedParams.add(entry); return this; } public RequestAssert hasParam(String key, int value) { isNotNull(); MapEntry<String, String> entry = MapEntry.entry(key, String.valueOf(value)); Assertions.assertThat(actual.getParams()).contains(entry); this.assertedParams.add(entry); return this; } public RequestAssert hasParam(String key, boolean value) { isNotNull(); MapEntry<String, String> entry = MapEntry.entry(key, String.valueOf(value)); Assertions.assertThat(actual.getParams()).contains(entry); this.assertedParams.add(entry); return this; } public RequestAssert hasParam(String key, List<String> values) { isNotNull(); MapEntry<String, String> entry = MapEntry.entry(key, values.toString()); Assertions.assertThat(actual.getParameters().getValues(key)).containsExactly(values.toArray(new String[0])); this.assertedParams.add(entry); return this; } public RequestAssert andNoOtherParam() { isNotNull(); Assertions.assertThat(actual.getParams()).hasSize(assertedParams.size()); return this; } } }