feat(s4): controller integration tests — 11 TransferController ITs (STA-144)#148
Conversation
…TA-144)
Add full-stack integration tests for all S4 controller endpoints using
TestContainers PostgreSQL, covering POST /v1/transfers (202, 200 idempotent,
400 missing fields, 400 invalid amount, 503 insufficient balance, balance
reservation verification), GET /v1/transfers/{id} (200, 404, 400), and
GET /v1/wallets/{id}/balance (200, 404). Also adds V5 migration to drop
chain_selection_log FK that prevented the selection log from being written
before the transfer row exists.
Closes STA-144
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR introduces an integration test suite for the Transfer REST API endpoints with database-backed scenarios, a database migration removing a problematic foreign key constraint on the chain_selection_log table, and a test fixture for generating transfer request payloads. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@blockchain-custody/blockchain-custody/src/integration-test/java/com/stablecoin/payments/custody/application/controller/TransferControllerIT.java`:
- Around line 81-103: The test in TransferControllerIT currently only checks
paymentId and status, allowing duplicate ChainTransfer creation; modify the test
to capture the first response's transferId (from the first mockMvc.perform POST)
using andReturn()/getResponse().getContentAsString() or by querying the transfer
repository (e.g., chainTransferRepository.findByPaymentId(PAYMENT_ID)), then on
the second (idempotent) request assert the returned transferId equals the first
transferId and additionally assert the repository contains exactly one
ChainTransfer for that paymentId (e.g., countByPaymentId or findAllByPaymentId
size == 1) to ensure no duplicate transfer was created.
In
`@blockchain-custody/blockchain-custody/src/testFixtures/java/com/stablecoin/payments/custody/fixtures/ChainTransferFixtures.java`:
- Around line 27-42: The fixture aTransferRequestJson currently hardcodes
PAYMENT_ID and CORRELATION_ID which forces tests to share IDs; add an overloaded
method (e.g., aTransferRequestJson(String paymentId, String correlationId,
String toWalletAddress)) or a small builder helper (e.g.,
TransferRequestFixture.with(paymentId, correlationId, toWalletAddress).json())
so tests can supply mutable fields while keeping the original parameterless
aTransferRequestJson() as a convenience that delegates to the new overload using
PAYMENT_ID, CORRELATION_ID, TO_ADDRESS; update callers to use the overload where
tests need custom IDs and retain the original for explicit replay scenarios.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c935e815-55f8-4ec6-b888-c2e8686f13c6
📒 Files selected for processing (3)
blockchain-custody/blockchain-custody/src/integration-test/java/com/stablecoin/payments/custody/application/controller/TransferControllerIT.javablockchain-custody/blockchain-custody/src/main/resources/db/migration/V5__drop_selection_log_fk.sqlblockchain-custody/blockchain-custody/src/testFixtures/java/com/stablecoin/payments/custody/fixtures/ChainTransferFixtures.java
| @Test | ||
| @DisplayName("should return 200 OK for idempotent replay with same paymentId") | ||
| void shouldReturn200ForIdempotentReplay() throws Exception { | ||
| // given | ||
| var wallet = walletRepository.save(anActiveWallet()); | ||
| var balance = WalletBalance.initialize(wallet.walletId(), CHAIN_BASE, USDC) | ||
| .syncFromChain(new BigDecimal("50000.00"), 100L); | ||
| walletBalanceRepository.save(balance); | ||
|
|
||
| // first request — 202 Accepted | ||
| mockMvc.perform(post("/v1/transfers") | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(aTransferRequestJson())) | ||
| .andExpect(status().isAccepted()); | ||
|
|
||
| // when — second request with same paymentId | ||
| mockMvc.perform(post("/v1/transfers") | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(aTransferRequestJson())) | ||
| .andExpect(status().isOk()) | ||
| .andExpect(jsonPath("$.paymentId", is(PAYMENT_ID.toString()))) | ||
| .andExpect(jsonPath("$.status", is("SUBMITTED"))); | ||
| } |
There was a problem hiding this comment.
Assert replay identity, not just HTTP 200.
A broken idempotency path could still create a second ChainTransfer and pass this test because the assertions only check paymentId and status. Capture the first transferId and assert the replay returns that same ID, or assert the repository still contains a single row for the request.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@blockchain-custody/blockchain-custody/src/integration-test/java/com/stablecoin/payments/custody/application/controller/TransferControllerIT.java`
around lines 81 - 103, The test in TransferControllerIT currently only checks
paymentId and status, allowing duplicate ChainTransfer creation; modify the test
to capture the first response's transferId (from the first mockMvc.perform POST)
using andReturn()/getResponse().getContentAsString() or by querying the transfer
repository (e.g., chainTransferRepository.findByPaymentId(PAYMENT_ID)), then on
the second (idempotent) request assert the returned transferId equals the first
transferId and additionally assert the repository contains exactly one
ChainTransfer for that paymentId (e.g., countByPaymentId or findAllByPaymentId
size == 1) to ensure no duplicate transfer was created.
| /** | ||
| * JSON body for a FORWARD transfer request on Base chain. | ||
| */ | ||
| public static String aTransferRequestJson() { | ||
| return """ | ||
| { | ||
| "paymentId": "%s", | ||
| "correlationId": "%s", | ||
| "transferType": "FORWARD", | ||
| "stablecoin": "USDC", | ||
| "amount": "1000.00", | ||
| "toWalletAddress": "%s", | ||
| "preferredChain": "base" | ||
| } | ||
| """.formatted(PAYMENT_ID, CORRELATION_ID, TO_ADDRESS); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Parameterize this request fixture.
This helper bakes the same paymentId and correlationId into every POST scenario. That makes unrelated controller ITs depend on perfect DB cleanup and forces payload duplication when one field needs to vary. Prefer an overload or builder that accepts the mutable fields, and keep the fixed IDs only for the explicit replay case.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@blockchain-custody/blockchain-custody/src/testFixtures/java/com/stablecoin/payments/custody/fixtures/ChainTransferFixtures.java`
around lines 27 - 42, The fixture aTransferRequestJson currently hardcodes
PAYMENT_ID and CORRELATION_ID which forces tests to share IDs; add an overloaded
method (e.g., aTransferRequestJson(String paymentId, String correlationId,
String toWalletAddress)) or a small builder helper (e.g.,
TransferRequestFixture.with(paymentId, correlationId, toWalletAddress).json())
so tests can supply mutable fields while keeping the original parameterless
aTransferRequestJson() as a convenience that delegates to the new overload using
PAYMENT_ID, CORRELATION_ID, TO_ADDRESS; update callers to use the overload where
tests need custom IDs and retain the original for explicit replay scenarios.
…o duplicates (STA-144) Capture first response transferId via andReturn(), assert second response returns the same transferId, and verify repository contains exactly one transfer for the paymentId. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
POST /v1/transfers,GET /v1/transfers/{id},GET /v1/wallets/{id}/balance)chain_selection_log.transfer_id → chain_transfers— selection log is written before the transfer row exists, so the FK prevented the full submission pipelineaTransferRequestJson()fixture for raw JSON transfer request body (testFixtures can't depend on the API module)Test coverage
POST /v1/transfersGET /v1/transfers/{id}GET /v1/wallets/{id}/balanceTotal: 408 tests across S4 (355 unit + 53 integration), all passing.
Closes STA-144
Test plan
./gradlew :blockchain-custody:blockchain-custody:test :blockchain-custody:blockchain-custody:integrationTest)🤖 Generated with Claude Code
Summary by CodeRabbit
Tests
Chores