// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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.api.ads.adwords.lib.utils;
import static com.google.api.ads.adwords.lib.utils.AdHocReportDownloadHelperInterface.REPORT_CHARSET;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;
import com.google.api.ads.adwords.lib.client.AdWordsSession;
import com.google.api.ads.adwords.lib.utils.DetailedReportDownloadResponseException.Builder;
import com.google.api.ads.adwords.lib.utils.ReportRequest.RequestType;
import com.google.api.ads.adwords.lib.utils.testing.GenericAdWordsServices;
import com.google.api.ads.common.lib.testing.MockHttpIntegrationTest;
import com.google.api.ads.common.lib.testing.TestPortFinder;
import com.google.api.ads.common.lib.utils.Streams;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.common.collect.Lists;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import com.google.common.net.UrlEscapers;
import java.io.InputStream;
import java.net.ConnectException;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
/**
* Tests for {@link AdHocReportDownloadHelper}.
*/
@RunWith(Parameterized.class)
public class AdHocReportDownloadHelperTest extends MockHttpIntegrationTest {
/**
* Error XML for an invalid field name of "AdFormatt" (instead of "AdFormat").
*/
private static final String ERROR_XML =
"<reportDownloadError>"
+ "<ApiError><type>ReportDefinitionError.INVALID_FIELD_NAME_FOR_REPORT</type>"
+ "<trigger>AdFormatt</trigger><fieldPath>foobar</fieldPath></ApiError>"
+ "</reportDownloadError>";
private final boolean isTestRawDownload;
private AdHocReportDownloadHelper helper;
private GoogleCredential credential;
private Builder exceptionBuilder;
@Mock
private ReportRequest reportRequest;
@Rule
public ExpectedException thrown = ExpectedException.none();
/** Enum of download format that's not version-specific */
enum TestDownloadFormat {
CSV
}
@Parameters(name = "isTestRawDownload = {0}")
public static List<Object[]> parameters() {
return Lists.newArrayList(new Object[] {Boolean.TRUE}, new Object[] {Boolean.FALSE});
}
/**
* The {@code isTestRawDownload} parameter determines if the {@code testDownloadReport...}
* tests will test {@link AdHocReportDownloadHelper#downloadReport(ReportRequest)} (true) or
* {@link AdHocReportDownloadHelper#downloadReport(ReportRequest, Builder)} (false).
*/
public AdHocReportDownloadHelperTest(boolean isTestRawDownload) {
this.isTestRawDownload = isTestRawDownload;
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
Enum<?> downloadFormat = TestDownloadFormat.CSV;
Mockito.<Enum<?>>when(reportRequest.getDownloadFormat()).thenReturn(downloadFormat);
credential = new GoogleCredential.Builder().setTransport(new NetHttpTransport())
.setJsonFactory(new JacksonFactory()).build();
credential.setAccessToken("TEST_ACCESS_TOKEN");
AdWordsSession session = new AdWordsSession.Builder()
.withUserAgent("TEST_APP")
.withOAuth2Credential(credential)
.withEndpoint(testHttpServer.getServerUrl())
.withDeveloperToken("TEST_DEVELOPER_TOKEN")
.withClientCustomerId("TEST_CLIENT_CUSTOMER_ID")
.build();
helper =
new GenericAdWordsServices()
.getBootstrapper()
.getInstanceOf(session, AdHocReportDownloadHelper.class);
exceptionBuilder =
new DetailedReportDownloadResponseException.Builder() {
@Override
public DetailedReportDownloadResponseException build(int httpStatus, String errorText) {
return new DetailedReportDownloadResponseException(httpStatus, errorText);
}
};
}
/**
* Tests that {@link AdHocReportDownloadHelper#getReportDownloadTimeout()} retrieves the global
* timeout if not set on the helper.
*/
@Test
public void testGetReportDownloadTimeout() {
assertEquals(
AdWordsInternals.getInstance().getAdWordsLibConfiguration().getReportDownloadTimeout(),
helper.getReportDownloadTimeout());
}
/**
* Tests that {@link AdHocReportDownloadHelper#setReportDownloadTimeout(int)} sets the
* helper-specific timeout and does not affect the global timeout.
*/
@Test
public void testSetReportDownloadTimeout() {
int internalsTimeout =
AdWordsInternals.getInstance().getAdWordsLibConfiguration().getReportDownloadTimeout();
assertEquals(internalsTimeout, helper.getReportDownloadTimeout());
int helperTimeout = internalsTimeout + 10;
helper.setReportDownloadTimeout(helperTimeout);
assertEquals("Timeout on helper does not reflect changes made via setReportDownloadTimeout",
helperTimeout, helper.getReportDownloadTimeout());
assertEquals("Setting the timeout on a helper instance modified the global timeout",
internalsTimeout,
AdWordsInternals.getInstance().getAdWordsLibConfiguration().getReportDownloadTimeout());
}
/**
* Helper method that invokes the correct overload of
* {@code AdHocReportDownloadHelper.downloadReport} based on this test's {@code isTestRawDownload}
* attribute.
*
* <p>
* Return type is {@code void} because this should be used for download requests that are expected
* to fail.
*/
private void downloadReport() throws ReportException, ReportDownloadResponseException {
if (isTestRawDownload) {
helper.downloadReport(reportRequest);
} else {
helper.downloadReport(reportRequest, exceptionBuilder);
}
}
/**
* Tests that the helper will throw a {@link ConnectException} wrapped in a
* {@link ReportException} when the endpoint is for an unused port on localhost.
*/
@Test
public void testDownloadReportWithBadEndpoint_fails() throws Throwable {
int port = TestPortFinder.getInstance().checkOutUnusedPort();
try {
// Construct the session to use an endpoint that is NOT in use on localhost.
AdWordsSession sessionWithBadEndpoint =
new AdWordsSession.Builder()
.withUserAgent("TEST_APP")
.withOAuth2Credential(credential)
.withEndpoint("https://localhost:" + port)
.withDeveloperToken("TEST_DEVELOPER_TOKEN")
.withClientCustomerId("TEST_CLIENT_CUSTOMER_ID")
.build();
helper =
new GenericAdWordsServices()
.getBootstrapper()
.getInstanceOf(sessionWithBadEndpoint, AdHocReportDownloadHelper.class);
// Set up the ReportRequest so it will pass basic validation.
when(reportRequest.getRequestType()).thenReturn(RequestType.AWQL);
String awqlString = "SELECT BadField1 FROM NOT_A_REPORT DURING NOT_A_TIME_PERIOD";
when(reportRequest.getReportRequestString()).thenReturn(awqlString);
// The cause should be a ConnectException (see expected annotation) since the endpoint
// port is not in use.
thrown.expect(ReportException.class);
thrown.expectCause(Matchers.<Exception>instanceOf(ConnectException.class));
downloadReport();
} finally {
TestPortFinder.getInstance().releaseUnusedPort(port);
}
}
/**
* Tests that validation on the XML report definition string is propagated by the helper.
*
* @throws Exception
*/
@Test
public void testDownloadReportWithMissingXmlString_fails() throws Exception {
when(reportRequest.getRequestType()).thenReturn(RequestType.XML);
thrown.expect(NullPointerException.class);
downloadReport();
}
/**
* Tests that validation on the AWQL report definition string is propagated by the helper.
*/
@Test
public void testDownloadReportWithMissingAwqlString_fails() throws Exception {
when(reportRequest.getRequestType()).thenReturn(RequestType.AWQL);
thrown.expect(NullPointerException.class);
downloadReport();
}
/**
* Tests that validation for {@link ReportRequest#getRequestType()} is propagated by the helper.
*/
@Test
public void testDownloadReportWithMissingRequestType_fails() throws Exception {
String awqlString = "SELECT BadField1 FROM NOT_A_REPORT DURING NOT_A_TIME_PERIOD";
when(reportRequest.getReportRequestString()).thenReturn(awqlString);
thrown.expect(NullPointerException.class);
downloadReport();
}
/**
* Tests that the helper will properly capture an internal server error (500) status returned by
* the mock HTTP server.
*/
@Test
public void testDownloadReportWithServerErrorStatus() throws Exception {
when(reportRequest.getRequestType()).thenReturn(RequestType.AWQL);
String awqlString = "SELECT BadField1 FROM NOT_A_REPORT DURING NOT_A_TIME_PERIOD";
when(reportRequest.getReportRequestString()).thenReturn(awqlString);
// Do not set the next response body on the test server. This will trigger an error
// (500) from the test server.
RawReportDownloadResponse response = helper.downloadReport(reportRequest);
assertEquals("Response status code not failure", 500, response.getHttpStatus());
assertEquals("", Streams.readAll(response.getInputStream(), response.getCharset()));
}
/**
* Tests that the helper will pass the expected HTTP request to the server for a valid XML-based
* report request.
*/
@Test
public void testDownloadReportWithValidXmlRequest() throws Exception {
when(reportRequest.getRequestType()).thenReturn(RequestType.XML);
String xmlString =
"<reportDefinition xmlns=\"https://adwords.google.com/api/adwords/cm/v209902\">"
+ " <selector> "
+ " <fields>CampaignId</fields> "
+ " <fields>CampaignName</fields> "
+ " <fields>Impressions</fields> "
+ " <predicates> "
+ " <field>Status</field> "
+ " <operator>IN</operator> "
+ " <values>ENABLED</values> "
+ " <values>PAUSED</values> "
+ " </predicates> "
+ " </selector> "
+ " <reportName>Custom Adgroup Performance Report</reportName>"
+ " <reportType>CAMPAIGN_PERFORMANCE_REPORT</reportType>"
+ " <dateRangeType>LAST_7_DAYS</dateRangeType> "
+ " <downloadFormat>CSV</downloadFormat> "
+ "</reportDefinition> ";
when(reportRequest.getReportRequestString()).thenReturn(xmlString);
testHttpServer.setMockResponseBody("test");
if (isTestRawDownload) {
RawReportDownloadResponse response = helper.downloadReport(reportRequest);
assertNotNull("Null response", response);
assertEquals("Response status code not success", 200, response.getHttpStatus());
assertEquals(
"Response charset incorrect",
REPORT_CHARSET,
response.getCharset());
assertEquals(
"Response contents incorrect",
"test",
Streams.readAll(response.getInputStream(), response.getCharset()));
} else {
ReportDownloadResponse response = helper.downloadReport(reportRequest, exceptionBuilder);
assertNotNull("Null response", response);
assertEquals("Response status code not success", 200, response.getHttpStatus());
assertEquals(
"Response contents incorrect",
"test",
Streams.readAll(response.getInputStream(), REPORT_CHARSET));
}
String lastRequestBody = testHttpServer.getLastRequestBody();
assertThat(
"xml parameter value incorrect",
lastRequestBody,
containsString("__rdxml=" + UrlEscapers.urlFormParameterEscaper().escape(xmlString)));
}
/**
* Tests that the helper will pass the expected HTTP request to the server for a valid AWQL-based
* report request.
*/
@Test
public void testDownloadReportWithValidAwqlRequest() throws Exception {
when(reportRequest.getRequestType()).thenReturn(RequestType.AWQL);
String awqlString = "SELECT CampaignId, CampaignName, Impressions "
+ "FROM CAMPAIGN_PERFORMANCE_REPORT DURING THIS_MONTH";
when(reportRequest.getReportRequestString()).thenReturn(awqlString);
testHttpServer.setMockResponseBody("test");
if (isTestRawDownload) {
RawReportDownloadResponse response = helper.downloadReport(reportRequest);
assertNotNull("Null response", response);
assertEquals("Response status code not success", 200, response.getHttpStatus());
assertEquals(
"Response charset incorrect",
REPORT_CHARSET,
response.getCharset());
assertEquals(
"Response contents incorrect",
"test",
Streams.readAll(response.getInputStream(), response.getCharset()));
} else {
ReportDownloadResponse response = helper.downloadReport(reportRequest, exceptionBuilder);
assertNotNull("Null response", response);
assertEquals("Response status code not success", 200, response.getHttpStatus());
assertEquals(
"Response contents incorrect",
"test",
Streams.readAll(response.getInputStream(), REPORT_CHARSET));
}
String lastRequestBody = testHttpServer.getLastRequestBody();
assertThat(
"awql parameter value incorrect",
lastRequestBody,
containsString("__rdquery=" + UrlEscapers.urlFormParameterEscaper().escape(awqlString)));
assertThat("format parameter incorrect", lastRequestBody, containsString("__fmt=CSV"));
}
@Test
public void testHandleSuccessfulResponse() throws Exception {
String responseBody = "Successful,report,response";
RawReportDownloadResponse rawResponse =
new RawReportDownloadResponse(
200,
ByteSource.wrap(responseBody.getBytes(REPORT_CHARSET)).openStream(),
REPORT_CHARSET,
"CSV");
ReportDownloadResponse reportResponse = helper.handleResponse(rawResponse, exceptionBuilder);
String actualResponseBody =
new String(ByteStreams.toByteArray(reportResponse.getInputStream()), REPORT_CHARSET);
assertEquals("Response body not expected value", responseBody, actualResponseBody);
}
@Test
public void testHandleErrorResponse_fails() throws Exception {
RawReportDownloadResponse rawResponse =
new RawReportDownloadResponse(
500,
ByteSource.wrap(ERROR_XML.getBytes(REPORT_CHARSET)).openStream(),
REPORT_CHARSET,
"CSV");
thrown.expect(DetailedReportDownloadResponseException.class);
thrown.expect(Matchers.hasProperty("fieldPath", Matchers.equalTo("foobar")));
thrown.expect(Matchers.hasProperty("trigger", Matchers.equalTo("AdFormatt")));
thrown.expect(
Matchers.hasProperty(
"type", Matchers.equalTo("ReportDefinitionError.INVALID_FIELD_NAME_FOR_REPORT")));
helper.handleResponse(rawResponse, exceptionBuilder);
}
@Test
public void testHandleResponseWithNullInputStream_fails() throws Exception {
RawReportDownloadResponse rawResponse =
new RawReportDownloadResponse(
500, (InputStream) null, AdHocReportDownloadHelper.REPORT_CHARSET, "CSV");
thrown.expect(DetailedReportDownloadResponseException.class);
helper.handleResponse(rawResponse, exceptionBuilder);
}
@Test
public void testHandleResponseWithEmptyInputStream_fails() throws Exception {
RawReportDownloadResponse rawResponse =
new RawReportDownloadResponse(
500, ByteSource.empty().openStream(), REPORT_CHARSET, "CSV");
thrown.expect(DetailedReportDownloadResponseException.class);
thrown.expect(Matchers.hasProperty("fieldPath", Matchers.nullValue()));
thrown.expect(Matchers.hasProperty("trigger", Matchers.nullValue()));
thrown.expect(Matchers.hasProperty("type", Matchers.nullValue()));
helper.handleResponse(rawResponse, exceptionBuilder);
}
@Test
public void testHandleResponseNullResponse_fails() throws Exception {
thrown.expect(NullPointerException.class);
thrown.expectMessage("Null response");
helper.handleResponse(null, exceptionBuilder);
}
@Test
public void testHandleResponseNullExceptionBuilder_fails() throws Exception {
RawReportDownloadResponse rawResponse =
new RawReportDownloadResponse(
200,
ByteSource.wrap("a,b,c".getBytes(REPORT_CHARSET)).openStream(),
REPORT_CHARSET,
"CSV");
thrown.expect(NullPointerException.class);
thrown.expectMessage("Null exception builder");
helper.handleResponse(rawResponse, null);
}
}