Skip to content

Commit

Permalink
Add http api for fetching accounts (#1015)
Browse files Browse the repository at this point in the history
* Add http api for fetching accounts

* Replace ObjectNode with Account type in HttpAccountResponse model

* Fixes after review
  • Loading branch information
SerhiiNahornyi authored Dec 2, 2020
1 parent 7e85714 commit dc27cf1
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 20 deletions.
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

0 comments on commit dc27cf1

Please sign in to comment.