From 3c365c8b6f66288937fa8f911a0fd4fdb80fddaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Fri, 15 Aug 2025 13:20:13 +0200 Subject: [PATCH 01/42] feat: add Mockserver setup --- .../github/pgmarc/space/MockServerTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/test/java/io/github/pgmarc/space/MockServerTest.java diff --git a/src/test/java/io/github/pgmarc/space/MockServerTest.java b/src/test/java/io/github/pgmarc/space/MockServerTest.java new file mode 100644 index 0000000..21183b9 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/MockServerTest.java @@ -0,0 +1,86 @@ +package io.github.pgmarc.space; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.junit.jupiter.MockServerExtension; +import org.mockserver.model.MediaType; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; +import static org.mockserver.model.OpenAPIDefinition.openAPI; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Map; + +@ExtendWith(MockServerExtension.class) +class MockServerTest { + + private static final String spaceOas = "https://raw.githubusercontent.com/Alex-GF/space/refs/heads/main/api/docs/space-api-docs.yaml"; + + private final ClientAndServer client; + + private final HttpClient httpClient = HttpClient.newBuilder().version(Version.HTTP_1_1).build(); + + public MockServerTest(ClientAndServer client) { + this.client = client; + } + + // TODO: Change field userId is defined as ObjectId which is false + @Test + void testOAS() { + client.when(openAPI(spaceOas, "addContracts")) + .respond(response().withBody("{'ping':'pong'}", MediaType.APPLICATION_JSON)); + + JSONObject object = new JSONObject() + .put("userContact", Map.of("username", "pgmarc", "userId", "68050bd09890322c57842f6f")) + .put("billingPeriod", Map.of("autoRenew", true, "renewalDays", 365)) + .put("contractedServices", Map.of("zoom", "2025", "petclinic", "2024")) + .put("subscriptionPlans", Map.of("zoom", "ENTERPRISE", "petclinic", "GOLD")) + .put("subscriptionAddOns", Map.of()); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + client.getPort().toString() + "/contracts")) + .header("Content-Type", MediaType.APPLICATION_JSON.toString()) + .header("x-api-key", "prueba") + .POST(BodyPublishers.ofString(object.toString())).build(); + try { + HttpResponse response = httpClient.send(request, BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + JSONObject json = new JSONObject(response.body()); + assertEquals("pong", json.getString("ping")); + } catch (IOException | InterruptedException e) { + fail(); + } + } + + @Test + void testMockServer() { + client.when(request().withMethod("GET").withPath("/test")) + .respond(response().withBody("{'test':'foo'}", MediaType.APPLICATION_JSON)); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + client.getPort().toString() + "/test")) + .GET().build(); + + try { + HttpResponse response = httpClient.send(request, BodyHandlers.ofString()); + JSONObject json = new JSONObject(response.body()); + assertEquals("foo", json.getString("test")); + } catch (IOException | InterruptedException e) { + fail(); + } + + } + +} From 772fd8a17a5132d7aef95e7d81d65158bc02885a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Fri, 15 Aug 2025 13:23:46 +0200 Subject: [PATCH 02/42] ci: renamed GA workflows --- .github/workflows/code-analysis.yaml | 2 +- .github/workflows/test.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-analysis.yaml b/.github/workflows/code-analysis.yaml index 7211604..e0ce3aa 100644 --- a/.github/workflows/code-analysis.yaml +++ b/.github/workflows/code-analysis.yaml @@ -6,7 +6,7 @@ on: pull_request: types: [opened, synchronize, reopened] jobs: - build: + sonarcloud: name: Build and analyze runs-on: ubuntu-latest steps: 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 From 30d10be7369ec02fb70b1f298a2ca63d03d385b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 16 Aug 2025 11:09:19 +0200 Subject: [PATCH 03/42] feat(contracts): UserContact modeled --- .../pgmarc/space/contracts/UserContact.java | 137 ++++++++++++++++++ .../space/contracts/UserContactTest.java | 70 +++++++++ 2 files changed, 207 insertions(+) create mode 100644 src/main/java/io/github/pgmarc/space/contracts/UserContact.java create mode 100644 src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java 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..9a6f0fb --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java @@ -0,0 +1,137 @@ +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 String firstName; + private String lastName; + private String email; + private 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) { + return new Builder(Objects.requireNonNull(userId, "userId must not be null"), + validateUsername(Objects.requireNonNull(username, "username must not be null"))); + } + + 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); + } + + private static String validateUsername(String username) { + if (username.isBlank()) { + throw new IllegalArgumentException("username is blank"); + } + + int length = username.length(); + if (length < 3 || length > 30) { + throw new IllegalArgumentException("username must be between 3 and 30 characters. Current " + username + + " has " + length + "characters"); + } + + return username; + } + + @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() { + return new UserContact(this); + } + } + +} 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..3319c7a --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -0,0 +1,70 @@ +package io.github.pgmarc.space.contracts; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +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 { + + @Test + void givenIdAndUsernameShouldCreateUserContact() { + + String userId = "123456789"; + String username = "alex"; + + UserContact contact = UserContact.builder(userId, username).build(); + assertEquals(userId, contact.getUserId()); + assertEquals(username, contact.getUsername()); + } + + @Test + void givenNullUserIdShouldThrow() { + + Exception ex = assertThrows(NullPointerException.class, + () -> UserContact.builder(null, "alex")); + assertEquals("userId must not be null", ex.getMessage()); + } + + @Test + void givenNullUsernameShouldThrow() { + + Exception ex = assertThrows(NullPointerException.class, + () -> UserContact.builder("123456789", null)); + assertEquals("username must not be null", ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = { "", "ab", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) + void givenInvalidUsernamesShouldThrow(String username) { + + assertThrows(IllegalArgumentException.class, () -> UserContact.builder("123456789", username)); + } + + // 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("123456789", "alexdoe") + .firstName(firstName) + .lastName(lastName) + .email(email) + .phone(phone).build(); + assertEquals(Optional.ofNullable(firstName), contact.getFirstName()); + assertEquals(Optional.ofNullable(lastName), contact.getLastName()); + assertEquals(Optional.ofNullable(email), contact.getEmail()); + assertEquals(Optional.ofNullable(phone), contact.getPhone()); + } + +} From 70a233c863184a96e2062218379e8adacd7bed02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 16 Aug 2025 13:50:31 +0200 Subject: [PATCH 04/42] feat(contracts): add Service models --- .../github/pgmarc/space/contracts/AddOn.java | 46 ++++++++ .../pgmarc/space/contracts/Service.java | 91 ++++++++++++++++ .../pgmarc/space/contracts/ServiceTest.java | 100 ++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 src/main/java/io/github/pgmarc/space/contracts/AddOn.java create mode 100644 src/main/java/io/github/pgmarc/space/contracts/Service.java create mode 100644 src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java 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..58b80fc --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/AddOn.java @@ -0,0 +1,46 @@ +package io.github.pgmarc.space.contracts; + +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/Service.java b/src/main/java/io/github/pgmarc/space/contracts/Service.java new file mode 100644 index 0000000..24022eb --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/Service.java @@ -0,0 +1,91 @@ +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); + } + + 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) { + Objects.requireNonNull(plan, "plan must not be null"); + 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/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..39bb25c --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java @@ -0,0 +1,100 @@ +package io.github.pgmarc.space.contracts; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class ServiceTest { + + @Test + void givenServiceWithPlanShouldCreateService() { + + String plan = "foo"; + + Service service = Service.builder("test", "alfa") + .plan(plan).build(); + + assertEquals("foo", service.getPlan().get()); + } + + @Test + void givenServiceWithNullPlanShouldThrow() { + Exception ex = assertThrows(NullPointerException.class, + () -> Service.builder("foo", "alfa").plan(null)); + + assertEquals("plan must not be null", ex.getMessage()); + } + + @Test + void givenServiceWithBlankPlanShouldThrow() { + Exception ex = assertThrows(IllegalArgumentException.class, + () -> Service.builder("foo", "alfa").plan("")); + + assertEquals("plan must not be blank", ex.getMessage()); + } + + @Test + void givenNoPlanOrAddOnShouldThrow() { + + Exception ex = assertThrows(IllegalStateException.class, + () -> Service.builder("test", "alfa").build()); + assertEquals("At least you have to be subscribed to a plan or add-on", ex.getMessage()); + } + + @Test + void givenAPlanShouldBePresentInService() { + + String plan = "FREE"; + + Service service = Service.builder("test", "alfa") + .plan(plan).build(); + + assertEquals(plan, service.getPlan().get()); + } + + @Test + void givenNullAsAddOnKeyShouldThrow() { + Exception ex = assertThrows(NullPointerException.class, + () -> Service.builder("test", "alfa").addOn(null, 1)); + + assertEquals("add-on name must not be null", ex.getMessage()); + } + + + @Test + void givenAddOnWithZeroQuantityShouldThrow() { + Exception ex = assertThrows(IllegalArgumentException.class, + () -> Service.builder("test", "alfa").addOn("zeroQuantity", 0)); + + assertEquals("zeroQuantity quantity must be greater than 0", ex.getMessage()); + } + + @Test + void givenAnAddOnShouldBePresentInService() { + + String addOn = "additionalItems"; + + Service service = Service.builder("test", "alfa") + .addOn(addOn, 1).build(); + + assertEquals(addOn, service.getAddOn(addOn).get().getName()); + } + + @Test + void givenPlanAndAddOnsShouldBePresent() { + String plan = "FREE"; + AddOn addOn1 = new AddOn("addOn1", 1); + AddOn addOn2 = new AddOn("addOn2", 2); + + Service service = Service.builder("test", "alfa") + .plan(plan) + .addOn(addOn1.getName(), addOn1.getQuantity()) + .addOn(addOn2.getName(), addOn1.getQuantity()).build(); + + assertEquals(plan, service.getPlan().get()); + assertEquals(addOn1, service.getAddOn(addOn1.getName()).get()); + assertEquals(addOn2, service.getAddOn(addOn2.getName()).get()); + } + +} From 37c2cad8c494640154f2bba79ec3cb9ffcb09255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sun, 17 Aug 2025 11:06:05 +0200 Subject: [PATCH 05/42] feat(contracts): add Subscription model --- .../pgmarc/space/contracts/Subscription.java | 110 +++++++++++++++++ .../space/contracts/SubscriptionTest.java | 111 ++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 src/main/java/io/github/pgmarc/space/contracts/Subscription.java create mode 100644 src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java 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..27e327c --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -0,0 +1,110 @@ +package io.github.pgmarc.space.contracts; + +import java.time.Duration; +import java.time.LocalDateTime; +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 Subscription { + + private final UserContact userContact; + private final Map services; + private final LocalDateTime startDate; + private final LocalDateTime endDate; + private Duration renewalDays; + + private Subscription(Builder builder) { + this.startDate = builder.startDate; + this.endDate = builder.endDate; + this.userContact = builder.userContact; + this.services = Collections.unmodifiableMap(builder.services); + this.renewalDays = builder.renewalDays; + } + + public static Builder builder(UserContact userContact, LocalDateTime startDate, LocalDateTime endDate) { + return new Builder(userContact, startDate, endDate); + } + + public LocalDateTime getStartDate() { + return startDate; + } + + public LocalDateTime getEndDate() { + return endDate; + } + + public String getUserId() { + return userContact.getUserId(); + } + + public String getUsername() { + return userContact.getUsername(); + } + + public boolean isExpired(LocalDateTime dateTime) { + return dateTime.isAfter(endDate); + } + + public boolean isAutoRenewable() { + return renewalDays != null; + } + + public Optional getRenewalDate() { + return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays) : null); + } + + public Optional getService(String serviceName) { + return Optional.ofNullable(this.services.get(serviceName)); + } + + public Set getServices() { + return Set.copyOf(services.values()); + } + + public static final class Builder { + + private final LocalDateTime startDate; + private final LocalDateTime endDate; + private final UserContact userContact; + private final Map services = new HashMap<>(); + private Duration renewalDays; + + private Builder(UserContact userContact, LocalDateTime startDate, LocalDateTime endDate) { + this.startDate = startDate; + this.endDate = endDate; + this.userContact = userContact; + } + + public Builder renewIn(Duration renewalDays) { + if (renewalDays != null && renewalDays.toDays() <= 0) { + throw new IllegalArgumentException("your subscription cannot expire in less than one day"); + } + this.renewalDays = renewalDays; + return this; + } + + public Builder subscribe(Service service) { + this.services.put(service.getName(), Objects.requireNonNull(service, "service must not be null")); + return this; + } + + public Subscription build() { + Objects.requireNonNull(startDate, "startDate must not be null"); + Objects.requireNonNull(endDate, "endDate must not be null"); + Objects.requireNonNull(userContact, "userContact must not be null"); + if (startDate.isAfter(endDate)) { + throw new IllegalStateException("startDate is after endDate"); + } + if (services.isEmpty()) { + throw new IllegalStateException("You have to be subscribed at least to a plan or an add-on"); + } + return new Subscription(this); + } + + } + +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java new file mode 100644 index 0000000..959bd63 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -0,0 +1,111 @@ +package io.github.pgmarc.space.contracts; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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 java.time.Duration; +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +class SubscriptionTest { + + private static final UserContact userContact = UserContact.builder("123456789", "alexdoe") + .build(); + private static final Service service = Service.builder("test", "alfa").plan("Foo").build(); + + private static final LocalDateTime start = LocalDateTime.of(2025, 8, 15, 0, 0); + + private static final LocalDateTime end = start.plusDays(30); + + @Test + void givenNoServiceInSubscriptionShouldThrow() { + + Exception ex = assertThrows(IllegalStateException.class, + () -> Subscription.builder(userContact, start, end).build()); + + assertEquals("You have to be subscribed at least to a plan or an add-on", ex.getMessage()); + } + + @Test + void givenMultipleServicesInSubscriptionShouldCreate() { + + long renewalDays = 30; + LocalDateTime renewalDate = end.plusDays(renewalDays); + 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(); + + Subscription sub = Subscription + .builder(userContact, start, end) + .subscribe(service1) + .subscribe(service2) + .renewIn(Duration.ofDays(renewalDays)) + .build(); + + assertAll( + () -> assertEquals(start, sub.getStartDate()), + () -> assertEquals(end, sub.getEndDate()), + () -> assertTrue(sub.isAutoRenewable()), + () -> assertEquals(renewalDate, sub.getRenewalDate().get())); + + assertEquals(2, sub.getServices().size()); + assertEquals(service1, sub.getService(service1Name).get()); + assertEquals(service2, sub.getService(service2Name).get()); + } + + @Test + void whenNoRequiredParametersInputShouldThrow() { + + Exception ex = assertThrows(NullPointerException.class, () -> Subscription.builder(null, start, end) + .subscribe(service) + .build()); + assertEquals("userContact must not be null", ex.getMessage()); + + ex = assertThrows(NullPointerException.class, () -> Subscription.builder(userContact, null, end) + .subscribe(service) + .build()); + assertEquals("startDate must not be null", ex.getMessage()); + + ex = assertThrows(NullPointerException.class, () -> Subscription.builder(userContact, start, null) + .subscribe(service) + .build()); + assertEquals("endDate must not be null", ex.getMessage()); + } + + @Test + void givenStartDateAfterEndDateShouldThrow() { + + LocalDateTime end = start.minusDays(1); + + Exception ex = assertThrows(IllegalStateException.class, () -> Subscription.builder(userContact, start, end) + .subscribe(service) + .build()); + assertEquals("startDate is after endDate", ex.getMessage()); + } + + @Test + void givenOptionalRenewalDaysShouldNotThrow() { + + assertDoesNotThrow(() -> Subscription.builder(userContact, start, end) + .subscribe(service) + .renewIn(null) + .build()); + } + + @Test + void givenZeroRenewalDaysShouldThrow() { + + Exception ex = assertThrows(IllegalArgumentException.class, () -> Subscription.builder(userContact, start, end) + .subscribe(service) + .renewIn(Duration.ofDays(0)) + .build()); + assertEquals("your subscription cannot expire in less than one day", ex.getMessage()); + } + +} From b76b5ce83815b7eb03f21ee2ab25dd9f48d08f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Mon, 18 Aug 2025 21:22:48 +0200 Subject: [PATCH 06/42] feat: add configuration class --- .../java/io/github/pgmarc/space/Config.java | 98 +++++++++++++++++++ .../io/github/pgmarc/space/ConfigTest.java | 63 ++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 src/main/java/io/github/pgmarc/space/Config.java create mode 100644 src/test/java/io/github/pgmarc/space/ConfigTest.java 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/test/java/io/github/pgmarc/space/ConfigTest.java b/src/test/java/io/github/pgmarc/space/ConfigTest.java new file mode 100644 index 0000000..f92a735 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/ConfigTest.java @@ -0,0 +1,63 @@ +package io.github.pgmarc.space; + +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 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(); + + assertEquals("http://" + TEST_HOST + ":5403/api/v1", config.getUrl().toString()); + assertEquals(TEST_API_KEY, config.getApiKey()); + } + + @Test + void givenNoHostAndPortShouldThrow() { + + assertAll( + () -> assertThrows(NullPointerException.class, () -> Config.builder(null, TEST_API_KEY).build()), + () -> assertThrows(NullPointerException.class, () -> Config.builder(TEST_HOST, null).build())); + } + + @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(); + + assertEquals("http://" + TEST_HOST + ":" + port + "/" + prefixPath, config.getUrl().toString()); + assertEquals(readTimeoutMillis, config.getReadTimeout().toMillis()); + assertEquals(writeTimeoutMillis, config.getWriteTimeout().toMillis()); + } + + @Test + void givenNullPathShouldUseDefaultPrefixPath() { + + Config config = Config.builder(TEST_HOST, TEST_API_KEY) + .prefixPath(null) + .build(); + + assertEquals("http://example.com:5403/api/v1", config.getUrl().toString()); + } + +} From 5d20c2079001a2d82926240e5147e21c9f9cefaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Mon, 18 Aug 2025 22:57:34 +0200 Subject: [PATCH 07/42] feat(contracts): check username in build method --- .../pgmarc/space/contracts/UserContact.java | 40 +++++++++---------- .../space/contracts/UserContactTest.java | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java index 9a6f0fb..9ea0617 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java @@ -7,10 +7,10 @@ public final class UserContact { private final String userId; private final String username; - private String firstName; - private String lastName; - private String email; - private String phone; + private final String firstName; + private final String lastName; + private final String email; + private final String phone; private UserContact(Builder builder) { this.userId = builder.userId; @@ -22,8 +22,9 @@ private UserContact(Builder builder) { } public static Builder builder(String userId, String username) { - return new Builder(Objects.requireNonNull(userId, "userId must not be null"), - validateUsername(Objects.requireNonNull(username, "username must not be null"))); + Objects.requireNonNull(userId, "userId must not be null"); + Objects.requireNonNull(username, "username must not be null"); + return new Builder(userId, username); } public String getUserId() { @@ -50,20 +51,6 @@ public Optional getPhone() { return Optional.ofNullable(phone); } - private static String validateUsername(String username) { - if (username.isBlank()) { - throw new IllegalArgumentException("username is blank"); - } - - int length = username.length(); - if (length < 3 || length > 30) { - throw new IllegalArgumentException("username must be between 3 and 30 characters. Current " + username - + " has " + length + "characters"); - } - - return username; - } - @Override public int hashCode() { final int prime = 31; @@ -130,8 +117,21 @@ public Builder phone(String phone) { } 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"); + } + } } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index 3319c7a..141172e 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -43,7 +43,7 @@ void givenNullUsernameShouldThrow() { @ValueSource(strings = { "", "ab", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) void givenInvalidUsernamesShouldThrow(String username) { - assertThrows(IllegalArgumentException.class, () -> UserContact.builder("123456789", username)); + assertThrows(IllegalArgumentException.class, () -> UserContact.builder("123456789", username).build()); } // Using pairwise testing From 96a438c53f117ef780d4e37824a08ad6fc38d20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Mon, 18 Aug 2025 23:49:38 +0200 Subject: [PATCH 08/42] feat(contracts): serialize UserContact as json --- .../java/io/github/pgmarc/space/Jsonable.java | 9 +++ .../pgmarc/space/contracts/UserContact.java | 40 +++++++++++- .../space/contracts/UserContactTest.java | 65 ++++++++++++++++--- 3 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/github/pgmarc/space/Jsonable.java diff --git a/src/main/java/io/github/pgmarc/space/Jsonable.java b/src/main/java/io/github/pgmarc/space/Jsonable.java new file mode 100644 index 0000000..a2defff --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/Jsonable.java @@ -0,0 +1,9 @@ +package io.github.pgmarc.space; + +import org.json.JSONObject; + +public interface Jsonable { + + JSONObject toJson(); + +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java index 9ea0617..6852641 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java @@ -3,7 +3,11 @@ import java.util.Objects; import java.util.Optional; -public final class UserContact { +import org.json.JSONObject; + +import io.github.pgmarc.space.Jsonable; + +public final class UserContact implements Jsonable { private final String userId; private final String username; @@ -134,4 +138,38 @@ private void validateUserNameLength() { } } + private enum JsonKeys { + + USER_ID("userId"), + USERNAME("username"), + FIRST_NAME("firstName"), + LAST_NAME("lastName"), + EMAIL("email"), + PHONE("phone"); + + private final String name; + + private JsonKeys(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Override + public JSONObject toJson() { + JSONObject obj = new JSONObject() + .put(JsonKeys.USER_ID.toString(), userId) + .put(JsonKeys.USERNAME.toString(), username) + .putOpt(JsonKeys.FIRST_NAME.toString(), firstName) + .putOpt(JsonKeys.LAST_NAME.toString(), lastName) + .putOpt(JsonKeys.EMAIL.toString(), email) + .putOpt(JsonKeys.PHONE.toString(), phone); + + return obj; + } + } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index 141172e..f0467dc 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -1,10 +1,14 @@ 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.util.Optional; +import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -12,22 +16,22 @@ class UserContactTest { + private static final String TEST_USER_ID = "123456789"; + private static final String TEST_USERNAME = "alex"; + @Test void givenIdAndUsernameShouldCreateUserContact() { - String userId = "123456789"; - String username = "alex"; - - UserContact contact = UserContact.builder(userId, username).build(); - assertEquals(userId, contact.getUserId()); - assertEquals(username, contact.getUsername()); + UserContact contact = UserContact.builder(TEST_USER_ID, TEST_USERNAME).build(); + assertEquals(TEST_USER_ID, contact.getUserId()); + assertEquals(TEST_USERNAME, contact.getUsername()); } @Test void givenNullUserIdShouldThrow() { Exception ex = assertThrows(NullPointerException.class, - () -> UserContact.builder(null, "alex")); + () -> UserContact.builder(null, TEST_USERNAME)); assertEquals("userId must not be null", ex.getMessage()); } @@ -35,7 +39,7 @@ void givenNullUserIdShouldThrow() { void givenNullUsernameShouldThrow() { Exception ex = assertThrows(NullPointerException.class, - () -> UserContact.builder("123456789", null)); + () -> UserContact.builder(TEST_USER_ID, null)); assertEquals("username must not be null", ex.getMessage()); } @@ -43,7 +47,7 @@ void givenNullUsernameShouldThrow() { @ValueSource(strings = { "", "ab", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) void givenInvalidUsernamesShouldThrow(String username) { - assertThrows(IllegalArgumentException.class, () -> UserContact.builder("123456789", username).build()); + assertThrows(IllegalArgumentException.class, () -> UserContact.builder(TEST_USER_ID, username).build()); } // Using pairwise testing @@ -56,7 +60,7 @@ void givenInvalidUsernamesShouldThrow(String username) { }, nullValues = "NIL") void givenOptionalParametersExpecttoBeDefined(String firstName, String lastName, String email, String phone) { - UserContact contact = UserContact.builder("123456789", "alexdoe") + UserContact contact = UserContact.builder(TEST_USER_ID, TEST_USERNAME) .firstName(firstName) .lastName(lastName) .email(email) @@ -67,4 +71,45 @@ void givenOptionalParametersExpecttoBeDefined(String firstName, String lastName, assertEquals(Optional.ofNullable(phone), contact.getPhone()); } + @Test + void givenRequiredParametersShouldSerializeMinimunJson() { + + UserContact userContact = UserContact.builder(TEST_USER_ID, TEST_USERNAME) + .build(); + + JSONObject userContactJson = userContact.toJson(); + + assertAll( + () -> assertFalse(userContactJson.has("firstName")), + () -> assertFalse(userContactJson.has("lastName")), + () -> assertFalse(userContactJson.has("email")), + () -> assertFalse(userContactJson.has("phone"))); + } + + @Test + void givenUserContactShouldSerializeToJson() { + + String firstName = "Alex"; + String lastName = "Doe"; + String email = "alex@example.com"; + String phone = "+(34) 123 456 789"; + + UserContact userContact = UserContact.builder(TEST_USER_ID, TEST_USERNAME) + .firstName(firstName) + .lastName(lastName) + .email(email) + .phone(phone) + .build(); + + JSONObject obj = new JSONObject() + .put("userId", TEST_USER_ID) + .put("username", TEST_USERNAME) + .put("firstName", firstName) + .put("lastName", lastName) + .put("email", email) + .put("phone", phone); + + assertTrue(obj.similar(userContact.toJson())); + } + } From 0fe455b1811ca6f9ebd7b0062a3dae0b1da25043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Tue, 19 Aug 2025 10:45:08 +0200 Subject: [PATCH 09/42] feat(contracts): add usage level --- .../pgmarc/space/contracts/UsageLevel.java | 63 +++++++++++++++++++ .../space/contracts/UsageLevelTest.java | 60 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java create mode 100644 src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java 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..3426b2d --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java @@ -0,0 +1,63 @@ +package io.github.pgmarc.space.contracts; + +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; + +public final class UsageLevel { + + private final String serviceName; + private final String name; + private final double consumed; + private LocalDateTime resetTimestamp; + + private UsageLevel(String serviceName, String name, double consumed) { + this.serviceName = serviceName; + this.name = name; + this.consumed = consumed; + } + + public String getServiceName() { + return serviceName; + } + + public String getName() { + return name; + } + + public Optional getResetTimestamp() { + return Optional.ofNullable(resetTimestamp); + } + + private void setResetTimestamp(LocalDateTime resetTimestamp) { + this.resetTimestamp = resetTimestamp; + } + + public boolean isRenewableUsageLimit() { + return resetTimestamp != null; + } + + public double getConsumption() { + return consumed; + } + + private static void validateUsageLevel(String serviceName, String name, double consumed) { + Objects.requireNonNull(serviceName, "service name must not be null"); + 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 nonRenewable(String serviceName, String name, double consumed) { + validateUsageLevel(serviceName, name, consumed); + return new UsageLevel(serviceName, name, consumed); + } + + public static UsageLevel renewable(String serviceName, String name, double consumed, LocalDateTime resetTimestamp) { + validateUsageLevel(serviceName, name, consumed); + UsageLevel level = new UsageLevel(serviceName, name, consumed); + level.setResetTimestamp(resetTimestamp); + return level; + } +} 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..8c96a65 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java @@ -0,0 +1,60 @@ +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.LocalDateTime; + +import org.junit.jupiter.api.Test; + +class UsageLevelTest { + + @Test + void givenNonRenewableUsageLimitShouldCreate() { + + String serviceName = "petclinic"; + String usageLimitName = "maxPets"; + double consumption = 5; + UsageLevel usageLevel = UsageLevel.nonRenewable(serviceName, usageLimitName, consumption); + + assertAll( + () -> assertEquals(serviceName, usageLevel.getServiceName()), + () -> assertEquals(usageLimitName, usageLevel.getName()), + () -> assertEquals(consumption, usageLevel.getConsumption()), + () -> assertFalse(usageLevel.isRenewableUsageLimit())); + } + + @Test + void givenInvalidParamertersShouldThrow() { + + String serviceName = "petclinic"; + String usageLimitName = "maxPets"; + double consumption = 5; + + assertAll( + () -> assertThrows(NullPointerException.class, () -> UsageLevel.nonRenewable(null, usageLimitName, consumption)), + () -> assertThrows(NullPointerException.class, () -> UsageLevel.nonRenewable(serviceName, null, consumption)), + () -> assertThrows(IllegalArgumentException.class, () -> UsageLevel.nonRenewable(serviceName, usageLimitName, -1))); + } + + @Test + void givenRenewableUsageLimitShouldCreate() { + + String serviceName = "Petclinic AI"; + String usageLimitName = "maxTokens"; + double consumption = 300; + LocalDateTime resetTimestamp = LocalDateTime.of(2025, 8, 19, 0, 0); + UsageLevel usageLevel = UsageLevel.renewable(serviceName, usageLimitName, consumption, resetTimestamp); + + assertAll( + () -> assertEquals(serviceName, usageLevel.getServiceName()), + () -> assertEquals(usageLimitName, usageLevel.getName()), + () -> assertEquals(consumption, usageLevel.getConsumption()), + () -> assertEquals(resetTimestamp, usageLevel.getResetTimestamp().get()), + () -> assertTrue(usageLevel.isRenewableUsageLimit())); + } + +} From 2f769dbbbfed17c37b5267e19928ac989f205cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Tue, 19 Aug 2025 15:59:48 +0200 Subject: [PATCH 10/42] feat(contracts): add subscription history --- .../pgmarc/space/contracts/Service.java | 48 ++++++++ .../pgmarc/space/contracts/Subscription.java | 33 +++-- .../space/contracts/SubscriptionSnapshot.java | 77 ++++++++++++ .../space/contracts/SubscriptionTest.java | 113 ++++++++++++------ 4 files changed, 228 insertions(+), 43 deletions(-) create mode 100644 src/main/java/io/github/pgmarc/space/contracts/SubscriptionSnapshot.java diff --git a/src/main/java/io/github/pgmarc/space/contracts/Service.java b/src/main/java/io/github/pgmarc/space/contracts/Service.java index 24022eb..f21fc80 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Service.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Service.java @@ -46,6 +46,54 @@ 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; diff --git a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index 27e327c..c9684df 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -2,8 +2,10 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -15,18 +17,21 @@ public final class Subscription { private final Map services; private final LocalDateTime startDate; private final LocalDateTime endDate; - private Duration renewalDays; + private final Duration renewalDays; + private final List history; private Subscription(Builder builder) { + this.userContact = builder.userContact; this.startDate = builder.startDate; this.endDate = builder.endDate; - this.userContact = builder.userContact; - this.services = Collections.unmodifiableMap(builder.services); this.renewalDays = builder.renewalDays; + this.services = Collections.unmodifiableMap(builder.services); + this.history = Collections.unmodifiableList(builder.history); } - public static Builder builder(UserContact userContact, LocalDateTime startDate, LocalDateTime endDate) { - return new Builder(userContact, startDate, endDate); + public static Builder builder(UserContact userContact, LocalDateTime startDate, LocalDateTime endDate, + Service service) { + return new Builder(userContact, startDate, endDate).subscribe(service); } public LocalDateTime getStartDate() { @@ -61,16 +66,25 @@ 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 static final class Builder { private final LocalDateTime startDate; private final LocalDateTime endDate; private final UserContact userContact; private final Map services = new HashMap<>(); + private final List history = new ArrayList<>(); private Duration renewalDays; private Builder(UserContact userContact, LocalDateTime startDate, LocalDateTime endDate) { @@ -92,6 +106,12 @@ public Builder subscribe(Service service) { return this; } + Builder addSnapshot(Subscription subscription) { + Objects.requireNonNull(subscription, "subscription must not be null"); + this.history.add(SubscriptionSnapshot.of(subscription)); + return this; + } + public Subscription build() { Objects.requireNonNull(startDate, "startDate must not be null"); Objects.requireNonNull(endDate, "endDate must not be null"); @@ -99,9 +119,6 @@ public Subscription build() { if (startDate.isAfter(endDate)) { throw new IllegalStateException("startDate is after endDate"); } - if (services.isEmpty()) { - throw new IllegalStateException("You have to be subscribed at least to a plan or an add-on"); - } return new Subscription(this); } diff --git a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionSnapshot.java b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionSnapshot.java new file mode 100644 index 0000000..f2b40d9 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionSnapshot.java @@ -0,0 +1,77 @@ +package io.github.pgmarc.space.contracts; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public final class SubscriptionSnapshot { + + private final LocalDateTime starDateTime; + private final LocalDateTime enDateTime; + private final Map services; + + private SubscriptionSnapshot(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 services; + } + + public Optional getService(String name) { + return Optional.ofNullable(services.get(name)); + } + + static SubscriptionSnapshot of(Subscription subscription) { + Objects.requireNonNull(subscription, "subscription must not be null"); + return new SubscriptionSnapshot(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; + SubscriptionSnapshot other = (SubscriptionSnapshot) 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/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java index 959bd63..a707599 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -8,6 +8,8 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Test; @@ -17,24 +19,15 @@ class SubscriptionTest { .build(); private static final Service service = Service.builder("test", "alfa").plan("Foo").build(); - private static final LocalDateTime start = LocalDateTime.of(2025, 8, 15, 0, 0); + private static final LocalDateTime START = LocalDateTime.of(2025, 8, 15, 0, 0); - private static final LocalDateTime end = start.plusDays(30); - - @Test - void givenNoServiceInSubscriptionShouldThrow() { - - Exception ex = assertThrows(IllegalStateException.class, - () -> Subscription.builder(userContact, start, end).build()); - - assertEquals("You have to be subscribed at least to a plan or an add-on", ex.getMessage()); - } + private static final LocalDateTime END = START.plusDays(30); @Test void givenMultipleServicesInSubscriptionShouldCreate() { long renewalDays = 30; - LocalDateTime renewalDate = end.plusDays(renewalDays); + LocalDateTime renewalDate = END.plusDays(renewalDays); String service1Name = "Petclinic"; String service2Name = "Petclinic Labs"; @@ -42,15 +35,14 @@ void givenMultipleServicesInSubscriptionShouldCreate() { Service service2 = Service.builder(service2Name, "v2").plan("PLATINUM").build(); Subscription sub = Subscription - .builder(userContact, start, end) - .subscribe(service1) + .builder(userContact, START, END, service1) .subscribe(service2) .renewIn(Duration.ofDays(renewalDays)) .build(); assertAll( - () -> assertEquals(start, sub.getStartDate()), - () -> assertEquals(end, sub.getEndDate()), + () -> assertEquals(START, sub.getStartDate()), + () -> assertEquals(END, sub.getEndDate()), () -> assertTrue(sub.isAutoRenewable()), () -> assertEquals(renewalDate, sub.getRenewalDate().get())); @@ -62,38 +54,37 @@ void givenMultipleServicesInSubscriptionShouldCreate() { @Test void whenNoRequiredParametersInputShouldThrow() { - Exception ex = assertThrows(NullPointerException.class, () -> Subscription.builder(null, start, end) - .subscribe(service) - .build()); + Exception ex = assertThrows(NullPointerException.class, + () -> Subscription.builder(null, START, END, service) + .build()); assertEquals("userContact must not be null", ex.getMessage()); - ex = assertThrows(NullPointerException.class, () -> Subscription.builder(userContact, null, end) - .subscribe(service) - .build()); + ex = assertThrows(NullPointerException.class, + () -> Subscription.builder(userContact, null, END, service) + .build()); assertEquals("startDate must not be null", ex.getMessage()); - ex = assertThrows(NullPointerException.class, () -> Subscription.builder(userContact, start, null) - .subscribe(service) - .build()); + ex = assertThrows(NullPointerException.class, + () -> Subscription.builder(userContact, START, null, service) + .build()); assertEquals("endDate must not be null", ex.getMessage()); } @Test void givenStartDateAfterEndDateShouldThrow() { - LocalDateTime end = start.minusDays(1); + LocalDateTime end = START.minusDays(1); - Exception ex = assertThrows(IllegalStateException.class, () -> Subscription.builder(userContact, start, end) - .subscribe(service) - .build()); + Exception ex = assertThrows(IllegalStateException.class, + () -> Subscription.builder(userContact, START, end, service) + .build()); assertEquals("startDate is after endDate", ex.getMessage()); } @Test void givenOptionalRenewalDaysShouldNotThrow() { - assertDoesNotThrow(() -> Subscription.builder(userContact, start, end) - .subscribe(service) + assertDoesNotThrow(() -> Subscription.builder(userContact, START, END, service) .renewIn(null) .build()); } @@ -101,11 +92,63 @@ void givenOptionalRenewalDaysShouldNotThrow() { @Test void givenZeroRenewalDaysShouldThrow() { - Exception ex = assertThrows(IllegalArgumentException.class, () -> Subscription.builder(userContact, start, end) - .subscribe(service) - .renewIn(Duration.ofDays(0)) - .build()); + Exception ex = assertThrows(IllegalArgumentException.class, + () -> Subscription.builder(userContact, START, END, service) + .renewIn(Duration.ofDays(0)) + .build()); assertEquals("your subscription cannot expire in less than one day", ex.getMessage()); } + private Subscription firstPetclinicSub(Service petclinic) { + LocalDateTime start = LocalDateTime.of(2024, 1, 1, 0, 0); + LocalDateTime end = LocalDateTime.of(2024, 2, 1, 0, 0); + + return Subscription.builder(userContact, start, end, petclinic) + .build(); + } + + private Subscription secondSubscription(Service petclinic, Service petclinicLabs) { + + LocalDateTime start = LocalDateTime.of(2024, 5, 1, 0, 0); + LocalDateTime end = LocalDateTime.of(2024, 6, 1, 0, 0); + + return Subscription + .builder(userContact, start, end, petclinic) + .subscribe(petclinicLabs) + .build(); + } + + @Test + void givenASubscriptionHistoryShouldBeVisbile() { + + String petclinic = "Petclinic"; + String petclinicLabs = "Petclinic Labs"; + Service petclinicV1 = Service.builder(petclinic, "v1").plan("FREE").build(); + Service petclinicV2 = Service.builder(petclinic, "v2").plan("GOLD").build(); + Service petclinicLabsV1 = Service.builder(petclinicLabs, "v1").plan("PLATINUM").build(); + + Subscription sub1 = firstPetclinicSub(petclinicV1); + Subscription sub2 = secondSubscription(petclinicV2, petclinicLabsV1); + + Subscription currentSubscription = Subscription + .builder(userContact, START, END, petclinicV2) + .addSnapshot(sub1) + .addSnapshot(sub2) + .build(); + + List history = new ArrayList<>(); + SubscriptionSnapshot snaphot1 = SubscriptionSnapshot.of(sub1); + SubscriptionSnapshot snaphot2 = SubscriptionSnapshot.of(sub2); + + history.add(snaphot1); + history.add(snaphot2); + + assertEquals(history, currentSubscription.getHistory()); + + assertAll( + () -> assertEquals(2, currentSubscription.getHistory().size()), + () -> assertEquals(history, currentSubscription.getHistory())); + + } + } From 0a9e38b3ae6f3f7b7850f2cb9eff56e43ef4876d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Tue, 19 Aug 2025 17:04:37 +0200 Subject: [PATCH 11/42] feat(contracts): add subscription usage levels --- .../pgmarc/space/contracts/Subscription.java | 22 ++++++ .../pgmarc/space/contracts/UsageLevel.java | 23 ++---- .../space/contracts/SubscriptionTest.java | 78 ++++++++++++++++--- .../space/contracts/UsageLevelTest.java | 14 +--- 4 files changed, 100 insertions(+), 37 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index c9684df..3090071 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -19,6 +19,7 @@ public final class Subscription { private final LocalDateTime endDate; private final Duration renewalDays; private final List history; + private final Map> usageLevels; private Subscription(Builder builder) { this.userContact = builder.userContact; @@ -27,6 +28,7 @@ private Subscription(Builder builder) { this.renewalDays = builder.renewalDays; this.services = Collections.unmodifiableMap(builder.services); this.history = Collections.unmodifiableList(builder.history); + this.usageLevels = Collections.unmodifiableMap(builder.usageLevels); } public static Builder builder(UserContact userContact, LocalDateTime startDate, LocalDateTime endDate, @@ -78,6 +80,10 @@ public List getHistory() { return history; } + public Map> getUsageLevels() { + return usageLevels; + } + public static final class Builder { private final LocalDateTime startDate; @@ -85,6 +91,7 @@ public static final class Builder { private final UserContact userContact; private final Map services = new HashMap<>(); private final List history = new ArrayList<>(); + private final Map> usageLevels = new HashMap<>(); private Duration renewalDays; private Builder(UserContact userContact, LocalDateTime startDate, LocalDateTime endDate) { @@ -102,6 +109,9 @@ public Builder renewIn(Duration renewalDays) { } public Builder subscribe(Service service) { + if (!services.containsKey(service.getName())) { + this.usageLevels.put(service.getName(), new HashMap<>()); + } this.services.put(service.getName(), Objects.requireNonNull(service, "service must not be null")); return this; } @@ -112,6 +122,18 @@ Builder addSnapshot(Subscription subscription) { return this; } + Builder addUsageLevel(String serviceName, UsageLevel usageLevel) { + if (!services.containsKey(serviceName)) { + throw new IllegalStateException("Service '" + serviceName + "' doesn't exist. Register it previously"); + } + + if (!usageLevels.get(serviceName).containsKey(usageLevel.getName())) { + this.usageLevels.get(serviceName).put(usageLevel.getName(), usageLevel); + } + + return this; + } + public Subscription build() { Objects.requireNonNull(startDate, "startDate must not be null"); Objects.requireNonNull(endDate, "endDate must not be null"); diff --git a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java index 3426b2d..6f47032 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java @@ -6,21 +6,15 @@ public final class UsageLevel { - private final String serviceName; private final String name; private final double consumed; private LocalDateTime resetTimestamp; - private UsageLevel(String serviceName, String name, double consumed) { - this.serviceName = serviceName; + private UsageLevel(String name, double consumed) { this.name = name; this.consumed = consumed; } - public String getServiceName() { - return serviceName; - } - public String getName() { return name; } @@ -41,22 +35,21 @@ public double getConsumption() { return consumed; } - private static void validateUsageLevel(String serviceName, String name, double consumed) { - Objects.requireNonNull(serviceName, "service name must not be null"); + 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 nonRenewable(String serviceName, String name, double consumed) { - validateUsageLevel(serviceName, name, consumed); - return new UsageLevel(serviceName, name, consumed); + public static UsageLevel nonRenewable(String name, double consumed) { + validateUsageLevel(name, consumed); + return new UsageLevel(name, consumed); } - public static UsageLevel renewable(String serviceName, String name, double consumed, LocalDateTime resetTimestamp) { - validateUsageLevel(serviceName, name, consumed); - UsageLevel level = new UsageLevel(serviceName, name, consumed); + public static UsageLevel renewable(String name, double consumed, LocalDateTime resetTimestamp) { + validateUsageLevel(name, consumed); + UsageLevel level = new UsageLevel(name, consumed); level.setResetTimestamp(resetTimestamp); return level; } diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java index a707599..7e790f2 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -15,9 +15,9 @@ class SubscriptionTest { - private static final UserContact userContact = UserContact.builder("123456789", "alexdoe") + private static final UserContact TEST_USER_CONTACT = UserContact.builder("123456789", "alexdoe") .build(); - private static final Service service = Service.builder("test", "alfa").plan("Foo").build(); + private static final Service TEST_SERVICE = Service.builder("test", "alfa").plan("Foo").build(); private static final LocalDateTime START = LocalDateTime.of(2025, 8, 15, 0, 0); @@ -35,7 +35,7 @@ void givenMultipleServicesInSubscriptionShouldCreate() { Service service2 = Service.builder(service2Name, "v2").plan("PLATINUM").build(); Subscription sub = Subscription - .builder(userContact, START, END, service1) + .builder(TEST_USER_CONTACT, START, END, service1) .subscribe(service2) .renewIn(Duration.ofDays(renewalDays)) .build(); @@ -55,17 +55,17 @@ void givenMultipleServicesInSubscriptionShouldCreate() { void whenNoRequiredParametersInputShouldThrow() { Exception ex = assertThrows(NullPointerException.class, - () -> Subscription.builder(null, START, END, service) + () -> Subscription.builder(null, START, END, TEST_SERVICE) .build()); assertEquals("userContact must not be null", ex.getMessage()); ex = assertThrows(NullPointerException.class, - () -> Subscription.builder(userContact, null, END, service) + () -> Subscription.builder(TEST_USER_CONTACT, null, END, TEST_SERVICE) .build()); assertEquals("startDate must not be null", ex.getMessage()); ex = assertThrows(NullPointerException.class, - () -> Subscription.builder(userContact, START, null, service) + () -> Subscription.builder(TEST_USER_CONTACT, START, null, TEST_SERVICE) .build()); assertEquals("endDate must not be null", ex.getMessage()); } @@ -76,7 +76,7 @@ void givenStartDateAfterEndDateShouldThrow() { LocalDateTime end = START.minusDays(1); Exception ex = assertThrows(IllegalStateException.class, - () -> Subscription.builder(userContact, START, end, service) + () -> Subscription.builder(TEST_USER_CONTACT, START, end, TEST_SERVICE) .build()); assertEquals("startDate is after endDate", ex.getMessage()); } @@ -84,7 +84,7 @@ void givenStartDateAfterEndDateShouldThrow() { @Test void givenOptionalRenewalDaysShouldNotThrow() { - assertDoesNotThrow(() -> Subscription.builder(userContact, START, END, service) + assertDoesNotThrow(() -> Subscription.builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) .renewIn(null) .build()); } @@ -93,7 +93,7 @@ void givenOptionalRenewalDaysShouldNotThrow() { void givenZeroRenewalDaysShouldThrow() { Exception ex = assertThrows(IllegalArgumentException.class, - () -> Subscription.builder(userContact, START, END, service) + () -> Subscription.builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) .renewIn(Duration.ofDays(0)) .build()); assertEquals("your subscription cannot expire in less than one day", ex.getMessage()); @@ -103,7 +103,7 @@ private Subscription firstPetclinicSub(Service petclinic) { LocalDateTime start = LocalDateTime.of(2024, 1, 1, 0, 0); LocalDateTime end = LocalDateTime.of(2024, 2, 1, 0, 0); - return Subscription.builder(userContact, start, end, petclinic) + return Subscription.builder(TEST_USER_CONTACT, start, end, petclinic) .build(); } @@ -113,7 +113,7 @@ private Subscription secondSubscription(Service petclinic, Service petclinicLabs LocalDateTime end = LocalDateTime.of(2024, 6, 1, 0, 0); return Subscription - .builder(userContact, start, end, petclinic) + .builder(TEST_USER_CONTACT, start, end, petclinic) .subscribe(petclinicLabs) .build(); } @@ -131,7 +131,7 @@ void givenASubscriptionHistoryShouldBeVisbile() { Subscription sub2 = secondSubscription(petclinicV2, petclinicLabsV1); Subscription currentSubscription = Subscription - .builder(userContact, START, END, petclinicV2) + .builder(TEST_USER_CONTACT, START, END, petclinicV2) .addSnapshot(sub1) .addSnapshot(sub2) .build(); @@ -151,4 +151,58 @@ void givenASubscriptionHistoryShouldBeVisbile() { } + @Test + void givenSubscriptionWithUsageLevelsShouldCreate() { + + String usageLimitName = "maxAlfa"; + UsageLevel ul1 = UsageLevel.nonRenewable(usageLimitName, 5); + + Subscription sub = Subscription + .builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + .addUsageLevel(TEST_SERVICE.getName(), ul1) + .build(); + + assertEquals(ul1, sub.getUsageLevels().get(TEST_SERVICE.getName()).get(usageLimitName)); + + } + + @Test + void givenSubscriptionToAServiceRelatedUsageLevelShoudBeEmpty() { + + Subscription sub = Subscription + .builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + .build(); + + assertAll(() -> assertTrue(sub.getUsageLevels().containsKey(TEST_SERVICE.getName())), + () -> assertTrue(sub.getUsageLevels().get(TEST_SERVICE.getName()).isEmpty())); + } + + @Test + void givenDuplicateUsageLevelShouldNotRegisterTwice() { + String usageLimitName = "maxAlfa"; + UsageLevel ul1 = UsageLevel.nonRenewable(usageLimitName, 5); + + Subscription sub = Subscription + .builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + .addUsageLevel(TEST_SERVICE.getName(), ul1) + .addUsageLevel(TEST_SERVICE.getName(), ul1) + .build(); + + assertEquals(1, sub.getUsageLevels().get(TEST_SERVICE.getName()).size()); + } + + @Test + void givenNonExistentServiceShouldThrowWhenAddingUsageLevel() { + + UsageLevel ul1 = UsageLevel.nonRenewable("maxAlfa", 5); + + Exception ex = assertThrows(IllegalStateException.class, () -> Subscription + .builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + .addUsageLevel("nonExistent", ul1) + .build()); + + assertEquals("Service 'nonExistent' doesn't exist. Register it previously", ex.getMessage()); + + } + } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java index 8c96a65..27e875e 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java @@ -15,13 +15,11 @@ class UsageLevelTest { @Test void givenNonRenewableUsageLimitShouldCreate() { - String serviceName = "petclinic"; String usageLimitName = "maxPets"; double consumption = 5; - UsageLevel usageLevel = UsageLevel.nonRenewable(serviceName, usageLimitName, consumption); + UsageLevel usageLevel = UsageLevel.nonRenewable(usageLimitName, consumption); assertAll( - () -> assertEquals(serviceName, usageLevel.getServiceName()), () -> assertEquals(usageLimitName, usageLevel.getName()), () -> assertEquals(consumption, usageLevel.getConsumption()), () -> assertFalse(usageLevel.isRenewableUsageLimit())); @@ -30,27 +28,23 @@ void givenNonRenewableUsageLimitShouldCreate() { @Test void givenInvalidParamertersShouldThrow() { - String serviceName = "petclinic"; String usageLimitName = "maxPets"; double consumption = 5; assertAll( - () -> assertThrows(NullPointerException.class, () -> UsageLevel.nonRenewable(null, usageLimitName, consumption)), - () -> assertThrows(NullPointerException.class, () -> UsageLevel.nonRenewable(serviceName, null, consumption)), - () -> assertThrows(IllegalArgumentException.class, () -> UsageLevel.nonRenewable(serviceName, usageLimitName, -1))); + () -> assertThrows(NullPointerException.class, () -> UsageLevel.nonRenewable(null, consumption)), + () -> assertThrows(IllegalArgumentException.class, () -> UsageLevel.nonRenewable(usageLimitName, -1))); } @Test void givenRenewableUsageLimitShouldCreate() { - String serviceName = "Petclinic AI"; String usageLimitName = "maxTokens"; double consumption = 300; LocalDateTime resetTimestamp = LocalDateTime.of(2025, 8, 19, 0, 0); - UsageLevel usageLevel = UsageLevel.renewable(serviceName, usageLimitName, consumption, resetTimestamp); + UsageLevel usageLevel = UsageLevel.renewable(usageLimitName, consumption, resetTimestamp); assertAll( - () -> assertEquals(serviceName, usageLevel.getServiceName()), () -> assertEquals(usageLimitName, usageLevel.getName()), () -> assertEquals(consumption, usageLevel.getConsumption()), () -> assertEquals(resetTimestamp, usageLevel.getResetTimestamp().get()), From cb23e58f0d89d7adf3d5c2cc60edb14499b515f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Tue, 19 Aug 2025 18:24:54 +0200 Subject: [PATCH 12/42] feat(contracts): extract subscription billing period --- .../pgmarc/space/contracts/BillingPeriod.java | 55 ++++++++++++++ .../pgmarc/space/contracts/Subscription.java | 68 +++++++---------- .../space/contracts/BillingPeriodTest.java | 45 +++++++++++ .../space/contracts/SubscriptionTest.java | 74 ++++++------------- 4 files changed, 152 insertions(+), 90 deletions(-) create mode 100644 src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java create mode 100644 src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java 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..67c79d0 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -0,0 +1,55 @@ +package io.github.pgmarc.space.contracts; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; + +public final class BillingPeriod { + + private final LocalDateTime startDate; + private final LocalDateTime endDate; + private Duration renewalDays; + + private BillingPeriod(LocalDateTime startDate, LocalDateTime enDateTime) { + this.startDate = startDate; + this.endDate = enDateTime; + } + + public LocalDateTime getStartDate() { + return startDate; + } + + public LocalDateTime getEndDate() { + return endDate; + } + + public boolean isExpired(LocalDateTime dateTime) { + return dateTime.isAfter(endDate); + } + + public boolean isAutoRenewable() { + return renewalDays != null; + } + + public Optional getRenewalDate() { + return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays) : null); + } + + void setRenewalDays(Duration renewalDays) { + if (renewalDays != null && renewalDays.toDays() <= 0) { + throw new IllegalArgumentException("your subscription cannot expire in less than one day"); + } + this.renewalDays = renewalDays; + } + + static BillingPeriod of(LocalDateTime startDate, LocalDateTime endDate) { + 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"); + } + return new BillingPeriod(startDate, endDate); + } + +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index 3090071..d673308 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -15,33 +15,37 @@ public final class Subscription { private final UserContact userContact; private final Map services; - private final LocalDateTime startDate; - private final LocalDateTime endDate; - private final Duration renewalDays; + private final BillingPeriod billingPeriod; private final List history; private final Map> usageLevels; private Subscription(Builder builder) { this.userContact = builder.userContact; - this.startDate = builder.startDate; - this.endDate = builder.endDate; - this.renewalDays = builder.renewalDays; + 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, LocalDateTime startDate, LocalDateTime endDate, + public static Builder builder(UserContact userContact, BillingPeriod billingPeriod, Service service) { - return new Builder(userContact, startDate, endDate).subscribe(service); + return new Builder(userContact, billingPeriod).subscribe(service); + } + + public BillingPeriod getBillingPeriod() { + return billingPeriod; } public LocalDateTime getStartDate() { - return startDate; + return billingPeriod.getStartDate(); } public LocalDateTime getEndDate() { - return endDate; + return billingPeriod.getEndDate(); + } + + public boolean isAutoRenewable() { + return billingPeriod.isAutoRenewable(); } public String getUserId() { @@ -52,18 +56,6 @@ public String getUsername() { return userContact.getUsername(); } - public boolean isExpired(LocalDateTime dateTime) { - return dateTime.isAfter(endDate); - } - - public boolean isAutoRenewable() { - return renewalDays != null; - } - - public Optional getRenewalDate() { - return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays) : null); - } - public Optional getService(String serviceName) { return Optional.ofNullable(this.services.get(serviceName)); } @@ -86,30 +78,24 @@ public Map> getUsageLevels() { public static final class Builder { - private final LocalDateTime startDate; - private final LocalDateTime endDate; + 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 Duration renewalDays; - private Builder(UserContact userContact, LocalDateTime startDate, LocalDateTime endDate) { - this.startDate = startDate; - this.endDate = endDate; + private Builder(UserContact userContact, BillingPeriod billingPeriod) { + this.billingPeriod = billingPeriod; this.userContact = userContact; } public Builder renewIn(Duration renewalDays) { - if (renewalDays != null && renewalDays.toDays() <= 0) { - throw new IllegalArgumentException("your subscription cannot expire in less than one day"); - } - this.renewalDays = renewalDays; + this.billingPeriod.setRenewalDays(renewalDays); return this; } public Builder subscribe(Service service) { - if (!services.containsKey(service.getName())) { + if (!hasService(service.getName())) { this.usageLevels.put(service.getName(), new HashMap<>()); } this.services.put(service.getName(), Objects.requireNonNull(service, "service must not be null")); @@ -123,24 +109,26 @@ Builder addSnapshot(Subscription subscription) { } Builder addUsageLevel(String serviceName, UsageLevel usageLevel) { - if (!services.containsKey(serviceName)) { + if (!hasService(serviceName)) { throw new IllegalStateException("Service '" + serviceName + "' doesn't exist. Register it previously"); } - if (!usageLevels.get(serviceName).containsKey(usageLevel.getName())) { + boolean hasUsageLevel = usageLevels.get(serviceName).containsKey(usageLevel.getName()); + + if (!hasUsageLevel) { this.usageLevels.get(serviceName).put(usageLevel.getName(), usageLevel); } return this; } + private boolean hasService(String name) { + return services.containsKey(name); + } + public Subscription build() { - Objects.requireNonNull(startDate, "startDate must not be null"); - Objects.requireNonNull(endDate, "endDate must not be null"); + Objects.requireNonNull(billingPeriod, "billingPeriod must not be null"); Objects.requireNonNull(userContact, "userContact must not be null"); - if (startDate.isAfter(endDate)) { - throw new IllegalStateException("startDate is after endDate"); - } return new Subscription(this); } 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..ba260f9 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -0,0 +1,45 @@ +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +class BillingPeriodTest { + + private final LocalDateTime start = LocalDateTime.of(2025, 8, 15, 0, 0); + private final LocalDateTime end = start.plusDays(30); + + @Test + void givenZeroRenewalDaysShouldThrow() { + + BillingPeriod period = BillingPeriod.of(start, end); + Exception ex = assertThrows(IllegalArgumentException.class, + () -> period.setRenewalDays(Duration.ofHours(12))); + assertEquals("your subscription cannot expire in less than one day", ex.getMessage()); + } + + @Test + void givenStartDateAfterEndDateShouldThrow() { + + Exception ex = assertThrows(IllegalStateException.class, + () -> BillingPeriod.of(start, start.minusDays(1))); + assertEquals("startDate is after endDate", ex.getMessage()); + } + + @Test + void givenRenewableDateShouldBeRenowable() { + + BillingPeriod billingPeriod = BillingPeriod.of(start,end); + billingPeriod.setRenewalDays(Duration.ofDays(30)); + + assertAll( + () -> assertTrue(billingPeriod.isAutoRenewable()), + () -> assertEquals(end.plusDays(30), billingPeriod.getRenewalDate().get())); + } +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java index 7e790f2..c2007fb 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SubscriptionTest { @@ -19,15 +20,20 @@ class SubscriptionTest { .build(); private static final Service TEST_SERVICE = Service.builder("test", "alfa").plan("Foo").build(); - private static final LocalDateTime START = LocalDateTime.of(2025, 8, 15, 0, 0); + private BillingPeriod billingPeriod; - private static final LocalDateTime END = START.plusDays(30); + @BeforeEach + void setUp() { + LocalDateTime start = LocalDateTime.of(2025, 8, 15, 0, 0); + LocalDateTime end = start.plusDays(30); + billingPeriod = BillingPeriod.of(start, end); + billingPeriod.setRenewalDays(Duration.ofDays(30)); + } @Test void givenMultipleServicesInSubscriptionShouldCreate() { long renewalDays = 30; - LocalDateTime renewalDate = END.plusDays(renewalDays); String service1Name = "Petclinic"; String service2Name = "Petclinic Labs"; @@ -35,75 +41,43 @@ void givenMultipleServicesInSubscriptionShouldCreate() { Service service2 = Service.builder(service2Name, "v2").plan("PLATINUM").build(); Subscription sub = Subscription - .builder(TEST_USER_CONTACT, START, END, service1) + .builder(TEST_USER_CONTACT, billingPeriod, service1) .subscribe(service2) .renewIn(Duration.ofDays(renewalDays)) .build(); - assertAll( - () -> assertEquals(START, sub.getStartDate()), - () -> assertEquals(END, sub.getEndDate()), - () -> assertTrue(sub.isAutoRenewable()), - () -> assertEquals(renewalDate, sub.getRenewalDate().get())); - - assertEquals(2, sub.getServices().size()); - assertEquals(service1, sub.getService(service1Name).get()); - assertEquals(service2, sub.getService(service2Name).get()); + assertAll(() -> assertEquals(2, sub.getServices().size()), + () -> assertEquals(service1, sub.getService(service1Name).get()), + () -> assertEquals(service2, sub.getService(service2Name).get())); } @Test void whenNoRequiredParametersInputShouldThrow() { Exception ex = assertThrows(NullPointerException.class, - () -> Subscription.builder(null, START, END, TEST_SERVICE) + () -> Subscription.builder(null, billingPeriod, TEST_SERVICE) .build()); assertEquals("userContact must not be null", ex.getMessage()); ex = assertThrows(NullPointerException.class, - () -> Subscription.builder(TEST_USER_CONTACT, null, END, TEST_SERVICE) - .build()); - assertEquals("startDate must not be null", ex.getMessage()); - - ex = assertThrows(NullPointerException.class, - () -> Subscription.builder(TEST_USER_CONTACT, START, null, TEST_SERVICE) - .build()); - assertEquals("endDate must not be null", ex.getMessage()); - } - - @Test - void givenStartDateAfterEndDateShouldThrow() { - - LocalDateTime end = START.minusDays(1); - - Exception ex = assertThrows(IllegalStateException.class, - () -> Subscription.builder(TEST_USER_CONTACT, START, end, TEST_SERVICE) + () -> Subscription.builder(TEST_USER_CONTACT, null, TEST_SERVICE) .build()); - assertEquals("startDate is after endDate", ex.getMessage()); + assertEquals("billingPeriod must not be null", ex.getMessage()); } @Test void givenOptionalRenewalDaysShouldNotThrow() { - assertDoesNotThrow(() -> Subscription.builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + assertDoesNotThrow(() -> Subscription.builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) .renewIn(null) .build()); } - @Test - void givenZeroRenewalDaysShouldThrow() { - - Exception ex = assertThrows(IllegalArgumentException.class, - () -> Subscription.builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) - .renewIn(Duration.ofDays(0)) - .build()); - assertEquals("your subscription cannot expire in less than one day", ex.getMessage()); - } - private Subscription firstPetclinicSub(Service petclinic) { LocalDateTime start = LocalDateTime.of(2024, 1, 1, 0, 0); LocalDateTime end = LocalDateTime.of(2024, 2, 1, 0, 0); - return Subscription.builder(TEST_USER_CONTACT, start, end, petclinic) + return Subscription.builder(TEST_USER_CONTACT, BillingPeriod.of(start, end), petclinic) .build(); } @@ -113,7 +87,7 @@ private Subscription secondSubscription(Service petclinic, Service petclinicLabs LocalDateTime end = LocalDateTime.of(2024, 6, 1, 0, 0); return Subscription - .builder(TEST_USER_CONTACT, start, end, petclinic) + .builder(TEST_USER_CONTACT, BillingPeriod.of(start, end), petclinic) .subscribe(petclinicLabs) .build(); } @@ -131,7 +105,7 @@ void givenASubscriptionHistoryShouldBeVisbile() { Subscription sub2 = secondSubscription(petclinicV2, petclinicLabsV1); Subscription currentSubscription = Subscription - .builder(TEST_USER_CONTACT, START, END, petclinicV2) + .builder(TEST_USER_CONTACT, billingPeriod, petclinicV2) .addSnapshot(sub1) .addSnapshot(sub2) .build(); @@ -158,7 +132,7 @@ void givenSubscriptionWithUsageLevelsShouldCreate() { UsageLevel ul1 = UsageLevel.nonRenewable(usageLimitName, 5); Subscription sub = Subscription - .builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + .builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) .addUsageLevel(TEST_SERVICE.getName(), ul1) .build(); @@ -170,7 +144,7 @@ void givenSubscriptionWithUsageLevelsShouldCreate() { void givenSubscriptionToAServiceRelatedUsageLevelShoudBeEmpty() { Subscription sub = Subscription - .builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + .builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) .build(); assertAll(() -> assertTrue(sub.getUsageLevels().containsKey(TEST_SERVICE.getName())), @@ -183,7 +157,7 @@ void givenDuplicateUsageLevelShouldNotRegisterTwice() { UsageLevel ul1 = UsageLevel.nonRenewable(usageLimitName, 5); Subscription sub = Subscription - .builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + .builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) .addUsageLevel(TEST_SERVICE.getName(), ul1) .addUsageLevel(TEST_SERVICE.getName(), ul1) .build(); @@ -197,7 +171,7 @@ void givenNonExistentServiceShouldThrowWhenAddingUsageLevel() { UsageLevel ul1 = UsageLevel.nonRenewable("maxAlfa", 5); Exception ex = assertThrows(IllegalStateException.class, () -> Subscription - .builder(TEST_USER_CONTACT, START, END, TEST_SERVICE) + .builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) .addUsageLevel("nonExistent", ul1) .build()); From f6531a7ff2a9e0bad83b82122530832cc8c3a27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Wed, 20 Aug 2025 11:09:34 +0200 Subject: [PATCH 13/42] feat(contracts): parse json to billing period --- .../pgmarc/space/contracts/BillingPeriod.java | 77 +++++++++++++++++++ .../space/contracts/BillingPeriodTest.java | 27 ++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java index 67c79d0..7554e79 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -2,9 +2,13 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.Objects; import java.util.Optional; +import org.json.JSONObject; + public final class BillingPeriod { private final LocalDateTime startDate; @@ -52,4 +56,77 @@ static BillingPeriod of(LocalDateTime startDate, LocalDateTime endDate) { return new BillingPeriod(startDate, endDate); } + private 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; + } + } + + static BillingPeriod fromJson(JSONObject json) { + Objects.requireNonNull(json, "billing period json must not be null"); + OffsetDateTime startUtc = OffsetDateTime.parse(json.getString(Keys.START_DATE.toString())); + OffsetDateTime endUtc = OffsetDateTime.parse(json.getString(Keys.END_DATE.toString())); + BillingPeriod billingPeriod = BillingPeriod.of(startUtc.toLocalDateTime(), endUtc.toLocalDateTime()); + billingPeriod.setRenewalDays(Duration.ofDays(json.optLong(Keys.RENEWAL_DAYS.toString()))); + + return billingPeriod; + } + + @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/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java index ba260f9..9704bd0 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -7,7 +7,9 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import org.json.JSONObject; import org.junit.jupiter.api.Test; class BillingPeriodTest { @@ -35,11 +37,34 @@ void givenStartDateAfterEndDateShouldThrow() { @Test void givenRenewableDateShouldBeRenowable() { - BillingPeriod billingPeriod = BillingPeriod.of(start,end); + BillingPeriod billingPeriod = BillingPeriod.of(start, end); billingPeriod.setRenewalDays(Duration.ofDays(30)); assertAll( () -> assertTrue(billingPeriod.isAutoRenewable()), () -> assertEquals(end.plusDays(30), billingPeriod.getRenewalDate().get())); } + + @Test + void givenJsonShouldCreateBillingPeriod() { + + String startUtc = "2024-08-20T12:00Z"; + String endUtc = "2025-08-20T12:00Z"; + LocalDateTime start = OffsetDateTime.parse(startUtc).toLocalDateTime(); + LocalDateTime end = OffsetDateTime.parse(endUtc).toLocalDateTime(); + 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, BillingPeriod.fromJson(input)); + } + + @Test + void giveNullJsonShouldThrow() { + + Exception ex = assertThrows(NullPointerException.class, () -> BillingPeriod.fromJson(null)); + assertEquals("billing period json must not be null", ex.getMessage()); + } } From 491c8252968dd54b71dc05d352c06ea115be795a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Wed, 20 Aug 2025 11:21:28 +0200 Subject: [PATCH 14/42] feat(contracts): parse json to user contact --- .../pgmarc/space/contracts/UserContact.java | 15 ++++++++-- .../space/contracts/UserContactTest.java | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java index 6852641..86564b5 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java @@ -159,17 +159,26 @@ public String toString() { } } + static UserContact fromJson(JSONObject json) { + Objects.requireNonNull(json, "user contact json must not be null"); + return UserContact.builder(json.getString(JsonKeys.USER_ID.toString()), + json.getString(JsonKeys.USERNAME.toString())) + .firstName(json.optString(JsonKeys.FIRST_NAME.toString())) + .lastName(json.optString(JsonKeys.LAST_NAME.toString())) + .email(json.optString(JsonKeys.EMAIL.toString())) + .phone(json.optString(JsonKeys.PHONE.toString())) + .build(); + } + @Override public JSONObject toJson() { - JSONObject obj = new JSONObject() + return new JSONObject() .put(JsonKeys.USER_ID.toString(), userId) .put(JsonKeys.USERNAME.toString(), username) .putOpt(JsonKeys.FIRST_NAME.toString(), firstName) .putOpt(JsonKeys.LAST_NAME.toString(), lastName) .putOpt(JsonKeys.EMAIL.toString(), email) .putOpt(JsonKeys.PHONE.toString(), phone); - - return obj; } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index f0467dc..fd28275 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -112,4 +112,34 @@ void givenUserContactShouldSerializeToJson() { assertTrue(obj.similar(userContact.toJson())); } + @Test + void givenUserContactJsonShouldParse() { + + String firtName = "Alex"; + String lastName = "Doe"; + String email = "alexdoe@example.com"; + String phone = "(+34) 666 666 666"; + UserContact expected = UserContact.builder(TEST_USER_ID, TEST_USERNAME) + .firstName(firtName) + .lastName(lastName) + .email(email) + .phone(phone) + .build(); + + JSONObject input = new JSONObject().put("userId", TEST_USER_ID) + .put("username", TEST_USERNAME) + .put("firstName", firtName) + .put("lastName", lastName) + .put("email", email) + .put("phone", phone); + assertEquals(expected, UserContact.fromJson(input)); + } + + @Test + void givenNullJsonShouldThrow() { + + Exception ex = assertThrows(NullPointerException.class, () -> UserContact.fromJson(null)); + assertEquals("user contact json must not be null", ex.getMessage()); + } + } From d1638401fe0fd5003e3a2fb64b3b20c498ebc8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Wed, 20 Aug 2025 13:58:09 +0200 Subject: [PATCH 15/42] fix(contracts): incorrect UserContact json parsing --- .../github/pgmarc/space/contracts/UserContact.java | 8 ++++---- .../pgmarc/space/contracts/UserContactTest.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java index 86564b5..4a3669f 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java @@ -163,10 +163,10 @@ static UserContact fromJson(JSONObject json) { Objects.requireNonNull(json, "user contact json must not be null"); return UserContact.builder(json.getString(JsonKeys.USER_ID.toString()), json.getString(JsonKeys.USERNAME.toString())) - .firstName(json.optString(JsonKeys.FIRST_NAME.toString())) - .lastName(json.optString(JsonKeys.LAST_NAME.toString())) - .email(json.optString(JsonKeys.EMAIL.toString())) - .phone(json.optString(JsonKeys.PHONE.toString())) + .firstName(json.optString(JsonKeys.FIRST_NAME.toString(), null)) + .lastName(json.optString(JsonKeys.LAST_NAME.toString(), null)) + .email(json.optString(JsonKeys.EMAIL.toString(), null)) + .phone(json.optString(JsonKeys.PHONE.toString(), null)) .build(); } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index fd28275..4313b61 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -135,6 +135,20 @@ void givenUserContactJsonShouldParse() { assertEquals(expected, UserContact.fromJson(input)); } + @Test + void givenMinimunUserContactOptionalsShouldNotBeParsed() { + + JSONObject input = new JSONObject() + .put("userId", TEST_USER_ID) + .put("username", TEST_USERNAME); + + UserContact actual = UserContact.fromJson(input); + assertAll(() -> assertTrue(actual.getFirstName().isEmpty()), + () -> assertTrue(actual.getLastName().isEmpty()), + () -> assertTrue(actual.getEmail().isEmpty()), + () -> assertTrue(actual.getPhone().isEmpty())); + } + @Test void givenNullJsonShouldThrow() { From d37b5c91bd583b82403ed50de608813a3116a03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Wed, 20 Aug 2025 20:51:03 +0200 Subject: [PATCH 16/42] feat(contracts): parse subscription json to Subscription --- .../pgmarc/space/contracts/BillingPeriod.java | 18 +- .../pgmarc/space/contracts/Subscription.java | 220 ++++++++++++++++-- .../space/contracts/SubscriptionSnapshot.java | 77 ------ .../pgmarc/space/contracts/UsageLevel.java | 17 +- .../space/contracts/SubscriptionTest.java | 172 ++++++-------- .../space/contracts/UsageLevelTest.java | 8 +- 6 files changed, 286 insertions(+), 226 deletions(-) delete mode 100644 src/main/java/io/github/pgmarc/space/contracts/SubscriptionSnapshot.java diff --git a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java index 7554e79..6dac438 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -3,13 +3,12 @@ import java.time.Duration; import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.Objects; import java.util.Optional; import org.json.JSONObject; -public final class BillingPeriod { +final class BillingPeriod { private final LocalDateTime startDate; private final LocalDateTime endDate; @@ -20,23 +19,23 @@ private BillingPeriod(LocalDateTime startDate, LocalDateTime enDateTime) { this.endDate = enDateTime; } - public LocalDateTime getStartDate() { + LocalDateTime getStartDate() { return startDate; } - public LocalDateTime getEndDate() { + LocalDateTime getEndDate() { return endDate; } - public boolean isExpired(LocalDateTime dateTime) { + boolean isExpired(LocalDateTime dateTime) { return dateTime.isAfter(endDate); } - public boolean isAutoRenewable() { + boolean isAutoRenewable() { return renewalDays != null; } - public Optional getRenewalDate() { + Optional getRenewalDate() { return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays) : null); } @@ -124,9 +123,6 @@ public boolean equals(Object obj) { @Override public String toString() { - return "From " + startDate + " to " + endDate + ", renews in " + renewalDays; + return "From " + startDate + " to " + endDate + ", renews in " + renewalDays; } - - - } diff --git a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index d673308..3d772b6 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -2,7 +2,10 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -10,13 +13,17 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONObject; public final class Subscription { private final UserContact userContact; private final Map services; private final BillingPeriod billingPeriod; - private final List history; + private final List history; private final Map> usageLevels; private Subscription(Builder builder) { @@ -32,8 +39,9 @@ public static Builder builder(UserContact userContact, BillingPeriod billingPeri return new Builder(userContact, billingPeriod).subscribe(service); } - public BillingPeriod getBillingPeriod() { - return billingPeriod; + public static Builder builder(UserContact usagerContact, BillingPeriod billingPeriod, + Collection services) { + return new Builder(usagerContact, billingPeriod).subscribeAll(services); } public LocalDateTime getStartDate() { @@ -48,6 +56,10 @@ public boolean isAutoRenewable() { return billingPeriod.isAutoRenewable(); } + public Optional getRenewalDate() { + return billingPeriod.getRenewalDate(); + } + public String getUserId() { return userContact.getUserId(); } @@ -68,7 +80,7 @@ public Set getServices() { return Set.copyOf(services.values()); } - public List getHistory() { + public List getHistory() { return history; } @@ -76,12 +88,87 @@ public Map> getUsageLevels() { return usageLevels; } + private 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; + } + } + + static Map servicesFromJson(JSONObject json) { + JSONObject contractedServices = json.getJSONObject(Keys.CONTRACTED_SERVICES.toString()); + JSONObject subscriptionPlans = json.getJSONObject(Keys.SUBSCRIPTION_PLANS.toString()); + JSONObject subscriptionAddOns = json.getJSONObject(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; + } + + private static Map> usageLevelsFromJson(JSONObject usageLevel) { + Objects.requireNonNull(usageLevel, "usage level must not be null"); + + Map> usageLevelMap = new HashMap<>(); + if (usageLevel.isEmpty()) { + return Collections.unmodifiableMap(usageLevelMap); + } + for (String serviceName : usageLevel.keySet()) { + Map serviceLevels = new HashMap<>(); + JSONObject rawServiceUsageLevels = usageLevel.getJSONObject(serviceName); + for (String usageLimitName : rawServiceUsageLevels.keySet()) { + JSONObject rawUsageLevel = rawServiceUsageLevels.getJSONObject(usageLimitName); + LocalDateTime expirationDate = null; + if (rawUsageLevel.has("resetTimestamp")) { + expirationDate = ZonedDateTime.parse(rawUsageLevel.getString("resetTimestamp")).toLocalDateTime(); + } + UsageLevel ul = UsageLevel.of(serviceName, rawUsageLevel.getDouble("consumed"), expirationDate); + serviceLevels.put(usageLimitName, ul); + } + usageLevelMap.put(serviceName, Collections.unmodifiableMap(serviceLevels)); + } + return usageLevelMap; + } + + static Subscription fromJson(JSONObject json) { + + BillingPeriod billingPeriod = BillingPeriod.fromJson(json.getJSONObject(Keys.BILLING_PERIOD.toString())); + UserContact userContact = UserContact.fromJson(json.getJSONObject(Keys.USER_CONTACT.toString())); + Map> usageLevels = usageLevelsFromJson( + json.getJSONObject(Keys.USAGE_LEVEL.toString())); + Map services = servicesFromJson(json); + List history = Snapshot.fromJson(json.optJSONArray(Keys.HISTORY.toString())); + return Subscription.builder(userContact, billingPeriod, services.values()) + .addUsageLevels(usageLevels).addSnapshots(history).build(); + } + 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 List history = new ArrayList<>(); private final Map> usageLevels = new HashMap<>(); private Builder(UserContact userContact, BillingPeriod billingPeriod) { @@ -95,35 +182,30 @@ public Builder renewIn(Duration renewalDays) { } public Builder subscribe(Service service) { - if (!hasService(service.getName())) { - this.usageLevels.put(service.getName(), new HashMap<>()); - } this.services.put(service.getName(), Objects.requireNonNull(service, "service must not be null")); return this; } - Builder addSnapshot(Subscription subscription) { - Objects.requireNonNull(subscription, "subscription must not be null"); - this.history.add(SubscriptionSnapshot.of(subscription)); - return this; + private Map collectionToServiceMap(Collection services) { + return services.stream() + .collect(Collectors.toUnmodifiableMap(Service::getName, service -> service)); } - Builder addUsageLevel(String serviceName, UsageLevel usageLevel) { - if (!hasService(serviceName)) { - throw new IllegalStateException("Service '" + serviceName + "' doesn't exist. Register it previously"); - } - - boolean hasUsageLevel = usageLevels.get(serviceName).containsKey(usageLevel.getName()); - - if (!hasUsageLevel) { - this.usageLevels.get(serviceName).put(usageLevel.getName(), usageLevel); - } + public Builder subscribeAll(Collection services) { + Objects.requireNonNull(services, "services must not be null"); + this.services.putAll(collectionToServiceMap(services)); + return this; + } + private Builder addSnapshots(Collection snaphsots) { + Objects.requireNonNull(snaphsots, "snapshots must not be null"); + this.history.addAll(snaphsots); return this; } - private boolean hasService(String name) { - return services.containsKey(name); + private Builder addUsageLevels(Map> usageLevels) { + this.usageLevels.putAll(usageLevels); + return this; } public Subscription build() { @@ -131,6 +213,96 @@ public Subscription build() { 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; + + private Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, + Map services) { + this.starDateTime = startDateTime; + this.enDateTime = endDateTime; + this.services = Collections.unmodifiableMap(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 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; + } + + private static List fromJson(JSONArray rawHistory) { + List history = new ArrayList<>(); + for (int i = 0; i < rawHistory.length(); i++) { + JSONObject snaphsot = rawHistory.getJSONObject(i); + OffsetDateTime startUtc = OffsetDateTime.parse(snaphsot.getString("startDate")); + OffsetDateTime end = OffsetDateTime.parse(snaphsot.getString("endDate")); + history.add(new Snapshot(startUtc.toLocalDateTime(), end.toLocalDateTime(), + Subscription.servicesFromJson(snaphsot))); + } + return history; + } } diff --git a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionSnapshot.java b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionSnapshot.java deleted file mode 100644 index f2b40d9..0000000 --- a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionSnapshot.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.github.pgmarc.space.contracts; - -import java.time.LocalDateTime; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -public final class SubscriptionSnapshot { - - private final LocalDateTime starDateTime; - private final LocalDateTime enDateTime; - private final Map services; - - private SubscriptionSnapshot(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 services; - } - - public Optional getService(String name) { - return Optional.ofNullable(services.get(name)); - } - - static SubscriptionSnapshot of(Subscription subscription) { - Objects.requireNonNull(subscription, "subscription must not be null"); - return new SubscriptionSnapshot(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; - SubscriptionSnapshot other = (SubscriptionSnapshot) 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/UsageLevel.java b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java index 6f47032..c7e82f8 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java @@ -15,6 +15,12 @@ private UsageLevel(String name, double consumed) { this.consumed = consumed; } + private UsageLevel(String name, double consumed, LocalDateTime resetTimestamp) { + this.name = name; + this.consumed = consumed; + this.resetTimestamp = resetTimestamp; + } + public String getName() { return name; } @@ -23,10 +29,6 @@ public Optional getResetTimestamp() { return Optional.ofNullable(resetTimestamp); } - private void setResetTimestamp(LocalDateTime resetTimestamp) { - this.resetTimestamp = resetTimestamp; - } - public boolean isRenewableUsageLimit() { return resetTimestamp != null; } @@ -42,15 +44,14 @@ private static void validateUsageLevel(String name, double consumed) { } } - public static UsageLevel nonRenewable(String name, double consumed) { + static UsageLevel of(String name, double consumed) { validateUsageLevel(name, consumed); return new UsageLevel(name, consumed); } - public static UsageLevel renewable(String name, double consumed, LocalDateTime resetTimestamp) { + static UsageLevel of(String name, double consumed, LocalDateTime resetTimestamp) { validateUsageLevel(name, consumed); - UsageLevel level = new UsageLevel(name, consumed); - level.setResetTimestamp(resetTimestamp); + UsageLevel level = new UsageLevel(name, consumed, resetTimestamp); return level; } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java index c2007fb..a96e216 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -8,10 +8,17 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONStringer; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class SubscriptionTest { @@ -73,110 +80,71 @@ void givenOptionalRenewalDaysShouldNotThrow() { .build()); } - private Subscription firstPetclinicSub(Service petclinic) { - LocalDateTime start = LocalDateTime.of(2024, 1, 1, 0, 0); - LocalDateTime end = LocalDateTime.of(2024, 2, 1, 0, 0); - - return Subscription.builder(TEST_USER_CONTACT, BillingPeriod.of(start, end), petclinic) - .build(); - } - - private Subscription secondSubscription(Service petclinic, Service petclinicLabs) { - - LocalDateTime start = LocalDateTime.of(2024, 5, 1, 0, 0); - LocalDateTime end = LocalDateTime.of(2024, 6, 1, 0, 0); - - return Subscription - .builder(TEST_USER_CONTACT, BillingPeriod.of(start, end), petclinic) - .subscribe(petclinicLabs) - .build(); - } - @Test - void givenASubscriptionHistoryShouldBeVisbile() { - - String petclinic = "Petclinic"; - String petclinicLabs = "Petclinic Labs"; - Service petclinicV1 = Service.builder(petclinic, "v1").plan("FREE").build(); - Service petclinicV2 = Service.builder(petclinic, "v2").plan("GOLD").build(); - Service petclinicLabsV1 = Service.builder(petclinicLabs, "v1").plan("PLATINUM").build(); - - Subscription sub1 = firstPetclinicSub(petclinicV1); - Subscription sub2 = secondSubscription(petclinicV2, petclinicLabsV1); - - Subscription currentSubscription = Subscription - .builder(TEST_USER_CONTACT, billingPeriod, petclinicV2) - .addSnapshot(sub1) - .addSnapshot(sub2) - .build(); - - List history = new ArrayList<>(); - SubscriptionSnapshot snaphot1 = SubscriptionSnapshot.of(sub1); - SubscriptionSnapshot snaphot2 = SubscriptionSnapshot.of(sub2); - - history.add(snaphot1); - history.add(snaphot2); - - assertEquals(history, currentSubscription.getHistory()); - + void givenSubscriptionAsJsonShouldCreateSubscription() { + + String startUtcString = "2025-04-18T00:00:00Z"; + String endUtcString = "2025-12-31T00:00:00Z"; + int renewalDays = 365; + String zoomName = "zoom"; + String zoomVersion = "2025"; + String zoomPlan = "ENTERPRISE"; + String zoomExtraSeats = "extraSeats"; + int zoomExtraSeatsQuantity = 2; + String zoomHugeMeetings = "hugeMeetings"; + int zoomHugeMeetingQuantity = 1; + + String petclinicService = "petclinic"; + String petclinicVersion = "2024"; + String petclinicPlan = "GOLD"; + String petclinicPetsAdoptionCentre = "petsAdoptionCentre"; + int petclinicPetsAdoptionCentreQuantity = 1; + + Map billingPeriodMap = Map.of( + "startDate", startUtcString, + "endDate", endUtcString, + "autoRenew", true, + "renewalDays", renewalDays); + Map> usageLevel = Map.of( + zoomName, + Map.of("maxSeats", + Map.of("consumed", 10.0)), + petclinicService, + Map.of("maxPets", + Map.of("consumed", 2.0), + "maxVisits", Map.of("consumed", 5.0, "resetTimestamp", "2025-07-31T00:00:00Z"))); + Map contractedServices = Map.of(zoomName, zoomVersion, petclinicService, petclinicVersion); + Map contractedPlans = Map.of(zoomName, zoomPlan, petclinicService, petclinicPlan); + Map> contractedAddOns = Map.of( + zoomName, + Map.of(zoomExtraSeats, zoomExtraSeatsQuantity, + zoomHugeMeetings, zoomHugeMeetingQuantity), + petclinicService, + Map.of(petclinicPetsAdoptionCentre, petclinicPetsAdoptionCentreQuantity)); + + JSONObject snapshot1 = new JSONObject() + .put("startDate", "2024-04-18T00:00:00Z") + .put("endDate", "2024-05-18T00:00:00Z") + .put("contractedServices", contractedServices) + .put("subscriptionPlans", contractedPlans) + .put("subscriptionAddOns", contractedAddOns); + JSONArray history = new JSONArray() + .put(snapshot1); + + JSONObject jsonInput = new JSONObject() + .put("userContact", TEST_USER_CONTACT.toJson()) + .put("billingPeriod", billingPeriodMap) + .put("usageLevel", usageLevel) + .put("contractedServices", contractedServices) + .put("subscriptionPlans", contractedPlans) + .put("subscriptionAddOns", contractedAddOns) + .put("history", history); + + Subscription actual = Subscription.fromJson(jsonInput); assertAll( - () -> assertEquals(2, currentSubscription.getHistory().size()), - () -> assertEquals(history, currentSubscription.getHistory())); + () -> assertEquals(1, actual.getHistory().size()), + () -> assertEquals(2, actual.getUsageLevels().size()), + () -> assertEquals(2, actual.getServicesMap().size())); } - - @Test - void givenSubscriptionWithUsageLevelsShouldCreate() { - - String usageLimitName = "maxAlfa"; - UsageLevel ul1 = UsageLevel.nonRenewable(usageLimitName, 5); - - Subscription sub = Subscription - .builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) - .addUsageLevel(TEST_SERVICE.getName(), ul1) - .build(); - - assertEquals(ul1, sub.getUsageLevels().get(TEST_SERVICE.getName()).get(usageLimitName)); - - } - - @Test - void givenSubscriptionToAServiceRelatedUsageLevelShoudBeEmpty() { - - Subscription sub = Subscription - .builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) - .build(); - - assertAll(() -> assertTrue(sub.getUsageLevels().containsKey(TEST_SERVICE.getName())), - () -> assertTrue(sub.getUsageLevels().get(TEST_SERVICE.getName()).isEmpty())); - } - - @Test - void givenDuplicateUsageLevelShouldNotRegisterTwice() { - String usageLimitName = "maxAlfa"; - UsageLevel ul1 = UsageLevel.nonRenewable(usageLimitName, 5); - - Subscription sub = Subscription - .builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) - .addUsageLevel(TEST_SERVICE.getName(), ul1) - .addUsageLevel(TEST_SERVICE.getName(), ul1) - .build(); - - assertEquals(1, sub.getUsageLevels().get(TEST_SERVICE.getName()).size()); - } - - @Test - void givenNonExistentServiceShouldThrowWhenAddingUsageLevel() { - - UsageLevel ul1 = UsageLevel.nonRenewable("maxAlfa", 5); - - Exception ex = assertThrows(IllegalStateException.class, () -> Subscription - .builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) - .addUsageLevel("nonExistent", ul1) - .build()); - - assertEquals("Service 'nonExistent' doesn't exist. Register it previously", ex.getMessage()); - - } - } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java index 27e875e..baa2697 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java @@ -17,7 +17,7 @@ void givenNonRenewableUsageLimitShouldCreate() { String usageLimitName = "maxPets"; double consumption = 5; - UsageLevel usageLevel = UsageLevel.nonRenewable(usageLimitName, consumption); + UsageLevel usageLevel = UsageLevel.of(usageLimitName, consumption); assertAll( () -> assertEquals(usageLimitName, usageLevel.getName()), @@ -32,8 +32,8 @@ void givenInvalidParamertersShouldThrow() { double consumption = 5; assertAll( - () -> assertThrows(NullPointerException.class, () -> UsageLevel.nonRenewable(null, consumption)), - () -> assertThrows(IllegalArgumentException.class, () -> UsageLevel.nonRenewable(usageLimitName, -1))); + () -> assertThrows(NullPointerException.class, () -> UsageLevel.of(null, consumption)), + () -> assertThrows(IllegalArgumentException.class, () -> UsageLevel.of(usageLimitName, -1))); } @Test @@ -42,7 +42,7 @@ void givenRenewableUsageLimitShouldCreate() { String usageLimitName = "maxTokens"; double consumption = 300; LocalDateTime resetTimestamp = LocalDateTime.of(2025, 8, 19, 0, 0); - UsageLevel usageLevel = UsageLevel.renewable(usageLimitName, consumption, resetTimestamp); + UsageLevel usageLevel = UsageLevel.of(usageLimitName, consumption, resetTimestamp); assertAll( () -> assertEquals(usageLimitName, usageLevel.getName()), From 1534a2ce422eb408f0502d64c3f59d2af8625789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Wed, 20 Aug 2025 21:48:53 +0200 Subject: [PATCH 17/42] feat(contracts): change date types to represent time zone --- .../pgmarc/space/contracts/BillingPeriod.java | 26 +++++++++---------- .../pgmarc/space/contracts/Subscription.java | 4 +-- .../pgmarc/space/contracts/UsageLevel.java | 9 ++++--- .../space/contracts/BillingPeriodTest.java | 13 +++++----- .../space/contracts/SubscriptionTest.java | 11 ++------ .../space/contracts/UsageLevelTest.java | 6 ++--- 6 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java index 6dac438..a265495 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -2,7 +2,7 @@ import java.time.Duration; import java.time.LocalDateTime; -import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.Objects; import java.util.Optional; @@ -10,25 +10,25 @@ final class BillingPeriod { - private final LocalDateTime startDate; - private final LocalDateTime endDate; + private final ZonedDateTime startDate; + private final ZonedDateTime endDate; private Duration renewalDays; - private BillingPeriod(LocalDateTime startDate, LocalDateTime enDateTime) { + private BillingPeriod(ZonedDateTime startDate, ZonedDateTime endDate) { this.startDate = startDate; - this.endDate = enDateTime; + this.endDate = endDate; } LocalDateTime getStartDate() { - return startDate; + return startDate.toLocalDateTime(); } LocalDateTime getEndDate() { - return endDate; + return endDate.toLocalDateTime(); } boolean isExpired(LocalDateTime dateTime) { - return dateTime.isAfter(endDate); + return endDate.isAfter(startDate); } boolean isAutoRenewable() { @@ -36,7 +36,7 @@ boolean isAutoRenewable() { } Optional getRenewalDate() { - return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays) : null); + return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays).toLocalDateTime() : null); } void setRenewalDays(Duration renewalDays) { @@ -46,7 +46,7 @@ void setRenewalDays(Duration renewalDays) { this.renewalDays = renewalDays; } - static BillingPeriod of(LocalDateTime startDate, LocalDateTime endDate) { + static BillingPeriod of(ZonedDateTime startDate, ZonedDateTime endDate) { Objects.requireNonNull(startDate, "startDate must not be null"); Objects.requireNonNull(endDate, "endDate must not be null"); if (startDate.isAfter(endDate)) { @@ -76,9 +76,9 @@ public String toString() { static BillingPeriod fromJson(JSONObject json) { Objects.requireNonNull(json, "billing period json must not be null"); - OffsetDateTime startUtc = OffsetDateTime.parse(json.getString(Keys.START_DATE.toString())); - OffsetDateTime endUtc = OffsetDateTime.parse(json.getString(Keys.END_DATE.toString())); - BillingPeriod billingPeriod = BillingPeriod.of(startUtc.toLocalDateTime(), endUtc.toLocalDateTime()); + ZonedDateTime start = ZonedDateTime.parse(json.getString(Keys.START_DATE.toString())); + ZonedDateTime end = ZonedDateTime.parse(json.getString(Keys.END_DATE.toString())); + BillingPeriod billingPeriod = BillingPeriod.of(start, end); billingPeriod.setRenewalDays(Duration.ofDays(json.optLong(Keys.RENEWAL_DAYS.toString()))); return billingPeriod; diff --git a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index 3d772b6..8c79d8a 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -139,9 +139,9 @@ private static Map> usageLevelsFromJson(JSONObje JSONObject rawServiceUsageLevels = usageLevel.getJSONObject(serviceName); for (String usageLimitName : rawServiceUsageLevels.keySet()) { JSONObject rawUsageLevel = rawServiceUsageLevels.getJSONObject(usageLimitName); - LocalDateTime expirationDate = null; + ZonedDateTime expirationDate = null; if (rawUsageLevel.has("resetTimestamp")) { - expirationDate = ZonedDateTime.parse(rawUsageLevel.getString("resetTimestamp")).toLocalDateTime(); + expirationDate = ZonedDateTime.parse(rawUsageLevel.getString("resetTimestamp")); } UsageLevel ul = UsageLevel.of(serviceName, rawUsageLevel.getDouble("consumed"), expirationDate); serviceLevels.put(usageLimitName, ul); diff --git a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java index c7e82f8..00dd241 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java @@ -1,6 +1,7 @@ package io.github.pgmarc.space.contracts; import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.Objects; import java.util.Optional; @@ -8,14 +9,14 @@ public final class UsageLevel { private final String name; private final double consumed; - private LocalDateTime resetTimestamp; + private ZonedDateTime resetTimestamp; private UsageLevel(String name, double consumed) { this.name = name; this.consumed = consumed; } - private UsageLevel(String name, double consumed, LocalDateTime resetTimestamp) { + private UsageLevel(String name, double consumed, ZonedDateTime resetTimestamp) { this.name = name; this.consumed = consumed; this.resetTimestamp = resetTimestamp; @@ -26,7 +27,7 @@ public String getName() { } public Optional getResetTimestamp() { - return Optional.ofNullable(resetTimestamp); + return resetTimestamp != null ? Optional.of(resetTimestamp.toLocalDateTime()) : Optional.empty(); } public boolean isRenewableUsageLimit() { @@ -49,7 +50,7 @@ static UsageLevel of(String name, double consumed) { return new UsageLevel(name, consumed); } - static UsageLevel of(String name, double consumed, LocalDateTime resetTimestamp) { + static UsageLevel of(String name, double consumed, ZonedDateTime resetTimestamp) { validateUsageLevel(name, consumed); UsageLevel level = new UsageLevel(name, consumed, resetTimestamp); return level; diff --git a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java index 9704bd0..2505166 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -6,16 +6,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Duration; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import org.json.JSONObject; import org.junit.jupiter.api.Test; class BillingPeriodTest { - private final LocalDateTime start = LocalDateTime.of(2025, 8, 15, 0, 0); - private final LocalDateTime end = start.plusDays(30); + private final ZonedDateTime start = ZonedDateTime.parse("2025-08-15T00:00:00Z"); + private final ZonedDateTime end = start.plusDays(30); @Test void givenZeroRenewalDaysShouldThrow() { @@ -42,7 +41,7 @@ void givenRenewableDateShouldBeRenowable() { assertAll( () -> assertTrue(billingPeriod.isAutoRenewable()), - () -> assertEquals(end.plusDays(30), billingPeriod.getRenewalDate().get())); + () -> assertEquals(end.plusDays(30).toLocalDateTime(), billingPeriod.getRenewalDate().get())); } @Test @@ -50,8 +49,8 @@ void givenJsonShouldCreateBillingPeriod() { String startUtc = "2024-08-20T12:00Z"; String endUtc = "2025-08-20T12:00Z"; - LocalDateTime start = OffsetDateTime.parse(startUtc).toLocalDateTime(); - LocalDateTime end = OffsetDateTime.parse(endUtc).toLocalDateTime(); + ZonedDateTime start = ZonedDateTime.parse(startUtc); + ZonedDateTime end = ZonedDateTime.parse(endUtc); BillingPeriod expected = BillingPeriod.of(start, end); expected.setRenewalDays(Duration.ofDays(30)); diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java index a96e216..c3ba7a4 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -4,21 +4,14 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 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 java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import org.json.JSONStringer; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class SubscriptionTest { @@ -31,8 +24,8 @@ class SubscriptionTest { @BeforeEach void setUp() { - LocalDateTime start = LocalDateTime.of(2025, 8, 15, 0, 0); - LocalDateTime end = start.plusDays(30); + ZonedDateTime start = ZonedDateTime.parse("2025-08-15T00:00:00Z"); + ZonedDateTime end = start.plusDays(30); billingPeriod = BillingPeriod.of(start, end); billingPeriod.setRenewalDays(Duration.ofDays(30)); } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java index baa2697..4a2ebc5 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UsageLevelTest.java @@ -6,7 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.time.LocalDateTime; +import java.time.ZonedDateTime; import org.junit.jupiter.api.Test; @@ -41,13 +41,13 @@ void givenRenewableUsageLimitShouldCreate() { String usageLimitName = "maxTokens"; double consumption = 300; - LocalDateTime resetTimestamp = LocalDateTime.of(2025, 8, 19, 0, 0); + 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, usageLevel.getResetTimestamp().get()), + () -> assertEquals(resetTimestamp.toLocalDateTime(), usageLevel.getResetTimestamp().get()), () -> assertTrue(usageLevel.isRenewableUsageLimit())); } From 43ba799c86ea9b31c65ae488f886cc3fa86ebbe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Thu, 21 Aug 2025 16:58:19 +0200 Subject: [PATCH 18/42] feat(contracts): raw json to Subscription --- .../github/pgmarc/space/contracts/AddOn.java | 2 +- .../pgmarc/space/contracts/BillingPeriod.java | 36 ++++---- .../pgmarc/space/contracts/Subscription.java | 79 +--------------- .../pgmarc/space/contracts/UsageLevel.java | 24 +++-- .../pgmarc/space/contracts/UserContact.java | 26 ++---- .../BillingPeriodDeserializer.java | 28 ++++++ .../space/serializers/JsonDeserializable.java | 8 ++ .../serializers/ServicesDeserializer.java | 32 +++++++ .../serializers/SnapshotsDeserializer.java | 37 ++++++++ .../serializers/SubscriptionDeserializer.java | 35 ++++++++ .../serializers/UsageLevelDeserializer.java | 41 +++++++++ .../serializers/UserContactDeserializer.java | 24 +++++ .../space/contracts/BillingPeriodTest.java | 22 ----- .../space/contracts/SubscriptionTest.java | 70 --------------- .../space/contracts/UserContactTest.java | 45 ---------- .../BillingPeriodSerializerTest.java | 43 +++++++++ .../SubscriptionSerializerTest.java | 89 +++++++++++++++++++ .../UserContactSerializerTest.java | 62 +++++++++++++ 18 files changed, 446 insertions(+), 257 deletions(-) create mode 100644 src/main/java/io/github/pgmarc/space/serializers/BillingPeriodDeserializer.java create mode 100644 src/main/java/io/github/pgmarc/space/serializers/JsonDeserializable.java create mode 100644 src/main/java/io/github/pgmarc/space/serializers/ServicesDeserializer.java create mode 100644 src/main/java/io/github/pgmarc/space/serializers/SnapshotsDeserializer.java create mode 100644 src/main/java/io/github/pgmarc/space/serializers/SubscriptionDeserializer.java create mode 100644 src/main/java/io/github/pgmarc/space/serializers/UsageLevelDeserializer.java create mode 100644 src/main/java/io/github/pgmarc/space/serializers/UserContactDeserializer.java create mode 100644 src/test/java/io/github/pgmarc/space/serializers/BillingPeriodSerializerTest.java create mode 100644 src/test/java/io/github/pgmarc/space/serializers/SubscriptionSerializerTest.java create mode 100644 src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java diff --git a/src/main/java/io/github/pgmarc/space/contracts/AddOn.java b/src/main/java/io/github/pgmarc/space/contracts/AddOn.java index 58b80fc..07a4104 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/AddOn.java +++ b/src/main/java/io/github/pgmarc/space/contracts/AddOn.java @@ -1,6 +1,6 @@ package io.github.pgmarc.space.contracts; -final class AddOn { +public final class AddOn { private final String name; private final long quantity; diff --git a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java index a265495..d368c73 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -6,17 +6,16 @@ import java.util.Objects; import java.util.Optional; -import org.json.JSONObject; - -final class BillingPeriod { +public final class BillingPeriod { private final ZonedDateTime startDate; private final ZonedDateTime endDate; private Duration renewalDays; - private BillingPeriod(ZonedDateTime startDate, ZonedDateTime endDate) { + private BillingPeriod(ZonedDateTime startDate, ZonedDateTime endDate, Duration renewalDays) { this.startDate = startDate; this.endDate = endDate; + this.renewalDays = renewalDays; } LocalDateTime getStartDate() { @@ -39,23 +38,32 @@ Optional getRenewalDate() { return Optional.ofNullable(isAutoRenewable() ? endDate.plus(renewalDays).toLocalDateTime() : null); } - void setRenewalDays(Duration renewalDays) { + 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"); } - this.renewalDays = renewalDays; } - static BillingPeriod of(ZonedDateTime startDate, ZonedDateTime endDate) { + 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"); } - return new BillingPeriod(startDate, endDate); + validateRenewalDays(renewalDays); + return new BillingPeriod(startDate, endDate, renewalDays); } - private enum Keys { + public enum Keys { START_DATE("startDate"), END_DATE("endDate"), @@ -74,16 +82,6 @@ public String toString() { } } - static BillingPeriod fromJson(JSONObject json) { - Objects.requireNonNull(json, "billing period json must not be null"); - ZonedDateTime start = ZonedDateTime.parse(json.getString(Keys.START_DATE.toString())); - ZonedDateTime end = ZonedDateTime.parse(json.getString(Keys.END_DATE.toString())); - BillingPeriod billingPeriod = BillingPeriod.of(start, end); - billingPeriod.setRenewalDays(Duration.ofDays(json.optLong(Keys.RENEWAL_DAYS.toString()))); - - return billingPeriod; - } - @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index 8c79d8a..c015f18 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -2,8 +2,6 @@ import java.time.Duration; import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -15,9 +13,6 @@ import java.util.Set; import java.util.stream.Collectors; -import org.json.JSONArray; -import org.json.JSONObject; - public final class Subscription { private final UserContact userContact; @@ -88,7 +83,7 @@ public Map> getUsageLevels() { return usageLevels; } - private enum Keys { + public enum Keys { USER_CONTACT("userContact"), BILLING_PERIOD("billingPeriod"), CONTRACTED_SERVICES("contractedServices"), @@ -109,60 +104,6 @@ public String toString() { } } - static Map servicesFromJson(JSONObject json) { - JSONObject contractedServices = json.getJSONObject(Keys.CONTRACTED_SERVICES.toString()); - JSONObject subscriptionPlans = json.getJSONObject(Keys.SUBSCRIPTION_PLANS.toString()); - JSONObject subscriptionAddOns = json.getJSONObject(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; - } - - private static Map> usageLevelsFromJson(JSONObject usageLevel) { - Objects.requireNonNull(usageLevel, "usage level must not be null"); - - Map> usageLevelMap = new HashMap<>(); - if (usageLevel.isEmpty()) { - return Collections.unmodifiableMap(usageLevelMap); - } - for (String serviceName : usageLevel.keySet()) { - Map serviceLevels = new HashMap<>(); - JSONObject rawServiceUsageLevels = usageLevel.getJSONObject(serviceName); - for (String usageLimitName : rawServiceUsageLevels.keySet()) { - JSONObject rawUsageLevel = rawServiceUsageLevels.getJSONObject(usageLimitName); - ZonedDateTime expirationDate = null; - if (rawUsageLevel.has("resetTimestamp")) { - expirationDate = ZonedDateTime.parse(rawUsageLevel.getString("resetTimestamp")); - } - UsageLevel ul = UsageLevel.of(serviceName, rawUsageLevel.getDouble("consumed"), expirationDate); - serviceLevels.put(usageLimitName, ul); - } - usageLevelMap.put(serviceName, Collections.unmodifiableMap(serviceLevels)); - } - return usageLevelMap; - } - - static Subscription fromJson(JSONObject json) { - - BillingPeriod billingPeriod = BillingPeriod.fromJson(json.getJSONObject(Keys.BILLING_PERIOD.toString())); - UserContact userContact = UserContact.fromJson(json.getJSONObject(Keys.USER_CONTACT.toString())); - Map> usageLevels = usageLevelsFromJson( - json.getJSONObject(Keys.USAGE_LEVEL.toString())); - Map services = servicesFromJson(json); - List history = Snapshot.fromJson(json.optJSONArray(Keys.HISTORY.toString())); - return Subscription.builder(userContact, billingPeriod, services.values()) - .addUsageLevels(usageLevels).addSnapshots(history).build(); - } - public static final class Builder { private final BillingPeriod billingPeriod; @@ -197,13 +138,13 @@ public Builder subscribeAll(Collection services) { return this; } - private Builder addSnapshots(Collection snaphsots) { + public Builder addSnapshots(Collection snaphsots) { Objects.requireNonNull(snaphsots, "snapshots must not be null"); this.history.addAll(snaphsots); return this; } - private Builder addUsageLevels(Map> usageLevels) { + public Builder addUsageLevels(Map> usageLevels) { this.usageLevels.putAll(usageLevels); return this; } @@ -221,7 +162,7 @@ public static final class Snapshot { private final LocalDateTime enDateTime; private final Map services; - private Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, + public Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, Map services) { this.starDateTime = startDateTime; this.enDateTime = endDateTime; @@ -292,18 +233,6 @@ public boolean equals(Object obj) { return true; } - private static List fromJson(JSONArray rawHistory) { - List history = new ArrayList<>(); - for (int i = 0; i < rawHistory.length(); i++) { - JSONObject snaphsot = rawHistory.getJSONObject(i); - OffsetDateTime startUtc = OffsetDateTime.parse(snaphsot.getString("startDate")); - OffsetDateTime end = OffsetDateTime.parse(snaphsot.getString("endDate")); - history.add(new Snapshot(startUtc.toLocalDateTime(), end.toLocalDateTime(), - Subscription.servicesFromJson(snaphsot))); - } - return history; - } - } } diff --git a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java index 00dd241..7117366 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java @@ -11,9 +11,20 @@ public final class UsageLevel { private final double consumed; private ZonedDateTime resetTimestamp; - private UsageLevel(String name, double consumed) { - this.name = name; - this.consumed = consumed; + 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) { @@ -45,12 +56,11 @@ private static void validateUsageLevel(String name, double consumed) { } } - static UsageLevel of(String name, double consumed) { - validateUsageLevel(name, consumed); - return new UsageLevel(name, consumed); + public static UsageLevel of(String name, double consumed) { + return of(name, consumed, null); } - static UsageLevel of(String name, double consumed, ZonedDateTime resetTimestamp) { + public static UsageLevel of(String name, double consumed, ZonedDateTime resetTimestamp) { validateUsageLevel(name, consumed); UsageLevel level = new UsageLevel(name, consumed, resetTimestamp); return level; diff --git a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java index 4a3669f..00738ec 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java @@ -138,7 +138,7 @@ private void validateUserNameLength() { } } - private enum JsonKeys { + public enum Keys { USER_ID("userId"), USERNAME("username"), @@ -149,7 +149,7 @@ private enum JsonKeys { private final String name; - private JsonKeys(String name) { + private Keys(String name) { this.name = name; } @@ -159,26 +159,16 @@ public String toString() { } } - static UserContact fromJson(JSONObject json) { - Objects.requireNonNull(json, "user contact json must not be null"); - return UserContact.builder(json.getString(JsonKeys.USER_ID.toString()), - json.getString(JsonKeys.USERNAME.toString())) - .firstName(json.optString(JsonKeys.FIRST_NAME.toString(), null)) - .lastName(json.optString(JsonKeys.LAST_NAME.toString(), null)) - .email(json.optString(JsonKeys.EMAIL.toString(), null)) - .phone(json.optString(JsonKeys.PHONE.toString(), null)) - .build(); - } @Override public JSONObject toJson() { return new JSONObject() - .put(JsonKeys.USER_ID.toString(), userId) - .put(JsonKeys.USERNAME.toString(), username) - .putOpt(JsonKeys.FIRST_NAME.toString(), firstName) - .putOpt(JsonKeys.LAST_NAME.toString(), lastName) - .putOpt(JsonKeys.EMAIL.toString(), email) - .putOpt(JsonKeys.PHONE.toString(), phone); + .put(Keys.USER_ID.toString(), userId) + .put(Keys.USERNAME.toString(), username) + .putOpt(Keys.FIRST_NAME.toString(), firstName) + .putOpt(Keys.LAST_NAME.toString(), lastName) + .putOpt(Keys.EMAIL.toString(), email) + .putOpt(Keys.PHONE.toString(), phone); } } diff --git a/src/main/java/io/github/pgmarc/space/serializers/BillingPeriodDeserializer.java b/src/main/java/io/github/pgmarc/space/serializers/BillingPeriodDeserializer.java new file mode 100644 index 0000000..0473894 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/BillingPeriodDeserializer.java @@ -0,0 +1,28 @@ +package io.github.pgmarc.space.serializers; + +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/serializers/JsonDeserializable.java b/src/main/java/io/github/pgmarc/space/serializers/JsonDeserializable.java new file mode 100644 index 0000000..5816ca0 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/JsonDeserializable.java @@ -0,0 +1,8 @@ +package io.github.pgmarc.space.serializers; + +import org.json.JSONObject; + +public interface JsonDeserializable { + + U fromJson(JSONObject json); +} diff --git a/src/main/java/io/github/pgmarc/space/serializers/ServicesDeserializer.java b/src/main/java/io/github/pgmarc/space/serializers/ServicesDeserializer.java new file mode 100644 index 0000000..f571e91 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/ServicesDeserializer.java @@ -0,0 +1,32 @@ +package io.github.pgmarc.space.serializers; + +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/serializers/SnapshotsDeserializer.java b/src/main/java/io/github/pgmarc/space/serializers/SnapshotsDeserializer.java new file mode 100644 index 0000000..fc7a883 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/SnapshotsDeserializer.java @@ -0,0 +1,37 @@ +package io.github.pgmarc.space.serializers; + +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 HistoryDeserializer implements JsonDeserializable> { + + private final ServicesDeserializer servicesDeserializer; + + HistoryDeserializer(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/serializers/SubscriptionDeserializer.java b/src/main/java/io/github/pgmarc/space/serializers/SubscriptionDeserializer.java new file mode 100644 index 0000000..7b2988e --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/SubscriptionDeserializer.java @@ -0,0 +1,35 @@ +package io.github.pgmarc.space.serializers; + +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 UserDeserializer userContactDeserializer = new UserDeserializer(); + private final UsageLevelDeserializer usageLevelDeserializer = new UsageLevelDeserializer(); + private final ServicesDeserializer servicesDeserializer = new ServicesDeserializer(); + private final HistoryDeserializer historyDeserializer = new HistoryDeserializer(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/serializers/UsageLevelDeserializer.java b/src/main/java/io/github/pgmarc/space/serializers/UsageLevelDeserializer.java new file mode 100644 index 0000000..e7cb4df --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/UsageLevelDeserializer.java @@ -0,0 +1,41 @@ +package io.github.pgmarc.space.serializers; + +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>> { + + @Override + public Map> fromJson(JSONObject usageLevel) { + Objects.requireNonNull(usageLevel, "usage level must not be null"); + + Map> usageLevelMap = new HashMap<>(); + if (usageLevel.isEmpty()) { + return usageLevelMap; + } + for (String serviceName : usageLevel.keySet()) { + Map serviceLevels = new HashMap<>(); + JSONObject rawServiceUsageLevels = usageLevel.getJSONObject(serviceName); + for (String usageLimitName : rawServiceUsageLevels.keySet()) { + JSONObject rawUsageLevel = rawServiceUsageLevels.getJSONObject(usageLimitName); + ZonedDateTime expirationDate = null; + if (rawUsageLevel.has(UsageLevel.Keys.RESET_TIMESTAMP.toString())) { + expirationDate = ZonedDateTime + .parse(rawUsageLevel.getString(UsageLevel.Keys.RESET_TIMESTAMP.toString())); + } + UsageLevel ul = UsageLevel.of(serviceName, rawUsageLevel.getDouble(UsageLevel.Keys.CONSUMED.toString()), + expirationDate); + serviceLevels.put(usageLimitName, ul); + } + usageLevelMap.put(serviceName, Collections.unmodifiableMap(serviceLevels)); + } + return usageLevelMap; + } +} diff --git a/src/main/java/io/github/pgmarc/space/serializers/UserContactDeserializer.java b/src/main/java/io/github/pgmarc/space/serializers/UserContactDeserializer.java new file mode 100644 index 0000000..692fbdd --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/serializers/UserContactDeserializer.java @@ -0,0 +1,24 @@ +package io.github.pgmarc.space.serializers; + +import java.util.Objects; + +import org.json.JSONObject; + +import io.github.pgmarc.space.contracts.UserContact; + +final class UserDeserializer 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/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java index 2505166..26d995a 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -44,26 +44,4 @@ void givenRenewableDateShouldBeRenowable() { () -> assertEquals(end.plusDays(30).toLocalDateTime(), billingPeriod.getRenewalDate().get())); } - @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, BillingPeriod.fromJson(input)); - } - - @Test - void giveNullJsonShouldThrow() { - - Exception ex = assertThrows(NullPointerException.class, () -> BillingPeriod.fromJson(null)); - assertEquals("billing period json must not be null", ex.getMessage()); - } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java index c3ba7a4..9dc5768 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java @@ -7,10 +7,7 @@ import java.time.Duration; import java.time.ZonedDateTime; -import java.util.Map; -import org.json.JSONArray; -import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -73,71 +70,4 @@ void givenOptionalRenewalDaysShouldNotThrow() { .build()); } - @Test - void givenSubscriptionAsJsonShouldCreateSubscription() { - - String startUtcString = "2025-04-18T00:00:00Z"; - String endUtcString = "2025-12-31T00:00:00Z"; - int renewalDays = 365; - String zoomName = "zoom"; - String zoomVersion = "2025"; - String zoomPlan = "ENTERPRISE"; - String zoomExtraSeats = "extraSeats"; - int zoomExtraSeatsQuantity = 2; - String zoomHugeMeetings = "hugeMeetings"; - int zoomHugeMeetingQuantity = 1; - - String petclinicService = "petclinic"; - String petclinicVersion = "2024"; - String petclinicPlan = "GOLD"; - String petclinicPetsAdoptionCentre = "petsAdoptionCentre"; - int petclinicPetsAdoptionCentreQuantity = 1; - - Map billingPeriodMap = Map.of( - "startDate", startUtcString, - "endDate", endUtcString, - "autoRenew", true, - "renewalDays", renewalDays); - Map> usageLevel = Map.of( - zoomName, - Map.of("maxSeats", - Map.of("consumed", 10.0)), - petclinicService, - Map.of("maxPets", - Map.of("consumed", 2.0), - "maxVisits", Map.of("consumed", 5.0, "resetTimestamp", "2025-07-31T00:00:00Z"))); - Map contractedServices = Map.of(zoomName, zoomVersion, petclinicService, petclinicVersion); - Map contractedPlans = Map.of(zoomName, zoomPlan, petclinicService, petclinicPlan); - Map> contractedAddOns = Map.of( - zoomName, - Map.of(zoomExtraSeats, zoomExtraSeatsQuantity, - zoomHugeMeetings, zoomHugeMeetingQuantity), - petclinicService, - Map.of(petclinicPetsAdoptionCentre, petclinicPetsAdoptionCentreQuantity)); - - JSONObject snapshot1 = new JSONObject() - .put("startDate", "2024-04-18T00:00:00Z") - .put("endDate", "2024-05-18T00:00:00Z") - .put("contractedServices", contractedServices) - .put("subscriptionPlans", contractedPlans) - .put("subscriptionAddOns", contractedAddOns); - JSONArray history = new JSONArray() - .put(snapshot1); - - JSONObject jsonInput = new JSONObject() - .put("userContact", TEST_USER_CONTACT.toJson()) - .put("billingPeriod", billingPeriodMap) - .put("usageLevel", usageLevel) - .put("contractedServices", contractedServices) - .put("subscriptionPlans", contractedPlans) - .put("subscriptionAddOns", contractedAddOns) - .put("history", history); - - Subscription actual = Subscription.fromJson(jsonInput); - 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/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index 4313b61..98922e4 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -111,49 +111,4 @@ void givenUserContactShouldSerializeToJson() { assertTrue(obj.similar(userContact.toJson())); } - - @Test - void givenUserContactJsonShouldParse() { - - String firtName = "Alex"; - String lastName = "Doe"; - String email = "alexdoe@example.com"; - String phone = "(+34) 666 666 666"; - UserContact expected = UserContact.builder(TEST_USER_ID, TEST_USERNAME) - .firstName(firtName) - .lastName(lastName) - .email(email) - .phone(phone) - .build(); - - JSONObject input = new JSONObject().put("userId", TEST_USER_ID) - .put("username", TEST_USERNAME) - .put("firstName", firtName) - .put("lastName", lastName) - .put("email", email) - .put("phone", phone); - assertEquals(expected, UserContact.fromJson(input)); - } - - @Test - void givenMinimunUserContactOptionalsShouldNotBeParsed() { - - JSONObject input = new JSONObject() - .put("userId", TEST_USER_ID) - .put("username", TEST_USERNAME); - - UserContact actual = UserContact.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, () -> UserContact.fromJson(null)); - assertEquals("user contact json must not be null", ex.getMessage()); - } - } diff --git a/src/test/java/io/github/pgmarc/space/serializers/BillingPeriodSerializerTest.java b/src/test/java/io/github/pgmarc/space/serializers/BillingPeriodSerializerTest.java new file mode 100644 index 0000000..9f16a4e --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/serializers/BillingPeriodSerializerTest.java @@ -0,0 +1,43 @@ +package io.github.pgmarc.space.serializers; + +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/serializers/SubscriptionSerializerTest.java b/src/test/java/io/github/pgmarc/space/serializers/SubscriptionSerializerTest.java new file mode 100644 index 0000000..a3d8b8e --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/serializers/SubscriptionSerializerTest.java @@ -0,0 +1,89 @@ +package io.github.pgmarc.space.serializers; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +import io.github.pgmarc.space.contracts.Subscription; +import io.github.pgmarc.space.contracts.UserContact; + +class SubscriptionSerializerTest { + + private final SubscriptionDeserializer serializer = new SubscriptionDeserializer(); + + @Test + void givenSubscriptionAsJsonShouldCreateSubscription() { + + String startUtcString = "2025-04-18T00:00:00Z"; + String endUtcString = "2025-12-31T00:00:00Z"; + int renewalDays = 365; + String zoomName = "zoom"; + String zoomVersion = "2025"; + String zoomPlan = "ENTERPRISE"; + String zoomExtraSeats = "extraSeats"; + int zoomExtraSeatsQuantity = 2; + String zoomHugeMeetings = "hugeMeetings"; + int zoomHugeMeetingQuantity = 1; + + String petclinicService = "petclinic"; + String petclinicVersion = "2024"; + String petclinicPlan = "GOLD"; + String petclinicPetsAdoptionCentre = "petsAdoptionCentre"; + int petclinicPetsAdoptionCentreQuantity = 1; + + Map billingPeriodMap = Map.of( + "startDate", startUtcString, + "endDate", endUtcString, + "autoRenew", true, + "renewalDays", renewalDays); + Map> usageLevel = Map.of( + zoomName, + Map.of("maxSeats", + Map.of("consumed", 10.0)), + petclinicService, + Map.of("maxPets", + Map.of("consumed", 2.0), + "maxVisits", Map.of("consumed", 5.0, "resetTimestamp", "2025-07-31T00:00:00Z"))); + Map contractedServices = Map.of(zoomName, zoomVersion, petclinicService, petclinicVersion); + Map contractedPlans = Map.of(zoomName, zoomPlan, petclinicService, petclinicPlan); + Map> contractedAddOns = Map.of( + zoomName, + Map.of(zoomExtraSeats, zoomExtraSeatsQuantity, + zoomHugeMeetings, zoomHugeMeetingQuantity), + petclinicService, + Map.of(petclinicPetsAdoptionCentre, petclinicPetsAdoptionCentreQuantity)); + + JSONObject snapshot1 = new JSONObject() + .put("startDate", "2024-04-18T00:00:00Z") + .put("endDate", "2024-05-18T00:00:00Z") + .put("contractedServices", contractedServices) + .put("subscriptionPlans", contractedPlans) + .put("subscriptionAddOns", contractedAddOns); + JSONArray history = new JSONArray() + .put(snapshot1); + + UserContact userContact = UserContact.builder("123456789", "alex").build(); + + JSONObject jsonInput = new JSONObject() + .put("userContact", userContact.toJson()) + .put("billingPeriod", billingPeriodMap) + .put("usageLevel", usageLevel) + .put("contractedServices", contractedServices) + .put("subscriptionPlans", contractedPlans) + .put("subscriptionAddOns", contractedAddOns) + .put("history", history); + + Subscription actual = serializer.fromJson(jsonInput); + 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/serializers/UserContactSerializerTest.java b/src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java new file mode 100644 index 0000000..c1355af --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java @@ -0,0 +1,62 @@ +package io.github.pgmarc.space.serializers; + +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 UserDeserializer deserializer = new UserDeserializer(); + + @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()); + } +} From 99ebd1ca50d696de28e610f6ff605e9adfc42ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Thu, 21 Aug 2025 18:18:23 +0200 Subject: [PATCH 19/42] chore: bad naming serializers folder --- .../BillingPeriodDeserializer.java | 0 .../{serializers => deserializers}/JsonDeserializable.java | 0 .../{serializers => deserializers}/ServicesDeserializer.java | 0 .../{serializers => deserializers}/SnapshotsDeserializer.java | 4 ++-- .../SubscriptionDeserializer.java | 4 ++-- .../UsageLevelDeserializer.java | 0 .../UserContactDeserializer.java | 2 +- .../io/github/pgmarc/space/contracts/BillingPeriodTest.java | 1 - .../pgmarc/space/serializers/UserContactSerializerTest.java | 2 +- 9 files changed, 6 insertions(+), 7 deletions(-) rename src/main/java/io/github/pgmarc/space/{serializers => deserializers}/BillingPeriodDeserializer.java (100%) rename src/main/java/io/github/pgmarc/space/{serializers => deserializers}/JsonDeserializable.java (100%) rename src/main/java/io/github/pgmarc/space/{serializers => deserializers}/ServicesDeserializer.java (100%) rename src/main/java/io/github/pgmarc/space/{serializers => deserializers}/SnapshotsDeserializer.java (89%) rename src/main/java/io/github/pgmarc/space/{serializers => deserializers}/SubscriptionDeserializer.java (88%) rename src/main/java/io/github/pgmarc/space/{serializers => deserializers}/UsageLevelDeserializer.java (100%) rename src/main/java/io/github/pgmarc/space/{serializers => deserializers}/UserContactDeserializer.java (91%) diff --git a/src/main/java/io/github/pgmarc/space/serializers/BillingPeriodDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java similarity index 100% rename from src/main/java/io/github/pgmarc/space/serializers/BillingPeriodDeserializer.java rename to src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java diff --git a/src/main/java/io/github/pgmarc/space/serializers/JsonDeserializable.java b/src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java similarity index 100% rename from src/main/java/io/github/pgmarc/space/serializers/JsonDeserializable.java rename to src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java diff --git a/src/main/java/io/github/pgmarc/space/serializers/ServicesDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java similarity index 100% rename from src/main/java/io/github/pgmarc/space/serializers/ServicesDeserializer.java rename to src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java diff --git a/src/main/java/io/github/pgmarc/space/serializers/SnapshotsDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java similarity index 89% rename from src/main/java/io/github/pgmarc/space/serializers/SnapshotsDeserializer.java rename to src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java index fc7a883..1ee4d94 100644 --- a/src/main/java/io/github/pgmarc/space/serializers/SnapshotsDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java @@ -11,11 +11,11 @@ import io.github.pgmarc.space.contracts.Subscription; import io.github.pgmarc.space.contracts.Subscription.Snapshot; -class HistoryDeserializer implements JsonDeserializable> { +class SnapshotsDeserializer implements JsonDeserializable> { private final ServicesDeserializer servicesDeserializer; - HistoryDeserializer(ServicesDeserializer servicesDeserializer) { + SnapshotsDeserializer(ServicesDeserializer servicesDeserializer) { this.servicesDeserializer = servicesDeserializer; } diff --git a/src/main/java/io/github/pgmarc/space/serializers/SubscriptionDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java similarity index 88% rename from src/main/java/io/github/pgmarc/space/serializers/SubscriptionDeserializer.java rename to src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java index 7b2988e..3b5aae4 100644 --- a/src/main/java/io/github/pgmarc/space/serializers/SubscriptionDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java @@ -15,10 +15,10 @@ public final class SubscriptionDeserializer implements JsonDeserializable { private final BillingPeriodDeserializer billingSerializer = new BillingPeriodDeserializer(); - private final UserDeserializer userContactDeserializer = new UserDeserializer(); + private final UserContactDeserializer userContactDeserializer = new UserContactDeserializer(); private final UsageLevelDeserializer usageLevelDeserializer = new UsageLevelDeserializer(); private final ServicesDeserializer servicesDeserializer = new ServicesDeserializer(); - private final HistoryDeserializer historyDeserializer = new HistoryDeserializer(servicesDeserializer); + private final SnapshotsDeserializer historyDeserializer = new SnapshotsDeserializer(servicesDeserializer); @Override public Subscription fromJson(JSONObject json) { diff --git a/src/main/java/io/github/pgmarc/space/serializers/UsageLevelDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java similarity index 100% rename from src/main/java/io/github/pgmarc/space/serializers/UsageLevelDeserializer.java rename to src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java diff --git a/src/main/java/io/github/pgmarc/space/serializers/UserContactDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java similarity index 91% rename from src/main/java/io/github/pgmarc/space/serializers/UserContactDeserializer.java rename to src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java index 692fbdd..90e9cc4 100644 --- a/src/main/java/io/github/pgmarc/space/serializers/UserContactDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java @@ -6,7 +6,7 @@ import io.github.pgmarc.space.contracts.UserContact; -final class UserDeserializer implements JsonDeserializable { +final class UserContactDeserializer implements JsonDeserializable { @Override public UserContact fromJson(JSONObject json) { diff --git a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java index 26d995a..c91731e 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -8,7 +8,6 @@ import java.time.Duration; import java.time.ZonedDateTime; -import org.json.JSONObject; import org.junit.jupiter.api.Test; class BillingPeriodTest { diff --git a/src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java b/src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java index c1355af..7c983a9 100644 --- a/src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java +++ b/src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java @@ -12,7 +12,7 @@ class UserContactSerializerTest { - private final UserDeserializer deserializer = new UserDeserializer(); + private final UserContactDeserializer deserializer = new UserContactDeserializer(); @Test void givenUserContactJsonShouldParse() { From f4f7faed1a44318b67ae0751705494fd325e6fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Thu, 21 Aug 2025 18:39:00 +0200 Subject: [PATCH 20/42] fix(contracts): deserializers package not renamed --- .../pgmarc/space/deserializers/BillingPeriodDeserializer.java | 2 +- .../github/pgmarc/space/deserializers/JsonDeserializable.java | 2 +- .../github/pgmarc/space/deserializers/ServicesDeserializer.java | 2 +- .../pgmarc/space/deserializers/SnapshotsDeserializer.java | 2 +- .../pgmarc/space/deserializers/SubscriptionDeserializer.java | 2 +- .../pgmarc/space/deserializers/UsageLevelDeserializer.java | 2 +- .../pgmarc/space/deserializers/UserContactDeserializer.java | 2 +- .../BillingPeriodSerializerTest.java | 2 +- .../SubscriptionSerializerTest.java | 2 +- .../UserContactSerializerTest.java | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) rename src/test/java/io/github/pgmarc/space/{serializers => deserializers}/BillingPeriodSerializerTest.java (96%) rename src/test/java/io/github/pgmarc/space/{serializers => deserializers}/SubscriptionSerializerTest.java (98%) rename src/test/java/io/github/pgmarc/space/{serializers => deserializers}/UserContactSerializerTest.java (97%) diff --git a/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java index 0473894..457c4cd 100644 --- a/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/BillingPeriodDeserializer.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import java.time.Duration; import java.time.ZonedDateTime; diff --git a/src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java b/src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java index 5816ca0..529909b 100644 --- a/src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/JsonDeserializable.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import org.json.JSONObject; diff --git a/src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java index f571e91..a364a8f 100644 --- a/src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/ServicesDeserializer.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java index 1ee4d94..f9baa8e 100644 --- a/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/SnapshotsDeserializer.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import java.time.OffsetDateTime; import java.util.ArrayList; diff --git a/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java index 3b5aae4..6ae231b 100644 --- a/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/SubscriptionDeserializer.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import java.util.List; import java.util.Map; diff --git a/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java index e7cb4df..f8671c4 100644 --- a/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import java.time.ZonedDateTime; import java.util.Collections; diff --git a/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java index 90e9cc4..c51cd5b 100644 --- a/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/UserContactDeserializer.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import java.util.Objects; diff --git a/src/test/java/io/github/pgmarc/space/serializers/BillingPeriodSerializerTest.java b/src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java similarity index 96% rename from src/test/java/io/github/pgmarc/space/serializers/BillingPeriodSerializerTest.java rename to src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java index 9f16a4e..0211f85 100644 --- a/src/test/java/io/github/pgmarc/space/serializers/BillingPeriodSerializerTest.java +++ b/src/test/java/io/github/pgmarc/space/deserializers/BillingPeriodSerializerTest.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/io/github/pgmarc/space/serializers/SubscriptionSerializerTest.java b/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java similarity index 98% rename from src/test/java/io/github/pgmarc/space/serializers/SubscriptionSerializerTest.java rename to src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java index a3d8b8e..60a32d1 100644 --- a/src/test/java/io/github/pgmarc/space/serializers/SubscriptionSerializerTest.java +++ b/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java b/src/test/java/io/github/pgmarc/space/deserializers/UserContactSerializerTest.java similarity index 97% rename from src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java rename to src/test/java/io/github/pgmarc/space/deserializers/UserContactSerializerTest.java index 7c983a9..0a6a75c 100644 --- a/src/test/java/io/github/pgmarc/space/serializers/UserContactSerializerTest.java +++ b/src/test/java/io/github/pgmarc/space/deserializers/UserContactSerializerTest.java @@ -1,4 +1,4 @@ -package io.github.pgmarc.space.serializers; +package io.github.pgmarc.space.deserializers; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; From 4fa4dcb4a90f28e6097b04464507db643c5fae95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Fri, 22 Aug 2025 11:41:56 +0200 Subject: [PATCH 21/42] feat: replace Jsonable interface --- .../java/io/github/pgmarc/space/Jsonable.java | 9 --- .../pgmarc/space/contracts/UserContact.java | 19 +---- .../deserializers/UsageLevelDeserializer.java | 40 +++++----- .../space/contracts/UserContactTest.java | 45 ----------- .../SubscriptionSerializerTest.java | 74 +++---------------- src/test/resources/subscription-response.json | 73 ++++++++++++++++++ 6 files changed, 106 insertions(+), 154 deletions(-) delete mode 100644 src/main/java/io/github/pgmarc/space/Jsonable.java create mode 100644 src/test/resources/subscription-response.json diff --git a/src/main/java/io/github/pgmarc/space/Jsonable.java b/src/main/java/io/github/pgmarc/space/Jsonable.java deleted file mode 100644 index a2defff..0000000 --- a/src/main/java/io/github/pgmarc/space/Jsonable.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.pgmarc.space; - -import org.json.JSONObject; - -public interface Jsonable { - - JSONObject toJson(); - -} diff --git a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java index 00738ec..95f17b3 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UserContact.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UserContact.java @@ -3,11 +3,7 @@ import java.util.Objects; import java.util.Optional; -import org.json.JSONObject; - -import io.github.pgmarc.space.Jsonable; - -public final class UserContact implements Jsonable { +public final class UserContact { private final String userId; private final String username; @@ -158,17 +154,4 @@ public String toString() { return name; } } - - - @Override - public JSONObject toJson() { - return new JSONObject() - .put(Keys.USER_ID.toString(), userId) - .put(Keys.USERNAME.toString(), username) - .putOpt(Keys.FIRST_NAME.toString(), firstName) - .putOpt(Keys.LAST_NAME.toString(), lastName) - .putOpt(Keys.EMAIL.toString(), email) - .putOpt(Keys.PHONE.toString(), phone); - } - } diff --git a/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java b/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java index f8671c4..c993a34 100644 --- a/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java +++ b/src/main/java/io/github/pgmarc/space/deserializers/UsageLevelDeserializer.java @@ -12,30 +12,30 @@ 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> usageLevelMap = new HashMap<>(); - if (usageLevel.isEmpty()) { - return usageLevelMap; - } + Map> res = new HashMap<>(); for (String serviceName : usageLevel.keySet()) { - Map serviceLevels = new HashMap<>(); - JSONObject rawServiceUsageLevels = usageLevel.getJSONObject(serviceName); - for (String usageLimitName : rawServiceUsageLevels.keySet()) { - JSONObject rawUsageLevel = rawServiceUsageLevels.getJSONObject(usageLimitName); - ZonedDateTime expirationDate = null; - if (rawUsageLevel.has(UsageLevel.Keys.RESET_TIMESTAMP.toString())) { - expirationDate = ZonedDateTime - .parse(rawUsageLevel.getString(UsageLevel.Keys.RESET_TIMESTAMP.toString())); - } - UsageLevel ul = UsageLevel.of(serviceName, rawUsageLevel.getDouble(UsageLevel.Keys.CONSUMED.toString()), - expirationDate); - serviceLevels.put(usageLimitName, ul); - } - usageLevelMap.put(serviceName, Collections.unmodifiableMap(serviceLevels)); + res.put(serviceName, getServiceUsageLevels(usageLevel.getJSONObject(serviceName))); } - return usageLevelMap; + return Collections.unmodifiableMap(res); } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index 98922e4..6dfa487 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -1,14 +1,10 @@ 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.util.Optional; -import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -70,45 +66,4 @@ void givenOptionalParametersExpecttoBeDefined(String firstName, String lastName, assertEquals(Optional.ofNullable(email), contact.getEmail()); assertEquals(Optional.ofNullable(phone), contact.getPhone()); } - - @Test - void givenRequiredParametersShouldSerializeMinimunJson() { - - UserContact userContact = UserContact.builder(TEST_USER_ID, TEST_USERNAME) - .build(); - - JSONObject userContactJson = userContact.toJson(); - - assertAll( - () -> assertFalse(userContactJson.has("firstName")), - () -> assertFalse(userContactJson.has("lastName")), - () -> assertFalse(userContactJson.has("email")), - () -> assertFalse(userContactJson.has("phone"))); - } - - @Test - void givenUserContactShouldSerializeToJson() { - - String firstName = "Alex"; - String lastName = "Doe"; - String email = "alex@example.com"; - String phone = "+(34) 123 456 789"; - - UserContact userContact = UserContact.builder(TEST_USER_ID, TEST_USERNAME) - .firstName(firstName) - .lastName(lastName) - .email(email) - .phone(phone) - .build(); - - JSONObject obj = new JSONObject() - .put("userId", TEST_USER_ID) - .put("username", TEST_USERNAME) - .put("firstName", firstName) - .put("lastName", lastName) - .put("email", email) - .put("phone", phone); - - assertTrue(obj.similar(userContact.toJson())); - } } diff --git a/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java b/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java index 60a32d1..abaf866 100644 --- a/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java +++ b/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java @@ -2,15 +2,16 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; -import java.util.Map; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; -import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; import io.github.pgmarc.space.contracts.Subscription; -import io.github.pgmarc.space.contracts.UserContact; class SubscriptionSerializerTest { @@ -19,66 +20,15 @@ class SubscriptionSerializerTest { @Test void givenSubscriptionAsJsonShouldCreateSubscription() { - String startUtcString = "2025-04-18T00:00:00Z"; - String endUtcString = "2025-12-31T00:00:00Z"; - int renewalDays = 365; - String zoomName = "zoom"; - String zoomVersion = "2025"; - String zoomPlan = "ENTERPRISE"; - String zoomExtraSeats = "extraSeats"; - int zoomExtraSeatsQuantity = 2; - String zoomHugeMeetings = "hugeMeetings"; - int zoomHugeMeetingQuantity = 1; + String content = null; + try { + byte[] raw = Files.readAllBytes(Paths.get("src", "test", "resources", "subscription-response.json")); + content = new String(raw); + } catch (IOException e) { + fail(e.getCause()); + } - String petclinicService = "petclinic"; - String petclinicVersion = "2024"; - String petclinicPlan = "GOLD"; - String petclinicPetsAdoptionCentre = "petsAdoptionCentre"; - int petclinicPetsAdoptionCentreQuantity = 1; - - Map billingPeriodMap = Map.of( - "startDate", startUtcString, - "endDate", endUtcString, - "autoRenew", true, - "renewalDays", renewalDays); - Map> usageLevel = Map.of( - zoomName, - Map.of("maxSeats", - Map.of("consumed", 10.0)), - petclinicService, - Map.of("maxPets", - Map.of("consumed", 2.0), - "maxVisits", Map.of("consumed", 5.0, "resetTimestamp", "2025-07-31T00:00:00Z"))); - Map contractedServices = Map.of(zoomName, zoomVersion, petclinicService, petclinicVersion); - Map contractedPlans = Map.of(zoomName, zoomPlan, petclinicService, petclinicPlan); - Map> contractedAddOns = Map.of( - zoomName, - Map.of(zoomExtraSeats, zoomExtraSeatsQuantity, - zoomHugeMeetings, zoomHugeMeetingQuantity), - petclinicService, - Map.of(petclinicPetsAdoptionCentre, petclinicPetsAdoptionCentreQuantity)); - - JSONObject snapshot1 = new JSONObject() - .put("startDate", "2024-04-18T00:00:00Z") - .put("endDate", "2024-05-18T00:00:00Z") - .put("contractedServices", contractedServices) - .put("subscriptionPlans", contractedPlans) - .put("subscriptionAddOns", contractedAddOns); - JSONArray history = new JSONArray() - .put(snapshot1); - - UserContact userContact = UserContact.builder("123456789", "alex").build(); - - JSONObject jsonInput = new JSONObject() - .put("userContact", userContact.toJson()) - .put("billingPeriod", billingPeriodMap) - .put("usageLevel", usageLevel) - .put("contractedServices", contractedServices) - .put("subscriptionPlans", contractedPlans) - .put("subscriptionAddOns", contractedAddOns) - .put("history", history); - - Subscription actual = serializer.fromJson(jsonInput); + Subscription actual = serializer.fromJson(new JSONObject(content)); assertAll( () -> assertEquals(1, actual.getHistory().size()), () -> assertEquals(2, actual.getUsageLevels().size()), diff --git a/src/test/resources/subscription-response.json b/src/test/resources/subscription-response.json new file mode 100644 index 0000000..c49ef71 --- /dev/null +++ b/src/test/resources/subscription-response.json @@ -0,0 +1,73 @@ +{ + "id": "68050bd09890322c57842f6f", + "userContact": { + "userId": "01c36d29-0d6a-4b41-83e9-8c6d9310c508", + "username": "johndoe", + "fistName": "John", + "lastName": "Doe", + "email": "john.doe@my-domain.com", + "phone": "+34 666 666 666" + }, + "billingPeriod": { + "startDate": "2025-12-31T00:00:00Z", + "endDate": "2025-12-31T00:00:00Z", + "autoRenew": true, + "renewalDays": 365 + }, + "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": [ + { + "startDate": "2025-12-31T00:00:00Z", + "endDate": "2025-12-31T00:00:00Z", + "contractedServices": { + "zoom": "2025", + "petclinic": "2024" + }, + "subscriptionPlans": { + "zoom": "ENTERPRISE", + "petclinic": "GOLD" + }, + "subscriptionAddOns": { + "zoom": { + "extraSeats": 2, + "hugeMeetings": 1 + }, + "petclinic": { + "petsAdoptionCentre": 1 + } + } + } + ] +} From 06719e2b7ff9d33999147daca60b758a79b73781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Fri, 22 Aug 2025 21:52:18 +0200 Subject: [PATCH 22/42] chore: json identation --- .editorconfig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From 420617f8c689e9ae50a152a217ab410547b2f322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Fri, 22 Aug 2025 21:57:17 +0200 Subject: [PATCH 23/42] feat: migrate MockServer to WireMock --- pom.xml | 18 +++-- .../SubscriptionSerializerTest.java | 65 +++++++++++++++---- .../addContracts-response.json} | 0 .../__files/subscription-request.json | 31 +++++++++ 4 files changed, 96 insertions(+), 18 deletions(-) rename src/test/resources/{subscription-response.json => __files/addContracts-response.json} (100%) create mode 100644 src/test/resources/__files/subscription-request.json diff --git a/pom.xml b/pom.xml index fb34988..6854116 100644 --- a/pom.xml +++ b/pom.xml @@ -137,12 +137,18 @@ 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 + diff --git a/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java b/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java index abaf866..4601d9e 100644 --- a/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java +++ b/src/test/java/io/github/pgmarc/space/deserializers/SubscriptionSerializerTest.java @@ -2,11 +2,9 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; +import java.util.List; +import java.util.Map; import org.json.JSONObject; import org.junit.jupiter.api.Test; @@ -20,15 +18,58 @@ class SubscriptionSerializerTest { @Test void givenSubscriptionAsJsonShouldCreateSubscription() { - String content = null; - try { - byte[] raw = Files.readAllBytes(Paths.get("src", "test", "resources", "subscription-response.json")); - content = new String(raw); - } catch (IOException e) { - fail(e.getCause()); - } + 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(new JSONObject(content)); + Subscription actual = serializer.fromJson(input); assertAll( () -> assertEquals(1, actual.getHistory().size()), () -> assertEquals(2, actual.getUsageLevels().size()), diff --git a/src/test/resources/subscription-response.json b/src/test/resources/__files/addContracts-response.json similarity index 100% rename from src/test/resources/subscription-response.json rename to src/test/resources/__files/addContracts-response.json diff --git a/src/test/resources/__files/subscription-request.json b/src/test/resources/__files/subscription-request.json new file mode 100644 index 0000000..c4db859 --- /dev/null +++ b/src/test/resources/__files/subscription-request.json @@ -0,0 +1,31 @@ +{ + "userContact": { + "userId": "01c36d29-0d6a-4b41-83e9-8c6d9310c508", + "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 + } + } +} From 2d6e9f2ab49ddef4db6e7ee4333b1a88a8b4219a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Fri, 22 Aug 2025 21:58:33 +0200 Subject: [PATCH 24/42] fix: deleted MockServer example --- .../github/pgmarc/space/MockServerTest.java | 86 ------------------- 1 file changed, 86 deletions(-) delete mode 100644 src/test/java/io/github/pgmarc/space/MockServerTest.java diff --git a/src/test/java/io/github/pgmarc/space/MockServerTest.java b/src/test/java/io/github/pgmarc/space/MockServerTest.java deleted file mode 100644 index 21183b9..0000000 --- a/src/test/java/io/github/pgmarc/space/MockServerTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.github.pgmarc.space; - -import org.json.JSONObject; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockserver.integration.ClientAndServer; -import org.mockserver.junit.jupiter.MockServerExtension; -import org.mockserver.model.MediaType; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; -import static org.mockserver.model.OpenAPIDefinition.openAPI; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpClient.Version; -import java.net.http.HttpRequest.BodyPublishers; -import java.net.http.HttpResponse.BodyHandlers; -import java.util.Map; - -@ExtendWith(MockServerExtension.class) -class MockServerTest { - - private static final String spaceOas = "https://raw.githubusercontent.com/Alex-GF/space/refs/heads/main/api/docs/space-api-docs.yaml"; - - private final ClientAndServer client; - - private final HttpClient httpClient = HttpClient.newBuilder().version(Version.HTTP_1_1).build(); - - public MockServerTest(ClientAndServer client) { - this.client = client; - } - - // TODO: Change field userId is defined as ObjectId which is false - @Test - void testOAS() { - client.when(openAPI(spaceOas, "addContracts")) - .respond(response().withBody("{'ping':'pong'}", MediaType.APPLICATION_JSON)); - - JSONObject object = new JSONObject() - .put("userContact", Map.of("username", "pgmarc", "userId", "68050bd09890322c57842f6f")) - .put("billingPeriod", Map.of("autoRenew", true, "renewalDays", 365)) - .put("contractedServices", Map.of("zoom", "2025", "petclinic", "2024")) - .put("subscriptionPlans", Map.of("zoom", "ENTERPRISE", "petclinic", "GOLD")) - .put("subscriptionAddOns", Map.of()); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + client.getPort().toString() + "/contracts")) - .header("Content-Type", MediaType.APPLICATION_JSON.toString()) - .header("x-api-key", "prueba") - .POST(BodyPublishers.ofString(object.toString())).build(); - try { - HttpResponse response = httpClient.send(request, BodyHandlers.ofString()); - assertEquals(200, response.statusCode()); - JSONObject json = new JSONObject(response.body()); - assertEquals("pong", json.getString("ping")); - } catch (IOException | InterruptedException e) { - fail(); - } - } - - @Test - void testMockServer() { - client.when(request().withMethod("GET").withPath("/test")) - .respond(response().withBody("{'test':'foo'}", MediaType.APPLICATION_JSON)); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + client.getPort().toString() + "/test")) - .GET().build(); - - try { - HttpResponse response = httpClient.send(request, BodyHandlers.ofString()); - JSONObject json = new JSONObject(response.body()); - assertEquals("foo", json.getString("test")); - } catch (IOException | InterruptedException e) { - fail(); - } - - } - -} From d181b60af671ba3fc8dbb3e996bb8d6f55edfb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Fri, 22 Aug 2025 22:04:05 +0200 Subject: [PATCH 25/42] feat: add Contracts endpoints domain models --- .../space/contracts/SubscriptionRequest.java | 111 ++++++++++++++++++ .../contracts/SubscriptionUpdateRequest.java | 48 ++++++++ .../contracts/SubscriptionRequestTest.java | 107 +++++++++++++++++ .../space/contracts/SubscriptionTest.java | 73 ------------ 4 files changed, 266 insertions(+), 73 deletions(-) create mode 100644 src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java create mode 100644 src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java create mode 100644 src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java delete mode 100644 src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java 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..ea1eb7c --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java @@ -0,0 +1,111 @@ +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; + } + + private boolean isServiceBuilderAlive() { + return serviceBuilder != null; + } + + private void validateServiceBuilderCalled(String message) { + if (!isServiceBuilderAlive()) { + throw new IllegalStateException(message); + } + } + + public Builder service(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; + } + + private void destroyServiceBuilder() { + this.serviceBuilder = null; + } + + public Builder buildService() { + validateServiceBuilderCalled("you must call 'newService' before adding a service"); + services.add(serviceBuilder.build()); + destroyServiceBuilder(); + return this; + } + + 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 SubscriptionRequest build() { + Objects.requireNonNull(userContact, "userContact must not be null"); + return new SubscriptionRequest(this); + } + } +} 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..4528a48 --- /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 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, 0); + 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/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..5e39b15 --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java @@ -0,0 +1,107 @@ +package io.github.pgmarc.space.contracts; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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 java.util.Set; + +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(); + + assertEquals(Set.of(service1, service2), sub.getServices()); + } + + @Test + void givenConsecutiveServiceCreationShouldThrow() { + + Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest + .builder(TEST_USER_CONTACT) + .service("test", "v1") + .service("incorrect", "v1") + .build()); + assertEquals("you must build a service before creating another", ex.getMessage()); + } + + @Test + void givenPlanCallBeforeCallingCreationServiceShouldThrow() { + + Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest + .builder(TEST_USER_CONTACT) + .plan("foo") + .build()); + assertEquals("you must call 'newService' before setting a plan: foo", ex.getMessage()); + } + + @Test + void givenAddOnCallBeforeCallingCreationServiceShouldThrow() { + + Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest + .builder(TEST_USER_CONTACT) + .addOn("foo", 1) + .build()); + assertEquals("you must call 'newService' before setting an add-on: foo", ex.getMessage()); + } + + @Test + void givenServiceBuildCallBeforeCreationServiceShouldThrow() { + + Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest + .builder(TEST_USER_CONTACT) + .buildService() + .build()); + assertEquals("you must call 'newService' before adding a service", ex.getMessage()); + } + + @Test + void whenNoRequiredParametersInputShouldThrow() { + + Exception ex = assertThrows(NullPointerException.class, + () -> SubscriptionRequest.builder(null) + .build()); + assertEquals("userContact must not be null", ex.getMessage()); + } + + @Test + void givenOptionalRenewalDaysShouldNotThrow() { + + assertDoesNotThrow(() -> SubscriptionRequest.builder(TEST_USER_CONTACT) + .renewIn(null) + .build()); + } + +} diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java deleted file mode 100644 index 9dc5768..0000000 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.github.pgmarc.space.contracts; - -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -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.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class SubscriptionTest { - - private static final UserContact TEST_USER_CONTACT = UserContact.builder("123456789", "alexdoe") - .build(); - private static final Service TEST_SERVICE = Service.builder("test", "alfa").plan("Foo").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(); - - Subscription sub = Subscription - .builder(TEST_USER_CONTACT, billingPeriod, service1) - .subscribe(service2) - .renewIn(Duration.ofDays(renewalDays)) - .build(); - - assertAll(() -> assertEquals(2, sub.getServices().size()), - () -> assertEquals(service1, sub.getService(service1Name).get()), - () -> assertEquals(service2, sub.getService(service2Name).get())); - } - - @Test - void whenNoRequiredParametersInputShouldThrow() { - - Exception ex = assertThrows(NullPointerException.class, - () -> Subscription.builder(null, billingPeriod, TEST_SERVICE) - .build()); - assertEquals("userContact must not be null", ex.getMessage()); - - ex = assertThrows(NullPointerException.class, - () -> Subscription.builder(TEST_USER_CONTACT, null, TEST_SERVICE) - .build()); - assertEquals("billingPeriod must not be null", ex.getMessage()); - } - - @Test - void givenOptionalRenewalDaysShouldNotThrow() { - - assertDoesNotThrow(() -> Subscription.builder(TEST_USER_CONTACT, billingPeriod, TEST_SERVICE) - .renewIn(null) - .build()); - } - -} From 4af5355bc2df4e6ba0db81f901cb8f95dc3a1267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Fri, 22 Aug 2025 22:06:43 +0200 Subject: [PATCH 26/42] feat(contracts): add serializers to domain models --- .../space/serializers/JsonSerializable.java | 7 ++ .../SubscriptionRequestSerializer.java | 84 +++++++++++++++++++ .../SubscriptionUpdateRequestSerializer.java | 22 +++++ .../SubscriptionRequestSerializerTest.java | 63 ++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 src/main/java/io/github/pgmarc/space/serializers/JsonSerializable.java create mode 100644 src/main/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializer.java create mode 100644 src/main/java/io/github/pgmarc/space/serializers/SubscriptionUpdateRequestSerializer.java create mode 100644 src/test/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializerTest.java 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/serializers/SubscriptionRequestSerializerTest.java b/src/test/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializerTest.java new file mode 100644 index 0000000..0da5e4b --- /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)) + .service(zoom, "2025") + .plan("ENTERPRISE") + .addOn("extraSeats", 2) + .addOn("hugeMeetings", 1) + .buildService() + .service("petclinic", "2024") + .plan("GOLD") + .addOn("petsAdoptionCentre", 1) + .buildService() + .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)); + + } + +} From 52b591b7247ab42c3cc7112efb94be8423ef4d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 19:07:55 +0200 Subject: [PATCH 27/42] feat(contracts): contracts endpoints implemented --- .../pgmarc/space/contracts/BillingPeriod.java | 4 + .../space/contracts/ContractsEndpoint.java | 111 +++++++++++ .../pgmarc/space/contracts/Subscription.java | 8 + .../deserializers/ErrorDeserializer.java | 55 ++++++ .../space/exceptions/SpaceApiError.java | 26 +++ .../space/exceptions/SpaceApiException.java | 16 ++ .../contracts/ContractsEndpointTest.java | 180 ++++++++++++++++++ .../__files/addContracts-response.hbs | 30 +++ ...nse.json => getContractById-response.json} | 36 +--- .../__files/subscription-request.json | 2 +- 10 files changed, 436 insertions(+), 32 deletions(-) create mode 100644 src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java create mode 100644 src/main/java/io/github/pgmarc/space/deserializers/ErrorDeserializer.java create mode 100644 src/main/java/io/github/pgmarc/space/exceptions/SpaceApiError.java create mode 100644 src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java create mode 100644 src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java create mode 100644 src/test/resources/__files/addContracts-response.hbs rename src/test/resources/__files/{addContracts-response.json => getContractById-response.json} (50%) diff --git a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java index d368c73..865d46c 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -26,6 +26,10 @@ LocalDateTime getEndDate() { return endDate.toLocalDateTime(); } + Duration getDuration() { + return renewalDays; + } + boolean isExpired(LocalDateTime dateTime) { return endDate.isAfter(startDate); } 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..15e3f04 --- /dev/null +++ b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java @@ -0,0 +1,111 @@ +package io.github.pgmarc.space.contracts; + +import java.io.IOException; +import java.util.Objects; + +import org.json.JSONArray; +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 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) { + 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("statusCode", response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + + res = subscriptionDeserializer.fromJson(jsonResponse); + } catch (IOException e) { + e.printStackTrace(); + } + + return res; + } + + public Subscription getContractByUserId(String userId) { + + 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("statusCode", response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + res = subscriptionDeserializer.fromJson(jsonResponse); + } catch (IOException e) { + e.printStackTrace(); + } + + return res; + } + + public Subscription updateContractByUserId(String userId, SubscriptionUpdateRequest subscription) { + 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("statusCode", response.code()); + throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); + } + res = subscriptionDeserializer.fromJson(jsonResponse); + } catch (IOException e) { + e.printStackTrace(); + } + + return res; + } + +} diff --git a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index c015f18..bcfa30f 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -47,10 +47,18 @@ 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(); } 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/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..bac1d24 --- /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 SpaceApiError error; + + public SpaceApiException(SpaceApiError error) { + super(error.toString()); + this.error = error; + } + + public int getCode() { + return error.getCode(); + } +} 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..65f5d9e --- /dev/null +++ b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java @@ -0,0 +1,180 @@ +package io.github.pgmarc.space.contracts; + +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)) + .service("zoom", "2025") + .plan("ENTERPRISE") + .addOn("extraSeats", 2) + .addOn("hugeMeetings", 1) + .buildService() + .service("petclinic", "2024") + .plan("GOLD") + .addOn("petsAdoptionCentre", 1) + .buildService() + .build(); + + assertThatNoException().isThrownBy(() -> endpoint.addContract(subReq)); + Subscription subscription = endpoint.addContract(subReq); + assertThat(subscription.getServices()).isEqualTo(subReq.getServices()); + assertThat(subscription.getUserId()).isEqualTo(userId); + assertThat(subscription.getRenewalDuration().get()).isEqualTo(Duration.ofDays(45)); + assertThat(subscription.getHistory()).isEmpty(); + } + + @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) + .service("err", "v1") + .plan("Error") + .buildService() + .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 = endpoint.getContractByUserId(userId); + assertThat(subscription.getUserId()).isEqualTo(userId); + + } + + @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 = endpoint.updateContractByUserId(userId, subscription); + + assertThat(sub.getUserId()).isEqualTo(userId); + + } + +} 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/addContracts-response.json b/src/test/resources/__files/getContractById-response.json similarity index 50% rename from src/test/resources/__files/addContracts-response.json rename to src/test/resources/__files/getContractById-response.json index c49ef71..6c950bd 100644 --- a/src/test/resources/__files/addContracts-response.json +++ b/src/test/resources/__files/getContractById-response.json @@ -1,18 +1,14 @@ { "id": "68050bd09890322c57842f6f", "userContact": { - "userId": "01c36d29-0d6a-4b41-83e9-8c6d9310c508", - "username": "johndoe", - "fistName": "John", - "lastName": "Doe", - "email": "john.doe@my-domain.com", - "phone": "+34 666 666 666" + "userId": "{{request.path.userId}}", + "username": "alex" }, "billingPeriod": { "startDate": "2025-12-31T00:00:00Z", "endDate": "2025-12-31T00:00:00Z", - "autoRenew": true, - "renewalDays": 365 + "autoRenew": false, + "renewalDays": 30 }, "usageLevel": { "zoom": { @@ -47,27 +43,5 @@ "petsAdoptionCentre": 1 } }, - "history": [ - { - "startDate": "2025-12-31T00:00:00Z", - "endDate": "2025-12-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 index c4db859..8bc27c8 100644 --- a/src/test/resources/__files/subscription-request.json +++ b/src/test/resources/__files/subscription-request.json @@ -1,6 +1,6 @@ { "userContact": { - "userId": "01c36d29-0d6a-4b41-83e9-8c6d9310c508", + "userId": "{{jsonPath request.body '$.userContact.userId'}}", "username": "johndoe", "fistName": "John", "lastName": "Doe", From 6f3a7b17e0da0cefff24a0f979e94531d6c95cba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 19:46:03 +0200 Subject: [PATCH 28/42] ci(sonar): disable CI analysis --- .github/workflows/code-analysis.yaml | 39 -------- pom.xml | 91 ------------------- .../pgmarc/space/contracts/Subscription.java | 4 +- .../contracts/SubscriptionUpdateRequest.java | 2 +- 4 files changed, 3 insertions(+), 133 deletions(-) delete mode 100644 .github/workflows/code-analysis.yaml diff --git a/.github/workflows/code-analysis.yaml b/.github/workflows/code-analysis.yaml deleted file mode 100644 index e0ce3aa..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: - sonarcloud: - 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/pom.xml b/pom.xml index 6854116..f0809ae 100644 --- a/pom.xml +++ b/pom.xml @@ -154,40 +154,11 @@ - - 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 - @@ -252,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/contracts/Subscription.java b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java index bcfa30f..9c1f552 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Subscription.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Subscription.java @@ -174,7 +174,7 @@ public Snapshot(LocalDateTime startDateTime, LocalDateTime endDateTime, Map services) { this.starDateTime = startDateTime; this.enDateTime = endDateTime; - this.services = Collections.unmodifiableMap(services); + this.services = new HashMap<>(services); } private Snapshot(Subscription subscription) { @@ -192,7 +192,7 @@ public LocalDateTime getEndDate() { } public Map getServices() { - return services; + return Collections.unmodifiableMap(services); } public Optional getService(String name) { diff --git a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java index 4528a48..5134ca3 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java +++ b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java @@ -14,7 +14,7 @@ private SubscriptionUpdateRequest() { } public Set getServices() { - return services; + return Set.copyOf(services); } public static SubscriptionUpdateRequest builder() { From 5bc32f907ead0ea9ce68b633ce9f2394c5849e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 19:54:05 +0200 Subject: [PATCH 29/42] ci(sonar): upload JaCoCo to Sonarcloud --- .github/workflows/test.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2918079..d64b6b5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,3 +13,8 @@ jobs: distribution: 'temurin' - name: Run tests run: mvn test + - name: Upload code coverage to Sonarcloud + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn org.sonarsource.scanner.maven:sonar-maven-plugin:5.1.0.4751:sonar + -Dsonar.projectKey=pgmarc_space-java-client -Pcoverage From 47557198f309b904120c1ad33f5d17a073b777e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 19:58:05 +0200 Subject: [PATCH 30/42] ci(sonar): enabled automatic analysis --- .github/workflows/test.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d64b6b5..2918079 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,8 +13,3 @@ jobs: distribution: 'temurin' - name: Run tests run: mvn test - - name: Upload code coverage to Sonarcloud - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn org.sonarsource.scanner.maven:sonar-maven-plugin:5.1.0.4751:sonar - -Dsonar.projectKey=pgmarc_space-java-client -Pcoverage From 2f3debe0e4e7916d3d25a9a5545c3bec593cab33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 20:36:17 +0200 Subject: [PATCH 31/42] feat(contracts): do not catch IOException --- .../space/contracts/ContractsEndpoint.java | 20 +++------- .../contracts/ContractsEndpointTest.java | 37 +++++++++++++------ 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java index 15e3f04..f513778 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java +++ b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.util.Objects; -import org.json.JSONArray; import org.json.JSONObject; import io.github.pgmarc.space.deserializers.ErrorDeserializer; @@ -40,7 +39,7 @@ public ContractsEndpoint(OkHttpClient client, HttpUrl baseUrl, String apiKey) { .add("x-api-key", apiKey).build(); } - public Subscription addContract(SubscriptionRequest subscriptionReq) { + public Subscription addContract(SubscriptionRequest subscriptionReq) throws IOException { Objects.requireNonNull(subscriptionReq, "subscription request must not be null"); HttpUrl url = this.baseUrl.newBuilder().addPathSegment(ENDPOINT).build(); @@ -48,8 +47,7 @@ public Subscription addContract(SubscriptionRequest subscriptionReq) { 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(); + try (Response response = client.newCall(request).execute()) { ResponseBody responseBody = response.body(); JSONObject jsonResponse = new JSONObject(responseBody.string()); if (!response.isSuccessful()) { @@ -58,42 +56,36 @@ public Subscription addContract(SubscriptionRequest subscriptionReq) { } res = subscriptionDeserializer.fromJson(jsonResponse); - } catch (IOException e) { - e.printStackTrace(); } return res; } - public Subscription getContractByUserId(String userId) { + 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(); + try (Response response = client.newCall(request).execute()) { JSONObject jsonResponse = new JSONObject(response.body().string()); if (!response.isSuccessful()) { jsonResponse.put("statusCode", response.code()); throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); } res = subscriptionDeserializer.fromJson(jsonResponse); - } catch (IOException e) { - e.printStackTrace(); } return res; } - public Subscription updateContractByUserId(String userId, SubscriptionUpdateRequest subscription) { + 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(); + try (Response response = client.newCall(request).execute()) { ResponseBody responseBody = response.body(); JSONObject jsonResponse = new JSONObject(responseBody.string()); if (!response.isSuccessful()) { diff --git a/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java index 65f5d9e..40cd4fb 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java @@ -1,5 +1,6 @@ package io.github.pgmarc.space.contracts; +import java.io.IOException; import java.time.Duration; import org.junit.jupiter.api.BeforeAll; @@ -24,7 +25,6 @@ class ContractsEndpointTest { 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)) @@ -75,11 +75,17 @@ void givenASubscriptionShouldBeCreated() { .build(); assertThatNoException().isThrownBy(() -> endpoint.addContract(subReq)); - Subscription subscription = endpoint.addContract(subReq); - assertThat(subscription.getServices()).isEqualTo(subReq.getServices()); - assertThat(subscription.getUserId()).isEqualTo(userId); - assertThat(subscription.getRenewalDuration().get()).isEqualTo(Duration.ofDays(45)); - assertThat(subscription.getHistory()).isEmpty(); + Subscription subscription; + try { + subscription = endpoint.addContract(subReq); + assertThat(subscription.getServices()).isEqualTo(subReq.getServices()); + assertThat(subscription.getUserId()).isEqualTo(userId); + assertThat(subscription.getRenewalDuration().get()).isEqualTo(Duration.ofDays(45)); + assertThat(subscription.getHistory()).isEmpty(); + } catch (IOException e) { + fail(); + } + } @Test @@ -122,8 +128,13 @@ void givenAnUserIdShouldReturnASubscription() { .withHeader("Content-Type", "application/json") .withBodyFile("getContractById-response.json"))); - Subscription subscription = endpoint.getContractByUserId(userId); - assertThat(subscription.getUserId()).isEqualTo(userId); + Subscription subscription; + try { + subscription = endpoint.getContractByUserId(userId); + assertThat(subscription.getUserId()).isEqualTo(userId); + } catch (IOException e) { + fail(); + } } @@ -171,9 +182,13 @@ void givenAnUserIdAndServicesShouldUpdateSubscription() { .service("petclinic", "v1") .plan("GOLD") .add(); - Subscription sub = endpoint.updateContractByUserId(userId, subscription); - - assertThat(sub.getUserId()).isEqualTo(userId); + Subscription sub; + try { + sub = endpoint.updateContractByUserId(userId, subscription); + assertThat(sub.getUserId()).isEqualTo(userId); + } catch (IOException e) { + fail(); + } } From 1c1d6eb1c7dc32777070f4925fe9687bdaff06e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 20:44:16 +0200 Subject: [PATCH 32/42] fix: remove stacktrace --- .../io/github/pgmarc/space/contracts/ContractsEndpoint.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java index f513778..7922060 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java +++ b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java @@ -78,7 +78,8 @@ public Subscription getContractByUserId(String userId) throws IOException { return res; } - public Subscription updateContractByUserId(String userId, SubscriptionUpdateRequest subscription) throws IOException { + 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) @@ -93,8 +94,6 @@ public Subscription updateContractByUserId(String userId, SubscriptionUpdateRequ throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); } res = subscriptionDeserializer.fromJson(jsonResponse); - } catch (IOException e) { - e.printStackTrace(); } return res; From 4c58a169ee1dea7deb2d70deb40fcd7907ac2e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 20:59:44 +0200 Subject: [PATCH 33/42] fix: wrong date comparisons --- .../java/io/github/pgmarc/space/contracts/BillingPeriod.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java index 865d46c..e8758fb 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -2,6 +2,8 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Objects; import java.util.Optional; @@ -31,7 +33,7 @@ Duration getDuration() { } boolean isExpired(LocalDateTime dateTime) { - return endDate.isAfter(startDate); + return endDate.isAfter(ZonedDateTime.of(dateTime, ZoneId.of("UTC"))); } boolean isAutoRenewable() { From cbb30447ed332294faabe93f9b0b487efd499837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 21:06:01 +0200 Subject: [PATCH 34/42] feat: extract statusCode --- .../io/github/pgmarc/space/contracts/BillingPeriod.java | 1 - .../github/pgmarc/space/contracts/ContractsEndpoint.java | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java index e8758fb..2c057de 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java +++ b/src/main/java/io/github/pgmarc/space/contracts/BillingPeriod.java @@ -3,7 +3,6 @@ import java.time.Duration; import java.time.LocalDateTime; import java.time.ZoneId; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Objects; import java.util.Optional; diff --git a/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java index 7922060..2014a99 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java +++ b/src/main/java/io/github/pgmarc/space/contracts/ContractsEndpoint.java @@ -23,6 +23,7 @@ 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; @@ -51,7 +52,7 @@ public Subscription addContract(SubscriptionRequest subscriptionReq) throws IOEx ResponseBody responseBody = response.body(); JSONObject jsonResponse = new JSONObject(responseBody.string()); if (!response.isSuccessful()) { - jsonResponse.put("statusCode", response.code()); + jsonResponse.put(STATUS_CODE, response.code()); throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); } @@ -69,7 +70,7 @@ public Subscription getContractByUserId(String userId) throws IOException { try (Response response = client.newCall(request).execute()) { JSONObject jsonResponse = new JSONObject(response.body().string()); if (!response.isSuccessful()) { - jsonResponse.put("statusCode", response.code()); + jsonResponse.put(STATUS_CODE, response.code()); throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); } res = subscriptionDeserializer.fromJson(jsonResponse); @@ -90,7 +91,7 @@ public Subscription updateContractByUserId(String userId, SubscriptionUpdateRequ ResponseBody responseBody = response.body(); JSONObject jsonResponse = new JSONObject(responseBody.string()); if (!response.isSuccessful()) { - jsonResponse.put("statusCode", response.code()); + jsonResponse.put(STATUS_CODE, response.code()); throw new SpaceApiException(errorDeserializer.fromJson(jsonResponse)); } res = subscriptionDeserializer.fromJson(jsonResponse); From 18db024071c47ed91b8515dedf5f5bf9adc70a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 21:08:03 +0200 Subject: [PATCH 35/42] feat: inmediately return --- src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java index 7117366..b351d67 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java +++ b/src/main/java/io/github/pgmarc/space/contracts/UsageLevel.java @@ -62,7 +62,6 @@ public static UsageLevel of(String name, double consumed) { public static UsageLevel of(String name, double consumed, ZonedDateTime resetTimestamp) { validateUsageLevel(name, consumed); - UsageLevel level = new UsageLevel(name, consumed, resetTimestamp); - return level; + return new UsageLevel(name, consumed, resetTimestamp); } } From 62aab08b20f151920eac6e4b657baa1aeff4aab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 22:11:05 +0200 Subject: [PATCH 36/42] feat: renamed SubscriptionRequest methods --- .../space/contracts/SubscriptionRequest.java | 50 +++++++++---------- .../contracts/ContractsEndpointTest.java | 12 ++--- .../contracts/SubscriptionRequestTest.java | 6 +-- .../SubscriptionRequestSerializerTest.java | 8 +-- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java index ea1eb7c..10f7d9f 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java +++ b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java @@ -46,17 +46,23 @@ private Builder(UserContact userContact) { this.userContact = userContact; } - private boolean isServiceBuilderAlive() { - return serviceBuilder != null; + public Builder subscribe(Service service) { + this.services.add(Objects.requireNonNull(service, "service must not be null")); + return this; } - private void validateServiceBuilderCalled(String message) { - if (!isServiceBuilderAlive()) { - throw new IllegalStateException(message); - } + 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 service(String name, String version) { + public Builder startService(String name, String version) { if (isServiceBuilderAlive()) { throw new IllegalStateException("you must build a service before creating another"); } @@ -76,36 +82,30 @@ public Builder addOn(String addOnName, long quantity) { return this; } - private void destroyServiceBuilder() { - this.serviceBuilder = null; - } - - public Builder buildService() { + public Builder endService() { validateServiceBuilderCalled("you must call 'newService' before adding a service"); services.add(serviceBuilder.build()); destroyServiceBuilder(); return this; } - public Builder subscribe(Service service) { - this.services.add(Objects.requireNonNull(service, "service must not be null")); - return this; + public SubscriptionRequest build() { + Objects.requireNonNull(userContact, "userContact must not be null"); + return new SubscriptionRequest(this); } - public Builder subscribeAll(Collection services) { - Objects.requireNonNull(services, "services must not be null"); - this.services.addAll(services); - return this; + private boolean isServiceBuilderAlive() { + return serviceBuilder != null; } - public Builder renewIn(Duration renewalDays) { - this.renewalDays = renewalDays; - return this; + private void validateServiceBuilderCalled(String message) { + if (!isServiceBuilderAlive()) { + throw new IllegalStateException(message); + } } - public SubscriptionRequest build() { - Objects.requireNonNull(userContact, "userContact must not be null"); - return new SubscriptionRequest(this); + private void destroyServiceBuilder() { + this.serviceBuilder = null; } } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java index 40cd4fb..4a758c4 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java @@ -63,15 +63,15 @@ void givenASubscriptionShouldBeCreated() { SubscriptionRequest subReq = SubscriptionRequest.builder(userContact) .renewIn(Duration.ofDays(45)) - .service("zoom", "2025") + .startService("zoom", "2025") .plan("ENTERPRISE") .addOn("extraSeats", 2) .addOn("hugeMeetings", 1) - .buildService() - .service("petclinic", "2024") + .endService() + .startService("petclinic", "2024") .plan("GOLD") .addOn("petsAdoptionCentre", 1) - .buildService() + .endService() .build(); assertThatNoException().isThrownBy(() -> endpoint.addContract(subReq)); @@ -104,9 +104,9 @@ void givenRequestWithNoApiKeyShouldThrow() { .build(); SubscriptionRequest subReq = SubscriptionRequest.builder(userContact) - .service("err", "v1") + .startService("err", "v1") .plan("Error") - .buildService() + .endService() .build(); assertThatExceptionOfType(SpaceApiException.class).isThrownBy(() -> endpoint.addContract(subReq)) diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java index 5e39b15..05b88b8 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java @@ -51,8 +51,8 @@ void givenConsecutiveServiceCreationShouldThrow() { Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest .builder(TEST_USER_CONTACT) - .service("test", "v1") - .service("incorrect", "v1") + .startService("test", "v1") + .startService("incorrect", "v1") .build()); assertEquals("you must build a service before creating another", ex.getMessage()); } @@ -82,7 +82,7 @@ void givenServiceBuildCallBeforeCreationServiceShouldThrow() { Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest .builder(TEST_USER_CONTACT) - .buildService() + .endService() .build()); assertEquals("you must call 'newService' before adding a service", 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 index 0da5e4b..a633a19 100644 --- a/src/test/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializerTest.java +++ b/src/test/java/io/github/pgmarc/space/serializers/SubscriptionRequestSerializerTest.java @@ -29,15 +29,15 @@ void givenSubscriptionRequestShouldSerialize() { String petclinic = "petclinic"; SubscriptionRequest subReq = SubscriptionRequest.builder(userContact) .renewIn(Duration.ofDays(365)) - .service(zoom, "2025") + .startService(zoom, "2025") .plan("ENTERPRISE") .addOn("extraSeats", 2) .addOn("hugeMeetings", 1) - .buildService() - .service("petclinic", "2024") + .endService() + .startService("petclinic", "2024") .plan("GOLD") .addOn("petsAdoptionCentre", 1) - .buildService() + .endService() .build(); SubscriptionRequestSerializer serializer = new SubscriptionRequestSerializer(); From b35d88da0ffb21fe70e0012c550bc24e300d5e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 23:15:11 +0200 Subject: [PATCH 37/42] feat: add fluent assertions --- .../io/github/pgmarc/space/ConfigTest.java | 29 ++++++----- .../space/contracts/BillingPeriodTest.java | 26 +++++----- .../pgmarc/space/contracts/ServiceTest.java | 49 ++++++++++--------- .../contracts/SubscriptionRequestTest.java | 39 ++++++--------- .../space/contracts/UserContactTest.java | 31 ++++++------ 5 files changed, 86 insertions(+), 88 deletions(-) diff --git a/src/test/java/io/github/pgmarc/space/ConfigTest.java b/src/test/java/io/github/pgmarc/space/ConfigTest.java index f92a735..d578238 100644 --- a/src/test/java/io/github/pgmarc/space/ConfigTest.java +++ b/src/test/java/io/github/pgmarc/space/ConfigTest.java @@ -1,8 +1,7 @@ package io.github.pgmarc.space; +import static org.assertj.core.api.Assertions.*; 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 java.time.Duration; @@ -18,16 +17,20 @@ void givenRequiredParametersShouldCreateConfig() { Config config = Config.builder(TEST_HOST, TEST_API_KEY).build(); - assertEquals("http://" + TEST_HOST + ":5403/api/v1", config.getUrl().toString()); - assertEquals(TEST_API_KEY, config.getApiKey()); + assertAll( + () -> assertThat(config.getUrl().toString()).isEqualTo("http://" + TEST_HOST + ":5403/api/v1"), + () -> assertThat(config.getApiKey()).isEqualTo(TEST_API_KEY)); + } @Test void givenNoHostAndPortShouldThrow() { - assertAll( - () -> assertThrows(NullPointerException.class, () -> Config.builder(null, TEST_API_KEY).build()), - () -> assertThrows(NullPointerException.class, () -> Config.builder(TEST_HOST, null).build())); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> Config.builder(null, TEST_API_KEY).build()); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> Config.builder(TEST_HOST, null).build()); + } @Test @@ -45,9 +48,12 @@ void givenOptionalParemetersShoudCreate() { .writeTimeout(Duration.ofMillis(writeTimeoutMillis)) .build(); - assertEquals("http://" + TEST_HOST + ":" + port + "/" + prefixPath, config.getUrl().toString()); - assertEquals(readTimeoutMillis, config.getReadTimeout().toMillis()); - assertEquals(writeTimeoutMillis, config.getWriteTimeout().toMillis()); + assertAll( + () -> assertThat(config.getUrl().toString()) + .isEqualTo("http://" + TEST_HOST + ":" + port + "/" + prefixPath), + () -> assertThat(config.getReadTimeout().toMillis()).isEqualTo(readTimeoutMillis), + () -> assertThat(config.getWriteTimeout().toMillis()).isEqualTo(writeTimeoutMillis)); + } @Test @@ -56,8 +62,7 @@ void givenNullPathShouldUseDefaultPrefixPath() { Config config = Config.builder(TEST_HOST, TEST_API_KEY) .prefixPath(null) .build(); - - assertEquals("http://example.com:5403/api/v1", config.getUrl().toString()); + assertThat(config.getUrl().toString()).isEqualTo("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 index c91731e..ff254a0 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -1,9 +1,6 @@ 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.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.time.ZonedDateTime; @@ -19,28 +16,29 @@ class BillingPeriodTest { void givenZeroRenewalDaysShouldThrow() { BillingPeriod period = BillingPeriod.of(start, end); - Exception ex = assertThrows(IllegalArgumentException.class, - () -> period.setRenewalDays(Duration.ofHours(12))); - assertEquals("your subscription cannot expire in less than one day", ex.getMessage()); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> period.setRenewalDays(Duration.ofHours(12))) + .withMessage("your subscription cannot expire in less than one day"); } @Test void givenStartDateAfterEndDateShouldThrow() { - Exception ex = assertThrows(IllegalStateException.class, - () -> BillingPeriod.of(start, start.minusDays(1))); - assertEquals("startDate is after endDate", ex.getMessage()); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> BillingPeriod.of(start, start.minusDays(1))) + .withMessage("startDate is after endDate"); } @Test void givenRenewableDateShouldBeRenowable() { + int days = 30; BillingPeriod billingPeriod = BillingPeriod.of(start, end); - billingPeriod.setRenewalDays(Duration.ofDays(30)); + billingPeriod.setRenewalDays(Duration.ofDays(days)); - assertAll( - () -> assertTrue(billingPeriod.isAutoRenewable()), - () -> assertEquals(end.plusDays(30).toLocalDateTime(), billingPeriod.getRenewalDate().get())); + assertThat(billingPeriod.getDuration().toDays()).isNotNull().isEqualTo(days); + assertThat(billingPeriod.getRenewalDate().get()).isEqualTo(end.plusDays(30).toLocalDateTime()); } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java b/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java index 39bb25c..11bec18 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java @@ -1,7 +1,6 @@ package io.github.pgmarc.space.contracts; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; @@ -15,31 +14,33 @@ void givenServiceWithPlanShouldCreateService() { Service service = Service.builder("test", "alfa") .plan(plan).build(); - assertEquals("foo", service.getPlan().get()); + assertThat(service.getPlan()).isPresent().hasValue(plan); + } @Test void givenServiceWithNullPlanShouldThrow() { - Exception ex = assertThrows(NullPointerException.class, - () -> Service.builder("foo", "alfa").plan(null)); - assertEquals("plan must not be null", ex.getMessage()); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> Service.builder("foo", "alfa").plan(null)) + .withMessage("plan must not be null"); + } @Test void givenServiceWithBlankPlanShouldThrow() { - Exception ex = assertThrows(IllegalArgumentException.class, - () -> Service.builder("foo", "alfa").plan("")); - assertEquals("plan must not be blank", ex.getMessage()); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> Service.builder("foo", "alfa").plan("")) + .withMessage("plan must not be blank"); } @Test void givenNoPlanOrAddOnShouldThrow() { - Exception ex = assertThrows(IllegalStateException.class, - () -> Service.builder("test", "alfa").build()); - assertEquals("At least you have to be subscribed to a plan or add-on", ex.getMessage()); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> Service.builder("test", "alfa").build()) + .withMessage("At least you have to be subscribed to a plan or add-on"); } @Test @@ -50,24 +51,23 @@ void givenAPlanShouldBePresentInService() { Service service = Service.builder("test", "alfa") .plan(plan).build(); - assertEquals(plan, service.getPlan().get()); + assertThat(service.getPlan()).isPresent().hasValue(plan); } @Test void givenNullAsAddOnKeyShouldThrow() { - Exception ex = assertThrows(NullPointerException.class, - () -> Service.builder("test", "alfa").addOn(null, 1)); - assertEquals("add-on name must not be null", ex.getMessage()); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> Service.builder("test", "alfa").addOn(null, 1)) + .withMessage("add-on name must not be null"); } - @Test void givenAddOnWithZeroQuantityShouldThrow() { - Exception ex = assertThrows(IllegalArgumentException.class, - () -> Service.builder("test", "alfa").addOn("zeroQuantity", 0)); - assertEquals("zeroQuantity quantity must be greater than 0", ex.getMessage()); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> Service.builder("test", "alfa").addOn("zeroQuantity", 0)) + .withMessage("zeroQuantity quantity must be greater than 0"); } @Test @@ -78,7 +78,8 @@ void givenAnAddOnShouldBePresentInService() { Service service = Service.builder("test", "alfa") .addOn(addOn, 1).build(); - assertEquals(addOn, service.getAddOn(addOn).get().getName()); + assertThat(service.getAddOn(addOn)).isPresent().hasValue(new AddOn(addOn, 1)); + } @Test @@ -92,9 +93,9 @@ void givenPlanAndAddOnsShouldBePresent() { .addOn(addOn1.getName(), addOn1.getQuantity()) .addOn(addOn2.getName(), addOn1.getQuantity()).build(); - assertEquals(plan, service.getPlan().get()); - assertEquals(addOn1, service.getAddOn(addOn1.getName()).get()); - assertEquals(addOn2, service.getAddOn(addOn2.getName()).get()); + assertThat(service.getPlan()).isPresent().hasValue(plan); + assertThat(service.getAddOn(addOn1.getName())).isPresent().hasValue(addOn1); + assertThat(service.getAddOn(addOn2.getName())).isPresent().hasValue(addOn2); } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java index 05b88b8..24849bb 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java @@ -1,12 +1,9 @@ package io.github.pgmarc.space.contracts; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.time.ZonedDateTime; -import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,65 +40,61 @@ void givenMultipleServicesInSubscriptionShouldCreate() { .renewIn(Duration.ofDays(renewalDays)) .build(); - assertEquals(Set.of(service1, service2), sub.getServices()); + assertThat(sub.getServices()).contains(service1, service2); } @Test void givenConsecutiveServiceCreationShouldThrow() { - Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SubscriptionRequest .builder(TEST_USER_CONTACT) .startService("test", "v1") .startService("incorrect", "v1") - .build()); - assertEquals("you must build a service before creating another", ex.getMessage()); + .build()).withMessage("you must build a service before creating another"); } @Test void givenPlanCallBeforeCallingCreationServiceShouldThrow() { - Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SubscriptionRequest .builder(TEST_USER_CONTACT) .plan("foo") - .build()); - assertEquals("you must call 'newService' before setting a plan: foo", ex.getMessage()); + .build()).withMessage("you must call 'newService' before setting a plan: foo"); } @Test void givenAddOnCallBeforeCallingCreationServiceShouldThrow() { - Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SubscriptionRequest .builder(TEST_USER_CONTACT) .addOn("foo", 1) - .build()); - assertEquals("you must call 'newService' before setting an add-on: foo", ex.getMessage()); + .build()).withMessage("you must call 'newService' before setting an add-on: foo"); + } @Test void givenServiceBuildCallBeforeCreationServiceShouldThrow() { - Exception ex = assertThrows(IllegalStateException.class, () -> SubscriptionRequest + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SubscriptionRequest .builder(TEST_USER_CONTACT) .endService() - .build()); - assertEquals("you must call 'newService' before adding a service", ex.getMessage()); + .build()).withMessage("you must call 'newService' before adding a service"); } @Test void whenNoRequiredParametersInputShouldThrow() { - Exception ex = assertThrows(NullPointerException.class, - () -> SubscriptionRequest.builder(null) - .build()); - assertEquals("userContact must not be null", ex.getMessage()); + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> SubscriptionRequest.builder(null) + .build()).withMessage("userContact must not be null"); } @Test void givenOptionalRenewalDaysShouldNotThrow() { - assertDoesNotThrow(() -> SubscriptionRequest.builder(TEST_USER_CONTACT) + assertThat(SubscriptionRequest.builder(TEST_USER_CONTACT) .renewIn(null) - .build()); + .build().getRenewalDays()).isNull(); + } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index 6dfa487..c54f95b 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -1,7 +1,6 @@ package io.github.pgmarc.space.contracts; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.*; import java.util.Optional; @@ -19,31 +18,33 @@ class UserContactTest { void givenIdAndUsernameShouldCreateUserContact() { UserContact contact = UserContact.builder(TEST_USER_ID, TEST_USERNAME).build(); - assertEquals(TEST_USER_ID, contact.getUserId()); - assertEquals(TEST_USERNAME, contact.getUsername()); + assertThat(contact.getUserId()).isEqualTo(TEST_USER_ID); + assertThat(contact.getUsername()).isEqualTo(TEST_USERNAME); } @Test void givenNullUserIdShouldThrow() { - Exception ex = assertThrows(NullPointerException.class, - () -> UserContact.builder(null, TEST_USERNAME)); - assertEquals("userId must not be null", ex.getMessage()); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> UserContact.builder(null, TEST_USERNAME)) + .withMessage("userId must not be null"); } @Test void givenNullUsernameShouldThrow() { - Exception ex = assertThrows(NullPointerException.class, - () -> UserContact.builder(TEST_USER_ID, null)); - assertEquals("username must not be null", ex.getMessage()); + 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) { - assertThrows(IllegalArgumentException.class, () -> UserContact.builder(TEST_USER_ID, username).build()); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> UserContact.builder(TEST_USER_ID, username).build()); + } // Using pairwise testing @@ -61,9 +62,9 @@ void givenOptionalParametersExpecttoBeDefined(String firstName, String lastName, .lastName(lastName) .email(email) .phone(phone).build(); - assertEquals(Optional.ofNullable(firstName), contact.getFirstName()); - assertEquals(Optional.ofNullable(lastName), contact.getLastName()); - assertEquals(Optional.ofNullable(email), contact.getEmail()); - assertEquals(Optional.ofNullable(phone), contact.getPhone()); + 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)); } } From 145441a091c5d5e5422c82d143083d21bbcb98f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 23:39:21 +0200 Subject: [PATCH 38/42] fix: high mantainability issues --- .../github/pgmarc/space/exceptions/SpaceApiException.java | 2 +- src/test/java/io/github/pgmarc/space/ConfigTest.java | 8 ++++++-- .../github/pgmarc/space/contracts/BillingPeriodTest.java | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java b/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java index bac1d24..38f63bf 100644 --- a/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java +++ b/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java @@ -3,7 +3,7 @@ public class SpaceApiException extends RuntimeException { - private final SpaceApiError error; + private transient final SpaceApiError error; public SpaceApiException(SpaceApiError error) { super(error.toString()); diff --git a/src/test/java/io/github/pgmarc/space/ConfigTest.java b/src/test/java/io/github/pgmarc/space/ConfigTest.java index d578238..4ae721f 100644 --- a/src/test/java/io/github/pgmarc/space/ConfigTest.java +++ b/src/test/java/io/github/pgmarc/space/ConfigTest.java @@ -26,10 +26,14 @@ void givenRequiredParametersShouldCreateConfig() { @Test void givenNoHostAndPortShouldThrow() { + Config.Builder config1 = Config.builder(null, TEST_API_KEY); assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> Config.builder(null, TEST_API_KEY).build()); + .isThrownBy(() -> config1.build()) + .withMessage("host must not be null"); + Config.Builder config2 = Config.builder(TEST_HOST, null); assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> Config.builder(TEST_HOST, null).build()); + .isThrownBy(() -> config2.build()) + .withMessage("api key must not be null"); } diff --git a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java index ff254a0..bf8780a 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -37,8 +37,8 @@ void givenRenewableDateShouldBeRenowable() { BillingPeriod billingPeriod = BillingPeriod.of(start, end); billingPeriod.setRenewalDays(Duration.ofDays(days)); - assertThat(billingPeriod.getDuration().toDays()).isNotNull().isEqualTo(days); - assertThat(billingPeriod.getRenewalDate().get()).isEqualTo(end.plusDays(30).toLocalDateTime()); + assertThat(billingPeriod.getDuration().toDays()).isEqualTo(days); + assertThat(billingPeriod.getRenewalDate()).isPresent().hasValue(end.plusDays(30).toLocalDateTime()); } } From 9f453e9fa9a7a01558f0a0f816749786b172140b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sat, 23 Aug 2025 23:49:35 +0200 Subject: [PATCH 39/42] fix: minor issues sonar --- .../github/pgmarc/space/exceptions/SpaceApiException.java | 2 +- src/test/java/io/github/pgmarc/space/ConfigTest.java | 8 ++++---- .../pgmarc/space/contracts/ContractsEndpointTest.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java b/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java index 38f63bf..1ce43af 100644 --- a/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java +++ b/src/main/java/io/github/pgmarc/space/exceptions/SpaceApiException.java @@ -3,7 +3,7 @@ public class SpaceApiException extends RuntimeException { - private transient final SpaceApiError error; + private final transient SpaceApiError error; public SpaceApiException(SpaceApiError error) { super(error.toString()); diff --git a/src/test/java/io/github/pgmarc/space/ConfigTest.java b/src/test/java/io/github/pgmarc/space/ConfigTest.java index 4ae721f..c2625c5 100644 --- a/src/test/java/io/github/pgmarc/space/ConfigTest.java +++ b/src/test/java/io/github/pgmarc/space/ConfigTest.java @@ -18,7 +18,7 @@ void givenRequiredParametersShouldCreateConfig() { Config config = Config.builder(TEST_HOST, TEST_API_KEY).build(); assertAll( - () -> assertThat(config.getUrl().toString()).isEqualTo("http://" + TEST_HOST + ":5403/api/v1"), + () -> assertThat(config.getUrl()).hasToString("http://" + TEST_HOST + ":5403/api/v1"), () -> assertThat(config.getApiKey()).isEqualTo(TEST_API_KEY)); } @@ -53,8 +53,8 @@ void givenOptionalParemetersShoudCreate() { .build(); assertAll( - () -> assertThat(config.getUrl().toString()) - .isEqualTo("http://" + TEST_HOST + ":" + port + "/" + prefixPath), + () -> assertThat(config.getUrl()) + .hasToString("http://" + TEST_HOST + ":" + port + "/" + prefixPath), () -> assertThat(config.getReadTimeout().toMillis()).isEqualTo(readTimeoutMillis), () -> assertThat(config.getWriteTimeout().toMillis()).isEqualTo(writeTimeoutMillis)); @@ -66,7 +66,7 @@ void givenNullPathShouldUseDefaultPrefixPath() { Config config = Config.builder(TEST_HOST, TEST_API_KEY) .prefixPath(null) .build(); - assertThat(config.getUrl().toString()).isEqualTo("http://example.com:5403/api/v1"); + assertThat(config.getUrl()).hasToString("http://example.com:5403/api/v1"); } } diff --git a/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java index 4a758c4..cce38df 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/ContractsEndpointTest.java @@ -80,7 +80,7 @@ void givenASubscriptionShouldBeCreated() { subscription = endpoint.addContract(subReq); assertThat(subscription.getServices()).isEqualTo(subReq.getServices()); assertThat(subscription.getUserId()).isEqualTo(userId); - assertThat(subscription.getRenewalDuration().get()).isEqualTo(Duration.ofDays(45)); + assertThat(subscription.getRenewalDuration()).isPresent().hasValue(Duration.ofDays(45)); assertThat(subscription.getHistory()).isEmpty(); } catch (IOException e) { fail(); From bac35aac6c72364e70ffb3ccc5e62dfb9bb0d5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sun, 24 Aug 2025 10:08:06 +0200 Subject: [PATCH 40/42] feat(contracts): add service creation corner cases --- .../space/contracts/SubscriptionRequest.java | 6 ++ .../contracts/SubscriptionRequestTest.java | 75 ++++++++++++++----- 2 files changed, 61 insertions(+), 20 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java index 10f7d9f..b4ad24a 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java +++ b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionRequest.java @@ -90,7 +90,13 @@ public Builder endService() { } 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); } diff --git a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java index 24849bb..8728c2c 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/SubscriptionRequestTest.java @@ -46,55 +46,90 @@ void givenMultipleServicesInSubscriptionShouldCreate() { @Test void givenConsecutiveServiceCreationShouldThrow() { - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SubscriptionRequest + SubscriptionRequest.Builder builder = SubscriptionRequest .builder(TEST_USER_CONTACT) - .startService("test", "v1") - .startService("incorrect", "v1") - .build()).withMessage("you must build a service before creating another"); + .startService("test", "v1"); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> builder.startService("incorrect", "v1")) + .withMessage("you must build a service before creating another"); } @Test void givenPlanCallBeforeCallingCreationServiceShouldThrow() { - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SubscriptionRequest - .builder(TEST_USER_CONTACT) - .plan("foo") - .build()).withMessage("you must call 'newService' before setting a plan: foo"); + 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() { - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SubscriptionRequest - .builder(TEST_USER_CONTACT) - .addOn("foo", 1) - .build()).withMessage("you must call 'newService' before setting an add-on: foo"); + 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() { - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SubscriptionRequest - .builder(TEST_USER_CONTACT) - .endService() - .build()).withMessage("you must call 'newService' before adding a service"); + 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() { - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> SubscriptionRequest.builder(null) - .build()).withMessage("userContact must not be null"); + SubscriptionRequest.Builder builder = SubscriptionRequest.builder(null); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> builder.build()) + .withMessage("userContact must not be null"); } @Test void givenOptionalRenewalDaysShouldNotThrow() { - assertThat(SubscriptionRequest.builder(TEST_USER_CONTACT) + 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) - .build().getRenewalDays()).isNull(); + .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"); } } From 8240a4446e7d5ce4de8aef63d6fb02b559ab2a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sun, 24 Aug 2025 10:09:01 +0200 Subject: [PATCH 41/42] fix: missing parameter method in addOn --- .../pgmarc/space/contracts/SubscriptionUpdateRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java index 5134ca3..f117600 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java +++ b/src/main/java/io/github/pgmarc/space/contracts/SubscriptionUpdateRequest.java @@ -34,7 +34,7 @@ public SubscriptionUpdateRequest plan(String plan) { public SubscriptionUpdateRequest addOn(String name, long quantity) { Objects.requireNonNull(serviceBuilder, "you call service first"); - this.serviceBuilder.addOn(name, 0); + this.serviceBuilder.addOn(name, quantity); return this; } From 0d5be99d348c635d53e37c3ee40a490906d422c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Gonz=C3=A1lez=20Marcos?= Date: Sun, 24 Aug 2025 10:28:26 +0200 Subject: [PATCH 42/42] feat(sonar): fix test issues Sonarcloud --- .../pgmarc/space/contracts/Service.java | 1 - .../space/contracts/BillingPeriodTest.java | 7 +- .../pgmarc/space/contracts/ServiceTest.java | 73 +++++-------------- .../space/contracts/UserContactTest.java | 4 +- 4 files changed, 25 insertions(+), 60 deletions(-) diff --git a/src/main/java/io/github/pgmarc/space/contracts/Service.java b/src/main/java/io/github/pgmarc/space/contracts/Service.java index f21fc80..98cb1d9 100644 --- a/src/main/java/io/github/pgmarc/space/contracts/Service.java +++ b/src/main/java/io/github/pgmarc/space/contracts/Service.java @@ -107,7 +107,6 @@ private Builder(String name, String version) { } public Builder plan(String plan) { - Objects.requireNonNull(plan, "plan must not be null"); if (plan.isBlank()) { throw new IllegalArgumentException("plan must not be blank"); } diff --git a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java index bf8780a..9355532 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/BillingPeriodTest.java @@ -16,17 +16,20 @@ class BillingPeriodTest { void givenZeroRenewalDaysShouldThrow() { BillingPeriod period = BillingPeriod.of(start, end); + Duration duration = Duration.ofHours(12); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> period.setRenewalDays(Duration.ofHours(12))) + .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, start.minusDays(1))) + .isThrownBy(() -> BillingPeriod.of(start, end)) .withMessage("startDate is after endDate"); } diff --git a/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java b/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java index 11bec18..57e8ef5 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/ServiceTest.java @@ -6,32 +6,33 @@ class ServiceTest { + + private final Service.Builder baseBuilder = Service.builder("test", "alfa"); + @Test void givenServiceWithPlanShouldCreateService() { - String plan = "foo"; + String name = "petclinic"; + String version = "v1"; + String plan = "GOLD"; + String addOnName = "petsAdoptionCentre"; - Service service = Service.builder("test", "alfa") - .plan(plan).build(); + 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); - - } - - @Test - void givenServiceWithNullPlanShouldThrow() { - - assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> Service.builder("foo", "alfa").plan(null)) - .withMessage("plan must not be null"); + assertThat(service.getAddOn(addOnName)).isPresent().hasValue(new AddOn(addOnName, 1)); } @Test void givenServiceWithBlankPlanShouldThrow() { + assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> Service.builder("foo", "alfa").plan("")) + .isThrownBy(() -> baseBuilder.plan("")) .withMessage("plan must not be blank"); } @@ -39,26 +40,15 @@ void givenServiceWithBlankPlanShouldThrow() { void givenNoPlanOrAddOnShouldThrow() { assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> Service.builder("test", "alfa").build()) + .isThrownBy(() -> baseBuilder.build()) .withMessage("At least you have to be subscribed to a plan or add-on"); } - @Test - void givenAPlanShouldBePresentInService() { - - String plan = "FREE"; - - Service service = Service.builder("test", "alfa") - .plan(plan).build(); - - assertThat(service.getPlan()).isPresent().hasValue(plan); - } - @Test void givenNullAsAddOnKeyShouldThrow() { assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> Service.builder("test", "alfa").addOn(null, 1)) + .isThrownBy(() -> baseBuilder.addOn(null, 1)) .withMessage("add-on name must not be null"); } @@ -66,36 +56,7 @@ void givenNullAsAddOnKeyShouldThrow() { void givenAddOnWithZeroQuantityShouldThrow() { assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> Service.builder("test", "alfa").addOn("zeroQuantity", 0)) + .isThrownBy(() -> baseBuilder.addOn("zeroQuantity", 0)) .withMessage("zeroQuantity quantity must be greater than 0"); } - - @Test - void givenAnAddOnShouldBePresentInService() { - - String addOn = "additionalItems"; - - Service service = Service.builder("test", "alfa") - .addOn(addOn, 1).build(); - - assertThat(service.getAddOn(addOn)).isPresent().hasValue(new AddOn(addOn, 1)); - - } - - @Test - void givenPlanAndAddOnsShouldBePresent() { - String plan = "FREE"; - AddOn addOn1 = new AddOn("addOn1", 1); - AddOn addOn2 = new AddOn("addOn2", 2); - - Service service = Service.builder("test", "alfa") - .plan(plan) - .addOn(addOn1.getName(), addOn1.getQuantity()) - .addOn(addOn2.getName(), addOn1.getQuantity()).build(); - - assertThat(service.getPlan()).isPresent().hasValue(plan); - assertThat(service.getAddOn(addOn1.getName())).isPresent().hasValue(addOn1); - assertThat(service.getAddOn(addOn2.getName())).isPresent().hasValue(addOn2); - } - } diff --git a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java index c54f95b..a747fbb 100644 --- a/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java +++ b/src/test/java/io/github/pgmarc/space/contracts/UserContactTest.java @@ -42,8 +42,10 @@ void givenNullUsernameShouldThrow() { @ValueSource(strings = { "", "ab", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) void givenInvalidUsernamesShouldThrow(String username) { + UserContact.Builder builder = UserContact.builder(TEST_USER_ID, username); + assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> UserContact.builder(TEST_USER_ID, username).build()); + .isThrownBy(() -> builder.build()); }