Skip to content

test: add NWC faucet integration test#535

Open
DSanich wants to merge 6 commits intogetAlby:masterfrom
DSanich:feat/jest-nwc-faucet-test
Open

test: add NWC faucet integration test#535
DSanich wants to merge 6 commits intogetAlby:masterfrom
DSanich:feat/jest-nwc-faucet-test

Conversation

@DSanich
Copy link
Contributor

@DSanich DSanich commented Feb 18, 2026

Summary

Adds a single Jest integration test that creates a wallet via the NWC faucet and verifies the balance is 10_000 sats. Per reviewer feedback, this PR focuses on the test setup for easier review.

Changes

  • src/test/helpers.tscreateTestWallet(balanceSats) helper using https://faucet.nwc.dev (inspired by Alby CLI tests)
  • src/test/nwc-faucet.test.ts — Integration test: create wallet → get balance via NWCClient.getBalance() → assert 10_000 sats
  • .gitignore — Add test-results and playwright-report

How to run

yarn test src/test/nwc-faucet.test.ts

Summary by CodeRabbit

  • Tests

    • Added end-to-end integration tests for NWC faucet functionality with balance verification.
  • Chores

    • Added test helper utilities and dedicated Jest configuration for E2E test execution.
    • Added npm script for running E2E tests.
    • Updated test path patterns to support E2E test discovery.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces end-to-end testing infrastructure for NWC faucet integration. It adds a test helper module for wallet creation with retry logic, a test suite verifying NWCClient balance functionality, separate Jest E2E configuration, and a new npm script for running E2E tests.

Changes

Cohort / File(s) Summary
Test Infrastructure
e2e/helpers.ts, e2e/nwc-faucet.test.ts
New E2E helpers module exporting TestWallet interface and createTestWallet() function with retry logic for faucet requests; new integration test verifying NWCClient balance against faucet-funded wallet.
Jest Configuration
jest.config.ts, jest.e2e.config.ts
Updated main Jest config to ignore broader /e2e/ path; new dedicated E2E Jest configuration targeting e2e test files with ts-jest preset and node environment.
Package Configuration
package.json
Added test:e2e npm script to run E2E tests using dedicated Jest E2E configuration.

Sequence Diagram

sequenceDiagram
    participant Test as E2E Test Suite
    participant Helper as Test Helper
    participant Faucet as NWC Faucet
    participant Client as NWCClient
    participant WS as WebSocket Connection

    Test->>Helper: createTestWallet(10000 sats)
    loop Retry Logic
        Helper->>Faucet: POST /faucet?balance=10000
        alt Success
            Faucet-->>Helper: NWC URL + lud16 param
            Helper->>Helper: Parse URL & extract lightning address
            Helper-->>Test: TestWallet {nwcUrl, lightningAddress}
        else Retry
            Helper->>Helper: Wait & retry
        end
    end
    
    Test->>Client: new NWCClient(nwcUrl)
    Test->>Client: getBalance()
    Client->>WS: Connect & send request
    WS-->>Client: Balance response (10,000,000 msats)
    Client-->>Test: 10,000,000 msats
    Test->>Test: Assert balance == expected
    Test->>Client: close()
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

Suggested Reviewers

  • rolznz

Poem

🐰 A faucet test hops into place,
With wallets created at breakneck pace,
Retry logic bounces with care,
NWCClient dances through the air,
E2E tests now get their fair share! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'test: add NWC faucet integration test' clearly and specifically describes the main change: adding an integration test for the NWC faucet, which aligns with the changeset that adds test files (helpers.ts, nwc-faucet.test.ts), test configuration (jest.e2e.config.ts), and test infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@DSanich
Copy link
Contributor Author

DSanich commented Feb 18, 2026

@rolznz Hi!

#533 (comment) - As we discussed here, I added one Jest test. I look forward to your review 🙏

Copy link

@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.

🧹 Nitpick comments (3)
.gitignore (1)

