Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add http api for fetching accounts #1015

Merged
merged 4 commits into from
Dec 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.Future;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.execution.Timeout;
import org.prebid.server.json.DecodeException;
Expand All @@ -14,6 +16,7 @@
import org.prebid.server.settings.model.StoredDataResult;
import org.prebid.server.settings.model.StoredDataType;
import org.prebid.server.settings.model.StoredResponseDataResult;
import org.prebid.server.settings.proto.response.HttpAccountsResponse;
import org.prebid.server.settings.proto.response.HttpFetcherResponse;
import org.prebid.server.util.HttpUtil;
import org.prebid.server.vertx.http.HttpClient;
Expand Down Expand Up @@ -59,7 +62,6 @@
*/
public class HttpApplicationSettings implements ApplicationSettings {

private static final String NOT_SUPPORTED = "Not supported";
private static final Logger logger = LoggerFactory.getLogger(HttpApplicationSettings.class);

private String endpoint;
Expand All @@ -77,12 +79,62 @@ public HttpApplicationSettings(HttpClient httpClient, JacksonMapper mapper, Stri
this.videoEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(videoEndpoint));
}

/**
* Not supported and returns failed result.
*/
@Override
public Future<Account> getAccountById(String accountId, Timeout timeout) {
return Future.failedFuture(new PreBidException("Not supported"));

return fetchAccountsByIds(Collections.singleton(accountId), timeout)
.map(accounts -> accounts.stream()
.findFirst()
.orElseThrow(() ->
new PreBidException(String.format("Account with id : %s not found", accountId))));
}

private Future<Set<Account>> fetchAccountsByIds(Set<String> accountIds, Timeout timeout) {
if (CollectionUtils.isEmpty(accountIds)) {
return Future.succeededFuture(Collections.emptySet());
}
final long remainingTimeout = timeout.remaining();
if (timeout.remaining() <= 0) {
return Future.failedFuture(new TimeoutException("Timeout has been exceeded"));
}

return httpClient.get(accountsRequestUrlFrom(endpoint, accountIds), HttpUtil.headers(), remainingTimeout)
.compose(response -> processAccountsResponse(response, accountIds))
.recover(Future::failedFuture);
}

private static String accountsRequestUrlFrom(String endpoint, Set<String> accountIds) {
final StringBuilder url = new StringBuilder(endpoint);
url.append(endpoint.contains("?") ? "&" : "?");

if (!accountIds.isEmpty()) {
url.append("account-ids=[\"").append(joinIds(accountIds)).append("\"]");
}

return url.toString();
}

private Future<Set<Account>> processAccountsResponse(HttpClientResponse response, Set<String> accountIds) {
return Future.succeededFuture(
toAccountsResult(response.getStatusCode(), response.getBody(), accountIds));
}

private Set<Account> toAccountsResult(int statusCode, String body, Set<String> accountIds) {
if (statusCode != HttpResponseStatus.OK.code()) {
throw new PreBidException(String.format("Error fetching accounts %s via http: "
+ "unexpected response status %d", accountIds, statusCode));
}

final HttpAccountsResponse response;
try {
response = mapper.decodeValue(body, HttpAccountsResponse.class);
} catch (DecodeException e) {
throw new PreBidException(String.format("Error fetching accounts %s "
+ "via http: failed to parse response: %s", accountIds, e.getMessage()));
}
final Map<String, Account> accounts = response.getAccounts();

return MapUtils.isNotEmpty(accounts) ? new HashSet<>(accounts.values()) : Collections.emptySet();
}

