Skip to content

Commit 3ae1fa6

Browse files
committed
Test: Mongo delete outbox 전환 테스트 반영 (retry/maxRetry/done 경로 테스트 보강)
1 parent 27e16ed commit 3ae1fa6

8 files changed

Lines changed: 218 additions & 265 deletions

File tree

src/test/java/io/ejangs/docsa/domain/branch/app/BranchServiceTest.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@
1818
import io.ejangs.docsa.global.exception.CustomException;
1919
import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode;
2020
import io.ejangs.docsa.global.mongo.deletion.dto.MongoIdsDto;
21+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.DomainType;
22+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.OriginType;
23+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.TriggerType;
24+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutboxFactory;
2125
import org.junit.jupiter.api.DisplayName;
2226
import org.junit.jupiter.api.Test;
2327
import org.junit.jupiter.api.extension.ExtendWith;
2428
import org.mockito.ArgumentCaptor;
2529
import org.mockito.InjectMocks;
2630
import org.mockito.Mock;
2731
import org.mockito.junit.jupiter.MockitoExtension;
28-
import org.springframework.context.ApplicationEventPublisher;
2932
import org.springframework.test.util.ReflectionTestUtils;
3033

3134
import java.util.List;
@@ -52,15 +55,15 @@ class BranchServiceTest {
5255
@Mock
5356
private CommitBlockSequenceRepository commitBlockSequenceRepository;
5457

55-
@Mock
56-
private ApplicationEventPublisher eventPublisher;
57-
5858
@Mock
5959
private BranchQueryService branchQueryService;
6060

6161
@Mock
6262
private BranchCreateOrchestrator branchCreateOrchestrator;
6363

64+
@Mock
65+
private MongoDeleteOutboxFactory mongoDeleteOutboxFactory;
66+
6467
@Test
6568
@DisplayName("leaf이면서 root이면서 save가 존재하는 상황에서 브랜치를 이미 있는 이름으로 생성 시 - BRANCH_NAME_DUPLICATED")
6669
void testAddSaveToExistingBranch() {
@@ -273,9 +276,15 @@ void deleteBranch_success() {
273276
// then - 브랜치 실제 삭제
274277
verify(branchQueryService).delete(branch);
275278

276-
// 이벤트 발행 검증
279+
// Outbox 적재 검증
277280
ArgumentCaptor<MongoIdsDto> captor = ArgumentCaptor.forClass(MongoIdsDto.class);
278-
verify(eventPublisher).publishEvent(captor.capture());
281+
verify(mongoDeleteOutboxFactory).create(
282+
eq(TriggerType.DELETE),
283+
eq(DomainType.BRANCH),
284+
eq(OriginType.BRANCH_ID),
285+
eq(branchId),
286+
captor.capture()
287+
);
279288

280289
MongoIdsDto emitted = captor.getValue();
281290
assertEquals(List.of("seq1", "seq2"), emitted.commitBlockSequenceIds());

src/test/java/io/ejangs/docsa/domain/commit/app/CommitCreateOrchestratorTest.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.ArgumentMatchers.eq;
57
import static org.mockito.Mockito.never;
68
import static org.mockito.Mockito.verify;
79
import static org.mockito.Mockito.when;
@@ -13,7 +15,10 @@
1315
import io.ejangs.docsa.domain.commit.dto.request.CreateCommitRequest;
1416
import io.ejangs.docsa.domain.commit.entity.Commit;
1517
import io.ejangs.docsa.domain.doc.entity.Doc;
16-
import io.ejangs.docsa.global.mongo.deletion.app.MongoDeleteRetryService;
18+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.DomainType;
19+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.OriginType;
20+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.TriggerType;
21+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutboxFactory;
1722
import io.ejangs.docsa.global.mongo.deletion.dto.MongoIdsDto;
1823
import java.util.Collections;
1924
import org.junit.jupiter.api.DisplayName;
@@ -33,7 +38,7 @@ class CommitCreateOrchestratorTest {
3338
private CommitMongoTxService commitMongoTxService;
3439

3540
@Mock
36-
private MongoDeleteRetryService mongoDeleteRetryService;
41+
private MongoDeleteOutboxFactory mongoDeleteOutboxFactory;
3742

3843
@InjectMocks
3944
private CommitCreateOrchestrator orchestrator;
@@ -53,7 +58,7 @@ void create_success() {
5358
Commit result = orchestrator.create(request, "base-cbs", doc, branch);
5459

5560
assertThat(result).isEqualTo(commit);
56-
verify(mongoDeleteRetryService, never()).deleteMongoData(ids);
61+
verify(mongoDeleteOutboxFactory, never()).create(any(), any(), any(), any(String.class), any());
5762
}
5863

5964
@Test
@@ -72,6 +77,12 @@ void create_fail_compensateMongo() {
7277
.isInstanceOf(RuntimeException.class)
7378
.hasMessage("mysql fail");
7479

75-
verify(mongoDeleteRetryService).deleteMongoData(ids);
80+
verify(mongoDeleteOutboxFactory).create(
81+
eq(TriggerType.COMPENSATE),
82+
eq(DomainType.COMMIT),
83+
eq(OriginType.CBS_ID),
84+
eq("cbs-1"),
85+
eq(ids)
86+
);
7687
}
7788
}

src/test/java/io/ejangs/docsa/domain/commit/app/CreateCommitIntegrationTest.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import io.ejangs.docsa.domain.user.entity.User;
3232
import io.ejangs.docsa.global.exception.CustomException;
3333
import io.ejangs.docsa.global.exception.errorcode.BlockSequenceErrorCode;
34+
import io.ejangs.docsa.global.mongo.deletion.dao.mysql.MongoDeleteOutboxRepository;
35+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox;
3436
import org.junit.jupiter.api.BeforeEach;
3537
import org.junit.jupiter.api.DisplayName;
3638
import org.junit.jupiter.api.Nested;
@@ -77,6 +79,9 @@ public class CreateCommitIntegrationTest {
7779
@Autowired
7880
private BlockRepository blockRepository;
7981

82+
@Autowired
83+
private MongoDeleteOutboxRepository mongoDeleteOutboxRepository;
84+
8085
private User testUser;
8186
private Doc testDoc;
8287
private Branch baseBranch;
@@ -209,10 +214,11 @@ class MySqlFailureCompensationTest {
209214
private CommitMySqlTxService commitMySqlTxService;
210215

211216
@Test
212-
@DisplayName("Mongo 성공 후 MySQL 실패 시 생성된 block/CBS는 보상 삭제된다")
217+
@DisplayName("Mongo 성공 후 MySQL 실패 시 보상 Outbox가 생성된다")
213218
void mysqlFail_compensateMongoDelete() {
214219
long beforeCbsCount = cbsRepository.count();
215220
long beforeBlockCount = blockRepository.count();
221+
long beforeOutboxCount = mongoDeleteOutboxRepository.count();
216222

217223
CreateCommitRequest request =
218224
new CreateCommitRequest("mysql fail", "description", baseBranch.getId(), blocks, blockOrders);
@@ -223,8 +229,18 @@ void mysqlFail_compensateMongoDelete() {
223229
assertThatThrownBy(() -> commitService.createCommit(testDoc.getId(), request, testUser.getId()))
224230
.isInstanceOf(RuntimeException.class);
225231

226-
assertThat(cbsRepository.count()).isEqualTo(beforeCbsCount);
227-
assertThat(blockRepository.count()).isEqualTo(beforeBlockCount);
232+
assertThat(cbsRepository.count()).isEqualTo(beforeCbsCount + 1);
233+
assertThat(blockRepository.count()).isEqualTo(beforeBlockCount + blocks.size());
234+
assertThat(mongoDeleteOutboxRepository.count()).isEqualTo(beforeOutboxCount + 1);
235+
236+
assertThat(mongoDeleteOutboxRepository.findAll())
237+
.anySatisfy(outbox -> {
238+
assertThat(outbox.getTriggerType()).isEqualTo(MongoDeleteOutbox.TriggerType.COMPENSATE);
239+
assertThat(outbox.getDomainType()).isEqualTo(MongoDeleteOutbox.DomainType.COMMIT);
240+
assertThat(outbox.getOriginType()).isEqualTo(MongoDeleteOutbox.OriginType.CBS_ID);
241+
assertThat(outbox.getStatus()).isEqualTo(MongoDeleteOutbox.OutboxStatus.OPEN);
242+
assertThat(outbox.getOriginId()).isNotBlank();
243+
});
228244
}
229245
}
230246

src/test/java/io/ejangs/docsa/domain/doc/integration/DocServiceIntegrationTests.java

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5-
import static org.awaitility.Awaitility.await;
65
import static org.junit.jupiter.api.Assertions.assertEquals;
76
import static org.junit.jupiter.api.Assertions.assertThrows;
87
import static org.junit.jupiter.api.Assertions.assertTrue;
98
import static org.mockito.ArgumentMatchers.any;
109
import static org.mockito.ArgumentMatchers.anyString;
11-
import static org.mockito.Mockito.doThrow;
1210
import static org.mockito.Mockito.when;
1311

1412
import com.mongodb.MongoTimeoutException;
@@ -182,13 +180,15 @@ class MySqlFailureTest {
182180
@Autowired
183181
private SaveContentRepository saveContentRepository;
184182

183+
@Autowired
184+
private MongoDeleteOutboxRepository mongoDeleteOutboxRepository;
185+
185186
@MockitoBean
186187
private DocCreateMySqlTxService docCreateMySqlTxService; // MySQL 파트만 실패 유도
187188

188189
@Test
189-
@DisplayName("MySQL 생성 실패 시 Mongo에 먼저 생성된 SaveContent는 보상 삭제된다")
190-
@Transactional(propagation = Propagation.NOT_SUPPORTED)
191-
void mysqlFail_compensateMongoDelete() {
190+
@DisplayName("MySQL 생성 실패 시 Mongo 보상 Outbox가 적재된다")
191+
void mysqlFail_createCompensateOutbox() {
192192
// given
193193
User user = userRepository.save(DocTestUtils.createUser());
194194
DocTitleRequest request = new DocTitleRequest("MySQL 실패 케이스");
@@ -203,13 +203,21 @@ void mysqlFail_compensateMongoDelete() {
203203
.isInstanceOf(RuntimeException.class)
204204
.hasMessageContaining("MySQL 생성 실패");
205205

206-
// 보상 삭제 결과: 이번 요청에서 생성된 SaveContent는 남지 않아야 함
207-
assertThat(saveContentRepository.count()).isEqualTo(beforeSaveContentCount);
206+
// Mongo에 저장된 데이터는 비동기 워커가 삭제하므로 즉시 1건 증가 상태다.
207+
assertThat(saveContentRepository.count()).isEqualTo(beforeSaveContentCount + 1);
208+
209+
List<MongoDeleteOutbox> outboxes = mongoDeleteOutboxRepository.findAll();
210+
assertThat(outboxes).hasSize(1);
211+
MongoDeleteOutbox outbox = outboxes.getFirst();
212+
assertThat(outbox.getTriggerType()).isEqualTo(MongoDeleteOutbox.TriggerType.COMPENSATE);
213+
assertThat(outbox.getDomainType()).isEqualTo(MongoDeleteOutbox.DomainType.DOC);
214+
assertThat(outbox.getOriginType()).isEqualTo(MongoDeleteOutbox.OriginType.SAVE_CONTENT_ID);
215+
assertThat(outbox.getStatus()).isEqualTo(MongoDeleteOutbox.OutboxStatus.OPEN);
208216
}
209217
}
210218
@Nested
211-
@DisplayName("MySQL 실패 + 보상 삭제 3회 실패")
212-
class MySqlFailureWithCompensateFailureTest {
219+
@DisplayName("MySQL 실패 보상 Outbox 상태")
220+
class MySqlFailureWithCompensateOutboxTest {
213221

214222
@Autowired
215223
private DocService docService;
@@ -223,13 +231,9 @@ class MySqlFailureWithCompensateFailureTest {
223231
@MockitoBean
224232
private DocCreateMySqlTxService docCreateMySqlTxService;
225233

226-
@MockitoBean
227-
private CommitBlockSequenceRepository mockedCommitBlockSequenceRepository;
228-
229234
@Test
230-
@DisplayName("보상 삭제가 3회 모두 실패하면 MongoDeleteFailure가 저장된다")
231-
@Transactional(propagation = Propagation.NOT_SUPPORTED)
232-
void mysqlFail_and_compensateFail_storeFailure() {
235+
@DisplayName("MySQL 생성 실패 시 보상 Outbox는 OPEN 상태로 저장된다")
236+
void mysqlFail_storeOpenOutbox() {
233237

234238
// given
235239
User user = userRepository.save(DocTestUtils.createUser());
@@ -239,23 +243,15 @@ void mysqlFail_and_compensateFail_storeFailure() {
239243
when(docCreateMySqlTxService.createMySqlPart(any(), any(), anyString()))
240244
.thenThrow(new RuntimeException("MySQL 생성 실패"));
241245

242-
// 보상 삭제 실패 유도 (Retry + Recover 경로를 실제로 타게 함)
243-
doThrow(new RuntimeException("보상 삭제 실패"))
244-
.when(mockedCommitBlockSequenceRepository).deleteAllById(any());
245-
246-
// when
246+
// when & then
247247
assertThatThrownBy(() -> docService.create(request, user.getId()))
248248
.isInstanceOf(RuntimeException.class)
249249
.hasMessageContaining("MySQL 생성 실패");
250250

251-
// then (비동기 or recover 고려)
252-
await().untilAsserted(() -> {
253-
List<MongoDeleteOutbox> failures =
254-
mongoDeleteOutboxRepository.findAll();
255-
256-
assertThat(failures).hasSize(1);
257-
assertThat(failures.get(0).getResolved()).isFalse();
258-
});
251+
List<MongoDeleteOutbox> outboxes = mongoDeleteOutboxRepository.findAll();
252+
assertThat(outboxes).hasSize(1);
253+
assertThat(outboxes.getFirst().getStatus()).isEqualTo(MongoDeleteOutbox.OutboxStatus.OPEN);
254+
assertThat(outboxes.getFirst().getRetryCount()).isEqualTo(0);
259255
}
260256
}
261257

src/test/java/io/ejangs/docsa/domain/doc/unit/DocCreateOrchestratorTest.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.ArgumentMatchers.anyString;
7+
import static org.mockito.ArgumentMatchers.argThat;
8+
import static org.mockito.ArgumentMatchers.eq;
69
import static org.mockito.Mockito.never;
710
import static org.mockito.Mockito.verify;
811
import static org.mockito.Mockito.when;
@@ -13,7 +16,10 @@
1316
import io.ejangs.docsa.domain.save.app.SaveQueryService;
1417
import io.ejangs.docsa.domain.save.document.SaveContent;
1518
import io.ejangs.docsa.domain.user.entity.User;
16-
import io.ejangs.docsa.global.mongo.deletion.app.MongoDeleteRetryService;
19+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.DomainType;
20+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.OriginType;
21+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutbox.TriggerType;
22+
import io.ejangs.docsa.global.mongo.deletion.entity.MongoDeleteOutboxFactory;
1723
import org.junit.jupiter.api.DisplayName;
1824
import org.junit.jupiter.api.Test;
1925
import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,7 +38,7 @@ class DocCreateOrchestratorTest {
3238
private DocCreateMySqlTxService docCreateMySqlTxService;
3339

3440
@Mock
35-
private MongoDeleteRetryService mongoDeleteRetryService;
41+
private MongoDeleteOutboxFactory mongoDeleteOutboxFactory;
3642

3743
@InjectMocks
3844
private DocCreateOrchestrator orchestrator;
@@ -51,7 +57,7 @@ void create_success() {
5157
DocCreateResponse result = orchestrator.create("doc", user);
5258

5359
assertThat(result).isEqualTo(expected);
54-
verify(mongoDeleteRetryService, never()).deleteMongoData(any());
60+
verify(mongoDeleteOutboxFactory, never()).create(any(), any(), any(), anyString(), any());
5561
}
5662

5763
@Test
@@ -69,6 +75,15 @@ void create_fail_compensateMongo() {
6975
.isInstanceOf(RuntimeException.class)
7076
.hasMessage("mysql fail");
7177

72-
verify(mongoDeleteRetryService).deleteMongoData(any());
78+
verify(mongoDeleteOutboxFactory).create(
79+
eq(TriggerType.COMPENSATE),
80+
eq(DomainType.DOC),
81+
eq(OriginType.SAVE_CONTENT_ID),
82+
eq("save-1"),
83+
argThat(ids ->
84+
ids.saveContentsIds().contains("save-1")
85+
&& ids.commitBlockSequenceIds().isEmpty()
86+
&& ids.blockIds().isEmpty())
87+
);
7388
}
7489
}

0 commit comments

Comments
 (0)