3-4: playwright-report entry seems unrelated to this Jest-only PR.

If Playwright isn't used anywhere in the repo, this entry is dead weight. If it's anticipating future Playwright adoption, a comment would help.

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

In @.gitignore around lines 3 - 4, The .gitignore contains an entry
"playwright-report" that appears unrelated to this Jest-only PR; either remove
the "playwright-report" line to avoid dead-weight ignores or keep it but add a
clarifying comment above it indicating it's reserved for future Playwright
artifacts (e.g., "reserved for Playwright reports if/when Playwright is
adopted"); ensure you only modify the "playwright-report" entry (leave
"test-results" as-is) and commit the change so the intent is clear to reviewers.
src/test/helpers.ts (2)

23-25: Add an AbortController signal to the fetch call.

Without a signal, the underlying TCP connection persists even after Jest's 30_000 ms timeout fires, which can delay teardown or interfere with subsequent test runs.

♻️ Proposed fix
+      const controller = new AbortController();
+      const fetchTimeout = setTimeout(() => controller.abort(), 25_000);
       const response = await fetch(`${FAUCET_URL}?balance=${balanceSats}`, {
         method: "POST",
+        signal: controller.signal,
       });
+      clearTimeout(fetchTimeout);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/helpers.ts` around lines 23 - 25, The fetch call that posts to
`${FAUCET_URL}?balance=${balanceSats}` (the const response = await fetch(...) in
helpers.ts) needs an AbortController signal so the request is cancelled on test
timeout; create an AbortController, start a timer (e.g., via setTimeout) that
calls controller.abort() after Jest's timeout threshold, pass controller.signal
into the fetch options, and clear the timer once the fetch resolves/rejects so
the TCP connection is properly torn down.

27-30: Duplicated backoff logic and an unreachable final throw.

Two separate issues in the retry loop:

  1. The delay-and-continue pattern (lines 27–30 and 43–46) is identical in both branches — extract to a shared helper or consolidate into a single finally-style check after the try/catch.
  2. The throw at line 50 is unreachable: in the last iteration the throw at line 31 is raised inside try, immediately caught at line 42, and re-thrown at line 47, so the for loop never completes normally.
♻️ Proposed refactor
 export async function createTestWallet(
   balanceSats = 10_000,
   retries = 5,
 ): Promise<TestWallet> {
+  let lastError: unknown;
   for (let i = 0; i < retries; i++) {
     try {
       const response = await fetch(`${FAUCET_URL}?balance=${balanceSats}`, {
         method: "POST",
       });
       if (!response.ok) {
         throw new Error(`Faucet request failed: ${response.status}`);
       }
       const nwcUrl = (await response.text()).trim();
       const lud16Match = nwcUrl.match(/lud16=([^&\s]+)/);
       if (!lud16Match) {
         throw new Error("No lud16 in NWC URL");
       }
       return {
         nwcUrl,
         lightningAddress: decodeURIComponent(lud16Match[1]),
       };
     } catch (error) {
+      lastError = error;
       if (i < retries - 1) {
         await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
-        continue;
       }
-      throw error;
     }
   }
-  throw new Error("Failed to create test wallet");
+  throw lastError ?? new Error("Failed to create test wallet");
 }

Also applies to: 43-46, 50-50

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

In `@src/test/helpers.ts` around lines 27 - 30, Consolidate the duplicate backoff
logic by extracting the setTimeout-based delay into a shared helper (e.g.,
sleep(ms)) and replace both duplicated await new Promise((r) => setTimeout(...))
occurrences with await sleep(2000 * (i + 1)); also fix the unreachable throw by
removing the throw that never executes and instead either rethrow the caught
error when i === retries - 1 inside the catch or capture the last error (e.g.,
lastError) and after the for loop throw lastError so that the final error is
actually propagated; update the retry loop that uses variables retries and i to
use this single-place delay and proper final throw behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.gitignore:
- Around line 3-4: The .gitignore contains an entry "playwright-report" that
appears unrelated to this Jest-only PR; either remove the "playwright-report"
line to avoid dead-weight ignores or keep it but add a clarifying comment above
it indicating it's reserved for future Playwright artifacts (e.g., "reserved for
Playwright reports if/when Playwright is adopted"); ensure you only modify the
"playwright-report" entry (leave "test-results" as-is) and commit the change so
the intent is clear to reviewers.

In `@src/test/helpers.ts`:
- Around line 23-25: The fetch call that posts to
`${FAUCET_URL}?balance=${balanceSats}` (the const response = await fetch(...) in
helpers.ts) needs an AbortController signal so the request is cancelled on test
timeout; create an AbortController, start a timer (e.g., via setTimeout) that
calls controller.abort() after Jest's timeout threshold, pass controller.signal
into the fetch options, and clear the timer once the fetch resolves/rejects so
the TCP connection is properly torn down.
- Around line 27-30: Consolidate the duplicate backoff logic by extracting the
setTimeout-based delay into a shared helper (e.g., sleep(ms)) and replace both
duplicated await new Promise((r) => setTimeout(...)) occurrences with await
sleep(2000 * (i + 1)); also fix the unreachable throw by removing the throw that
never executes and instead either rethrow the caught error when i === retries -
1 inside the catch or capture the last error (e.g., lastError) and after the for
loop throw lastError so that the final error is actually propagated; update the
retry loop that uses variables retries and i to use this single-place delay and
proper final throw behavior.

@rolznz
Copy link
Contributor

rolznz commented Feb 19, 2026

@DSanich thanks for the PR! these should somehow be differentiated from our non-E2E tests. How can we do that?

@DSanich
Copy link
Contributor Author

DSanich commented Feb 19, 2026

@rolznz Hello! Glad to help. My suggestion is to rename the script and create a separate directory for e2e tests. I did this in the commit 2cc0e44. Look at this please 🙏

Copy link

@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

🧹 Nitpick comments (2)
e2e/helpers.ts (1)

23-25: No timeout on fetch — a stalled connection will hang until Jest's test-level timeout fires, blocking all retry attempts.

Add an AbortController signal so each attempt has a bounded duration:

♻️ Suggested fix
+      const controller = new AbortController();
+      const fetchTimeout = setTimeout(() => controller.abort(), 5000);
       const response = await fetch(`${FAUCET_URL}?balance=${balanceSats}`, {
         method: "POST",
+        signal: controller.signal,
       });
+      clearTimeout(fetchTimeout);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/helpers.ts` around lines 23 - 25, The fetch call that posts to
`${FAUCET_URL}?balance=${balanceSats}` has no timeout and can hang; modify the
helper so each attempt creates an AbortController, set a per-attempt timeout
(e.g., setTimeout that calls controller.abort()) and pass controller.signal into
fetch, and clear the timeout after fetch completes so stalled connections are
aborted and retries can proceed; update the code that uses the response variable
to handle AbortError appropriately.
package.json (1)

