diff --git a/.editorconfig b/.editorconfig index 2925810..3218641 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,7 +18,11 @@ indent_size = 4 indent_style = space indent_size = 2 +[*.json] +indent_style = space +indent_size = 2 + # Matches the exact files either package.json or .travis.yml [{pom.xml}] indent_style = tab -indent_size = 4 \ No newline at end of file +indent_size = 4 diff --git a/.github/workflows/code-analysis.yaml b/.github/workflows/code-analysis.yaml deleted file mode 100644 index 7211604..0000000 --- a/.github/workflows/code-analysis.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: SonarQube -on: - push: - branches: - - main - pull_request: - types: [opened, synchronize, reopened] -jobs: - build: - name: Build and analyze - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: 'temurin' # Alternative distribution options are available. - - name: Cache SonarQube packages - uses: actions/cache@v4 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v4 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - name: Build and analyze - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: > - mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:5.1.0.4751:sonar - -Dsonar.projectKey=pgmarc_space-java-client -Pcoverage \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9b0b77a..2918079 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,7 +1,7 @@ name: Run tests on: push jobs: - build: + tests: runs-on: ubuntu-latest steps: - name: Checkout code @@ -12,4 +12,4 @@ jobs: java-version: '11' distribution: 'temurin' - name: Run tests - run: mvn test \ No newline at end of file + run: mvn test diff --git a/pom.xml b/pom.xml index fb34988..f0809ae 100644 --- a/pom.xml +++ b/pom.xml @@ -137,51 +137,28 @@ junit-jupiter test - - org.mock-server - mockserver-junit-jupiter-no-dependencies - 5.14.0 - test - + + org.wiremock + wiremock + 3.13.1 + test + + + org.assertj + assertj-core + 3.26.3 + test + - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.6.0 - - - com.puppycrawl.tools - checkstyle - 10.26.1 - - - org.jacoco jacoco-maven-plugin 0.8.13 - - com.github.spotbugs - spotbugs-maven-plugin - 4.9.3.2 - - - com.github.spotbugs - spotbugs - 4.9.4 - - - - - org.apache.maven.plugins - maven-pmd-plugin - 3.27.0 - @@ -246,68 +223,6 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - - google_checks.xml - true - true - false - - - - validate - validate - - check - - - - - - com.github.spotbugs - spotbugs-maven-plugin - - - - com.h3xstream.findsecbugs - findsecbugs-plugin - 1.12.0 - - - - - - check - - check - - - - - - org.apache.maven.plugins - maven-pmd-plugin - - true - false - - - - check - - check - - - - cpd-check - - cpd-check - - - - diff --git a/src/main/java/io/github/pgmarc/space/Config.java b/src/main/java/io/github/pgmarc/space/Config.java new file mode 100644 index 0000000..0c046b8 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/Config.java @@ -0,0 +1,98 @@ +package io.github.pgmarc.space; + +import java.time.Duration; +import java.util.Objects; + +import okhttp3.HttpUrl; + +public final class Config { + + private static final String DEFAULT_SCHEME = "http"; + + private final String apiKey; + private final String host; + private final int port; + private final String prefixPath; + private final Duration readTimeout; + private final Duration writeTimeout; + + private Config(Builder builder) { + this.host = builder.host; + this.apiKey = builder.apiKey; + this.port = builder.port; + this.prefixPath = builder.prefixPath; + this.readTimeout = builder.readTimeout; + this.writeTimeout = builder.writeTimeout; + } + + public String getApiKey() { + return apiKey; + } + + public Duration getReadTimeout() { + return readTimeout; + } + + public Duration getWriteTimeout() { + return writeTimeout; + } + + public HttpUrl getUrl() { + return new HttpUrl.Builder().scheme(DEFAULT_SCHEME) + .host(this.host).port(this.port) + .addPathSegments(prefixPath).build(); + } + + public static Builder builder(String host, String apiKey) { + return new Builder(host, apiKey); + } + + public static final class Builder { + + private final String host; + private final String apiKey; + private int port = 5403; + private String prefixPath = "api/v1"; + private Duration readTimeout; + private Duration writeTimeout; + + private Builder(String host, String apiKey) { + this.host = host; + this.apiKey = apiKey; + } + + public Builder port(int port) { + this.port = port; + return this; + } + + public Builder readTimeout(Duration duration) { + if (duration != null) { + this.readTimeout = duration; + } + return this; + } + + public Builder writeTimeout(Duration duration) { + if (duration != null) { + this.writeTimeout = duration; + } + return this; + } + + public Builder prefixPath(String prefixPath) { + if (prefixPath != null) { + this.prefixPath = prefixPath; + } + return this; + } + + public Config build() { + Objects.requireNonNull(this.host, "host must not be null"); + Objects.requireNonNull(this.apiKey, "api key must not be null"); + return new Config(this); + } + + } + +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/AddOn.java b/src/main/java/io/github/pgmarc/space/contracts/AddOn.java new file mode 100644 index 0000000..07a4104 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/AddOn.java @@ -0,0 +1,46 @@ +package io.github.pgmarc.space.contracts; + +public final class AddOn { + + private final String name; + private final long quantity; + + AddOn(String name, long quantity) { + this.name = name; + this.quantity = quantity; + } + + public String getName() { + return name; + } + + public long getQuantity() { + return quantity; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.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; + AddOn other = (AddOn) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java new file mode 100644 index 0000000..2c057de --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -0,0 +1,131 @@ +package io.github.pgmarc.space.contracts; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.Optional; + +public final class BillingPeriod { + + private final ZonedDateTime startDate; + private final ZonedDateTime endDate; + private Duration renewalDays; + + private BillingPeriod(ZonedDateTime startDate, ZonedDateTime endDate, Duration renewalDays) { + this.startDate = startDate; + this.endDate = endDate; + this.renewalDays = renewalDays; + } + + LocalDateTime getStartDate() { + return startDate.toLocalDateTime(); + } + + LocalDateTime getEndDate() { + return endDate.toLocalDateTime(); + } + + Duration getDuration() { + return renewalDays; + } + + boolean isExpired(LocalDateTime dateTime) { + return endDate.isAfter(ZonedDateTime.of(dateTime, ZoneId.of("UTC"))); + } + + boolean isAutoRenewable() { + return renewalDays != null; + } + + Optional getRenewalDate() { + return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays).toLocalDateTime() : null); + } + + public void setRenewalDays(Duration renewalDays) { + validateRenewalDays(renewalDays); + this.renewalDays = renewalDays; + } + + private static void validateRenewalDays(Duration renewalDays) { + if (renewalDays != null && renewalDays.toDays() <= 0) { + throw new IllegalArgumentException("your subscription cannot expire in less than one day"); + } + } + + public static BillingPeriod of(ZonedDateTime startDate, ZonedDateTime endDate) { + return of(startDate, endDate, null); + } + + public static BillingPeriod of(ZonedDateTime startDate, ZonedDateTime endDate, Duration renewalDays) { + Objects.requireNonNull(startDate, "startDate must not be null"); + Objects.requireNonNull(endDate, "endDate must not be null"); + if (startDate.isAfter(endDate)) { + throw new IllegalStateException("startDate is after endDate"); + } + validateRenewalDays(renewalDays); + return new BillingPeriod(startDate, endDate, renewalDays); + } + + public enum Keys { + + START_DATE("startDate"), + END_DATE("endDate"), + AUTORENEW("autoRenew"), + RENEWAL_DAYS("renewalDays"); + + private final String name; + + private Keys(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @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; + } + + @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; + } + + @Override + public String toString() { + return "From " + startDate + " to " + endDate + ", renews in " + renewalDays; + } +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java new file mode 100644 index 0000000..2014a99 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java @@ -0,0 +1,103 @@ +package io.github.pgmarc.space.contracts; + +import java.io.IOException; +import java.util.Objects; + +import org.json.JSONObject; + +import io.github.pgmarc.space.deserializers.ErrorDeserializer; +import io.github.pgmarc.space.deserializers.SubscriptionDeserializer; +import io.github.pgmarc.space.exceptions.SpaceApiException; +import io.github.pgmarc.space.serializers.SubscriptionRequestSerializer; +import io.github.pgmarc.space.serializers.SubscriptionUpdateRequestSerializer; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public final class ContractsEndpoint { + + private static final MediaType JSON = MediaType.get("application/json"); + private static final String ENDPOINT = "contracts"; + private static final String STATUS_CODE = "statusCode"; + + private final OkHttpClient client; + private final HttpUrl baseUrl; + private final SubscriptionDeserializer subscriptionDeserializer = new SubscriptionDeserializer(); + private final SubscriptionRequestSerializer subscriptionRequestSerializer = new SubscriptionRequestSerializer(); + private final SubscriptionUpdateRequestSerializer subscriptionUpdateRequestSerializer = new SubscriptionUpdateRequestSerializer(); + private final ErrorDeserializer errorDeserializer = new ErrorDeserializer(); + private final Headers requiredHeaders; + + public ContractsEndpoint(OkHttpClient client, HttpUrl baseUrl, String apiKey) { + this.client = client; + this.baseUrl = baseUrl; + this.requiredHeaders = new Headers.Builder().add("Accept", JSON.toString()) + .add("x-api-key", apiKey).build(); + } + + public Subscription addContract(SubscriptionRequest subscriptionReq) throws IOException { + Objects.requireNonNull(subscriptionReq, "subscription request must not be null"); + + HttpUrl url = this.baseUrl.newBuilder().addPathSegment(ENDPOINT).build(); + Subscription res = null; + Request request = new Request.Builder().url(url) + .post(RequestBody.create(subscriptionRequestSerializer.toJson(subscriptionReq).toString(), JSON)) + .headers(requiredHeaders).build(); + try (Response response = client.newCall(request).execute()) { + ResponseBody responseBody = response.body(); + JSONObject jsonResponse = new JSONObject(responseBody.string()); + if (!response.isSuccessful()) { + jsonResponse.put(STATUS_CODE, response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + + res = subscriptionDeserializer.fromJson(jsonResponse); + } + + return res; + } + + public Subscription getContractByUserId(String userId) throws IOException { + + HttpUrl url = this.baseUrl.newBuilder().addPathSegment(ENDPOINT).addEncodedPathSegment(userId).build(); + Subscription res = null; + Request request = new Request.Builder().url(url).headers(requiredHeaders).build(); + 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)); + } + res = subscriptionDeserializer.fromJson(jsonResponse); + } + + return res; + } + + public Subscription updateContractByUserId(String userId, SubscriptionUpdateRequest subscription) + throws IOException { + HttpUrl url = this.baseUrl.newBuilder().addPathSegment(ENDPOINT).addEncodedPathSegment(userId).build(); + Subscription res = null; + Request request = new Request.Builder().url(url) + .put(RequestBody.create(subscriptionUpdateRequestSerializer.toJson(subscription).toString(), JSON)) + .headers(requiredHeaders) + .build(); + try (Response response = client.newCall(request).execute()) { + ResponseBody responseBody = response.body(); + JSONObject jsonResponse = new JSONObject(responseBody.string()); + if (!response.isSuccessful()) { + jsonResponse.put(STATUS_CODE, response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + res = subscriptionDeserializer.fromJson(jsonResponse); + } + + return res; + } + +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/Service.java b/src/main/java/io/github/pgmarc/space/contracts/Service.java new file mode 100644 index 0000000..98cb1d9 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/Service.java @@ -0,0 +1,138 @@ +package io.github.pgmarc.space.contracts; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public final class Service { + + private final String name; + private final String version; + private final Map addOns; + private String plan; + + private Service(Builder builder) { + this.name = builder.name; + this.version = builder.version; + this.addOns = Collections.unmodifiableMap(builder.addOns); + this.plan = builder.plan; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public Optional getPlan() { + return Optional.ofNullable(plan); + } + + public Optional getAddOn(String addOn) { + Objects.requireNonNull(addOn, "key must not be null"); + return Optional.ofNullable(this.addOns.get(addOn)); + } + + public Set getAddOns() { + return Set.copyOf(this.addOns.values()); + } + + public static Builder builder(String name, String version) { + return new Builder(name, version); + } + + @Override + public String toString() { + return name + ": " + version + " plan" + plan + " addOns " + addOns; + } + + @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; + } + + @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 static final class Builder { + + private final String name; + private final String version; + private final Map addOns = new HashMap<>(); + private String plan; + + private Builder(String name, String version) { + this.name = name; + this.version = version; + } + + public Builder plan(String plan) { + if (plan.isBlank()) { + throw new IllegalArgumentException("plan must not be blank"); + } + this.plan = plan; + return this; + } + + public Builder addOn(String name, long quantity) { + Objects.requireNonNull(name, "add-on name must not be null"); + if (name.isBlank()) { + throw new IllegalArgumentException("add-on name must no be blank"); + } + if (quantity <= 0) { + throw new IllegalArgumentException(name + " quantity must be greater than 0"); + } + this.addOns.put(name, new AddOn(name, quantity)); + return this; + } + + public Service build() { + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(version, "version must not be null"); + if (plan == null && this.addOns.isEmpty()) { + throw new IllegalStateException("At least you have to be subscribed to a plan or add-on"); + } + return new Service(this); + } + } +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java new file mode 100644 index 0000000..9c1f552 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -0,0 +1,246 @@ +package io.github.pgmarc.space.contracts; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public final class Subscription { + + private final UserContact userContact; + private final Map services; + private final BillingPeriod billingPeriod; + private final List history; + private final Map> usageLevels; + + private Subscription(Builder builder) { + this.userContact = builder.userContact; + this.billingPeriod = builder.billingPeriod; + this.services = Collections.unmodifiableMap(builder.services); + this.history = Collections.unmodifiableList(builder.history); + this.usageLevels = Collections.unmodifiableMap(builder.usageLevels); + } + + public static Builder builder(UserContact userContact, BillingPeriod billingPeriod, + Service service) { + return new Builder(userContact, billingPeriod).subscribe(service); + } + + public static Builder builder(UserContact usagerContact, BillingPeriod billingPeriod, + Collection services) { + return new Builder(usagerContact, billingPeriod).subscribeAll(services); + } + + public LocalDateTime getStartDate() { + return billingPeriod.getStartDate(); + } + + public LocalDateTime getEndDate() { + return billingPeriod.getEndDate(); + } + + public Optional getRenewalDuration() { + return Optional.of(billingPeriod.getDuration()); + } + + public boolean isAutoRenewable() { + return billingPeriod.isAutoRenewable(); + } + + public boolean isExpired(LocalDateTime date) { + return billingPeriod.isExpired(date); + } + + public Optional getRenewalDate() { + return billingPeriod.getRenewalDate(); + } + + public String getUserId() { + return userContact.getUserId(); + } + + public String getUsername() { + return userContact.getUsername(); + } + + public Optional getService(String serviceName) { + return Optional.ofNullable(this.services.get(serviceName)); + } + + public Map getServicesMap() { + return services; + } + + public Set getServices() { + return Set.copyOf(services.values()); + } + + public List getHistory() { + return history; + } + + public Map> getUsageLevels() { + return usageLevels; + } + + public enum Keys { + USER_CONTACT("userContact"), + BILLING_PERIOD("billingPeriod"), + CONTRACTED_SERVICES("contractedServices"), + SUBSCRIPTION_PLANS("subscriptionPlans"), + SUBSCRIPTION_ADDONS("subscriptionAddOns"), + USAGE_LEVEL("usageLevel"), + HISTORY("history"); + + private final String name; + + private Keys(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + public static final class Builder { + + private final BillingPeriod billingPeriod; + private final UserContact userContact; + private final Map services = new HashMap<>(); + private final List history = new ArrayList<>(); + private final Map> usageLevels = new HashMap<>(); + + private Builder(UserContact userContact, BillingPeriod billingPeriod) { + this.billingPeriod = billingPeriod; + this.userContact = userContact; + } + + public Builder renewIn(Duration renewalDays) { + this.billingPeriod.setRenewalDays(renewalDays); + return this; + } + + public Builder subscribe(Service service) { + this.services.put(service.getName(), Objects.requireNonNull(service, "service must not be null")); + return this; + } + + private Map collectionToServiceMap(Collection services) { + return services.stream() + .collect(Collectors.toUnmodifiableMap(Service::getName, service -> service)); + } + + public Builder subscribeAll(Collection services) { + Objects.requireNonNull(services, "services must not be null"); + this.services.putAll(collectionToServiceMap(services)); + return this; + } + + public Builder addSnapshots(Collection snaphsots) { + Objects.requireNonNull(snaphsots, "snapshots must not be null"); + this.history.addAll(snaphsots); + return this; + } + + public Builder addUsageLevels(Map> usageLevels) { + this.usageLevels.putAll(usageLevels); + return this; + } + + public Subscription build() { + Objects.requireNonNull(billingPeriod, "billingPeriod must not be null"); + Objects.requireNonNull(userContact, "userContact must not be null"); + return new Subscription(this); + } + } + + public static final class Snapshot { + + private final LocalDateTime starDateTime; + private final LocalDateTime enDateTime; + private final Map services; + + public Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, + Map services) { + this.starDateTime = startDateTime; + this.enDateTime = 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; + } + + public Map getServices() { + return Collections.unmodifiableMap(services); + } + + 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); + } + + @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/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java new file mode 100644 index 0000000..b4ad24a --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java @@ -0,0 +1,117 @@ +package io.github.pgmarc.space.contracts; + +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public final class SubscriptionRequest { + + private final UserContact userContact; + private final Set services; + private final Duration renewalDays; + + private SubscriptionRequest(Builder builder) { + this.userContact = builder.userContact; + this.renewalDays = builder.renewalDays; + this.services = Collections.unmodifiableSet(builder.services); + } + + public static Builder builder(UserContact userContact) { + return new Builder(userContact); + } + + public UserContact getUserContact() { + return userContact; + } + + public Set getServices() { + return services; + } + + public Duration getRenewalDays() { + return renewalDays; + } + + public static final class Builder { + + private final UserContact userContact; + private final Set services = new HashSet<>(); + private Service.Builder serviceBuilder; + private Duration renewalDays; + + private Builder(UserContact userContact) { + this.userContact = userContact; + } + + public Builder subscribe(Service service) { + this.services.add(Objects.requireNonNull(service, "service must not be null")); + return this; + } + + public Builder subscribeAll(Collection services) { + Objects.requireNonNull(services, "services must not be null"); + this.services.addAll(services); + return this; + } + + public Builder renewIn(Duration renewalDays) { + this.renewalDays = renewalDays; + return this; + } + + public Builder startService(String name, String version) { + if (isServiceBuilderAlive()) { + throw new IllegalStateException("you must build a service before creating another"); + } + this.serviceBuilder = Service.builder(name, version); + return this; + } + + public Builder plan(String plan) { + validateServiceBuilderCalled("you must call 'newService' before setting a plan: " + plan); + serviceBuilder.plan(plan); + return this; + } + + public Builder addOn(String addOnName, long quantity) { + validateServiceBuilderCalled("you must call 'newService' before setting an add-on: " + addOnName); + serviceBuilder.addOn(addOnName, quantity); + return this; + } + + public Builder endService() { + validateServiceBuilderCalled("you must call 'newService' before adding a service"); + services.add(serviceBuilder.build()); + destroyServiceBuilder(); + return this; + } + + public SubscriptionRequest build() { + if (isServiceBuilderAlive()) { + throw new IllegalStateException("finish the creation of your service by calling endService"); + } + Objects.requireNonNull(userContact, "userContact must not be null"); + if (services.isEmpty()) { + throw new IllegalStateException("you have to be subscribed al least to one service"); + } + return new SubscriptionRequest(this); + } + + private boolean isServiceBuilderAlive() { + return serviceBuilder != null; + } + + private void validateServiceBuilderCalled(String message) { + if (!isServiceBuilderAlive()) { + throw new IllegalStateException(message); + } + } + + private void destroyServiceBuilder() { + this.serviceBuilder = null; + } + } +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java new file mode 100644 index 0000000..f117600 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java @@ -0,0 +1,48 @@ +package io.github.pgmarc.space.contracts; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class SubscriptionUpdateRequest { + + private final Set services = new HashSet<>(); + private Service.Builder serviceBuilder; + + private SubscriptionUpdateRequest() { + + } + + public Set getServices() { + return Set.copyOf(services); + } + + public static SubscriptionUpdateRequest builder() { + return new SubscriptionUpdateRequest(); + } + + public SubscriptionUpdateRequest service(String name, String version) { + this.serviceBuilder = Service.builder(name, version); + return this; + } + + public SubscriptionUpdateRequest plan(String plan) { + Objects.requireNonNull(serviceBuilder, "you call service first"); + serviceBuilder.plan(plan); + return this; + } + + public SubscriptionUpdateRequest addOn(String name, long quantity) { + Objects.requireNonNull(serviceBuilder, "you call service first"); + this.serviceBuilder.addOn(name, quantity); + return this; + } + + public SubscriptionUpdateRequest add() { + Objects.requireNonNull(serviceBuilder, "you call service first"); + this.services.add(serviceBuilder.build()); + this.serviceBuilder = null; + return this; + } + +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java new file mode 100644 index 0000000..b351d67 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java @@ -0,0 +1,67 @@ +package io.github.pgmarc.space.contracts; + +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.Optional; + +public final class UsageLevel { + + private final String name; + private final double consumed; + private ZonedDateTime resetTimestamp; + + public enum Keys { + CONSUMED("consumed"), + RESET_TIMESTAMP("resetTimestamp"); + + private final String name; + + private Keys(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + private UsageLevel(String name, double consumed, ZonedDateTime resetTimestamp) { + this.name = name; + this.consumed = consumed; + this.resetTimestamp = resetTimestamp; + } + + public String getName() { + return name; + } + + public Optional getResetTimestamp() { + return resetTimestamp != null ? Optional.of(resetTimestamp.toLocalDateTime()) : Optional.empty(); + } + + public boolean isRenewableUsageLimit() { + return resetTimestamp != null; + } + + public double getConsumption() { + return consumed; + } + + private static void validateUsageLevel(String name, double consumed) { + Objects.requireNonNull(name, "usage limit name must not be null"); + if (consumed <= 0) { + throw new IllegalArgumentException("consumption must be greater than 0"); + } + } + + public static UsageLevel of(String name, double consumed) { + return of(name, consumed, null); + } + + public static UsageLevel of(String name, double consumed, ZonedDateTime resetTimestamp) { + validateUsageLevel(name, consumed); + return new UsageLevel(name, consumed, resetTimestamp); + } +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java new file mode 100644 index 0000000..95f17b3 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java @@ -0,0 +1,157 @@ +package io.github.pgmarc.space.contracts; + +import java.util.Objects; +import java.util.Optional; + +public final class UserContact { + + private final String userId; + private final String username; + private final String firstName; + private final String lastName; + private final String email; + private final String phone; + + private UserContact(Builder builder) { + this.userId = builder.userId; + this.username = builder.username; + this.firstName = builder.firstName; + this.lastName = builder.lastName; + this.email = builder.email; + this.phone = builder.phone; + } + + public static Builder builder(String userId, String username) { + Objects.requireNonNull(userId, "userId must not be null"); + Objects.requireNonNull(username, "username must not be null"); + return new Builder(userId, username); + } + + public String getUserId() { + return userId; + } + + public String getUsername() { + return username; + } + + public Optional getFirstName() { + return Optional.ofNullable(firstName); + } + + public Optional getLastName() { + return Optional.ofNullable(lastName); + } + + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((userId == null) ? 0 : userId.hashCode()); + result = prime * result + ((username == null) ? 0 : username.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; + UserContact other = (UserContact) obj; + if (userId == null) { + if (other.userId != null) + return false; + } else if (!userId.equals(other.userId)) + return false; + if (username == null) { + if (other.username != null) + return false; + } else if (!username.equals(other.username)) + return false; + return true; + } + + public static class Builder { + + private final String userId; + private final String username; + private String firstName; + private String lastName; + private String email; + private String phone; + + private Builder(String userId, String username) { + this.userId = userId; + this.username = username; + } + + public Builder firstName(String firstName) { + this.firstName = firstName; + return this; + } + + public Builder lastName(String lastName) { + this.lastName = lastName; + return this; + } + + public Builder email(String email) { + this.email = email; + return this; + } + + public Builder phone(String phone) { + this.phone = phone; + return this; + } + + public UserContact build() { + if (username.isBlank()) { + throw new IllegalArgumentException("username is blank"); + } + validateUserNameLength(); + return new UserContact(this); + } + + private void validateUserNameLength() { + int length = this.username.length(); + boolean isValidLength = length >= 3 && length <= 30; + if (!isValidLength) { + throw new IllegalArgumentException("username must be between 3 and 30 characters. Current " + username + + " has " + length + "characters"); + } + } + } + + public enum Keys { + + USER_ID("userId"), + USERNAME("username"), + FIRST_NAME("firstName"), + LAST_NAME("lastName"), + EMAIL("email"), + PHONE("phone"); + + private final String name; + + private Keys(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java new file mode 100644 index 0000000..457c4cd --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java @@ -0,0 +1,28 @@ +package io.github.pgmarc.space.deserializers; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.Objects; + +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.BillingPeriod; + +class BillingPeriodDeserializer implements JsonDeserializable { + + @Override + public BillingPeriod fromJson(JSONObject json) { + + Objects.requireNonNull(json, "billing period json must not be null"); + ZonedDateTime start = ZonedDateTime.parse(json.getString(BillingPeriod.Keys.START_DATE.toString())); + ZonedDateTime end = ZonedDateTime.parse(json.getString(BillingPeriod.Keys.END_DATE.toString())); + Duration renewalDays = null; + if (json.has(BillingPeriod.Keys.RENEWAL_DAYS.toString())) { + renewalDays = Duration.ofDays(json.getLong(BillingPeriod.Keys.RENEWAL_DAYS.toString())); + } + + return BillingPeriod.of(start, end, renewalDays); + + } + +} diff --git a/src/main/java/io/github/pgmarc/space/deserializers/ErrorDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/ErrorDeserializer.java new file mode 100644 index 0000000..231e83a --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/deserializers/ErrorDeserializer.java @@ -0,0 +1,55 @@ +package io.github.pgmarc.space.deserializers; + +import java.util.HashSet; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONObject; + +import io.github.pgmarc.space.exceptions.SpaceApiError; + +public final class ErrorDeserializer implements JsonDeserializable { + + private enum Keys { + ERROR("error"), + ERRORS("errors"), + CODE("statusCode"), + TYPE("field"), + MSG("msg"), + PATH("path"), + LOCATION("location"), + VALUE("value"); + + private final String name; + + private Keys(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Override + public SpaceApiError fromJson(JSONObject json) { + + Set messages = new HashSet<>(); + if (json.has(Keys.ERROR.toString())) { + messages.add(json.getString(Keys.ERROR.toString())); + } + + if (json.has(Keys.ERRORS.toString())) { + JSONArray jsonErrors = json.getJSONArray(Keys.ERRORS.toString()); + for (int i = 0; i < jsonErrors.length(); i++) { + messages.add(jsonErrors.getJSONObject(i).getString(Keys.MSG.toString())); + } + } + + int statusCode = json.getInt(Keys.CODE.toString()); + + return new SpaceApiError(statusCode, messages); + } + +} diff --git a/src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java b/src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java new file mode 100644 index 0000000..529909b --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java @@ -0,0 +1,8 @@ +package io.github.pgmarc.space.deserializers; + +import org.json.JSONObject; + +public interface JsonDeserializable { + + U fromJson(JSONObject json); +} diff --git a/src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java new file mode 100644 index 0000000..a364a8f --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java @@ -0,0 +1,32 @@ +package io.github.pgmarc.space.deserializers; + +import java.util.HashMap; +import java.util.Map; + +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.Service; +import io.github.pgmarc.space.contracts.Subscription; + +final class ServicesDeserializer implements JsonDeserializable> { + + @Override + public Map fromJson(JSONObject json) { + + JSONObject contractedServices = json.getJSONObject(Subscription.Keys.CONTRACTED_SERVICES.toString()); + JSONObject subscriptionPlans = json.getJSONObject(Subscription.Keys.SUBSCRIPTION_PLANS.toString()); + JSONObject subscriptionAddOns = json.getJSONObject(Subscription.Keys.SUBSCRIPTION_ADDONS.toString()); + Map services = new HashMap<>(); + + for (String serviceName : contractedServices.keySet()) { + Service.Builder serviceBuilder = Service.builder(serviceName, contractedServices.getString(serviceName)) + .plan(subscriptionPlans.getString(serviceName)); + + for (String addOnName : subscriptionAddOns.getJSONObject(serviceName).keySet()) { + serviceBuilder.addOn(addOnName, subscriptionAddOns.getJSONObject(serviceName).getLong(addOnName)); + } + services.put(serviceName, serviceBuilder.build()); + } + return services; + } +} diff --git a/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java new file mode 100644 index 0000000..f9baa8e --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java @@ -0,0 +1,37 @@ +package io.github.pgmarc.space.deserializers; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.BillingPeriod; +import io.github.pgmarc.space.contracts.Subscription; +import io.github.pgmarc.space.contracts.Subscription.Snapshot; + +class SnapshotsDeserializer implements JsonDeserializable> { + + private final ServicesDeserializer servicesDeserializer; + + SnapshotsDeserializer(ServicesDeserializer servicesDeserializer) { + this.servicesDeserializer = servicesDeserializer; + } + + @Override + 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); + OffsetDateTime startUtc = OffsetDateTime + .parse(snaphsot.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))); + } + return res; + } + +} diff --git a/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java new file mode 100644 index 0000000..6ae231b --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java @@ -0,0 +1,35 @@ +package io.github.pgmarc.space.deserializers; + +import java.util.List; +import java.util.Map; + +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.BillingPeriod; +import io.github.pgmarc.space.contracts.UsageLevel; +import io.github.pgmarc.space.contracts.UserContact; +import io.github.pgmarc.space.contracts.Service; +import io.github.pgmarc.space.contracts.Subscription; +import io.github.pgmarc.space.contracts.Subscription.Snapshot; + +public final class SubscriptionDeserializer implements JsonDeserializable { + + private final BillingPeriodDeserializer billingSerializer = new BillingPeriodDeserializer(); + private final UserContactDeserializer userContactDeserializer = new UserContactDeserializer(); + private final UsageLevelDeserializer usageLevelDeserializer = new UsageLevelDeserializer(); + private final ServicesDeserializer servicesDeserializer = new ServicesDeserializer(); + private final SnapshotsDeserializer historyDeserializer = new SnapshotsDeserializer(servicesDeserializer); + + @Override + public Subscription fromJson(JSONObject json) { + BillingPeriod billingPeriod = billingSerializer.fromJson(json.getJSONObject(Subscription.Keys.BILLING_PERIOD.toString())); + UserContact userContact = userContactDeserializer.fromJson(json.getJSONObject(Subscription.Keys.USER_CONTACT.toString())); + Map> usageLevels = usageLevelDeserializer.fromJson( + json.getJSONObject(Subscription.Keys.USAGE_LEVEL.toString())); + Map services = servicesDeserializer.fromJson(json); + List history = historyDeserializer.fromJson(json); + return Subscription.builder(userContact, billingPeriod, services.values()) + .addUsageLevels(usageLevels).addSnapshots(history).build(); + } + +} diff --git a/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java new file mode 100644 index 0000000..c993a34 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java @@ -0,0 +1,41 @@ +package io.github.pgmarc.space.deserializers; + +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.UsageLevel; + +final class UsageLevelDeserializer implements JsonDeserializable>> { + + private Map getServiceUsageLevels(JSONObject usageLevels) { + Map res = new HashMap<>(); + for (String usageLimitName : usageLevels.keySet()) { + JSONObject rawUsageLevel = usageLevels.getJSONObject(usageLimitName); + ZonedDateTime resetTimestamp = null; + if (rawUsageLevel.has(UsageLevel.Keys.RESET_TIMESTAMP.toString())) { + resetTimestamp = ZonedDateTime + .parse(rawUsageLevel.getString(UsageLevel.Keys.RESET_TIMESTAMP.toString())); + } + UsageLevel ul = UsageLevel.of(usageLimitName, rawUsageLevel.getDouble(UsageLevel.Keys.CONSUMED.toString()), + resetTimestamp); + res.put(usageLimitName, ul); + } + return Collections.unmodifiableMap(res); + + } + + @Override + public Map> fromJson(JSONObject usageLevel) { + Objects.requireNonNull(usageLevel, "usage level must not be null"); + Map> res = new HashMap<>(); + for (String serviceName : usageLevel.keySet()) { + res.put(serviceName, getServiceUsageLevels(usageLevel.getJSONObject(serviceName))); + } + return Collections.unmodifiableMap(res); + } +} diff --git a/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java new file mode 100644 index 0000000..c51cd5b --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java @@ -0,0 +1,24 @@ +package io.github.pgmarc.space.deserializers; + +import java.util.Objects; + +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.UserContact; + +final class UserContactDeserializer implements JsonDeserializable { + + @Override + public UserContact fromJson(JSONObject json) { + + Objects.requireNonNull(json, "user contact json must not be null"); + return UserContact.builder(json.getString(UserContact.Keys.USER_ID.toString()), + json.getString(UserContact.Keys.USERNAME.toString())) + .firstName(json.optString(UserContact.Keys.FIRST_NAME.toString(), null)) + .lastName(json.optString(UserContact.Keys.LAST_NAME.toString(), null)) + .email(json.optString(UserContact.Keys.EMAIL.toString(), null)) + .phone(json.optString(UserContact.Keys.PHONE.toString(), null)) + .build(); + } + +} diff --git a/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiError.java b/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiError.java new file mode 100644 index 0000000..ed2779d --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiError.java @@ -0,0 +1,26 @@ +package io.github.pgmarc.space.exceptions; + +import java.util.Collections; +import java.util.Set; + +public class SpaceApiError { + + private final Set messages; + + private final int code; + + public SpaceApiError(int code, Set messages) { + this.code = code; + this.messages = Collections.unmodifiableSet(messages); + } + + int getCode() { + return code; + } + + @Override + public String toString() { + return String.join("\n", messages); + } + +} diff --git a/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java b/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java new file mode 100644 index 0000000..1ce43af --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java @@ -0,0 +1,16 @@ +package io.github.pgmarc.space.exceptions; + + +public class SpaceApiException extends RuntimeException { + + private final transient SpaceApiError error; + + public SpaceApiException(SpaceApiError error) { + super(error.toString()); + this.error = error; + } + + public int getCode() { + return error.getCode(); + } +} diff --git a/src/main/java/io/github/pgmarc/space/serializers/JsonSerializable.java b/src/main/java/io/github/pgmarc/space/serializers/JsonSerializable.java new file mode 100644 index 0000000..66ec98e --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/JsonSerializable.java @@ -0,0 +1,7 @@ +package io.github.pgmarc.space.serializers; + +import org.json.JSONObject; + +public interface JsonSerializable { + JSONObject toJson(T object); +} diff --git a/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java b/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java new file mode 100644 index 0000000..50dc460 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java @@ -0,0 +1,84 @@ +package io.github.pgmarc.space.serializers; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.AddOn; +import io.github.pgmarc.space.contracts.BillingPeriod; +import io.github.pgmarc.space.contracts.Service; +import io.github.pgmarc.space.contracts.Subscription; +import io.github.pgmarc.space.contracts.SubscriptionRequest; +import io.github.pgmarc.space.contracts.UserContact; + +public class SubscriptionRequestSerializer implements JsonSerializable { + + @Override + public JSONObject toJson(SubscriptionRequest object) { + + JSONObject json = new JSONObject() + .put(Subscription.Keys.USER_CONTACT.toString(), userContact(object.getUserContact())) + .put(Subscription.Keys.BILLING_PERIOD.toString(), + Map.of(BillingPeriod.Keys.AUTORENEW.toString(), object.getRenewalDays() != null)) + .put(Subscription.Keys.CONTRACTED_SERVICES.toString(), contractedServices(object.getServices())) + .put(Subscription.Keys.SUBSCRIPTION_PLANS.toString(), subscriptionPlans(object.getServices())) + .put(Subscription.Keys.SUBSCRIPTION_ADDONS.toString(), subscriptionAddOns(object.getServices())); + + if (object.getRenewalDays() != null) { + json.getJSONObject(Subscription.Keys.BILLING_PERIOD.toString()) + .put(BillingPeriod.Keys.RENEWAL_DAYS.toString(), object.getRenewalDays().toDays()); + } + + return json; + } + + public Map userContact(UserContact userContact) { + Map res = new HashMap<>(); + res.put(UserContact.Keys.USER_ID.toString(), userContact.getUserId()); + res.put(UserContact.Keys.USERNAME.toString(), userContact.getUsername()); + res.put(UserContact.Keys.FIRST_NAME.toString(), userContact.getFirstName().orElse(null)); + res.put(UserContact.Keys.LAST_NAME.toString(), userContact.getLastName().orElse(null)); + res.put(UserContact.Keys.EMAIL.toString(), userContact.getEmail().orElse(null)); + res.put(UserContact.Keys.PHONE.toString(), userContact.getPhone().orElse(null)); + return res; + } + + public static Map contractedServices(Set services) { + Map res = new HashMap<>(); + for (Service service : services) { + res.put(service.getName(), service.getVersion()); + } + return Collections.unmodifiableMap(res); + } + + public static Map subscriptionPlans(Set services) { + Map res = new HashMap<>(); + for (Service service : services) { + if (service.getPlan().isEmpty()) { + continue; + } + res.put(service.getName(), service.getPlan().get()); + } + return Collections.unmodifiableMap(res); + + } + + public static Map> subscriptionAddOns(Set services) { + + Map> res = new HashMap<>(); + + for (Service service : services) { + Map serviceMap = new HashMap<>(); + for (AddOn addOn : service.getAddOns()) { + serviceMap.put(addOn.getName(), addOn.getQuantity()); + } + + res.put(service.getName(), Collections.unmodifiableMap(serviceMap)); + } + return Collections.unmodifiableMap(res); + } + +} diff --git a/src/main/java/io/github/pgmarc/space/serializers/SubscriptionUpdateRequestSerializer.java b/src/main/java/io/github/pgmarc/space/serializers/SubscriptionUpdateRequestSerializer.java new file mode 100644 index 0000000..7a8f6cb --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/SubscriptionUpdateRequestSerializer.java @@ -0,0 +1,22 @@ +package io.github.pgmarc.space.serializers; + +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.Subscription; +import io.github.pgmarc.space.contracts.SubscriptionUpdateRequest; + +public class SubscriptionUpdateRequestSerializer implements JsonSerializable { + + @Override + public JSONObject toJson(SubscriptionUpdateRequest subscription) { + + return new JSONObject() + .put(Subscription.Keys.CONTRACTED_SERVICES.toString(), + SubscriptionRequestSerializer.contractedServices(subscription.getServices())) + .put(Subscription.Keys.SUBSCRIPTION_PLANS.toString(), + SubscriptionRequestSerializer.subscriptionPlans(subscription.getServices())) + .put(Subscription.Keys.SUBSCRIPTION_ADDONS.toString(), + SubscriptionRequestSerializer.subscriptionAddOns(subscription.getServices())); + } + +} diff --git a/src/test/java/io/github/pgmarc/space/ConfigTest.java b/src/test/java/io/github/pgmarc/space/ConfigTest.java new file mode 100644 index 0000000..c2625c5 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/ConfigTest.java @@ -0,0 +1,72 @@ +package io.github.pgmarc.space; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +class ConfigTest { + + private static final String TEST_API_KEY = "fc851e857c18a5df8ef91dd8c63a1ca3"; + private static final String TEST_HOST = "example.com"; + + @Test + void givenRequiredParametersShouldCreateConfig() { + + Config config = Config.builder(TEST_HOST, TEST_API_KEY).build(); + + assertAll( + () -> assertThat(config.getUrl()).hasToString("http://" + TEST_HOST + ":5403/api/v1"), + () -> assertThat(config.getApiKey()).isEqualTo(TEST_API_KEY)); + + } + + @Test + void givenNoHostAndPortShouldThrow() { + + Config.Builder config1 = Config.builder(null, TEST_API_KEY); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> config1.build()) + .withMessage("host must not be null"); + Config.Builder config2 = Config.builder(TEST_HOST, null); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> config2.build()) + .withMessage("api key must not be null"); + + } + + @Test + void givenOptionalParemetersShoudCreate() { + + int port = 3000; + String prefixPath = "api/v2"; + long writeTimeoutMillis = 700; + long readTimeoutMillis = 500; + + Config config = Config.builder(TEST_HOST, TEST_API_KEY) + .port(port) + .prefixPath(prefixPath) + .readTimeout(Duration.ofMillis(readTimeoutMillis)) + .writeTimeout(Duration.ofMillis(writeTimeoutMillis)) + .build(); + + assertAll( + () -> assertThat(config.getUrl()) + .hasToString("http://" + TEST_HOST + ":" + port + "/" + prefixPath), + () -> assertThat(config.getReadTimeout().toMillis()).isEqualTo(readTimeoutMillis), + () -> assertThat(config.getWriteTimeout().toMillis()).isEqualTo(writeTimeoutMillis)); + + } + + @Test + void givenNullPathShouldUseDefaultPrefixPath() { + + Config config = Config.builder(TEST_HOST, TEST_API_KEY) + .prefixPath(null) + .build(); + assertThat(config.getUrl()).hasToString("http://example.com:5403/api/v1"); + } + +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java new file mode 100644 index 0000000..9355532 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -0,0 +1,47 @@ +package io.github.pgmarc.space.contracts; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Duration; +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.Test; + +class BillingPeriodTest { + + private final ZonedDateTime start = ZonedDateTime.parse("2025-08-15T00:00:00Z"); + private final ZonedDateTime end = start.plusDays(30); + + @Test + void givenZeroRenewalDaysShouldThrow() { + + BillingPeriod period = BillingPeriod.of(start, end); + Duration duration = Duration.ofHours(12); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> period.setRenewalDays(duration)) + .withMessage("your subscription cannot expire in less than one day"); + } + + @Test + void givenStartDateAfterEndDateShouldThrow() { + + ZonedDateTime end = start.minusDays(1); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> BillingPeriod.of(start, end)) + .withMessage("startDate is after endDate"); + } + + @Test + void givenRenewableDateShouldBeRenowable() { + + int days = 30; + BillingPeriod billingPeriod = BillingPeriod.of(start, end); + billingPeriod.setRenewalDays(Duration.ofDays(days)); + + assertThat(billingPeriod.getDuration().toDays()).isEqualTo(days); + assertThat(billingPeriod.getRenewalDate()).isPresent().hasValue(end.plusDays(30).toLocalDateTime()); + } + +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java new file mode 100644 index 0000000..cce38df --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java @@ -0,0 +1,195 @@ +package io.github.pgmarc.space.contracts; + +import java.io.IOException; +import java.time.Duration; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; + +import io.github.pgmarc.space.exceptions.SpaceApiException; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; + +import static org.assertj.core.api.Assertions.*; + +class ContractsEndpointTest { + + private static final String TEST_API_KEY = "prueba"; + private final OkHttpClient httpClient = new OkHttpClient.Builder().build(); + private static HttpUrl url; + private final ContractsEndpoint endpoint = new ContractsEndpoint(httpClient, url, TEST_API_KEY); + + @RegisterExtension + 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(); + } + + @Test + void givenASubscriptionShouldBeCreated() { + + String userId = "01c36d29-0d6a-4b41-83e9-8c6d9310c508"; + + wm.stubFor(post(urlEqualTo("/contracts")) + .withHeader("x-api-key", equalTo(TEST_API_KEY)) + .withHeader("Content-Type", equalToIgnoreCase("application/json; charset=utf-8")) + .withHeader("Accept", equalTo("application/json")) + .withRequestBody(matchingJsonPath("$.userContact")) + .withRequestBody(matchingJsonPath("$.billingPeriod")) + .withRequestBody(matchingJsonPath("$.contractedServices")) + .withRequestBody(matchingJsonPath("$.subscriptionPlans")) + .withRequestBody(matchingJsonPath("$.subscriptionAddOns")) + .willReturn( + created() + .withHeader("Content-Type", "application/json") + .withBodyFile("addContracts-response.hbs"))); + + UserContact userContact = UserContact.builder("01c36d29-0d6a-4b41-83e9-8c6d9310c508", "johndoe") + .firstName("John") + .lastName("Doe") + .email("john.doe@my-domain.com") + .phone("+34 666 666 666") + .build(); + + SubscriptionRequest subReq = SubscriptionRequest.builder(userContact) + .renewIn(Duration.ofDays(45)) + .startService("zoom", "2025") + .plan("ENTERPRISE") + .addOn("extraSeats", 2) + .addOn("hugeMeetings", 1) + .endService() + .startService("petclinic", "2024") + .plan("GOLD") + .addOn("petsAdoptionCentre", 1) + .endService() + .build(); + + assertThatNoException().isThrownBy(() -> endpoint.addContract(subReq)); + Subscription subscription; + try { + subscription = endpoint.addContract(subReq); + assertThat(subscription.getServices()).isEqualTo(subReq.getServices()); + assertThat(subscription.getUserId()).isEqualTo(userId); + assertThat(subscription.getRenewalDuration()).isPresent().hasValue(Duration.ofDays(45)); + assertThat(subscription.getHistory()).isEmpty(); + } catch (IOException e) { + fail(); + } + + } + + @Test + void givenRequestWithNoApiKeyShouldThrow() { + + wm.stubFor(post(urlEqualTo("/contracts")) + .willReturn( + unauthorized() + .withHeader("Content-Type", "application/json") + .withBody("{\r\n" + // + " \"error\": \"API Key not found. Please ensure to add an API Key as value of the \\\"x-api-key\\\" header.\"\r\n" + + // + "}"))); + + UserContact userContact = UserContact.builder("error", "alex") + .build(); + + SubscriptionRequest subReq = SubscriptionRequest.builder(userContact) + .startService("err", "v1") + .plan("Error") + .endService() + .build(); + + assertThatExceptionOfType(SpaceApiException.class).isThrownBy(() -> endpoint.addContract(subReq)) + .withMessageContaining("API Key not found"); + + } + + @Test + void givenAnUserIdShouldReturnASubscription() { + + String userId = "01c36d29-0d6a-4b41-83e9-8c6d9310c508"; + + wm.stubFor(get(urlPathTemplate("/contracts/{userId}")) + .withPathParam("userId", equalTo(userId)) + .withHeader("x-api-key", equalTo(TEST_API_KEY)) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + created() + .withHeader("Content-Type", "application/json") + .withBodyFile("getContractById-response.json"))); + + Subscription subscription; + try { + subscription = endpoint.getContractByUserId(userId); + assertThat(subscription.getUserId()).isEqualTo(userId); + } catch (IOException e) { + fail(); + } + + } + + @Test + void givenAnUserIdThatDoesNotExistShouldThrowError() { + + String userId = "non-existent"; + + wm.stubFor(get(urlPathTemplate("/contracts/{userId}")) + .withPathParam("userId", equalTo(userId)) + .withHeader("x-api-key", equalTo(TEST_API_KEY)) + .withHeader("Accept", equalTo("application/json")) + .willReturn( + aResponse() + .withStatus(404) + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBody("{\"error\":\"Contract with userId {{request.path.userId}} not found\"}"))); + + assertThatExceptionOfType(SpaceApiException.class) + .isThrownBy(() -> endpoint.getContractByUserId(userId)) + .withMessage("Contract with userId " + userId + " not found") + .extracting(SpaceApiException::getCode).isEqualTo(404); + + } + + @Test + void givenAnUserIdAndServicesShouldUpdateSubscription() { + + String userId = "01c36d29-0d6a-4b41-83e9-8c6d9310c508"; + + wm.stubFor(put(urlPathTemplate("/contracts/{userId}")) + .withPathParam("userId", equalTo(userId)) + .withHeader("x-api-key", equalTo(TEST_API_KEY)) + .withHeader("Accept", equalTo("application/json")) + .withHeader("Content-Type", equalToIgnoreCase("application/json; charset=utf-8")) + .withRequestBody(matchingJsonPath("$.contractedServices")) + .withRequestBody(matchingJsonPath("$.subscriptionPlans")) + .withRequestBody(matchingJsonPath("$.subscriptionAddOns")) + .willReturn( + ok() + .withHeader("Content-Type", "application/json") + .withBodyFile("getContractById-response.json"))); + + SubscriptionUpdateRequest subscription = SubscriptionUpdateRequest.builder() + .service("petclinic", "v1") + .plan("GOLD") + .add(); + Subscription sub; + try { + sub = endpoint.updateContractByUserId(userId, subscription); + assertThat(sub.getUserId()).isEqualTo(userId); + } catch (IOException e) { + fail(); + } + + } + +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java b/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java new file mode 100644 index 0000000..57e8ef5 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java @@ -0,0 +1,62 @@ +package io.github.pgmarc.space.contracts; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ServiceTest { + + + private final Service.Builder baseBuilder = Service.builder("test", "alfa"); + + @Test + void givenServiceWithPlanShouldCreateService() { + + String name = "petclinic"; + String version = "v1"; + String plan = "GOLD"; + String addOnName = "petsAdoptionCentre"; + + Service service = Service.builder(name, version) + .plan(plan).addOn(addOnName, 1).build(); + + assertThat(service.getName()).isEqualTo(name); + assertThat(service.getVersion()).isEqualTo(version); + assertThat(service.getPlan()).isPresent().hasValue(plan); + assertThat(service.getAddOn(addOnName)).isPresent().hasValue(new AddOn(addOnName, 1)); + + } + + @Test + void givenServiceWithBlankPlanShouldThrow() { + + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> baseBuilder.plan("")) + .withMessage("plan must not be blank"); + } + + @Test + void givenNoPlanOrAddOnShouldThrow() { + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> baseBuilder.build()) + .withMessage("At least you have to be subscribed to a plan or add-on"); + } + + @Test + void givenNullAsAddOnKeyShouldThrow() { + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> baseBuilder.addOn(null, 1)) + .withMessage("add-on name must not be null"); + } + + @Test + void givenAddOnWithZeroQuantityShouldThrow() { + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> baseBuilder.addOn("zeroQuantity", 0)) + .withMessage("zeroQuantity quantity must be greater than 0"); + } +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java new file mode 100644 index 0000000..8728c2c --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java @@ -0,0 +1,135 @@ +package io.github.pgmarc.space.contracts; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Duration; +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SubscriptionRequestTest { + + private static final UserContact TEST_USER_CONTACT = UserContact.builder("123456789", "alexdoe") + .build(); + + private BillingPeriod billingPeriod; + + @BeforeEach + void setUp() { + ZonedDateTime start = ZonedDateTime.parse("2025-08-15T00:00:00Z"); + ZonedDateTime end = start.plusDays(30); + billingPeriod = BillingPeriod.of(start, end); + billingPeriod.setRenewalDays(Duration.ofDays(30)); + } + + @Test + void givenMultipleServicesInSubscriptionShouldCreate() { + + long renewalDays = 30; + String service1Name = "Petclinic"; + String service2Name = "Petclinic Labs"; + + Service service1 = Service.builder(service1Name, "v1").plan("GOLD").build(); + Service service2 = Service.builder(service2Name, "v2").plan("PLATINUM").build(); + + SubscriptionRequest sub = SubscriptionRequest + .builder(TEST_USER_CONTACT) + .subscribe(service1) + .subscribe(service2) + .renewIn(Duration.ofDays(renewalDays)) + .build(); + + assertThat(sub.getServices()).contains(service1, service2); + } + + @Test + void givenConsecutiveServiceCreationShouldThrow() { + + SubscriptionRequest.Builder builder = SubscriptionRequest + .builder(TEST_USER_CONTACT) + .startService("test", "v1"); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> builder.startService("incorrect", "v1")) + .withMessage("you must build a service before creating another"); + } + + @Test + void givenPlanCallBeforeCallingCreationServiceShouldThrow() { + + SubscriptionRequest.Builder builder = SubscriptionRequest + .builder(TEST_USER_CONTACT); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> builder.plan("foo")) + .withMessage("you must call 'newService' before setting a plan: foo"); + } + + @Test + void givenAddOnCallBeforeCallingCreationServiceShouldThrow() { + + SubscriptionRequest.Builder builder = SubscriptionRequest + .builder(TEST_USER_CONTACT); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> builder.addOn("foo", 1)) + .withMessage("you must call 'newService' before setting an add-on: foo"); + + } + + @Test + void givenServiceBuildCallBeforeCreationServiceShouldThrow() { + + SubscriptionRequest.Builder builder = SubscriptionRequest.builder(TEST_USER_CONTACT); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> builder + .endService()) + .withMessage("you must call 'newService' before adding a service"); + } + + @Test + void whenNoRequiredParametersInputShouldThrow() { + + SubscriptionRequest.Builder builder = SubscriptionRequest.builder(null); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> builder.build()) + .withMessage("userContact must not be null"); + } + + @Test + void givenOptionalRenewalDaysShouldNotThrow() { + + SubscriptionRequest subReq = SubscriptionRequest.builder(TEST_USER_CONTACT) + .renewIn(null) + .startService("foo", "bar").plan("baz") + .endService().build(); + + assertThat(subReq.getRenewalDays()).isNull(); + + } + + @Test + void givenNoEndServiceShouldThrow() { + + SubscriptionRequest.Builder builder = SubscriptionRequest.builder(TEST_USER_CONTACT) + .renewIn(null) + .startService("foo", "bar").plan("baz"); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> builder.build()) + .withMessage("finish the creation of your service by calling endService"); + } + + @Test + void foo() { + + SubscriptionRequest.Builder builder = SubscriptionRequest.builder(TEST_USER_CONTACT); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> builder.build()) + .withMessage("you have to be subscribed al least to one service"); + } + +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java new file mode 100644 index 0000000..4a2ebc5 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java @@ -0,0 +1,54 @@ +package io.github.pgmarc.space.contracts; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.ZonedDateTime; + +import org.junit.jupiter.api.Test; + +class UsageLevelTest { + + @Test + void givenNonRenewableUsageLimitShouldCreate() { + + String usageLimitName = "maxPets"; + double consumption = 5; + UsageLevel usageLevel = UsageLevel.of(usageLimitName, consumption); + + assertAll( + () -> assertEquals(usageLimitName, usageLevel.getName()), + () -> assertEquals(consumption, usageLevel.getConsumption()), + () -> assertFalse(usageLevel.isRenewableUsageLimit())); + } + + @Test + void givenInvalidParamertersShouldThrow() { + + String usageLimitName = "maxPets"; + double consumption = 5; + + assertAll( + () -> assertThrows(NullPointerException.class, () -> UsageLevel.of(null, consumption)), + () -> assertThrows(IllegalArgumentException.class, () -> UsageLevel.of(usageLimitName, -1))); + } + + @Test + void givenRenewableUsageLimitShouldCreate() { + + String usageLimitName = "maxTokens"; + double consumption = 300; + ZonedDateTime resetTimestamp = ZonedDateTime.parse("2025-08-19T00:00:00Z"); + UsageLevel usageLevel = UsageLevel.of(usageLimitName, consumption, resetTimestamp); + + assertAll( + () -> assertEquals(usageLimitName, usageLevel.getName()), + () -> assertEquals(consumption, usageLevel.getConsumption()), + () -> assertEquals(resetTimestamp.toLocalDateTime(), usageLevel.getResetTimestamp().get()), + () -> assertTrue(usageLevel.isRenewableUsageLimit())); + } + +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java new file mode 100644 index 0000000..a747fbb --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -0,0 +1,72 @@ +package io.github.pgmarc.space.contracts; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class UserContactTest { + + private static final String TEST_USER_ID = "123456789"; + private static final String TEST_USERNAME = "alex"; + + @Test + void givenIdAndUsernameShouldCreateUserContact() { + + UserContact contact = UserContact.builder(TEST_USER_ID, TEST_USERNAME).build(); + assertThat(contact.getUserId()).isEqualTo(TEST_USER_ID); + assertThat(contact.getUsername()).isEqualTo(TEST_USERNAME); + } + + @Test + void givenNullUserIdShouldThrow() { + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> UserContact.builder(null, TEST_USERNAME)) + .withMessage("userId must not be null"); + } + + @Test + void givenNullUsernameShouldThrow() { + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> UserContact.builder(TEST_USER_ID, null)) + .withMessage("username must not be null"); + } + + @ParameterizedTest + @ValueSource(strings = { "", "ab", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) + void givenInvalidUsernamesShouldThrow(String username) { + + UserContact.Builder builder = UserContact.builder(TEST_USER_ID, username); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> builder.build()); + + } + + // Using pairwise testing + @ParameterizedTest + @CsvSource(value = { + "NIL,NIL,NIL,NIL", + "Alex,Doe,NIL,666666666", + "NIL,Doe,alexdoe@example.com,666666666", + "Alex,NIL,alexdoe@example.com,666666666" + }, nullValues = "NIL") + void givenOptionalParametersExpecttoBeDefined(String firstName, String lastName, String email, String phone) { + + UserContact contact = UserContact.builder(TEST_USER_ID, TEST_USERNAME) + .firstName(firstName) + .lastName(lastName) + .email(email) + .phone(phone).build(); + assertThat(contact.getFirstName()).isEqualTo(Optional.ofNullable(firstName)); + assertThat(contact.getLastName()).isEqualTo(Optional.ofNullable(lastName)); + assertThat(contact.getEmail()).isEqualTo(Optional.ofNullable(email)); + assertThat(contact.getPhone()).isEqualTo(Optional.ofNullable(phone)); + } +} diff --git a/src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java b/src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java new file mode 100644 index 0000000..0211f85 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java @@ -0,0 +1,43 @@ +package io.github.pgmarc.space.deserializers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.Duration; +import java.time.ZonedDateTime; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import io.github.pgmarc.space.contracts.BillingPeriod; + +class BillingPeriodSerializerTest { + + private final BillingPeriodDeserializer serializer = new BillingPeriodDeserializer(); + + + + @Test + void givenJsonShouldCreateBillingPeriod() { + + String startUtc = "2024-08-20T12:00Z"; + String endUtc = "2025-08-20T12:00Z"; + ZonedDateTime start = ZonedDateTime.parse(startUtc); + ZonedDateTime end = ZonedDateTime.parse(endUtc); + BillingPeriod expected = BillingPeriod.of(start, end); + expected.setRenewalDays(Duration.ofDays(30)); + + JSONObject input = new JSONObject().put("startDate", startUtc) + .put("endDate", endUtc).put("autoRenew", true).put("renewalDays", 30L); + + assertEquals(expected, serializer.fromJson(input)); + } + + @Test + void giveNullJsonShouldThrow() { + + Exception ex = assertThrows(NullPointerException.class, () -> serializer.fromJson(null)); + assertEquals("billing period json must not be null", ex.getMessage()); + } + +} diff --git a/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java b/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java new file mode 100644 index 0000000..4601d9e --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java @@ -0,0 +1,80 @@ +package io.github.pgmarc.space.deserializers; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import io.github.pgmarc.space.contracts.Subscription; + +class SubscriptionSerializerTest { + + private final SubscriptionDeserializer serializer = new SubscriptionDeserializer(); + + @Test + void givenSubscriptionAsJsonShouldCreateSubscription() { + + JSONObject input = new JSONObject(Map.of( + "id", "68050bd09890322c57842f6f", + "userContact", Map.of( + "userId", "01c36d29-0d6a-4b41-83e9-8c6d9310c508", + "username", "johndoe", + "fistName", "John", + "lastName", "Doe", + "email", "john.doe@my-domain.com", + "phone", "+34 666 666 666"), + "billingPeriod", Map.of( + "startDate", "2025-12-31T00:00:00Z", + "endDate", "2025-12-31T00:00:00Z", + "autoRenew", true, + "renewalDays", 365), + "usageLevel", Map.of( + "zoom", Map.of( + "maxSeats", Map.of("consumed", 10)), + "petclinic", Map.of( + "maxPets", Map.of("consumed", 2), + "maxVisits", Map.of( + "consumed", 5, + "resetTimeStamp", "2025-07-31T00:00:00Z"))), + "contractedServices", Map.of( + "zoom", "2025", + "petclinic", "2024"), + "subscriptionPlans", Map.of( + "zoom", "ENTERPRISE", + "petclinic", "GOLD"), + "subscriptionAddOns", Map.of( + "zoom", Map.of( + "extraSeats", 2, + "hugeMeetings", 1), + "petclinic", Map.of( + "petsAdoptionCentre", 1)), + "history", List.of( + Map.of( + "startDate", "2025-12-31T00:00:00Z", + "endDate", "2025-12-31T00:00:00Z", + "contractedServices", Map.of( + "zoom", "2025", + "petclinic", "2024"), + "subscriptionPlans", Map.of( + "zoom", "ENTERPRISE", + "petclinic", "GOLD"), + "subscriptionAddOns", Map.of( + "zoom", Map.of( + "extraSeats", 2, + "hugeMeetings", 1), + "petclinic", Map.of( + "petsAdoptionCentre", 1)))))); + + Subscription actual = serializer.fromJson(input); + assertAll( + () -> assertEquals(1, actual.getHistory().size()), + () -> assertEquals(2, actual.getUsageLevels().size()), + () -> assertEquals(2, actual.getServicesMap().size())); + + } + +} diff --git a/src/test/java/io/github/pgmarc/space/deserializers/UserContactSerializerTest.java b/src/test/java/io/github/pgmarc/space/deserializers/UserContactSerializerTest.java new file mode 100644 index 0000000..0a6a75c --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/deserializers/UserContactSerializerTest.java @@ -0,0 +1,62 @@ +package io.github.pgmarc.space.deserializers; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import io.github.pgmarc.space.contracts.UserContact; + +class UserContactSerializerTest { + + private final UserContactDeserializer deserializer = new UserContactDeserializer(); + + @Test + void givenUserContactJsonShouldParse() { + + String userId = "123456789"; + String username = "alex"; + String firtName = "Alex"; + String lastName = "Doe"; + String email = "alexdoe@example.com"; + String phone = "(+34) 666 666 666"; + UserContact expected = UserContact.builder(userId, username) + .firstName(firtName) + .lastName(lastName) + .email(email) + .phone(phone) + .build(); + + JSONObject input = new JSONObject().put("userId", userId) + .put("username", username) + .put("firstName", firtName) + .put("lastName", lastName) + .put("email", email) + .put("phone", phone); + assertEquals(expected, deserializer.fromJson(input)); + } + + @Test + void givenMinimunUserContactOptionalsShouldNotBeParsed() { + + JSONObject input = new JSONObject() + .put("userId", "123456789") + .put("username", "test"); + + UserContact actual = deserializer.fromJson(input); + assertAll(() -> assertTrue(actual.getFirstName().isEmpty()), + () -> assertTrue(actual.getLastName().isEmpty()), + () -> assertTrue(actual.getEmail().isEmpty()), + () -> assertTrue(actual.getPhone().isEmpty())); + } + + @Test + void givenNullJsonShouldThrow() { + + Exception ex = assertThrows(NullPointerException.class, () -> deserializer.fromJson(null)); + assertEquals("user contact json must not be null", ex.getMessage()); + } +} diff --git a/src/test/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializerTest.java b/src/test/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializerTest.java new file mode 100644 index 0000000..a633a19 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializerTest.java @@ -0,0 +1,63 @@ +package io.github.pgmarc.space.serializers; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.util.Set; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import io.github.pgmarc.space.contracts.Subscription; +import io.github.pgmarc.space.contracts.SubscriptionRequest; +import io.github.pgmarc.space.contracts.UserContact; + +class SubscriptionRequestSerializerTest { + + @Test + void givenSubscriptionRequestShouldSerialize() { + + UserContact userContact = UserContact.builder("01c36d29-0d6a-4b41-83e9-8c6d9310c508", "johndoe") + .firstName("John") + .lastName("Doe") + .email("john.doe@my-domain.com") + .phone("+34 666 666 666") + .build(); + + String zoom = "zoom"; + String petclinic = "petclinic"; + SubscriptionRequest subReq = SubscriptionRequest.builder(userContact) + .renewIn(Duration.ofDays(365)) + .startService(zoom, "2025") + .plan("ENTERPRISE") + .addOn("extraSeats", 2) + .addOn("hugeMeetings", 1) + .endService() + .startService("petclinic", "2024") + .plan("GOLD") + .addOn("petsAdoptionCentre", 1) + .endService() + .build(); + + SubscriptionRequestSerializer serializer = new SubscriptionRequestSerializer(); + JSONObject actual = serializer.toJson(subReq); + + Set serviceKeys = Set.of(zoom, petclinic); + Set addOnZoomKeys = Set.of("hugeMeetings", "extraSeats"); + Set addOnPetclinicKeys = Set.of( "petsAdoptionCentre"); + + Set actualZoomAddOnKeys = actual.getJSONObject(Subscription.Keys.SUBSCRIPTION_ADDONS.toString()).getJSONObject(zoom).keySet(); + Set actualPetclinicAddOnKeys = actual.getJSONObject(Subscription.Keys.SUBSCRIPTION_ADDONS.toString()).getJSONObject(petclinic).keySet(); + + assertAll( + () -> assertEquals(serviceKeys, + actual.getJSONObject(Subscription.Keys.CONTRACTED_SERVICES.toString()).keySet()), + () -> assertEquals(serviceKeys, actual.getJSONObject(Subscription.Keys.SUBSCRIPTION_PLANS.toString()).keySet()), + () -> assertEquals(serviceKeys, actual.getJSONObject(Subscription.Keys.SUBSCRIPTION_ADDONS.toString()).keySet()), + () -> assertEquals(addOnZoomKeys, actualZoomAddOnKeys), + () -> assertEquals(addOnPetclinicKeys, actualPetclinicAddOnKeys)); + + } + +} diff --git a/src/test/resources/__files/addContracts-response.hbs b/src/test/resources/__files/addContracts-response.hbs new file mode 100644 index 0000000..82cbcea --- /dev/null +++ b/src/test/resources/__files/addContracts-response.hbs @@ -0,0 +1,30 @@ +{ + "id": "68050bd09890322c57842f6f", + "userContact": {{jsonPath request.body '$.userContact'}}, + "billingPeriod": { + "startDate": "2025-12-31T00:00:00Z", + "endDate": "2025-12-31T00:00:00Z", + "autoRenew": true, + "renewalDays": {{jsonPath request.body '$.billingPeriod.renewalDays'}} + }, + "usageLevel": { + "zoom": { + "maxSeats": { + "consumed": 10 + } + }, + "petclinic": { + "maxPets": { + "consumed": 2 + }, + "maxVisits": { + "consumed": 5, + "resetTimeStamp": "2025-07-31T00:00:00Z" + } + } + }, + "contractedServices": {{jsonPath request.body '$.contractedServices'}}, + "subscriptionPlans": {{jsonPath request.body '$.subscriptionPlans'}}, + "subscriptionAddOns": {{jsonPath request.body '$.subscriptionAddOns'}}, + "history": [] +} diff --git a/src/test/resources/__files/getContractById-response.json b/src/test/resources/__files/getContractById-response.json new file mode 100644 index 0000000..6c950bd --- /dev/null +++ b/src/test/resources/__files/getContractById-response.json @@ -0,0 +1,47 @@ +{ + "id": "68050bd09890322c57842f6f", + "userContact": { + "userId": "{{request.path.userId}}", + "username": "alex" + }, + "billingPeriod": { + "startDate": "2025-12-31T00:00:00Z", + "endDate": "2025-12-31T00:00:00Z", + "autoRenew": false, + "renewalDays": 30 + }, + "usageLevel": { + "zoom": { + "maxSeats": { + "consumed": 10 + } + }, + "petclinic": { + "maxPets": { + "consumed": 2 + }, + "maxVisits": { + "consumed": 5, + "resetTimeStamp": "2025-07-31T00:00:00Z" + } + } + }, + "contractedServices": { + "zoom": "2025", + "petclinic": "2024" + }, + "subscriptionPlans": { + "zoom": "ENTERPRISE", + "petclinic": "GOLD" + }, + "subscriptionAddOns": { + "zoom": { + "extraSeats": 2, + "hugeMeetings": 1 + }, + "petclinic": { + "petsAdoptionCentre": 1 + } + }, + "history": [] +} diff --git a/src/test/resources/__files/subscription-request.json b/src/test/resources/__files/subscription-request.json new file mode 100644 index 0000000..8bc27c8 --- /dev/null +++ b/src/test/resources/__files/subscription-request.json @@ -0,0 +1,31 @@ +{ + "userContact": { + "userId": "{{jsonPath request.body '$.userContact.userId'}}", + "username": "johndoe", + "fistName": "John", + "lastName": "Doe", + "email": "john.doe@my-domain.com", + "phone": "+34 666 666 666" + }, + "billingPeriod": { + "autoRenew": true, + "renewalDays": 365 + }, + "contractedServices": { + "zoom": "2025", + "petclinic": "2024" + }, + "subscriptionPlans": { + "zoom": "ENTERPRISE", + "petclinic": "GOLD" + }, + "subscriptionAddOns": { + "zoom": { + "extraSeats": 2, + "hugeMeetings": 1 + }, + "petclinic": { + "petsAdoptionCentre": 1 + } + } +}