Skip to content

Commit 4b6bb53

Browse files
Merge pull request #71 from trynoice/feat/db-garbage-collection-2
Implement garbage collection in services for database tables
2 parents 5e0efce + 9bf4594 commit 4b6bb53

27 files changed

Lines changed: 539 additions & 47 deletions

.idea/misc.xml

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.trynoice.api.identity.entities;
2+
3+
import lombok.val;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.boot.test.context.SpringBootTest;
7+
import org.springframework.transaction.annotation.Transactional;
8+
9+
import javax.persistence.EntityManager;
10+
import java.time.OffsetDateTime;
11+
import java.util.stream.Collectors;
12+
import java.util.stream.IntStream;
13+
14+
import static com.trynoice.api.testing.AuthTestUtils.createAuthUser;
15+
import static org.junit.jupiter.api.Assertions.assertFalse;
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
17+
18+
@SpringBootTest
19+
@Transactional
20+
public class AuthUserRepositoryTest {
21+
22+
@Autowired
23+
private EntityManager entityManager;
24+
25+
@Autowired
26+
private AuthUserRepository authUserRepository;
27+
28+
@Test
29+
void findAllIdsDeactivatedBefore() {
30+
val deactivatedUsers = IntStream.range(0, 5)
31+
.mapToObj(i -> {
32+
val user = createAuthUser(entityManager);
33+
user.setDeactivatedAt(OffsetDateTime.now().minusHours(i));
34+
return entityManager.merge(user);
35+
})
36+
.collect(Collectors.toUnmodifiableList());
37+
38+
val activeUsers = IntStream.range(0, 5)
39+
.mapToObj(i -> createAuthUser(entityManager))
40+
.collect(Collectors.toList());
41+
42+
val expiredBefore = OffsetDateTime.now().minusHours(2);
43+
val ids = authUserRepository.findAllIdsDeactivatedBefore(expiredBefore);
44+
45+
deactivatedUsers.stream()
46+
.filter(u -> u.getDeactivatedAt().isBefore(expiredBefore))
47+
.forEach(u -> assertTrue(ids.contains(u.getId())));
48+
49+
deactivatedUsers.stream()
50+
.filter(u -> u.getDeactivatedAt().isAfter(expiredBefore))
51+
.forEach(u -> assertFalse(ids.contains(u.getId())));
52+
53+
activeUsers.forEach(u -> assertFalse(ids.contains(u.getId())));
54+
}
55+
}

src/integrationTest/java/com/trynoice/api/identity/entities/RefreshTokenRepositoryTest.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.trynoice.api.identity.entities;
22