51-51: lint:js does not cover e2e/.

format and format:fix were extended to include e2e/**/*.(js|ts) (lines 54–55), but lint:js still targets only src. E2E files will be formatted but never statically analysed.

♻️ Suggested fix
-    "lint:js": "eslint src --ext .js,.ts --max-warnings 0",
+    "lint:js": "eslint src e2e --ext .js,.ts --max-warnings 0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 51, The "lint:js" npm script currently only lints src
files; update the "lint:js" script (script name lint:js) to include the e2e
directory as well (same extensions as format/format:fix) so E2E files are
statically analysed—modify the command to target both src and e2e (or use a glob
like e2e/**/*.{js,ts}) while keeping --ext .js,.ts and --max-warnings 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/helpers.ts`:
- Around line 33-48: The parse error "No lud16 in NWC URL" is being swallowed by
the surrounding retry loop because the throw inside the try is caught by the
catch that retries; update the logic in the function that reads nwcUrl (use
symbols nwcUrl, lud16Match, lightningAddress and the retries loop) so that only
transient I/O/errors trigger a retry: after reading nwcUrl and computing
lud16Match, if lud16Match is falsy immediately throw the parse error (or
rethrow) outside the retry catch path (e.g., validate and throw before the
try/retry block or detect parse errors in the catch and rethrow without
retrying) so parse/logic errors do not incur retry delays.

In `@e2e/nwc-faucet.test.ts`:
- Around line 19-23: The test currently calls nwc.getBalance() and then
nwc.close(), but if NWCClient.getBalance() rejects the nwc.close() call is
skipped and sockets remain open; wrap the call to nwc.getBalance() in a
try/finally (using the existing nwc variable and NWCClient.close()) so that
nwc.close() is invoked in the finally block regardless of success or failure,
ensuring WebSocket connections are always closed even on errors.
- Around line 14-26: The Jest test timeout (30_000) is too small for
createTestWallet's linear backoff (retries default = 5) plus network round-trips
and NWCClient.getBalance's replyTimeout; update the test to either increase the
Jest timeout (e.g., to cover worst-case backoff + replyTimeout) or call
createTestWallet with a reduced retries/ceiling (e.g., retries = 3) so the total
backoff (2+4s) fits inside 30_000; adjust the call site in the test that invokes
createTestWallet and/or the test's third-argument timeout value to implement the
chosen option and rerun the test.

---

Nitpick comments:
In `@e2e/helpers.ts`:
- Around line 23-25: The fetch call that posts to
`${FAUCET_URL}?balance=${balanceSats}` has no timeout and can hang; modify the
helper so each attempt creates an AbortController, set a per-attempt timeout
(e.g., setTimeout that calls controller.abort()) and pass controller.signal into
fetch, and clear the timeout after fetch completes so stalled connections are
aborted and retries can proceed; update the code that uses the response variable
to handle AbortError appropriately.

In `@package.json`:
- Line 51: The "lint:js" npm script currently only lints src files; update the
"lint:js" script (script name lint:js) to include the e2e directory as well
(same extensions as format/format:fix) so E2E files are statically
analysed—modify the command to target both src and e2e (or use a glob like
e2e/**/*.{js,ts}) while keeping --ext .js,.ts and --max-warnings 0.

Copy link

@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.

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

Duplicate comments:
In `@e2e/nwc-faucet.test.ts`:
- Around line 14-28: No changes needed — the test was updated to increase the
timeout to 60_000 and call createTestWallet(EXPECTED_BALANCE_SATS, 3) to handle
retries, and the NWCClient instance is properly closed in a finally block via
nwc.close() after calling nwc.getBalance(), so leave the test as-is.

@DSanich DSanich force-pushed the feat/jest-nwc-faucet-test branch from f8c77bd to 7e7f034 Compare February 20, 2026 10:53
@DSanich
Copy link
Contributor Author

DSanich commented Feb 20, 2026

@rolznz I rebased this pull request to reflect our merge #534

preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/', '/e2e/browser/'],
testPathIgnorePatterns: ['/node_modules/', '/e2e/'],
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks wrong, isn't it? @DSanich

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, I see there's a new config for e2e

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['<rootDir>/e2e/**/*.test.ts'],
Copy link
Contributor

Choose a reason for hiding this comment

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

how does this work? the browser folder is in there and that should be done by playwright?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rolznz Good catch!

The browser tests in e2e/browser are run by Playwright, not Jest. Jest was only picking up *.test.ts files, while the browser tests use *.spec.ts, so there was no overlap — but that was implicit and fragile. I’ve added e2e/browser to testPathIgnorePatterns so Jest explicitly skips that folder and the split between Jest and Playwright is clear.

Copy link

@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.

🧹 Nitpick comments (2)
e2e/helpers.ts (2)

43-48: Fragile error identity check via string comparison.

The rethrow guard on line 45 matches against a hardcoded string literal. If the error message at line 36 is ever changed, this silently breaks — parse errors would resume being retried without any test or type-checker catching it. Using a typed custom error (as was originally suggested) makes the relationship explicit and refactor-safe.

♻️ Proposed refactor — use a custom error class
+class FaucetParseError extends Error {}
+
 export async function createTestWallet(
   ...
       if (!lud16Match) {
-        throw new Error("No lud16 in NWC URL");
+        throw new FaucetParseError("No lud16 in NWC URL");
       }
       ...
     } catch (error) {
-      if (
-        error instanceof Error &&
-        error.message === "No lud16 in NWC URL"
-      ) {
-        throw error;
-      }
+      if (error instanceof FaucetParseError) throw error;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/helpers.ts` around lines 43 - 48, Replace the fragile string comparison
with a typed custom error: define a NoLud16Error class extending Error, throw
new NoLud16Error() at the site that currently creates Error("No lud16 in NWC
URL"), and change the rethrow guard to check error instanceof NoLud16Error
instead of comparing error.message; update any imports/exports so the throw site
and the check both reference the same error class (e.g., NoLud16Error) to make
the relationship refactor-safe.

23-25: Consider adding a fetch timeout to prevent indefinite hangs.

Without an AbortSignal, a stalled TCP connection will block the fetch promise forever. The only current safeguard is Jest's test-level timeout, which produces a generic "test timed out" error rather than a descriptive failure.

⏱️ Proposed fix — add a per-request timeout
-      const response = await fetch(`${FAUCET_URL}?balance=${balanceSats}`, {
-        method: "POST",
-      });
+      const response = await fetch(`${FAUCET_URL}?balance=${balanceSats}`, {
+        method: "POST",
+        signal: AbortSignal.timeout(10_000),
+      });

Note: AbortSignal.timeout() was added in Node.js 17.3.0. If the project must run on Node 14/16, use an AbortController + setTimeout pair instead.

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

In `@e2e/helpers.ts` around lines 23 - 25, The fetch to
`${FAUCET_URL}?balance=${balanceSats}` can hang indefinitely; add a per-request
abort timeout by creating an AbortController, start a timer (e.g., setTimeout)
that calls controller.abort() after a chosen ms, pass controller.signal into the
fetch options, and clear the timer after fetch resolves or rejects;
alternatively use AbortSignal.timeout(ms) if the runtime supports it. Ensure the
signal is passed into the fetch call that currently only sets method:"POST" and
handle the abort error path so tests fail with a descriptive timeout message
rather than a generic Jest timeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/helpers.ts`:
- Around line 43-48: Replace the fragile string comparison with a typed custom
error: define a NoLud16Error class extending Error, throw new NoLud16Error() at
the site that currently creates Error("No lud16 in NWC URL"), and change the
rethrow guard to check error instanceof NoLud16Error instead of comparing
error.message; update any imports/exports so the throw site and the check both
reference the same error class (e.g., NoLud16Error) to make the relationship
refactor-safe.
- Around line 23-25: The fetch to `${FAUCET_URL}?balance=${balanceSats}` can
hang indefinitely; add a per-request abort timeout by creating an
AbortController, start a timer (e.g., setTimeout) that calls controller.abort()
after a chosen ms, pass controller.signal into the fetch options, and clear the
timer after fetch resolves or rejects; alternatively use AbortSignal.timeout(ms)
if the runtime supports it. Ensure the signal is passed into the fetch call that
currently only sets method:"POST" and handle the abort error path so tests fail
with a descriptive timeout message rather than a generic Jest timeout.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8c77bd and 7070630.

📒 Files selected for processing (5)
  • e2e/helpers.ts
  • e2e/nwc-faucet.test.ts
  • jest.config.ts
  • jest.e2e.config.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • jest.e2e.config.ts
  • e2e/nwc-faucet.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants