Skip to content

SOL-38: add end-to-end integration tests for both stream adapters#104

Merged
Puneethkumarck merged 3 commits intomainfrom
feature/SOL-38-e2e-integration-test
Apr 11, 2026
Merged

SOL-38: add end-to-end integration tests for both stream adapters#104
Puneethkumarck merged 3 commits intomainfrom
feature/SOL-38-e2e-integration-test

Conversation

@Puneethkumarck
Copy link
Copy Markdown
Owner

@Puneethkumarck Puneethkumarck commented Apr 11, 2026

SOL Issue

Closes #40

Summary

Phase 7 end-to-end coverage: the indexer pipeline is now exercised for both stream adapters against a real Testcontainers Postgres via the real Helidon WebServer, with a shared canonical block fixture and an explicit parser-parity proof. No production code changes — the entire deliverable is test code plus one testFixtures build dependency and two testFixtures bridge classes.

Context

The Phase 6 lifecycle wiring (SOL-36/SOL-37, #101) stopped short of driving real protobuf / JSON through the pipeline — IndexerApplicationIntegrationTest intentionally uses a RecordingTransactionStream stub. SOL-38 is the missing piece: prove that both YellowstoneTransactionStream (gRPC) and WebSocketTransactionStream (WebSocket) take the same logical transaction all the way from raw wire format to a REST response, producing identical domain objects along the way.

Approach

One canonical 3-transaction logical block is encoded twice by E2eBlockFixture:

Transaction Purpose Expected landing tables
5 SOL transfer (accountKeys=[feePayer, sender, receiver]) Large transfer transactions, large_transfers, accounts
Failed transfer with meta.err set Failure path failed_transactions, accounts
0.1 SOL transfer with top-level Memo v1 instruction Memo (below large threshold) transactions, memos, accounts

The fixture exposes the block as both:

  • List<SubscribeUpdateTransaction> (protobuf) for the gRPC adapter, and
  • a full blockNotification JSON-RPC frame for the WebSocket adapter (built via Jackson ObjectNode).

It also exposes expectedDomainTransactions() / expectedDomainFeePayers() so parser-parity and pipeline assertions build against the same source of truth. Balance math was engineered so SolanaBalanceMath.compute picks the intended sender/receiver indices in each scenario, and fee payer pubkeys are distinct across all three transactions so accounts stores three rows.

Changes

  • prism/src/testFixtures/.../fixtures/E2eBlockFixture.java — canonical block fixture. Public methods: grpcUpdates(), webSocketFrame(), webSocketBlockValue(), expectedDomainTransactions(), expectedDomainFeePayers(). Public constants for every signature / pubkey / lamport balance used in assertions.
  • prism/src/testFixtures/.../application/TestIndexerApplication.java — tiny bridge class in the application package exposing a public start(config, writePool, readPool, stream) that delegates to the package-private IndexerApplication.start. Lets integration tests live in any package without opening production API visibility.
  • prism/src/test/.../e2e/AdapterParityTest.java — unit-level proof (no DB, no server) that TransactionParser and BlockNotificationParser produce identical SolanaTransaction and Account outputs for the canonical block. Four tests: transactions parity, fee payers parity, success/failure classification parity, distinct fee payers. Satisfies the "both adapters produce identical domain objects" acceptance criterion explicitly.
  • prism/src/integrationTest/.../e2e/GrpcEndToEndIntegrationTest.java — full pipeline via InProcessServerBuilder + an inline TestGeyserService that captures the StreamObserver and forwards SubscribeUpdate messages. @BeforeAll starts SharedPostgresContainer, boots TestIndexerApplication.start() against the real YellowstoneTransactionStream, waits for the in-process stream to bind, pushes the three canonical updates, and waits via Awaitility until each table has the expected row count. Ten @Test methods then verify via a real java.net.http.HttpClient:
    • /api/transactions?limit=50 — paginated list of two successful txs
    • /api/transactions/{sig} for the large transfer
    • /api/transactions/{sig} for the memo tx
    • /api/slots/{slot} — both successful txs at the shared slot
    • /api/transfers?min_amount=0 — the one large transfer
    • /api/memos — the one memo with the expected text
    • /api/accounts/{pubkey} for each of the three fee payers
    • direct JDBC read of failed_transactions (no REST route) asserted via a local record + usingRecursiveComparison().
  • prism/src/integrationTest/.../infrastructure/websocket/WebSocketEndToEndIntegrationTest.java — same assertion matrix, same canonical block, same TestIndexerApplication.start(). Lives in the infrastructure.websocket package to reach the package-private WebSocketFactory interface and the 6-arg WebSocketTransactionStream constructor (same trick the existing WebSocketTransactionStreamTest uses). The scripted factory captures the WebSocket.Listener, listener.onText(...) pushes E2eBlockFixture.webSocketFrame() into the real stream's virtual-thread loop, and downstream verification is identical to the gRPC test.
  • prism/build.gradle.kts — adds jackson-bom + jackson-databind to testFixturesImplementation so the fixture can build the WebSocket JSON frame via ObjectMapper. Jackson is already a production runtime dependency; this only opens the test-fixture compile classpath.

Architecture notes

  • No production code changes. IndexerApplication.start stays package-private; WebSocketFactory and the 6-arg WebSocketTransactionStream constructor stay package-private. The testFixtures TestIndexerApplication bridge keeps the production API surface unchanged while letting test classes in any package boot the full pipeline.
  • Parser parity is proven twice. Once explicitly in AdapterParityTest at the unit level (fast, no DB), and once implicitly in each pipeline test by asserting against the same expectedDomainTransactions() / expectedDomainFeePayers() source of truth. Divergence in either adapter fails both the parity test and at least one pipeline assertion.
  • Balance math is deterministic. Each logical tx was designed so SolanaBalanceMath.compute picks specific senderIndex / receiverIndex values: the large transfer has the sender at index 1 with the max decrease; the failed tx has the fee payer at index 0 as the only decrease; the memo tx has the fee payer at index 0 as both sender and max decrease. See the constants in E2eBlockFixture for the exact pre/post balances.
  • Pubkey encoding. All 32-byte test pubkeys use a single sentinel byte with leading-zero padding, producing base58 strings that stay within the Pubkey's 44-char limit. Base58 signatures are 64 bytes and stay within the Signature's 88-char limit.
  • Async settlement uses Awaitility. Each table's row count is polled with await().atMost(15, SECONDS).untilAsserted(...) — no Thread.sleep, no hardcoded delays.
  • Test isolation. Each test class truncates all tables in @BeforeAll before pushing its block, so running both tests in the same integrationTest JVM is safe. State loaded by @BeforeAll is consumed read-only by the @Test methods, so test ordering doesn't matter.

Testing conventions followed

  • Golden rule. Every object assertion builds an expected object and calls usingRecursiveComparison() once. Auto-increment id fields are excluded via ignoringFieldsMatchingRegexes(".*\\.id") and DB-populated createdAt via ignoringFieldsOfTypes(Instant.class). The failed_transactions check uses a local FailedTransactionRow record so even that direct JDBC read goes through recursive comparison.
  • BDD Mockito only. The WebSocket test uses given(stubSocket.sendText(BLOCK_SUBSCRIBE_PAYLOAD, true)).willReturn(...) with the exact expected payload — no any() / anyString().
  • // given / // when / // then markers on every test, var for every local, no comments or Javadoc.
  • Fixtures live in testFixtures/; no test-helper logic leaks into src/test/ or src/integrationTest/.

Checklist

  • ./gradlew build passes (compile + Spotless + unit + integration + ArchUnit)
  • Unit test added: AdapterParityTest (parser parity, success/failure classification, distinct fee payers)
  • Integration tests added: GrpcEndToEndIntegrationTest, WebSocketEndToEndIntegrationTest
  • ArchUnit rules unchanged and still pass (no domain-layer imports)
  • Spotless applied
  • No production code changes — testFixtures bridge classes only
  • Golden-rule usingRecursiveComparison() used for every object assertion
  • BDD Mockito with actual values only; no generic matchers

Known follow-ups (out of scope)

  • SOL-39 graceful shutdown test — separate branch, separate PR (still Phase 7).
  • Phase 7 milestone — the GitHub repo currently lists milestones for Phases 0, 1, 3, 4, 5, 6. A Phase 7: E2E & Docs milestone does not yet exist; this PR is therefore not attached to a milestone. Creating that milestone is a project-bookkeeping task, not part of this story.
  • testFixtures TestIndexerApplication bridge — if/when more tests need to boot the full indexer outside the application package, this bridge becomes the canonical entry point. Alternatively, IndexerApplication.start could be promoted to public in a future cleanup.

Summary by CodeRabbit

  • Tests

    • Added comprehensive end-to-end integration tests for gRPC and WebSocket ingestion, plus adapter parity tests to validate consistency across ingestion paths.
    • Added deterministic end-to-end fixtures and test utilities to support repeatable integration scenarios and shared test infrastructure.
  • Chores

    • Added Jackson JSON dependencies to the test runtime to support JSON serialization/deserialization in tests.

Introduce a canonical logical block fixture that encodes the same three
transactions (large transfer, failed tx, memo tx) as both protobuf
SubscribeUpdate messages and a WebSocket blockNotification JSON frame,
along with the expected domain projections.

AdapterParityTest drives both TransactionParser and BlockNotificationParser
against this fixture and proves they produce identical SolanaTransaction
and Account outputs.

Full-pipeline integration tests boot IndexerApplication against a shared
Testcontainers Postgres and verify stream -> batch -> DB -> HTTP round-trips:

- GrpcEndToEndIntegrationTest uses an in-process Geyser service feeding
  YellowstoneTransactionStream.
- WebSocketEndToEndIntegrationTest injects a scripted WebSocketFactory
  that drives the canonical JSON frame through WebSocketTransactionStream.

Both assert that successful transactions, failed transactions, memos,
large transfers, and fee payer accounts land in their respective tables
and are queryable via the REST API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck added the phase:7-e2e End-to-end tests and docs label Apr 11, 2026
@Puneethkumarck Puneethkumarck self-assigned this Apr 11, 2026
@Puneethkumarck Puneethkumarck added the phase:7-e2e End-to-end tests and docs label Apr 11, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 11, 2026

Warning

Rate limit exceeded

@Puneethkumarck has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 2 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 2 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b0f809c2-b244-4bc9-84bb-d210e306b479

📥 Commits

Reviewing files that changed from the base of the PR and between 00f672b and de91b2c.

📒 Files selected for processing (6)
  • prism/build.gradle.kts
  • prism/src/integrationTest/java/com/stablebridge/prism/e2e/GrpcEndToEndIntegrationTest.java
  • prism/src/integrationTest/java/com/stablebridge/prism/infrastructure/websocket/WebSocketEndToEndIntegrationTest.java
  • prism/src/test/java/com/stablebridge/prism/e2e/AdapterParityTest.java
  • prism/src/testFixtures/java/com/stablebridge/prism/fixtures/E2eBlockFixture.java
  • prism/src/testFixtures/java/com/stablebridge/prism/fixtures/E2ePipelineAssertions.java
📝 Walkthrough

Walkthrough

Adds end-to-end integration tests (gRPC and WebSocket), adapter-parity tests, shared test fixtures/utilities, and a small build change (Jackson added to testFixtures). Tests exercise full pipeline: stream → indexer → Postgres → REST API and include deterministic fixtures and test startup helpers.

Changes

Cohort / File(s) Summary
Build
prism/build.gradle.kts
Added platform(libs.jackson.bom) and libs.jackson.databind to testFixturesImplementation.
gRPC E2E tests
prism/src/integrationTest/java/com/stablebridge/prism/e2e/GrpcEndToEndIntegrationTest.java
New integration test that starts an in-process gRPC server (TestGeyserService), boots the indexer, waits for DB rows, and validates REST API responses and direct DB rows.
WebSocket E2E tests
prism/src/integrationTest/java/com/stablebridge/prism/infrastructure/websocket/WebSocketEndToEndIntegrationTest.java
New integration test that stubs a WebSocket stream, starts the indexer, waits for DB rows, and validates REST API responses and direct DB rows.
Adapter parity tests
prism/src/test/java/com/stablebridge/prism/e2e/AdapterParityTest.java
New tests asserting parity between gRPC and WebSocket parsers for domain transactions, fee-payers, success/failure classification, and distinct fee-payers.
Test application helper
prism/src/testFixtures/java/com/stablebridge/prism/application/TestIndexerApplication.java
New utility exposing start(IndexerConfig, DataSource, DataSource, TransactionStream) delegating to IndexerApplication.start.
E2E fixtures
prism/src/testFixtures/java/com/stablebridge/prism/fixtures/E2eBlockFixture.java
New deterministic fixture with slot/signatures/pubkeys/memo/amount constants and factory methods producing gRPC updates, WebSocket frame/JsonNode, and expected domain objects.
Test utils
prism/src/testFixtures/java/com/stablebridge/prism/testutil/SharedPostgresContainer.java
Added exported constant SHARED_DB_LOCK = "prism.e2e.shared-postgres" for shared test DB coordination.

Sequence Diagram(s)

sequenceDiagram
    actor Test as TestRunner
    participant Stream as (gRPC / WebSocket) Mock
    participant Indexer as IndexerApplication
    participant DB as Postgres
    participant API as Indexer REST API
    Test->>Stream: send fixture updates
    Stream->>Indexer: deliver transaction updates
    Indexer->>DB: write transactions / failed_transactions / memos / large_transfers / accounts
    Indexer->>API: start HTTP server (ephemeral port)
    Test->>API: HTTP GET requests
    API->>DB: query persisted data
    DB-->>API: return rows
    API-->>Test: return JSON responses
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

phase:3-streaming

Poem

🐰 I hopped through streams both grpc and ws,

Packed fixtures, signatures, and a memo to bless.
From mockled stream to DB so neat,
The API answers—oh what a treat!
Hooray for tests that make pipelines complete.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding end-to-end integration tests for both stream adapters (gRPC and WebSocket), which is the core objective of this PR.
Linked Issues check ✅ Passed The PR fully addresses both acceptance criteria from issue #40: (1) tests verify identical domain objects via AdapterParityTest and both E2E suites, and (2) full pipeline coverage is demonstrated across both gRPC and WebSocket adapters with DB and API assertions.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the PR objectives: E2eBlockFixture and TestIndexerApplication are test fixtures, both E2E test classes validate the pipeline, AdapterParityTest verifies parser consistency, and Jackson dependency adds support for test fixtures.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/SOL-38-e2e-integration-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@prism/src/integrationTest/java/com/stablebridge/prism/e2e/GrpcEndToEndIntegrationTest.java`:
- Around line 69-73: The test invokes truncateTables against the shared Postgres
fixture in startPipelineAndDispatchBlock (calling
SharedPostgresContainer.writePool()/readPool()), which can race with other
suites using the same DB; change this by either isolating this suite to its own
schema/database in SharedPostgresContainer (so startPipelineAndDispatchBlock
truncates only its schema) or by applying a cross-class lock/non-parallel
execution annotation (e.g., add a common `@ResourceLock` or disable
parallelization) to this class and the WebSocket suite so truncateTables cannot
run concurrently; update references in startPipelineAndDispatchBlock and any
other tests that call truncateTables to use the chosen approach.

In
`@prism/src/integrationTest/java/com/stablebridge/prism/infrastructure/websocket/WebSocketEndToEndIntegrationTest.java`:
- Around line 121-405: The tests duplicate assertion logic that also exists in
GrpcEndToEndIntegrationTest; extract the shared assertion matrix into reusable
helpers on testFixtures and call them from WebSocketEndToEndIntegrationTest.
Create helper methods such as
assertTransactionListPage(Page<TransactionResponse> actual),
assertTransactionBySignature(TransactionResponse actual, String
expectedSignature), assertSlotTransactions(List<TransactionResponse> actual,
long slot), assertTransfersPage(Page<TransferResponse> actual),
assertMemosPage(Page<MemoResponse> actual),
assertAccountResponse(AccountResponse actual, String pubkey, long lamports), and
assertFailedTransactionRows(List<FailedTransactionRow> actual) that encapsulate
the recursiveComparison settings (ignoring Instant.class, id regexes, collection
order where used) and expected builders used in methods like
shouldExposeSuccessfulTransactionsViaListEndpoint,
shouldReturnLargeTransferTransactionBySignature,
shouldReturnBothSuccessfulTransactionsForSlot,
shouldReturnLargeTransferViaTransfersEndpoint, shouldReturnMemoViaMemosEndpoint,
shouldReturnLargeTransferFeePayerAccount and shouldPersistFailedTransactionRow;
then replace the inline assertions in those test methods with calls to the new
testFixtures helpers so both WebSocketEndToEndIntegrationTest and
GrpcEndToEndIntegrationTest reuse the same contract checks.

In
`@prism/src/testFixtures/java/com/stablebridge/prism/fixtures/E2eBlockFixture.java`:
- Around line 31-54: The public mutable byte[] fixtures
(LARGE_TRANSFER_SIGNATURE_BYTES, FAILED_TX_SIGNATURE_BYTES,
MEMO_TX_SIGNATURE_BYTES, LARGE_TRANSFER_FEE_PAYER_BYTES,
FAILED_TX_FEE_PAYER_BYTES, MEMO_TX_FEE_PAYER_BYTES,
FAILED_TX_OTHER_PUBKEY_BYTES, SENDER_PUBKEY_BYTES, RECEIVER_PUBKEY_BYTES) must
be made private static final and replaced with public accessor methods that
return clones (e.g., getLargeTransferSignatureBytes() returns
LARGE_TRANSFER_SIGNATURE_BYTES.clone()); keep the existing Base58 constants
computed from the private arrays (or compute them once from the private arrays)
so the Base58 values remain stable, and update any callers (like grpcUpdates())
to use the new getters to avoid exposing internal mutable arrays.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c275e489-fd60-498b-bf03-14755658090e

📥 Commits

Reviewing files that changed from the base of the PR and between 778bde2 and c5035c7.

📒 Files selected for processing (6)
  • prism/build.gradle.kts
  • prism/src/integrationTest/java/com/stablebridge/prism/e2e/GrpcEndToEndIntegrationTest.java
  • prism/src/integrationTest/java/com/stablebridge/prism/infrastructure/websocket/WebSocketEndToEndIntegrationTest.java
  • prism/src/test/java/com/stablebridge/prism/e2e/AdapterParityTest.java
  • prism/src/testFixtures/java/com/stablebridge/prism/application/TestIndexerApplication.java
  • prism/src/testFixtures/java/com/stablebridge/prism/fixtures/E2eBlockFixture.java

Comment on lines +31 to +54
public static final byte[] LARGE_TRANSFER_SIGNATURE_BYTES = sigBytes((byte) 0xA1);
public static final byte[] FAILED_TX_SIGNATURE_BYTES = sigBytes((byte) 0xB2);
public static final byte[] MEMO_TX_SIGNATURE_BYTES = sigBytes((byte) 0xC3);

public static final String LARGE_TRANSFER_SIGNATURE_BASE58 =
Base58.encode(LARGE_TRANSFER_SIGNATURE_BYTES);
public static final String FAILED_TX_SIGNATURE_BASE58 = Base58.encode(FAILED_TX_SIGNATURE_BYTES);
public static final String MEMO_TX_SIGNATURE_BASE58 = Base58.encode(MEMO_TX_SIGNATURE_BYTES);

public static final byte[] LARGE_TRANSFER_FEE_PAYER_BYTES = pubkeyBytes((byte) 0x41);
public static final byte[] FAILED_TX_FEE_PAYER_BYTES = pubkeyBytes((byte) 0x52);
public static final byte[] MEMO_TX_FEE_PAYER_BYTES = pubkeyBytes((byte) 0x63);
public static final byte[] FAILED_TX_OTHER_PUBKEY_BYTES = pubkeyBytes((byte) 0x74);
public static final byte[] SENDER_PUBKEY_BYTES = pubkeyBytes((byte) 0x85);
public static final byte[] RECEIVER_PUBKEY_BYTES = pubkeyBytes((byte) 0x96);

public static final String LARGE_TRANSFER_FEE_PAYER_BASE58 =
Base58.encode(LARGE_TRANSFER_FEE_PAYER_BYTES);
public static final String FAILED_TX_FEE_PAYER_BASE58 = Base58.encode(FAILED_TX_FEE_PAYER_BYTES);
public static final String MEMO_TX_FEE_PAYER_BASE58 = Base58.encode(MEMO_TX_FEE_PAYER_BYTES);
public static final String FAILED_TX_OTHER_PUBKEY_BASE58 =
Base58.encode(FAILED_TX_OTHER_PUBKEY_BYTES);
public static final String SENDER_PUBKEY_BASE58 = Base58.encode(SENDER_PUBKEY_BYTES);
public static final String RECEIVER_PUBKEY_BASE58 = Base58.encode(RECEIVER_PUBKEY_BYTES);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the canonical byte fixtures immutable.

These public static final byte[] fields are still mutable. Any test that writes to one of them can make grpcUpdates() emit signatures/pubkeys that no longer match the precomputed *_BASE58 constants, which is a subtle source of cross-test flakiness. Prefer private arrays plus clone-returning accessors if raw bytes need to stay externally visible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@prism/src/testFixtures/java/com/stablebridge/prism/fixtures/E2eBlockFixture.java`
around lines 31 - 54, The public mutable byte[] fixtures
(LARGE_TRANSFER_SIGNATURE_BYTES, FAILED_TX_SIGNATURE_BYTES,
MEMO_TX_SIGNATURE_BYTES, LARGE_TRANSFER_FEE_PAYER_BYTES,
FAILED_TX_FEE_PAYER_BYTES, MEMO_TX_FEE_PAYER_BYTES,
FAILED_TX_OTHER_PUBKEY_BYTES, SENDER_PUBKEY_BYTES, RECEIVER_PUBKEY_BYTES) must
be made private static final and replaced with public accessor methods that
return clones (e.g., getLargeTransferSignatureBytes() returns
LARGE_TRANSFER_SIGNATURE_BYTES.clone()); keep the existing Base58 constants
computed from the private arrays (or compute them once from the private arrays)
so the Base58 values remain stable, and update any callers (like grpcUpdates())
to use the new getters to avoid exposing internal mutable arrays.

- Serialize both E2E suites on SharedPostgresContainer via @ResourceLock so
  future parallel execution cannot race their truncateTables calls.
- Make E2eBlockFixture byte[] fixtures private to prevent mutation drift
  from the precomputed Base58 constants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
`@prism/src/integrationTest/java/com/stablebridge/prism/e2e/GrpcEndToEndIntegrationTest.java`:
- Around line 101-103: The test currently only awaits
geyserService.responseObserver() and then injects updates, so it never verifies
the adapter's subscribe-handshake; modify the test to capture and await the
initial SubscribeRequest from the client before sending updates: after awaiting
geyserService.responseObserver(), wait for the TestGeyserService to record a
client request (the SubscribeRequest) — e.g., poll or await a
geyserService.receivedRequests()/nextReceivedRequest() and assert it is a
SubscribeRequest — only then call E2eBlockFixture.grpcUpdates() and
geyserService.send(...); ensure this pattern is applied to the other occurrences
referenced (around lines 437-468) so the test verifies
YellowstoneTransactionStream actually calls requestObserver.onNext(...) during
the handshake.

In
`@prism/src/integrationTest/java/com/stablebridge/prism/infrastructure/websocket/WebSocketEndToEndIntegrationTest.java`:
- Around line 73-80: The test currently bypasses the real send by directly
invoking listener.onText, so add an explicit assertion that the adapter actually
calls stubSocket.sendText(BLOCK_SUBSCRIBE_PAYLOAD, true) (use Mockito.verify or
equivalent) before you trigger the incoming text; locate the mock WebSocket
stubSocket and the BLOCK_SUBSCRIBE_PAYLOAD constant used in the WebSocketFactory
lambda and assert sendText was invoked (or verify invocation count) to ensure
WebSocketTransactionStream actually performs the subscribe handshake rather than
merely handling onText.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b49d3309-279f-49fb-855e-afacdd4df87a

📥 Commits

Reviewing files that changed from the base of the PR and between c5035c7 and 00f672b.

📒 Files selected for processing (4)
  • prism/src/integrationTest/java/com/stablebridge/prism/e2e/GrpcEndToEndIntegrationTest.java
  • prism/src/integrationTest/java/com/stablebridge/prism/infrastructure/websocket/WebSocketEndToEndIntegrationTest.java
  • prism/src/testFixtures/java/com/stablebridge/prism/fixtures/E2eBlockFixture.java
  • prism/src/testFixtures/java/com/stablebridge/prism/testutil/SharedPostgresContainer.java

Comment on lines +101 to +103
await().atMost(10, SECONDS).until(() -> geyserService.responseObserver() != null);
E2eBlockFixture.grpcUpdates().forEach(update ->
geyserService.send(SubscribeUpdate.newBuilder().setTransaction(update).build()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Capture and await the initial SubscribeRequest.

Right now the test only waits for responseObserver != null, and TestGeyserService discards every client request. The suite therefore still passes if YellowstoneTransactionStream stops calling requestObserver.onNext(...), because updates are injected directly into the response observer. That misses the adapter's subscribe-handshake path.

Minimal fix
-        await().atMost(10, SECONDS).until(() -> geyserService.responseObserver() != null);
+        await().atMost(10, SECONDS).until(() -> geyserService.firstRequest() != null);
         E2eBlockFixture.grpcUpdates().forEach(update ->
                 geyserService.send(SubscribeUpdate.newBuilder().setTransaction(update).build()));
...
     private static final class TestGeyserService extends GeyserGrpc.GeyserImplBase {
 
         private volatile StreamObserver<SubscribeUpdate> responseObserver;
+        private volatile SubscribeRequest firstRequest;
 
         `@Override`
         public StreamObserver<SubscribeRequest> subscribe(StreamObserver<SubscribeUpdate> observer) {
             this.responseObserver = observer;
             return new StreamObserver<>() {
                 `@Override`
-                public void onNext(SubscribeRequest value) {}
+                public void onNext(SubscribeRequest value) {
+                    firstRequest = value;
+                }
 
                 `@Override`
                 public void onError(Throwable t) {
                     responseObserver = null;
                 }
@@
         StreamObserver<SubscribeUpdate> responseObserver() {
             return responseObserver;
         }
+
+        SubscribeRequest firstRequest() {
+            return firstRequest;
+        }

Also applies to: 437-468

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@prism/src/integrationTest/java/com/stablebridge/prism/e2e/GrpcEndToEndIntegrationTest.java`
around lines 101 - 103, The test currently only awaits
geyserService.responseObserver() and then injects updates, so it never verifies
the adapter's subscribe-handshake; modify the test to capture and await the
initial SubscribeRequest from the client before sending updates: after awaiting
geyserService.responseObserver(), wait for the TestGeyserService to record a
client request (the SubscribeRequest) — e.g., poll or await a
geyserService.receivedRequests()/nextReceivedRequest() and assert it is a
SubscribeRequest — only then call E2eBlockFixture.grpcUpdates() and
geyserService.send(...); ensure this pattern is applied to the other occurrences
referenced (around lines 437-468) so the test verifies
YellowstoneTransactionStream actually calls requestObserver.onNext(...) during
the handshake.

Comment on lines +73 to +80
stubSocket = mock(WebSocket.class);
given(stubSocket.sendText(BLOCK_SUBSCRIBE_PAYLOAD, true))
.willReturn(CompletableFuture.completedFuture(stubSocket));
capturedListeners = new CopyOnWriteArrayList<>();
WebSocketFactory factory = (uri, listener) -> {
capturedListeners.add(listener);
return CompletableFuture.completedFuture(stubSocket);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Assert the adapter actually sends the block-subscribe frame.

This setup pushes the fixture straight into listener.onText(...), so the suite still passes if WebSocketTransactionStream regresses and never calls sendText(BLOCK_SUBSCRIBE_PAYLOAD, true). That misses a production-breaking subscribe-handshake bug in the adapter.

Minimal fix
         await().atMost(10, SECONDS).until(() -> !capturedListeners.isEmpty());
+        await().atMost(10, SECONDS).untilAsserted(
+                () -> org.mockito.BDDMockito.then(stubSocket)
+                        .should()
+                        .sendText(BLOCK_SUBSCRIBE_PAYLOAD, true));
         var listener = capturedListeners.get(0);
         listener.onText(stubSocket, E2eBlockFixture.webSocketFrame(), true);

Also applies to: 100-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@prism/src/integrationTest/java/com/stablebridge/prism/infrastructure/websocket/WebSocketEndToEndIntegrationTest.java`
around lines 73 - 80, The test currently bypasses the real send by directly
invoking listener.onText, so add an explicit assertion that the adapter actually
calls stubSocket.sendText(BLOCK_SUBSCRIBE_PAYLOAD, true) (use Mockito.verify or
equivalent) before you trigger the incoming text; locate the mock WebSocket
stubSocket and the BLOCK_SUBSCRIBE_PAYLOAD constant used in the WebSocketFactory
lambda and assert sendText was invoked (or verify invocation count) to ensure
WebSocketTransactionStream actually performs the subscribe handshake rather than
merely handling onText.

Extract an E2ePipelineAssertions helper in testFixtures so both
adapter pipelines delegate to a single set of HTTP + JDBC
verifications. Route-contract changes now live in one place.

Cache a static ObjectMapper in E2eBlockFixture so the WebSocket
frame builder avoids allocating a new mapper per call. Expose
LAMPORTS_PER_SOL to replace the magic 1_000_000_000d divisor.

Rename fixture constants to the SOME_* prefix convention used by
the rest of the fixture suite.

Consolidate the classification parity assertion into a single
recursive-comparison check on a ClassificationCounts record so
each test has exactly one when-action.

Make the WebSocketTransactionStream reference a local variable in
the @BeforeAll bootstrap since it is no longer read after setup.
Narrow the JDBC helper's catch clause from Exception to SQLException.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck merged commit 679f9f3 into main Apr 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

phase:7-e2e End-to-end tests and docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SOL-38: End-to-end integration test

1 participant