/**
Expand Down Expand Up @@ -139,15 +191,15 @@ private Future<StoredDataResult> fetchStoredData(String endpoint, Set<String> re

final long remainingTimeout = timeout.remaining();
if (remainingTimeout <= 0) {
return failResponse(new TimeoutException("Timeout has been exceeded"), requestIds, impIds);
return failStoredDataResponse(new TimeoutException("Timeout has been exceeded"), requestIds, impIds);
}

return httpClient.get(urlFrom(endpoint, requestIds, impIds), HttpUtil.headers(), remainingTimeout)
.compose(response -> processResponse(response, requestIds, impIds))
.recover(exception -> failResponse(exception, requestIds, impIds));
return httpClient.get(storeRequestUrlFrom(endpoint, requestIds, impIds), HttpUtil.headers(), remainingTimeout)
.compose(response -> processStoredDataResponse(response, requestIds, impIds))
.recover(exception -> failStoredDataResponse(exception, requestIds, impIds));
}

private static String urlFrom(String endpoint, Set<String> requestIds, Set<String> impIds) {
private static String storeRequestUrlFrom(String endpoint, Set<String> requestIds, Set<String> impIds) {
final StringBuilder url = new StringBuilder(endpoint);
url.append(endpoint.contains("?") ? "&" : "?");

Expand All @@ -169,14 +221,14 @@ private static String joinIds(Set<String> ids) {
return String.join("\",\"", ids);
}

private static Future<StoredDataResult> failResponse(Throwable throwable, Set<String> requestIds,
Set<String> impIds) {
private static Future<StoredDataResult> failStoredDataResponse(Throwable throwable, Set<String> requestIds,
Set<String> impIds) {
return Future.succeededFuture(
toFailedStoredDataResult(requestIds, impIds, throwable.getMessage()));
}

private Future<StoredDataResult> processResponse(HttpClientResponse response, Set<String> requestIds,
Set<String> impIds) {
private Future<StoredDataResult> processStoredDataResponse(HttpClientResponse response, Set<String> requestIds,
Set<String> impIds) {
return Future.succeededFuture(
toStoredDataResult(requestIds, impIds, response.getStatusCode(), response.getBody()));
}
Expand All @@ -197,7 +249,7 @@ private static StoredDataResult toFailedStoredDataResult(Set<String> requestIds,

private StoredDataResult toStoredDataResult(Set<String> requestIds, Set<String> impIds,
int statusCode, String body) {
if (statusCode != 200) {
if (statusCode != HttpResponseStatus.OK.code()) {
return toFailedStoredDataResult(requestIds, impIds, "HTTP status code %d", statusCode);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.prebid.server.settings.proto.response;

import lombok.AllArgsConstructor;
import lombok.Value;
import org.prebid.server.settings.model.Account;

import java.util.Map;

@AllArgsConstructor(staticName = "of")
@Value
public class HttpAccountsResponse {

Map<String, Account> accounts;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@Value
public class HttpFetcherResponse {

private Map<String, ObjectNode> requests;
Map<String, ObjectNode> requests;

private Map<String, ObjectNode> imps;
Map<String, ObjectNode> imps;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
import org.prebid.server.settings.model.Account;
import org.prebid.server.settings.model.StoredDataResult;
import org.prebid.server.settings.model.StoredResponseDataResult;
import org.prebid.server.settings.proto.response.HttpAccountsResponse;
import org.prebid.server.settings.proto.response.HttpFetcherResponse;
import org.prebid.server.vertx.http.HttpClient;
import org.prebid.server.vertx.http.model.HttpClientResponse;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;

Expand Down Expand Up @@ -94,13 +96,90 @@ public void creationShouldFailsOnInvalidVideoEndpoint() {
}

@Test
public void getAccountByIdShouldReturnEmptyResult() {
public void getAccountByIdShouldReturnFetchedAccount() throws JsonProcessingException {
// given
final Account account = Account.builder()
.id("someId")
.enforceCcpa(true)
.priceGranularity("testPriceGranularity")
.build();
HttpAccountsResponse response = HttpAccountsResponse.of(Collections.singletonMap("someId", account));
givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response));

// when
final Future<Account> future = httpApplicationSettings.getAccountById("someId", timeout);

// then
assertThat(future.succeeded()).isTrue();
assertThat(future.result().getId()).isEqualTo("someId");
assertThat(future.result().getEnforceCcpa()).isEqualTo(true);
assertThat(future.result().getPriceGranularity()).isEqualTo("testPriceGranularity");

verify(httpClient).get(eq("http://stored-requests?account-ids=[\"someId\"]"), any(),
anyLong());
}

@Test
public void getAccountByIdShouldReturnFaildedFutureIfResponseIsNotPresent() throws JsonProcessingException {
// given
HttpAccountsResponse response = HttpAccountsResponse.of(null);
givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response));

// when
final Future<Account> future = httpApplicationSettings.getAccountById(null, null);
final Future<Account> future = httpApplicationSettings.getAccountById("notFoundId", timeout);

// then
assertThat(future.failed()).isTrue();
assertThat(future.cause()).isInstanceOf(PreBidException.class).hasMessage("Not supported");
assertThat(future.cause())
.isInstanceOf(PreBidException.class)
.hasMessage("Account with id : notFoundId not found");
}

@Test
public void getAccountByIdShouldReturnErrorIdAccountNotFound() throws JsonProcessingException {
// given
HttpAccountsResponse response = HttpAccountsResponse.of(Collections.emptyMap());
givenHttpClientReturnsResponse(200, mapper.writeValueAsString(response));

// when
final Future<Account> future = httpApplicationSettings.getAccountById("notExistingId", timeout);

// then
assertThat(future.failed()).isTrue();
assertThat(future.cause())
.isInstanceOf(PreBidException.class)
.hasMessage("Account with id : notExistingId not found");
}

@Test
public void getAccountByIdShouldReturnErrorIfResponseStatusIsDifferentFromOk() {
// given
givenHttpClientReturnsResponse(400, null);

// when
final Future<Account> future = httpApplicationSettings.getAccountById("accountId", timeout);

// then
assertThat(future.failed()).isTrue();
assertThat(future.cause())
.isInstanceOf(PreBidException.class)
.hasMessage("Error fetching accounts [accountId] via http: unexpected response status 400");
}

@Test
public void getAccountByIdShouldReturnErrorIfResponseHasInvalidStructure() {
// given
givenHttpClientReturnsResponse(200, "not valid response");

// when
final Future<Account> future = httpApplicationSettings.getAccountById("accountId", timeout);

// then
assertThat(future.failed()).isTrue();
assertThat(future.cause())
.isInstanceOf(PreBidException.class)
.hasMessageContaining("Error fetching accounts [accountId] via http: "
+ "failed to parse response: Failed to decode:");
}

@Test
Expand Down