33
import lombok.val;
4+
import org.junit.jupiter.api.Assertions;
45
import org.junit.jupiter.api.Test;
56
import org.springframework.beans.factory.annotation.Autowired;
67
import org.springframework.boot.test.context.SpringBootTest;
@@ -28,7 +29,6 @@ public class RefreshTokenRepositoryTest {
2829
@Test
2930
void updateExpiresAtOfAllByOwnerId() {
3031
val user = createAuthUser(entityManager);
31-
3232
val ownedRefreshTokens = IntStream.range(0, 5)
3333
.mapToObj(i -> createRefreshToken(entityManager, user))
3434
.collect(Collectors.toUnmodifiableList());
@@ -46,4 +46,56 @@ void updateExpiresAtOfAllByOwnerId() {
4646
.map(t -> entityManager.find(RefreshToken.class, t.getId()))
4747
.forEach(t -> assertTrue(t.getExpiresAt().isAfter(OffsetDateTime.now())));
4848
}
49+
50+
@Test
51+
void deleteAllByOwnerId() {
52+
val user = createAuthUser(entityManager);
53+
val ownedRefreshTokens = IntStream.range(0, 5)
54+
.mapToObj(i -> createRefreshToken(entityManager, user))
55+
.collect(Collectors.toUnmodifiableList());
56+
57+
val unownedRefreshTokens = IntStream.range(0, 5)
58+
.mapToObj(i -> createRefreshToken(entityManager, createAuthUser(entityManager)))
59+
.collect(Collectors.toUnmodifiableList());
60+
61+
refreshTokenRepository.deleteAllByOwnerId(user.getId());
62+
ownedRefreshTokens.stream()
63+
.map(t -> entityManager.find(RefreshToken.class, t.getId()))
64+
.forEach(Assertions::assertNull);
65+
66+
unownedRefreshTokens.stream()
67+
.map(t -> entityManager.find(RefreshToken.class, t.getId()))
68+
.forEach(Assertions::assertNotNull);
69+
}
70+
71+
@Test
72+
void deleteAllExpiredBefore() {
73+
val expiredRefreshTokens = IntStream.range(0, 5)
74+
.mapToObj(i -> {
75+
val token = createRefreshToken(entityManager, createAuthUser(entityManager));
76+
token.setExpiresAt(OffsetDateTime.now().minusHours(i));
77+
return entityManager.merge(token);
78+
})
79+
.collect(Collectors.toUnmodifiableList());
80+
81+
val activeRefreshTokens = IntStream.range(0, 5)
82+
.mapToObj(i -> createRefreshToken(entityManager, createAuthUser(entityManager)))
83+
.collect(Collectors.toUnmodifiableList());
84+
85+
val deleteBefore = OffsetDateTime.now().minusHours(2);
86+
refreshTokenRepository.deleteAllExpiredBefore(deleteBefore);
87+
expiredRefreshTokens.stream()
88+
.filter(t -> t.getExpiresAt().isBefore(deleteBefore))
89+
.map(t -> entityManager.find(RefreshToken.class, t.getId()))
90+
.forEach(Assertions::assertNull);
91+
92+
expiredRefreshTokens.stream()
93+
.filter(t -> t.getExpiresAt().isAfter(deleteBefore))
94+
.map(t -> entityManager.find(RefreshToken.class, t.getId()))
95+
.forEach(Assertions::assertNotNull);
96+
97+
activeRefreshTokens.stream()
98+
.map(t -> entityManager.find(RefreshToken.class, t.getId()))
99+
.forEach(Assertions::assertNotNull);
100+
}
49101
}

src/integrationTest/java/com/trynoice/api/subscription/SubscriptionControllerTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.stripe.exception.StripeException;
66
import com.stripe.model.checkout.Session;
77
import com.trynoice.api.identity.entities.AuthUser;
8-
import com.trynoice.api.subscription.ecb.ForeignExchangeRatesProvider;
98
import com.trynoice.api.subscription.entities.Customer;
109
import com.trynoice.api.subscription.entities.CustomerRepository;
1110
import com.trynoice.api.subscription.entities.GiftCardRepository;
@@ -15,6 +14,9 @@
1514
import com.trynoice.api.subscription.entities.SubscriptionRepository;
1615
import com.trynoice.api.subscription.payload.SubscriptionFlowParams;
1716
import com.trynoice.api.subscription.payload.SubscriptionPlanResponse;
17+
import com.trynoice.api.subscription.upstream.AndroidPublisherApi;
18+
import com.trynoice.api.subscription.upstream.ForeignExchangeRatesProvider;
19+
import com.trynoice.api.subscription.upstream.StripeApi;
1820
import com.trynoice.api.testing.AuthTestUtils;
1921
import lombok.NonNull;
2022
import lombok.val;

src/integrationTest/java/com/trynoice/api/subscription/SubscriptionControllerV2Test.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import com.stripe.model.checkout.Session;
66
import com.trynoice.api.identity.entities.AuthUser;
7-
import com.trynoice.api.subscription.ecb.ForeignExchangeRatesProvider;
87
import com.trynoice.api.subscription.entities.CustomerRepository;
98
import com.trynoice.api.subscription.entities.GiftCardRepository;
109
import com.trynoice.api.subscription.entities.Subscription;
1110
import com.trynoice.api.subscription.entities.SubscriptionPlan;
1211
import com.trynoice.api.subscription.entities.SubscriptionPlanRepository;
1312
import com.trynoice.api.subscription.entities.SubscriptionRepository;
1413
import com.trynoice.api.subscription.payload.SubscriptionFlowParams;
14+
import com.trynoice.api.subscription.upstream.ForeignExchangeRatesProvider;
15+
import com.trynoice.api.subscription.upstream.StripeApi;
1516
import com.trynoice.api.testing.AuthTestUtils;
1617
import lombok.NonNull;
1718
import lombok.val;

src/integrationTest/java/com/trynoice/api/subscription/SubscriptionServiceTest.java

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package com.trynoice.api.subscription;
22

3+
import com.stripe.exception.StripeException;
4+
import com.trynoice.api.contracts.AccountServiceContract;
35
import com.trynoice.api.identity.entities.AuthUser;
46
import com.trynoice.api.subscription.entities.Customer;
57
import com.trynoice.api.subscription.entities.CustomerRepository;
68
import com.trynoice.api.subscription.entities.Subscription;
79
import com.trynoice.api.subscription.entities.SubscriptionPlan;
810
import com.trynoice.api.subscription.entities.SubscriptionRepository;
911
import com.trynoice.api.subscription.payload.GooglePlayDeveloperNotification;
10-
import com.trynoice.api.subscription.payload.GooglePlaySubscriptionPurchase;
12+
import com.trynoice.api.subscription.upstream.AndroidPublisherApi;
13+
import com.trynoice.api.subscription.upstream.StripeApi;
14+
import com.trynoice.api.subscription.upstream.models.GooglePlaySubscriptionPurchase;
1115
import lombok.NonNull;
1216
import lombok.val;
1317
import org.junit.jupiter.api.Test;
@@ -17,11 +21,14 @@
1721
import org.springframework.beans.factory.annotation.Autowired;
1822
import org.springframework.boot.test.context.SpringBootTest;
1923
import org.springframework.boot.test.mock.mockito.MockBean;
24+
import org.springframework.context.ApplicationEventPublisher;
2025
import org.springframework.transaction.annotation.Transactional;
2126

2227
import javax.persistence.EntityManager;
2328
import java.time.OffsetDateTime;
2429
import java.util.UUID;
30+
import java.util.stream.Collectors;
31+
import java.util.stream.IntStream;
2532
import java.util.stream.Stream;
2633

2734
import static com.trynoice.api.subscription.SubscriptionTestUtils.buildSubscriptionPlan;
@@ -50,6 +57,12 @@ public class SubscriptionServiceTest {
5057
@MockBean
5158
private AndroidPublisherApi androidPublisherApi;
5259

60+
@MockBean
61+
private StripeApi stripeApi;
62+
63+
@Autowired
64+
private ApplicationEventPublisher eventPublisher;
65+
5366
@Autowired
5467
private SubscriptionService service;
5568

@@ -65,8 +78,9 @@ void handleGooglePlayWebhookEvent(
6578
) throws Exception {
6679
val purchaseToken = UUID.randomUUID().toString();
6780
val authUser = createAuthUser(entityManager);
81+
val customer = buildCustomer(authUser, null);
6882
val plan = buildSubscriptionPlan(entityManager, SubscriptionPlan.Provider.GOOGLE_PLAY, purchase.getProductId());
69-
val subscription = buildSubscription(authUser, plan, wasActive, wasPaymentPending, wasActive ? purchaseToken : null);
83+
val subscription = buildSubscription(customer, plan, wasActive, wasPaymentPending, wasActive ? purchaseToken : null);
7084
purchase = purchase.withObfuscatedExternalAccountId(String.valueOf(subscription.getId()));
7185
when(androidPublisherApi.getSubscriptionPurchase(purchaseToken))
7286
.thenReturn(purchase);
@@ -157,7 +171,8 @@ void handleGooglePlayWebhookEvent_planUpgrade() throws Exception {
157171
val oldPurchaseToken = UUID.randomUUID().toString();
158172
val newPurchaseToken = UUID.randomUUID().toString();
159173
val authUser = createAuthUser(entityManager);
160-
val subscription = buildSubscription(authUser, oldPlan, true, false, oldPurchaseToken);
174+
val customer = buildCustomer(authUser, null);
175+
val subscription = buildSubscription(customer, oldPlan, true, false, oldPurchaseToken);
161176
val purchase = GooglePlaySubscriptionPurchase.builder()
162177
.productId(newPlan.getProvidedId())
163178
.startTimeMillis(System.currentTimeMillis())
@@ -188,8 +203,9 @@ void handleGooglePlayWebhookEvent_doublePurchase() throws Exception {
188203
val authUser = createAuthUser(entityManager);
189204
val plan = buildSubscriptionPlan(entityManager, SubscriptionPlan.Provider.GOOGLE_PLAY, "test-plan");
190205
val purchaseToken = UUID.randomUUID().toString();
191-
val subscription1 = buildSubscription(authUser, plan, true, false, UUID.randomUUID().toString());
192-
val subscription2 = buildSubscription(authUser, plan, false, false, null);
206+
val customer = buildCustomer(authUser, null);
207+
val subscription1 = buildSubscription(customer, plan, true, false, UUID.randomUUID().toString());
208+
val subscription2 = buildSubscription(customer, plan, false, false, null);
193209
val purchase = GooglePlaySubscriptionPurchase.builder()
194210
.productId(plan.getProvidedId())
195211
.startTimeMillis(System.currentTimeMillis())
@@ -213,21 +229,56 @@ void handleGooglePlayWebhookEvent_doublePurchase() throws Exception {
213229
verify(androidPublisherApi, times(0)).acknowledgePurchase(plan.getProvidedId(), purchaseToken);
214230
}
215231

232+
@Test
233+
void onUserDeleted() throws StripeException {
234+
val plan = buildSubscriptionPlan(entityManager, SubscriptionPlan.Provider.STRIPE, "stripe-test-plan");
235+
val deletedCustomers = IntStream.range(0, 5)
236+
.mapToObj(i -> buildCustomer(createAuthUser(entityManager), "stripe-customer-" + i))
237+
.collect(Collectors.toUnmodifiableList());
238+
239+
val activeCustomers = IntStream.range(5, 10)
240+
.mapToObj(i -> buildCustomer(createAuthUser(entityManager), "stripe-customer-" + i))
241+
.collect(Collectors.toUnmodifiableList());
242+
243+
val activeSubscriptions = IntStream.range(0, 5)
244+
.mapToObj(i -> i % 2 == 0 ? deletedCustomers.get(i) : activeCustomers.get(i))
245+
.map(c -> buildSubscription(c, plan, true, false, "stripe-subscription-" + c.getUserId()))
246+
.collect(Collectors.toUnmodifiableList());
247+
248+
// TODO: how to test it using ApplicationEventPublisher#publishEvent? Since the following is
249+
// a TransactionalEventListener, it probably gets invoked after the test has finished.
250+
deletedCustomers.forEach(c -> service.onUserDeleted(new AccountServiceContract.UserDeletedEvent(c.getUserId())));
251+
252+
for (val deletedCustomer : deletedCustomers) {
253+
verify(stripeApi, times(1)).resetCustomerNameAndEmail(deletedCustomer.getStripeId());
254+
}
255+
256+
for (val activeCustomer : activeCustomers) {
257+
verify(stripeApi, times(0)).resetCustomerNameAndEmail(activeCustomer.getStripeId());
258+
}
259+
260+
activeSubscriptions.stream()
261+
.filter(s -> deletedCustomers.contains(s.getCustomer()))
262+
.map(s -> subscriptionRepository.findById(s.getId()).orElseThrow())
263+
.forEach(s -> assertFalse(s.isActive()));
264+
265+
activeSubscriptions.stream()
266+
.filter(s -> activeCustomers.contains(s.getCustomer()))
267+
.map(s -> subscriptionRepository.findById(s.getId()).orElseThrow())
268+
.forEach(s -> assertTrue(s.isActive()));
269+
}
270+
216271
@NonNull
217272
private Subscription buildSubscription(
218-
@NonNull AuthUser owner,
273+
@NonNull Customer customer,
219274
@NonNull SubscriptionPlan plan,
220275
boolean isActive,
221276
boolean isPaymentPending,
222277
String providedId
223278
) {
224279
return subscriptionRepository.save(
225280
Subscription.builder()
226-
.customer(
227-
customerRepository.save(
228-
Customer.builder()
229-
.userId(owner.getId())
230-
.build()))
281+
.customer(customer)
231282
.plan(plan)
232283
.providedId(providedId)
233284
.isPaymentPending(isPaymentPending)
@@ -236,6 +287,15 @@ private Subscription buildSubscription(
236287
.build());
237288
}
238289

290+
@NonNull
291+
private Customer buildCustomer(@NonNull AuthUser user, String stripeCustomerId) {
292+
return customerRepository.save(
293+
Customer.builder()
294+
.userId(user.getId())
295+
.stripeId(stripeCustomerId)
296+
.build());
297+
}
298+
239299
@NonNull
240300
private static GooglePlayDeveloperNotification buildGooglePlayDeveloperNotification(int type, @NonNull String purchaseToken) {
241301
return GooglePlayDeveloperNotification.builder()

0 commit comments

Comments
 (0)