diff --git a/space-client/src/main/java/io/github/pgmarc/space/FeaturesEndpoint.java b/space-client/src/main/java/io/github/pgmarc/space/FeaturesEndpoint.java new file mode 100644 index 0000000..a103730 --- /dev/null +++ b/space-client/src/main/java/io/github/pgmarc/space/FeaturesEndpoint.java @@ -0,0 +1,131 @@ +package io.github.pgmarc.space; + +import io.github.pgmarc.space.deserializers.ErrorDeserializer; +import io.github.pgmarc.space.deserializers.FeatureEvaluationDeserializer; +import io.github.pgmarc.space.exceptions.SpaceApiException; +import io.github.pgmarc.space.features.Consumption; +import io.github.pgmarc.space.features.FeatureEvaluationResult; +import io.github.pgmarc.space.serializers.ConsumptionSerializer; +import okhttp3.*; +import org.json.JSONObject; + +import java.io.IOException; + +public final class FeaturesEndpoint { + + private static final String ENDPOINT = "features"; + private static final MediaType JSON = MediaType.get("application/json"); + private static final String STATUS_CODE = "statusCode"; + + + private final OkHttpClient client; + private final HttpUrl baseUrl; + private final Headers requiredHeaders; + private final ConsumptionSerializer consumptionSerializer; + private final ErrorDeserializer errorDeserializer; + + FeaturesEndpoint(OkHttpClient client, HttpUrl baseUrl, String apiKey) { + this.client = client; + this.baseUrl = baseUrl.newBuilder().addPathSegment(ENDPOINT).build(); + this.requiredHeaders = new Headers.Builder().add("Accept", JSON.toString()) + .add("x-api-key", apiKey).build(); + this.consumptionSerializer = new ConsumptionSerializer(); + this.errorDeserializer = new ErrorDeserializer(); + } + + private static String formatFeatureId(String service, String feature) { + return service.toLowerCase() + "-" + feature; + } + + public FeatureEvaluationResult evaluate(String userId, String service, String feature) throws IOException { + HttpUrl url = this.baseUrl.newBuilder().addEncodedPathSegment(userId) + .addEncodedPathSegment(formatFeatureId(service, feature)).build(); + Request request = new Request(url, requiredHeaders ,"POST" , RequestBody.EMPTY); + + FeatureEvaluationResult res = null; + try (Response response = client.newCall(request).execute()) { + JSONObject jsonResponse = new JSONObject(response.body().string()); + if (!response.isSuccessful()) { + jsonResponse.put(STATUS_CODE, response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + FeatureEvaluationDeserializer deserializer = new FeatureEvaluationDeserializer(service.length()); + res = deserializer.fromJson(jsonResponse); + } + + return res; + } + + public FeatureEvaluationResult evaluateOptimistically(String userId, String service, String featureId, Consumption consumption) + throws IOException { + HttpUrl url = this.baseUrl.newBuilder().addEncodedPathSegment(userId) + .addEncodedPathSegment(formatFeatureId(service, featureId)).build(); + RequestBody body = RequestBody.create(consumptionSerializer.toJson(consumption).toString(), JSON); + Request request = new Request(url, requiredHeaders ,"POST" , body); + + FeatureEvaluationResult res = null; + try (Response response = client.newCall(request).execute()) { + JSONObject jsonResponse = new JSONObject(response.body().string()); + if (!response.isSuccessful()) { + jsonResponse.put(STATUS_CODE, response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + FeatureEvaluationDeserializer deserializer = new FeatureEvaluationDeserializer(service.length()); + res = deserializer.fromJson(jsonResponse); + } + + return res; + } + + public enum Revert { + OLDEST_VALUE(false), + NEWEST_VALUE(true); + + private final boolean latest; + + Revert(boolean latest) { + this.latest = latest; + } + + public boolean isLatest() { + return latest; + } + } + + public boolean revert(String userId, String service, String feature, Revert revert) throws IOException { + HttpUrl url = this.baseUrl.newBuilder().addEncodedPathSegment(userId) + .addEncodedPathSegment(formatFeatureId(service, feature)) + .addQueryParameter("revert", String.valueOf(true)) + .addQueryParameter("latest", String.valueOf(revert.isLatest())).build(); + + Request request = new Request(url, requiredHeaders ,"POST" , RequestBody.EMPTY); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + JSONObject jsonResponse = new JSONObject(response.body().string()); + jsonResponse.put(STATUS_CODE, response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + + return response.code() == 204; + } + + } + + public String generatePricingTokenForUser(String userId) throws IOException { + + HttpUrl url = this.baseUrl.newBuilder().addEncodedPathSegment(userId) + .addPathSegment("pricing-token").build(); + Request request = new Request(url, requiredHeaders ,"POST" , RequestBody.EMPTY); + + FeatureEvaluationResult res = null; + try (Response response = client.newCall(request).execute()) { + JSONObject jsonResponse = new JSONObject(response.body().string()); + if (!response.isSuccessful()) { + jsonResponse.put(STATUS_CODE, response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + return jsonResponse.getString("pricingToken"); + } + } +} diff --git a/space-client/src/main/java/io/github/pgmarc/space/SpaceClient.java b/space-client/src/main/java/io/github/pgmarc/space/SpaceClient.java index 953d53d..b1db1f9 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/SpaceClient.java +++ b/space-client/src/main/java/io/github/pgmarc/space/SpaceClient.java @@ -13,6 +13,7 @@ public final class SpaceClient { private final String apiKey; private ContractsEndpoint contracts; + private FeaturesEndpoint features; private SpaceClient(OkHttpClient httpClient, HttpUrl baseUrl, String apiKey) { this.httpClient = httpClient; @@ -22,11 +23,18 @@ private SpaceClient(OkHttpClient httpClient, HttpUrl baseUrl, String apiKey) { public ContractsEndpoint contracts() { if (contracts == null) { - this.contracts = new ContractsEndpoint(httpClient, baseUrl, apiKey); + contracts = new ContractsEndpoint(httpClient, baseUrl, apiKey); } return contracts; } + public FeaturesEndpoint features() { + if (features == null) { + features = new FeaturesEndpoint(httpClient, baseUrl, apiKey); + } + return features; + } + public static Builder builder(String host, String apiKey) { return new Builder(host, apiKey); } diff --git a/space-client/src/main/java/io/github/pgmarc/space/contracts/AddOn.java b/space-client/src/main/java/io/github/pgmarc/space/contracts/AddOn.java index 07a4104..0e54a89 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/contracts/AddOn.java +++ b/space-client/src/main/java/io/github/pgmarc/space/contracts/AddOn.java @@ -1,5 +1,7 @@ package io.github.pgmarc.space.contracts; +import java.util.Objects; + public final class AddOn { private final String name; @@ -19,28 +21,14 @@ public long getQuantity() { } @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((name == null) ? 0 : name.hashCode()); - return result; + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + AddOn addOn = (AddOn) o; + return quantity == addOn.quantity && Objects.equals(name, addOn.name); } @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - AddOn other = (AddOn) obj; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - return true; + public int hashCode() { + return Objects.hash(name, quantity); } - } diff --git a/space-client/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/space-client/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java index 2c057de..a256724 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ b/space-client/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -31,8 +31,13 @@ Duration getDuration() { return renewalDays; } - boolean isExpired(LocalDateTime dateTime) { - return endDate.isAfter(ZonedDateTime.of(dateTime, ZoneId.of("UTC"))); + boolean isActive(ZonedDateTime date) { + return (startDate.isEqual(date) || startDate.isBefore(date)) && + (endDate.isAfter(date) || endDate.isEqual(date)); + } + + boolean isExpired(ZonedDateTime date) { + return startDate.isBefore(date) && endDate.isBefore(date); } boolean isAutoRenewable() { @@ -77,7 +82,7 @@ public enum Keys { private final String name; - private Keys(String name) { + Keys(String name) { this.name = name; } @@ -88,40 +93,15 @@ public String toString() { } @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((startDate == null) ? 0 : startDate.hashCode()); - result = prime * result + ((endDate == null) ? 0 : endDate.hashCode()); - result = prime * result + ((renewalDays == null) ? 0 : renewalDays.hashCode()); - return result; + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + BillingPeriod that = (BillingPeriod) o; + return Objects.equals(startDate, that.startDate) && Objects.equals(endDate, that.endDate) && Objects.equals(renewalDays, that.renewalDays); } @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - BillingPeriod other = (BillingPeriod) obj; - if (startDate == null) { - if (other.startDate != null) - return false; - } else if (!startDate.equals(other.startDate)) - return false; - if (endDate == null) { - if (other.endDate != null) - return false; - } else if (!endDate.equals(other.endDate)) - return false; - if (renewalDays == null) { - if (other.renewalDays != null) - return false; - } else if (!renewalDays.equals(other.renewalDays)) - return false; - return true; + public int hashCode() { + return Objects.hash(startDate, endDate, renewalDays); } @Override diff --git a/space-client/src/main/java/io/github/pgmarc/space/contracts/Service.java b/space-client/src/main/java/io/github/pgmarc/space/contracts/Service.java index 98cb1d9..67697c3 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/contracts/Service.java +++ b/space-client/src/main/java/io/github/pgmarc/space/contracts/Service.java @@ -12,7 +12,7 @@ public final class Service { private final String name; private final String version; private final Map addOns; - private String plan; + private final String plan; private Service(Builder builder) { this.name = builder.name; @@ -52,46 +52,15 @@ public String toString() { } @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((version == null) ? 0 : version.hashCode()); - result = prime * result + ((addOns == null) ? 0 : addOns.hashCode()); - result = prime * result + ((plan == null) ? 0 : plan.hashCode()); - return result; + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Service service = (Service) o; + return Objects.equals(name, service.name) && Objects.equals(version, service.version) && Objects.equals(addOns, service.addOns) && Objects.equals(plan, service.plan); } @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Service other = (Service) obj; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (version == null) { - if (other.version != null) - return false; - } else if (!version.equals(other.version)) - return false; - if (addOns == null) { - if (other.addOns != null) - return false; - } else if (!addOns.equals(other.addOns)) - return false; - if (plan == null) { - if (other.plan != null) - return false; - } else if (!plan.equals(other.plan)) - return false; - return true; + public int hashCode() { + return Objects.hash(name, version, addOns, plan); } public static final class Builder { diff --git a/space-client/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/space-client/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index 9c1f552..362fb11 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/space-client/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -2,6 +2,8 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -34,9 +36,9 @@ public static Builder builder(UserContact userContact, BillingPeriod billingPeri return new Builder(userContact, billingPeriod).subscribe(service); } - public static Builder builder(UserContact usagerContact, BillingPeriod billingPeriod, + public static Builder builder(UserContact userContact, BillingPeriod billingPeriod, Collection services) { - return new Builder(usagerContact, billingPeriod).subscribeAll(services); + return new Builder(userContact, billingPeriod).subscribeAll(services); } public LocalDateTime getStartDate() { @@ -55,8 +57,23 @@ public boolean isAutoRenewable() { return billingPeriod.isAutoRenewable(); } + /** + * Checks whether a date is within the subscription interval. + * A subscription is active if date is between subscription + * start date and end date (both boundaries are inclusive). + */ + public boolean isActive(LocalDateTime date) { + Objects.requireNonNull(date, "date must not be null"); + return billingPeriod.isActive(ZonedDateTime.of(date, ZoneId.of("UTC"))); + } + + /** + * A subscription has expired if given date is after + * subscription interval. + */ public boolean isExpired(LocalDateTime date) { - return billingPeriod.isExpired(date); + Objects.requireNonNull(date, "date must not be null"); + return billingPeriod.isExpired(ZonedDateTime.of(date, ZoneId.of("UTC"))); } public Optional getRenewalDate() { @@ -102,7 +119,7 @@ public enum Keys { private final String name; - private Keys(String name) { + Keys(String name) { this.name = name; } @@ -146,9 +163,9 @@ public Builder subscribeAll(Collection services) { return this; } - public Builder addSnapshots(Collection snaphsots) { - Objects.requireNonNull(snaphsots, "snapshots must not be null"); - this.history.addAll(snaphsots); + public Builder addSnapshots(Collection snapshots) { + Objects.requireNonNull(snapshots, "snapshots must not be null"); + this.history.addAll(snapshots); return this; } @@ -167,28 +184,22 @@ public Subscription build() { public static final class Snapshot { private final LocalDateTime starDateTime; - private final LocalDateTime enDateTime; + private final LocalDateTime endDateTime; private final Map services; - public Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, + private Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, Map services) { this.starDateTime = startDateTime; - this.enDateTime = endDateTime; + this.endDateTime = endDateTime; this.services = new HashMap<>(services); } - private Snapshot(Subscription subscription) { - this.starDateTime = subscription.getStartDate(); - this.enDateTime = subscription.getEndDate(); - this.services = subscription.getServicesMap(); - } - public LocalDateTime getStartDate() { return starDateTime; } public LocalDateTime getEndDate() { - return enDateTime; + return endDateTime; } public Map getServices() { @@ -199,48 +210,9 @@ public Optional getService(String name) { return Optional.ofNullable(services.get(name)); } - static Snapshot of(Subscription subscription) { - Objects.requireNonNull(subscription, "subscription must not be null"); - return new Snapshot(subscription); + public static Snapshot of(LocalDateTime startDateTime, LocalDateTime endDateTime, + Map services) { + return new Snapshot(startDateTime, endDateTime, services); } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((starDateTime == null) ? 0 : starDateTime.hashCode()); - result = prime * result + ((enDateTime == null) ? 0 : enDateTime.hashCode()); - result = prime * result + ((services == null) ? 0 : services.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Snapshot other = (Snapshot) obj; - if (starDateTime == null) { - if (other.starDateTime != null) - return false; - } else if (!starDateTime.equals(other.starDateTime)) - return false; - if (enDateTime == null) { - if (other.enDateTime != null) - return false; - } else if (!enDateTime.equals(other.enDateTime)) - return false; - if (services == null) { - if (other.services != null) - return false; - } else if (!services.equals(other.services)) - return false; - return true; - } - } - } diff --git a/space-client/src/main/java/io/github/pgmarc/space/deserializers/ErrorDeserializer.java b/space-client/src/main/java/io/github/pgmarc/space/deserializers/ErrorDeserializer.java index 231e83a..aac8b6c 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/deserializers/ErrorDeserializer.java +++ b/space-client/src/main/java/io/github/pgmarc/space/deserializers/ErrorDeserializer.java @@ -13,16 +13,12 @@ public final class ErrorDeserializer implements JsonDeserializable { + + private final int serviceLength; + + public FeatureEvaluationDeserializer(int serviceLength) { + this.serviceLength = serviceLength; + } + + private enum Keys { + EVAL("eval"), + ERROR("error"), + CODE("code"), + MESSAGE("message"), + USED("used"), + LIMIT("limit"); + + private final String name; + + Keys(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Override + public FeatureEvaluationResult fromJson(JSONObject json) { + + JSONObject error = json.optJSONObject(Keys.ERROR.toString()); + if (error != null) { + FeatureEvaluationError evalError = FeatureEvaluationError.of(error.getString(Keys.CODE.toString()), + error.getString(Keys.MESSAGE.toString())); + throw new SpaceApiException(evalError.toString()); + } + + boolean available = json.getBoolean(Keys.EVAL.toString()); + Map quotas = featureQuotasFromJson(json, serviceLength); + + return FeatureEvaluationResult.of(available, quotas); + } + + private static Map featureQuotasFromJson(JSONObject json, int serviceNameLength) { + Map res = new HashMap<>(); + + if (json.isNull(Keys.USED.toString())) { + return res; + } + + for (String usageLimitId : json.getJSONObject(Keys.USED.toString()).keySet()) { + String usedJsonPointer = "/" + Keys.USED + "/" + usageLimitId; + String limitJsonPointer = "/" + Keys.LIMIT + "/" + usageLimitId; + String usageLimit = usageLimitId.substring(serviceNameLength + 1); + Number used = (Number) json.query(usedJsonPointer); + Number limit = (Number) json.query(limitJsonPointer); + res.put(usageLimit, FeatureEvaluationResult.Usage.of(used, limit)); + } + + return res; + } +} diff --git a/space-client/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java b/space-client/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java index f9baa8e..6f02dee 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java +++ b/space-client/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java @@ -24,12 +24,12 @@ public List fromJson(JSONObject json) { JSONArray history = json.getJSONArray(Subscription.Keys.HISTORY.toString()); List res = new ArrayList<>(); for (int i = 0; i < history.length(); i++) { - JSONObject snaphsot = history.getJSONObject(i); + JSONObject snapshot = history.getJSONObject(i); OffsetDateTime startUtc = OffsetDateTime - .parse(snaphsot.getString(BillingPeriod.Keys.START_DATE.toString())); + .parse(snapshot.getString(BillingPeriod.Keys.START_DATE.toString())); OffsetDateTime end = OffsetDateTime - .parse(snaphsot.getString(BillingPeriod.Keys.END_DATE.toString())); - res.add(new Snapshot(startUtc.toLocalDateTime(), end.toLocalDateTime(), servicesDeserializer.fromJson(snaphsot))); + .parse(snapshot.getString(BillingPeriod.Keys.END_DATE.toString())); + res.add(Snapshot.of(startUtc.toLocalDateTime(), end.toLocalDateTime(), servicesDeserializer.fromJson(snapshot))); } return res; } diff --git a/space-client/src/main/java/io/github/pgmarc/space/exceptions/FeatureEvaluationError.java b/space-client/src/main/java/io/github/pgmarc/space/exceptions/FeatureEvaluationError.java new file mode 100644 index 0000000..aa1916d --- /dev/null +++ b/space-client/src/main/java/io/github/pgmarc/space/exceptions/FeatureEvaluationError.java @@ -0,0 +1,35 @@ +package io.github.pgmarc.space.exceptions; + +public final class FeatureEvaluationError { + + public enum Code { + EVALUATION_ERROR, + FLAG_NOT_FOUND, + GENERAL, + INVALID_EXPECTED_CONSUMPTION, + PARSE_ERROR, + TYPE_MISMATCH + } + + private final Code code; + private final String message; + + private FeatureEvaluationError(Code code, String reason) { + this.code = code; + this.message = reason; + } + + public static FeatureEvaluationError of(String code, String reason) { + return new FeatureEvaluationError(Code.valueOf(code), reason); + } + + private String formatMessage() { + return message.endsWith(".") ? message : message + "."; + } + + @Override + public String toString() { + return formatMessage() + " Error code: " + code; + } + +} diff --git a/space-client/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiError.java b/space-client/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiError.java index ed2779d..47bc1d8 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiError.java +++ b/space-client/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiError.java @@ -7,15 +7,15 @@ public class SpaceApiError { private final Set messages; - private final int code; + private final int statusCode; public SpaceApiError(int code, Set messages) { - this.code = code; + this.statusCode = code; this.messages = Collections.unmodifiableSet(messages); } - int getCode() { - return code; + int getStatusCode() { + return statusCode; } @Override diff --git a/space-client/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java b/space-client/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java index 1ce43af..795d0bf 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java +++ b/space-client/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java @@ -3,14 +3,18 @@ public class SpaceApiException extends RuntimeException { - private final transient SpaceApiError error; + private transient SpaceApiError error; public SpaceApiException(SpaceApiError error) { super(error.toString()); this.error = error; } + public SpaceApiException(String message) { + super(message); + } + public int getCode() { - return error.getCode(); + return error.getStatusCode(); } } diff --git a/space-client/src/main/java/io/github/pgmarc/space/features/Consumption.java b/space-client/src/main/java/io/github/pgmarc/space/features/Consumption.java new file mode 100644 index 0000000..1fd25b2 --- /dev/null +++ b/space-client/src/main/java/io/github/pgmarc/space/features/Consumption.java @@ -0,0 +1,89 @@ +package io.github.pgmarc.space.features; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class Consumption { + + private final Set> items; + + private Consumption(Builder builder) { + this.items = Collections.unmodifiableSet(builder.items); + } + + public Set> getConsumption() { + return items; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Item { + private final String serviceName; + private final String usageLimit; + private final T quantity; + + private Item(String serviceName, String usageLimit, T quantity) { + this.serviceName = serviceName; + this.usageLimit = usageLimit; + this.quantity = quantity; + } + + public String getServiceName() { + return serviceName; + } + + public String getUsageLimit() { + return usageLimit; + } + + public T getQuantity() { + return quantity; + } + } + + public static class Builder { + + private final Set> items = new HashSet<>(); + + private void validateFeatureId(String service, String usageLimit) { + Objects.requireNonNull(service, "service name must not be null"); + Objects.requireNonNull(usageLimit, "usage limit name must not be null"); + } + + public Builder addInt(String serviceName, String usageLimit, int quantity) { + validateFeatureId(serviceName, usageLimit); + this.items.add(new Item<>(serviceName, usageLimit, quantity)); + return this; + } + + public Builder addLong(String serviceName, String usageLimit, long quantity) { + validateFeatureId(serviceName, usageLimit); + this.items.add(new Item<>(serviceName, usageLimit, quantity)); + return this; + } + + public Builder addFloat(String serviceName, String usageLimit, float quantity) { + validateFeatureId(serviceName, usageLimit); + this.items.add(new Item<>(serviceName, usageLimit, quantity)); + return this; + } + + public Builder addDouble(String serviceName, String usageLimit, double quantity) { + validateFeatureId(serviceName, usageLimit); + this.items.add(new Item<>(serviceName, usageLimit, quantity)); + return this; + } + + public Consumption build() { + if (items.isEmpty()) { + throw new IllegalStateException("consumption must not be empty"); + } + return new Consumption(this); + } + + } +} diff --git a/space-client/src/main/java/io/github/pgmarc/space/features/FeatureEvaluationResult.java b/space-client/src/main/java/io/github/pgmarc/space/features/FeatureEvaluationResult.java new file mode 100644 index 0000000..615b802 --- /dev/null +++ b/space-client/src/main/java/io/github/pgmarc/space/features/FeatureEvaluationResult.java @@ -0,0 +1,65 @@ +package io.github.pgmarc.space.features; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public final class FeatureEvaluationResult { + + private final boolean available; + private final Map quotas; + + private FeatureEvaluationResult(boolean available, Map quotas) { + this.available = available; + this.quotas = quotas; + } + + public boolean isAvailable() { + return available; + } + + public Map getQuotas() { + return Collections.unmodifiableMap(quotas); + } + + public Optional getConsumed(String usageLimit) { + Objects.requireNonNull(usageLimit, "usage limit must not be null"); + return quotas.containsKey(usageLimit) ? + Optional.of(quotas.get(usageLimit).getUsed()) : Optional.empty(); + } + + public Optional getLimit(String usageLimit) { + return quotas.containsKey(usageLimit) ? + Optional.of(quotas.get(usageLimit).getLimit()) : Optional.empty(); + } + + + public static FeatureEvaluationResult of(boolean available, Map quotas) { + return new FeatureEvaluationResult(available, quotas); + } + + public final static class Usage { + + private final Number used; + private final Number limit; + + private Usage(Number used, Number limit) { + this.used = used; + this.limit = limit; + } + + public Number getUsed() { + return used; + } + + public Number getLimit() { + return limit; + } + + public static Usage of(Number used, Number limit) { + return new Usage(used, limit); + } + } + +} diff --git a/space-client/src/main/java/io/github/pgmarc/space/serializers/ConsumptionSerializer.java b/space-client/src/main/java/io/github/pgmarc/space/serializers/ConsumptionSerializer.java new file mode 100644 index 0000000..7597928 --- /dev/null +++ b/space-client/src/main/java/io/github/pgmarc/space/serializers/ConsumptionSerializer.java @@ -0,0 +1,24 @@ +package io.github.pgmarc.space.serializers; + +import io.github.pgmarc.space.features.Consumption; +import org.json.JSONObject; + +import java.util.stream.Collectors; + +public final class ConsumptionSerializer implements JsonSerializable { + + private static String formatConsumptionKey(String serviceName, String usageLimitName) { + return serviceName.toLowerCase() + "-" + usageLimitName; + } + + @Override + public JSONObject toJson(Consumption consumption) { + + JSONObject jsonObject = new JSONObject(); + for (Consumption.Item item: consumption.getConsumption()) { + jsonObject.put(formatConsumptionKey(item.getServiceName(), item.getUsageLimit()), item.getQuantity()); + } + + return jsonObject; + } +} diff --git a/space-client/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java b/space-client/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java index 50dc460..545e0a3 100644 --- a/space-client/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java +++ b/space-client/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java @@ -1,9 +1,6 @@ package io.github.pgmarc.space.serializers; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; +import java.util.*; import org.json.JSONObject; @@ -57,10 +54,11 @@ public static Map contractedServices(Set services) { public static Map subscriptionPlans(Set services) { Map res = new HashMap<>(); for (Service service : services) { - if (service.getPlan().isEmpty()) { + Optional plan = service.getPlan(); + if (plan.isEmpty()) { continue; } - res.put(service.getName(), service.getPlan().get()); + res.put(service.getName(), plan.get()); } return Collections.unmodifiableMap(res); diff --git a/space-client/src/test/java/io/github/pgmarc/space/BaseEndpointTest.java b/space-client/src/test/java/io/github/pgmarc/space/BaseEndpointTest.java new file mode 100644 index 0000000..81adfa1 --- /dev/null +++ b/space-client/src/test/java/io/github/pgmarc/space/BaseEndpointTest.java @@ -0,0 +1,28 @@ +package io.github.pgmarc.space; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; + +public class BaseEndpointTest { + + protected static final String TEST_API_KEY = "prueba"; + protected static final OkHttpClient httpClient = new OkHttpClient.Builder().build(); + protected static HttpUrl url; + + @RegisterExtension + protected static WireMockExtension wm = WireMockExtension.newInstance() + .options(wireMockConfig().dynamicPort().globalTemplating(true)) + .build(); + + @BeforeAll + static void setUp() { + url = new HttpUrl.Builder().scheme("http").host("localhost").port(wm.getPort()).build(); + } + + +} diff --git a/space-client/src/test/java/io/github/pgmarc/space/FeaturesEndpointTest.java b/space-client/src/test/java/io/github/pgmarc/space/FeaturesEndpointTest.java new file mode 100644 index 0000000..777a4c4 --- /dev/null +++ b/space-client/src/test/java/io/github/pgmarc/space/FeaturesEndpointTest.java @@ -0,0 +1,150 @@ +package io.github.pgmarc.space; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import io.github.pgmarc.space.features.Consumption; +import io.github.pgmarc.space.features.FeatureEvaluationResult; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +import static org.assertj.core.api.Assertions.*; + +@WireMockTest +class FeaturesEndpointTest extends BaseEndpointTest { + + private static FeaturesEndpoint endpoint; + + @BeforeAll + static void setup() { + endpoint = new FeaturesEndpoint(httpClient, url, TEST_API_KEY); + } + + @Test + void givenSimpleFeatureIdShouldEvaluate() { + + String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; + String featureId = "petclinic-featureA"; + + wm.stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) + .withHeader("x-api-key", equalTo("prueba")) + .withPathParam("userId", equalTo(userId)) + .withPathParam("featureId", equalTo(featureId)) + .willReturn( + ok() + .withHeader("Content-Type", "application/json") + .withBodyFile("boolean-feature-evaluation.json"))); + + try { + FeatureEvaluationResult res = endpoint.evaluate(userId, "Petclinic", "featureA"); + assertThat(res.isAvailable()).isTrue(); + assertThat(res.getQuotas()).isEmpty(); + } catch (IOException e) { + fail(); + } + + } + + @Test + void givenConsumptionShouldEvaluateOptimistically() { + + String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; + String featureId = "petclinic-featureA"; + + wm.stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) + .withHeader("x-api-key", equalTo("prueba")) + .withPathParam("userId", equalTo(userId)) + .withPathParam("featureId", equalTo(featureId)) + .willReturn( + ok() + .withHeader("Content-Type", "application/json") + .withBodyFile("optimistic-evaluation-response.json"))); + + String service = "Petclinic"; + String feature = "featureA"; + String usageLimit = "featureALimit"; + + try { + Consumption consumption = Consumption.builder().addInt(service, usageLimit, 100).build(); + FeatureEvaluationResult res = endpoint.evaluateOptimistically(userId, service, feature, consumption); + assertThat(res.isAvailable()).isTrue(); + assertThat(res.getConsumed(usageLimit)).hasValue(100); + assertThat(res.getLimit(usageLimit)).hasValue(500); + } catch (IOException e) { + fail(); + } + } + + @Test + void givenRevertNewestCallShouldCompleteSucessfully() { + String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; + String featureId = "petclinic-featureA"; + + wm.stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) + .withHeader("x-api-key", equalTo("prueba")) + .withRequestBody(absent()) + .withPathParam("userId", equalTo(userId)) + .withPathParam("featureId", equalTo(featureId)) + .withQueryParam("revert", equalTo("true")) + .withQueryParam("latest", equalTo("true")) + .willReturn( + noContent())); + + String service = "Petclinic"; + String feature = "featureA"; + + try { + assertThat(endpoint.revert(userId, service, feature, FeaturesEndpoint.Revert.NEWEST_VALUE)) + .isTrue(); + } catch (IOException e) { + fail(); + } + } + + @Test + void givenRevertOldestCallShouldCompleteSucessfully() { + String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; + String featureId = "petclinic-featureA"; + + wm.stubFor(post(urlPathTemplate("/features/{userId}/{featureId}")) + .withHeader("x-api-key", equalTo("prueba")) + .withRequestBody(absent()) + .withPathParam("userId", equalTo(userId)) + .withPathParam("featureId", equalTo(featureId)) + .withQueryParam("revert", equalTo("true")) + .withQueryParam("latest", equalTo("false")) + .willReturn( + noContent())); + + String service = "Petclinic"; + String feature = "featureA"; + + try { + assertThat(endpoint.revert(userId, service, feature, FeaturesEndpoint.Revert.OLDEST_VALUE)) + .isTrue(); + } catch (IOException e) { + fail(); + } + } + + @Test + void getPricingTokenByUserId() { + + String userId = "e8e053c5-fd2b-4e4c-85a0-f1a52f0da72e"; + + wm.stubFor(post(urlPathTemplate("/features/{userId}/pricing-token")) + .withHeader("x-api-key", equalTo("prueba")) + .withRequestBody(absent()) + .withPathParam("userId", equalTo(userId)) + .willReturn( + ok().withBodyFile("pricing-token-response.json"))); + + try { + assertThat(endpoint.generatePricingTokenForUser(userId).length()).isEqualTo(879); + } catch (IOException e) { + fail(); + } + } +} diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java index 9355532..9f8e214 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.*; import java.time.Duration; +import java.time.LocalDateTime; import java.time.ZonedDateTime; import org.junit.jupiter.api.Test; @@ -26,10 +27,10 @@ void givenZeroRenewalDaysShouldThrow() { @Test void givenStartDateAfterEndDateShouldThrow() { - ZonedDateTime end = start.minusDays(1); + ZonedDateTime endDate = start.minusDays(1); assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> BillingPeriod.of(start, end)) + .isThrownBy(() -> BillingPeriod.of(start, endDate)) .withMessage("startDate is after endDate"); } @@ -43,5 +44,4 @@ void givenRenewableDateShouldBeRenowable() { assertThat(billingPeriod.getDuration().toDays()).isEqualTo(days); assertThat(billingPeriod.getRenewalDate()).isPresent().hasValue(end.plusDays(30).toLocalDateTime()); } - } diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java index 57e8ef5..3c1425a 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java @@ -40,7 +40,7 @@ void givenServiceWithBlankPlanShouldThrow() { void givenNoPlanOrAddOnShouldThrow() { assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> baseBuilder.build()) + .isThrownBy(baseBuilder::build) .withMessage("At least you have to be subscribed to a plan or add-on"); } diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java index 8728c2c..985e2fc 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java @@ -83,8 +83,7 @@ void givenServiceBuildCallBeforeCreationServiceShouldThrow() { SubscriptionRequest.Builder builder = SubscriptionRequest.builder(TEST_USER_CONTACT); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> builder - .endService()) + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(builder::endService) .withMessage("you must call 'newService' before adding a service"); } @@ -94,7 +93,7 @@ void whenNoRequiredParametersInputShouldThrow() { SubscriptionRequest.Builder builder = SubscriptionRequest.builder(null); assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> builder.build()) + .isThrownBy(builder::build) .withMessage("userContact must not be null"); } @@ -118,7 +117,7 @@ void givenNoEndServiceShouldThrow() { .startService("foo", "bar").plan("baz"); assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> builder.build()) + .isThrownBy(builder::build) .withMessage("finish the creation of your service by calling endService"); } @@ -128,7 +127,7 @@ void foo() { SubscriptionRequest.Builder builder = SubscriptionRequest.builder(TEST_USER_CONTACT); assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> builder.build()) + .isThrownBy(builder::build) .withMessage("you have to be subscribed al least to one service"); } diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java new file mode 100644 index 0000000..29fe1c2 --- /dev/null +++ b/space-client/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -0,0 +1,89 @@ +package io.github.pgmarc.space.contracts; + +import static org.assertj.core.api.Assertions.*; + + +import org.eclipse.jetty.server.Authentication; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.Set; + +public class SubscriptionTest { + + private static final UserContact TEST_CONTACT = UserContact.builder("123456789", "alex").build(); + private static final Service TEST_SERVICE = Service.builder("petclinic", "2025").plan("GOLD").build(); + + @Test + void givenSubscriptionShould() { + + String userId = "123456789"; + String username = "alex"; + UserContact contact = UserContact.builder(userId, username).build(); + ZonedDateTime start = ZonedDateTime.parse("2025-08-28T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2025-08-29T00:00:00Z"); + + String serviceName = "petclinic"; + BillingPeriod billingPeriod = BillingPeriod.of(start, end, Duration.ofDays(45)); + Service service = Service.builder(serviceName, "2025").plan("GOLD").build(); + + ZonedDateTime snapshotStart = ZonedDateTime.parse("2024-01-01T00:00:00Z"); + ZonedDateTime snapshotEnd = ZonedDateTime.parse("2025-01-01T00:00:00Z"); + Service snapshotService = Service.builder(serviceName, "2024").plan("FREE").build(); + Subscription.Snapshot snapshot1 = Subscription.Snapshot.of(snapshotStart.toLocalDateTime(), + snapshotEnd.toLocalDateTime(), Map.of("petclinic", snapshotService)); + Set history = Set.of(snapshot1); + + Subscription subscription = Subscription.builder(contact, billingPeriod, service) + .addSnapshots(history).build(); + + assertThat(subscription.getUsername()).isEqualTo(username); + assertThat(subscription.getStartDate()).isEqualTo(start.toLocalDateTime()); + assertThat(subscription.getEndDate()).isEqualTo(end.toLocalDateTime()); + assertThat(subscription.isAutoRenewable()).isTrue(); + assertThat(subscription.getRenewalDate()).hasValue(end.plusDays(45).toLocalDateTime()); + assertThat(subscription.getService(serviceName)).hasValue(service); + assertThat(subscription.getHistory()).contains(snapshot1); + assertThat(subscription.getHistory().get(0).getStartDate()).isEqualTo(snapshotStart.toLocalDateTime()); + assertThat(subscription.getHistory().get(0).getEndDate()).isEqualTo(snapshotEnd.toLocalDateTime()); + assertThat(subscription.getHistory().get(0).getServices()).isNotEmpty(); + assertThat(subscription.getHistory().get(0).getService(serviceName)).hasValue(snapshotService); + } + + @Test + void givenDateAfterSubscriptionShouldBeExpired() { + + ZonedDateTime start = ZonedDateTime.parse("2025-01-01T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2025-02-01T00:00:00Z"); + BillingPeriod period = BillingPeriod.of(start, end); + Subscription subscription = Subscription.builder(TEST_CONTACT, period, TEST_SERVICE).build(); + + LocalDateTime dateToCheck = ZonedDateTime.parse("2025-02-01T00:00:01Z").toLocalDateTime(); + assertThat(subscription.isExpired(dateToCheck)).isTrue(); + assertThat(subscription.isActive(dateToCheck)).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = + { + "2025-01-01T00:00:00Z", + "2025-01-15T00:00:00Z", + "2025-02-01T00:00:00Z" + }) + void givenDateInsideSubscriptionShouldBeActive(String utcDate) { + + ZonedDateTime start = ZonedDateTime.parse("2025-01-01T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2025-02-01T00:00:00Z"); + BillingPeriod period = BillingPeriod.of(start, end); + Subscription subscription = Subscription.builder(TEST_CONTACT, period, TEST_SERVICE).build(); + + LocalDateTime dateToCheck = ZonedDateTime.parse(utcDate).toLocalDateTime(); + assertThat(subscription.isExpired(dateToCheck)).isFalse(); + assertThat(subscription.isActive(dateToCheck)).isTrue(); + } +} diff --git a/space-client/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/space-client/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index a747fbb..b0bf3e0 100644 --- a/space-client/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/space-client/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -45,7 +45,7 @@ void givenInvalidUsernamesShouldThrow(String username) { UserContact.Builder builder = UserContact.builder(TEST_USER_ID, username); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> builder.build()); + .isThrownBy(builder::build); } diff --git a/space-client/src/test/java/io/github/pgmarc/space/deserializers/FeatureEvaluationDeserializerTest.java b/space-client/src/test/java/io/github/pgmarc/space/deserializers/FeatureEvaluationDeserializerTest.java new file mode 100644 index 0000000..d829eb3 --- /dev/null +++ b/space-client/src/test/java/io/github/pgmarc/space/deserializers/FeatureEvaluationDeserializerTest.java @@ -0,0 +1,90 @@ +package io.github.pgmarc.space.deserializers; + +import io.github.pgmarc.space.exceptions.SpaceApiException; +import io.github.pgmarc.space.features.FeatureEvaluationResult; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; + +class FeatureEvaluationDeserializerTest { + + + @Test + void givenBooleanEvaluationResultShouldCreateObject() { + + String service = "Petclinic"; + JSONObject jsonObject = new JSONObject() + .put("eval", true) + .put("used", JSONObject.NULL) + .put("limit", JSONObject.NULL) + .put("error", JSONObject.NULL); + + FeatureEvaluationDeserializer deserializer = new FeatureEvaluationDeserializer(service.length()); + assertThatNoException().isThrownBy(() -> deserializer.fromJson(jsonObject)); + } + + @Test + void givenJsonShouldCreateEvaluationResult() { + + String zoom = "Zoom"; + String zoomStorage = "zoom-storage"; + String zoomApiCalls = "zoom-apiCalls"; + String zoomBandwidth = "zoom-bandwidth"; + + JSONObject jsonObject = new JSONObject() + .put("eval", true) + .put("used", Map.of(zoomStorage, 50, zoomApiCalls, 1, zoomBandwidth, 20)) + .put("limit", Map.of(zoomStorage, 500, zoomApiCalls, 1000, zoomBandwidth, 200)) + .put("error", JSONObject.NULL); + + FeatureEvaluationDeserializer deserializer = new FeatureEvaluationDeserializer(zoom.length()); + assertThatNoException().isThrownBy(() -> deserializer.fromJson(jsonObject)); + + FeatureEvaluationResult res = deserializer.fromJson(jsonObject); + assertThat(res.isAvailable()).isTrue(); + assertThat(res.getConsumed("storage")).hasValue(50); + assertThat(res.getLimit("storage")).hasValue(500); + } + + @Test + void givenJsonShouldNotBeEmptyNonExistentService() { + + String service = "Zoom"; + String zoomStorage = "zoom-storage"; + + JSONObject jsonObject = new JSONObject() + .put("eval", true) + .put("used", Map.of(zoomStorage, 50)) + .put("limit", Map.of(zoomStorage, 500)) + .put("error", JSONObject.NULL); + + FeatureEvaluationDeserializer deserializer = new FeatureEvaluationDeserializer(service.length()); + assertThatNoException().isThrownBy(() -> deserializer.fromJson(jsonObject)); + + FeatureEvaluationResult res = deserializer.fromJson(jsonObject); + assertThat(res.getQuotas()).isNotEmpty(); + assertThat(res.getConsumed("test")).isEmpty(); + assertThat(res.getLimit("tests")).isEmpty(); + } + + @Test + void givenFeatureEvaluationErrorShouldThrow() { + + String service = "Petclinic"; + String code = "FLAG_NOT_FOUND"; + String message = "Feature pets not found in \"pricingContext\"."; + JSONObject jsonObject = new JSONObject() + .put("eval", false) + .put("used", JSONObject.NULL) + .put("limit", JSONObject.NULL) + .put("error", Map.of("code", code, "message", message)); + + FeatureEvaluationDeserializer deserializer = new FeatureEvaluationDeserializer(service.length()); + assertThatExceptionOfType(SpaceApiException.class) + .isThrownBy(() -> deserializer.fromJson(jsonObject)) + .withMessage("Feature pets not found in \"pricingContext\". Error code: FLAG_NOT_FOUND"); + } +} diff --git a/space-client/src/test/java/io/github/pgmarc/space/serializers/ConsumptionSerializerTest.java b/space-client/src/test/java/io/github/pgmarc/space/serializers/ConsumptionSerializerTest.java new file mode 100644 index 0000000..4f99b2c --- /dev/null +++ b/space-client/src/test/java/io/github/pgmarc/space/serializers/ConsumptionSerializerTest.java @@ -0,0 +1,44 @@ +package io.github.pgmarc.space.serializers; + +import io.github.pgmarc.space.features.Consumption; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class ConsumptionSerializerTest { + + private final ConsumptionSerializer serializer = new ConsumptionSerializer(); + + @Test + void givenConsumptionShouldSerialize() { + + String service = "Petclinic"; + int petsRegistered = 1; + long fooConsumption = (long) Integer.MAX_VALUE + 1; + float barConsumption = 0.1f; + double consumedSeconds = (double) Float.MAX_VALUE + 1; + + Consumption consumption = Consumption.builder() + .addInt(service, "maxPets", petsRegistered) + .addLong(service, "fooLimit", fooConsumption) + .addDouble(service, "maxSeconds", consumedSeconds) + .addFloat(service, "barLimit", barConsumption) + .build(); + + assertThatNoException().isThrownBy(() -> serializer.toJson(consumption)); + + String intUsageLimit = "petclinic-maxPets"; + String longUsageLimit = "petclinic-fooLimit"; + String floatUsageLimit = "petclinic-barLimit"; + String doubleUsageLimit = "petclinic-maxSeconds"; + JSONObject consumptionPayload = serializer.toJson(consumption); + + assertThat(consumptionPayload.keySet()).contains(intUsageLimit, longUsageLimit, floatUsageLimit, doubleUsageLimit); + assertThat(consumptionPayload.getInt(intUsageLimit)).isEqualTo(petsRegistered); + assertThat(consumptionPayload.getLong(longUsageLimit)).isEqualTo(fooConsumption); + assertThat(consumptionPayload.getFloat(floatUsageLimit)).isEqualTo(barConsumption); + assertThat(consumptionPayload.getDouble(doubleUsageLimit)).isEqualTo(consumedSeconds); + } + +} diff --git a/space-client/src/test/resources/__files/boolean-feature-evaluation.json b/space-client/src/test/resources/__files/boolean-feature-evaluation.json new file mode 100644 index 0000000..1993ea9 --- /dev/null +++ b/space-client/src/test/resources/__files/boolean-feature-evaluation.json @@ -0,0 +1,6 @@ +{ + "eval": true, + "used": null, + "limit": null, + "error": null +} diff --git a/space-client/src/test/resources/__files/invalid-expected-consumption.json b/space-client/src/test/resources/__files/invalid-expected-consumption.json new file mode 100644 index 0000000..9e6f8be --- /dev/null +++ b/space-client/src/test/resources/__files/invalid-expected-consumption.json @@ -0,0 +1,9 @@ +{ + "eval": false, + "limit": null, + "used": null, + "error": { + "code": "INVALID_EXPECTED_CONSUMPTION", + "message": "No expectedConsumption value was provided for limit 'petclinic-featureA', which is used in the evaluation of feature '{{request.path.featureId}}'. Please note that if you provide an expectedConsumption for any limit, you must provide it for all limits involved in that feature's evaluation." + } +} diff --git a/space-client/src/test/resources/__files/optimistic-evaluation-response.json b/space-client/src/test/resources/__files/optimistic-evaluation-response.json new file mode 100644 index 0000000..4afca21 --- /dev/null +++ b/space-client/src/test/resources/__files/optimistic-evaluation-response.json @@ -0,0 +1,10 @@ +{ + "eval": true, + "used": { + "petclinic-featureALimit": 100 + }, + "limit": { + "petclinic-featureALimit": 500 + }, + "error": null +} diff --git a/space-client/src/test/resources/__files/pricing-token-response.json b/space-client/src/test/resources/__files/pricing-token-response.json new file mode 100644 index 0000000..8a5c466 --- /dev/null +++ b/space-client/src/test/resources/__files/pricing-token-response.json @@ -0,0 +1,3 @@ +{ + "pricingToken": "eyJhbGciOiJIUzI1NiJ9.eyJmZWF0dXJlcyI6eyJ3aXJlbW9jay1tb2NrQVBJIjp7ImV2YWwiOnRydWUsImxpbWl0Ijp7IndpcmVtb2NrLW1vY2tBUElDYWxsc0xpbWl0IjoxMDAwMDAwMDB9LCJ1c2VkIjp7IndpcmVtb2NrLW1vY2tBUElDYWxsc0xpbWl0IjpudWxsfSwiZXJyb3IiOm51bGx9LCJ3aXJlbW9jay1SQkFDIjp7ImV2YWwiOnRydWUsImxpbWl0IjpudWxsLCJ1c2VkIjpudWxsLCJlcnJvciI6bnVsbH0sIndpcmVtb2NrLWdpdFN5bmMiOnsiZXZhbCI6dHJ1ZSwibGltaXQiOm51bGwsInVzZWQiOm51bGwsImVycm9yIjpudWxsfX0sInByaWNpbmdDb250ZXh0Ijp7ImZlYXR1cmVzIjp7IndpcmVtb2NrLW1vY2tBUEkiOnRydWUsIndpcmVtb2NrLVJCQUMiOnRydWUsIndpcmVtb2NrLWdpdFN5bmMiOnRydWV9LCJ1c2FnZUxpbWl0cyI6eyJ3aXJlbW9jay1tb2NrQVBJQ2FsbHNMaW1pdCI6MTAwMDAwMDAwLCJ3aXJlbW9jay1tYXhVc2VycyI6MTAwMDAwMDAwfX0sInN1YnNjcmlwdGlvbkNvbnRleHQiOnsid2lyZW1vY2stbW9ja0FQSUNhbGxzTGltaXQiOm51bGwsIndpcmVtb2NrLW1heFVzZXJzIjowfSwiaWF0IjoxNzU2NDAzOTE4LCJleHAiOjE3NTY0OTAzMTgsInN1YiI6IjQyNDI0MjQyNDIifQ.8rUSccrKK_hAqS3AX14cK4w-Snfe5Eoynx-KAgcPnNM" +}